From 04d965d67a7264a1c7c211494aebda1953df7603 Mon Sep 17 00:00:00 2001 From: "info@mode42.com" Date: Fri, 7 Aug 2026 18:25:13 +0000 Subject: Initial push --- stacks/tncs/tnc2c-host-reset.py | 229 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 229 insertions(+) create mode 100755 stacks/tncs/tnc2c-host-reset.py (limited to 'stacks/tncs/tnc2c-host-reset.py') diff --git a/stacks/tncs/tnc2c-host-reset.py b/stacks/tncs/tnc2c-host-reset.py new file mode 100755 index 0000000..340642f --- /dev/null +++ b/stacks/tncs/tnc2c-host-reset.py @@ -0,0 +1,229 @@ +#!/usr/bin/env python3 +""" +tnc2c-host-reset - recover TNC-2 class terminal/host mode over serial. + +Uses full software recovery ladder (KISS-return, JHOST0, RESTART) — no power +cycle required unless DTR was low at cold boot. + +Usage: + ./tnc2c-host-reset.sh + ./tnc2c-host-reset.sh /dev/ttyS4 + ./tnc2c-host-reset.sh --kiss # recover + enter KISS + ./tnc2c-host-reset.sh --power-hint # remind to cycle TNC power first + ./tnc2c-host-reset.sh --no-kiss # skip KISS reset frame (step 2 only) +""" + +from __future__ import annotations + +import argparse +import importlib.util +import os +import sys +import time + +BAUD_DEFAULT = 19200 +LINE_DEFAULT = "8n1" + +FIRMWARE_MARKERS = ( + b"TheFirmware", + b"NORD", + b"Version 2.7", + b"Checksum", + b"Copyright", + b"DAMA", + b"SMACK", +) + + +def load_recovery(): + root = os.path.dirname(os.path.abspath(__file__)) + path = os.path.join(root, "tnc_serial_recovery.py") + spec = importlib.util.spec_from_file_location("tnc_serial_recovery", path) + if spec is None or spec.loader is None: + raise RuntimeError(f"cannot load {path}") + mod = importlib.util.module_from_spec(spec) + spec.loader.exec_module(mod) + return mod + + +def load_env(path: str) -> dict[str, str]: + out: dict[str, str] = {} + if not os.path.isfile(path): + return out + with open(path, encoding="utf-8") as f: + for line in f: + line = line.strip() + if not line or line.startswith("#") or "=" not in line: + continue + k, v = line.split("=", 1) + out[k.strip()] = v.strip() + return out + + +def parse_line(line: str) -> tuple[int, int]: + import termios + + line = line.lower() + if line == "7e1": + return termios.CS7, termios.PARENB + if line == "8n1": + return termios.CS8, 0 + raise ValueError(f"unsupported serial line: {line}") + + +def parse_baud(baud: int) -> int: + import termios + + table = { + 1200: termios.B1200, + 2400: termios.B2400, + 4800: termios.B4800, + 9600: termios.B9600, + 19200: termios.B19200, + } + if baud not in table: + raise ValueError(f"unsupported baud: {baud}") + return table[baud] + + +def open_serial(dev: str, baud: int, line: str) -> int: + import fcntl + import struct + import termios + + speed = parse_baud(baud) + databits, parity = parse_line(line) + fd = os.open(dev, os.O_RDWR | os.O_NOCTTY | os.O_NONBLOCK) + t = termios.tcgetattr(fd) + t[0] = t[1] = 0 + t[2] = termios.CLOCAL | termios.CREAD | databits | parity + t[3] = t[4] = t[5] = speed + t[6][termios.VMIN] = 0 + t[6][termios.VTIME] = 5 + termios.tcsetattr(fd, termios.TCSANOW, t) + termios.tcflush(fd, termios.TCIOFLUSH) + flags = struct.unpack("I", fcntl.ioctl(fd, 0x5415, struct.pack("I", 0)))[0] + flags |= 0x004 | 0x002 + fcntl.ioctl(fd, 0x5416, struct.pack("I", flags)) + return fd + + +def read_for(fd: int, seconds: float) -> bytes: + end = time.time() + seconds + chunks: list[bytes] = [] + while time.time() < end: + try: + b = os.read(fd, 4096) + if b: + chunks.append(b) + except BlockingIOError: + time.sleep(0.02) + return b"".join(chunks) + + +def write_flush(fd: int, data: bytes) -> None: + import termios + + os.write(fd, data) + termios.tcdrain(fd) + + +POWER_HINT = """\ +Note: If recovery fails (echo only), disconnect TNC power (10-15 s), reconnect +while DTR+RTS stay high — run tnc2c-boot-wait.sh or this script immediately. +""" + + +def main() -> int: + parser = argparse.ArgumentParser( + description="TNC host-mode recovery with DTR+RTS (TheFirmware ladder)" + ) + parser.add_argument("device", nargs="?", default=None) + parser.add_argument("--baud", type=int, default=None) + parser.add_argument("--line", default=None, choices=("8n1", "7e1")) + parser.add_argument("--kiss", action="store_true", help="enter KISS after recover") + parser.add_argument( + "--kiss-entry", + default="kiss_on", + choices=("kiss_on", "auto"), + help="KISS entry method with --kiss", + ) + parser.add_argument("--power-hint", action="store_true") + parser.add_argument( + "--no-kiss", + action="store_true", + help="skip KISS return frame in recovery ladder", + ) + args = parser.parse_args() + + root = os.path.dirname(os.path.abspath(__file__)) + env = load_env(os.path.join(root, "tnc2c-serial.env")) + dev = ( + args.device + or env.get("TNC_DEV") + or env.get("TNC2C_DEV") + or env.get("PKTNC2_DEV") + or "/dev/ttyS4" + ) + baud = args.baud or int( + env.get("TNC_BAUD") or env.get("TNC2C_BAUD") or env.get("PKTNC2_BAUD") or BAUD_DEFAULT + ) + line = args.line or env.get("TNC_LINE") or env.get("TNC2C_LINE") or LINE_DEFAULT + + if args.power_hint: + print(POWER_HINT) + + if not os.path.exists(dev): + print(f"FAIL: {dev} does not exist", file=sys.stderr) + return 2 + if not os.access(dev, os.R_OK | os.W_OK): + print(f"FAIL: no access to {dev}", file=sys.stderr) + return 2 + + recovery = load_recovery() + + print(f"TNC host-reset @ {dev} {baud} {line.upper()} (DTR+RTS high)") + try: + fd = open_serial(dev, baud, line) + except OSError as e: + print(f"FAIL: {dev}: {e}", file=sys.stderr) + return 2 + + def wf(data: bytes) -> None: + write_flush(fd, data) + + def rf(seconds: float) -> bytes: + return read_for(fd, seconds) + + def log(msg: str) -> None: + print(f" {msg}") + + ok, received = recovery.recover_terminal( + wf, rf, skip_kiss_frame=args.no_kiss, log=log + ) + + if ok and args.kiss: + recovery.enter_kiss(wf, rf, args.kiss_entry) + print(" KISS entry sent") + + os.close(fd) + + print() + if ok: + label = "HOST+KISS" if args.kiss else "HOST" + print(f"OK: {label} - terminal mode active (software recovery)") + if received: + text = received.decode("ascii", errors="replace") + if "NORD" in text or "TheFirmware" in text: + for line_out in text.splitlines(): + if any(m.decode() in line_out for m in FIRMWARE_MARKERS[:5]): + print(f" | {line_out.strip()}") + return 0 + + print("FAIL: software recovery exhausted") + print(" -> Power-cycle with DTR high: ./tnc2c-boot-wait.sh") + return 1 + + +if __name__ == "__main__": + sys.exit(main()) -- cgit v1.3.1