feat(test): BLE test mode for validating alerts, CAN speed, and buzzer

Add TEST_ENTER/TEST_EXIT commands via BLE to enter/exit test mode.
In test mode, inference results are suppressed; testers can directly
trigger BLE JSON (TEST_BLE *), CAN+Buzzer actions (TEST_CAN *),
or run sequenced full-coverage tests (TEST_BLE_ALL, TEST_CAN_ALL,
TEST_ALL). Auto-exits after 60s idle. Routes unrecognised BLE
commands through new bt_uart_set_extra_cmd_cb() hook.
This commit is contained in:
miketsai 2026-06-13 18:12:05 +08:00
parent 286291fcfd
commit 953b7a348a
5 changed files with 292 additions and 0 deletions

View File

@ -43,6 +43,10 @@ void bt_uart_set_rent_status(int status);
typedef int (*bt_intervention_fn)(void);
void bt_uart_set_intervention_cb(bt_intervention_fn fn);
/* Extra command callback — called for unrecognised BLE commands. */
typedef void (*bt_extra_cmd_fn)(const char *cmd);
void bt_uart_set_extra_cmd_cb(bt_extra_cmd_fn fn);
/* Reset handshake and notify peer; DX-BT24 is passthrough — no HW disconnect API. */
void bt_uart_disconnect(void);

View File

@ -40,4 +40,7 @@ void event_recorder_provide_frame(void);
* level=0 type NULL */
void fire_collision_warning(int level, const char *type);
/* Test mode: 1=test (inference results skipped), 0=normal. */
extern volatile int g_test_mode;
#endif /* EVENT_RECORDER_H */

View File

@ -144,6 +144,7 @@ static void print_yolo_result(kp_app_yolo_result_t *yolo_data)
yolo_data->boxes[i].x2, yolo_data->boxes[i].y2, yolo_data->boxes[i].score, yolo_data->boxes[i].class_num);
}
#else
if (g_test_mode) return;
/* ── [YOLO] collision_warning2.2.5)─────────────────────────────
* vehicle(class=2) BLE notify
* /debounce 2
@ -504,6 +505,7 @@ int app_header_recv_inference(uint32_t buf_addr, bool *bl_run_next_inference)
printf("[%s] STDC inference error %u\n", __func__, header_stamp->status_code);
return header_stamp->status_code;
}
if (g_test_mode) return KP_SUCCESS;
stdc_inf_result_t *stdc_res = (stdc_inf_result_t *)header_stamp;
stdc_analysis_t *ana = &stdc_res->stdc_result.analysis;

View File

@ -57,6 +57,7 @@ static pthread_mutex_t s_rent_mtx = PTHREAD_MUTEX_INITIALIZER;
/* ── Cart-control intervention callback ──────────────────────────────────── */
static bt_intervention_fn s_intervention_cb = NULL;
static bt_extra_cmd_fn s_extra_cmd_cb = NULL;
/* ── BLE Handshake context ───────────────────────────────────────────────── */
static handshake_ctx_t s_handshake_ctx;
@ -150,6 +151,8 @@ static void handle_command(const char *json)
pthread_mutex_unlock(&s_rent_mtx);
printf("[BT CMD] rent processed\n");
reply_rent_status();
} else if (s_extra_cmd_cb) {
s_extra_cmd_cb(json);
} else {
printf("[BT RX] unrecognised command\n");
}
@ -530,3 +533,8 @@ void bt_uart_set_intervention_cb(bt_intervention_fn fn)
{
s_intervention_cb = fn;
}
void bt_uart_set_extra_cmd_cb(bt_extra_cmd_fn fn)
{
s_extra_cmd_cb = fn;
}

View File

@ -32,6 +32,7 @@
#include <arpa/inet.h>
#include <dirent.h>
#include <errno.h>
#include <ctype.h>
#include <MemBroker/mem_broker.h>
#include <vmf/video_snapshot_mechanism.h>
@ -186,6 +187,13 @@ static int g_last_left_alert = 0;
static int g_last_right_alert = 0;
static int g_last_collision_warning = 0;
volatile int g_test_mode = 0;
static time_t s_test_mode_entered = 0;
static volatile int s_test_all_running = 0;
#define TEST_MODE_TIMEOUT_SEC 60
#define TEST_OBS_DEFAULT_SEC 10
/* *URL / network helpers (Channel B only)**/
static void parse_url_into(const char *url,
@ -422,6 +430,264 @@ static void fire_json_async(const char *event_id, const char *type, int level)
bt_uart_send_json(json);
}
/* ── Test mode helpers ───────────────────────────────────────────────────── */
static void str_toupper(char *s)
{
for (; *s; s++) *s = (char)toupper((unsigned char)*s);
}
static void test_reply(const char *msg)
{
bt_uart_send_json(msg);
printf("[TEST] reply: %s\n", msg);
}
static void test_mode_exit_internal(const char *reason)
{
g_test_mode = 0;
s_test_mode_entered = 0;
can_ctrl_cmd_t ctrl = { .speed = SPEED_LIMIT_NORMAL,
.led_gpio = 0, .led_enable = 0,
.keepalive_interval_ms = 0 };
can_bus_send_control_cmd(&ctrl);
buzzer_set_pattern(BUZZER_PATTERN_OFF);
printf("[TEST] exit test mode (%s)\n", reason);
test_reply("TEST_EXIT_OK");
}
static void test_ble_violation(int level)
{
char event_id[24], date[32];
snprintf(event_id, sizeof(event_id), "TEST%ld", (long)time(NULL));
now_iso(date, sizeof(date));
char json[192];
snprintf(json, sizeof(json),
"{\"response_type\":\"violation\","
"\"content\":{\"id\":\"%s\",\"date\":\"%s\","
"\"type\":\"lane\",\"level\":%d}}",
event_id, date, level);
bt_uart_send_json(json);
char ack[64];
snprintf(ack, sizeof(ack), "TEST_ACK BLE VIOLATION %d OK", level);
test_reply(ack);
}
static void test_ble_collision(int level, const char *type)
{
char type_str[24];
if (level && type && type[0])
snprintf(type_str, sizeof(type_str), "\"%s\"", type);
else
snprintf(type_str, sizeof(type_str), "null");
char json[128];
snprintf(json, sizeof(json),
"{\"response_type\":\"collision_warning\","
"\"content\":{\"level\":%d,\"type\":%s}}",
level, type_str);
bt_uart_send_json(json);
char ack[64];
snprintf(ack, sizeof(ack), "TEST_ACK BLE COLLISION %d OK", level);
test_reply(ack);
}
static void test_ble_alert(int ll, const char *lt, int rl, const char *rt)
{
char ls[24], rs[24];
if (ll && lt && lt[0]) snprintf(ls, sizeof(ls), "\"%s\"", lt);
else snprintf(ls, sizeof(ls), "null");
if (rl && rt && rt[0]) snprintf(rs, sizeof(rs), "\"%s\"", rt);
else snprintf(rs, sizeof(rs), "null");
char json[256];
snprintf(json, sizeof(json),
"{\"response_type\":\"alert\","
"\"content\":{"
"\"left\":{\"level\":%d,\"type\":%s},"
"\"right\":{\"level\":%d,\"type\":%s}}}",
ll, ls, rl, rs);
bt_uart_send_json(json);
char ack[96];
snprintf(ack, sizeof(ack),
"TEST_ACK BLE ALERT left=%d right=%d OK", ll, rl);
test_reply(ack);
}
static void test_can_event(const char *event, int level,
uint8_t speed, buzzer_pattern_t pat)
{
can_ctrl_cmd_t ctrl = { .speed = speed, .led_gpio = 0,
.led_enable = 0, .keepalive_interval_ms = 0 };
can_bus_send_control_cmd(&ctrl);
buzzer_set_pattern(pat);
char ack[96];
snprintf(ack, sizeof(ack),
"TEST_ACK CAN %s %d speed=%u buzzer=%s",
event, level, (unsigned)speed,
pat == BUZZER_PATTERN_COLLISION ? "collision" :
pat == BUZZER_PATTERN_ALERT ? "alert" :
pat == BUZZER_PATTERN_GRASS ? "grass" : "off");
test_reply(ack);
}
static void *test_ble_all_thread(void *arg)
{
(void)arg;
test_reply("TEST_BLE_ALL_START");
test_ble_violation(1); sleep(3);
test_ble_violation(0); sleep(2);
test_ble_collision(1, "vehicle"); sleep(3);
test_ble_collision(0, NULL); sleep(2);
test_ble_alert(1, "tree", 0, NULL); sleep(3);
test_ble_alert(0, NULL, 0, NULL); sleep(2);
test_ble_alert(1, "tree", 1, "vehicle"); sleep(3);
test_ble_alert(0, NULL, 0, NULL); sleep(2);
test_reply("TEST_BLE_ALL_DONE");
test_mode_exit_internal("BLE_ALL complete");
s_test_all_running = 0;
return NULL;
}
static void *test_can_all_thread(void *arg)
{
int obs = arg ? (int)(intptr_t)arg : TEST_OBS_DEFAULT_SEC;
test_reply("TEST_CAN_ALL_START");
test_can_event("VIOLATION", 1, SPEED_LIMIT_ALERT, BUZZER_PATTERN_GRASS); sleep(obs);
test_can_event("VIOLATION", 0, SPEED_LIMIT_NORMAL, BUZZER_PATTERN_OFF); sleep(3);
test_can_event("COLLISION", 1, SPEED_LIMIT_STOP, BUZZER_PATTERN_COLLISION); sleep(obs);
test_can_event("COLLISION", 0, SPEED_LIMIT_NORMAL, BUZZER_PATTERN_OFF); sleep(3);
test_can_event("ALERT", 1, SPEED_LIMIT_ALERT, BUZZER_PATTERN_ALERT); sleep(obs);
test_can_event("ALERT", 0, SPEED_LIMIT_NORMAL, BUZZER_PATTERN_OFF); sleep(3);
test_reply("TEST_CAN_BUZZER_ONLY_START");
buzzer_set_pattern(BUZZER_PATTERN_GRASS); sleep(4);
buzzer_set_pattern(BUZZER_PATTERN_ALERT); sleep(4);
buzzer_set_pattern(BUZZER_PATTERN_COLLISION); sleep(4);
buzzer_set_pattern(BUZZER_PATTERN_OFF);
test_reply("TEST_CAN_ALL_DONE");
test_mode_exit_internal("CAN_ALL complete");
s_test_all_running = 0;
return NULL;
}
static void *test_all_thread(void *arg)
{
int obs = arg ? (int)(intptr_t)arg : TEST_OBS_DEFAULT_SEC;
test_reply("TEST_ALL_START");
test_reply("TEST_ALL_BLE_GROUP_START");
test_ble_violation(1); sleep(3);
test_ble_violation(0); sleep(2);
test_ble_collision(1, "vehicle"); sleep(3);
test_ble_collision(0, NULL); sleep(2);
test_ble_alert(1, "tree", 0, NULL); sleep(3);
test_ble_alert(0, NULL, 0, NULL); sleep(2);
test_ble_alert(1, "tree", 1, "vehicle"); sleep(3);
test_ble_alert(0, NULL, 0, NULL); sleep(2);
test_reply("TEST_ALL_BLE_GROUP_DONE");
sleep(3);
test_reply("TEST_ALL_CAN_GROUP_START");
test_can_event("VIOLATION", 1, SPEED_LIMIT_ALERT, BUZZER_PATTERN_GRASS); sleep(obs);
test_can_event("VIOLATION", 0, SPEED_LIMIT_NORMAL, BUZZER_PATTERN_OFF); sleep(3);
test_can_event("COLLISION", 1, SPEED_LIMIT_STOP, BUZZER_PATTERN_COLLISION); sleep(obs);
test_can_event("COLLISION", 0, SPEED_LIMIT_NORMAL, BUZZER_PATTERN_OFF); sleep(3);
test_can_event("ALERT", 1, SPEED_LIMIT_ALERT, BUZZER_PATTERN_ALERT); sleep(obs);
test_can_event("ALERT", 0, SPEED_LIMIT_NORMAL, BUZZER_PATTERN_OFF); sleep(3);
buzzer_set_pattern(BUZZER_PATTERN_GRASS); sleep(4);
buzzer_set_pattern(BUZZER_PATTERN_ALERT); sleep(4);
buzzer_set_pattern(BUZZER_PATTERN_COLLISION); sleep(4);
buzzer_set_pattern(BUZZER_PATTERN_OFF);
test_reply("TEST_ALL_CAN_GROUP_DONE");
test_reply("TEST_ALL_DONE");
test_mode_exit_internal("ALL complete");
s_test_all_running = 0;
return NULL;
}
static void handle_test_cmd(const char *raw)
{
char cmd[128];
strncpy(cmd, raw, sizeof(cmd) - 1);
cmd[sizeof(cmd) - 1] = '\0';
str_toupper(cmd);
printf("[TEST CMD] %s\n", cmd);
if (g_test_mode) s_test_mode_entered = time(NULL);
if (strncmp(cmd, "TEST_ENTER", 10) == 0) {
if (g_test_mode) { test_reply("TEST_ACK ALREADY_IN_TEST"); return; }
g_test_mode = 1;
s_test_mode_entered = time(NULL);
test_reply("TEST_ACK ENTER");
return;
}
if (strncmp(cmd, "TEST_EXIT", 9) == 0) {
s_test_all_running = 0;
test_mode_exit_internal("user request");
return;
}
if (!g_test_mode) { test_reply("TEST_ERR NOT_IN_TEST_MODE"); return; }
if (strncmp(cmd, "TEST_BLE_ALL", 12) == 0) {
if (s_test_all_running) { test_reply("TEST_ERR ALL_ALREADY_RUNNING"); return; }
s_test_all_running = 1;
pthread_t tid; pthread_create(&tid, NULL, test_ble_all_thread, NULL); pthread_detach(tid);
} else if (strncmp(cmd, "TEST_BLE VIOLATION", 18) == 0) {
int lv = atoi(cmd + 18);
test_ble_violation(lv ? 1 : 0);
} else if (strncmp(cmd, "TEST_BLE COLLISION", 18) == 0) {
int lv = atoi(cmd + 18);
test_ble_collision(lv ? 1 : 0, lv ? "vehicle" : NULL);
} else if (strncmp(cmd, "TEST_BLE ALERT", 14) == 0) {
int ll = 0, rl = 0;
char lt[16] = "null", rt[16] = "null";
sscanf(cmd + 14, " %d %15s %d %15s", &ll, lt, &rl, rt);
test_ble_alert(ll, strcmp(lt, "NULL") == 0 ? NULL : lt,
rl, strcmp(rt, "NULL") == 0 ? NULL : rt);
} else if (strncmp(cmd, "TEST_CAN_ALL", 12) == 0) {
if (s_test_all_running) { test_reply("TEST_ERR ALL_ALREADY_RUNNING"); return; }
int obs = TEST_OBS_DEFAULT_SEC;
sscanf(cmd + 12, " %d", &obs);
if (obs <= 0 || obs > 60) obs = TEST_OBS_DEFAULT_SEC;
s_test_all_running = 1;
pthread_t tid; pthread_create(&tid, NULL, test_can_all_thread, (void*)(intptr_t)obs); pthread_detach(tid);
} else if (strncmp(cmd, "TEST_CAN VIOLATION", 18) == 0) {
int lv = atoi(cmd + 18);
test_can_event("VIOLATION", lv,
lv ? SPEED_LIMIT_ALERT : SPEED_LIMIT_NORMAL,
lv ? BUZZER_PATTERN_GRASS : BUZZER_PATTERN_OFF);
} else if (strncmp(cmd, "TEST_CAN COLLISION", 18) == 0) {
int lv = atoi(cmd + 18);
test_can_event("COLLISION", lv,
lv ? SPEED_LIMIT_STOP : SPEED_LIMIT_NORMAL,
lv ? BUZZER_PATTERN_COLLISION : BUZZER_PATTERN_OFF);
} else if (strncmp(cmd, "TEST_CAN ALERT", 14) == 0) {
int ll = 0, rl = 0;
sscanf(cmd + 14, " %d %d", &ll, &rl);
int active = ll || rl;
test_can_event("ALERT", active,
active ? SPEED_LIMIT_ALERT : SPEED_LIMIT_NORMAL,
active ? BUZZER_PATTERN_ALERT : BUZZER_PATTERN_OFF);
} else if (strncmp(cmd, "TEST_BUZZER", 11) == 0) {
char pat[16] = "OFF";
sscanf(cmd + 11, " %15s", pat);
buzzer_pattern_t p = BUZZER_PATTERN_OFF;
if (strcmp(pat, "COLLISION") == 0) p = BUZZER_PATTERN_COLLISION;
else if (strcmp(pat, "ALERT") == 0) p = BUZZER_PATTERN_ALERT;
else if (strcmp(pat, "GRASS") == 0) p = BUZZER_PATTERN_GRASS;
buzzer_set_pattern(p);
char ack[48];
snprintf(ack, sizeof(ack), "TEST_ACK BUZZER %s OK", pat);
test_reply(ack);
} else if (strncmp(cmd, "TEST_ALL", 8) == 0) {
if (s_test_all_running) { test_reply("TEST_ERR ALL_ALREADY_RUNNING"); return; }
int obs = TEST_OBS_DEFAULT_SEC;
sscanf(cmd + 8, " %d", &obs);
if (obs <= 0 || obs > 60) obs = TEST_OBS_DEFAULT_SEC;
s_test_all_running = 1;
pthread_t tid; pthread_create(&tid, NULL, test_all_thread, (void*)(intptr_t)obs); pthread_detach(tid);
} else {
test_reply("TEST_ERR UNKNOWN_CMD");
}
}
/* collision_warning notify (§2.2.5) */
void fire_collision_warning(int level, const char *type)
{
@ -666,6 +932,7 @@ void event_recorder_init(const char *upload_url,
s_enabled,
s_up_host, s_up_port, s_up_path,
s_sd_path, sd_max_mb, s_upload_delay_ms);
bt_uart_set_extra_cmd_cb(handle_test_cmd);
}
/* Recv callback: drives state machine */
@ -674,6 +941,14 @@ void event_recorder_update(const stdc_analysis_t *ana)
#if 1
if (!s_enabled) return;
if (g_test_mode) {
if (s_test_mode_entered > 0 && !s_test_all_running) {
if (time(NULL) - s_test_mode_entered >= TEST_MODE_TIMEOUT_SEC)
test_mode_exit_internal("timeout");
}
return;
}
int on_grass = ana->on_grass;
int grass_trigger = on_grass && ana->is_moving;
uint8_t speed_val = 0;