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())
|
||||
@@ -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
@@ -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()
|
||||
|
||||
@@ -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')
|
||||
Reference in New Issue
Block a user