固件 0.1.14~0.1.16-c5:Hub 按需启停(hubctl)、开机长按统一 5 s
Co-Authored-By: Claude Opus 5.5 <[email protected]>
这个提交包含在:
+34
-1
@@ -1,5 +1,38 @@
|
|||||||
# 交接
|
# 交接
|
||||||
更新:2026-09-28 23:20 固件 **0.1.13-c5**(断电自动退出刷机模式);板 B 经 OTA 推上;rack 已部署
|
更新:2026-09-30 固件 **0.1.16-c5**(开机键统一 5 s);板 B 经 OTA 推上;rack 已部署
|
||||||
|
|
||||||
|
## 0.1.15 / 0.1.16:开机长按 3 s 试验失败,所有开机操作统一 5 s(2026-09-30)
|
||||||
|
- 0.1.15 把 `ACT_KEY_MS_ON` 5000 → 3000:板 B 第 2 路 `power_cycle` 三轮全是「按住 330~390 mA、松手掉回 0」⇒ `no-boot`(事件 19);
|
||||||
|
同一台手工 `key pwr 5000` 一次开起来(松手后稳在 ~250 mA)。⇒ 小米8 开机键 2 s、3 s 都不够,**别再往下调**。
|
||||||
|
- 0.1.16 改回 5000;rack 单路「更多 → 开机」原来发的是 `ms:2000`(多半从来就开不了机),改成 5000、文案「长按电源 5 秒」。
|
||||||
|
- 上板:第 3 路 `power_cycle` → hr-boot 5 s → 松键后 ~210 mA 平台 → `res ok`。
|
||||||
|
|
||||||
|
## 0.1.14:Hub 按需启停(2026-09-29,用户定「方案一:GPIO8 按住复位」)
|
||||||
|
- 起因:五路 VBUS 全关时,上行连着电脑 Hub 仍是活动态(第 6 口的 ESP32 USB 让电脑挂不起它),约多耗 1 W。
|
||||||
|
- **推翻 R2 D13 代价 2 / R-11「运行期不许动 `HUB_RST#`」**,理由与退路见 `刀片式方案规划-R2.md` §8.2a、`main/hubctl.h` 文件头。
|
||||||
|
- 新模块 `hubctl.{h,c}`:执行器任务每拍 `hubctl_tick()`(空闲时 ≤100 ms 一拍)。开的理由(按优先级)
|
||||||
|
`mode` 常开 > `vbus` 任一路 VBUS > `flash` 刷机模式 > `act` 执行器忙 > `hold` 保持窗口 > `link` MQTT 断满 60 s;
|
||||||
|
都没有时再给 `idle` 10 分钟(`HUB_IDLE_MS`),从最后一次「需要」算起 —— 启动、`UP_VBUS_DET` 上升沿、USB 上来一条命令都算。
|
||||||
|
关 = GPIO8 配输出拉低;开 = 配成输入靠 R716 拉高,**从不驱高**。
|
||||||
|
- **命令 15 `hub`**(两条链路都收,会动东西 ⇒ 必须带 seq):
|
||||||
|
`{"cmd":"hub","mode":"auto"|"on"}` 改模式(NVS 键 `hub_mode`,默认 auto);
|
||||||
|
`{"cmd":"hub","min":30}` 保持 1~240 分钟;`{"cmd":"hub","min":0}` 取消保持并结束 idle 窗口(没别的理由下一拍就关,量功耗用)。
|
||||||
|
两个键可同时给。回应带 `"hub":{...}`。
|
||||||
|
- status 新增 `"hub":{"on":true,"mode":"auto","why":"vbus","hold":秒,"idle":秒}`(满配 1140 / 1536 字节)。
|
||||||
|
- **事件 22 `LOG_EV_HUB`**(rail 0):arg 0 = 关、1 = 开、0x10|模式 = 改模式;bus_mv = 开的理由(`hub_why_t`,冻结:
|
||||||
|
0 off / 1 mode / 2 vbus / 3 flash / 4 act / 5 hold / 6 link / 7 idle)。
|
||||||
|
- `bringup.c` 的上电脉冲不变(仍只在 POWERON);脉冲后 `hubctl_init()` 把脚放成输入。
|
||||||
|
- 测试:新增 `test_hubctl`(9 例:启动窗口、理由优先级、断网 60 s、电脑插上 / USB 命令刷新、保持与 min=0、
|
||||||
|
时钟回绕不复活、引脚从不写 1 且每次翻转只记一条、模式存盘、NVS 垃圾值回 auto),`test_cmd` +1(hub 命令)、
|
||||||
|
`test_evlog` / 冻结编号同步;**27 ctest 全绿**;IDF 构建通过(0xf5960,余 68%)。
|
||||||
|
- ✅ **板 B 实测(2026-09-29,0.1.14-c5,三路手机开着、VBUS 全关、上行插着电脑)**:Hub 开 / 关交替各 60 s,
|
||||||
|
看「底板损耗 = 输入 − 三路主板合计」(手机电流 ±100 mA 的抖动会淹没输入电流):开 **1.77 W**,关 0.69 / 0.72 / 0.80 W ⇒
|
||||||
|
**Hub 空转 ≈ 1.03 W**(12 V 输入电流 377 → 288 mA)。原始数据 `10.5.5.13:/root/phonerail-src/bench-hub-20260929.jsonl`,
|
||||||
|
脚本 `bench-hub.sh` 同目录。`hub min:0` 下一拍就关、`hub min:3` 立刻开(why = hold),事件 22 各记一条。
|
||||||
|
- ✅ **rack**:白名单放行 `hub`(mode auto/on、min 0~240);事件 22 命名(唤醒理由 / 休眠 / 模式改为…,mV 列置空);
|
||||||
|
单板页「Hub」一行:状态 + 按需 / 常开 + 「唤醒 30 分」+「马上休眠」;模拟板同步。pytest 50 例,已部署。
|
||||||
|
- ⏳ **没验证**:上行电脑不是本机也不是服务器,电脑那一侧 Hub 消失 / 重现、手机 VBUS 开后 adb 1~2 s 回来,都还没亲眼看过。
|
||||||
|
- ⚠️ 现在起板 B 没人用 USB 满 10 分钟 COM 口就消失 —— 要 USB 烧录 / 看日志,先在 rack 点「唤醒 30 分」(或 MQTT 发 `hub min`)。
|
||||||
|
|
||||||
## 0.1.13:这一路断了电 ⇒ 刷机模式一并退出(2026-09-28)
|
## 0.1.13:这一路断了电 ⇒ 刷机模式一并退出(2026-09-28)
|
||||||
- 起因:第 2 槽刀片带电拔走后,刷机模式还挂了约 48 分钟、占着「最多 2 路」的名额。`check_present` / SR-5 / 刀片不应答 /
|
- 起因:第 2 槽刀片带电拔走后,刷机模式还挂了约 48 分钟、占着「最多 2 路」的名额。`check_present` / SR-5 / 刀片不应答 /
|
||||||
|
|||||||
@@ -29,6 +29,7 @@ set(PHONERAIL_CORE_SRCS
|
|||||||
${FW_ROOT}/main/sched.c
|
${FW_ROOT}/main/sched.c
|
||||||
${FW_ROOT}/main/tasks.c
|
${FW_ROOT}/main/tasks.c
|
||||||
${FW_ROOT}/main/chgmode.c
|
${FW_ROOT}/main/chgmode.c
|
||||||
|
${FW_ROOT}/main/hubctl.c
|
||||||
${FW_ROOT}/main/t_safety.c
|
${FW_ROOT}/main/t_safety.c
|
||||||
${FW_ROOT}/main/t_actuator.c
|
${FW_ROOT}/main/t_actuator.c
|
||||||
${FW_ROOT}/main/t_thermal.c
|
${FW_ROOT}/main/t_thermal.c
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ idf_component_register(
|
|||||||
"sched.c"
|
"sched.c"
|
||||||
"tasks.c"
|
"tasks.c"
|
||||||
"chgmode.c"
|
"chgmode.c"
|
||||||
|
"hubctl.c"
|
||||||
"t_safety.c"
|
"t_safety.c"
|
||||||
"t_actuator.c"
|
"t_actuator.c"
|
||||||
"t_thermal.c"
|
"t_thermal.c"
|
||||||
|
|||||||
@@ -16,6 +16,7 @@
|
|||||||
#include "hal_i2c.h"
|
#include "hal_i2c.h"
|
||||||
#include "hal_lock.h"
|
#include "hal_lock.h"
|
||||||
#include "hal_time.h"
|
#include "hal_time.h"
|
||||||
|
#include "hubctl.h"
|
||||||
#include "i2cbus.h"
|
#include "i2cbus.h"
|
||||||
#include "ina226.h"
|
#include "ina226.h"
|
||||||
#include "pca9555.h"
|
#include "pca9555.h"
|
||||||
@@ -231,6 +232,7 @@ static void task_actuator(void *arg)
|
|||||||
cmd_msg_t m;
|
cmd_msg_t m;
|
||||||
for (;;) {
|
for (;;) {
|
||||||
cmd_chgmode_tick(hal_time_now_ms()); /* 刷机模式到期清理 + 记事件(chgmode.h:只在本任务写) */
|
cmd_chgmode_tick(hal_time_now_ms()); /* 刷机模式到期清理 + 记事件(chgmode.h:只在本任务写) */
|
||||||
|
hubctl_tick(hal_time_now_ms()); /* Hub 按需启停(hubctl.h:只在本任务写)。空闲时 ≤100 ms 一拍 */
|
||||||
const TickType_t t = pdMS_TO_TICKS(t_actuator_step());
|
const TickType_t t = pdMS_TO_TICKS(t_actuator_step());
|
||||||
/* ⚠️ `CONFIG_FREERTOS_HZ=100` ⇒ 一个 tick 是 10 ms,`pdMS_TO_TICKS(5)`
|
/* ⚠️ `CONFIG_FREERTOS_HZ=100` ⇒ 一个 tick 是 10 ms,`pdMS_TO_TICKS(5)`
|
||||||
是 **0**,而「等 0 tick」只是查一眼就回来 —— 那会让这个中优先级任务
|
是 **0**,而「等 0 tick」只是查一眼就回来 —— 那会让这个中优先级任务
|
||||||
@@ -342,6 +344,43 @@ static void want_save(void *ctx, int rail, bool on)
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* ---- Hub 模式的持久化(0.1.14,hubctl.h)-------------------------------------
|
||||||
|
同一个命名空间,一个字节。新板 / 读不到 = AUTO(用户 2026-09-29 定:默认按需)。 */
|
||||||
|
#define HUB_KEY "hub_mode"
|
||||||
|
|
||||||
|
static uint8_t hub_mode_load(void)
|
||||||
|
{
|
||||||
|
nvs_handle_t h;
|
||||||
|
uint8_t v = HUB_MODE_AUTO;
|
||||||
|
if (nvs_open(WANT_NS, NVS_READONLY, &h) == ESP_OK) {
|
||||||
|
if (nvs_get_u8(h, HUB_KEY, &v) != ESP_OK || v >= HUB_MODE_COUNT) {
|
||||||
|
v = HUB_MODE_AUTO;
|
||||||
|
}
|
||||||
|
nvs_close(h);
|
||||||
|
}
|
||||||
|
return v;
|
||||||
|
}
|
||||||
|
|
||||||
|
static void hub_mode_save(void *ctx, hub_mode_t mode)
|
||||||
|
{
|
||||||
|
(void)ctx;
|
||||||
|
if (hub_mode_load() == (uint8_t)mode) {
|
||||||
|
return; /* 没变就不写 flash */
|
||||||
|
}
|
||||||
|
nvs_handle_t h;
|
||||||
|
esp_err_t e = nvs_open(WANT_NS, NVS_READWRITE, &h);
|
||||||
|
if (e == ESP_OK) {
|
||||||
|
e = nvs_set_u8(h, HUB_KEY, (uint8_t)mode);
|
||||||
|
if (e == ESP_OK) {
|
||||||
|
e = nvs_commit(h);
|
||||||
|
}
|
||||||
|
nvs_close(h);
|
||||||
|
}
|
||||||
|
if (e != ESP_OK) {
|
||||||
|
ESP_LOGW(TAG, "Hub 模式没存进 NVS:%s(这次照样生效)", esp_err_to_name(e));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
static void log_report(const bringup_report_t *r)
|
static void log_report(const bringup_report_t *r)
|
||||||
{
|
{
|
||||||
ESP_LOGI(TAG, "启动:降级位 0x%02x,空槽 0x%02x,盲跑 0x%02x,PCA 失联 0x%02x,PD %u mA → 上限 %u mW",
|
ESP_LOGI(TAG, "启动:降级位 0x%02x,空槽 0x%02x,盲跑 0x%02x,PCA 失联 0x%02x,PD %u mA → 上限 %u mW",
|
||||||
@@ -441,6 +480,12 @@ void app_main(void)
|
|||||||
ESP_LOGE(TAG, "风扇起不来:%s —— 没有主动散热", hal_err_str(err));
|
ESP_LOGE(TAG, "风扇起不来:%s —— 没有主动散热", hal_err_str(err));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Hub 按需启停(0.1.14)。放开 `HUB_RST#`、启动算一次「需要」⇒ 至少 HUB_IDLE_MS 之后才可能关。
|
||||||
|
必须在执行器任务起来之前:它的第一拍就会调 `hubctl_tick()`。 */
|
||||||
|
hubctl_init((hub_mode_t)hub_mode_load(), hub_mode_save, NULL, hal_time_now_ms());
|
||||||
|
ESP_LOGI(TAG, "Hub 模式 %s(无人用 USB %u 分钟后按住复位)", hub_mode_str(hubctl_mode()),
|
||||||
|
(unsigned)(HUB_IDLE_MS / 60000u));
|
||||||
|
|
||||||
/* 执行器:PG 从巡检的快照里取,本任务自己不碰 I²C(t_actuator.h 开头那一段)。
|
/* 执行器:PG 从巡检的快照里取,本任务自己不碰 I²C(t_actuator.h 开头那一段)。
|
||||||
⚠️ 必须在 `spawn(TASK_SAFETY, ...)` 之前:判据要关路时会先 `t_actuator_abort()`,
|
⚠️ 必须在 `spawn(TASK_SAFETY, ...)` 之前:判据要关路时会先 `t_actuator_abort()`,
|
||||||
而没 init 的执行器只会直接返回,那时序列还在跑,电关掉几秒又会被抬回来。 */
|
而没 init 的执行器只会直接返回,那时序列还在跑,电关掉几秒又会被抬回来。 */
|
||||||
|
|||||||
@@ -48,9 +48,9 @@ extern "C" {
|
|||||||
#define BOARD_PIN_INA_ALERT_N 38 /* 模组脚 31 → U13.3 */
|
#define BOARD_PIN_INA_ALERT_N 38 /* 模组脚 31 → U13.3 */
|
||||||
|
|
||||||
/* USB2517I 的复位,上拉到 +3V3_HUB ⇒ 低有效。
|
/* USB2517I 的复位,上拉到 +3V3_HUB ⇒ 低有效。
|
||||||
⛔⛔ **运行期一个字都不许写它**(R2 D13 代价 2):ESP32 自己的控制台与烧录口
|
ESP32 自己的控制台与烧录口就挂在这个 Hub 的第 6 个下行口上,复位 Hub = 自断控制台。
|
||||||
就挂在这个 Hub 的第 6 个下行口上,误复位 = 自断控制台,而且下一次烧录也进不来。
|
上电复位时脉冲一次(bringup.c 的 `hub_pulse()`);0.1.14 起运行期**只有 hubctl.c 许碰它**
|
||||||
只在**上电复位**时脉冲一次(其余复位原因不碰,见 bringup.c 的 `hub_pulse()`);真出事的退路是 J33 那个 5P 应急串口。 */
|
(没人用 USB 时按住复位省约 1 W,退路见 hubctl.h);最后的退路是 J33 那个 5P 应急串口。 */
|
||||||
#define BOARD_PIN_HUB_RST_N 8 /* 模组脚 12 → U32.43 */
|
#define BOARD_PIN_HUB_RST_N 8 /* 模组脚 12 → U32.43 */
|
||||||
|
|
||||||
#define BOARD_PIN_UP_VBUS_DET 6 /* 模组脚 6 → R721/R722 分压,上游 USB-C 插没插 */
|
#define BOARD_PIN_UP_VBUS_DET 6 /* 模组脚 6 → R721/R722 分压,上游 USB-C 插没插 */
|
||||||
|
|||||||
+7
-8
@@ -73,16 +73,14 @@ static hal_err_t pd_fn(void *arg)
|
|||||||
|
|
||||||
/* ---- 第一段 -------------------------------------------------------------- */
|
/* ---- 第一段 -------------------------------------------------------------- */
|
||||||
|
|
||||||
/* USB2517I 的复位脉冲 —— **整个固件里唯一碰 `HUB_RST#` 的地方,而且只在启动调一次**
|
/* USB2517I 的复位脉冲 —— 启动时的那一次(A15 从 app_main.c 搬过来的)。
|
||||||
(A15 从 app_main.c 搬过来的,理由原样保留)。
|
|
||||||
|
|
||||||
低有效(板上 R716 上拉到 +3V3_HUB),给 1 ms 足够:手册要的是几十微秒量级。
|
低有效(板上 R716 上拉到 +3V3_HUB),给 1 ms 足够:手册要的是几十微秒量级。
|
||||||
|
|
||||||
⛔⛔ **运行期一个字都不许再写它**(R2 D13 代价 2)。ESP32 自己的
|
⚠️ 运行期**只有 hubctl.c 许碰它**(0.1.14 起按需启停,推翻了 R2 D13 代价 2 的
|
||||||
USB-Serial-JTAG 就挂在这个 Hub 的第 6 个下行口上,所以复位 Hub =
|
「运行期一个字都不许写」,退路见 hubctl.h)。ESP32 自己的 USB-Serial-JTAG 就挂在
|
||||||
**自己把自己的控制台拔了**,而且下一次 `idf.py flash` 也进不来
|
这个 Hub 的第 6 个下行口上,所以复位 Hub = 自己把自己的控制台拔了 ——
|
||||||
—— 到那时候唯一的退路是 J33 那个 5P 应急串口(UART0,IO43/IO44),
|
别处写一行 "Hub 卡了就复位一下" 的自愈代码,就绕开了 hubctl 的那几条退路。
|
||||||
得拆机接线。写一行 "Hub 卡了就复位一下" 的自愈代码,代价是整机变砖。
|
|
||||||
|
|
||||||
⛔ **只在上电复位(`ESP_RST_POWERON`)时脉冲**(2026-09-27 上板改)。
|
⛔ **只在上电复位(`ESP_RST_POWERON`)时脉冲**(2026-09-27 上板改)。
|
||||||
ESP32 的每一次重启都算启动:esptool 经 USB 复位芯片、崩溃 / 看门狗重启、
|
ESP32 的每一次重启都算启动:esptool 经 USB 复位芯片、崩溃 / 看门狗重启、
|
||||||
@@ -100,7 +98,7 @@ static void hub_pulse(void)
|
|||||||
}
|
}
|
||||||
(void)hal_gpio_write(BOARD_PIN_HUB_RST_N, 0); /* 低 = 复位 */
|
(void)hal_gpio_write(BOARD_PIN_HUB_RST_N, 0); /* 低 = 复位 */
|
||||||
hal_time_delay_ms(1);
|
hal_time_delay_ms(1);
|
||||||
(void)hal_gpio_write(BOARD_PIN_HUB_RST_N, 1); /* 放开,之后再也不动 */
|
(void)hal_gpio_write(BOARD_PIN_HUB_RST_N, 1); /* 放开;随后 hubctl_init() 把它放成输入 */
|
||||||
}
|
}
|
||||||
|
|
||||||
/* mux:`i2cbus_init()` 认不到 ⇒ 按 §5.8 先脉冲一次 `MUX_RST#` 再判。 */
|
/* mux:`i2cbus_init()` 认不到 ⇒ 按 §5.8 先脉冲一次 `MUX_RST#` 再判。 */
|
||||||
@@ -212,6 +210,7 @@ hal_err_t bringup_devices(const bringup_in_t *in, bringup_report_t *r)
|
|||||||
/* `UP_VBUS_DET`:上游 USB-C 的 VBUS 经 R721/R722 分压进来,高 = 电脑插着。
|
/* `UP_VBUS_DET`:上游 USB-C 的 VBUS 经 R721/R722 分压进来,高 = 电脑插着。
|
||||||
板上 R722 下拉到地,所以不开内部上下拉。只进遥测(status 的 `host`),不参与任何判据:
|
板上 R722 下拉到地,所以不开内部上下拉。只进遥测(status 的 `host`),不参与任何判据:
|
||||||
Hub 连着电脑时比挂起多吃约 1 W,没有它面板上的「损耗」会无缘无故跳一档。
|
Hub 连着电脑时比挂起多吃约 1 W,没有它面板上的「损耗」会无缘无故跳一档。
|
||||||
|
0.1.14 起 hubctl 也读它:上升沿(电脑刚插上)算一次「需要」。
|
||||||
失败不 abort —— 读不到时 status 报 null。每次启动都配:配成输入没有副作用。 */
|
失败不 abort —— 读不到时 status 报 null。每次启动都配:配成输入没有副作用。 */
|
||||||
(void)hal_gpio_config(BOARD_PIN_UP_VBUS_DET, HAL_GPIO_INPUT);
|
(void)hal_gpio_config(BOARD_PIN_UP_VBUS_DET, HAL_GPIO_INPUT);
|
||||||
|
|
||||||
|
|||||||
@@ -5,6 +5,8 @@
|
|||||||
|
|
||||||
#include "cmd_priv.h"
|
#include "cmd_priv.h"
|
||||||
#include "evlog.h"
|
#include "evlog.h"
|
||||||
|
#include "hal_time.h"
|
||||||
|
#include "hubctl.h"
|
||||||
|
|
||||||
/* ---- 名字表(线上用的,全 ASCII) ---------------------------------------- */
|
/* ---- 名字表(线上用的,全 ASCII) ---------------------------------------- */
|
||||||
|
|
||||||
@@ -14,6 +16,7 @@ static const char *const k_names[CMD_COUNT] = {
|
|||||||
[CMD_VBUS] = "vbus", [CMD_FAN] = "fan", [CMD_EVENTS] = "events",
|
[CMD_VBUS] = "vbus", [CMD_FAN] = "fan", [CMD_EVENTS] = "events",
|
||||||
[CMD_POWER] = "power", [CMD_WIFI] = "wifi", [CMD_MQTT] = "mqtt",
|
[CMD_POWER] = "power", [CMD_WIFI] = "wifi", [CMD_MQTT] = "mqtt",
|
||||||
[CMD_NET] = "net", [CMD_OTA] = "ota", [CMD_FLASH_MODE] = "flash_mode",
|
[CMD_NET] = "net", [CMD_OTA] = "ota", [CMD_FLASH_MODE] = "flash_mode",
|
||||||
|
[CMD_HUB] = "hub",
|
||||||
};
|
};
|
||||||
|
|
||||||
static const char *const k_errs[CMD_ERR_COUNT] = {
|
static const char *const k_errs[CMD_ERR_COUNT] = {
|
||||||
@@ -106,6 +109,7 @@ static const handler_t k_handlers[CMD_COUNT] = {
|
|||||||
[CMD_VBUS] = do_vbus, [CMD_FAN] = do_fan, [CMD_EVENTS] = do_events,
|
[CMD_VBUS] = do_vbus, [CMD_FAN] = do_fan, [CMD_EVENTS] = do_events,
|
||||||
[CMD_POWER] = do_power, [CMD_WIFI] = do_wifi, [CMD_MQTT] = do_mqtt,
|
[CMD_POWER] = do_power, [CMD_WIFI] = do_wifi, [CMD_MQTT] = do_mqtt,
|
||||||
[CMD_NET] = do_net, [CMD_OTA] = do_ota, [CMD_FLASH_MODE] = do_flash_mode,
|
[CMD_NET] = do_net, [CMD_OTA] = do_ota, [CMD_FLASH_MODE] = do_flash_mode,
|
||||||
|
[CMD_HUB] = do_hub,
|
||||||
};
|
};
|
||||||
|
|
||||||
static cmd_id_t lookup(const char *name)
|
static cmd_id_t lookup(const char *name)
|
||||||
@@ -154,6 +158,9 @@ static size_t reply(char *out, size_t len, bool has_seq, uint32_t seq, cmd_err_t
|
|||||||
size_t cmd_handle_line(cmd_link_t link, const char *line, char *out, size_t out_len)
|
size_t cmd_handle_line(cmd_link_t link, const char *line, char *out, size_t out_len)
|
||||||
{
|
{
|
||||||
s_stats.lines++;
|
s_stats.lines++;
|
||||||
|
if (link == CMD_LINK_USB) {
|
||||||
|
hubctl_touch(hal_time_now_ms()); /* 解析得过得不过都算:有人正对着 USB 敲 */
|
||||||
|
}
|
||||||
|
|
||||||
/* 两块大缓冲放静态区:处理器只在执行器任务里跑(cmd.h),不可重入,
|
/* 两块大缓冲放静态区:处理器只在执行器任务里跑(cmd.h),不可重入,
|
||||||
而 1.5 KB 放在那个任务的栈上就是把栈顶到头。 */
|
而 1.5 KB 放在那个任务的栈上就是把栈顶到头。 */
|
||||||
|
|||||||
@@ -60,6 +60,7 @@ typedef enum {
|
|||||||
CMD_NET = 12, /* A14:网络状态(只读,不回显密码) */
|
CMD_NET = 12, /* A14:网络状态(只读,不回显密码) */
|
||||||
CMD_OTA = 13, /* C2:从 URL 下载固件、校验 sha256、切启动分区(ota.h) */
|
CMD_OTA = 13, /* C2:从 URL 下载固件、校验 sha256、切启动分区(ota.h) */
|
||||||
CMD_FLASH_MODE = 14, /* 0.1.5:刷机模式——VBUS 通着时允许手机充电、不断 USB(chgmode.h) */
|
CMD_FLASH_MODE = 14, /* 0.1.5:刷机模式——VBUS 通着时允许手机充电、不断 USB(chgmode.h) */
|
||||||
|
CMD_HUB = 15, /* 0.1.14:Hub 按需启停的模式 / 保持窗口(hubctl.h) */
|
||||||
CMD_COUNT,
|
CMD_COUNT,
|
||||||
} cmd_id_t;
|
} cmd_id_t;
|
||||||
|
|
||||||
@@ -143,6 +144,8 @@ void cmd_stats(cmd_stats_t *st);
|
|||||||
(chgmode.h:窗口表只有这个任务写),app_main 每转一圈调一次。 */
|
(chgmode.h:窗口表只有这个任务写),app_main 每转一圈调一次。 */
|
||||||
void cmd_chgmode_tick(uint32_t now);
|
void cmd_chgmode_tick(uint32_t now);
|
||||||
|
|
||||||
|
/* ⚠️ USB 链路上来的每一行都会调 `hubctl_touch()`(算一次「有人在用 USB」,hubctl.h)。 */
|
||||||
|
|
||||||
#ifdef __cplusplus
|
#ifdef __cplusplus
|
||||||
}
|
}
|
||||||
#endif
|
#endif
|
||||||
|
|||||||
@@ -9,6 +9,7 @@
|
|||||||
#include "cmd_priv.h"
|
#include "cmd_priv.h"
|
||||||
#include "evlog.h"
|
#include "evlog.h"
|
||||||
#include "hal_time.h"
|
#include "hal_time.h"
|
||||||
|
#include "hubctl.h"
|
||||||
#include "i2cbus.h"
|
#include "i2cbus.h"
|
||||||
#include "rail.h"
|
#include "rail.h"
|
||||||
#include "rail_cfg.h"
|
#include "rail_cfg.h"
|
||||||
@@ -359,6 +360,38 @@ void cmd_chgmode_tick(uint32_t now)
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* 0.1.14:Hub 按需启停(hubctl.h)。整板命令,两条链路都收。
|
||||||
|
`{"cmd":"hub","mode":"auto"|"on"}` —— 改模式,存 NVS;
|
||||||
|
`{"cmd":"hub","min":30}` —— 保持 30 分钟(1~240),要用 ESP32 的 USB 控制台 / 烧录时用;
|
||||||
|
`{"cmd":"hub","min":0}` —— 取消保持并结束 idle 窗口:没别的理由就下一拍关。
|
||||||
|
两个键可以同时给,至少给一个。回应带 `"hub":{...}`(同 status)。
|
||||||
|
⚠️ 下一拍(执行器循环,≤ 几十 ms)才真的动引脚,回应里的 on 是命令执行前那一拍的。 */
|
||||||
|
cmd_err_t do_hub(ctx_t *c)
|
||||||
|
{
|
||||||
|
const char *ms = NULL;
|
||||||
|
int64_t min = 0;
|
||||||
|
const int gm = cj_get_str(c->o, "mode", &ms);
|
||||||
|
const int gn = cj_get_int(c->o, "min", &min);
|
||||||
|
if (gm < 0 || gn < 0 || (gm == 0 && gn == 0)) {
|
||||||
|
return CMD_ERR_ARG;
|
||||||
|
}
|
||||||
|
hub_mode_t mode = HUB_MODE_AUTO;
|
||||||
|
if (gm == 1 && !hub_mode_parse(ms, &mode)) {
|
||||||
|
return CMD_ERR_ARG;
|
||||||
|
}
|
||||||
|
if (gn == 1 && (min < 0 || min > HUB_HOLD_MAX_MIN)) {
|
||||||
|
return reject(c, CMD_ERR_RANGE, min);
|
||||||
|
}
|
||||||
|
if (gm == 1) {
|
||||||
|
(void)hubctl_set_mode(mode);
|
||||||
|
}
|
||||||
|
if (gn == 1) {
|
||||||
|
(void)hubctl_hold((uint32_t)min, hal_time_now_ms());
|
||||||
|
}
|
||||||
|
cmd_report_hub(c->w);
|
||||||
|
return CMD_OK;
|
||||||
|
}
|
||||||
|
|
||||||
cmd_err_t do_fan(ctx_t *c)
|
cmd_err_t do_fan(ctx_t *c)
|
||||||
{
|
{
|
||||||
int64_t duty = 0;
|
int64_t duty = 0;
|
||||||
|
|||||||
@@ -27,6 +27,10 @@ cmd_err_t do_vbus(ctx_t *c);
|
|||||||
cmd_err_t do_power(ctx_t *c);
|
cmd_err_t do_power(ctx_t *c);
|
||||||
cmd_err_t do_fan(ctx_t *c);
|
cmd_err_t do_fan(ctx_t *c);
|
||||||
cmd_err_t do_flash_mode(ctx_t *c);
|
cmd_err_t do_flash_mode(ctx_t *c);
|
||||||
|
cmd_err_t do_hub(ctx_t *c);
|
||||||
|
|
||||||
|
/* `"hub":{...}` 对象(status 与 hub 命令的回应共用,以逗号开头)。 */
|
||||||
|
void cmd_report_hub(cj_writer_t *w);
|
||||||
|
|
||||||
/* cmd_net.c:网络配置(只收 USB)与网络状态。 */
|
/* cmd_net.c:网络配置(只收 USB)与网络状态。 */
|
||||||
cmd_err_t do_wifi(ctx_t *c);
|
cmd_err_t do_wifi(ctx_t *c);
|
||||||
|
|||||||
@@ -12,6 +12,7 @@
|
|||||||
#include "evlog.h"
|
#include "evlog.h"
|
||||||
#include "hal_gpio.h"
|
#include "hal_gpio.h"
|
||||||
#include "hal_time.h"
|
#include "hal_time.h"
|
||||||
|
#include "hubctl.h"
|
||||||
#include "ota.h"
|
#include "ota.h"
|
||||||
#include "pca9555.h"
|
#include "pca9555.h"
|
||||||
#include "phonerail.h"
|
#include "phonerail.h"
|
||||||
@@ -102,6 +103,17 @@ static void thermal_obj(cj_writer_t *w, const thermal_snap_t *t)
|
|||||||
cj_w(w, "]},\"derate\":%s", b(t->derate));
|
cj_w(w, "]},\"derate\":%s", b(t->derate));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* 0.1.14:`"hub":{"on":true,"mode":"auto","why":"vbus","hold":0,"idle":0}`(hubctl.h)。
|
||||||
|
`why` 是上一拍开着的理由(关着是 "off");`hold` = 保持窗口剩余秒数;
|
||||||
|
`idle` = 只剩 idle 这一条理由时,离自动关还有几秒(否则 0)。 */
|
||||||
|
void cmd_report_hub(cj_writer_t *w)
|
||||||
|
{
|
||||||
|
const uint32_t now = hal_time_now_ms();
|
||||||
|
cj_w(w, ",\"hub\":{\"on\":%s,\"mode\":\"%s\",\"why\":\"%s\",\"hold\":%lu,\"idle\":%lu}",
|
||||||
|
b(hubctl_is_on()), hub_mode_str(hubctl_mode()), hub_why_str(hubctl_why()),
|
||||||
|
(unsigned long)hubctl_hold_left_s(now), (unsigned long)hubctl_idle_left_s(now));
|
||||||
|
}
|
||||||
|
|
||||||
void cmd_report_status(cj_writer_t *w, const cmd_env_t *env)
|
void cmd_report_status(cj_writer_t *w, const cmd_env_t *env)
|
||||||
{
|
{
|
||||||
const safety_snap_t *s = env->safety;
|
const safety_snap_t *s = env->safety;
|
||||||
@@ -114,6 +126,7 @@ void cmd_report_status(cj_writer_t *w, const cmd_env_t *env)
|
|||||||
} else {
|
} else {
|
||||||
cj_w(w, ",\"host\":null");
|
cj_w(w, ",\"host\":null");
|
||||||
}
|
}
|
||||||
|
cmd_report_hub(w);
|
||||||
if (s != NULL) {
|
if (s != NULL) {
|
||||||
if (s->input_ok) {
|
if (s->input_ok) {
|
||||||
cj_w(w, ",\"in\":{\"mv\":%ld,\"ua\":%ld}", (long)s->input.bus_mv,
|
cj_w(w, ",\"in\":{\"mv\":%ld,\"ua\":%ld}", (long)s->input.bus_mv,
|
||||||
|
|||||||
@@ -576,6 +576,7 @@ const char *evlog_ev_str(log_ev_t ev)
|
|||||||
case LOG_EV_OTA: return "固件升级";
|
case LOG_EV_OTA: return "固件升级";
|
||||||
case LOG_EV_BOOT_CHECK: return "开机重按";
|
case LOG_EV_BOOT_CHECK: return "开机重按";
|
||||||
case LOG_EV_CHG_MODE: return "刷机模式";
|
case LOG_EV_CHG_MODE: return "刷机模式";
|
||||||
|
case LOG_EV_HUB: return "Hub启停";
|
||||||
case LOG_EV_BLADE_MUTE: return "刀片不应答关路";
|
case LOG_EV_BLADE_MUTE: return "刀片不应答关路";
|
||||||
case LOG_EV_COUNT: break;
|
case LOG_EV_COUNT: break;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -97,6 +97,11 @@ typedef enum {
|
|||||||
0 = 命令关(CHG_EV_OFF),255 = 到期(CHG_EV_EXPIRED)。bus_mv / current_ua 是当时这一路的读数。 */
|
0 = 命令关(CHG_EV_OFF),255 = 到期(CHG_EV_EXPIRED)。bus_mv / current_ua 是当时这一路的读数。 */
|
||||||
LOG_EV_CHG_MODE = 21,
|
LOG_EV_CHG_MODE = 21,
|
||||||
|
|
||||||
|
/* 0.1.14 新增:Hub 按需启停(hubctl.h)。整板级(rail = 0)。
|
||||||
|
arg:0 = 关(`HUB_RST#` 拉低)、1 = 开(放开),0x10|模式 = 改模式(0 auto / 1 on)。
|
||||||
|
bus_mv = `hub_why_t`(开的理由;关那一条是 0)。 */
|
||||||
|
LOG_EV_HUB = 22,
|
||||||
|
|
||||||
LOG_EV_COUNT,
|
LOG_EV_COUNT,
|
||||||
} log_ev_t;
|
} log_ev_t;
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,205 @@
|
|||||||
|
/* Hub 按需启停。契约、判据表与退路在 hubctl.h。 */
|
||||||
|
#include "hubctl.h"
|
||||||
|
|
||||||
|
#include <string.h>
|
||||||
|
|
||||||
|
#include "board_pins.h"
|
||||||
|
#include "chgmode.h"
|
||||||
|
#include "evlog.h"
|
||||||
|
#include "hal_gpio.h"
|
||||||
|
#include "netcfg.h"
|
||||||
|
#include "phonerail.h"
|
||||||
|
#include "slot.h"
|
||||||
|
#include "t_actuator.h"
|
||||||
|
|
||||||
|
static hub_mode_t s_mode;
|
||||||
|
static hub_save_fn s_save;
|
||||||
|
static void *s_save_ctx;
|
||||||
|
|
||||||
|
static uint32_t s_last_need; /* 最后一次「需要」的时刻 */
|
||||||
|
static bool s_idle; /* idle 窗口还开着(过期即清:32 位时钟 49.7 天回绕,放着不管会「复活」) */
|
||||||
|
static uint32_t s_hold_until;
|
||||||
|
static bool s_hold;
|
||||||
|
static uint32_t s_down_since; /* MQTT 从什么时候开始不通 */
|
||||||
|
static bool s_down;
|
||||||
|
static bool s_host_prev;
|
||||||
|
|
||||||
|
static bool s_on;
|
||||||
|
static hub_why_t s_why;
|
||||||
|
|
||||||
|
/* `a` 在 `b` 之前(回绕安全)。 */
|
||||||
|
static bool before(uint32_t a, uint32_t b) { return (int32_t)(a - b) < 0; }
|
||||||
|
|
||||||
|
static void need(uint32_t now)
|
||||||
|
{
|
||||||
|
s_last_need = now;
|
||||||
|
s_idle = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 开 = 放成输入,R716 拉高;关 = 配成输出、拉低。**不主动驱高**(hubctl.h)。 */
|
||||||
|
static hal_err_t apply(bool on)
|
||||||
|
{
|
||||||
|
if (on) {
|
||||||
|
return hal_gpio_config(BOARD_PIN_HUB_RST_N, HAL_GPIO_INPUT);
|
||||||
|
}
|
||||||
|
hal_err_t e = hal_gpio_config(BOARD_PIN_HUB_RST_N, HAL_GPIO_OUTPUT);
|
||||||
|
if (e == HAL_OK) {
|
||||||
|
e = hal_gpio_write(BOARD_PIN_HUB_RST_N, 0);
|
||||||
|
}
|
||||||
|
return e;
|
||||||
|
}
|
||||||
|
|
||||||
|
void hubctl_init(hub_mode_t mode, hub_save_fn save, void *ctx, uint32_t now)
|
||||||
|
{
|
||||||
|
s_mode = (mode < HUB_MODE_COUNT) ? mode : HUB_MODE_AUTO;
|
||||||
|
s_save = save;
|
||||||
|
s_save_ctx = ctx;
|
||||||
|
s_hold = false;
|
||||||
|
s_hold_until = 0;
|
||||||
|
s_down = false;
|
||||||
|
s_down_since = now;
|
||||||
|
s_host_prev = false;
|
||||||
|
need(now); /* 启动算一次「需要」:刚复位完,最可能就是有人要用 USB */
|
||||||
|
s_on = (apply(true) == HAL_OK);
|
||||||
|
s_why = HUB_WHY_IDLE;
|
||||||
|
}
|
||||||
|
|
||||||
|
hub_why_t hub_decide(const hub_in_t *in, uint32_t now)
|
||||||
|
{
|
||||||
|
if (in->host && !s_host_prev) {
|
||||||
|
need(now); /* 电脑刚插上 */
|
||||||
|
}
|
||||||
|
s_host_prev = in->host;
|
||||||
|
|
||||||
|
if (in->link_up) {
|
||||||
|
s_down = false;
|
||||||
|
} else if (!s_down) {
|
||||||
|
s_down = true;
|
||||||
|
s_down_since = now;
|
||||||
|
}
|
||||||
|
if (s_hold && !before(now, s_hold_until)) {
|
||||||
|
s_hold = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
hub_why_t why = HUB_WHY_OFF;
|
||||||
|
if (s_mode == HUB_MODE_ON) {
|
||||||
|
why = HUB_WHY_MODE;
|
||||||
|
} else if (in->vbus_any) {
|
||||||
|
why = HUB_WHY_VBUS;
|
||||||
|
} else if (in->flash_any) {
|
||||||
|
why = HUB_WHY_FLASH;
|
||||||
|
} else if (in->act_any) {
|
||||||
|
why = HUB_WHY_ACT;
|
||||||
|
} else if (s_hold) {
|
||||||
|
why = HUB_WHY_HOLD;
|
||||||
|
} else if (s_down && !before(now, s_down_since + HUB_LINK_LOST_MS)) {
|
||||||
|
why = HUB_WHY_LINK;
|
||||||
|
}
|
||||||
|
if (why != HUB_WHY_OFF) {
|
||||||
|
need(now);
|
||||||
|
return why;
|
||||||
|
}
|
||||||
|
if (s_idle && before(now, s_last_need + HUB_IDLE_MS)) {
|
||||||
|
return HUB_WHY_IDLE;
|
||||||
|
}
|
||||||
|
s_idle = false;
|
||||||
|
return HUB_WHY_OFF;
|
||||||
|
}
|
||||||
|
|
||||||
|
void hubctl_tick(uint32_t now)
|
||||||
|
{
|
||||||
|
hub_in_t in;
|
||||||
|
memset(&in, 0, sizeof(in));
|
||||||
|
for (int r = 1; r <= PHONERAIL_RAIL_COUNT; r++) {
|
||||||
|
in.vbus_any = in.vbus_any || slot_vbus_is_on(r);
|
||||||
|
in.act_any = in.act_any || t_actuator_busy(r);
|
||||||
|
}
|
||||||
|
in.flash_any = chgmode_count(now) > 0;
|
||||||
|
net_status_t st;
|
||||||
|
net_status_get(&st);
|
||||||
|
in.link_up = (st.mqtt == NET_UP);
|
||||||
|
int host = 0;
|
||||||
|
in.host = (hal_gpio_read(BOARD_PIN_UP_VBUS_DET, &host) == HAL_OK) && host != 0;
|
||||||
|
|
||||||
|
s_why = hub_decide(&in, now);
|
||||||
|
const bool want = (s_why != HUB_WHY_OFF);
|
||||||
|
if (want == s_on) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (apply(want) != HAL_OK) {
|
||||||
|
return; /* 引脚号写错才会走到这 —— 下一拍再试,状态不假装变了 */
|
||||||
|
}
|
||||||
|
s_on = want;
|
||||||
|
evlog_put(0, LOG_EV_HUB, want ? HUB_EV_ON : HUB_EV_OFF, (int32_t)s_why, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
void hubctl_touch(uint32_t now) { need(now); }
|
||||||
|
|
||||||
|
hal_err_t hubctl_set_mode(hub_mode_t mode)
|
||||||
|
{
|
||||||
|
if (mode >= HUB_MODE_COUNT) {
|
||||||
|
return HAL_ERR_ARG;
|
||||||
|
}
|
||||||
|
if (mode != s_mode) {
|
||||||
|
s_mode = mode;
|
||||||
|
evlog_put(0, LOG_EV_HUB, (uint8_t)(HUB_EV_MODE | (uint32_t)mode), (int32_t)s_why, 0);
|
||||||
|
}
|
||||||
|
if (s_save != NULL) {
|
||||||
|
s_save(s_save_ctx, mode); /* 没变也交给它:存盘函数自己判「没变不写」 */
|
||||||
|
}
|
||||||
|
return HAL_OK;
|
||||||
|
}
|
||||||
|
|
||||||
|
hal_err_t hubctl_hold(uint32_t minutes, uint32_t now)
|
||||||
|
{
|
||||||
|
if (minutes > HUB_HOLD_MAX_MIN) {
|
||||||
|
return HAL_ERR_ARG;
|
||||||
|
}
|
||||||
|
if (minutes == 0) {
|
||||||
|
s_hold = false;
|
||||||
|
s_idle = false;
|
||||||
|
return HAL_OK;
|
||||||
|
}
|
||||||
|
s_hold_until = now + minutes * 60000u;
|
||||||
|
s_hold = true;
|
||||||
|
return HAL_OK;
|
||||||
|
}
|
||||||
|
|
||||||
|
hub_mode_t hubctl_mode(void) { return s_mode; }
|
||||||
|
bool hubctl_is_on(void) { return s_on; }
|
||||||
|
hub_why_t hubctl_why(void) { return s_why; }
|
||||||
|
|
||||||
|
uint32_t hubctl_hold_left_s(uint32_t now)
|
||||||
|
{
|
||||||
|
if (!s_hold || !before(now, s_hold_until)) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
return (s_hold_until - now + 999u) / 1000u;
|
||||||
|
}
|
||||||
|
|
||||||
|
uint32_t hubctl_idle_left_s(uint32_t now)
|
||||||
|
{
|
||||||
|
if (s_why != HUB_WHY_IDLE || !s_idle || !before(now, s_last_need + HUB_IDLE_MS)) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
return (s_last_need + HUB_IDLE_MS - now + 999u) / 1000u;
|
||||||
|
}
|
||||||
|
|
||||||
|
static const char *const k_modes[HUB_MODE_COUNT] = { "auto", "on" };
|
||||||
|
static const char *const k_whys[HUB_WHY_COUNT] = {
|
||||||
|
"off", "mode", "vbus", "flash", "act", "hold", "link", "idle",
|
||||||
|
};
|
||||||
|
|
||||||
|
const char *hub_mode_str(hub_mode_t m) { return (m < HUB_MODE_COUNT) ? k_modes[m] : "?"; }
|
||||||
|
const char *hub_why_str(hub_why_t w) { return (w < HUB_WHY_COUNT) ? k_whys[w] : "?"; }
|
||||||
|
|
||||||
|
bool hub_mode_parse(const char *s, hub_mode_t *out)
|
||||||
|
{
|
||||||
|
for (int i = 0; i < HUB_MODE_COUNT; i++) {
|
||||||
|
if (strcmp(s, k_modes[i]) == 0) {
|
||||||
|
*out = (hub_mode_t)i;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
@@ -0,0 +1,121 @@
|
|||||||
|
/* Hub(U32 USB2517I)按需启停(0.1.14,2026-09-29 用户定,推翻 R2 D13 代价 2 / R-11 的
|
||||||
|
「运行期不许动 `HUB_RST#`」)。
|
||||||
|
|
||||||
|
为什么要有它:五台手机的 VBUS 平时全关,但 Hub 只要上行插着电脑就一直是活动态 ——
|
||||||
|
ESP32 自己的 USB-Serial-JTAG 挂在第 6 口上,电脑永远不会把它挂起。
|
||||||
|
2026-09-29 板 B 实测 Hub 工作 vs 按住复位差 1.03 W(12 V 输入差约 88 mA),而这 1 W 大部分时间什么都没在干。
|
||||||
|
把 `RESET_N`(43 脚)拉低 ⇒ Hub 从电脑那边消失,PHY 与 24 MHz 晶振都停,只剩 U31 的静态电流。
|
||||||
|
|
||||||
|
---- 什么时候开(任一条成立就开,并刷新「最后一次需要」的时刻)-------------
|
||||||
|
| why | 条件 |
|
||||||
|
|---------|------|
|
||||||
|
| mode | 模式 = 常开(`hub` 命令,存 NVS) |
|
||||||
|
| vbus | 任一路 VBUS 通着 —— 手机的 USB 只有这时才在 Hub 上 |
|
||||||
|
| flash | 任一路刷机模式窗口内(chgmode.h) |
|
||||||
|
| act | 任一路执行器在跑序列(EDL 要枚举 9008;硬重启中途 VBUS 暂时是关的) |
|
||||||
|
| hold | `hub` 命令给的保持窗口内(要用 ESP32 的 USB 控制台 / 烧录) |
|
||||||
|
| link | MQTT 连续 HUB_LINK_LOST_MS 没连上(含从没配过网)⇒ **USB 是唯一的路,必须开** |
|
||||||
|
| idle | 以上都不成立,但离「最后一次需要」还不到 HUB_IDLE_MS |
|
||||||
|
「最后一次需要」还会被三件事刷新:启动、上行电脑插上(`UP_VBUS_DET` 上升沿)、
|
||||||
|
USB 链路上来了一条命令 —— 都是「有人正要用 USB」的信号。
|
||||||
|
全不成立 ⇒ 关(`HUB_RST#` 拉低)。
|
||||||
|
|
||||||
|
---- 退路(这是推翻 R-11 的前提,一条都不许删)-----------------------------
|
||||||
|
1. `link`:WiFi/MQTT 断了 ⇒ 1 分钟内 Hub 自己回来,USB 控制台可用;
|
||||||
|
2. ESP32 任何一次复位(上电 / 看门狗 / esp_restart / esptool / J33 EN)引脚都回到高阻,
|
||||||
|
R716 把 Hub 拉出复位,而且启动算一次「需要」⇒ 至少 HUB_IDLE_MS 的窗口;
|
||||||
|
3. 板子断电重上 = 第 2 条;
|
||||||
|
4. J33 UART 应急口不经过 Hub,永远在。
|
||||||
|
⚠️ 只许**拉低**或**放成输入**(靠 R716 拉高),不许主动驱高 —— 和 bringup 的脉冲同一口径:
|
||||||
|
放开 = 高阻,ESP32 这一侧出任何事 Hub 都停在「工作」那一边。
|
||||||
|
|
||||||
|
---- 线程 ----------------------------------------------------------------
|
||||||
|
**只有执行器任务写**(`hubctl_tick()` 在执行器循环里、命令处理器也在那个任务里),
|
||||||
|
status 跨任务读几个单字,遥测无害(cmd.h 同一条)。 */
|
||||||
|
#ifndef HUBCTL_H
|
||||||
|
#define HUBCTL_H
|
||||||
|
|
||||||
|
#include <stdbool.h>
|
||||||
|
#include <stdint.h>
|
||||||
|
|
||||||
|
#include "hal_err.h"
|
||||||
|
|
||||||
|
#ifdef __cplusplus
|
||||||
|
extern "C" {
|
||||||
|
#endif
|
||||||
|
|
||||||
|
#define HUB_IDLE_MS (10u * 60000u) /* 最后一次需要之后再开多久 */
|
||||||
|
#define HUB_LINK_LOST_MS 60000u /* MQTT 断多久算「只剩 USB 这条路」 */
|
||||||
|
#define HUB_HOLD_MAX_MIN 240
|
||||||
|
|
||||||
|
/* 模式。⚠️ 进 NVS,冻结。 */
|
||||||
|
typedef enum {
|
||||||
|
HUB_MODE_AUTO = 0,
|
||||||
|
HUB_MODE_ON = 1,
|
||||||
|
HUB_MODE_COUNT,
|
||||||
|
} hub_mode_t;
|
||||||
|
|
||||||
|
/* 为什么开着 / 关着。⚠️ 进事件(LOG_EV_HUB 的 bus_mv),冻结,只许在末尾追加。 */
|
||||||
|
typedef enum {
|
||||||
|
HUB_WHY_OFF = 0,
|
||||||
|
HUB_WHY_MODE = 1,
|
||||||
|
HUB_WHY_VBUS = 2,
|
||||||
|
HUB_WHY_FLASH = 3,
|
||||||
|
HUB_WHY_ACT = 4,
|
||||||
|
HUB_WHY_HOLD = 5,
|
||||||
|
HUB_WHY_LINK = 6,
|
||||||
|
HUB_WHY_IDLE = 7,
|
||||||
|
HUB_WHY_COUNT,
|
||||||
|
} hub_why_t;
|
||||||
|
|
||||||
|
/* LOG_EV_HUB 的 arg。⚠️ 进 flash,冻结。 */
|
||||||
|
#define HUB_EV_OFF 0u
|
||||||
|
#define HUB_EV_ON 1u
|
||||||
|
#define HUB_EV_MODE 0x10u /* | 新模式 */
|
||||||
|
|
||||||
|
/* 一拍的输入(`hubctl_tick()` 自己采;宿主机测试直接喂 `hub_decide()`)。 */
|
||||||
|
typedef struct {
|
||||||
|
bool vbus_any;
|
||||||
|
bool flash_any;
|
||||||
|
bool act_any;
|
||||||
|
bool link_up;
|
||||||
|
bool host; /* UP_VBUS_DET */
|
||||||
|
} hub_in_t;
|
||||||
|
|
||||||
|
/* 模式存盘回调(app_main 写 NVS)。NULL = 不存(宿主机测试)。 */
|
||||||
|
typedef void (*hub_save_fn)(void *ctx, hub_mode_t mode);
|
||||||
|
|
||||||
|
/* 起步:记下模式、把 `HUB_RST#` 放成输入(Hub 工作),启动算一次「需要」。
|
||||||
|
模式越界按 AUTO。 */
|
||||||
|
void hubctl_init(hub_mode_t mode, hub_save_fn save, void *ctx, uint32_t now);
|
||||||
|
|
||||||
|
/* 纯判定:这一拍 Hub 该不该开、为什么。会推进内部的时刻(最后一次需要 / 断线起点 / 上行边沿),
|
||||||
|
**不碰引脚**。宿主机测试用它。 */
|
||||||
|
hub_why_t hub_decide(const hub_in_t *in, uint32_t now);
|
||||||
|
|
||||||
|
/* 执行器循环每拍调:采输入 → `hub_decide()` → 状态变了才动引脚、记事件。 */
|
||||||
|
void hubctl_tick(uint32_t now);
|
||||||
|
|
||||||
|
/* USB 链路上来了一条命令(cmd.c 调):算一次「需要」。 */
|
||||||
|
void hubctl_touch(uint32_t now);
|
||||||
|
|
||||||
|
/* 命令:改模式(存盘、记事件)。越界 HAL_ERR_ARG。 */
|
||||||
|
hal_err_t hubctl_set_mode(hub_mode_t mode);
|
||||||
|
/* 命令:保持 minutes 分钟(1~HUB_HOLD_MAX_MIN,覆盖原窗口);0 = 取消保持**并结束 idle 窗口**
|
||||||
|
(没有别的理由就下一拍关 —— 实测省电用)。越界 HAL_ERR_ARG。 */
|
||||||
|
hal_err_t hubctl_hold(uint32_t minutes, uint32_t now);
|
||||||
|
|
||||||
|
hub_mode_t hubctl_mode(void);
|
||||||
|
bool hubctl_is_on(void); /* 引脚此刻放着(Hub 在工作) */
|
||||||
|
hub_why_t hubctl_why(void); /* 上一拍的结论 */
|
||||||
|
uint32_t hubctl_hold_left_s(uint32_t now);
|
||||||
|
uint32_t hubctl_idle_left_s(uint32_t now); /* why == idle 时离自动关还有几秒,否则 0 */
|
||||||
|
const char *hub_mode_str(hub_mode_t m);
|
||||||
|
const char *hub_why_str(hub_why_t w);
|
||||||
|
bool hub_mode_parse(const char *s, hub_mode_t *out);
|
||||||
|
|
||||||
|
#ifdef __cplusplus
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
|
||||||
|
#endif /* HUBCTL_H */
|
||||||
@@ -3,7 +3,7 @@
|
|||||||
#define PHONERAIL_H
|
#define PHONERAIL_H
|
||||||
|
|
||||||
/* 固件版本。规则:<主>.<次>.<修订>-<最后完成的任务包> */
|
/* 固件版本。规则:<主>.<次>.<修订>-<最后完成的任务包> */
|
||||||
#define PHONERAIL_FW_VERSION "0.1.13-c5"
|
#define PHONERAIL_FW_VERSION "0.1.16-c5"
|
||||||
|
|
||||||
/* 通道数。五路直供,见设计文档 §2。 */
|
/* 通道数。五路直供,见设计文档 §2。 */
|
||||||
#define PHONERAIL_RAIL_COUNT 5
|
#define PHONERAIL_RAIL_COUNT 5
|
||||||
|
|||||||
@@ -110,6 +110,8 @@ extern "C" {
|
|||||||
/* SW-5 的三档按键时长。
|
/* SW-5 的三档按键时长。
|
||||||
⚠️ 开机那一档 2026-09-28 小米8 实测:按 2 s 时 PMIC 起来了(415 mA),一松手 3 s 内
|
⚠️ 开机那一档 2026-09-28 小米8 实测:按 2 s 时 PMIC 起来了(415 mA),一松手 3 s 内
|
||||||
掉回 ~1 mA —— SoC 还没接住 PS_HOLD。按 5 s 松手后稳在 ~210 mA、约 45 s 进系统。
|
掉回 ~1 mA —— SoC 还没接住 PS_HOLD。按 5 s 松手后稳在 ~210 mA、约 45 s 进系统。
|
||||||
|
2026-09-30 试过 3 s(0.1.15):板 B 第 2 路 power_cycle 三轮全是「按住 330~390 mA、松手掉回 0」
|
||||||
|
⇒ no-boot;同一台手工按 5 s 一次开起来。⛔ 别再往下调。
|
||||||
上限是 ACT_KEY_MS_FORCE:按到 10 s 就成了强制重启。 */
|
上限是 ACT_KEY_MS_FORCE:按到 10 s 就成了强制重启。 */
|
||||||
#define ACT_KEY_MS_TAP 100 /* 短按:唤醒 / 息屏 */
|
#define ACT_KEY_MS_TAP 100 /* 短按:唤醒 / 息屏 */
|
||||||
#define ACT_KEY_MS_ON 5000 /* 长按:开机 */
|
#define ACT_KEY_MS_ON 5000 /* 长按:开机 */
|
||||||
|
|||||||
+36
-1
@@ -16,6 +16,7 @@
|
|||||||
#include "dac63204.h"
|
#include "dac63204.h"
|
||||||
#include "evlog.h"
|
#include "evlog.h"
|
||||||
#include "hal_i2c.h"
|
#include "hal_i2c.h"
|
||||||
|
#include "hubctl.h"
|
||||||
#include "hal_lock.h"
|
#include "hal_lock.h"
|
||||||
#include "hal_time.h"
|
#include "hal_time.h"
|
||||||
#include "i2cbus.h"
|
#include "i2cbus.h"
|
||||||
@@ -189,7 +190,8 @@ static void test_cmd_numbers_are_frozen(void)
|
|||||||
TEST_ASSERT_EQUAL_INT(12, CMD_NET);
|
TEST_ASSERT_EQUAL_INT(12, CMD_NET);
|
||||||
TEST_ASSERT_EQUAL_INT(13, CMD_OTA); /* C2 */
|
TEST_ASSERT_EQUAL_INT(13, CMD_OTA); /* C2 */
|
||||||
TEST_ASSERT_EQUAL_INT(14, CMD_FLASH_MODE); /* 0.1.5 刷机模式 */
|
TEST_ASSERT_EQUAL_INT(14, CMD_FLASH_MODE); /* 0.1.5 刷机模式 */
|
||||||
TEST_ASSERT_EQUAL_INT(15, CMD_COUNT);
|
TEST_ASSERT_EQUAL_INT(15, CMD_HUB); /* 0.1.14 Hub 按需启停 */
|
||||||
|
TEST_ASSERT_EQUAL_INT(16, CMD_COUNT);
|
||||||
|
|
||||||
TEST_ASSERT_EQUAL_INT(0, CMD_OK);
|
TEST_ASSERT_EQUAL_INT(0, CMD_OK);
|
||||||
TEST_ASSERT_EQUAL_INT(1, CMD_ERR_PARSE);
|
TEST_ASSERT_EQUAL_INT(1, CMD_ERR_PARSE);
|
||||||
@@ -590,6 +592,38 @@ static void test_fan_floor(void)
|
|||||||
TEST_ASSERT_EQUAL_UINT16(0, t_thermal_floor());
|
TEST_ASSERT_EQUAL_UINT16(0, t_thermal_floor());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* 0.1.14 hub:改模式 / 保持窗口,两条链路都收;越界记事件、缺参数不记;
|
||||||
|
USB 链路上的任何一行都算一次「有人在用 USB」。 */
|
||||||
|
static void test_hub_command(void)
|
||||||
|
{
|
||||||
|
hubctl_init(HUB_MODE_AUTO, NULL, NULL, hal_time_now_ms());
|
||||||
|
|
||||||
|
const char *r = send_on(CMD_LINK_WIFI, "{\"seq\":1,\"cmd\":\"hub\",\"mode\":\"on\"}");
|
||||||
|
ASSERT_HAS(r, "\"ok\":true");
|
||||||
|
ASSERT_HAS(r, "\"hub\":{\"on\":true,\"mode\":\"on\"");
|
||||||
|
TEST_ASSERT_EQUAL_INT(HUB_MODE_ON, hubctl_mode());
|
||||||
|
|
||||||
|
r = send("{\"seq\":2,\"cmd\":\"hub\",\"mode\":\"auto\",\"min\":30}");
|
||||||
|
ASSERT_HAS(r, "\"mode\":\"auto\"");
|
||||||
|
ASSERT_HAS(r, "\"hold\":1800");
|
||||||
|
TEST_ASSERT_EQUAL_UINT32(1800, hubctl_hold_left_s(hal_time_now_ms()));
|
||||||
|
|
||||||
|
ASSERT_HAS(send("{\"seq\":3,\"cmd\":\"hub\",\"min\":241}"), "\"err\":\"range\"");
|
||||||
|
TEST_ASSERT_EQUAL_INT(1, count_events(LOG_EV_CMD_REJECT));
|
||||||
|
ASSERT_HAS(send("{\"seq\":4,\"cmd\":\"hub\",\"mode\":\"off\"}"), "\"err\":\"arg\"");
|
||||||
|
ASSERT_HAS(send("{\"seq\":5,\"cmd\":\"hub\"}"), "\"err\":\"arg\"");
|
||||||
|
TEST_ASSERT_EQUAL_INT(1, count_events(LOG_EV_CMD_REJECT));
|
||||||
|
ASSERT_HAS(send("{\"seq\":5,\"cmd\":\"hub\"}"), "\"dup\":true"); /* 会动东西 ⇒ 进判重窗口 */
|
||||||
|
|
||||||
|
ASSERT_HAS(send("{\"seq\":7,\"cmd\":\"hub\",\"min\":0}"), "\"hold\":0");
|
||||||
|
const hub_in_t idle = { .link_up = true };
|
||||||
|
TEST_ASSERT_EQUAL_INT(HUB_WHY_OFF, hub_decide(&idle, hal_time_now_ms() + 1));
|
||||||
|
|
||||||
|
/* USB 上来一行(哪怕是 status)⇒ 重新给一个 idle 窗口。 */
|
||||||
|
(void)send_on(CMD_LINK_USB, "{\"cmd\":\"status\"}");
|
||||||
|
TEST_ASSERT_EQUAL_INT(HUB_WHY_IDLE, hub_decide(&idle, hal_time_now_ms() + 1));
|
||||||
|
}
|
||||||
|
|
||||||
/* ---- 只读命令 ------------------------------------------------------------ */
|
/* ---- 只读命令 ------------------------------------------------------------ */
|
||||||
|
|
||||||
/* events 分页:一次最多 CMD_EVENTS_MAX 条,逐条带序号,游标接得上;RAM 里的先落盘。 */
|
/* events 分页:一次最多 CMD_EVENTS_MAX 条,逐条带序号,游标接得上;RAM 里的先落盘。 */
|
||||||
@@ -858,6 +892,7 @@ int main(void)
|
|||||||
RUN_TEST(test_power_off_drops_vbus_and_always_drops_the_efuse);
|
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_power_records_intent_only_when_it_took_effect);
|
||||||
RUN_TEST(test_fan_floor);
|
RUN_TEST(test_fan_floor);
|
||||||
|
RUN_TEST(test_hub_command);
|
||||||
RUN_TEST(test_events_pages_with_contiguous_seqs);
|
RUN_TEST(test_events_pages_with_contiguous_seqs);
|
||||||
RUN_TEST(test_status_fits_and_reports_unknowns_as_null);
|
RUN_TEST(test_status_fits_and_reports_unknowns_as_null);
|
||||||
RUN_TEST(test_small_output_buffer);
|
RUN_TEST(test_small_output_buffer);
|
||||||
|
|||||||
@@ -89,7 +89,8 @@ static void test_flash_format_is_frozen(void)
|
|||||||
TEST_ASSERT_EQUAL_INT(19, LOG_EV_BOOT_CHECK); /* 0.1.2 开机校验 */
|
TEST_ASSERT_EQUAL_INT(19, LOG_EV_BOOT_CHECK); /* 0.1.2 开机校验 */
|
||||||
TEST_ASSERT_EQUAL_INT(20, LOG_EV_BLADE_MUTE); /* 0.1.4 刀片不应答 */
|
TEST_ASSERT_EQUAL_INT(20, LOG_EV_BLADE_MUTE); /* 0.1.4 刀片不应答 */
|
||||||
TEST_ASSERT_EQUAL_INT(21, LOG_EV_CHG_MODE); /* 0.1.5 刷机模式 */
|
TEST_ASSERT_EQUAL_INT(21, LOG_EV_CHG_MODE); /* 0.1.5 刷机模式 */
|
||||||
TEST_ASSERT_EQUAL_INT(22, LOG_EV_COUNT);
|
TEST_ASSERT_EQUAL_INT(22, LOG_EV_HUB); /* 0.1.14 Hub 按需启停 */
|
||||||
|
TEST_ASSERT_EQUAL_INT(23, LOG_EV_COUNT);
|
||||||
}
|
}
|
||||||
|
|
||||||
/* §10.7 的「必记事件」有八类,A10 只产生其中两类。剩下的号先占住,
|
/* §10.7 的「必记事件」有八类,A10 只产生其中两类。剩下的号先占住,
|
||||||
|
|||||||
@@ -0,0 +1,237 @@
|
|||||||
|
/* 0.1.14:Hub 按需启停(hubctl.h)。
|
||||||
|
|
||||||
|
口径同其它测试:判据用 `hub_decide()` 直接喂输入;引脚与事件用 `hubctl_tick()`
|
||||||
|
打在假 GPIO / 事件缓冲上。R716 的上拉用 `sim_gpio_drive(HUB_RST#, 1)` 模拟 ——
|
||||||
|
放成输入时读回 1 = Hub 在工作,配成输出拉低时读回 0 = 按住复位。 */
|
||||||
|
#include <string.h>
|
||||||
|
|
||||||
|
#include "board_pins.h"
|
||||||
|
#include "evlog.h"
|
||||||
|
#include "hal_time.h"
|
||||||
|
#include "hubctl.h"
|
||||||
|
#include "netcfg.h"
|
||||||
|
#include "sim_hal.h"
|
||||||
|
#include "unity.h"
|
||||||
|
|
||||||
|
#define PIN BOARD_PIN_HUB_RST_N
|
||||||
|
|
||||||
|
static hub_mode_t s_saved;
|
||||||
|
static int s_saves;
|
||||||
|
|
||||||
|
static void save(void *ctx, hub_mode_t m)
|
||||||
|
{
|
||||||
|
(void)ctx;
|
||||||
|
s_saved = m;
|
||||||
|
s_saves++;
|
||||||
|
}
|
||||||
|
|
||||||
|
static void link(bool up)
|
||||||
|
{
|
||||||
|
net_status_t st;
|
||||||
|
memset(&st, 0, sizeof(st));
|
||||||
|
st.mqtt = up ? NET_UP : NET_CONNECTING;
|
||||||
|
net_status_set(&st);
|
||||||
|
}
|
||||||
|
|
||||||
|
static uint32_t now(void) { return hal_time_now_ms(); }
|
||||||
|
|
||||||
|
/* 每 100 ms 一拍(执行器空闲时的节奏),推 ms 毫秒。 */
|
||||||
|
static void run_ms(uint32_t ms)
|
||||||
|
{
|
||||||
|
for (uint32_t t = 0; t < ms; t += 100) {
|
||||||
|
sim_time_advance_ms(100);
|
||||||
|
hubctl_tick(now());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static int hub_events(uint8_t arg)
|
||||||
|
{
|
||||||
|
(void)evlog_flush(0);
|
||||||
|
evlog_rec_t r[16];
|
||||||
|
const uint32_t n = evlog_read(0, r, 16);
|
||||||
|
int k = 0;
|
||||||
|
for (uint32_t i = 0; i < n; i++) {
|
||||||
|
k += (r[i].type == LOG_EV_HUB && r[i].arg == arg) ? 1 : 0;
|
||||||
|
}
|
||||||
|
return k;
|
||||||
|
}
|
||||||
|
|
||||||
|
void setUp(void)
|
||||||
|
{
|
||||||
|
sim_hal_reset();
|
||||||
|
TEST_ASSERT_EQUAL_INT(HAL_OK, evlog_init());
|
||||||
|
sim_gpio_drive(PIN, 1); /* R716 */
|
||||||
|
s_saves = 0;
|
||||||
|
link(true);
|
||||||
|
hubctl_init(HUB_MODE_AUTO, save, NULL, now());
|
||||||
|
}
|
||||||
|
|
||||||
|
void tearDown(void) {}
|
||||||
|
|
||||||
|
static const hub_in_t k_idle = { .link_up = true };
|
||||||
|
|
||||||
|
/* ---- 判据 ---------------------------------------------------------------- */
|
||||||
|
|
||||||
|
/* 启动算一次需要:HUB_IDLE_MS 之内开着(idle),到点就关。 */
|
||||||
|
static void test_boot_window_then_off(void)
|
||||||
|
{
|
||||||
|
TEST_ASSERT_TRUE(hubctl_is_on());
|
||||||
|
TEST_ASSERT_EQUAL_INT(1, sim_gpio_level(PIN));
|
||||||
|
TEST_ASSERT_EQUAL_INT(HUB_WHY_IDLE, hub_decide(&k_idle, now() + HUB_IDLE_MS - 1));
|
||||||
|
TEST_ASSERT_EQUAL_INT(HUB_WHY_OFF, hub_decide(&k_idle, now() + HUB_IDLE_MS));
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 优先级:mode > vbus > flash > act > hold > link。任一条都刷新「最后一次需要」。 */
|
||||||
|
static void test_reasons_and_their_order(void)
|
||||||
|
{
|
||||||
|
hub_in_t in = k_idle;
|
||||||
|
in.vbus_any = in.flash_any = in.act_any = true;
|
||||||
|
TEST_ASSERT_EQUAL_INT(HUB_WHY_VBUS, hub_decide(&in, now()));
|
||||||
|
in.vbus_any = false;
|
||||||
|
TEST_ASSERT_EQUAL_INT(HUB_WHY_FLASH, hub_decide(&in, now()));
|
||||||
|
in.flash_any = false;
|
||||||
|
TEST_ASSERT_EQUAL_INT(HUB_WHY_ACT, hub_decide(&in, now()));
|
||||||
|
in.act_any = false;
|
||||||
|
TEST_ASSERT_EQUAL_INT(HUB_WHY_IDLE, hub_decide(&in, now()));
|
||||||
|
|
||||||
|
/* VBUS 一直开着 30 分钟,关掉之后还要再开 HUB_IDLE_MS。 */
|
||||||
|
const uint32_t t0 = now() + 30u * 60000u;
|
||||||
|
in.vbus_any = true;
|
||||||
|
TEST_ASSERT_EQUAL_INT(HUB_WHY_VBUS, hub_decide(&in, t0));
|
||||||
|
in.vbus_any = false;
|
||||||
|
TEST_ASSERT_EQUAL_INT(HUB_WHY_IDLE, hub_decide(&in, t0 + HUB_IDLE_MS - 1));
|
||||||
|
TEST_ASSERT_EQUAL_INT(HUB_WHY_OFF, hub_decide(&in, t0 + HUB_IDLE_MS));
|
||||||
|
|
||||||
|
TEST_ASSERT_EQUAL_INT(HAL_OK, hubctl_set_mode(HUB_MODE_ON));
|
||||||
|
TEST_ASSERT_EQUAL_INT(HUB_WHY_MODE, hub_decide(&in, t0 + 10u * HUB_IDLE_MS));
|
||||||
|
}
|
||||||
|
|
||||||
|
/* MQTT 断满 HUB_LINK_LOST_MS 才算(WiFi 抖一下不开);恢复后再给一个 idle 窗口。 */
|
||||||
|
static void test_link_lost_forces_on(void)
|
||||||
|
{
|
||||||
|
const uint32_t t0 = now() + HUB_IDLE_MS; /* 启动窗口已过 */
|
||||||
|
hub_in_t in = k_idle;
|
||||||
|
TEST_ASSERT_EQUAL_INT(HUB_WHY_OFF, hub_decide(&in, t0));
|
||||||
|
in.link_up = false;
|
||||||
|
TEST_ASSERT_EQUAL_INT(HUB_WHY_OFF, hub_decide(&in, t0));
|
||||||
|
TEST_ASSERT_EQUAL_INT(HUB_WHY_OFF, hub_decide(&in, t0 + HUB_LINK_LOST_MS - 1));
|
||||||
|
TEST_ASSERT_EQUAL_INT(HUB_WHY_LINK, hub_decide(&in, t0 + HUB_LINK_LOST_MS));
|
||||||
|
TEST_ASSERT_EQUAL_INT(HUB_WHY_LINK, hub_decide(&in, t0 + 5u * HUB_IDLE_MS));
|
||||||
|
in.link_up = true;
|
||||||
|
const uint32_t t1 = t0 + 5u * HUB_IDLE_MS + 1;
|
||||||
|
TEST_ASSERT_EQUAL_INT(HUB_WHY_IDLE, hub_decide(&in, t1));
|
||||||
|
TEST_ASSERT_EQUAL_INT(HUB_WHY_OFF, hub_decide(&in, t1 + HUB_IDLE_MS));
|
||||||
|
|
||||||
|
/* 断了不到一分钟又连上:计时重来,不累计。 */
|
||||||
|
in.link_up = false;
|
||||||
|
TEST_ASSERT_EQUAL_INT(HUB_WHY_OFF, hub_decide(&in, t1 + HUB_IDLE_MS + 1));
|
||||||
|
in.link_up = true;
|
||||||
|
TEST_ASSERT_EQUAL_INT(HUB_WHY_OFF, hub_decide(&in, t1 + HUB_IDLE_MS + 30000u));
|
||||||
|
in.link_up = false;
|
||||||
|
TEST_ASSERT_EQUAL_INT(HUB_WHY_OFF, hub_decide(&in, t1 + HUB_IDLE_MS + 30001u));
|
||||||
|
TEST_ASSERT_EQUAL_INT(HUB_WHY_OFF,
|
||||||
|
hub_decide(&in, t1 + HUB_IDLE_MS + 30001u + HUB_LINK_LOST_MS - 1));
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 电脑插上(UP_VBUS_DET 上升沿)与 USB 命令都算一次需要;电平一直高不算。 */
|
||||||
|
static void test_host_edge_and_usb_touch(void)
|
||||||
|
{
|
||||||
|
const uint32_t t0 = now() + HUB_IDLE_MS;
|
||||||
|
hub_in_t in = k_idle;
|
||||||
|
in.host = true;
|
||||||
|
TEST_ASSERT_EQUAL_INT(HUB_WHY_IDLE, hub_decide(&in, t0)); /* 上升沿 */
|
||||||
|
TEST_ASSERT_EQUAL_INT(HUB_WHY_OFF, hub_decide(&in, t0 + HUB_IDLE_MS)); /* 电平不算 */
|
||||||
|
|
||||||
|
hubctl_touch(t0 + 2u * HUB_IDLE_MS);
|
||||||
|
TEST_ASSERT_EQUAL_INT(HUB_WHY_IDLE, hub_decide(&in, t0 + 3u * HUB_IDLE_MS - 1));
|
||||||
|
TEST_ASSERT_EQUAL_INT(HUB_WHY_OFF, hub_decide(&in, t0 + 3u * HUB_IDLE_MS));
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 保持窗口;min=0 取消保持并结束 idle 窗口(下一拍就关)。 */
|
||||||
|
static void test_hold(void)
|
||||||
|
{
|
||||||
|
const uint32_t t0 = now();
|
||||||
|
TEST_ASSERT_EQUAL_INT(HAL_ERR_ARG, hubctl_hold(HUB_HOLD_MAX_MIN + 1, t0));
|
||||||
|
TEST_ASSERT_EQUAL_INT(HAL_OK, hubctl_hold(30, t0));
|
||||||
|
TEST_ASSERT_EQUAL_UINT32(1800, hubctl_hold_left_s(t0));
|
||||||
|
TEST_ASSERT_EQUAL_INT(HUB_WHY_HOLD, hub_decide(&k_idle, t0 + 30u * 60000u - 1));
|
||||||
|
TEST_ASSERT_EQUAL_INT(HUB_WHY_IDLE, hub_decide(&k_idle, t0 + 30u * 60000u));
|
||||||
|
TEST_ASSERT_EQUAL_UINT32(0, hubctl_hold_left_s(t0 + 30u * 60000u));
|
||||||
|
|
||||||
|
TEST_ASSERT_EQUAL_INT(HAL_OK, hubctl_hold(0, t0 + 30u * 60000u));
|
||||||
|
TEST_ASSERT_EQUAL_INT(HUB_WHY_OFF, hub_decide(&k_idle, t0 + 30u * 60000u + 1));
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 关着 49.7 天,32 位时钟回绕之后 idle 窗口不许「复活」。 */
|
||||||
|
static void test_idle_window_does_not_revive_after_wrap(void)
|
||||||
|
{
|
||||||
|
const uint32_t t0 = now() + HUB_IDLE_MS;
|
||||||
|
TEST_ASSERT_EQUAL_INT(HUB_WHY_OFF, hub_decide(&k_idle, t0));
|
||||||
|
TEST_ASSERT_EQUAL_INT(HUB_WHY_OFF, hub_decide(&k_idle, t0 + 0x80000000u));
|
||||||
|
TEST_ASSERT_EQUAL_INT(HUB_WHY_OFF, hub_decide(&k_idle, t0 + 0xFFFFFFF0u));
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ---- 引脚与事件 ---------------------------------------------------------- */
|
||||||
|
|
||||||
|
/* 关 = 配成输出拉低;开 = 放成输入(R716 拉高),**从不主动写 1**。每次翻转一条事件。 */
|
||||||
|
static void test_tick_drives_the_pin_and_logs(void)
|
||||||
|
{
|
||||||
|
run_ms(HUB_IDLE_MS + 200);
|
||||||
|
TEST_ASSERT_FALSE(hubctl_is_on());
|
||||||
|
TEST_ASSERT_EQUAL_INT(0, sim_gpio_level(PIN));
|
||||||
|
TEST_ASSERT_EQUAL_INT(1, hub_events(HUB_EV_OFF));
|
||||||
|
TEST_ASSERT_EQUAL_UINT64(0, sim_gpio_rise_us(PIN)); /* 固件没写过 1 */
|
||||||
|
|
||||||
|
/* 断网一分钟 ⇒ 自己回来。 */
|
||||||
|
link(false);
|
||||||
|
run_ms(HUB_LINK_LOST_MS + 200);
|
||||||
|
TEST_ASSERT_TRUE(hubctl_is_on());
|
||||||
|
TEST_ASSERT_EQUAL_INT(HUB_WHY_LINK, hubctl_why());
|
||||||
|
TEST_ASSERT_EQUAL_INT(1, sim_gpio_level(PIN));
|
||||||
|
TEST_ASSERT_EQUAL_INT(1, hub_events(HUB_EV_ON));
|
||||||
|
TEST_ASSERT_EQUAL_UINT64(0, sim_gpio_rise_us(PIN));
|
||||||
|
|
||||||
|
/* 一直开着不重复记。 */
|
||||||
|
run_ms(5000);
|
||||||
|
TEST_ASSERT_EQUAL_INT(1, hub_events(HUB_EV_ON));
|
||||||
|
}
|
||||||
|
|
||||||
|
static void test_mode_is_saved_and_logged(void)
|
||||||
|
{
|
||||||
|
TEST_ASSERT_EQUAL_INT(HAL_ERR_ARG, hubctl_set_mode(HUB_MODE_COUNT));
|
||||||
|
TEST_ASSERT_EQUAL_INT(HAL_OK, hubctl_set_mode(HUB_MODE_ON));
|
||||||
|
TEST_ASSERT_EQUAL_INT(1, s_saves);
|
||||||
|
TEST_ASSERT_EQUAL_INT(HUB_MODE_ON, s_saved);
|
||||||
|
TEST_ASSERT_EQUAL_INT(1, hub_events(HUB_EV_MODE | HUB_MODE_ON));
|
||||||
|
run_ms(HUB_IDLE_MS + 200);
|
||||||
|
TEST_ASSERT_TRUE(hubctl_is_on()); /* 常开 */
|
||||||
|
TEST_ASSERT_EQUAL_INT(HAL_OK, hubctl_set_mode(HUB_MODE_ON));
|
||||||
|
TEST_ASSERT_EQUAL_INT(1, hub_events(HUB_EV_MODE | HUB_MODE_ON)); /* 没变不记 */
|
||||||
|
|
||||||
|
hub_mode_t m = HUB_MODE_ON;
|
||||||
|
TEST_ASSERT_TRUE(hub_mode_parse("auto", &m));
|
||||||
|
TEST_ASSERT_EQUAL_INT(HUB_MODE_AUTO, m);
|
||||||
|
TEST_ASSERT_FALSE(hub_mode_parse("off", &m));
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 从 NVS 读到的是垃圾 ⇒ 按 AUTO。 */
|
||||||
|
static void test_bad_mode_falls_back_to_auto(void)
|
||||||
|
{
|
||||||
|
hubctl_init((hub_mode_t)7, NULL, NULL, now());
|
||||||
|
TEST_ASSERT_EQUAL_INT(HUB_MODE_AUTO, hubctl_mode());
|
||||||
|
}
|
||||||
|
|
||||||
|
int main(void)
|
||||||
|
{
|
||||||
|
UNITY_BEGIN();
|
||||||
|
RUN_TEST(test_boot_window_then_off);
|
||||||
|
RUN_TEST(test_reasons_and_their_order);
|
||||||
|
RUN_TEST(test_link_lost_forces_on);
|
||||||
|
RUN_TEST(test_host_edge_and_usb_touch);
|
||||||
|
RUN_TEST(test_hold);
|
||||||
|
RUN_TEST(test_idle_window_does_not_revive_after_wrap);
|
||||||
|
RUN_TEST(test_tick_drives_the_pin_and_logs);
|
||||||
|
RUN_TEST(test_mode_is_saved_and_logged);
|
||||||
|
RUN_TEST(test_bad_mode_falls_back_to_auto);
|
||||||
|
return UNITY_END();
|
||||||
|
}
|
||||||
@@ -1020,7 +1020,7 @@ static void test_timings_match_the_documents(void)
|
|||||||
TEST_ASSERT_TRUE(ACT_HR_PG_POLL_MS > 0 && ACT_HR_PG_POLL_MS * 10 <= ACT_HR_PG_TIMEOUT_MS);
|
TEST_ASSERT_TRUE(ACT_HR_PG_POLL_MS > 0 && ACT_HR_PG_POLL_MS * 10 <= ACT_HR_PG_TIMEOUT_MS);
|
||||||
|
|
||||||
TEST_ASSERT_EQUAL_UINT32(100, ACT_KEY_MS_TAP); /* SW-5 三档 */
|
TEST_ASSERT_EQUAL_UINT32(100, ACT_KEY_MS_TAP); /* SW-5 三档 */
|
||||||
TEST_ASSERT_EQUAL_UINT32(5000, ACT_KEY_MS_ON); /* 小米8 实测 2 s 不够(t_actuator.h) */
|
TEST_ASSERT_EQUAL_UINT32(5000, ACT_KEY_MS_ON); /* 小米8 实测 2 s、3 s 都不够(t_actuator.h) */
|
||||||
TEST_ASSERT_TRUE(ACT_KEY_MS_ON < ACT_KEY_MS_FORCE); /* 开机那一下不能按成强制重启 */
|
TEST_ASSERT_TRUE(ACT_KEY_MS_ON < ACT_KEY_MS_FORCE); /* 开机那一下不能按成强制重启 */
|
||||||
TEST_ASSERT_EQUAL_UINT32(ACT_KEY_MS_ON, ACT_HR_BOOT_KEY_MS);
|
TEST_ASSERT_EQUAL_UINT32(ACT_KEY_MS_ON, ACT_HR_BOOT_KEY_MS);
|
||||||
TEST_ASSERT_EQUAL_UINT32(10000, ACT_KEY_MS_FORCE);
|
TEST_ASSERT_EQUAL_UINT32(10000, ACT_KEY_MS_FORCE);
|
||||||
@@ -1099,7 +1099,7 @@ static void test_format_prints_every_rail(void)
|
|||||||
/* ==== 0.1.2:硬重启的开机校验与重按 =======================================
|
/* ==== 0.1.2:硬重启的开机校验与重按 =======================================
|
||||||
2026-09-28 板 B 第 1 路:电源键弹片压刀片铜盘接触不良,7 次硬重启只开起来 1 次,
|
2026-09-28 板 B 第 1 路:电源键弹片压刀片铜盘接触不良,7 次硬重启只开起来 1 次,
|
||||||
失败时电流一直 1~2 mA(屏都不亮)。假手机照实测建模:
|
失败时电流一直 1~2 mA(屏都不亮)。假手机照实测建模:
|
||||||
· 第 `boots_on` 次按满 5 s 松手后开机 ⇒ 电流 ~210 mA(logo 段);
|
· 第 `boots_on` 次按满 ACT_KEY_MS_ON 松手后开机 ⇒ 电流 ~210 mA(logo 段);
|
||||||
· 其余情况 1.5 mA(关机态);
|
· 其余情况 1.5 mA(关机态);
|
||||||
· `half_on`:第一次按下去 PMIC 醒了、SoC 没接住 —— 松手后 415 mA 撑 2.5 s 再掉回去
|
· `half_on`:第一次按下去 PMIC 醒了、SoC 没接住 —— 松手后 415 mA 撑 2.5 s 再掉回去
|
||||||
(照「按 2 s」那次的实测形态),校验不能被它骗过去。 */
|
(照「按 2 s」那次的实测形态),校验不能被它骗过去。 */
|
||||||
@@ -1202,7 +1202,7 @@ static void run_phone_ms(int rail, uint32_t ms)
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/* 一轮按键 + 校验最长多久:按 5 s + 松键后判到窗口末尾。 */
|
/* 一轮按键 + 校验最长多久:按 ACT_KEY_MS_ON + 松键后判到窗口末尾。 */
|
||||||
#define ONE_TRY_MS (ACT_HR_BOOT_KEY_MS + ACT_HR_VERIFY_DELAY_MS + ACT_HR_VERIFY_WINDOW_MS + 50)
|
#define ONE_TRY_MS (ACT_HR_BOOT_KEY_MS + ACT_HR_VERIFY_DELAY_MS + ACT_HR_VERIFY_WINDOW_MS + 50)
|
||||||
#define HR_TO_PRESS_MS (ACT_HR_OFF_MS + ACT_HR_RAMP_MS + ACT_HR_PG_POLL_MS)
|
#define HR_TO_PRESS_MS (ACT_HR_OFF_MS + ACT_HR_RAMP_MS + ACT_HR_PG_POLL_MS)
|
||||||
|
|
||||||
|
|||||||
在新工单中引用
屏蔽一个用户