ra8-firmware 0.1.0
Bare-metal firmware for the Renesas RA8 family (RA8D2 / RA8P1)
Loading...
Searching...
No Matches
uart_write.py
Go to the documentation of this file.
1#!/usr/bin/env python3
2# SPDX-License-Identifier: MIT
3# Copyright (c) 2026 Brighton Sikarskie
4"""Write one bounded runtime-provisioning packet to an already owned UART."""
5
6from __future__ import annotations
7
8import argparse
9import os
10import sys
11import termios
12from pathlib import Path
13
14MAX_PACKET_BYTES = 1225
15DEFAULT_BAUD = 115200
16PACKET_PREFIX = b"RA8NET1:"
17PACKET_FIELD_COUNT = 3
18HEX_DIGITS = frozenset(b"0123456789abcdef")
19
20
21def validate_packet(packet: bytes) -> None:
22 """Reject malformed or oversized input before opening the serial device."""
23 if not packet or len(packet) > MAX_PACKET_BYTES:
24 message = "runtime packet is empty or exceeds its protocol bound"
25 raise ValueError(message)
26 if not packet.startswith(PACKET_PREFIX) or not packet.endswith(b"\n"):
27 message = "runtime packet has an invalid frame"
28 raise ValueError(message)
29 body = packet[len(PACKET_PREFIX) : -1]
30 fields = body.split(b":")
31 invalid_hex = any(any(byte not in HEX_DIGITS for byte in field) for field in fields)
32 if len(fields) != PACKET_FIELD_COUNT or invalid_hex:
33 message = "runtime packet fields must be lowercase hexadecimal"
34 raise ValueError(message)
35
36
37def termios_baud(baud: int) -> int:
38 """Return the platform termios constant for one supported positive baud."""
39 if baud <= 0:
40 message = "baud must be a positive supported integer"
41 raise ValueError(message)
42 constant = getattr(termios, f"B{baud}", None)
43 if constant is None:
44 message = f"unsupported baud rate: {baud}"
45 raise ValueError(message)
46 return int(constant)
47
48
49def argparse_baud(raw: str) -> int:
50 """Parse and validate a baud value before any serial device is opened."""
51 try:
52 baud = int(raw, 10)
53 termios_baud(baud)
54 except ValueError as exc:
55 raise argparse.ArgumentTypeError(str(exc)) from exc
56 return baud
57
58
59def write_packet(port: Path, packet: bytes, baud: int) -> None:
60 """Configure the requested baud at 8N1 raw mode and write the packet."""
61 validate_packet(packet)
62 baud_flag = termios_baud(baud)
63 fd = os.open(port, os.O_WRONLY | os.O_NOCTTY)
64 try:
65 attrs = termios.tcgetattr(fd)
66 attrs[0] = 0
67 attrs[1] = 0
68 attrs[2] = (attrs[2] & ~termios.CSIZE) | termios.CS8 | termios.CLOCAL | termios.CREAD
69 attrs[2] &= ~(termios.PARENB | termios.CSTOPB)
70 attrs[3] = 0
71 attrs[4] = baud_flag
72 attrs[5] = baud_flag
73 termios.tcsetattr(fd, termios.TCSANOW, attrs)
74 offset = 0
75 while offset < len(packet):
76 written = os.write(fd, packet[offset:])
77 if written <= 0:
78 message = "serial write made no progress"
79 raise OSError(message)
80 offset += written
81 termios.tcdrain(fd)
82 finally:
83 os.close(fd)
84
85
86def _packet_is_valid(packet: bytes) -> bool:
87 """Return whether a packet passes validation."""
88 try:
89 validate_packet(packet)
90 except ValueError:
91 return False
92 return True
93
94
95def run_selftest() -> int:
96 """Prove valid framing stays quiet and malformed frames fire."""
97 exact_maximum = PACKET_PREFIX + (b"a" * 64) + b":" + (b"b" * 128) + b":" + (b"c" * 1022) + b"\n"
98 valid = (
99 b"RA8NET1:61:6262626262626262:\n",
100 b"RA8NET1:61:6262626262626262:6874747073\n",
101 exact_maximum,
102 )
103 invalid = (
104 b"",
105 b"RA8NET1:zz:62:\n",
106 b"RA8NET1:61:62\n",
107 b"OTHER:61:62:\n",
108 exact_maximum[:-1] + b"cc\n",
109 )
110 failures = []
111 failures.extend("valid packet was rejected" for packet in valid if not _packet_is_valid(packet))
112 failures.extend("invalid packet was accepted" for packet in invalid if _packet_is_valid(packet))
113 if len(exact_maximum) != MAX_PACKET_BYTES:
114 failures.append("maximum packet fixture does not match the C protocol bound")
115 try:
116 if argparse_baud(str(DEFAULT_BAUD)) != DEFAULT_BAUD:
117 failures.append("default baud changed during validation")
118 except argparse.ArgumentTypeError:
119 failures.append("default baud is unsupported on this platform")
120 for invalid_baud in ("0", "-1", "115201", "1.5", "invalid"):
121 try:
122 argparse_baud(invalid_baud)
123 except argparse.ArgumentTypeError:
124 continue
125 failures.append(f"invalid or unsupported baud was accepted: {invalid_baud}")
126 if failures:
127 for failure in failures:
128 print(f"uart_write.py --selftest: FAIL: {failure}", file=sys.stderr)
129 return 1
130 print("uart_write.py --selftest: PASS (3 frames accepted, 5 frames + 5 bauds refused)")
131 return 0
132
133
134def main() -> int:
135 """Parse arguments, read one packet from stdin, and write it to the UART."""
136 parser = argparse.ArgumentParser(description=__doc__)
137 parser.add_argument("port", nargs="?", type=Path)
138 parser.add_argument("--baud", type=argparse_baud, default=DEFAULT_BAUD)
139 parser.add_argument("--selftest", action="store_true")
140 args = parser.parse_args()
141 if args.selftest:
142 if args.port is not None:
143 parser.error("--selftest accepts no port")
144 return run_selftest()
145 if args.port is None:
146 parser.error("a serial port is required")
147 packet = sys.stdin.buffer.read(MAX_PACKET_BYTES + 1)
148 try:
149 write_packet(args.port, packet, args.baud)
150 except (OSError, ValueError) as exc:
151 print(f"uart_write.py: {exc}", file=sys.stderr)
152 return 2
153 return 0
154
155
156if __name__ == "__main__":
157 sys.exit(main())
void main(void)
The application entry point Reset_Handler hands control to.
Definition main.c:298