3 Commits
22 changed files with 1379 additions and 100 deletions
+89
View File
@@ -0,0 +1,89 @@
# 现场网络诊断固件
GPIO84 原报警/留言按键改为诊断键。释放时按住超过 50 ms、少于 3 秒,开启主动模式。超过或等于 3 秒不执行动作。重复短按只打印已开启,不创建重复任务;重启恢复关闭。该按键不再报警、留言、播放提示音或停止音频。
设备处于定时休眠、OTA 或关机流程时不响应开启。开启之后遇到这些状态,暂停采样和新探测。TCP 等待期间每不超过 200 ms 检查一次取消;SDK 的域名解析是同步调用,必须等待返回才能取消,未修改全局 DNS 超时设置。原有休眠和定位流程保留。
## Mac 直接读取 USB 日志
刷入这版固件后,用支持数据传输的 USB 线连接设备与 Mac。固件通过 `QL_USB_CDC_PORT` 输出 UTF-8 纯文本,并保留 UART 输出。没有使用基带 DIAG 二进制日志端口。
在仓库根目录运行下面命令。读取工具只用 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 的固定缓冲池中,通过只传递 4 字节缓冲区索引的有界队列交给独立低优先级 USB 转发任务,避免整条日志超过 SDK 的队列消息尺寸限制。业务日志入队使用零等待;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=-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;非业务丢包率
```
## 一轮对话的分段计时
诊断模式开启后的下一轮录音开始计时,依次记录:开始录音、确认用户语音、录音结束、结束包入发送队列、收到 END_ACK(服务器实际返回时)、收到 TTS_START、收到首段音频 URL、首段开始下载/下载完成、首 PCM 交给播放接口。
首段根据通过当前 session 校验的完整 URL 匹配,只跟踪第一段正常回复,排除不匹配的本地提示、留言确认和错误提示;后续录音轮次开始后不接受上一轮的延迟阶段事件。VAD 统计显示总帧、活跃录音帧,以及首次确认前连续语音峰值,例如 `13/20`,帮助判断为什么一直没有确认开口。没有根据一份日志直接降低 VAD 阈值。
阶段齐全且顺序合理时,在首 PCM 提交后输出汇总:
```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 或为该目标添加同名编译宏。
## 验证与上板
```sh
python3 tools/test_net_diag.py
python3 tools/test_conversation_timeout.py
python3 tools/test_read_usb_log.py
```
诊断测试编译执行真实诊断源码,模拟 SDK 的成功、DNS 失败、连接拒绝、超时、初始化失败、取消和资源释放;验证统计、SINR/CQI 不可用处理、USB 零等待队列/部分写/断线重开、对话分段与旧轮次隔离、GNSS 静默仍保留副作用。已有对话回归测试验证超时、打断和角色提示流程。Mac 读取工具使用临时伪终端验证 UTF-8 分包、过滤、Ctrl+C 与末行保存,不打开物理串口。
宿主测试只在临时头文件副本/编译选项中处理 SDK 的 32 位 size_t 和 timeval 与 Mac 的差异,不修改 SDK。测试不能代替完整 ARM 固件构建与硬件验收。正式构建沿用项目 Windows 工具链;当前应用 Makefile 使用 arm-none-eabi-gcc。
上板后确认:直接 USB 枚举与端口识别;UTF-8 日志;晚插线、拔插、拥塞和 USB 丢弃计数;离线仍可开启诊断;两个目标的 DNS/TCP 结果;对话时音频正常且汇总匹配;OTA/定时休眠暂停;重启关闭。核实模组信号单位、实际内存余量和 USB SDK 的返回语义。
@@ -67,6 +67,10 @@ SRC_FILES+= \
mp3_player.c \
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 \
@@ -113,7 +117,7 @@ INC_DIRS+= \
#-------------------------------------------------------------------------------
# Configure compile flag for C
#-------------------------------------------------------------------------------
U_FLAGS:=
U_FLAGS:= -Dprintf=field_log_printf
#-------------------------------------------------------------------------------
# Configure compile flag for C++
@@ -27,6 +27,7 @@
#include "ql_uart.h"
#include "ql_application.h"
#include "ql_log.h"
#include "field_log.h"
#include "hal_api.h"
#include "nfc.h"
#include "ota_baseline.h"
@@ -78,34 +79,7 @@ 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,154 @@
/* 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]; };
/* Queue only a 32-bit slot index; full records exceed this SDK's queue
* message-size limit. Slots remain owned until the worker finishes writing. */
static struct log_record log_pool[LOG_QUEUE_DEPTH];
static unsigned char slot_used[LOG_QUEUE_DEPTH];
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();
}
static unsigned int reserve_slot(void)
{
unsigned int slot;
ql_rtos_task_sched_lock();
for (slot = 0; slot < LOG_QUEUE_DEPTH; ++slot) {
if (!slot_used[slot]) { slot_used[slot] = 1; break; }
}
ql_rtos_task_sched_unlock();
return slot;
}
static void release_slot(unsigned int slot)
{
ql_rtos_task_sched_lock();
slot_used[slot] = 0;
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) {
unsigned int slot = reserve_slot();
if (slot == LOG_QUEUE_DEPTH) {
note_drop();
} else {
log_pool[slot] = record;
if (ql_rtos_queue_release(usb_queue, sizeof(slot), (u8 *)&slot, QL_NO_WAIT) != 0) {
release_slot(slot);
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)
{
unsigned int slot;
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 *)&slot, sizeof(slot), 1000) == 0 &&
slot < LOG_QUEUE_DEPTH) {
int result = usb_write_all(log_pool[slot].text, log_pool[slot].length);
release_slot(slot);
if (result != 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(unsigned int), 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正在运行中(理论上不会到这里)
}
@@ -4,6 +4,7 @@
#include "ql_gpio.h"
#include "ql_uart.h"
#include "app_inc.h"
#include "net_diag.h"
#define KEY_PIN_WAKEUP_MIC GPIO_PIN_NO_53
#define KEY_PIN_ALARM GPIO_PIN_NO_84
@@ -209,29 +210,19 @@ static void key_thread_entry(void *parameter)
if(value!=PIN_LEVEL_LOW){//up
key_press_alarm_status = 0;
if(ql_rtos_get_systicks() - key_press_alarm_time > KEY_LONG_PRESS_3S){
build_alarm();
}
else if(ql_rtos_get_systicks() - key_press_alarm_time > KEY_WAIT_TIME){
if(!vad_audio_is_recording()&&key_start_leave==0&&get_mqtt_connect_status()==1){
key_start_leave = 1;
build_leave_to_parent();
}else{
key_start_leave = 0;
app_play_tips_file(2);
}
}
/* GPIO84 is diagnostic-only; long holds have no action. */
{
u32 held = (u32)(ql_rtos_get_systicks() - key_press_alarm_time);
if (held > KEY_WAIT_TIME && held < KEY_LONG_PRESS_3S)
net_diag_start();
}
key_press_alarm_time = ql_rtos_get_systicks();
key_press_alarm_counter = 0;
pre_key_press_mic_counter = 0;
pre_key_press_alarm_counter = 0;
}else{ //一直按着
if(key_press_alarm_status==0){
key_press_alarm_status = 1;//表示已经按下了
//先停掉音频播放线程
if(key_start_leave==0&&!vad_audio_is_recording()){
stop_player_func();
}
}
}
}
@@ -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
@@ -0,0 +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
@@ -27,6 +27,7 @@
#include <netdb.h>
#include "sockets.h"
#include "ql_rtos.h"
#include "net_diag.h"
#include <time.h>
#include "rws_handshake.h"
#ifdef LIBRWS_USING_MBED_TLS
@@ -213,12 +214,13 @@ static int rws_tls_send_all(_rws_socket *s, const unsigned char *data, size_t si
rws_bool rws_socket_send(_rws_socket *s, const void *data, const size_t data_size)
{
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,6 +245,12 @@ rws_bool rws_socket_send(_rws_socket *s, const void *data, const size_t data_siz
}
#endif
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;
@@ -14,6 +14,7 @@
#include "sockets.h"
#include "mp3dec.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);
@@ -0,0 +1,229 @@
/* On-demand field diagnostics. No PDP reconfiguration or recovery actions. */
#include <stdio.h>
#include <string.h>
#include "app_inc.h"
#include "hal_api.h"
#include "ql_nw.h"
#include "ql_data_call.h"
#include "ql_func.h"
#include "net_diag_probe.h"
#include "net_diag.h"
extern uint8_t get_sleep_mode(void);
extern int get_mqtt_connect_status(void);
extern unsigned char get_xiaoice_connected_status(void);
extern int fota_start;
extern u8 power_down_status;
static volatile int enabled;
static ql_task_t sample_task, probe_task;
static int sinr_unavailable;
static const char *targets[] = {"223.5.5.5", "banban.api.talkingq.com"};
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)
{
switch (state) {
case 0: return "未注册";
case 1: return "已注册本地网络";
case 2: return "正在搜索网络";
case 3: return "注册被拒绝";
case 5: return "已注册漫游网络";
case -1: return "读取失败";
default: return "未知状态";
}
}
static unsigned long now_ms(void)
{
return (unsigned long)hal_os_up_time_ms();
}
void net_diag_event(const char *event, int code)
{
if (enabled)
printf("[现场诊断][%lu ms][事件] %s;状态码=%d\r\n", now_ms(), event, code);
}
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;
unsigned long down_since = 0, next_cell = 0;
int down = 0, had_cell = 0;
unsigned int cell_id = 0, pci = 0;
static QL_NW_CELL_INFO_T cells;
(void)arg;
for (;;) {
if (enabled) {
int hold = paused();
if (hold != previous_pause) {
net_diag_event(hold ? "休眠/升级/关机中,暂停采样与新探测" : "诊断采样运行中", hold);
previous_pause = hold;
}
if (!hold) {
QL_NW_SIGNAL_STRENGTH_INFO_T signal;
QL_NW_REG_STATUS_INFO_T reg;
struct ql_data_call_info data;
int sr, rr, dr, registered, pdp;
memset(&signal, 0, sizeof(signal));
memset(&reg, 0, sizeof(reg));
memset(&data, 0, sizeof(data));
sr = ql_nw_get_signal_strength(&signal);
rr = ql_nw_get_reg_status(&reg);
dr = ql_get_data_call_info(1, 0, &data);
registered = rr == QL_NW_SUCCESS ? (int)reg.data_reg.state : -1;
pdp = dr == 0 ? data.v4.state : -1;
printf("[现场诊断][%lu ms][连接] 注册=%s(%d) 数据连接=%s MQTT=%s WSS=%s\r\n",
now_ms(), registration_name(registered), registered,
pdp == 1 ? "已连接" : (pdp == -1 ? "读取失败" : "未连接"),
get_mqtt_connect_status() == 1 ? "已连接" : "未连接",
get_xiaoice_connected_status() ? "已连接" : "未连接");
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, cqi);
} else {
printf("[现场诊断][%lu ms][信号] 暂不可判读;接口返回=%d 注册=%d\r\n", now_ms(), sr, registered);
}
if (registered != previous_reg || pdp != previous_pdp) {
net_diag_event("注册或数据连接状态变化(详见连接行)", registered);
previous_reg = registered; previous_pdp = pdp;
}
/* Unknown readings never become a fabricated disconnect/recovery. */
if (rr == QL_NW_SUCCESS && dr == 0) {
int online = (registered == 1 || registered == 5) && pdp == 1;
if (!online && !down) { down = 1; down_since = now_ms(); net_diag_event("网络不可用,开始计时", 0); }
if (online && down) {
printf("[现场诊断][%lu ms][恢复] 本次网络不可用观测时长=%lu ms(含采样误差)\r\n", now_ms(), now_ms() - down_since);
down = 0;
}
}
if ((unsigned long)(now_ms() - next_cell) >= 10000UL || next_cell == 0) {
int sinr, result;
next_cell = now_ms();
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) {
int i;
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原始编码=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) ? ";驻留小区发生变化" : "");
cell_id = c->cid; pci = c->pci; had_cell = 1;
break;
}
if (i == cells.lte_info_num || i == QL_NW_CELL_MAX_NUM)
net_diag_event("无有效LTE驻留小区记录", 0);
} else net_diag_event("LTE小区信息不可用", result);
}
}
}
ql_rtos_task_sleep_ms(2000);
}
}
static void probe_once(int index)
{
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;阶段=%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 阶段=%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;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]);
}
static void probe_worker(void *arg)
{
(void)arg;
for (;;) {
if (enabled && !paused()) {
int i;
for (i = 0; i < 2; ++i) {
struct ql_data_call_info data;
memset(&data, 0, sizeof(data));
if (paused()) break;
if (ql_get_data_call_info(1, 0, &data) != 0 || data.v4.state != 1) {
net_diag_event("数据连接未就绪,跳过主动探测", 0); break;
}
probe_once(i);
}
}
ql_rtos_task_sleep_ms(10000);
}
}
/* Called only by the serialized key worker, never from the GPIO interrupt. */
void net_diag_start(void)
{
int ret;
if (enabled) { net_diag_event("主动模式已开启,继续采集;重启退出", 1); return; }
if (!sample_task) {
ret = ql_rtos_task_create(&sample_task, 4096, 120, "net_sample", sample_worker, NULL);
if (ret != 0) { sample_task = NULL; printf("[现场诊断][错误] 采样任务创建失败=%d,请短按重试\r\n", ret); return; }
}
if (!probe_task) {
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");
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);
}
@@ -18,6 +18,7 @@
#include "ql_ping.h"
#include "ql_nw.h"
#include "net_diag.h"
#include "ql_atcmd.h"
#include "http_downfile.h"
#include "ota_post_update.h"
@@ -97,6 +98,7 @@ void app_data_recall(void)
static void ql_data_call_cb(int profile_idx, int nw_status)
{
net_diag_event("数据连接回调", nw_status);
printf("[APP]ql_data_call_cb: profile(%d) status(%d)\r\n", profile_idx, nw_status);
if(nw_status==0){
ql_rtos_semaphore_release(net_mgt_datacall_sem);
@@ -3,6 +3,8 @@
#include <librws.h>
#include "cJSON.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 // 屏蔽播放结束后的尾音和音频通路切换噪声
@@ -210,15 +213,18 @@ int vad_audio_start(void) {
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;
}
@@ -4,6 +4,8 @@
#include "cJSON.h"
#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"
@@ -101,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,6 +121,7 @@ void conversation_no_speech(void)
conversation_lock();
wait_round = conversation_round;
conversation_wait = WAIT_NO_SPEECH;
net_diag_event("VAD未确认用户语音,本轮等待超时提示;不是AI回复耗时", 0);
conversation_unlock();
}
static volatile int player_mq_couter = 0;
@@ -222,6 +226,7 @@ static void onopen(rws_socket socket)
if (socket != app->socket || !app->init) return;
wss_connect_counter=0;
net_diag_event("WSS连接建立(尚不代表鉴权或AI回复完成)", 0);
LOG_D("websocket connected. ");
if(ws_connect_sem){
ql_rtos_semaphore_release(ws_connect_sem);
@@ -245,6 +250,7 @@ static void onclose(rws_socket socket)
}
if (socket != app->socket) return;
net_diag_event("WSS连接断开", error_code);
xiaoice_wss_connect_status = 0;
app->socket = NULL;
/* The worker owns and releases this socket after the callback. */
@@ -502,6 +508,7 @@ static void onmessage_text_locked(rws_socket socket, const char *text, const uns
if(strstr(text, "_START")){
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;
@@ -512,6 +519,8 @@ 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")){
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://");
@@ -581,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);
}
}
@@ -1054,6 +1066,8 @@ void sendEndPacket(void) {
conversation_lock();
if (sender_round == conversation_round) {
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)
+360
View File
@@ -0,0 +1,360 @@
"""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'
SAMPLE = r'''
#include <assert.h>
#include <setjmp.h>
#include "net_diag.c"
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;
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;
if (fail_create == creates) return -9;
*task = (void *)1; return 0;
}
void ql_rtos_task_sleep_ms(u32 ms) {
clock_ms += ms;
if (step_limit > 1) {
++sample_steps;
if (sample_steps == 1) pdp = 0;
if (sample_steps == 2) pdp = 1;
if (sample_steps < step_limit) return;
}
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; 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; 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) { (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;
}
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 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 = 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: diagnostic activation, statistics, invalid metrics, recovery and pause");
return 0;
}
'''
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) {
/* Model the target rejecting oversized messages (old code used 516). */
if (size > 64) return -6743;
assert(size == 4 && 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) {
unsigned int slot = *(unsigned int *)record;
assert(q && size == 4 && timeout == 0 && slot < LOG_QUEUE_DEPTH && slot_used[slot]);
last_record = log_pool[slot];
if (queue_mode == 2) return -1;
if (queue_mode != 3) release_slot(slot); /* Simulate a completed consumer. */
return 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);
unsigned int slot = reserve_slot();
assert(size == 4 && slot < LOG_QUEUE_DEPTH);
strcpy(log_pool[slot].text, "test");
log_pool[slot].length = 4;
*(unsigned int *)record = slot; 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);
{
unsigned int i;
ql_queue_t oversized;
assert(ql_rtos_queue_create(&oversized, 516, 16) == -6743);
for (i = 0; i < LOG_QUEUE_DEPTH; ++i) assert(!slot_used[i]);
queue_mode = 3;
for (i = 0; i < LOG_QUEUE_DEPTH; ++i) field_log_printf("held %u", i);
for (i = 0; i < LOG_QUEUE_DEPTH; ++i) {
char expected[32];
snprintf(expected, sizeof(expected), "held %u", i);
assert(slot_used[i] && strcmp(log_pool[i].text, expected) == 0);
}
field_log_printf("pool full"); assert(dropped == 2);
for (i = 0; i < LOG_QUEUE_DEPTH; ++i) release_slot(i);
queue_mode = 0;
field_log_printf("recovered"); assert(strcmp(last_record.text, "recovered") == 0);
}
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 == 3);
assert(!slot_used[0]);
puts("PASS: USB 4-byte queue, SDK size rejection, pool exhaustion/recovery, init rollback, no-wait, truncation, 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')