fix: improve field diagnostics and mirror logs to USB CDC
This commit is contained in:
@@ -0,0 +1,127 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Read a USB CDC tty on macOS using only Python's standard library."""
|
||||
import argparse
|
||||
import codecs
|
||||
from datetime import datetime
|
||||
import glob
|
||||
import os
|
||||
from pathlib import Path
|
||||
import select
|
||||
import sys
|
||||
import termios
|
||||
import time
|
||||
import tty
|
||||
|
||||
|
||||
def available_ports():
|
||||
return sorted(set(glob.glob('/dev/cu.usbmodem*') + glob.glob('/dev/cu.usbserial*')))
|
||||
|
||||
|
||||
def open_port(path):
|
||||
fd = os.open(path, os.O_RDWR | os.O_NOCTTY | os.O_NONBLOCK)
|
||||
try:
|
||||
saved = termios.tcgetattr(fd)
|
||||
tty.setraw(fd, termios.TCSANOW)
|
||||
config = termios.tcgetattr(fd)
|
||||
config[2] |= termios.CLOCAL | termios.CREAD
|
||||
config[2] &= ~(termios.PARENB | termios.CSTOPB | termios.CSIZE)
|
||||
config[2] |= termios.CS8
|
||||
if hasattr(termios, 'CRTSCTS'):
|
||||
config[2] &= ~termios.CRTSCTS
|
||||
config[4] = config[5] = termios.B115200
|
||||
termios.tcsetattr(fd, termios.TCSANOW, config)
|
||||
return fd, saved
|
||||
except BaseException:
|
||||
os.close(fd)
|
||||
raise
|
||||
|
||||
|
||||
def read_session(path, output, diag_only=False):
|
||||
fd, saved = open_port(path)
|
||||
decoder = codecs.getincrementaldecoder('utf-8')(errors='replace')
|
||||
pending = ''
|
||||
|
||||
def emit(line):
|
||||
line = line.rstrip('\r')
|
||||
if not line or (diag_only and '[现场诊断]' not in line and '[USB日志]' not in line):
|
||||
return
|
||||
stamped = f'{datetime.now().astimezone().isoformat(timespec="milliseconds")} {line}\n'
|
||||
output.write(stamped)
|
||||
output.flush()
|
||||
sys.stdout.write(stamped)
|
||||
sys.stdout.flush()
|
||||
|
||||
print(f'已连接 {path},UTF-8 / 115200 8N1;Ctrl+C 结束。', file=sys.stderr)
|
||||
try:
|
||||
while True:
|
||||
readable, _, _ = select.select([fd], [], [], 1)
|
||||
if not readable:
|
||||
continue
|
||||
try:
|
||||
block = os.read(fd, 4096)
|
||||
except BlockingIOError:
|
||||
continue
|
||||
if not block:
|
||||
raise OSError('USB端口已关闭')
|
||||
pending += decoder.decode(block)
|
||||
while '\n' in pending:
|
||||
line, pending = pending.split('\n', 1)
|
||||
emit(line)
|
||||
if len(pending) > 8192:
|
||||
emit(pending + ' [未换行,分段保存]')
|
||||
pending = ''
|
||||
finally:
|
||||
pending += decoder.decode(b'', final=True)
|
||||
if pending:
|
||||
emit(pending)
|
||||
try:
|
||||
termios.tcsetattr(fd, termios.TCSANOW, saved)
|
||||
except termios.error:
|
||||
pass
|
||||
os.close(fd)
|
||||
|
||||
|
||||
def main():
|
||||
parser = argparse.ArgumentParser(description=__doc__)
|
||||
parser.add_argument('--list', action='store_true', help='列出 USB 串口,不打开设备')
|
||||
parser.add_argument('--port', help='例如 /dev/cu.usbmodemXXXX;仅一个USB串口时可省略')
|
||||
parser.add_argument('--output', type=Path, help='日志路径;默认 usb-logs/banban-时间.log')
|
||||
parser.add_argument('--diag-only', action='store_true', help='只保存现场诊断与USB状态行')
|
||||
parser.add_argument('--reconnect', action='store_true', help='拔插后等待同名端口重连;端口改名需重新指定')
|
||||
args = parser.parse_args()
|
||||
ports = available_ports()
|
||||
if args.list:
|
||||
print('\n'.join(ports) if ports else '未发现 USB 串口;检查数据线和设备枚举。')
|
||||
return 0
|
||||
path = args.port
|
||||
if not path:
|
||||
if len(ports) != 1:
|
||||
print('请用 --port 指定设备 CDC 口;先运行 --list 并对比拔插前后的列表。', file=sys.stderr)
|
||||
for port in ports:
|
||||
print(port, file=sys.stderr)
|
||||
return 2
|
||||
path = ports[0]
|
||||
destination = args.output or Path('usb-logs') / f'banban-{datetime.now():%Y%m%d-%H%M%S}.log'
|
||||
destination.parent.mkdir(parents=True, exist_ok=True)
|
||||
print(f'日志保存到 {destination.resolve()};时间戳是电脑接收时间。', file=sys.stderr)
|
||||
try:
|
||||
with destination.open('a', encoding='utf-8') as output:
|
||||
while True:
|
||||
try:
|
||||
read_session(path, output, args.diag_only)
|
||||
except (OSError, termios.error) as error:
|
||||
notice = f'[电脑读取] {datetime.now().astimezone().isoformat(timespec="milliseconds")} 连接中断:{error}'
|
||||
print(notice, file=sys.stderr)
|
||||
output.write(notice + '\n')
|
||||
output.flush()
|
||||
if not args.reconnect:
|
||||
return 1
|
||||
print('等待同名USB端口重连……', file=sys.stderr)
|
||||
time.sleep(1)
|
||||
except KeyboardInterrupt:
|
||||
print('\n已停止,日志已保存。', file=sys.stderr)
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
raise SystemExit(main())
|
||||
Reference in New Issue
Block a user