/* A12:单一命令处理器(需求 §10.8,计划 §6.3)。验收原话三条: ① 同序号发两遍只执行一次; ② 越界命令被拒且记事件; ③ 空槽(`PRSNT#` 高)的命令一律拒。 和 A9 的测试同一个口径:断言一律打在**线上真实发生的事**上 —— 假 PCA9555 的锁存器与写事务计数、DAC 寄存器、`EFUSE_EN` 引脚电平、落盘的事件 —— 不看处理器自己回了什么。「回了 ok」和「真的按了一次」是两件事, 判重写错的现象恰恰是回应完全正常、手机被按了两遍。 */ #include #include #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_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:倒向安全的一侧) */ } /* 风扇:手动值只是下限,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); const char *r = send("{\"seq\":4000000000,\"cmd\":\"status\"}"); 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_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(); }