summaryrefslogtreecommitdiff
path: root/stacks/tncs/tnc2c-host-reset.py
blob: 340642f11781e14c4234f63c6052f84dd65779ac (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
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