- 0.1.2:power_cycle 按完电源键后看电流,没开就重按,最多 3 次;新事件 19 BOOT_CHECK - 0.1.3:UP_VBUS_DET(IO6)启动时配成输入,status 加 "host":true|false|null - rack:单板页显示「上游:已连电脑 / 未连电脑」 - HANDOFF:损耗两档的来历(Hub 工作 ≈1.1 W)、板 B 第 2 槽 PRSNT2# 卡低 Co-Authored-By: Claude Opus 5.5 <[email protected]>
720 行
30 KiB
C
720 行
30 KiB
C
/* A12:单一命令处理器(需求 §10.8,计划 §6.3)。验收原话三条:
|
||
① 同序号发两遍只执行一次;
|
||
② 越界命令被拒且记事件;
|
||
③ 空槽(`PRSNT#` 高)的命令一律拒。
|
||
|
||
和 A9 的测试同一个口径:断言一律打在**线上真实发生的事**上 ——
|
||
假 PCA9555 的锁存器与写事务计数、DAC 寄存器、`EFUSE_EN` 引脚电平、落盘的事件 ——
|
||
不看处理器自己回了什么。「回了 ok」和「真的按了一次」是两件事,
|
||
判重写错的现象恰恰是回应完全正常、手机被按了两遍。 */
|
||
#include <stdio.h>
|
||
#include <string.h>
|
||
|
||
#include "board_pins.h"
|
||
#include "cmd.h"
|
||
#include "dac63204.h"
|
||
#include "evlog.h"
|
||
#include "hal_i2c.h"
|
||
#include "hal_lock.h"
|
||
#include "hal_time.h"
|
||
#include "i2cbus.h"
|
||
#include "pca9555.h"
|
||
#include "phonerail.h"
|
||
#include "rail.h"
|
||
#include "rail_cfg.h"
|
||
#include "sim_hal.h"
|
||
#include "slot.h"
|
||
#include "t_actuator.h"
|
||
#include "t_thermal.h"
|
||
#include "tca9548a.h"
|
||
#include "unity.h"
|
||
|
||
#define N PHONERAIL_RAIL_COUNT
|
||
|
||
static char s_out[CMD_OUT_MAX];
|
||
|
||
static const char *send_on(cmd_link_t link, const char *line)
|
||
{
|
||
const size_t n = cmd_handle_line(link, line, s_out, sizeof(s_out));
|
||
TEST_ASSERT_EQUAL_size_t(strlen(s_out), n);
|
||
return s_out;
|
||
}
|
||
|
||
static const char *send(const char *line) { return send_on(CMD_LINK_TEST, line); }
|
||
|
||
/* ⚠️ 写成函数不写成宏:宏会把 `send(...)` 展开两次(条件一次、失败消息一次),
|
||
断言失败时打印出来的是**又发了一遍**的那条回应 —— 正好是判重命中的那一条,
|
||
排查时会被它带偏。 */
|
||
static void assert_has_at(const char *resp, const char *needle, int line)
|
||
{
|
||
UNITY_TEST_ASSERT_NOT_NULL(strstr(resp, needle), line, resp);
|
||
}
|
||
|
||
static void assert_lacks_at(const char *resp, const char *needle, int line)
|
||
{
|
||
UNITY_TEST_ASSERT_NULL(strstr(resp, needle), line, resp);
|
||
}
|
||
|
||
#define ASSERT_HAS(resp, needle) assert_has_at((resp), (needle), __LINE__)
|
||
#define ASSERT_LACKS(resp, needle) assert_lacks_at((resp), (needle), __LINE__)
|
||
|
||
/* ---- 假板子:mux + 五片刀片 PCA9555 + 主干两片 DAC ------------------------ */
|
||
|
||
static sim_i2c_dev_t *blade(int rail)
|
||
{
|
||
sim_i2c_dev_t *d = sim_i2c_dev_ch(rail_cfg(rail)->mux_ch, PCA9555_ADDR);
|
||
TEST_ASSERT_NOT_NULL(d);
|
||
return d;
|
||
}
|
||
|
||
static uint8_t latch(int rail) { return sim_pca9555_output(rail - 1, 0); }
|
||
static uint32_t writes(int rail) { return blade(rail)->n_write; }
|
||
static int en_pin(int rail) { return sim_gpio_level(rail_cfg(rail)->efuse_en); }
|
||
|
||
static uint16_t dac_code(int rail)
|
||
{
|
||
const uint16_t reg = sim_i2c_get_reg(dac63204_rail_addr(rail),
|
||
DAC63204_REG_DATA(dac63204_rail_ch(rail)));
|
||
return DAC63204_REG_TO_CODE(reg);
|
||
}
|
||
|
||
static void set_present(int rail, bool present)
|
||
{
|
||
sim_gpio_drive(rail_cfg(rail)->prsnt_n, present ? 0 : 1);
|
||
slot_prsnt_poll();
|
||
}
|
||
|
||
void setUp(void)
|
||
{
|
||
sim_hal_reset();
|
||
const hal_i2c_cfg_t cfg = { .sda_gpio = BOARD_PIN_I2C_SDA,
|
||
.scl_gpio = BOARD_PIN_I2C_SCL,
|
||
.freq_hz = 400000 };
|
||
TEST_ASSERT_EQUAL_INT(HAL_OK, hal_i2c_init(&cfg));
|
||
TEST_ASSERT_EQUAL_INT(HAL_OK, hal_lock_init());
|
||
sim_i2c_add_mux(TCA9548A_ADDR, BOARD_PIN_MUX_RST_N);
|
||
for (int rail = 1; rail <= N; rail++) {
|
||
sim_pca9555_attach_ch(rail - 1, rail_cfg(rail)->mux_ch, PCA9555_ADDR);
|
||
}
|
||
for (int i = 0; i < DAC63204_COUNT; i++) {
|
||
sim_i2c_add(dac63204_all_addr[i], 2);
|
||
}
|
||
TEST_ASSERT_EQUAL_INT(HAL_OK, i2cbus_init());
|
||
TEST_ASSERT_EQUAL_INT(HAL_OK, rail_en_init());
|
||
for (int rail = 1; rail <= N; rail++) {
|
||
TEST_ASSERT_EQUAL_INT(HAL_OK, slot_init(rail));
|
||
}
|
||
TEST_ASSERT_EQUAL_INT(HAL_OK, slot_prsnt_init());
|
||
for (int rail = 1; rail <= N; rail++) {
|
||
set_present(rail, true);
|
||
}
|
||
rail_setpoint_reset();
|
||
t_thermal_set_floor(0);
|
||
TEST_ASSERT_EQUAL_INT(HAL_OK, evlog_init());
|
||
t_actuator_init(NULL, NULL);
|
||
cmd_init(NULL);
|
||
|
||
/* 启动期独占,之后任何一次 I²C 事务没持锁就记违规 —— `set_voltage` 那一笔
|
||
主干写就靠这一条抓(`rail_set_mv()` 自己不拿锁)。 */
|
||
sim_lock_require(HAL_LOCK_I2C, true);
|
||
}
|
||
|
||
void tearDown(void)
|
||
{
|
||
TEST_ASSERT_EQUAL_UINT32_MESSAGE(0, sim_lock_violations(HAL_LOCK_I2C), "有 I²C 事务没持锁");
|
||
TEST_ASSERT_EQUAL_UINT32(0, sim_i2c_ambiguous());
|
||
}
|
||
|
||
/* 虚拟时钟往前推,期间按执行器自己要的节奏叫它(同 test_t_actuator.c)。 */
|
||
static void run_ms(uint32_t ms)
|
||
{
|
||
const uint32_t target = hal_time_now_ms() + ms;
|
||
for (;;) {
|
||
uint32_t w = t_actuator_step();
|
||
const uint32_t now = hal_time_now_ms();
|
||
if ((int32_t)(now - target) >= 0) {
|
||
return;
|
||
}
|
||
const uint32_t left = target - now;
|
||
if (w == 0 || w > left) {
|
||
w = left;
|
||
}
|
||
sim_time_advance_ms(w);
|
||
}
|
||
}
|
||
|
||
/* 把 flash 里的事件全拉出来,找第 k 条类型为 type 的。 */
|
||
static bool find_event(log_ev_t type, int k, evlog_rec_t *out)
|
||
{
|
||
evlog_rec_t rec[64];
|
||
(void)evlog_flush(0);
|
||
const uint32_t n = evlog_read(0, rec, 64);
|
||
for (uint32_t i = 0; i < n; i++) {
|
||
if (rec[i].type == (uint8_t)type && k-- == 0) {
|
||
*out = rec[i];
|
||
return true;
|
||
}
|
||
}
|
||
return false;
|
||
}
|
||
|
||
static int count_events(log_ev_t type)
|
||
{
|
||
evlog_rec_t r;
|
||
int k = 0;
|
||
while (find_event(type, k, &r)) {
|
||
k++;
|
||
}
|
||
return k;
|
||
}
|
||
|
||
/* ---- 冻结的编号 ---------------------------------------------------------- */
|
||
|
||
/* 命令号与拒绝原因会写进 flash(CMD_REJECT 的 arg / current_ua),和 log_ev_t 一样冻结。 */
|
||
static void test_cmd_numbers_are_frozen(void)
|
||
{
|
||
TEST_ASSERT_EQUAL_INT(1, CMD_STATUS);
|
||
TEST_ASSERT_EQUAL_INT(2, CMD_SET_VOLTAGE);
|
||
TEST_ASSERT_EQUAL_INT(3, CMD_POWER_CYCLE);
|
||
TEST_ASSERT_EQUAL_INT(4, CMD_KEY);
|
||
TEST_ASSERT_EQUAL_INT(5, CMD_EDL);
|
||
TEST_ASSERT_EQUAL_INT(6, CMD_VBUS);
|
||
TEST_ASSERT_EQUAL_INT(7, CMD_FAN);
|
||
TEST_ASSERT_EQUAL_INT(8, CMD_EVENTS);
|
||
TEST_ASSERT_EQUAL_INT(9, CMD_POWER);
|
||
TEST_ASSERT_EQUAL_INT(10, CMD_WIFI);
|
||
TEST_ASSERT_EQUAL_INT(11, CMD_MQTT);
|
||
TEST_ASSERT_EQUAL_INT(12, CMD_NET);
|
||
TEST_ASSERT_EQUAL_INT(13, CMD_OTA); /* C2 */
|
||
TEST_ASSERT_EQUAL_INT(14, CMD_COUNT);
|
||
|
||
TEST_ASSERT_EQUAL_INT(0, CMD_OK);
|
||
TEST_ASSERT_EQUAL_INT(1, CMD_ERR_PARSE);
|
||
TEST_ASSERT_EQUAL_INT(2, CMD_ERR_UNKNOWN);
|
||
TEST_ASSERT_EQUAL_INT(3, CMD_ERR_NO_SEQ);
|
||
TEST_ASSERT_EQUAL_INT(4, CMD_ERR_ARG);
|
||
TEST_ASSERT_EQUAL_INT(5, CMD_ERR_RANGE);
|
||
TEST_ASSERT_EQUAL_INT(6, CMD_ERR_EMPTY);
|
||
TEST_ASSERT_EQUAL_INT(7, CMD_ERR_BUSY);
|
||
TEST_ASSERT_EQUAL_INT(8, CMD_ERR_STATE);
|
||
TEST_ASSERT_EQUAL_INT(9, CMD_ERR_IO);
|
||
TEST_ASSERT_EQUAL_INT(10, CMD_ERR_OVERFLOW);
|
||
TEST_ASSERT_EQUAL_INT(11, CMD_ERR_LINK);
|
||
TEST_ASSERT_EQUAL_INT(0x21, CMD_REBOOT_ARG(true, CMD_LINK_WIFI));
|
||
TEST_ASSERT_EQUAL_INT(0x10, CMD_REBOOT_ARG(false, CMD_LINK_USB));
|
||
|
||
for (int i = CMD_NONE + 1; i < CMD_COUNT; i++) {
|
||
TEST_ASSERT_TRUE(strlen(cmd_name((cmd_id_t)i)) > 0);
|
||
}
|
||
for (int i = 0; i < CMD_ERR_COUNT; i++) {
|
||
TEST_ASSERT_NOT_EQUAL(0, strcmp("?", cmd_err_str((cmd_err_t)i)));
|
||
}
|
||
}
|
||
|
||
/* ---- ① 判重 -------------------------------------------------------------- */
|
||
|
||
/* ⭐ 验收主项一。同一个 seq 从 USB 再到 WiFi:第二遍必须回第一次的结论、
|
||
**一个字节都不许再发到刀片上**。 */
|
||
static void test_same_seq_twice_executes_once_across_links(void)
|
||
{
|
||
const char *line = "{\"seq\":41,\"cmd\":\"key\",\"rail\":2,\"keys\":\"pwr\",\"ms\":100}";
|
||
const uint32_t w0 = writes(2);
|
||
ASSERT_HAS(send_on(CMD_LINK_WIFI, line), "\"ok\":true");
|
||
const uint32_t w1 = writes(2);
|
||
TEST_ASSERT_EQUAL_UINT32(w0 + 1, w1); /* 按下去正好一次写 */
|
||
TEST_ASSERT_TRUE((latch(2) & (1u << pca9555_key_bit(PCA9555_KEY_PWR))) != 0);
|
||
|
||
const char *r = send_on(CMD_LINK_USB, line);
|
||
ASSERT_HAS(r, "\"seq\":41");
|
||
ASSERT_HAS(r, "\"ok\":true");
|
||
ASSERT_HAS(r, "\"dup\":true");
|
||
TEST_ASSERT_EQUAL_UINT32(w1, writes(2));
|
||
|
||
cmd_stats_t st;
|
||
cmd_stats(&st);
|
||
TEST_ASSERT_EQUAL_UINT32(1, st.dups);
|
||
}
|
||
|
||
/* ⭐ §10.8 原话的那个场景:「按 10 秒电源键」执行两遍,第二遍正好按在刚开机的手机上。
|
||
第一遍**走完之后**重传到达,照样不许执行。 */
|
||
static void test_retransmit_after_the_sequence_finished_is_still_dropped(void)
|
||
{
|
||
const char *line = "{\"seq\":7,\"cmd\":\"key\",\"rail\":4,\"keys\":\"pwr\",\"ms\":10000}";
|
||
ASSERT_HAS(send(line), "\"ok\":true");
|
||
run_ms(10500);
|
||
TEST_ASSERT_FALSE(t_actuator_busy(4));
|
||
TEST_ASSERT_EQUAL_HEX8(0x00, latch(4));
|
||
const uint32_t w = writes(4);
|
||
|
||
ASSERT_HAS(send(line), "\"dup\":true");
|
||
TEST_ASSERT_FALSE(t_actuator_busy(4));
|
||
TEST_ASSERT_EQUAL_UINT32(w, writes(4));
|
||
TEST_ASSERT_EQUAL_INT(1, count_events(LOG_EV_REBOOT)); /* 软重启只记了一次 */
|
||
}
|
||
|
||
/* 被拒的命令也进窗口:重传拿到的是同一个拒绝,而不是「这次碰巧不忙了就执行」。 */
|
||
static void test_a_rejected_command_stays_rejected_on_retransmit(void)
|
||
{
|
||
ASSERT_HAS(send("{\"seq\":1,\"cmd\":\"key\",\"rail\":1,\"keys\":\"vup\",\"ms\":2000}"),
|
||
"\"ok\":true");
|
||
const char *busy = "{\"seq\":2,\"cmd\":\"edl\",\"rail\":1}";
|
||
ASSERT_HAS(send(busy), "\"err\":\"busy\"");
|
||
run_ms(2500);
|
||
TEST_ASSERT_FALSE(t_actuator_busy(1));
|
||
const char *r = send(busy);
|
||
ASSERT_HAS(r, "\"err\":\"busy\"");
|
||
ASSERT_HAS(r, "\"dup\":true");
|
||
TEST_ASSERT_EQUAL_UINT32(1, t_actuator_done(1)); /* 只走完过那一次按键,EDL 从没启动 */
|
||
TEST_ASSERT_FALSE(t_actuator_busy(1));
|
||
}
|
||
|
||
/* 上位机把同一个 seq 用在了两条命令上:不执行后一条,回 arg 让它看得见。 */
|
||
static void test_same_seq_different_command_is_not_executed(void)
|
||
{
|
||
ASSERT_HAS(send("{\"seq\":9,\"cmd\":\"fan\",\"duty\":500}"), "\"ok\":true");
|
||
const uint32_t w = writes(3);
|
||
const char *r = send("{\"seq\":9,\"cmd\":\"key\",\"rail\":3,\"keys\":\"pwr\",\"ms\":100}");
|
||
ASSERT_HAS(r, "\"err\":\"arg\"");
|
||
ASSERT_HAS(r, "\"dup\":true");
|
||
TEST_ASSERT_EQUAL_UINT32(w, writes(3));
|
||
}
|
||
|
||
/* 窗口按「值相等」判,不按「≤ 最大值」判:上位机重启后序号从头数,新命令照常执行。 */
|
||
static void test_window_is_by_value_so_a_restarted_host_is_not_blocked(void)
|
||
{
|
||
char line[96];
|
||
for (int s = 1000; s < 1000 + CMD_SEQ_WINDOW; s++) {
|
||
snprintf(line, sizeof(line), "{\"seq\":%d,\"cmd\":\"fan\",\"duty\":0}", s);
|
||
ASSERT_LACKS(send(line), "\"dup\":true");
|
||
}
|
||
ASSERT_LACKS(send("{\"seq\":3,\"cmd\":\"fan\",\"duty\":400}"), "\"dup\":true"); /* 比窗口里的都小 */
|
||
TEST_ASSERT_EQUAL_UINT16(400, t_thermal_floor());
|
||
/* 窗口满了之后最老的那条被挤出去:1000 这时已经不认识了。 */
|
||
ASSERT_LACKS(send("{\"seq\":1000,\"cmd\":\"fan\",\"duty\":0}"), "\"dup\":true");
|
||
ASSERT_HAS(send("{\"seq\":1031,\"cmd\":\"fan\",\"duty\":0}"), "\"dup\":true");
|
||
}
|
||
|
||
/* 会动东西的命令必须带 seq;status / events 幂等,带不带都行、也不判重。 */
|
||
static void test_seq_rules(void)
|
||
{
|
||
const uint32_t w = writes(1);
|
||
ASSERT_HAS(send("{\"cmd\":\"key\",\"rail\":1,\"keys\":\"pwr\",\"ms\":100}"), "\"err\":\"no_seq\"");
|
||
ASSERT_HAS(send("{\"seq\":0,\"cmd\":\"edl\",\"rail\":1}"), "\"err\":\"no_seq\"");
|
||
ASSERT_HAS(send("{\"seq\":4294967296,\"cmd\":\"edl\",\"rail\":1}"), "\"err\":\"no_seq\"");
|
||
ASSERT_HAS(send("{\"seq\":\"5\",\"cmd\":\"edl\",\"rail\":1}"), "\"err\":\"no_seq\"");
|
||
TEST_ASSERT_EQUAL_UINT32(w, writes(1));
|
||
TEST_ASSERT_FALSE(t_actuator_busy(1));
|
||
|
||
ASSERT_HAS(send("{\"cmd\":\"status\"}"), "\"ok\":true");
|
||
ASSERT_LACKS(send("{\"seq\":77,\"cmd\":\"status\"}"), "\"dup\":true");
|
||
const char *r = send("{\"seq\":77,\"cmd\":\"status\"}");
|
||
ASSERT_LACKS(r, "\"dup\":true");
|
||
ASSERT_HAS(r, "\"seq\":77");
|
||
}
|
||
|
||
/* ---- ② 越界即拒,而且记事件 ------------------------------------------------ */
|
||
|
||
/* ⭐ 验收主项二。SR-13:4011 / 4300 / 3523 都拒,DAC 一个字节都不动,每一次都记一条
|
||
CMD_REJECT(arg = 命令号、bus_mv = 那个越界值、current_ua = 原因)。 */
|
||
static void test_voltage_out_of_range_is_rejected_and_logged(void)
|
||
{
|
||
const uint16_t before = dac_code(2);
|
||
const uint32_t dac_w = sim_i2c_dev(dac63204_rail_addr(2))->n_write;
|
||
static const int bad[] = { 4011, 4300, 3523, -1 };
|
||
char line[96];
|
||
for (size_t i = 0; i < sizeof(bad) / sizeof(bad[0]); i++) {
|
||
snprintf(line, sizeof(line), "{\"seq\":%d,\"cmd\":\"set_voltage\",\"rail\":2,\"mv\":%d}",
|
||
(int)i + 1, bad[i]);
|
||
ASSERT_HAS(send(line), "\"err\":\"range\"");
|
||
}
|
||
TEST_ASSERT_EQUAL_UINT16(before, dac_code(2));
|
||
TEST_ASSERT_EQUAL_UINT32(dac_w, sim_i2c_dev(dac63204_rail_addr(2))->n_write);
|
||
TEST_ASSERT_EQUAL_INT(RAIL_MV_SAFE, rail_setpoint_mv(2));
|
||
|
||
TEST_ASSERT_EQUAL_INT(4, count_events(LOG_EV_CMD_REJECT));
|
||
evlog_rec_t r;
|
||
TEST_ASSERT_TRUE(find_event(LOG_EV_CMD_REJECT, 1, &r));
|
||
TEST_ASSERT_EQUAL_UINT8(2, r.rail);
|
||
TEST_ASSERT_EQUAL_UINT8(CMD_SET_VOLTAGE, r.arg);
|
||
TEST_ASSERT_EQUAL_INT32(4300, r.bus_mv);
|
||
TEST_ASSERT_EQUAL_INT32(CMD_ERR_RANGE, r.current_ua);
|
||
}
|
||
|
||
/* 合法电压:主干上的 DAC 写**在锁里**(tearDown 查违规),码与 `rail_mv_to_code()` 一致。 */
|
||
static void test_voltage_in_range_is_written_under_the_bus_lock(void)
|
||
{
|
||
const char *r = send("{\"seq\":5,\"cmd\":\"set_voltage\",\"rail\":5,\"mv\":3800}");
|
||
ASSERT_HAS(r, "\"ok\":true");
|
||
ASSERT_HAS(r, "\"set\":3800");
|
||
TEST_ASSERT_EQUAL_UINT16(rail_mv_to_code(3800), dac_code(5));
|
||
TEST_ASSERT_EQUAL_INT(3800, rail_setpoint_mv(5));
|
||
ASSERT_HAS(send("{\"seq\":6,\"cmd\":\"set_voltage\",\"rail\":5,\"mv\":4010}"), "\"ok\":true");
|
||
TEST_ASSERT_EQUAL_INT(0, count_events(LOG_EV_CMD_REJECT));
|
||
}
|
||
|
||
static void test_other_range_checks(void)
|
||
{
|
||
ASSERT_HAS(send("{\"seq\":1,\"cmd\":\"edl\",\"rail\":6}"), "\"err\":\"range\"");
|
||
ASSERT_HAS(send("{\"seq\":2,\"cmd\":\"edl\",\"rail\":0}"), "\"err\":\"range\"");
|
||
ASSERT_HAS(send("{\"seq\":3,\"cmd\":\"key\",\"rail\":1,\"keys\":\"pwr\",\"ms\":0}"),
|
||
"\"err\":\"range\"");
|
||
char line[96];
|
||
snprintf(line, sizeof(line), "{\"seq\":4,\"cmd\":\"key\",\"rail\":1,\"keys\":\"pwr\",\"ms\":%d}",
|
||
ACT_KEY_MAX_MS + 1);
|
||
ASSERT_HAS(send(line), "\"err\":\"range\"");
|
||
ASSERT_HAS(send("{\"seq\":5,\"cmd\":\"fan\",\"duty\":1001}"), "\"err\":\"range\"");
|
||
TEST_ASSERT_EQUAL_INT(5, count_events(LOG_EV_CMD_REJECT));
|
||
|
||
evlog_rec_t r;
|
||
TEST_ASSERT_TRUE(find_event(LOG_EV_CMD_REJECT, 0, &r));
|
||
TEST_ASSERT_EQUAL_UINT8(0, r.rail); /* 路号本身越界:记整板级,不记一个不存在的路 */
|
||
TEST_ASSERT_EQUAL_INT32(6, r.bus_mv);
|
||
for (int rail = 1; rail <= N; rail++) {
|
||
TEST_ASSERT_FALSE(t_actuator_busy(rail));
|
||
}
|
||
}
|
||
|
||
/* 缺参数 / 类型不对 / 键名写错:arg,**不记事件**(那是上位机的 bug,不是操作)。 */
|
||
static void test_argument_errors_are_not_logged(void)
|
||
{
|
||
static const char *const bad[] = {
|
||
"{\"seq\":1,\"cmd\":\"set_voltage\",\"rail\":1}",
|
||
"{\"seq\":2,\"cmd\":\"set_voltage\",\"rail\":1,\"mv\":\"3900\"}",
|
||
"{\"seq\":3,\"cmd\":\"key\",\"rail\":1,\"keys\":\"pwr+xyz\",\"ms\":100}",
|
||
"{\"seq\":4,\"cmd\":\"key\",\"rail\":1,\"keys\":\"pwr+\",\"ms\":100}",
|
||
"{\"seq\":5,\"cmd\":\"key\",\"rail\":1,\"keys\":\"\",\"ms\":100}",
|
||
"{\"seq\":6,\"cmd\":\"vbus\",\"rail\":1,\"on\":1}",
|
||
"{\"seq\":7,\"cmd\":\"power\",\"rail\":1}",
|
||
"{\"seq\":8,\"cmd\":\"edl\"}",
|
||
};
|
||
for (size_t i = 0; i < sizeof(bad) / sizeof(bad[0]); i++) {
|
||
ASSERT_HAS(send(bad[i]), "\"err\":\"arg\"");
|
||
}
|
||
ASSERT_HAS(send("{\"seq\":9,\"cmd\":\"reboot_everything\"}"), "\"err\":\"unknown_cmd\"");
|
||
ASSERT_HAS(send("not json at all"), "\"err\":\"parse\"");
|
||
TEST_ASSERT_EQUAL_INT(0, count_events(LOG_EV_CMD_REJECT));
|
||
TEST_ASSERT_FALSE(t_actuator_busy(1));
|
||
TEST_ASSERT_EQUAL_HEX8(0x00, latch(1));
|
||
}
|
||
|
||
/* ---- ③ 空槽一律拒 -------------------------------------------------------- */
|
||
|
||
/* ⭐ 验收主项三。槽 3 空着:六条按路的命令全拒,刀片 3 一个字节不动、EN 不动,
|
||
每条都记一条事件;隔壁槽照常。 */
|
||
static void test_every_rail_command_is_refused_on_an_empty_slot(void)
|
||
{
|
||
set_present(3, false);
|
||
TEST_ASSERT_FALSE(slot_present(3));
|
||
const uint32_t w = writes(3);
|
||
const uint16_t code = dac_code(3);
|
||
|
||
static const char *const cmds[] = {
|
||
"{\"seq\":1,\"cmd\":\"set_voltage\",\"rail\":3,\"mv\":3800}",
|
||
"{\"seq\":2,\"cmd\":\"power_cycle\",\"rail\":3}",
|
||
"{\"seq\":3,\"cmd\":\"key\",\"rail\":3,\"keys\":\"pwr\",\"ms\":100}",
|
||
"{\"seq\":4,\"cmd\":\"edl\",\"rail\":3}",
|
||
"{\"seq\":5,\"cmd\":\"vbus\",\"rail\":3,\"on\":false}",
|
||
"{\"seq\":6,\"cmd\":\"power\",\"rail\":3,\"on\":true}",
|
||
};
|
||
for (size_t i = 0; i < sizeof(cmds) / sizeof(cmds[0]); i++) {
|
||
ASSERT_HAS(send(cmds[i]), "\"err\":\"empty\"");
|
||
}
|
||
run_ms(100);
|
||
TEST_ASSERT_EQUAL_UINT32(w, writes(3));
|
||
TEST_ASSERT_EQUAL_UINT16(code, dac_code(3));
|
||
TEST_ASSERT_EQUAL_INT(0, en_pin(3));
|
||
TEST_ASSERT_FALSE(t_actuator_busy(3));
|
||
|
||
TEST_ASSERT_EQUAL_INT(6, count_events(LOG_EV_CMD_REJECT));
|
||
evlog_rec_t r;
|
||
TEST_ASSERT_TRUE(find_event(LOG_EV_CMD_REJECT, 5, &r));
|
||
TEST_ASSERT_EQUAL_UINT8(3, r.rail);
|
||
TEST_ASSERT_EQUAL_UINT8(CMD_POWER, r.arg);
|
||
TEST_ASSERT_EQUAL_INT32(CMD_ERR_EMPTY, r.current_ua);
|
||
|
||
ASSERT_HAS(send("{\"seq\":7,\"cmd\":\"power\",\"rail\":4,\"on\":true}"), "\"ok\":true");
|
||
TEST_ASSERT_EQUAL_INT(1, en_pin(4));
|
||
}
|
||
|
||
/* ---- 各条命令的语义 ------------------------------------------------------ */
|
||
|
||
/* 组合键:一次写两位(§10.6),走的是命令里给的那一路。 */
|
||
static void test_combo_keys_land_on_the_named_rail(void)
|
||
{
|
||
uint32_t w[N];
|
||
for (int r = 1; r <= N; r++) {
|
||
w[r - 1] = writes(r);
|
||
}
|
||
ASSERT_HAS(send("{\"seq\":1,\"cmd\":\"key\",\"rail\":5,\"keys\":\"vdn+pwr\",\"ms\":2000}"),
|
||
"\"ok\":true");
|
||
const uint8_t want = (uint8_t)((1u << pca9555_key_bit(PCA9555_KEY_PWR)) |
|
||
(1u << pca9555_key_bit(PCA9555_KEY_VDN)));
|
||
TEST_ASSERT_EQUAL_HEX8(want, latch(5));
|
||
TEST_ASSERT_EQUAL_UINT32(w[4] + 1, writes(5));
|
||
for (int r = 1; r < 5; r++) {
|
||
TEST_ASSERT_EQUAL_UINT32(w[r - 1], writes(r));
|
||
}
|
||
run_ms(2100);
|
||
TEST_ASSERT_EQUAL_HEX8(0x00, latch(5));
|
||
TEST_ASSERT_EQUAL_INT(0, count_events(LOG_EV_REBOOT)); /* 组合键不算重启 */
|
||
}
|
||
|
||
/* 同一路正在跑序列:busy,不记事件(忙不是越界)。 */
|
||
static void test_busy_rail_refuses_a_second_sequence(void)
|
||
{
|
||
ASSERT_HAS(send("{\"seq\":1,\"cmd\":\"power_cycle\",\"rail\":1}"), "\"ok\":true");
|
||
ASSERT_HAS(send("{\"seq\":2,\"cmd\":\"key\",\"rail\":1,\"keys\":\"pwr\",\"ms\":100}"),
|
||
"\"err\":\"busy\"");
|
||
ASSERT_HAS(send("{\"seq\":3,\"cmd\":\"vbus\",\"rail\":1,\"on\":false}"), "\"err\":\"busy\"");
|
||
ASSERT_HAS(send("{\"seq\":4,\"cmd\":\"power\",\"rail\":1,\"on\":false}"), "\"err\":\"busy\"");
|
||
TEST_ASSERT_EQUAL_INT(ACT_SEQ_HARD_RESET, t_actuator_seq(1));
|
||
TEST_ASSERT_EQUAL_INT(0, count_events(LOG_EV_CMD_REJECT));
|
||
}
|
||
|
||
/* 两级重启的两条事件:硬 = power_cycle,软 = 单按电源键 ≥10 s。arg 带链路。 */
|
||
static void test_reboots_are_logged_with_kind_and_link(void)
|
||
{
|
||
ASSERT_HAS(send_on(CMD_LINK_WIFI, "{\"seq\":1,\"cmd\":\"power_cycle\",\"rail\":2}"),
|
||
"\"ok\":true");
|
||
ASSERT_HAS(send_on(CMD_LINK_USB,
|
||
"{\"seq\":2,\"cmd\":\"key\",\"rail\":3,\"keys\":\"pwr\",\"ms\":10000}"),
|
||
"\"ok\":true");
|
||
ASSERT_HAS(send("{\"seq\":3,\"cmd\":\"key\",\"rail\":4,\"keys\":\"pwr\",\"ms\":2000}"),
|
||
"\"ok\":true"); /* 2 s 是开机,不是重启 */
|
||
|
||
TEST_ASSERT_EQUAL_INT(2, count_events(LOG_EV_REBOOT));
|
||
evlog_rec_t r;
|
||
TEST_ASSERT_TRUE(find_event(LOG_EV_REBOOT, 0, &r));
|
||
TEST_ASSERT_EQUAL_UINT8(2, r.rail);
|
||
TEST_ASSERT_EQUAL_UINT8(CMD_REBOOT_ARG(true, CMD_LINK_WIFI), r.arg);
|
||
TEST_ASSERT_TRUE(find_event(LOG_EV_REBOOT, 1, &r));
|
||
TEST_ASSERT_EQUAL_UINT8(3, r.rail);
|
||
TEST_ASSERT_EQUAL_UINT8(CMD_REBOOT_ARG(false, CMD_LINK_USB), r.arg);
|
||
}
|
||
|
||
/* eFuse 关着时开 VBUS = 手机只靠 USB 供电:state,记事件。开了电之后才行。 */
|
||
static void test_vbus_needs_the_rail_powered(void)
|
||
{
|
||
const uint32_t vb = 1u << PCA9555_BIT_VBUS_EN;
|
||
ASSERT_HAS(send("{\"seq\":1,\"cmd\":\"vbus\",\"rail\":2,\"on\":true}"), "\"err\":\"state\"");
|
||
TEST_ASSERT_EQUAL_HEX8(0x00, latch(2) & vb);
|
||
TEST_ASSERT_EQUAL_INT(1, count_events(LOG_EV_CMD_REJECT));
|
||
|
||
ASSERT_HAS(send("{\"seq\":2,\"cmd\":\"power\",\"rail\":2,\"on\":true}"), "\"ok\":true");
|
||
TEST_ASSERT_EQUAL_INT(1, en_pin(2));
|
||
ASSERT_HAS(send("{\"seq\":3,\"cmd\":\"vbus\",\"rail\":2,\"on\":true}"), "\"ok\":true");
|
||
TEST_ASSERT_EQUAL_HEX8(vb, latch(2) & vb);
|
||
TEST_ASSERT_TRUE(slot_vbus_is_on(2));
|
||
}
|
||
|
||
/* 关电:先断 VBUS 再关 eFuse;VBUS 断不掉(刀片 NAK)也照样关 eFuse,回 io。 */
|
||
static void test_power_off_drops_vbus_and_always_drops_the_efuse(void)
|
||
{
|
||
const uint32_t vb = 1u << PCA9555_BIT_VBUS_EN;
|
||
ASSERT_HAS(send("{\"seq\":1,\"cmd\":\"power\",\"rail\":1,\"on\":true}"), "\"ok\":true");
|
||
ASSERT_HAS(send("{\"seq\":2,\"cmd\":\"vbus\",\"rail\":1,\"on\":true}"), "\"ok\":true");
|
||
ASSERT_HAS(send("{\"seq\":3,\"cmd\":\"power\",\"rail\":1,\"on\":false}"), "\"ok\":true");
|
||
TEST_ASSERT_EQUAL_HEX8(0x00, latch(1) & vb);
|
||
TEST_ASSERT_EQUAL_INT(0, en_pin(1));
|
||
|
||
ASSERT_HAS(send("{\"seq\":4,\"cmd\":\"power\",\"rail\":2,\"on\":true}"), "\"ok\":true");
|
||
ASSERT_HAS(send("{\"seq\":5,\"cmd\":\"vbus\",\"rail\":2,\"on\":true}"), "\"ok\":true");
|
||
sim_i2c_inject_nak_ch(rail_cfg(2)->mux_ch, PCA9555_ADDR, 100);
|
||
ASSERT_HAS(send("{\"seq\":6,\"cmd\":\"power\",\"rail\":2,\"on\":false}"), "\"err\":\"io\"");
|
||
TEST_ASSERT_EQUAL_INT(0, en_pin(2)); /* 直供照样断了 */
|
||
TEST_ASSERT_TRUE(slot_vbus_is_on(2)); /* 影子如实留在 on(slot.h:倒向安全的一侧) */
|
||
|
||
/* ⭐ 2026-09-27 上板实测的 bug:器件不应答被报成 HAL_ERR_STATE(IDF 5.4 的 NACK 码),
|
||
命令层又把 STATE 当成「执行器忙」回了 busy —— 上位机会以为没执行。总线上的失败一律 io。 */
|
||
ASSERT_HAS(send("{\"seq\":7,\"cmd\":\"power\",\"rail\":3,\"on\":true}"), "\"ok\":true");
|
||
ASSERT_HAS(send("{\"seq\":8,\"cmd\":\"vbus\",\"rail\":3,\"on\":true}"), "\"ok\":true");
|
||
sim_i2c_inject_err_ch(rail_cfg(3)->mux_ch, PCA9555_ADDR, 100, HAL_ERR_STATE);
|
||
const char *r = send("{\"seq\":9,\"cmd\":\"power\",\"rail\":3,\"on\":false}");
|
||
ASSERT_HAS(r, "\"err\":\"io\"");
|
||
ASSERT_LACKS(r, "busy");
|
||
TEST_ASSERT_EQUAL_INT(0, en_pin(3));
|
||
ASSERT_HAS(send("{\"seq\":10,\"cmd\":\"vbus\",\"rail\":3,\"on\":false}"), "\"err\":\"io\"");
|
||
}
|
||
|
||
/* A15:只有「真的开成 / 关成了」才改意图(下次开机按它恢复);被拒的、判重命中的都不改。 */
|
||
static int g_intent_n;
|
||
static int g_intent_rail;
|
||
static bool g_intent_on;
|
||
static void intent_spy(void *ctx, int rail, bool on)
|
||
{
|
||
(void)ctx;
|
||
g_intent_n++;
|
||
g_intent_rail = rail;
|
||
g_intent_on = on;
|
||
}
|
||
|
||
static void test_power_records_intent_only_when_it_took_effect(void)
|
||
{
|
||
g_intent_n = 0;
|
||
const cmd_env_t env = { .intent = intent_spy };
|
||
cmd_init(&env);
|
||
|
||
ASSERT_HAS(send("{\"seq\":1,\"cmd\":\"power\",\"rail\":3,\"on\":true}"), "\"ok\":true");
|
||
TEST_ASSERT_EQUAL_INT(1, g_intent_n);
|
||
TEST_ASSERT_EQUAL_INT(3, g_intent_rail);
|
||
TEST_ASSERT_TRUE(g_intent_on);
|
||
|
||
send("{\"seq\":1,\"cmd\":\"power\",\"rail\":3,\"on\":true}"); /* 判重命中:不再执行 */
|
||
TEST_ASSERT_EQUAL_INT(1, g_intent_n);
|
||
|
||
set_present(4, false);
|
||
ASSERT_HAS(send("{\"seq\":2,\"cmd\":\"power\",\"rail\":4,\"on\":true}"), "\"err\":\"empty\"");
|
||
TEST_ASSERT_EQUAL_INT(1, g_intent_n);
|
||
|
||
ASSERT_HAS(send("{\"seq\":3,\"cmd\":\"power\",\"rail\":3,\"on\":false}"), "\"ok\":true");
|
||
TEST_ASSERT_EQUAL_INT(2, g_intent_n);
|
||
TEST_ASSERT_FALSE(g_intent_on);
|
||
|
||
/* 硬重启不是意图(那是事故处理),不许碰它。 */
|
||
ASSERT_HAS(send("{\"seq\":4,\"cmd\":\"power\",\"rail\":1,\"on\":true}"), "\"ok\":true");
|
||
ASSERT_HAS(send("{\"seq\":5,\"cmd\":\"power_cycle\",\"rail\":1}"), "\"ok\":true");
|
||
TEST_ASSERT_EQUAL_INT(3, g_intent_n);
|
||
cmd_init(NULL);
|
||
}
|
||
|
||
/* 风扇:手动值只是下限,0 交回自动。 */
|
||
static void test_fan_floor(void)
|
||
{
|
||
const char *r = send("{\"seq\":1,\"cmd\":\"fan\",\"duty\":700}");
|
||
ASSERT_HAS(r, "\"floor\":700");
|
||
TEST_ASSERT_EQUAL_UINT16(700, t_thermal_floor());
|
||
ASSERT_HAS(send("{\"seq\":2,\"cmd\":\"fan\",\"duty\":0}"), "\"floor\":0");
|
||
TEST_ASSERT_EQUAL_UINT16(0, t_thermal_floor());
|
||
}
|
||
|
||
/* ---- 只读命令 ------------------------------------------------------------ */
|
||
|
||
/* events 分页:一次最多 CMD_EVENTS_MAX 条,逐条带序号,游标接得上;RAM 里的先落盘。 */
|
||
static void test_events_pages_with_contiguous_seqs(void)
|
||
{
|
||
for (int i = 0; i < 11; i++) {
|
||
evlog_put(1, LOG_EV_PG_CHANGE, (uint8_t)i, 3900 + i, 100 * i);
|
||
}
|
||
/* 这 11 条还在 RAM 里:events 自己会先落盘。 */
|
||
evlog_stats_t es;
|
||
evlog_stats(&es);
|
||
TEST_ASSERT_TRUE(es.pending > 0);
|
||
|
||
const char *r = send("{\"cmd\":\"events\"}");
|
||
ASSERT_HAS(r, "\"oldest\":0");
|
||
ASSERT_HAS(r, "\"next\":12"); /* BOOT + 11 */
|
||
ASSERT_HAS(r, "[0,"); /* 第一条是 BOOT,序号 0 */
|
||
ASSERT_HAS(r, "[7,");
|
||
ASSERT_LACKS(r, "[8,");
|
||
|
||
r = send("{\"cmd\":\"events\",\"from\":8}");
|
||
ASSERT_HAS(r, "[8,");
|
||
ASSERT_HAS(r, "[11,");
|
||
ASSERT_HAS(r, ",1,8,10,3910,1000]"); /* 第 10 条 PG_CHANGE:rail 1、type 8、arg 10 */
|
||
|
||
r = send("{\"cmd\":\"events\",\"from\":12}");
|
||
ASSERT_HAS(r, "\"ev\":[]");
|
||
ASSERT_HAS(send("{\"cmd\":\"events\",\"from\":-1}"), "\"err\":\"arg\"");
|
||
}
|
||
|
||
/* status:满配(两份快照都给)装得进 CMD_OUT_MAX;读不到的量是 null,不是 0。 */
|
||
static void test_status_fits_and_reports_unknowns_as_null(void)
|
||
{
|
||
static safety_snap_t s;
|
||
static thermal_snap_t t;
|
||
memset(&s, 0, sizeof(s));
|
||
memset(&t, 0, sizeof(t));
|
||
for (int i = 0; i < N; i++) {
|
||
s.rail_ok[i] = true;
|
||
s.io_ok[i] = true;
|
||
s.rail[i].bus_mv = -3999999; /* 故意取最长的数字,量上界 */
|
||
s.rail[i].current_ua = -3999999;
|
||
}
|
||
s.rail_ok[1] = false; /* 第 2 路这一圈没读到 */
|
||
s.io_ok[3] = false; /* 第 4 路 PCA9555 失联 */
|
||
s.input_ok = true;
|
||
s.input.bus_mv = 12030;
|
||
s.input.current_ua = 1234567;
|
||
s.cycles = 4000000000u;
|
||
t.temp_ok[0] = true;
|
||
t.temp_mc[0] = 41500;
|
||
t.fan[0].rpm_valid = true;
|
||
t.fan[0].rpm = 3300;
|
||
t.duty = 650;
|
||
t.fan_en = true;
|
||
const cmd_env_t env = { .safety = &s, .thermal = &t };
|
||
cmd_init(&env);
|
||
|
||
/* 上游 VBUS:没配过的脚读不到 ⇒ null;配成输入后如实报高低。 */
|
||
ASSERT_HAS(send("{\"cmd\":\"status\"}"), "\"host\":null");
|
||
TEST_ASSERT_EQUAL(HAL_OK, hal_gpio_config(BOARD_PIN_UP_VBUS_DET, HAL_GPIO_INPUT));
|
||
sim_gpio_drive(BOARD_PIN_UP_VBUS_DET, 0);
|
||
ASSERT_HAS(send("{\"cmd\":\"status\"}"), "\"host\":false");
|
||
sim_gpio_drive(BOARD_PIN_UP_VBUS_DET, 1); /* 留着高电平:下面量满配长度 */
|
||
|
||
const char *r = send("{\"seq\":4000000000,\"cmd\":\"status\"}");
|
||
ASSERT_HAS(r, "\"host\":true");
|
||
ASSERT_HAS(r, "{\"seq\":4000000000,\"ok\":true,\"fw\":\"" PHONERAIL_FW_VERSION "\"");
|
||
ASSERT_HAS(r, "\"in\":{\"mv\":12030,\"ua\":1234567}");
|
||
ASSERT_HAS(r, "{\"r\":2,\"pr\":true,\"en\":false,\"vb\":false,\"set\":3900,\"mv\":null,\"ua\":null");
|
||
ASSERT_HAS(r, "\"r\":4,");
|
||
ASSERT_HAS(r, "\"pg\":null,\"flt\":null");
|
||
ASSERT_HAS(r, "\"temp\":[41500,null]");
|
||
ASSERT_HAS(r, "\"rpm\":[3300,null]");
|
||
ASSERT_HAS(r, "\"st\":\"idle\"");
|
||
TEST_ASSERT_EQUAL_CHAR('}', r[strlen(r) - 1]);
|
||
printf("status 满配 %u 字节(上限 %u)\n", (unsigned)strlen(r), (unsigned)CMD_OUT_MAX);
|
||
TEST_ASSERT_TRUE(strlen(r) < CMD_OUT_MAX * 3 / 4); /* 留四分之一给以后加字段 */
|
||
|
||
/* 不给快照:只报影子,照样是一行合法回应。 */
|
||
cmd_init(NULL);
|
||
r = send("{\"cmd\":\"status\"}");
|
||
ASSERT_LACKS(r, "\"in\"");
|
||
ASSERT_LACKS(r, "\"temp\"");
|
||
ASSERT_HAS(r, "\"r\":5,");
|
||
}
|
||
|
||
/* 回应装不下:只读命令如实报 overflow;已经执行了的命令回一条不带附加字段的 ok。 */
|
||
static void test_small_output_buffer(void)
|
||
{
|
||
char small[64];
|
||
size_t n = cmd_handle_line(CMD_LINK_TEST, "{\"seq\":1,\"cmd\":\"status\"}", small,
|
||
sizeof(small));
|
||
TEST_ASSERT_EQUAL_size_t(strlen(small), n);
|
||
TEST_ASSERT_EQUAL_STRING("{\"seq\":1,\"ok\":false,\"err\":\"overflow\"}", small);
|
||
|
||
char tiny[24];
|
||
n = cmd_handle_line(CMD_LINK_TEST, "{\"seq\":2,\"cmd\":\"fan\",\"duty\":300}", tiny,
|
||
sizeof(tiny));
|
||
TEST_ASSERT_EQUAL_STRING("{\"seq\":2,\"ok\":true}", tiny);
|
||
TEST_ASSERT_EQUAL_UINT16(300, t_thermal_floor());
|
||
}
|
||
|
||
int main(void)
|
||
{
|
||
UNITY_BEGIN();
|
||
RUN_TEST(test_cmd_numbers_are_frozen);
|
||
RUN_TEST(test_same_seq_twice_executes_once_across_links);
|
||
RUN_TEST(test_retransmit_after_the_sequence_finished_is_still_dropped);
|
||
RUN_TEST(test_a_rejected_command_stays_rejected_on_retransmit);
|
||
RUN_TEST(test_same_seq_different_command_is_not_executed);
|
||
RUN_TEST(test_window_is_by_value_so_a_restarted_host_is_not_blocked);
|
||
RUN_TEST(test_seq_rules);
|
||
RUN_TEST(test_voltage_out_of_range_is_rejected_and_logged);
|
||
RUN_TEST(test_voltage_in_range_is_written_under_the_bus_lock);
|
||
RUN_TEST(test_other_range_checks);
|
||
RUN_TEST(test_argument_errors_are_not_logged);
|
||
RUN_TEST(test_every_rail_command_is_refused_on_an_empty_slot);
|
||
RUN_TEST(test_combo_keys_land_on_the_named_rail);
|
||
RUN_TEST(test_busy_rail_refuses_a_second_sequence);
|
||
RUN_TEST(test_reboots_are_logged_with_kind_and_link);
|
||
RUN_TEST(test_vbus_needs_the_rail_powered);
|
||
RUN_TEST(test_power_off_drops_vbus_and_always_drops_the_efuse);
|
||
RUN_TEST(test_power_records_intent_only_when_it_took_effect);
|
||
RUN_TEST(test_fan_floor);
|
||
RUN_TEST(test_events_pages_with_contiguous_seqs);
|
||
RUN_TEST(test_status_fits_and_reports_unknowns_as_null);
|
||
RUN_TEST(test_small_output_buffer);
|
||
return UNITY_END();
|
||
}
|