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
+67 -20
View File
@@ -2,41 +2,88 @@
GPIO84 原报警/留言按键改为诊断键。释放时按住超过 50 ms、少于 3 秒,开启主动模式。超过或等于 3 秒不执行动作。重复短按只打印已开启,不创建重复任务;重启恢复关闭。该按键不再报警、留言、播放提示音或停止音频。
设备处于定时休眠、OTA 或关机流程时不响应开启。开启之后遇到这些状态,暂停采样和新探测,已有探测可能继续到返回。没有新增常亮、常唤醒或自动重启操作;主动网络流量本身仍可能影响空闲态和耗电。
设备处于定时休眠、OTA 或关机流程时不响应开启。开启之后遇到这些状态,暂停采样和新探测。TCP 等待期间每不超过 200 ms 检查一次取消;SDK 的域名解析是同步调用,必须等待返回才能取消,未修改全局 DNS 超时设置。原有休眠和定位流程保留。
## 查看日志
## Mac 直接读取 USB 日志
使用原有固件日志串口,终端设置 UTF-8,保存完整串口输出。搜索 `[现场诊断]` 可筛出诊断行。时间为开机毫秒(目标平台计数会回绕),不是北京时间。日志不写 Flash、不上传服务器;断网时电脑仍可留存串口日志。
刷入这版固件后,用支持数据传输的 USB 线连接设备与 Mac。固件通过 `QL_USB_CDC_PORT` 输出 UTF-8 纯文本,并保留 UART 输出。没有使用基带 DIAG 二进制日志端口。
- 每约 2 秒:注册状态、PDP 数据连接、MQTT、WSS,及 LTE 信号原始字段。
- 每约 10 秒:SINR 原始值、LTE 驻留小区 CID/PCI/EARFCN 和 MCC/MNC。标记小区变化。
- 主动探测:依次 Ping `223.5.5.5` 和当前业务域名 `banban.api.talkingq.com`,每目标 1 个 32 字节载荷,ICMP 超时 2000 ms;整轮完成后等待 10 秒。域名解析和 SDK 调度可能增加耗时,非严格周期。
- 即时事件:数据连接回调、当前 WSS 的建立/断开;注册/PDP 的采样变化、不可用和恢复。
- 每次探测后:本次开机的已发送、收到、未回应、未回应率和执行异常数量。超时连续次数遇到收到回应时清零。未获得有效发包统计的 SDK 错误单独计数。
在仓库根目录运行下面命令。读取工具只用 Python 3 标准库,不需要安装 pyserial。
示例(演示数据,并非现场测量):
```sh
python3 tools/read_usb_log.py --list
python3 tools/read_usb_log.py --port /dev/cu.usbmodemXXXX --reconnect
```
将 `cu.usbmodemXXXX` 换成拔插设备后新增的实际端口。多个串口时,工具要求明确选择,不会自行选择第一个。确认选对端口的方法是看到 `[USB日志]` 在线提示;普通 USB 转串口适配器也可能出现在列表中。
默认保存到当前目录的 `usb-logs/banban-年月日-时分秒.log`,同时在终端显示。短按原报警键开始网络诊断,随后进行对话,按 Ctrl+C 停止保存。`--reconnect` 等待同名端口重新出现,端口改名后需重新指定。可添加 `--output 路径` 更改文件位置,或 `--diag-only` 只保存 `[现场诊断]` 和 `[USB日志]` 行。
也可以使用现有串口终端打开这个 USB CDC 口:115200、8N1、无流控、UTF-8。同一端口同时只用一个读取程序。没有新增端口时,先检查数据线和 Mac 的 USB 设备枚举;实际板上的 USB 数据连接、CDC 描述符和 Mac 驱动识别仍需上板确认。
日志通过约 8 KB 的有界队列交给独立低优先级 USB 转发任务。业务日志入队使用零等待;USB 写失败时关闭并重新打开 CDC。队列拥塞或写失败会丢弃 USB 副本并累计计数,在线提示每约 5 秒报告一次。电脑接收时间不等于设备事件时间;断线前 SDK 已接受但未送到电脑的数据也无法保证补回,不支持历史日志回放。
转发范围是 `custom_app` 目标内编译的 `printf`,包括这次新增的诊断、VAD、WSS 和播放器日志。预编译库、内核、基带和启动前日志不经过本转发。单次格式化最多 511 字节,超长记录会明确加 `[TRUNCATED]`,并在 UTF-8 字符边界截断。日志不写设备 Flash、不上传服务器。
## 日志内容
设备时间为开机毫秒;目标平台计数会回绕。搜索 `[现场诊断]` 筛选诊断行,按 `[对话#编号]` 关联一轮对话。
- 每约 2 秒:注册状态、PDP 数据连接、MQTT、WSS,及 LTE 信号原始字段。SDK 查询可能增加采样间隔。
- 每约 10 秒:可用的 SINR、LTE 驻留小区 CID/PCI/EARFCN、MCC/MNC 原始编码。标记小区变化。
- 主动探测:依次测量参照 DNS 服务 `223.5.5.5:53` 和业务 WSS 入口 `banban.api.talkingq.com:443` 的 DNS 解析与 TCP 建连耗时。整轮完成后等待 10 秒;使用 PDP 1 的实际数据连接地址绑定。TCP 等待上限 3000 ms,不发送业务内容,也不进行 TLS/WSS 握手。
- 即时事件:数据连接回调、当前 WSS 的建立/断开;注册/PDP 的采样变化、不可用和恢复。底层套接字写入失败或耗时达到 200 ms 时单独记录。
- 每次探测后:本次开机的 TCP 有效尝试、成功、失败、失败率和执行异常数量。连续 TCP 失败遇到成功时清零。DNS、套接字初始化、绑定和本地等待错误单独计数,不混作 TCP 失败。
原现场日志中两个目标均在 `ql_ping_init()` 阶段返回空指针,没有形成有效探测。当前默认直接使用业务已有的 DNS/TCP 路径,移除对可选 Ping API 的依赖;没有把空指针的根因武断归为网络错误或已确认的内核缺失。
SINR 接口未导出或返回 255 时只记录一次不可用提示,本次开机跳过该项。CQI 不在 0~15 范围内时标为不可用并保留原始值。MCC/MNC 用十六进制明确表示原始编码,避免将编码值误当作普通十进制运营商号码。
示例为演示数据,并非上板测量:
```text
[现场诊断][12000 ms][连接] 注册=已注册本地网络(1) 数据连接=已连接 MQTT=已连接 WSS=已连接
[现场诊断][12000 ms][信号原始值] RSSI=-71 RSRP=-97 RSRQ=-12 CQI=8;单位/无效值以模组为准
[现场诊断][12500 ms][探测成功] 参照节点 223.5.5.5;往返=123 ms
[现场诊断][12500 ms][累计统计] 参照节点;已发送=20 收到=19 未回应=1 未回应率=5% 执行异常=0(本次开机累计)
[现场诊断][12000 ms][信号原始值] RSSI=-55 RSRP=-80 RSRQ=-3 CQI=不可用(raw=255);单位/无效值以模组为准
[现场诊断][12500 ms][探测成功] 业务WSS入口;IP=192.0.2.1 解析=35 ms TCP建连=123 ms(非AI响应时间)
[现场诊断][12500 ms][累计统计] 业务WSS入口;TCP有效尝试=20 成功=19 失败=1 失败率=5% 执行异常=0;非业务丢包率
```
## 判断边界
## 一轮对话的分段计时
信号值以 SDK 返回的原始字段记录。必须结合模组实际单位、无效值和无线制式确认后解释,暂不直接分档。注册成功不代表有互联网;MQTT/WSS 状态是业务维护的状态,断网瞬间可能滞后。当前按 LTE 项目设计,其他制式不能直接使用 LTE 字段判读。
诊断模式开启后的下一轮录音开始计时,依次记录:开始录音、确认用户语音、录音结束、结束包入发送队列、收到 END_ACK(服务器实际返回时)、收到 TTS_START、收到首段音频 URL、首段开始下载/下载完成、首 PCM 交给播放接口。
参照节点属于外部公共服务,可能限速或禁 ICMP;业务域名对应入口也可能不回答 Ping。两者均不是 AI 服务健康检查。ICMP 未回应率不等于语音业务丢包率;域名探测结果中的 RTT 是 ICMP 往返时间,不包括 DNS 或 AI 处理时间。
首段根据通过当前 session 校验的完整 URL 匹配,只跟踪第一段正常回复,排除不匹配的本地提示、留言确认和错误提示;后续录音轮次开始后不接受上一轮的延迟阶段事件。VAD 统计显示总帧、活跃录音帧,以及首次确认前连续语音峰值,例如 `13/20`,帮助判断为什么一直没有确认开口。没有根据一份日志直接降低 VAD 阈值。
网络不可用计时依据注册状态和实际 PDP 查询;查询失败不虚构掉线/恢复。时间包含采样误差和暂停期间,不能当作精确无线断线时间。小区变化表示驻留记录改变,不等同于已确认发生连接态切换。
阶段齐全且顺序合理时,在首 PCM 提交后输出汇总:
本次实现侧重网络现场日志,尚未新增每轮对话的 VAD/上传/服务端/首音分段计时,也未新增 DNS/TCP/TLS 独立探测。若网络记录正常但回复仍慢,需结合已有 WSS/TLS/音频日志和服务端记录继续定位。
```text
[现场诊断][3350 ms][对话#3][耗时汇总] 录音结束到首PCM=2350 ms;收尾入队=100 等TTS通知=400 等首URL=1500 播放队列=100 下载=200 解码提交=50(均ms;含网络与服务端处理,尚未分离)
```
`录音结束` 是 VAD 停止录音的时间,不是人实际停止说话的时间。`结束包入队` 不表示底层发送完成或服务端收到;底层慢写事件可作辅助,END_ACK 也仅在后端返回该事件时存在。`首PCM提交` 不是扬声器的精确出声时刻。只有后端配合同一轮请求的收包/ASR/模型/TTS 时间,才能进一步分离服务端处理与网络等待。
## 如何判断
先看对话编号对应的慢阶段,再看同一时段的信号、DNS、TCP 和掉线事件。连续语音峰值未达到确认条件、最后以 no_speech 结束,应优先核对当时是否说话及采音/VAD。首 URL 出现得晚应结合服务器日志;URL 已到但下载/播放排队慢,则优先看对应下载和播放阶段。
TCP 建连成功只说明该次能连接目标端口,不代表 TLS、WSS 或 AI 业务正常;失败也可能是目标端口策略、服务状态等原因。参照端口和业务端口不相同,不能仅凭一次结果直接归责运营商。解析可能命中缓存,未清空业务 DNS 缓存,解析耗时不是强制冷查询耗时。TCP 失败率不等于业务丢包率。
信号按 SDK 原始字段记录,单位和无效值仍应以模组确认结果解释。注册成功不代表有互联网;MQTT/WSS 状态是业务维护的状态,可能滞后。网络不可用计时包含采样误差和暂停时间,查询失败不会虚构掉线/恢复。小区变化表示驻留记录改变,不等同于已确认发生连接态切换。
## GNSS 日志开关
`gnss.c` 中 `BANBAN_GNSS_LOG_ENABLED` 默认 0,临时屏蔽模式提示、倒计时、坐标、解析提示和原始 NMEA 输出。定位、卫星搜索、开关 GNSS 与位置上报流程保留。静默函数仍求值原日志参数,保留历史上写在参数中的 `ql_gnss_close()` 调用。需要恢复时设为 1 或为该目标添加同名编译宏。
## 验证与上板
运行 `python3 tools/test_net_diag.py`,以 SDK 头文件和模拟模组接口编译运行真实诊断源码,验证任务创建失败重试、重复开启、成功/无回应/执行异常统计、网络恢复计时和休眠暂停;并检查按键分支没有原报警/留言/音频操作。
```sh
python3 tools/test_net_diag.py
python3 tools/test_conversation_timeout.py
python3 tools/test_read_usb_log.py
```
macOS 测试仅在临时副本中移除 SDK 与宿主不兼容的 32 位 size_t 定义,不改 SDK 文件。该测试不代表 ARM 完整固件编译或硬件验收。正式构建沿用项目 Windows ARM Compiler 5 构建流程。
诊断测试编译执行真实诊断源码,模拟 SDK 的成功、DNS 失败、连接拒绝、超时、初始化失败、取消和资源释放;验证统计、SINR/CQI 不可用处理、USB 零等待队列/部分写/断线重开、对话分段与旧轮次隔离、GNSS 静默仍保留副作用。已有对话回归测试验证超时、打断和角色提示流程。Mac 读取工具使用临时伪终端验证 UTF-8 分包、过滤、Ctrl+C 与末行保存,不打开物理串口。
上板后核对:UTF-8 中文是否正确;50 ms 以下抖动、正常短按、重复短按、3 秒以上长按;断网仍可开启诊断;两目标成功和不回应;恢复网络;对话时不因按键中断声音;OTA/定时休眠暂停;重启关闭。核实实际模组信号单位、Ping 回调和内存余量。
宿主测试只在临时头文件副本/编译选项中处理 SDK 的 32 位 size_t 和 timeval 与 Mac 的差异,不修改 SDK。测试不能代替完整 ARM 固件构建与硬件验收。正式构建沿用项目 Windows 工具链;当前应用 Makefile 使用 arm-none-eabi-gcc。
上板后确认:直接 USB 枚举与端口识别;UTF-8 日志;晚插线、拔插、拥塞和 USB 丢弃计数;离线仍可开启诊断;两个目标的 DNS/TCP 结果;对话时音频正常且汇总匹配;OTA/定时休眠暂停;重启关闭。核实模组信号单位、实际内存余量和 USB SDK 的返回语义。
@@ -68,6 +68,9 @@ SRC_FILES+= \
http_aofei.c \
net_mgt.c \
net_diag.c \
net_diag_probe.c \
net_diag_voice.c \
field_log.c \
ws_talkingq.c \
talkingq_wss.c \
talkingq_root_ca.c \
@@ -114,7 +117,7 @@ INC_DIRS+= \
#-------------------------------------------------------------------------------
# Configure compile flag for C
#-------------------------------------------------------------------------------
U_FLAGS:=
U_FLAGS:= -Dprintf=field_log_printf
#-------------------------------------------------------------------------------
# Configure compile flag for C++
@@ -26,7 +26,8 @@
#include "ql_uart.h"
#include "ql_application.h"
#include "ql_log.h"
#include "ql_log.h"
#include "field_log.h"
#include "hal_api.h"
#include "nfc.h"
#include "ota_baseline.h"
@@ -78,35 +79,8 @@ int app_get_app_sleep_flag(void)
}
void quec_usbcdc_callback(QL_UART_PORT_NUMBER_E port, void *para)
{
int read_len = 0;
char r_data[1024] = {0};
read_len = ql_uart_read(port, r_data, sizeof(r_data));
printf("uart[%d] callback read data, data:%s, read_len:%d \n", port, r_data, read_len);
}
void quec_usbcdc_write(unsigned char *data, int len)
{
ql_uart_write(QL_USB_CDC_PORT, data, len);
}
static void quec_usbcdc_init(void)
{
ql_uart_config_t dcb;
ql_uart_open(QL_USB_CDC_PORT, QL_UART_BAUD_115200, QL_FC_NONE);
dcb.baudrate = QL_UART_BAUD_115200;
dcb.data_bit = 8;
dcb.stop_bit = 1;
dcb.parity_bit = 0;
dcb.flow_ctrl = 0;
ql_uart_set_dcbconfig(QL_USB_CDC_PORT, &dcb);
ql_uart_register_cb(QL_USB_CDC_PORT, quec_usbcdc_callback); //use callback to read uart data
}
/* USB CDC is owned by field_log.c; no receive/echo callback is installed. */
#define AT_CMD_SLEPP_SUFFIX "\r\n"
void ql_atcmd_sleep(void)
{
@@ -135,6 +109,8 @@ static void custom_app_task(void * argv)
uint8_t flag = 0;
u32 app_start_time_record = 0;
field_log_init();
// 安全修复:初始化互斥锁
ql_rtos_mutex_create(&g_app_op_mutex);
@@ -162,7 +138,6 @@ static void custom_app_task(void * argv)
//app_uartsh_init();
// quec_usbcdc_init();
//ql_rtos_task_sleep_s(15);
@@ -0,0 +1,120 @@
/* Mirror application printf calls to plain-text USB CDC without waiting
* for the USB host in producers. UART still uses the SDK's original printf. */
#include <stdio.h>
#include <stdarg.h>
#include <string.h>
#include "ql_rtos.h"
#include "ql_uart.h"
#include "field_log.h"
/* This module is also compiled with -Dprintf=field_log_printf. */
#undef printf
extern int printf(const char *format, ...);
#define LOG_RECORD_BYTES 512
#define LOG_QUEUE_DEPTH 16
struct log_record { unsigned int length; char text[LOG_RECORD_BYTES]; };
static ql_queue_t usb_queue;
static ql_task_t usb_task;
static volatile int ready;
static unsigned long dropped;
static void note_drop(void)
{
ql_rtos_task_sched_lock();
++dropped;
ql_rtos_task_sched_unlock();
}
int field_log_printf(const char *format, ...)
{
struct log_record record;
va_list args;
int n, ret;
va_start(args, format);
n = vsnprintf(record.text, sizeof(record.text), format, args);
va_end(args);
if (n < 0) return n;
if (n >= (int)sizeof(record.text)) {
const char suffix[] = " [TRUNCATED]\r\n";
size_t cut = sizeof(record.text) - sizeof(suffix);
/* Keep the UTF-8 prefix valid if a multi-byte character crosses cut. */
while (cut && ((unsigned char)record.text[cut] & 0xc0) == 0x80) --cut;
memcpy(record.text + cut, suffix, sizeof(suffix));
}
record.length = (unsigned int)strlen(record.text);
ret = printf("%s", record.text);
if (ready && record.length &&
ql_rtos_queue_release(usb_queue, sizeof(record), (u8 *)&record, QL_NO_WAIT) != 0)
note_drop();
return ret;
}
/* SDK variants return zero on success or a positive byte count. */
static int usb_write_all(const char *text, int length)
{
int offset = 0;
while (offset < length) {
int n = ql_uart_write(QL_USB_CDC_PORT, (unsigned char *)text + offset, length - offset);
if (n == 0) return 0;
if (n < 0 || n > length - offset) return -1;
offset += n;
}
return 0;
}
static void usb_worker(void *arg)
{
struct log_record record;
int opened = 0;
unsigned long last_notice = 0, reported = 0;
(void)arg;
for (;;) {
if (!opened) {
int open_result = ql_uart_open(QL_USB_CDC_PORT, QL_UART_BAUD_115200, QL_FC_NONE);
if (open_result == QL_UART_SUCCESS || open_result == QL_UART_ALREAD_OPEN) {
const char banner[] = "\r\n[USB日志] 板板现场日志 CDC 已开启;UTF-8 / 115200 8N1;短按原报警键开始网络诊断\r\n";
opened = usb_write_all(banner, sizeof(banner) - 1) == 0;
if (!opened) ql_uart_close(QL_USB_CDC_PORT);
}
if (!opened) { ql_rtos_task_sleep_ms(1000); continue; }
}
if (ql_rtos_queue_wait(usb_queue, (u8 *)&record, sizeof(record), 1000) == 0) {
if (usb_write_all(record.text, record.length) != 0) {
note_drop();
ql_uart_close(QL_USB_CDC_PORT);
opened = 0;
ql_rtos_task_sleep_ms(1000);
}
}
/* Beacons identify the correct serial interface even when app is idle.
* Drop totals are never reset: concurrent producers cannot lose them. */
if (opened && (u32)(ql_rtos_get_systicks() - last_notice) >= 1000) {
char status[160];
unsigned long total;
ql_rtos_task_sched_lock(); total = dropped; ql_rtos_task_sched_unlock();
snprintf(status, sizeof(status), "\r\n[USB日志] 在线;累计丢弃=%lu 条%s\r\n", total,
total != reported ? ";USB拥塞或未连接,记录可能不完整" : "");
if (usb_write_all(status, (int)strlen(status)) != 0) {
ql_uart_close(QL_USB_CDC_PORT); opened = 0;
} else reported = total;
last_notice = ql_rtos_get_systicks();
}
}
}
int field_log_init(void)
{
int ret;
if (ready) return 0;
ret = ql_rtos_queue_create(&usb_queue, sizeof(struct log_record), LOG_QUEUE_DEPTH);
if (ret != 0) { printf("[USB日志] 队列创建失败=%d;仅保留UART日志\r\n", ret); return ret; }
ret = ql_rtos_task_create(&usb_task, 4096, 130, "usb_log", usb_worker, NULL);
if (ret != 0) {
ql_rtos_queue_delete(usb_queue); usb_queue = NULL;
printf("[USB日志] 转发任务创建失败=%d;仅保留UART日志\r\n", ret);
return ret;
}
ready = 1;
field_log_printf("[USB日志] 应用日志已同步到USB CDC;内核/基带日志不在此转发范围\r\n");
return 0;
}
@@ -16,6 +16,18 @@
#include "netdb.h"
#include "app_inc.h"
/* Temporary field-debug switch: set to 1 to restore GNSS text/NMEA logs.
* The quiet function deliberately evaluates arguments (some close the GNSS). */
#ifndef BANBAN_GNSS_LOG_ENABLED
#define BANBAN_GNSS_LOG_ENABLED 0
#endif
#if BANBAN_GNSS_LOG_ENABLED
#define GNSS_LOG printf
#else
static int gnss_log_quiet(const char *format, ...) { (void)format; return 0; }
#define GNSS_LOG gnss_log_quiet
#endif
// 新增宏定义(原代码中未定义)
#define GNSS_FAST_MODE_HOLD_TIME (5 * 60 * 1000) // FAST模式持续5分钟后切SLOW
@@ -213,7 +225,7 @@ void quec_gnss_callback(char *data, int len)
// 打印原始 NMEA 数据
for (i = 0; i < len; i++) {
printf("%c", data[i]);
GNSS_LOG("%c", data[i]);
}
// 在 data 中定位 GGA 句子的起止位置
@@ -267,7 +279,7 @@ void quec_gnss_callback(char *data, int len)
g_gps_location.is_valid = (fix_status >= 1) ? 1 : 0;
} else {
g_gps_location.is_valid = 0;
printf("\n[GNSS Parse failed] FS=%d, %s\n",
GNSS_LOG("\n[GNSS Parse failed] FS=%d, %s\n",
fix_status,
(fix_status == 0) ? "No Fix (Invalid)" : "Format error");
}
@@ -321,7 +333,7 @@ void quec_gnss_callback(char *data, int len)
void set_gnss_mode(gnss_mode_t mode)
{
g_gnss_mode = mode;
printf("[GNSSMode] Set to %s Mode\n",
GNSS_LOG("[GNSSMode] Set to %s Mode\n",
(mode == GNSS_MODE_FAST) ? "Fast (30s)" : "Slow (5min)");
}
@@ -353,11 +365,11 @@ gps_location_info_t* get_gps_location_info(void)
void print_gps_location_info(void)
{
if (g_gps_location.is_valid) {
printf("[GPSinfo] Latitude: %.8f, Longitude: %.8f\n",
GNSS_LOG("[GPSinfo] Latitude: %.8f, Longitude: %.8f\n",
g_gps_location.latitude, g_gps_location.longitude);
printf("[GPSinfo] rawNMEA: %s\n", g_gps_location.raw_nmea);
GNSS_LOG("[GPSinfo] rawNMEA: %s\n", g_gps_location.raw_nmea);
} else {
printf("[GPSinfo] No valid position data\n");
GNSS_LOG("[GPSinfo] No valid position data\n");
}
}
@@ -395,26 +407,26 @@ static void quec_gnss_test(void * argv)
/* ========== FAST MODE ========== */
if (fast_mode_start_time == 0) {
fast_mode_start_time = hal_os_up_time_ms();
printf("[GNSS] Enter FAST mode\n");
GNSS_LOG("[GNSS] Enter FAST mode\n");
}
if (!gnss_is_open) {
gnss_is_open = 1;
app_set_app_operation(1);
ql_gnss_open(quec_gnss_callback);
printf("[GNSS] FAST: GPS OPEN\n");
GNSS_LOG("[GNSS] FAST: GPS OPEN\n");
}
// 5分钟超时切SLOW
if (hal_os_up_time_ms() - fast_mode_start_time >= GNSS_FAST_MODE_HOLD_TIME) {
printf("[GNSS] FAST 5min timeout -> SLOW\n");
GNSS_LOG("[GNSS] FAST 5min timeout -> SLOW\n");
g_gnss_mode = GNSS_MODE_SLOW;
fast_mode_start_time = 0;
slow_mode_start_time = 0;
slow_next_wake_time = 0;
gnss_is_open = 0;
printf("[GNSS] SLOW: GPS CLOSED ret:%d\n", ql_gnss_close());
GNSS_LOG("[GNSS] SLOW: GPS CLOSED ret:%d\n", ql_gnss_close());
}
}
else
@@ -426,12 +438,12 @@ static void quec_gnss_test(void * argv)
if (slow_mode_start_time == 0) {
slow_mode_start_time = now;
slow_next_wake_time = now; // 立即尝试第一次定位
printf("[GNSS] Enter SLOW mode (fixed 15min interval)\n");
GNSS_LOG("[GNSS] Enter SLOW mode (fixed 15min interval)\n");
}
// 检查是否到达唤醒时间
if (now < slow_next_wake_time) {
printf("[GNSS] SLOW: next wake in %d s\n",
GNSS_LOG("[GNSS] SLOW: next wake in %d s\n",
(slow_next_wake_time - now) / 1000);
}
else if (!gnss_is_open) {
@@ -444,7 +456,7 @@ static void quec_gnss_test(void * argv)
app_set_app_operation(1);
ql_gnss_open(quec_gnss_callback);
printf("[GNSS] SLOW: GPS OPEN, search start\n");
GNSS_LOG("[GNSS] SLOW: GPS OPEN, search start\n");
/* ---- 第一阶段:60秒卫星检测 ---- */
int sat_found = 0;
@@ -454,7 +466,7 @@ static void quec_gnss_test(void * argv)
elapsed++;
if (g_gnss_sat_count > 1) {
sat_found = 1;
printf("[GNSS] SLOW: Sat found! count=%d, after %ds\n",
GNSS_LOG("[GNSS] SLOW: Sat found! count=%d, after %ds\n",
g_gnss_sat_count, elapsed);
break;
}
@@ -462,10 +474,10 @@ static void quec_gnss_test(void * argv)
if (!sat_found) {
// 60秒无卫星 → 立即关闭,等待15分钟
printf("[GNSS] SLOW: No satellite in 30s, stop ret:%d.\n", ql_gnss_close());
GNSS_LOG("[GNSS] SLOW: No satellite in 30s, stop ret:%d.\n", ql_gnss_close());
gnss_is_open = 0;
slow_next_wake_time = search_start_time + (GNSS_SEARCH_INTERVAL * 1000);
printf("[GNSS] SLOW: GPS CLOSED, next search at +%ds\n", GNSS_SEARCH_INTERVAL);
GNSS_LOG("[GNSS] SLOW: GPS CLOSED, next search at +%ds\n", GNSS_SEARCH_INTERVAL);
continue;
}
@@ -478,17 +490,17 @@ static void quec_gnss_test(void * argv)
// 1.5分钟时刻检查卫星数
int sat_count_now = g_gnss_sat_count;
printf("[GNSS] SLOW: 1min check, satellite count = %d\n", sat_count_now);
GNSS_LOG("[GNSS] SLOW: 1min check, satellite count = %d\n", sat_count_now);
if (sat_count_now < GNSS_SAT_MIN_COUNT) {
printf("[GNSS] SLOW: Sat count < %d, stop search ret:%d.\n", GNSS_SAT_MIN_COUNT,ql_gnss_close());
GNSS_LOG("[GNSS] SLOW: Sat count < %d, stop search ret:%d.\n", GNSS_SAT_MIN_COUNT,ql_gnss_close());
gnss_is_open = 0;
slow_next_wake_time = search_start_time + (GNSS_SEARCH_INTERVAL * 1000);
printf("[GNSS] SLOW: GPS CLOSED, next search at +%ds\n", GNSS_SEARCH_INTERVAL);
GNSS_LOG("[GNSS] SLOW: GPS CLOSED, next search at +%ds\n", GNSS_SEARCH_INTERVAL);
continue;
}
/* ---- 第三阶段:卫星足够,继续定位,但总时间不超过3分钟 ---- */
printf("[GNSS] SLOW: Sat count OK, continue for fix (max %ds total)\n",
GNSS_LOG("[GNSS] SLOW: Sat count OK, continue for fix (max %ds total)\n",
GNSS_MAX_SEARCH_TIME);
// 继续等待直到总时间达到 GNSS_MAX_SEARCH_TIME (180秒)
@@ -498,28 +510,28 @@ static void quec_gnss_test(void * argv)
// 如果获得有效定位,打印提示(但继续等待至3分钟)
if (g_gps_location.is_valid) {
printf("[GNSS] SLOW: Fix obtained at %ds!\n", elapsed);
GNSS_LOG("[GNSS] SLOW: Fix obtained at %ds!\n", elapsed);
print_gps_location_info();
// 若希望在定位成功后立即关闭,可在此 break,但此处按“最长3分钟”处理
}
}
// 3分钟时间到,关闭GPS
printf("[GNSS] SLOW: Max search time reached, stop.\n");
GNSS_LOG("[GNSS] SLOW: Max search time reached, stop.\n");
if (g_gps_location.is_valid) {
printf("[GNSS] SLOW: Final valid position:\n");
GNSS_LOG("[GNSS] SLOW: Final valid position:\n");
print_gps_location_info();
} else {
printf("[GNSS] SLOW: No valid fix within 3min.\n");
GNSS_LOG("[GNSS] SLOW: No valid fix within 3min.\n");
}
printf("[GNSS] SLOW: GPS CLOSED ret:%d",ql_gnss_close());
GNSS_LOG("[GNSS] SLOW: GPS CLOSED ret:%d",ql_gnss_close());
gnss_is_open = 0;
g_gps_location.is_valid = 0; // 重置状态
// 设置下次唤醒时间为 15分钟 后
slow_next_wake_time = search_start_time + (GNSS_SEARCH_INTERVAL * 1000);
printf("[GNSS] SLOW: GPS CLOSED, next search at +%ds\n", GNSS_SEARCH_INTERVAL);
GNSS_LOG("[GNSS] SLOW: GPS CLOSED, next search at +%ds\n", GNSS_SEARCH_INTERVAL);
}
// else: GPS正在运行中(理论上不会到这里)
}
@@ -0,0 +1,6 @@
#ifndef BANBAN_FIELD_LOG_H
#define BANBAN_FIELD_LOG_H
/* Init once from the application startup task, before other app workers. */
int field_log_init(void);
int field_log_printf(const char *format, ...);
#endif
@@ -1,5 +1,7 @@
#ifndef BANBAN_NET_DIAG_H
#define BANBAN_NET_DIAG_H
int net_diag_is_enabled(void);
int net_diag_can_probe(void);
void net_diag_start(void);
void net_diag_event(const char *event, int code);
#endif
@@ -0,0 +1,11 @@
#ifndef BANBAN_NET_DIAG_PROBE_H
#define BANBAN_NET_DIAG_PROBE_H
/* Transport failures are distinct from local setup/resource failures. */
enum { PROBE_OK, PROBE_DNS, PROBE_LOCAL, PROBE_CONNECT, PROBE_TIMEOUT, PROBE_CANCELLED };
typedef struct {
int outcome, error, dns_result, tcp_started;
unsigned long dns_ms, tcp_ms;
char address[48];
} net_probe_result;
void net_diag_probe(const char *host, unsigned short port, net_probe_result *result);
#endif
@@ -0,0 +1,15 @@
#ifndef BANBAN_NET_DIAG_VOICE_H
#define BANBAN_NET_DIAG_VOICE_H
enum {
VOICE_SPEECH, VOICE_RECORD_STOP, VOICE_END_QUEUED, VOICE_END_ACK,
VOICE_TTS_START, VOICE_FIRST_URL, VOICE_DOWNLOAD_BEGIN,
VOICE_DOWNLOAD_DONE, VOICE_FIRST_PCM, VOICE_STAGE_COUNT
};
void net_diag_voice_begin(unsigned int round);
/* round=0 means the currently recording round (for the VAD task). */
void net_diag_voice_mark(unsigned int round, int stage);
void net_diag_voice_url(unsigned int round, const char *url);
unsigned int net_diag_audio_begin(const char *url);
void net_diag_audio_done(unsigned int round, int bytes, int result);
void net_diag_vad_stats(unsigned int frames, unsigned int speech, unsigned int peak);
#endif
@@ -26,7 +26,8 @@
#include "rws_string.h"
#include <netdb.h>
#include "sockets.h"
#include "ql_rtos.h"
#include "ql_rtos.h"
#include "net_diag.h"
#include <time.h>
#include "rws_handshake.h"
#ifdef LIBRWS_USING_MBED_TLS
@@ -212,13 +213,14 @@ static int rws_tls_send_all(_rws_socket *s, const unsigned char *data, size_t si
// need close socket on error
rws_bool rws_socket_send(_rws_socket *s, const void *data, const size_t data_size)
{
int sended = -1;
int sended = -1;
unsigned int diag_write_start = ql_rtos_get_systicks();
// rws_error_delete_clean(&s->error);
#ifdef LIBRWS_USING_MBED_TLS
if (s->scheme && strcmp(s->scheme, "wss") == 0)
{
LOG_D("[T:%.8d, L:%.5d, %*.*s] try send data with tls", ql_rtos_get_systicks(), __LINE__, DBG_FUNCTION_NUM, DBG_FUNCTION_NUM, __FUNCTION__);
/* Individual frame tracing overwhelms field logs; report slow writes below. */
sended = rws_tls_send_all(s, (const unsigned char *)data, data_size);
}
else
@@ -243,7 +245,13 @@ rws_bool rws_socket_send(_rws_socket *s, const void *data, const size_t data_siz
}
#endif
if (sended == (int)data_size)
if (net_diag_is_enabled()) {
unsigned long elapsed = (unsigned int)(ql_rtos_get_systicks() - diag_write_start) * 5UL;
if (elapsed >= 200UL || sended != (int)data_size)
printf("[现场诊断][%lu ms][底层发送] 请求=%lu 字节 已写=%d 耗时=%lu ms;未代表服务端已收到\r\n",
(unsigned long)ql_rtos_get_systicks() * 5UL, (unsigned long)data_size, sended, elapsed);
}
if (sended == (int)data_size)
{
rws_socket_error_cnt = 0;
rws_socket_error_staus = 0;
@@ -13,7 +13,8 @@
#include <netdb.h>
#include "sockets.h"
#include "mp3dec.h"
#include "test_mp3.h"
#include "test_mp3.h"
#include "net_diag_voice.h"
#define RLOGD printf
struct webclient_session *session = NULL;
@@ -24,6 +25,7 @@ static Mp3PlayConfigInfo mp3_configInfo;
static unsigned char mp3_player_status = 0;
QFILE *local_mp3_file_fd=NULL;
static unsigned int diag_player_round;
static int mp3_player_end_frame = 0;
static int mp3_player_last_result = 0;
@@ -252,7 +254,13 @@ int mp3_decoder_run(struct mp3_decoder* decoder)
current_sample_rate = decoder->frame_info.samprate;
}
ql_audio_track_write(track_hd, (char*)pcm_buff, 1152);
{
int pcm_ret = ql_audio_track_write(track_hd, (char*)pcm_buff, 1152);
if (diag_player_round && pcm_ret >= 0) {
net_diag_voice_mark(diag_player_round, VOICE_FIRST_PCM);
diag_player_round = 0;
}
}
}
return 0;
@@ -396,6 +404,7 @@ void aofei_mp3_player(char *file_name)
{
int ret = 0;
uint8_t *mp3_buff = NULL;
diag_player_round = 0;
mp3_player_last_result = -1;
if(file_name == NULL){
@@ -413,7 +422,9 @@ void aofei_mp3_player(char *file_name)
//直接下载到一个100k的buffer
if(strncmp(file_name, "http://", strlen("http://")) == 0 ||
strncmp(file_name, "https://", strlen("https://")) == 0){//播放url
diag_player_round = net_diag_audio_begin(file_name);
ret = url_mp3_data_fetch(file_name, mp3_buff, 100*1024);
net_diag_audio_done(diag_player_round, total_length, ret);
}else if(strstr(file_name, "test.mp3")){//播放本地音频
total_length = test_data_size;
memcpy(mp3_buff, test_data, total_length);
@@ -5,7 +5,8 @@
#include "hal_api.h"
#include "ql_nw.h"
#include "ql_data_call.h"
#include "ql_ping.h"
#include "ql_func.h"
#include "net_diag_probe.h"
#include "net_diag.h"
extern uint8_t get_sleep_mode(void);
@@ -16,10 +17,10 @@ extern u8 power_down_status;
static volatile int enabled;
static ql_task_t sample_task, probe_task;
static ql_ping_stat_t ping_result;
static int ping_seen, ping_error;
static int sinr_unavailable;
static const char *targets[] = {"223.5.5.5", "banban.api.talkingq.com"};
static const char *labels[] = {"参照节点", "业务域名"};
static const char *labels[] = {"参照DNS服务", "业务WSS入口"};
static const unsigned short ports[] = {53, 443};
static unsigned long attempts[2], received[2], streak[2], errors[2];
static const char *registration_name(int state)
@@ -51,6 +52,9 @@ static int paused(void)
return fota_start == 1 || power_down_status != 0 || get_sleep_mode();
}
int net_diag_is_enabled(void) { return enabled; }
int net_diag_can_probe(void) { return enabled && !paused(); }
static void sample_worker(void *arg)
{
int previous_reg = -1, previous_pdp = -1, previous_pause = -1;
@@ -84,10 +88,16 @@ static void sample_worker(void *arg)
pdp == 1 ? "已连接" : (pdp == -1 ? "读取失败" : "未连接"),
get_mqtt_connect_status() == 1 ? "已连接" : "未连接",
get_xiaoice_connected_status() ? "已连接" : "未连接");
if (sr == QL_NW_SUCCESS && (registered == 1 || registered == 5)) {
printf("[现场诊断][%lu ms][信号原始值] RSSI=%d RSRP=%d RSRQ=%d CQI=%d;单位/无效值以模组为准\r\n",
if (sr == QL_NW_SUCCESS && rr == QL_NW_SUCCESS &&
(reg.data_reg.rat == QL_NW_ACCESS_TECH_E_UTRAN ||
reg.data_reg.rat == QL_NW_ACCESS_TECH_E_UTRAN_CA)) {
char cqi[32];
if (signal.LTE_SignalStrength.cqi >= 0 && signal.LTE_SignalStrength.cqi <= 15)
snprintf(cqi, sizeof(cqi), "%d", signal.LTE_SignalStrength.cqi);
else snprintf(cqi, sizeof(cqi), "不可用(raw=%d)", signal.LTE_SignalStrength.cqi);
printf("[现场诊断][%lu ms][信号原始值] RSSI=%d RSRP=%d RSRQ=%d CQI=%s;单位/无效值以模组为准\r\n",
now_ms(), signal.LTE_SignalStrength.rssi, signal.LTE_SignalStrength.rsrp,
signal.LTE_SignalStrength.rsrq, signal.LTE_SignalStrength.cqi);
signal.LTE_SignalStrength.rsrq, cqi);
} else {
printf("[现场诊断][%lu ms][信号] 暂不可判读;接口返回=%d 注册=%d\r\n", now_ms(), sr, registered);
}
@@ -107,10 +117,20 @@ static void sample_worker(void *arg)
if ((unsigned long)(now_ms() - next_cell) >= 10000UL || next_cell == 0) {
int sinr, result;
next_cell = now_ms();
result = ql_nw_get_sinr(&sinr);
if (result == QL_NW_SUCCESS)
printf("[现场诊断][%lu ms][信噪比原始值] SINR=%d;编码以模组为准\r\n", now_ms(), sinr);
else net_diag_event("SINR读取失败", result);
if (!sinr_unavailable) {
if (!m_get_api_ptr || !m_get_api_ptr("ql_nw_get_sinr")) {
sinr_unavailable = 1;
net_diag_event("当前内核未导出SINR接口;本次开机跳过该项", 0);
} else {
result = ql_nw_get_sinr(&sinr);
if (result == QL_NW_SUCCESS && sinr != 255)
printf("[现场诊断][%lu ms][信噪比原始值] SINR=%d;编码以模组为准\r\n", now_ms(), sinr);
else if (result == 255 || (result == QL_NW_SUCCESS && sinr == 255)) {
sinr_unavailable = 1;
net_diag_event("SINR接口/数值不可用;本次开机跳过该项", result);
} else net_diag_event("SINR暂时读取失败", result);
}
}
memset(&cells, 0, sizeof(cells));
result = ql_nw_get_cell_info(&cells);
if (result == QL_NW_SUCCESS && cells.lte_info_valid) {
@@ -118,7 +138,7 @@ static void sample_worker(void *arg)
for (i = 0; i < cells.lte_info_num && i < QL_NW_CELL_MAX_NUM; ++i) {
QL_MCM_NW_LTE_CELL_INFO_T *c = &cells.lte_info[i];
if (c->flag != 0 || c->cid == 0) continue;
printf("[现场诊断][%lu ms][驻留小区] CID=%lu PCI=%lu EARFCN=%lu MCC=%lu MNC=%lu%s\r\n",
printf("[现场诊断][%lu ms][驻留小区] CID=%lu PCI=%lu EARFCN=%lu MCC原始编码=0x%lX MNC原始编码=0x%lX%s\r\n",
now_ms(), (unsigned long)c->cid, (unsigned long)c->pci,
(unsigned long)c->earfcn, (unsigned long)c->mcc, (unsigned long)c->mnc,
had_cell && (cell_id != c->cid || pci != c->pci) ? ";驻留小区发生变化" : "");
@@ -135,46 +155,34 @@ static void sample_worker(void *arg)
}
}
static void ping_callback(ql_ping_state_e state, int err, ql_ping_stat_t *stat)
{
(void)state;
if (err) ping_error = err;
if (stat) { ping_result = *stat; ping_seen = 1; }
}
static void probe_once(int index)
{
ql_ping_cfg_t cfg;
ql_ping_ctx_t *ctx;
int ret;
memset(&cfg, 0, sizeof(cfg));
cfg.pdpCid = 1; cfg.host = (char *)targets[index];
cfg.timeout = 2000; cfg.dataSize = 32; cfg.pingNum = 1;
ctx = ql_ping_init(cfg, ping_callback);
if (!ctx) { ++errors[index]; net_diag_event("探测初始化失败,下轮重试", index); return; }
memset(&ping_result, 0, sizeof(ping_result));
ping_seen = 0; ping_error = 0;
/* SDK declares ql_ping synchronous: callbacks complete before return. */
ret = ql_ping(ctx);
ql_ping_deinit(ctx);
if (ping_seen && ping_result.sentNum) {
attempts[index] += ping_result.sentNum;
received[index] += ping_result.rcvdNum;
if (ping_result.rcvdNum) {
streak[index] = 0;
printf("[现场诊断][%lu ms][探测成功] %s %s;往返=%lu ms\r\n",
now_ms(), labels[index], targets[index], (unsigned long)ping_result.rtt);
net_probe_result result;
printf("[现场诊断][%lu ms][探测开始] %s %s:%u;解析后TCP连接,连接超时3000ms\r\n",
now_ms(), labels[index], targets[index], (unsigned int)ports[index]);
net_diag_probe(targets[index], ports[index], &result);
if (result.outcome == PROBE_CANCELLED) {
net_diag_event("状态变化,已取消本轮新连接探测", 0); return;
}
if (result.outcome == PROBE_OK || result.outcome == PROBE_CONNECT || result.outcome == PROBE_TIMEOUT) {
++attempts[index];
if (result.outcome == PROBE_OK) {
++received[index]; streak[index] = 0;
printf("[现场诊断][%lu ms][探测成功] %s;IP=%s 解析=%lu ms TCP建连=%lu ms(非AI响应时间)\r\n",
now_ms(), labels[index], result.address, result.dns_ms, result.tcp_ms);
} else {
++streak[index];
printf("[现场诊断][%lu ms][探测未回应] %s;连续=%lu 次;可能超时或节点禁Ping,不能单独判定断网\r\n",
now_ms(), labels[index], streak[index]);
printf("[现场诊断][%lu ms][探测失败] %s;阶段=%s IP=%s 解析=%lu ms TCP等待=%lu ms 错误=%d 连续失败=%lu\r\n",
now_ms(), labels[index], result.outcome == PROBE_TIMEOUT ? "TCP超时" : "TCP拒绝/连接错误",
result.address, result.dns_ms, result.tcp_ms, result.error, streak[index]);
}
} else {
++errors[index];
printf("[现场诊断][%lu ms][探测执行异常] %s;返回=%d 回调错误=%d;无有效发包统计,不计入未回应率\r\n",
now_ms(), labels[index], ret, ping_error);
printf("[现场诊断][%lu ms][探测执行异常] 目标=%s 阶段=%s 错误=%d 解析返回=%d 解析=%lu ms;不计入TCP失败率\r\n",
now_ms(), labels[index], result.outcome == PROBE_DNS ? "域名解析" : "本地套接字/数据连接",
result.error, result.dns_result, result.dns_ms);
}
printf("[现场诊断][%lu ms][累计统计] %s;已发送=%lu 收到=%lu 未回应=%lu 未回应率=%lu%% 执行异常=%lu(本次开机累计)\r\n",
printf("[现场诊断][%lu ms][累计统计] %s;TCP有效尝试=%lu 成功=%lu 失败=%lu 失败率=%lu%% 执行异常=%lu;非业务丢包率\r\n",
now_ms(), labels[index], attempts[index], received[index], attempts[index] - received[index],
attempts[index] ? (attempts[index] - received[index]) * 100UL / attempts[index] : 0, errors[index]);
}
@@ -212,10 +220,10 @@ void net_diag_start(void)
ret = ql_rtos_task_create(&probe_task, 4096, 120, "net_probe", probe_worker, NULL);
if (ret != 0) { probe_task = NULL; printf("[现场诊断][错误] 探测任务创建失败=%d,请短按重试\r\n", ret); return; }
}
printf("\r\n[现场诊断] ===== 主动模式开启:重启退出 =====\r\n"
"[现场诊断] 时间=开机毫秒;串口请选择UTF-8并保存完整日志\r\n"
"[现场诊断] 信号/连接约2秒一条;小区约10秒一次;两节点依次探测后间隔10秒\r\n"
"[现场诊断] 每节点32字节,ICMP超时2000ms;业务域名探测可能另有解析耗时,不等于WSS/AI处理耗时\r\n"
"[现场诊断] 主动流量可能改变空闲态、耗电及故障表现;休眠/升级/关机时暂停新探测\r\n");
printf("\r\n[现场诊断] ===== 主动模式开启:重启退出 =====\r\n");
printf("[现场诊断] 时间=开机毫秒;UART与USB CDC同步,UTF-8;下一轮录音开始记录分段耗时\r\n");
printf("[现场诊断] 信号/连接约2秒一条;小区约10秒一次;两节点依次探测后间隔10秒\r\n");
printf("[现场诊断] 主动探测改用DNS/TCP,不依赖可选Ping接口;3000ms连接超时不含DNS解析耗时\r\n");
printf("[现场诊断] 主动流量可能改变空闲态、耗电及故障表现;休眠/升级/关机时暂停新探测\r\n");
enabled = 1;
}
@@ -0,0 +1,91 @@
/* Use the same PDP 1 socket path as the working WSS client. No raw ICMP
* or optional Ping API; select + SO_ERROR distinguish timeout/refusal/success. */
#include <string.h>
#include <stdio.h>
#include "ql_rtos.h"
#include "ql_data_call.h"
#include "sockets.h"
#include "netdb.h"
#include "hal_api.h"
#include "net_diag_probe.h"
#include "net_diag.h"
static unsigned long probe_ms(void) { return (unsigned long)hal_os_up_time_ms(); }
void net_diag_probe(const char *host, unsigned short port, net_probe_result *r)
{
struct addrinfo hints, *addresses = NULL, *address;
struct sockaddr_in local;
struct ql_data_call_info data;
char service[8];
unsigned long started;
int fd = -1, nonblocking = 1, ret, connect_error;
socklen_t error_size;
memset(r, 0, sizeof(*r)); r->outcome = PROBE_LOCAL;
memset(&hints, 0, sizeof(hints));
hints.ai_family = AF_INET; hints.ai_socktype = SOCK_STREAM;
snprintf(service, sizeof(service), "%u", (unsigned int)port);
if (!net_diag_can_probe()) { r->outcome = PROBE_CANCELLED; return; }
started = probe_ms();
ret = getaddrinfo_with_pcid(host, service, &hints, &addresses, 1);
r->dns_ms = probe_ms() - started; r->dns_result = ret;
if (ret != 0 || !addresses) {
r->outcome = PROBE_DNS; r->error = ret ? ret : -1;
if (addresses) freeaddrinfo(addresses);
return;
}
/* One address per round: avoid multiplying timeout/load by DNS answer count. */
for (address = addresses; address; address = address->ai_next)
if (address->ai_family == AF_INET && address->ai_addr &&
address->ai_addrlen >= sizeof(struct sockaddr_in)) break;
if (!address) { r->outcome = PROBE_DNS; r->error = -1; goto done; }
inet_ntop(AF_INET, &((struct sockaddr_in *)address->ai_addr)->sin_addr, r->address, sizeof(r->address));
if (!net_diag_can_probe()) { r->outcome = PROBE_CANCELLED; goto done; }
memset(&data, 0, sizeof(data));
ret = ql_get_data_call_info(1, 0, &data);
if (ret != 0 || data.v4.state != 1) { r->error = ret ? ret : -1; goto done; }
fd = socket(AF_INET, SOCK_STREAM, 0);
if (fd < 0) { r->error = ql_get_soc_errno(); goto done; }
if (ioctl(fd, FIONBIO, &nonblocking) != 0) { r->error = ql_get_soc_errno(); goto done; }
memset(&local, 0, sizeof(local));
local.sin_family = AF_INET; local.sin_port = htons(ql_soc_generate_port());
local.sin_addr = data.v4.addr.ip;
if (bind(fd, (struct sockaddr *)&local, sizeof(local)) != 0) { r->error = ql_get_soc_errno(); goto done; }
started = probe_ms();
ret = connect(fd, address->ai_addr, address->ai_addrlen);
r->tcp_started = 1;
if (ret == 0) { r->outcome = PROBE_OK; goto connected; }
connect_error = ql_get_soc_errno();
if (connect_error != EINPROGRESS && connect_error != EWOULDBLOCK && connect_error != EALREADY) {
r->outcome = PROBE_CONNECT; r->error = connect_error; goto connected;
}
/* Short slices allow OTA/sleep/shutdown to cancel within one select slice. */
r->outcome = PROBE_TIMEOUT;
while ((unsigned long)(probe_ms() - started) < 3000UL) {
fd_set writable, exceptional;
struct timeval wait;
int error = 0;
if (!net_diag_can_probe()) { r->outcome = PROBE_CANCELLED; break; }
FD_ZERO(&writable); FD_ZERO(&exceptional);
FD_SET(fd, &writable); FD_SET(fd, &exceptional);
{
unsigned long remaining = 3000UL - (unsigned long)(probe_ms() - started);
if (remaining > 3000UL) break;
wait.tv_sec = 0; wait.tv_usec = (remaining < 200UL ? remaining : 200UL) * 1000UL;
}
ret = select(fd + 1, NULL, &writable, &exceptional, &wait);
if (ret < 0) { r->outcome = PROBE_LOCAL; r->error = ql_get_soc_errno(); break; }
if (ret == 0) continue;
if (!FD_ISSET(fd, &writable) && !FD_ISSET(fd, &exceptional)) continue;
error_size = sizeof(error);
ret = getsockopt(fd, SOL_SOCKET, SO_ERROR, &error, &error_size);
if (ret != 0) { r->outcome = PROBE_LOCAL; r->error = ql_get_soc_errno(); }
else { r->error = error; r->outcome = error == 0 ? PROBE_OK : PROBE_CONNECT; }
break;
}
connected:
r->tcp_ms = probe_ms() - started;
done:
if (fd >= 0) close(fd);
freeaddrinfo(addresses);
}
@@ -0,0 +1,99 @@
#include <stdio.h>
#include <string.h>
#include "ql_rtos.h"
#include "hal_api.h"
#include "net_diag.h"
#include "net_diag_voice.h"
static struct {
unsigned int round, seen;
unsigned long started, stamp[VOICE_STAGE_COUNT];
char first_url[128];
} voice;
static const char *stages[] = {
"确认用户语音", "录音结束", "结束包已入发送队列(不代表服务端收到)",
"收到END_ACK", "收到TTS_START", "收到首段音频URL", "首段开始下载",
"首段下载完成", "首PCM提交播放接口(不等于实际出声)"
};
void net_diag_voice_begin(unsigned int round)
{
if (!net_diag_is_enabled()) return;
ql_rtos_task_sched_lock();
memset(&voice, 0, sizeof(voice));
voice.round = round; voice.started = (unsigned long)hal_os_up_time_ms();
ql_rtos_task_sched_unlock();
printf("[现场诊断][%lu ms][对话#%u] 开始录音;分段计时起点\r\n", voice.started, round);
}
void net_diag_voice_mark(unsigned int round, int stage)
{
unsigned long now, since_start, total = 0, tail = 0, server = 0, wait_url = 0, queue = 0, download = 0, pcm = 0;
unsigned int bit, seen;
if (!net_diag_is_enabled() || stage < 0 || stage >= VOICE_STAGE_COUNT) return;
now = (unsigned long)hal_os_up_time_ms(); bit = 1U << stage;
ql_rtos_task_sched_lock();
if (!voice.round || (round && round != voice.round) || (voice.seen & bit)) {
ql_rtos_task_sched_unlock(); return;
}
round = voice.round; voice.seen |= bit; voice.stamp[stage] = now;
since_start = now - voice.started; seen = voice.seen;
#define HAVE(s) ((seen & (1U << (s))) != 0)
if (stage == VOICE_FIRST_PCM && HAVE(VOICE_RECORD_STOP) && HAVE(VOICE_END_QUEUED) &&
HAVE(VOICE_TTS_START) && HAVE(VOICE_FIRST_URL) && HAVE(VOICE_DOWNLOAD_BEGIN) && HAVE(VOICE_DOWNLOAD_DONE)) {
total = now - voice.stamp[VOICE_RECORD_STOP];
tail = voice.stamp[VOICE_END_QUEUED] - voice.stamp[VOICE_RECORD_STOP];
server = voice.stamp[VOICE_TTS_START] - voice.stamp[VOICE_END_QUEUED];
wait_url = voice.stamp[VOICE_FIRST_URL] - voice.stamp[VOICE_TTS_START];
queue = voice.stamp[VOICE_DOWNLOAD_BEGIN] - voice.stamp[VOICE_FIRST_URL];
download = voice.stamp[VOICE_DOWNLOAD_DONE] - voice.stamp[VOICE_DOWNLOAD_BEGIN];
pcm = now - voice.stamp[VOICE_DOWNLOAD_DONE];
}
ql_rtos_task_sched_unlock();
printf("[现场诊断][%lu ms][对话#%u] %s;距录音开始=%lu ms\r\n", now, round, stages[stage], since_start);
/* Nonstandard server ordering is printed as events, never unsigned deltas. */
if (total && tail <= total && server <= total && wait_url <= total && queue <= total && download <= total && pcm <= total)
printf("[现场诊断][%lu ms][对话#%u][耗时汇总] 录音结束到首PCM=%lu ms;收尾入队=%lu 等TTS通知=%lu 等首URL=%lu 播放队列=%lu 下载=%lu 解码提交=%lu(均ms;含网络与服务端处理,尚未分离)\r\n",
now, round, total, tail, server, wait_url, queue, download, pcm);
#undef HAVE
}
void net_diag_voice_url(unsigned int round, const char *url)
{
int accepted = 0;
if (!net_diag_is_enabled() || !url) return;
ql_rtos_task_sched_lock();
if (round == voice.round && !voice.first_url[0] && strlen(url) < sizeof(voice.first_url)) {
strcpy(voice.first_url, url); accepted = 1;
}
ql_rtos_task_sched_unlock();
if (accepted) net_diag_voice_mark(round, VOICE_FIRST_URL);
}
unsigned int net_diag_audio_begin(const char *url)
{
unsigned int round = 0;
if (!net_diag_is_enabled() || !url) return 0;
ql_rtos_task_sched_lock();
if (voice.first_url[0] && strcmp(url, voice.first_url) == 0) round = voice.round;
ql_rtos_task_sched_unlock();
if (round) net_diag_voice_mark(round, VOICE_DOWNLOAD_BEGIN);
return round;
}
void net_diag_audio_done(unsigned int round, int bytes, int result)
{
if (!round || !net_diag_is_enabled()) return;
printf("[现场诊断][%lu ms][对话#%u][首段下载] 字节=%d 返回=%d\r\n",
(unsigned long)hal_os_up_time_ms(), round, bytes, result);
if (result >= 0 && bytes > 0) net_diag_voice_mark(round, VOICE_DOWNLOAD_DONE);
}
void net_diag_vad_stats(unsigned int frames, unsigned int speech, unsigned int peak)
{
unsigned int round;
if (!net_diag_is_enabled()) return;
ql_rtos_task_sched_lock(); round = voice.round; ql_rtos_task_sched_unlock();
printf("[现场诊断][%lu ms][对话#%u][语音检测] 总帧=%u 活跃录音帧=%u 首次确认前连续语音峰值=%u/20;帧长20ms\r\n",
(unsigned long)hal_os_up_time_ms(), round, frames, speech, peak);
}
@@ -2,7 +2,9 @@
#include <string.h>
#include <librws.h>
#include "cJSON.h"
#include "app_inc.h"
#include "app_inc.h"
#include "net_diag.h"
#include "net_diag_voice.h"
#include "ql_rtos.h"
#include "ql_application.h"
#include "ql_audio.h"
@@ -27,6 +29,7 @@
// 在文件顶部添加状态变量
static uint8_t speech_detect_peak = 0;
static uint8_t speech_detect_count = 0; // 连续语音帧计数器
#define SPEECH_DETECT_THRESHOLD 20 // 连续检测到语音的阈值
#define PLAYBACK_VAD_GUARD_MS 800 // 屏蔽播放结束后的尾音和音频通路切换噪声
@@ -209,16 +212,19 @@ int vad_audio_start(void) {
vad_audio_init();
frame_count = 0;
speech_frame_count = 0;
speech_frame_count = 0;
speech_detect_peak = 0;
printf("Audio recording started with FVAD");
printf("Audio recording started with FVAD\r\n");
return 0;
}
// 停止录音
int vad_audio_stop(void) {
printf("Audio recording stopped, frames: %d, speech: %d",
net_diag_voice_mark(0, VOICE_RECORD_STOP);
net_diag_vad_stats(frame_count, speech_frame_count, speech_detect_peak);
printf("Audio recording stopped, frames: %d, speech: %d\r\n",
frame_count, speech_frame_count);
vad_audio_deinit();
@@ -416,7 +422,7 @@ void vad_audio_process(void) {
}
if (current_vad == VAD_STATE_ACTIVE) {
speech_detect_count++;
printf("Speech frame count: %d\r\n", speech_detect_count);
if (speech_detect_count > speech_detect_peak) speech_detect_peak = speech_detect_count;
// 连续达到阈值才确认检测到声音
if (speech_detect_count >= SPEECH_DETECT_THRESHOLD) {
@@ -447,12 +453,13 @@ void vad_audio_process(void) {
vad_preroll_count = 0;
}
set_state(STATE_RECORDING_ACTIVE);
printf("First speech confirmed by FVAD (continuous %d frames)", SPEECH_DETECT_THRESHOLD);
net_diag_voice_mark(0, VOICE_SPEECH);
printf("First speech confirmed by FVAD (continuous %d frames)\r\n", SPEECH_DETECT_THRESHOLD);
}
} else {
// 出现静音帧,计数器清零
if (speech_detect_count > 0) {
printf("Speech interrupted, reset count\r\n");
/* Per-frame chatter replaced by the final peak statistic. */
speech_detect_count = 0;
}
@@ -5,6 +5,7 @@
#include "app_inc.h"
#include "talkingq_wss.h"
#include "net_diag.h"
#include "net_diag_voice.h"
#include "ql_rtos.h"
#include "ql_application.h"
#include "ql_audio.h"
@@ -102,6 +103,7 @@ void conversation_record_begin(void)
conversation_lock();
if (!g_mic_working) conversation_begin_round();
g_mic_working = 1;
net_diag_voice_begin(conversation_round);
conversation_unlock();
}
@@ -118,7 +120,8 @@ void conversation_no_speech(void)
{
conversation_lock();
wait_round = conversation_round;
conversation_wait = WAIT_NO_SPEECH;
conversation_wait = WAIT_NO_SPEECH;
net_diag_event("VAD未确认用户语音,本轮等待超时提示;不是AI回复耗时", 0);
conversation_unlock();
}
static volatile int player_mq_couter = 0;
@@ -504,7 +507,8 @@ static void onmessage_text_locked(rws_socket socket, const char *text, const uns
char url_buff_bak[128]={0x00};
if(strstr(text, "_START")){
printf("TTS_START\r\n");
printf("TTS_START\r\n");
if (active_session_valid) net_diag_voice_mark(active_session_round, VOICE_TTS_START);
//语音交互音频开始
tts_playing = 1;
tts_play_status = 1;
@@ -514,7 +518,9 @@ static void onmessage_text_locked(rws_socket socket, const char *text, const uns
//语音交互音频结束
// tts_playing = 0;
tts_play_status = 2;
}else if(strstr(text, "END_ACK")||strstr(text, "START_ACK")){
}else if(strstr(text, "END_ACK")||strstr(text, "START_ACK")){
if (active_session_valid && strstr(text, "END_ACK"))
net_diag_voice_mark(active_session_round, VOICE_END_ACK);
}else{//TTS_URL:audio_url
char *ret_str1 = strstr(text, "https://");
@@ -584,6 +590,9 @@ static void onmessage_text_locked(rws_socket socket, const char *text, const uns
/* 对话超时提示使用 type=3,云端播放失败时由播放线程切换到本地兜底。 */
if (active_session_valid && !is_tts_error_url && !is_message_ok_url &&
strstr(url_buff_bak, sessionId) != NULL)
net_diag_voice_url(active_session_round, url_buff);
add_file_to_play_player_queue(url_buff, is_tts_error_url == 1 ? 3 : 0);
}
}
@@ -1056,7 +1065,9 @@ void sendEndPacket(void) {
conversation_lock();
if (sender_round == conversation_round) {
int end_ret = sendRecordingAudioPacket(&endPacket);
int end_ret = sendRecordingAudioPacket(&endPacket);
if (end_ret == 0) net_diag_voice_mark(sender_round, VOICE_END_QUEUED);
else net_diag_event("结束包入队失败;不能按等待AI回复解释", end_ret);
if (!round_response_received) {
wait_round = sender_round;
reply_wait_start_ms = hal_os_up_time_ms();
+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')