From ab4a47f58df01450e3dc1cba0e69efb313503f41 Mon Sep 17 00:00:00 2001 From: Dmitry Ilyin Date: Mon, 20 Apr 2026 19:30:46 +0300 Subject: [PATCH] Add -t/--terminal flag to stream serial output after burn MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit After firmware upload completes, keeps the serial port open and streams raw output until Ctrl-C. This lets you see U-Boot boot messages immediately without losing data between burn and connecting a separate terminal. Usage: defib burn -c hi3516ev300 -p /dev/ttyUSB0 -f u-boot.bin -t Tested on hi3516av200 hardware — vendor U-Boot boot log visible immediately after transfer. Co-Authored-By: Claude Opus 4.6 (1M context) --- README.md | 3 +++ src/defib/cli/app.py | 39 ++++++++++++++++++++++++++++++++++++--- 2 files changed, 39 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index dfc6dae..48ac134 100644 --- a/README.md +++ b/README.md @@ -91,6 +91,9 @@ export DEFIB_POE_USER=admin export DEFIB_POE_PASS= defib burn -c hi3516ev300 -p /dev/uart-IVG85HG50PYA-S --power-cycle -b + +# Burn and open serial terminal to see boot output (Ctrl-C to exit) +defib burn -c hi3516ev300 -p /dev/uart-IVG85HG50PYA-S --power-cycle -t ``` The `--power-cycle` flag connects to the RouterOS API, auto-discovers the PoE diff --git a/src/defib/cli/app.py b/src/defib/cli/app.py index bb5db76..c1d75bd 100644 --- a/src/defib/cli/app.py +++ b/src/defib/cli/app.py @@ -17,6 +17,7 @@ def burn( file: str = typer.Option("", "-f", "--file", help="Firmware file (auto-downloads from OpenIPC if omitted)"), port: str = typer.Option("/dev/ttyUSB0", "-p", "--port", help="Serial port"), send_break: bool = typer.Option(False, "-b", "--break", help="Send Ctrl-C after upload"), + terminal: bool = typer.Option(False, "-t", "--terminal", help="Open serial terminal after upload"), power_cycle: bool = typer.Option(False, "--power-cycle", help="Auto power-cycle via PoE (needs DEFIB_POE_* env vars)"), output: str = typer.Option("human", "--output", help="Output mode: human, json, quiet"), debug: bool = typer.Option(False, "-d", "--debug", help="Enable debug logging"), @@ -27,11 +28,12 @@ def burn( the appropriate U-Boot from OpenIPC releases. """ import asyncio - asyncio.run(_burn_async(chip, file, port, send_break, power_cycle, output, debug)) + asyncio.run(_burn_async(chip, file, port, send_break, terminal, power_cycle, output, debug)) async def _burn_async( - chip: str, file: str, port: str, send_break: bool, power_cycle: bool, output: str, debug: bool + chip: str, file: str, port: str, send_break: bool, terminal: bool, + power_cycle: bool, output: str, debug: bool, ) -> None: import json as json_mod import logging @@ -218,7 +220,6 @@ def on_log(event: LogEvent) -> None: finally: if progress_ctx is not None: progress_ctx.stop() - await transport.close() if power_controller: await power_controller.close() @@ -236,8 +237,40 @@ def on_log(event: LogEvent) -> None: console.print(f"\n[red bold]Failed:[/red bold] {result.error}") if not result.success: + await transport.close() raise typer.Exit(1) + # Terminal mode: stream serial output until Ctrl-C + if terminal and result.success: + import signal + import sys as _sys + + if output == "human": + console.print("[dim]--- Terminal mode (Ctrl-C to exit) ---[/dim]") + + stop = False + + def on_sigint(*_: object) -> None: + nonlocal stop + stop = True + + signal.signal(signal.SIGINT, on_sigint) + + try: + while not stop: + try: + data = await transport.read(256, timeout=0.1) + _sys.stdout.buffer.write(data) + _sys.stdout.buffer.flush() + except Exception: + pass + finally: + signal.signal(signal.SIGINT, signal.SIG_DFL) + if output == "human": + console.print("\n[dim]--- Terminal closed ---[/dim]") + + await transport.close() + @app.command("list-chips") def list_chips_cmd(