summaryrefslogtreecommitdiff
path: root/stacks/tncs/tnc2c-host-reset.py
diff options
context:
space:
mode:
Diffstat (limited to 'stacks/tncs/tnc2c-host-reset.py')
-rwxr-xr-xstacks/tncs/tnc2c-host-reset.py229
1 files changed, 229 insertions, 0 deletions
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())
git clone -b <branch> https://cgit.mode42.com/<repo>.git
git clone -b <branch> git://cgit.mode42.com/<repo>.git

info@mode42.com