fix: improve field diagnostics and mirror logs to USB CDC

This commit is contained in:
chenjintao
2026-10-20 14:46:08 +08:00
parent 8bff1f569e
commit 07094d4a4e
20 changed files with 1036 additions and 200 deletions
+127
View File
@@ -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())
+6 -1
View File
@@ -25,6 +25,11 @@ harness = r'''
#include <stdio.h>
#include <stddef.h>
#include <string.h>
#include "net_diag.h"
#include "net_diag_voice.h"
void net_diag_voice_begin(unsigned int round) { (void)round; }
void net_diag_voice_mark(unsigned int round, int stage) { (void)round; (void)stage; }
void net_diag_event(const char *event, int code) { (void)event; (void)code; }
typedef void *ql_mutex_t;
static int g_mic_working, recording, shine_init, fota_start;
enum { STATE_IDLE, STATE_RECORDING_WAIT_SOUND, STATE_RECORDING_ACTIVE, STATE_WAITING_COMMAND };
@@ -214,5 +219,5 @@ with tempfile.TemporaryDirectory(prefix='banban-timeout-') as directory:
cfile = Path(directory) / 'timeout.c'
exe = Path(directory) / 'timeout-test'
cfile.write_text(harness)
subprocess.run(['cc', '-std=c99', '-Wall', '-Wextra', '-Werror', str(cfile), '-o', str(exe)], check=True)
subprocess.run(['cc', '-std=c99', '-Wall', '-Wextra', '-Werror', '-I' + str(root / 'ql-application/threadx/interface/custom_app/inc'), str(cfile), '-o', str(exe)], check=True)
subprocess.run([str(exe)], check=True)
+271 -55
View File
@@ -1,34 +1,38 @@
"""Run the real diagnostic code with mocked modem APIs (no network traffic)."""
"""Exercise real diagnostic modules with SDK headers and fake modem/USB APIs.
No live network or physical serial port is used. Host size_t adaptation is
restricted to temporary SDK header copies; firmware headers remain untouched.
"""
from pathlib import Path
import re
import shutil
import subprocess
import tempfile
root = Path(__file__).resolve().parents[1]
base = root / 'ql-application/threadx/interface/custom_app'
sdk = root / 'ql-application/threadx/common/include'
harness = r'''
ROOT = Path(__file__).resolve().parents[1]
BASE = ROOT / 'ql-application/threadx/interface/custom_app'
SDK = ROOT / 'ql-application/threadx/common/include'
SAMPLE = r'''
#include <assert.h>
#include <setjmp.h>
#include "net_diag.c"
static int creates, fail_create, mode, hold, pdp = 1;
static int creates, fail_create, mode, hold, pdp = 1, sinr_calls;
static unsigned long clock_ms;
static int sample_steps, step_limit = 1;
static jmp_buf loop_exit;
static ql_ping_ctx_t mock_ctx;
static ql_ping_cb_t callback;
uint64_t hal_os_up_time_ms(void) { return clock_ms; }
uint8_t get_sleep_mode(void) { return hold; }
int get_mqtt_connect_status(void) { return 1; }
unsigned char get_xiaoice_connected_status(void) { return 1; }
int fota_start;
u8 power_down_status;
static s32 api_lookup(char *name) { (void)name; return 1; }
_get_api_ptr_t m_get_api_ptr = api_lookup;
QlOSStatus ql_rtos_task_create(ql_task_t *task, u32 stack, u8 priority, char *name,
void (*entry)(void *), void *arg, ...) {
(void)name; (void)entry; (void)arg;
assert(stack == 4096 && priority == 120);
++creates;
assert(stack == 4096 && priority == 120); ++creates;
if (fail_create == creates) return -9;
*task = (void *)1; return 0;
}
@@ -43,75 +47,287 @@ void ql_rtos_task_sleep_ms(u32 ms) {
longjmp(loop_exit, 1);
}
QL_NW_ERROR_CODE ql_nw_get_signal_strength(QL_NW_SIGNAL_STRENGTH_INFO_T *s) {
s->LTE_SignalStrength.rsrp = -97; return QL_NW_SUCCESS;
s->LTE_SignalStrength.rsrp = -97; s->LTE_SignalStrength.cqi = 255; return QL_NW_SUCCESS;
}
QL_NW_ERROR_CODE ql_nw_get_reg_status(QL_NW_REG_STATUS_INFO_T *r) {
r->data_reg.state = 1; return QL_NW_SUCCESS;
r->data_reg.state = 1; r->data_reg.rat = QL_NW_ACCESS_TECH_E_UTRAN; return QL_NW_SUCCESS;
}
int ql_get_data_call_info(int profile, int version, struct ql_data_call_info *d) {
assert(profile == 1 && version == 0); d->v4.state = pdp; return 0;
}
QL_NW_ERROR_CODE ql_nw_get_sinr(int *s) { *s = 13; return QL_NW_SUCCESS; }
QL_NW_ERROR_CODE ql_nw_get_sinr(int *s) { (void)s; ++sinr_calls; return (QL_NW_ERROR_CODE)255; }
QL_NW_ERROR_CODE ql_nw_get_cell_info(QL_NW_CELL_INFO_T *c) {
c->lte_info_valid = 1; c->lte_info_num = 1;
c->lte_info[0].cid = 123; c->lte_info[0].pci = 42; return QL_NW_SUCCESS;
}
ql_ping_ctx_t *ql_ping_init(ql_ping_cfg_t cfg, ql_ping_cb_t cb) {
assert(cfg.timeout == 2000 && cfg.pingNum == 1 && cfg.dataSize == 32 && cfg.pdpCid == 1);
callback = cb; return mode == 3 ? NULL : &mock_ctx;
void net_diag_probe(const char *host, unsigned short port, net_probe_result *r) {
assert(host && (port == 53 || port == 443));
memset(r, 0, sizeof(*r));
r->outcome = mode; r->tcp_started = mode == PROBE_OK || mode == PROBE_TIMEOUT;
strcpy(r->address, "192.0.2.1"); r->tcp_ms = 123;
}
int ql_ping(ql_ping_ctx_t *ctx) {
ql_ping_stat_t s;
assert(ctx == &mock_ctx); memset(&s, 0, sizeof(s));
if (mode == 2) return -2;
s.sentNum = 1; s.rcvdNum = mode == 0; s.rtt = 123;
callback(QL_PING_FINISHED, mode == 1 ? -1 : 0, &s); return 0;
}
void ql_ping_deinit(ql_ping_ctx_t *ctx) { assert(ctx == &mock_ctx); }
int main(void) {
fail_create = 2;
net_diag_start(); assert(!enabled && sample_task && !probe_task);
fail_create = 0;
net_diag_start(); assert(enabled && creates == 3);
net_diag_start(); assert(creates == 3);
mode = 0; probe_once(0); assert(attempts[0] == 1 && received[0] == 1 && !streak[0]);
mode = 1; probe_once(0); assert(attempts[0] == 2 && received[0] == 1 && streak[0] == 1);
mode = 2; probe_once(0); assert(attempts[0] == 2 && errors[0] == 1);
mode = 3; probe_once(0); assert(attempts[0] == 2 && errors[0] == 2);
mode = 0; probe_once(0); assert(!streak[0]);
mode = PROBE_OK; probe_once(0); assert(attempts[0] == 1 && received[0] == 1 && !streak[0]);
mode = PROBE_TIMEOUT; probe_once(0); assert(attempts[0] == 2 && received[0] == 1 && streak[0] == 1);
mode = PROBE_DNS; probe_once(0); assert(attempts[0] == 2 && errors[0] == 1);
mode = PROBE_LOCAL; probe_once(0); assert(attempts[0] == 2 && errors[0] == 2);
mode = PROBE_CANCELLED; probe_once(0); assert(attempts[0] == 2 && errors[0] == 2);
mode = PROBE_OK; probe_once(0); assert(!streak[0]);
step_limit = 3;
if (!setjmp(loop_exit)) sample_worker(NULL);
step_limit = 1;
if (!setjmp(loop_exit)) sample_worker(NULL);
assert(sinr_calls == 1 && sinr_unavailable);
hold = 1;
if (!setjmp(loop_exit)) probe_worker(NULL);
assert(attempts[1] == 0);
puts("PASS: creation failure/retry, idempotent activation, probe counters, sampling, sleep pause");
puts("PASS: diagnostic activation, statistics, invalid metrics, recovery and pause");
return 0;
}
'''
with tempfile.TemporaryDirectory(prefix='banban-netdiag-') as directory:
tmp = Path(directory)
for name in ['ql_type.h', 'ql_rtos.h', 'ql_nw.h', 'ql_data_call.h', 'ql_ping.h']:
content = (sdk / name).read_bytes()
if name == 'ql_type.h':
# The ARM SDK defines 32-bit size_t unconditionally. Host tests use libc's.
content, count = re.subn(rb'typedef unsigned int\s+size_t;', b'/* host size_t */', content)
assert count == 1
(tmp / name).write_bytes(content)
(tmp / 'test.c').write_text(harness)
command = [shutil.which('clang') or 'cc', '-std=c99', '-Werror=implicit-function-declaration',
'-Wno-typedef-redefinition']
for include in [tmp, base, base / 'inc', base / 'lib/hal', sdk, sdk / 'lwipv4v6']:
command.extend(['-I', str(include)])
subprocess.run(command + [str(tmp / 'test.c'), '-o', str(tmp / 'test')], check=True)
result = subprocess.run([str(tmp / 'test')], check=True, capture_output=True, text=True)
print(result.stdout, end='')
assert '本次网络不可用观测时长=2000 ms' in result.stdout
assert '未回应率=50%' in result.stdout
key = (base / 'hal_key.c').read_text()
alarm = key[key.index('if(key_press_alarm_counter!=0'):key.index('ql_rtos_task_sleep_ms(10);')]
assert 'net_diag_start();' in alarm
for forbidden in ['build_alarm(', 'build_leave_to_parent(', 'stop_player_func(', 'app_play_tips_file(']:
assert forbidden not in alarm, forbidden
print('PASS: diagnostic key has no former alarm/message/audio actions')
PROBE = r'''
#include <assert.h>
#include "net_diag_probe.c"
static int mode, closes, frees, sys_error, permitted = 1, selects, socket_calls;
static unsigned long clock_ms;
static struct sockaddr_in remote;
static struct addrinfo address;
uint64_t hal_os_up_time_ms(void) { return clock_ms; }
int net_diag_can_probe(void) { return permitted; }
int getaddrinfo_with_pcid(const char *host, const char *service, const struct addrinfo *hints,
struct addrinfo **result, u8_t cid) {
assert(host && service && hints->ai_family == AF_INET && cid == 1);
clock_ms += 35;
if (mode == 1) return 202;
if (mode == 2) { *result = NULL; return 0; }
memset(&address, 0, sizeof(address)); memset(&remote, 0, sizeof(remote));
remote.sin_family = AF_INET;
address.ai_family = AF_INET; address.ai_addr = (struct sockaddr *)&remote; address.ai_addrlen = sizeof(remote);
*result = &address;
if (mode == 12) permitted = 0;
return 0;
}
void freeaddrinfo(struct addrinfo *p) { assert(p == &address); ++frees; }
int ql_get_data_call_info(int cid, int version, struct ql_data_call_info *d) {
assert(cid == 1 && version == 0); d->v4.state = mode == 14 ? 0 : 1; return 0;
}
int socket(int family, int type, int protocol) {
assert(family == AF_INET && type == SOCK_STREAM && protocol == 0); ++socket_calls;
return mode == 3 ? -1 : 2;
}
int ioctl(int fd, long cmd, void *value) {
assert(fd == 2 && cmd == FIONBIO && *(int *)value == 1); return mode == 4 ? -1 : 0;
}
u16 ql_soc_generate_port(void) { return 12345; }
u16 ql_get_soc_errno(void) { return sys_error; }
int bind(int fd, const struct sockaddr *addr, socklen_t len) {
assert(fd == 2 && addr && len == sizeof(struct sockaddr_in)); return mode == 5 ? -1 : 0;
}
int connect(int fd, const struct sockaddr *addr, socklen_t len) {
assert(fd == 2 && addr && len == sizeof(struct sockaddr_in));
if (mode == 7) return 0;
sys_error = mode == 6 ? 111 : EINPROGRESS;
if (mode == 13) permitted = 0;
return -1;
}
int select(int nfds, fd_set *readable, fd_set *writable, fd_set *exceptional, struct timeval *wait) {
assert(nfds == 3 && !readable && writable && exceptional && wait->tv_usec <= 200000);
++selects;
if (mode == 9) { clock_ms += wait->tv_usec / 1000; return 0; }
clock_ms += 120;
if (mode == 10) { sys_error = 5; return -1; }
FD_ZERO(exceptional); FD_SET(2, writable); return 1;
}
int getsockopt(int fd, int level, int option, void *value, socklen_t *len) {
assert(fd == 2 && level == SOL_SOCKET && option == SO_ERROR && *len == sizeof(int));
if (mode == 11) { sys_error = 5; return -1; }
*(int *)value = mode == 8 ? 111 : 0; return 0;
}
int close(int fd) { assert(fd == 2); ++closes; return 0; }
int main(void) {
net_probe_result r;
int expected[] = {PROBE_OK, PROBE_DNS, PROBE_DNS, PROBE_LOCAL, PROBE_LOCAL, PROBE_LOCAL,
PROBE_CONNECT, PROBE_OK, PROBE_CONNECT, PROBE_TIMEOUT, PROBE_LOCAL, PROBE_LOCAL,
PROBE_CANCELLED, PROBE_CANCELLED, PROBE_LOCAL};
for (mode = 0; mode < 15; ++mode) {
closes = frees = selects = socket_calls = sys_error = 0; clock_ms = 0; permitted = 1;
net_diag_probe("test.example", 443, &r);
assert(r.outcome == expected[mode]); assert(r.dns_ms == 35);
assert(frees == (mode != 1 && mode != 2));
assert(closes == (mode != 1 && mode != 2 && mode != 3 && mode != 12 && mode != 14));
if (mode == 0) assert(r.tcp_ms == 120 && r.error == 0);
if (mode == 8) assert(r.error == 111);
if (mode == 9) assert(r.tcp_ms == 3000 && selects == 15);
}
permitted = 0; socket_calls = 0;
net_diag_probe("test.example", 443, &r);
assert(r.outcome == PROBE_CANCELLED && !socket_calls && r.dns_ms == 0);
puts("PASS: real probe DNS/connect success, refusal, timeout, local errors, cancellation and cleanup");
return 0;
}
'''
USB = r'''
#include <assert.h>
#include <setjmp.h>
#include "field_log.c"
static int queue_mode, queue_deletes, task_error, writes, write_mode, close_count;
static int worker_run, worker_reads, open_count;
static jmp_buf usb_exit;
static struct log_record last_record;
static char sent_text[4096];
QlOSStatus ql_rtos_task_sched_lock(void) { return 0; }
QlOSStatus ql_rtos_task_sched_unlock(void) { return 0; }
QlOSStatus ql_rtos_queue_create(ql_queue_t *q, u32 size, u32 count) {
assert(size == sizeof(struct log_record) && size % 4 == 0 && count == 16);
*q = (void *)1; return queue_mode == 1 ? -1 : 0;
}
QlOSStatus ql_rtos_queue_delete(ql_queue_t q) { assert(q); ++queue_deletes; return 0; }
QlOSStatus ql_rtos_queue_release(ql_queue_t q, u32 size, u8 *record, u32 timeout) {
assert(q && size == sizeof(struct log_record) && timeout == 0);
last_record = *(struct log_record *)record; return queue_mode == 2 ? -1 : 0;
}
QlOSStatus ql_rtos_queue_wait(ql_queue_t q, u8 *record, u32 size, u32 timeout) {
(void)q; (void)size; (void)timeout;
if (worker_run) {
if (++worker_reads > 1) longjmp(usb_exit, 1);
strcpy(((struct log_record *)record)->text, "test");
((struct log_record *)record)->length = 4; return 0;
}
return -1;
}
QlOSStatus ql_rtos_task_create(ql_task_t *t, u32 size, u8 priority, char *name, void (*fn)(void *), void *arg, ...) {
assert(size == 4096 && priority == 130); (void)name; (void)fn; (void)arg;
*t = (void *)1; return task_error;
}
void ql_rtos_task_sleep_ms(u32 ms) { (void)ms; }
u32 ql_rtos_get_systicks(void) { return 0; }
int ql_uart_open(QL_UART_PORT_NUMBER_E port, QL_UART_BAUD_E baud, QL_UART_FLOWCTRL_E flow) {
assert(port == QL_USB_CDC_PORT && baud == QL_UART_BAUD_115200 && flow == QL_FC_NONE); ++open_count; return QL_UART_ALREAD_OPEN;
}
int ql_uart_close(QL_UART_PORT_NUMBER_E port) { assert(port == QL_USB_CDC_PORT); ++close_count; return 0; }
int ql_uart_write(QL_UART_PORT_NUMBER_E port, unsigned char *data, int len) {
int n = write_mode == 1 && len > 3 ? 3 : len;
assert(port == QL_USB_CDC_PORT); ++writes;
if (write_mode == 2 || (write_mode == 3 && len == 4)) return -1;
strncat(sent_text, (char *)data, write_mode == 0 ? len : n);
return write_mode == 0 ? 0 : n;
}
int main(void) {
char large[1001];
queue_mode = 1; assert(field_log_init() == -1 && !ready);
queue_mode = 0; task_error = -2; assert(field_log_init() == -2 && !ready && queue_deletes == 1);
task_error = 0; assert(field_log_init() == 0 && ready);
assert(field_log_init() == 0);
field_log_printf("[现场诊断] 中文 value=%d\r\n", 42);
assert(strcmp(last_record.text, "[现场诊断] 中文 value=42\r\n") == 0);
assert(last_record.length == strlen(last_record.text));
queue_mode = 2; field_log_printf("queue full\n"); assert(dropped == 1);
queue_mode = 0; memset(large, 'A', 1000); large[1000] = 0;
field_log_printf("%s", large); assert(strstr(last_record.text, "[TRUNCATED]") && last_record.length < 512);
write_mode = 0; assert(usb_write_all("abcdef", 6) == 0 && writes == 1 && strcmp(sent_text, "abcdef") == 0);
sent_text[0] = 0; writes = 0; write_mode = 1;
assert(usb_write_all("abcdef", 6) == 0 && writes == 2 && strcmp(sent_text, "abcdef") == 0);
write_mode = 2; assert(usb_write_all("abcdef", 6) == -1);
worker_run = 1; write_mode = 3; writes = 0; sent_text[0] = 0;
if (!setjmp(usb_exit)) usb_worker(NULL);
assert(open_count == 2 && close_count == 1 && dropped == 2);
puts("PASS: USB init rollback, no-wait queue, mirror, truncation, writes, disconnect/reopen");
return 0;
}
'''
VOICE = r'''
#include <assert.h>
#include "net_diag_voice.c"
static unsigned long clock_ms;
static int active = 1;
uint64_t hal_os_up_time_ms(void) { return clock_ms; }
int net_diag_is_enabled(void) { return active; }
QlOSStatus ql_rtos_task_sched_lock(void) { return 0; }
QlOSStatus ql_rtos_task_sched_unlock(void) { return 0; }
int main(void) {
unsigned int token;
clock_ms = 100; net_diag_voice_begin(3);
clock_ms = 500; net_diag_voice_mark(3, VOICE_SPEECH);
clock_ms = 1000; net_diag_voice_mark(3, VOICE_RECORD_STOP);
clock_ms = 1100; net_diag_voice_mark(3, VOICE_END_QUEUED);
clock_ms = 1200; net_diag_voice_mark(3, VOICE_END_ACK);
clock_ms = 1500; net_diag_voice_mark(3, VOICE_TTS_START);
clock_ms = 3000; net_diag_voice_url(3, "http://test/first.mp3");
net_diag_voice_url(3, "http://test/second.mp3");
assert(net_diag_audio_begin("http://test/unrelated.mp3") == 0);
clock_ms = 3100; token = net_diag_audio_begin("http://test/first.mp3"); assert(token == 3);
clock_ms = 3300; net_diag_audio_done(token, 8001, 0);
clock_ms = 3350; net_diag_voice_mark(token, VOICE_FIRST_PCM);
clock_ms = 3500; net_diag_voice_mark(token, VOICE_FIRST_PCM);
assert(voice.stamp[VOICE_FIRST_PCM] == 3350);
net_diag_voice_begin(4);
net_diag_voice_mark(3, VOICE_FIRST_PCM); assert(!voice.seen);
net_diag_voice_url(3, "http://old.mp3"); assert(!voice.first_url[0]);
active = 0; net_diag_voice_begin(5); assert(voice.round == 4);
puts("PASS: voice stage timing, first URL matching, duplicate/stale events, disabled mode");
return 0;
}
'''
def run_c(tmp, name, source, flags=()):
path = tmp / f'{name}.c'
path.write_text(source)
command = [shutil.which('clang') or 'cc', '-std=c99', '-Werror=implicit-function-declaration',
'-Wno-typedef-redefinition', *flags]
for include in [tmp, BASE, BASE / 'inc', BASE / 'lib/hal', SDK, SDK / 'lwipv4v6']:
command.extend(['-I', str(include)])
subprocess.run(command + [str(path), '-o', str(tmp / name)], check=True)
result = subprocess.run([str(tmp / name)], capture_output=True, text=True)
if result.returncode:
print(result.stdout, result.stderr)
result.check_returncode()
for line in result.stdout.splitlines():
if line.startswith('PASS:'):
print(line)
return result.stdout
def main():
with tempfile.TemporaryDirectory(prefix='banban-netdiag-') as directory:
tmp = Path(directory)
for name in ['ql_type.h', 'ql_rtos.h', 'ql_nw.h', 'ql_data_call.h']:
content = (SDK / name).read_bytes()
if name == 'ql_type.h':
content, count = re.subn(rb'typedef unsigned int\s+size_t;', b'/* host size_t */', content)
assert count == 1
(tmp / name).write_bytes(content)
sample = run_c(tmp, 'sample', SAMPLE)
assert '本次网络不可用观测时长=2000 ms' in sample
assert '失败率=50%' in sample and 'CQI=不可用(raw=255)' in sample
run_c(tmp, 'probe', PROBE, flags=['-include', 'sys/time.h', '-DTIME_VAL_DEF', '-Wno-macro-redefined'])
run_c(tmp, 'usb', USB, flags=['-Dprintf=field_log_printf'])
voice = run_c(tmp, 'voice', VOICE)
assert '录音结束到首PCM=2350 ms' in voice
assert '收尾入队=100 等TTS通知=400 等首URL=1500 播放队列=100 下载=200 解码提交=50' in voice
assert voice.count('[耗时汇总]') == 1
# Exercise the actual quiet macro: muting must still execute side effects.
gnss = (BASE / 'gnss.c').read_text()
config = gnss[gnss.index('#ifndef BANBAN_GNSS_LOG_ENABLED'):gnss.index('// 新增宏定义')]
run_c(tmp, 'gnss', '#include <assert.h>\n#include <stdio.h>\n' + config + r'''
int main(void) { int closed = 0; GNSS_LOG("close=%d", ++closed); assert(closed == 1);
puts("PASS: GNSS mute preserves side effects"); return 0; }
''')
key = (BASE / 'hal_key.c').read_text()
alarm = key[key.index('if(key_press_alarm_counter!=0'):key.index('ql_rtos_task_sleep_ms(10);')]
assert 'net_diag_start();' in alarm
for forbidden in ['build_alarm(', 'build_leave_to_parent(', 'stop_player_func(', 'app_play_tips_file(']:
assert forbidden not in alarm, forbidden
print('PASS: diagnostic key retains no former alarm/message/audio actions')
if __name__ == '__main__':
main()
+62
View File
@@ -0,0 +1,62 @@
"""Test the Mac USB reader against a temporary pseudo-terminal, not hardware."""
from pathlib import Path
import os
import pty
import select
import signal
import subprocess
import sys
import tempfile
import time
ROOT = Path(__file__).resolve().parents[1]
def exercise(diag_only):
master, slave = pty.openpty()
port = os.ttyname(slave)
process = None
try:
with tempfile.TemporaryDirectory(prefix='banban-usb-reader-') as directory:
output = Path(directory) / 'capture.log'
command = [sys.executable, str(ROOT / 'tools/read_usb_log.py'), '--port', port, '--output', str(output)]
if diag_only:
command.append('--diag-only')
process = subprocess.Popen(command, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
notice = b''
deadline = time.monotonic() + 5
while '已连接'.encode() not in notice:
assert time.monotonic() < deadline, notice.decode(errors='replace')
if select.select([process.stderr], [], [], 0.1)[0]:
notice += os.read(process.stderr.fileno(), 4096)
assert process.poll() is None, notice.decode(errors='replace')
payload = '[现场诊断] 中文连续测试\r\nordinary application log\r\n[USB日志] 在线\n[现场诊断] 最后一行'.encode()
os.write(master, payload[:3]) # Deliberately split a UTF-8 character.
time.sleep(0.02)
os.write(master, payload[3:])
deadline = time.monotonic() + 5
while not output.exists() or '[USB日志] 在线' not in output.read_text():
assert time.monotonic() < deadline
time.sleep(0.02)
process.send_signal(signal.SIGINT)
stdout, stderr = process.communicate(timeout=5)
assert process.returncode == 0, stderr.decode()
saved = output.read_text()
assert '\ufffd' not in saved
assert '[现场诊断] 中文连续测试' in saved
assert '[USB日志] 在线' in saved
assert '[现场诊断] 最后一行' in saved
assert ('ordinary application log' in saved) != diag_only
assert '[现场诊断] 中文连续测试' in stdout.decode()
finally:
if process and process.poll() is None:
process.kill()
process.communicate()
os.close(master)
os.close(slave)
if __name__ == '__main__':
exercise(False)
exercise(True)
print('PASS: pseudo-terminal USB capture, split UTF-8, diagnostic filter, Ctrl+C and final-line flush')