/* A12:单一命令处理器(需求 §10.8,计划 §6.3)。验收原话三条: ① 同序号发两遍只执行一次; ② 越界命令被拒且记事件; ③ 空槽(`PRSNT#` 高)的命令一律拒。 和 A9 的测试同一个口径:断言一律打在**线上真实发生的事**上 —— 假 PCA9555 的锁存器与写事务计数、DAC 寄存器、`EFUSE_EN` 引脚电平、落盘的事件 —— 不看处理器自己回了什么。「回了 ok」和「真的按了一次」是两件事, 判重写错的现象恰恰是回应完全正常、手机被按了两遍。 */ #include #include #include "board_pins.h" #include "chgmode.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); chgmode_reset(); 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_FLASH_MODE); /* 0.1.5 刷机模式 */ TEST_ASSERT_EQUAL_INT(15, 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()); } /* ---- 0.1.5 刷机模式(flash_mode) ----------------------------------------- */ static void test_flash_mode_command(void) { /* 关着电的路不许开(与 vbus 同一条规矩),记拒绝事件。 */ ASSERT_HAS(send("{\"seq\":1,\"cmd\":\"flash_mode\",\"rail\":1,\"on\":true}"), "\"err\":\"state\""); TEST_ASSERT_EQUAL_INT(1, count_events(LOG_EV_CMD_REJECT)); ASSERT_HAS(send("{\"seq\":2,\"cmd\":\"power\",\"rail\":1,\"on\":true}"), "\"ok\":true"); ASSERT_HAS(send("{\"seq\":3,\"cmd\":\"flash_mode\",\"rail\":1,\"on\":true}"), "\"left\":3600"); /* 默认 60 分钟 */ TEST_ASSERT_TRUE(chgmode_active(1, hal_time_now_ms())); evlog_rec_t r; TEST_ASSERT_TRUE(find_event(LOG_EV_CHG_MODE, 0, &r)); TEST_ASSERT_EQUAL_UINT8(1, r.rail); TEST_ASSERT_EQUAL_UINT8(CHG_MODE_DEFAULT_MIN, r.arg); /* status 每一路报剩余秒数 */ ASSERT_HAS(send("{\"cmd\":\"status\"}"), "\"chg\":3600"); /* 越界 / 参数错 */ ASSERT_HAS(send("{\"seq\":4,\"cmd\":\"flash_mode\",\"rail\":1,\"on\":true,\"min\":0}"), "\"err\":\"range\""); ASSERT_HAS(send("{\"seq\":5,\"cmd\":\"flash_mode\",\"rail\":1,\"on\":true,\"min\":121}"), "\"err\":\"range\""); ASSERT_HAS(send("{\"seq\":6,\"cmd\":\"flash_mode\",\"rail\":1,\"on\":false,\"min\":5}"), "\"err\":\"arg\""); ASSERT_HAS(send("{\"seq\":7,\"cmd\":\"flash_mode\",\"rail\":1}"), "\"err\":\"arg\""); /* 续期:本路再开一次不占新名额 */ ASSERT_HAS(send("{\"seq\":8,\"cmd\":\"flash_mode\",\"rail\":1,\"on\":true,\"min\":120}"), "\"left\":7200"); /* 第三路被拒 */ ASSERT_HAS(send("{\"seq\":9,\"cmd\":\"power\",\"rail\":2,\"on\":true}"), "\"ok\":true"); ASSERT_HAS(send("{\"seq\":10,\"cmd\":\"power\",\"rail\":3,\"on\":true}"), "\"ok\":true"); ASSERT_HAS(send("{\"seq\":11,\"cmd\":\"flash_mode\",\"rail\":2,\"on\":true,\"min\":10}"), "\"ok\":true"); ASSERT_HAS(send("{\"seq\":12,\"cmd\":\"flash_mode\",\"rail\":3,\"on\":true}"), "\"err\":\"state\""); TEST_ASSERT_FALSE(chgmode_active(3, hal_time_now_ms())); /* 关:回 left 0、记一条 arg=0;腾出的名额第三路就能用了 */ ASSERT_HAS(send("{\"seq\":13,\"cmd\":\"flash_mode\",\"rail\":1,\"on\":false}"), "\"left\":0"); TEST_ASSERT_TRUE(find_event(LOG_EV_CHG_MODE, 3, &r)); /* 开1、续期、开2、关1 */ TEST_ASSERT_EQUAL_UINT8(CHG_EV_OFF, r.arg); ASSERT_HAS(send("{\"seq\":14,\"cmd\":\"flash_mode\",\"rail\":3,\"on\":true}"), "\"ok\":true"); } /* 关这一路电 = 刷机模式一并退出(不白占名额),记一条 arg=0。 */ static void test_power_off_ends_flash_mode(void) { ASSERT_HAS(send("{\"seq\":1,\"cmd\":\"power\",\"rail\":4,\"on\":true}"), "\"ok\":true"); ASSERT_HAS(send("{\"seq\":2,\"cmd\":\"flash_mode\",\"rail\":4,\"on\":true}"), "\"ok\":true"); ASSERT_HAS(send("{\"seq\":3,\"cmd\":\"power\",\"rail\":4,\"on\":false}"), "\"ok\":true"); TEST_ASSERT_FALSE(chgmode_active(4, hal_time_now_ms())); TEST_ASSERT_EQUAL_INT(2, count_events(LOG_EV_CHG_MODE)); evlog_rec_t r; TEST_ASSERT_TRUE(find_event(LOG_EV_CHG_MODE, 1, &r)); TEST_ASSERT_EQUAL_UINT8(CHG_EV_OFF, r.arg); } /* 自然到期:执行器循环里的 cmd_chgmode_tick 记**一条** arg=255,之后不再重复记。 */ static void test_flash_mode_expiry_is_logged_once(void) { ASSERT_HAS(send("{\"seq\":1,\"cmd\":\"power\",\"rail\":5,\"on\":true}"), "\"ok\":true"); ASSERT_HAS(send("{\"seq\":2,\"cmd\":\"flash_mode\",\"rail\":5,\"on\":true,\"min\":1}"), "\"left\":60"); cmd_chgmode_tick(hal_time_now_ms()); TEST_ASSERT_EQUAL_INT(1, count_events(LOG_EV_CHG_MODE)); sim_time_advance_ms(60000 + CHG_MODE_GRACE_MS); cmd_chgmode_tick(hal_time_now_ms()); cmd_chgmode_tick(hal_time_now_ms() + 100); TEST_ASSERT_EQUAL_INT(2, count_events(LOG_EV_CHG_MODE)); evlog_rec_t r; TEST_ASSERT_TRUE(find_event(LOG_EV_CHG_MODE, 1, &r)); TEST_ASSERT_EQUAL_UINT8(5, r.rail); TEST_ASSERT_EQUAL_UINT8(CHG_EV_EXPIRED, r.arg); ASSERT_HAS(send("{\"cmd\":\"status\"}"), "\"chg\":0"); } /* 0.1.13:安全判据 / 拔刀片把这一路关了(不经过 power 命令)⇒ 下一次 tick 退出刷机模式、记 arg=254、 让出名额;硬重启中间 eFuse 关着的那几秒不算。 */ static void test_flash_mode_ends_when_the_rail_loses_power(void) { ASSERT_HAS(send("{\"seq\":1,\"cmd\":\"power\",\"rail\":1,\"on\":true}"), "\"ok\":true"); ASSERT_HAS(send("{\"seq\":2,\"cmd\":\"power\",\"rail\":2,\"on\":true}"), "\"ok\":true"); ASSERT_HAS(send("{\"seq\":3,\"cmd\":\"power\",\"rail\":3,\"on\":true}"), "\"ok\":true"); ASSERT_HAS(send("{\"seq\":4,\"cmd\":\"flash_mode\",\"rail\":1,\"on\":true}"), "\"ok\":true"); ASSERT_HAS(send("{\"seq\":5,\"cmd\":\"flash_mode\",\"rail\":2,\"on\":true}"), "\"ok\":true"); ASSERT_HAS(send("{\"seq\":6,\"cmd\":\"flash_mode\",\"rail\":3,\"on\":true}"), "\"err\":\"state\""); /* 硬重启:HR_OFF 期间 eFuse 关着,但这是序列的一部分 —— 不退。 */ ASSERT_HAS(send("{\"seq\":7,\"cmd\":\"power_cycle\",\"rail\":1}"), "\"ok\":true"); TEST_ASSERT_FALSE(rail_efuse_is_on(1)); cmd_chgmode_tick(hal_time_now_ms()); TEST_ASSERT_TRUE(chgmode_active(1, hal_time_now_ms())); /* 第 2 路被 t_safety 关了(这里直接拉低 EN 扮演):下一次 tick 退出。 */ TEST_ASSERT_EQUAL_INT(HAL_OK, rail_efuse_enable(2, false)); const int before = count_events(LOG_EV_CHG_MODE); cmd_chgmode_tick(hal_time_now_ms()); TEST_ASSERT_FALSE(chgmode_active(2, hal_time_now_ms())); TEST_ASSERT_EQUAL_INT(before + 1, count_events(LOG_EV_CHG_MODE)); evlog_rec_t r; TEST_ASSERT_TRUE(find_event(LOG_EV_CHG_MODE, before, &r)); TEST_ASSERT_EQUAL_UINT8(2, r.rail); TEST_ASSERT_EQUAL_UINT8(CHG_EV_DROPPED, r.arg); cmd_chgmode_tick(hal_time_now_ms() + 100); TEST_ASSERT_EQUAL_INT(before + 1, count_events(LOG_EV_CHG_MODE)); /* 只记一次 */ /* 名额让出来了。 */ ASSERT_HAS(send("{\"seq\":8,\"cmd\":\"flash_mode\",\"rail\":3,\"on\":true}"), "\"ok\":true"); } /* 0.1.6 加钟:只给开着的窗口加;剩余最多 120 分钟(超出截断);与 on / min 互斥;事件 arg = 0x80 | 分钟。 */ static void test_flash_mode_extend(void) { ASSERT_HAS(send("{\"seq\":1,\"cmd\":\"power\",\"rail\":2,\"on\":true}"), "\"ok\":true"); /* 没开着 ⇒ state(要重新开,不是加钟) */ ASSERT_HAS(send("{\"seq\":2,\"cmd\":\"flash_mode\",\"rail\":2,\"add\":30}"), "\"err\":\"state\""); ASSERT_HAS(send("{\"seq\":3,\"cmd\":\"flash_mode\",\"rail\":2,\"on\":true,\"min\":20}"), "\"left\":1200"); sim_time_advance_ms(5 * 60000); /* 过了 5 分钟,还剩 15 */ ASSERT_HAS(send("{\"seq\":4,\"cmd\":\"flash_mode\",\"rail\":2,\"add\":30}"), "\"left\":2700"); /* 15 + 30 */ evlog_rec_t r; TEST_ASSERT_TRUE(find_event(LOG_EV_CHG_MODE, 1, &r)); TEST_ASSERT_EQUAL_UINT8(CHG_EV_EXTEND | 30u, r.arg); /* 截断到 120 分钟 */ ASSERT_HAS(send("{\"seq\":5,\"cmd\":\"flash_mode\",\"rail\":2,\"add\":120}"), "\"left\":7200"); /* 越界 / 混用 */ ASSERT_HAS(send("{\"seq\":6,\"cmd\":\"flash_mode\",\"rail\":2,\"add\":0}"), "\"err\":\"range\""); ASSERT_HAS(send("{\"seq\":7,\"cmd\":\"flash_mode\",\"rail\":2,\"add\":121}"), "\"err\":\"range\""); ASSERT_HAS(send("{\"seq\":8,\"cmd\":\"flash_mode\",\"rail\":2,\"add\":10,\"on\":true}"), "\"err\":\"arg\""); ASSERT_HAS(send("{\"seq\":9,\"cmd\":\"flash_mode\",\"rail\":2,\"add\":10,\"min\":10}"), "\"err\":\"arg\""); ASSERT_HAS(send("{\"seq\":10,\"cmd\":\"flash_mode\",\"rail\":2,\"add\":true}"), "\"err\":\"arg\""); /* 加钟不占新名额:两路都开着时照样能加 */ ASSERT_HAS(send("{\"seq\":11,\"cmd\":\"power\",\"rail\":3,\"on\":true}"), "\"ok\":true"); ASSERT_HAS(send("{\"seq\":12,\"cmd\":\"flash_mode\",\"rail\":3,\"on\":true}"), "\"ok\":true"); ASSERT_HAS(send("{\"seq\":13,\"cmd\":\"flash_mode\",\"rail\":3,\"add\":10}"), "\"left\":4200"); } 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_flash_mode_command); RUN_TEST(test_power_off_ends_flash_mode); RUN_TEST(test_flash_mode_expiry_is_logged_once); RUN_TEST(test_flash_mode_ends_when_the_rail_loses_power); RUN_TEST(test_flash_mode_extend); 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(); }