Skip to content

Commit 004c09d

Browse files
widgetiiclaude
andauthored
agent CLI: accept tcp:// / rfc2217:// / socket:// (route via platform factory) (#87)
## Summary The six `defib agent ...` commands hardcoded `SerialTransport.create(port)`, so `-p tcp://<host>:<port>` errored with *\"No such file or directory: 'tcp://...'\"*, even though the underlying COBS protocol works fine over `SocketTransport` (now that #86 fixed the multi-packet recv-loss). Drop-in switch to `create_transport(normalize_port_name(port))` — the same factory `burn` / `install` / `restore` already use. Help strings on every command updated to mention the supported URL schemes globally. Affected: `agent upload`, `agent flash`, `agent info`, `agent read`, `agent write`, `agent scan`. ## Verification on rack pod at `10.216.128.69` ``` $ defib agent info -p tcp://10.216.128.69:9000 JEDEC ID: ef4018 Flash size: 16384 KB RAM base: 0x40000000 Sector size: 64 KB Agent ver: 2 Capabilities: flash_stream, sector_bitmap, page_skip, set_baud, reboot, selfupdate, scan ``` ``` $ defib agent read -p tcp://10.216.128.69:9000 -a 0x14050000 -s 256 -o uimage_dump.bin 256/256 (100%) 256 bytes in 0.0s CRC32: OK $ xxd uimage_dump.bin | head -2 00000000: 2705 1956 e28a ab68 ... '..V...hj... 00000020: 4c69 6e75 782d 342e ... Linux-4.9.37-hi3516ev300 ``` ``` $ defib agent scan -p tcp://10.216.128.69:9000 Scanned: 256/256 (29.6 s) Good: 256 Flash is healthy! ``` The scan command in particular is a stress test of the multi-packet bridge path — every sector returns its own `RSP_SCAN` packet plus the final `RSP_ACK`. Streams cleanly thanks to #86's leftover-buffer fix. ## Test plan - [ ] `uv run pytest tests/ -x -v --ignore=tests/fuzz` — 457 passed / 2 skipped - [ ] `uv run ruff check src/defib/cli/app.py` - [ ] `uv run mypy src/defib/cli/app.py --ignore-missing-imports` - [ ] Regression: confirm the same 6 commands still work against a local `/dev/ttyUSB*` (they go through the same `create_transport` factory, so should be unchanged). 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-authored-by: Dmitry Ilyin <widgetii@users.noreply.github.com> Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent 3ad3721 commit 004c09d

1 file changed

Lines changed: 38 additions & 26 deletions

File tree

src/defib/cli/app.py

Lines changed: 38 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@
1515
def burn(
1616
chip: str = typer.Option(..., "-c", "--chip", help="Chip model name"),
1717
file: str = typer.Option("", "-f", "--file", help="Firmware file (auto-downloads from OpenIPC if omitted)"),
18-
port: str = typer.Option("/dev/ttyUSB0", "-p", "--port", help="Serial port"),
18+
port: str = typer.Option("/dev/ttyUSB0", "-p", "--port", help="Serial device (/dev/ttyUSB0), tcp://host:port, rfc2217://host:port, or socket:///path"),
1919
send_break: bool = typer.Option(False, "-b", "--break", help="Send Ctrl-C after upload"),
2020
terminal: bool = typer.Option(False, "-t", "--terminal", help="Open serial terminal after upload"),
2121
power_cycle: bool = typer.Option(False, "--power-cycle", help="Auto power-cycle via PoE (needs DEFIB_POE_* env vars)"),
@@ -441,7 +441,7 @@ def ports(
441441

442442
@app.command()
443443
def detect(
444-
port: str = typer.Option("/dev/ttyUSB0", "-p", "--port", help="Serial port"),
444+
port: str = typer.Option("/dev/ttyUSB0", "-p", "--port", help="Serial device (/dev/ttyUSB0), tcp://host:port, rfc2217://host:port, or socket:///path"),
445445
output: str = typer.Option("human", "--output", help="Output mode: human, json"),
446446
timeout: float = typer.Option(25.0, "--timeout", help="Detection timeout in seconds"),
447447
) -> None:
@@ -508,7 +508,7 @@ async def _detect_async(port: str, output: str, timeout: float) -> None:
508508

509509
@app.command()
510510
def capture(
511-
port: str = typer.Option(..., "-p", "--port", help="Serial port"),
511+
port: str = typer.Option(..., "-p", "--port", help="Serial device (/dev/ttyUSB0), tcp://host:port, rfc2217://host:port, or socket:///path"),
512512
output_file: str = typer.Option(..., "-o", "--output", help="Output .dcap file"),
513513
chip: str = typer.Option("", "-c", "--chip", help="Chip name (metadata only)"),
514514
duration: float = typer.Option(60.0, "--duration", help="Max capture duration in seconds"),
@@ -629,7 +629,7 @@ def replay(
629629

630630
@app.command("dump-flash")
631631
def dump_flash_cmd(
632-
port: str = typer.Option("/dev/ttyUSB0", "-p", "--port", help="Serial port"),
632+
port: str = typer.Option("/dev/ttyUSB0", "-p", "--port", help="Serial device (/dev/ttyUSB0), tcp://host:port, rfc2217://host:port, or socket:///path"),
633633
output_file: str = typer.Option("flash_dump.bin", "-o", "--output", help="Output binary file"),
634634
size: str = typer.Option("", "--size", help="Flash size (e.g., 8MB, 16MB) — auto-detect if empty"),
635635
output: str = typer.Option("human", "--output-mode", help="Output mode: human, json"),
@@ -903,7 +903,7 @@ def list_interfaces_cmd(
903903
@agent_app.command("upload")
904904
def agent_upload(
905905
chip: str = typer.Option(..., "-c", "--chip", help="Chip model name"),
906-
port: str = typer.Option("/dev/ttyUSB0", "-p", "--port", help="Serial port"),
906+
port: str = typer.Option("/dev/ttyUSB0", "-p", "--port", help="Serial device (/dev/ttyUSB0), tcp://host:port, rfc2217://host:port, or socket:///path"),
907907
output: str = typer.Option("human", "--output", help="Output mode: human, json"),
908908
) -> None:
909909
"""Upload flash agent to device via boot protocol (requires power-cycle)."""
@@ -921,7 +921,9 @@ async def _agent_upload_async(chip: str, port: str, output: str) -> None:
921921
from defib.profiles.loader import load_profile
922922
from defib.protocol.hisilicon_standard import HiSiliconStandard
923923
from defib.recovery.events import ProgressEvent
924-
from defib.transport.serial import SerialTransport
924+
from defib.transport.serial_platform import (
925+
create_transport, normalize_port_name,
926+
)
925927

926928
console = Console()
927929

@@ -957,7 +959,7 @@ async def _agent_upload_async(chip: str, port: str, output: str) -> None:
957959
console.print(f"SPL: full U-Boot ({len(spl_data)} bytes — boundary auto-detected)")
958960
console.print("\n[yellow]Power-cycle the camera now![/yellow]\n")
959961

960-
transport = await SerialTransport.create(port)
962+
transport = await create_transport(normalize_port_name(port))
961963
protocol = HiSiliconStandard()
962964
protocol.set_profile(profile)
963965

@@ -996,7 +998,7 @@ def on_progress(e: ProgressEvent) -> None:
996998
import asyncio as aio
997999
await transport.close()
9981000
await aio.sleep(2)
999-
transport = await SerialTransport.create(port)
1001+
transport = await create_transport(normalize_port_name(port))
10001002

10011003
client = FlashAgentClient(transport, chip)
10021004
if await client.connect(timeout=10.0):
@@ -1021,7 +1023,7 @@ def on_progress(e: ProgressEvent) -> None:
10211023
def agent_flash(
10221024
chip: str = typer.Option(..., "-c", "--chip", help="Chip model name"),
10231025
input_file: str = typer.Option(..., "-i", "--input", help="Firmware binary file"),
1024-
port: str = typer.Option("/dev/ttyUSB0", "-p", "--port", help="Serial port"),
1026+
port: str = typer.Option("/dev/ttyUSB0", "-p", "--port", help="Serial device (/dev/ttyUSB0), tcp://host:port, rfc2217://host:port, or socket:///path"),
10251027
verify: bool = typer.Option(True, "--verify/--no-verify", help="CRC32 verify after write"),
10261028
reboot: bool = typer.Option(True, "--reboot/--no-reboot", help="Reboot after flash"),
10271029
output: str = typer.Option("human", "--output", help="Output mode: human, json"),
@@ -1052,7 +1054,9 @@ async def _agent_flash_async(
10521054
from defib.profiles.loader import load_profile
10531055
from defib.protocol.hisilicon_standard import HiSiliconStandard
10541056
from defib.recovery.events import ProgressEvent
1055-
from defib.transport.serial import SerialTransport
1057+
from defib.transport.serial_platform import (
1058+
create_transport, normalize_port_name,
1059+
)
10561060

10571061
console = Console()
10581062
FLASH_MEM = 0x14000000
@@ -1101,7 +1105,7 @@ async def _agent_flash_async(
11011105
console.print("\n[yellow]Power-cycle the camera now![/yellow]\n")
11021106

11031107
# --- Phase 1: Upload agent via boot protocol ---
1104-
transport = await SerialTransport.create(port)
1108+
transport = await create_transport(normalize_port_name(port))
11051109
protocol = HiSiliconStandard()
11061110
protocol.set_profile(profile)
11071111

@@ -1139,7 +1143,7 @@ def on_boot_progress(e: ProgressEvent) -> None:
11391143
import asyncio as aio
11401144
await transport.close()
11411145
await aio.sleep(2)
1142-
transport = await SerialTransport.create(port)
1146+
transport = await create_transport(normalize_port_name(port))
11431147

11441148
client = FlashAgentClient(transport, chip)
11451149
if not await client.connect(timeout=10.0):
@@ -1242,7 +1246,7 @@ def on_flash_progress(done: int, total: int) -> None:
12421246

12431247
@agent_app.command("info")
12441248
def agent_info(
1245-
port: str = typer.Option("/dev/ttyUSB0", "-p", "--port", help="Serial port"),
1249+
port: str = typer.Option("/dev/ttyUSB0", "-p", "--port", help="Serial device (/dev/ttyUSB0), tcp://host:port, rfc2217://host:port, or socket:///path"),
12461250
output: str = typer.Option("human", "--output", help="Output mode: human, json"),
12471251
) -> None:
12481252
"""Query info from a running flash agent."""
@@ -1256,10 +1260,12 @@ async def _agent_info_async(port: str, output: str) -> None:
12561260
from rich.console import Console
12571261

12581262
from defib.agent.client import FlashAgentClient
1259-
from defib.transport.serial import SerialTransport
1263+
from defib.transport.serial_platform import (
1264+
create_transport, normalize_port_name,
1265+
)
12601266

12611267
console = Console()
1262-
transport = await SerialTransport.create(port)
1268+
transport = await create_transport(normalize_port_name(port))
12631269
client = FlashAgentClient(transport)
12641270

12651271
if not await client.connect(timeout=5.0):
@@ -1299,7 +1305,7 @@ async def _agent_info_async(port: str, output: str) -> None:
12991305

13001306
@agent_app.command("read")
13011307
def agent_read(
1302-
port: str = typer.Option("/dev/ttyUSB0", "-p", "--port", help="Serial port"),
1308+
port: str = typer.Option("/dev/ttyUSB0", "-p", "--port", help="Serial device (/dev/ttyUSB0), tcp://host:port, rfc2217://host:port, or socket:///path"),
13031309
addr: str = typer.Option(None, "-a", "--addr", help="Start address (hex, default: flash base 0x14000000)"),
13041310
size: str = typer.Option(None, "-s", "--size", help="Size in bytes (or 1KB, 16MB, etc; default: auto-detect)"),
13051311
output_file: str = typer.Option("flash_dump.bin", "-o", "--output", help="Output binary file"),
@@ -1321,11 +1327,13 @@ async def _agent_read_async(
13211327
from rich.console import Console
13221328

13231329
from defib.agent.client import FlashAgentClient
1324-
from defib.transport.serial import SerialTransport
1330+
from defib.transport.serial_platform import (
1331+
create_transport, normalize_port_name,
1332+
)
13251333

13261334
console = Console()
13271335

1328-
transport = await SerialTransport.create(port)
1336+
transport = await create_transport(normalize_port_name(port))
13291337
client = FlashAgentClient(transport)
13301338
if not await client.connect(timeout=5.0):
13311339
console.print("[red]Agent not responding[/red]")
@@ -1379,7 +1387,7 @@ async def _agent_read_async(
13791387

13801388
@agent_app.command("write")
13811389
def agent_write(
1382-
port: str = typer.Option("/dev/ttyUSB0", "-p", "--port", help="Serial port"),
1390+
port: str = typer.Option("/dev/ttyUSB0", "-p", "--port", help="Serial device (/dev/ttyUSB0), tcp://host:port, rfc2217://host:port, or socket:///path"),
13831391
addr: str = typer.Option("0x14000000", "-a", "--addr", help="Start address (hex, default: flash base)"),
13841392
input_file: str = typer.Option(..., "-i", "--input", help="Input binary file"),
13851393
verify: bool = typer.Option(True, "--verify/--no-verify", help="CRC32 verify after write"),
@@ -1399,13 +1407,15 @@ async def _agent_write_async(
13991407
from rich.console import Console
14001408

14011409
from defib.agent.client import FlashAgentClient
1402-
from defib.transport.serial import SerialTransport
1410+
from defib.transport.serial_platform import (
1411+
create_transport, normalize_port_name,
1412+
)
14031413

14041414
console = Console()
14051415
address = int(addr_str, 0)
14061416
data = open(input_file, "rb").read()
14071417

1408-
transport = await SerialTransport.create(port)
1418+
transport = await create_transport(normalize_port_name(port))
14091419
client = FlashAgentClient(transport)
14101420
if not await client.connect(timeout=5.0):
14111421
console.print("[red]Agent not responding[/red]")
@@ -1449,7 +1459,7 @@ async def _agent_write_async(
14491459

14501460
@agent_app.command("scan")
14511461
def agent_scan(
1452-
port: str = typer.Option("/dev/ttyUSB0", "-p", "--port", help="Serial port"),
1462+
port: str = typer.Option("/dev/ttyUSB0", "-p", "--port", help="Serial device (/dev/ttyUSB0), tcp://host:port, rfc2217://host:port, or socket:///path"),
14531463
output_file: str = typer.Option("", "-o", "--output", help="Save recoverable data to file (bad sectors filled with 0xFF)"),
14541464
output: str = typer.Option("human", "--output-mode", help="Output mode: human, json"),
14551465
) -> None:
@@ -1472,10 +1482,12 @@ async def _agent_scan_async(port: str, output_file: str, output: str) -> None:
14721482
SectorResult,
14731483
SectorStatus,
14741484
)
1475-
from defib.transport.serial import SerialTransport
1485+
from defib.transport.serial_platform import (
1486+
create_transport, normalize_port_name,
1487+
)
14761488

14771489
console = Console()
1478-
transport = await SerialTransport.create(port)
1490+
transport = await create_transport(normalize_port_name(port))
14791491
client = FlashAgentClient(transport)
14801492

14811493
if not await client.connect(timeout=5.0):
@@ -1658,7 +1670,7 @@ def _parse_size(s: str) -> int:
16581670
def install(
16591671
chip: str = typer.Option(..., "-c", "--chip", help="Chip model name"),
16601672
firmware: str = typer.Option(..., "--firmware", help="OpenIPC firmware tarball (.tgz)"),
1661-
port: str = typer.Option("/dev/ttyUSB0", "-p", "--port", help="Serial port"),
1673+
port: str = typer.Option("/dev/ttyUSB0", "-p", "--port", help="Serial device (/dev/ttyUSB0), tcp://host:port, rfc2217://host:port, or socket:///path"),
16621674
power_cycle: bool = typer.Option(False, "--power-cycle", help="Auto power-cycle via PoE"),
16631675
poe_port_override: str = typer.Option("", "--poe-port", help="Explicit MikroTik ether port (e.g. ether3) — overrides comment-based auto-discovery. Requires --power-cycle."),
16641676
nic: str = typer.Option("", "--nic", help="Network interface for TFTP (auto-detect if empty)"),
@@ -2339,7 +2351,7 @@ async def tftp_and_flash(
23392351
def restore(
23402352
chip: str = typer.Option(..., "-c", "--chip", help="Chip model name"),
23412353
dump: str = typer.Option(..., "-i", "--input", help="Flash dump file or directory of mtdN files"),
2342-
port: str = typer.Option("/dev/ttyUSB0", "-p", "--port", help="Serial port"),
2354+
port: str = typer.Option("/dev/ttyUSB0", "-p", "--port", help="Serial device (/dev/ttyUSB0), tcp://host:port, rfc2217://host:port, or socket:///path"),
23432355
uboot: str = typer.Option("", "--uboot", help="U-Boot binary to load (auto-downloads if omitted)"),
23442356
flash_type: str = typer.Option("auto", "--flash-type", help="Flash type: auto, nor, nand, emmc"),
23452357
mtdparts: str = typer.Option("", "--mtdparts", help="NAND partition layout (e.g. hinand:1M(boot),4M(kernel),8M(rootfs),...)"),

0 commit comments

Comments
 (0)