104 lines
4.0 KiB
Python
104 lines
4.0 KiB
Python
"""Identity-recorded USB CLI for the explicitly erasable Ethernet RF bench.
|
|
|
|
No reset/erase/flash is performed. WiFi credentials are read only from a local
|
|
JSON file and never included in the printed command/response log.
|
|
"""
|
|
import argparse
|
|
import json
|
|
import os
|
|
from pathlib import Path
|
|
import time
|
|
|
|
import serial
|
|
from serial.tools import list_ports
|
|
|
|
from serial_session import (SerialCommandTimeout, assert_port_idle,
|
|
open_configured_session, read_command_reply)
|
|
|
|
|
|
def read_quiet(port, timeout=3):
|
|
until = time.monotonic() + timeout
|
|
# Wait the full response deadline for the first byte; silence at attach is
|
|
# not proof that a cooperative USB console has finished opening its session.
|
|
quiet = until
|
|
data = bytearray()
|
|
while time.monotonic() < min(until, quiet):
|
|
chunk = port.read(min(port.in_waiting or 1, 4096))
|
|
if chunk:
|
|
data.extend(chunk)
|
|
quiet = time.monotonic() + 0.35
|
|
return data.decode("utf-8", errors="replace")
|
|
|
|
|
|
def main():
|
|
parser = argparse.ArgumentParser(description=__doc__)
|
|
parser.add_argument("--port", required=True)
|
|
parser.add_argument("--command", action="append", default=[])
|
|
parser.add_argument("--wifi-json", type=Path)
|
|
parser.add_argument("--wait", type=float, default=10)
|
|
args = parser.parse_args()
|
|
matches = [p for p in list_ports.comports()
|
|
if os.path.realpath(p.device) == os.path.realpath(args.port)]
|
|
if len(matches) != 1:
|
|
raise SystemExit("Selected USB identity is absent or ambiguous")
|
|
info = matches[0]
|
|
secrets = []
|
|
commands = list(args.command)
|
|
if args.wifi_json:
|
|
credentials = json.loads(args.wifi_json.read_text())
|
|
ssid, password = credentials["ssid"], credentials["password"]
|
|
if not isinstance(ssid, str) or not isinstance(password, str) or any(
|
|
c in ssid + password for c in "\r\n\0"):
|
|
raise SystemExit("Invalid credential file")
|
|
secrets = [password] if password else []
|
|
commands.extend(["set wifi.ssid " + ssid, "set wifi.pwd " + password, "set wifi on"])
|
|
if args.wait <= 0 or any(not command or any(value in command for value in "\r\n\0") for command in commands):
|
|
raise SystemExit("Use single-line nonempty commands and a positive --wait")
|
|
|
|
def redact(text):
|
|
for secret in secrets:
|
|
text = text.replace(secret, "[redacted]")
|
|
return text
|
|
|
|
port = serial.Serial(port=None, baudrate=115200, timeout=0.04, exclusive=True)
|
|
port.port = info.device
|
|
port.rts = False
|
|
port.dtr = info.vid in (0x303A, 0x2886, 0x239A)
|
|
output = {"port": info.device, "usb_serial": info.serial_number,
|
|
"vid": info.vid, "pid": info.pid, "commands": []}
|
|
try:
|
|
assert_port_idle(info.device)
|
|
open_configured_session(port)
|
|
read_quiet(port, 0.5)
|
|
port.write(b"+++MESHCORE-TERM-START\n")
|
|
port.flush()
|
|
output["hello"] = redact(read_quiet(port))
|
|
for command in commands:
|
|
port.write((command + "\n").encode())
|
|
port.flush()
|
|
try:
|
|
reply = read_command_reply(port, command, args.wait)
|
|
except SerialCommandTimeout as error:
|
|
output["commands"].append({"command": redact(command),
|
|
"reply": redact(error.partial_reply), "completed": False})
|
|
output["error"] = str(error)
|
|
break # Never misattribute this pending reply to another command.
|
|
output["commands"].append({"command": redact(command), "reply": redact(reply), "completed": True})
|
|
finally:
|
|
if port.is_open:
|
|
try:
|
|
port.write(b"+++MESHCORE-TERM-STOP\n")
|
|
port.flush()
|
|
except Exception:
|
|
pass
|
|
try:
|
|
port.close()
|
|
except Exception:
|
|
pass
|
|
print(json.dumps(output), flush=True)
|
|
return 1 if "error" in output else 0
|
|
|
|
|
|
if __name__ == "__main__":
|
|
raise SystemExit(main())
|