409 行
15 KiB
C
409 行
15 KiB
C
/* 六条「会动东西的命令」的执行体 + 风扇下限(A12,需求 §10.8)。
|
||
|
||
每一条都是同一个形状:**路号 → 在位 → 参数限幅 → 忙不忙 → 下发**。
|
||
限幅与拒绝原因全在这里,适配器一个字都不碰(cmd.h 规矩 1)。
|
||
判重、名字表、回应拼装在 cmd.c;status / events 的正文在 cmd_report.c。 */
|
||
#include <string.h>
|
||
|
||
#include "chgmode.h"
|
||
#include "cmd_priv.h"
|
||
#include "evlog.h"
|
||
#include "hal_time.h"
|
||
#include "hubctl.h"
|
||
#include "i2cbus.h"
|
||
#include "rail.h"
|
||
#include "rail_cfg.h"
|
||
#include "slot.h"
|
||
#include "t_actuator.h"
|
||
|
||
/* ---- 共用的几步 ---------------------------------------------------------- */
|
||
|
||
static cmd_err_t reject(ctx_t *c, cmd_err_t e, int64_t bad)
|
||
{
|
||
c->bad = bad;
|
||
return e;
|
||
}
|
||
|
||
/* 路号 → 在位。**空槽一律拒**(计划 §5.8:空槽上电 = 金手指裸露带 12 V)。 */
|
||
static cmd_err_t need_rail(ctx_t *c)
|
||
{
|
||
int64_t r = 0;
|
||
if (cj_get_int(c->o, "rail", &r) != 1) {
|
||
return CMD_ERR_ARG;
|
||
}
|
||
if (r < 1 || r > PHONERAIL_RAIL_COUNT) {
|
||
return reject(c, CMD_ERR_RANGE, r);
|
||
}
|
||
c->rail = (int)r;
|
||
if (!slot_present(c->rail)) {
|
||
return reject(c, CMD_ERR_EMPTY, r);
|
||
}
|
||
return CMD_OK;
|
||
}
|
||
|
||
/* 执行器(t_actuator_*)的返回值 → 回应。它的 HAL_ERR_STATE 就是「这一路正在跑别的序列」。 */
|
||
static cmd_err_t from_hal(hal_err_t e)
|
||
{
|
||
switch (e) {
|
||
case HAL_OK: return CMD_OK;
|
||
case HAL_ERR_ARG: return CMD_ERR_RANGE; /* 前面已经限过幅,走到这里是两边的限值不一致 */
|
||
case HAL_ERR_STATE: return CMD_ERR_BUSY; /* 执行器:这一路正在跑别的序列 */
|
||
default: return CMD_ERR_IO;
|
||
}
|
||
}
|
||
|
||
/* 直接动器件(I²C / GPIO)的返回值 → 回应。**失败一律 io**,不许借用 from_hal:
|
||
总线上的 HAL_ERR_STATE 不是「忙」(2026-09-27:PCA9555 不应答时关电回了 busy,
|
||
上位机会以为没执行、换个时机再关一次,其实 eFuse 早就关了)。 */
|
||
static cmd_err_t from_bus(hal_err_t e)
|
||
{
|
||
switch (e) {
|
||
case HAL_OK: return CMD_OK;
|
||
case HAL_ERR_ARG: return CMD_ERR_RANGE;
|
||
default: return CMD_ERR_IO;
|
||
}
|
||
}
|
||
|
||
/* ---- 各条命令 ------------------------------------------------------------ */
|
||
|
||
typedef struct {
|
||
int rail;
|
||
int mv;
|
||
} set_mv_arg_t;
|
||
|
||
static hal_err_t set_mv_fn(void *arg)
|
||
{
|
||
const set_mv_arg_t *a = arg;
|
||
return rail_set_mv(a->rail, a->mv);
|
||
}
|
||
|
||
cmd_err_t do_set_voltage(ctx_t *c)
|
||
{
|
||
cmd_err_t e = need_rail(c);
|
||
if (e != CMD_OK) {
|
||
return e;
|
||
}
|
||
int64_t mv = 0;
|
||
if (cj_get_int(c->o, "mv", &mv) != 1) {
|
||
return CMD_ERR_ARG;
|
||
}
|
||
/* SR-13:没有任何命令能让输出超过 4.2 V。这里限的是 DAC 能到的 3524~4010,
|
||
比 4.2 V 更紧;`rail_set_mv()` 自己还会再判一次(第四道之后的第五道)。 */
|
||
if (mv < RAIL_MV_MIN || mv > RAIL_MV_MAX) {
|
||
return reject(c, CMD_ERR_RANGE, mv);
|
||
}
|
||
/* ⚠️ DAC 在主干上,但 `rail_set_mv()` 自己**不拿锁**(它比 i2cbus 早)。
|
||
不包这一层就是在巡检的事务中间插一笔 —— 总线上两个主机同时说话。 */
|
||
set_mv_arg_t a = { .rail = c->rail, .mv = (int)mv };
|
||
e = from_bus(i2cbus_xact(I2CBUS_CH_TRUNK, SLOT_LOCK_TIMEOUT_MS, set_mv_fn, &a));
|
||
if (e == CMD_OK) {
|
||
cj_w(c->w, ",\"set\":%d", rail_setpoint_mv(c->rail));
|
||
}
|
||
return e;
|
||
}
|
||
|
||
static int32_t now_mv(int rail)
|
||
{
|
||
const safety_snap_t *s = cmd_env()->safety;
|
||
return (s != NULL && s->rail_ok[rail - 1]) ? s->rail[rail - 1].bus_mv : 0;
|
||
}
|
||
|
||
static int32_t now_ua(int rail)
|
||
{
|
||
const safety_snap_t *s = cmd_env()->safety;
|
||
return (s != NULL && s->rail_ok[rail - 1]) ? s->rail[rail - 1].current_ua : 0;
|
||
}
|
||
|
||
cmd_err_t do_power_cycle(ctx_t *c)
|
||
{
|
||
cmd_err_t e = need_rail(c);
|
||
if (e != CMD_OK) {
|
||
return e;
|
||
}
|
||
/* 断电时长不收参数:§7.4 的 3~5 s 窗口由 `ACT_HR_OFF_MS` 定死。
|
||
让上位机传一个 1 s 进来,现象是「有的机型压根没重启」(t_actuator.h)。 */
|
||
e = from_hal(t_actuator_hard_reset(c->rail));
|
||
if (e == CMD_OK) {
|
||
evlog_put((uint8_t)c->rail, LOG_EV_REBOOT, CMD_REBOOT_ARG(true, c->link),
|
||
now_mv(c->rail), now_ua(c->rail));
|
||
}
|
||
return e;
|
||
}
|
||
|
||
/* "pwr+vdn" → 逻辑键位。未知的键名是参数错,不是「忽略掉那一个」。 */
|
||
static bool parse_keys(const char *s, uint32_t *keys)
|
||
{
|
||
static const struct { const char *name; uint32_t bit; } k[] = {
|
||
{ "pwr", ACT_KEYS_PWR }, { "vup", ACT_KEYS_VUP }, { "vdn", ACT_KEYS_VDN },
|
||
};
|
||
uint32_t m = 0;
|
||
while (*s != '\0') {
|
||
const char *end = strchr(s, '+');
|
||
const size_t n = end ? (size_t)(end - s) : strlen(s);
|
||
bool hit = false;
|
||
for (size_t i = 0; i < sizeof(k) / sizeof(k[0]); i++) {
|
||
if (strlen(k[i].name) == n && strncmp(k[i].name, s, n) == 0) {
|
||
m |= k[i].bit;
|
||
hit = true;
|
||
}
|
||
}
|
||
if (!hit) {
|
||
return false;
|
||
}
|
||
s += n;
|
||
if (*s == '+') {
|
||
s++;
|
||
if (*s == '\0') {
|
||
return false; /* "pwr+" */
|
||
}
|
||
}
|
||
}
|
||
*keys = m;
|
||
return m != 0;
|
||
}
|
||
|
||
cmd_err_t do_key(ctx_t *c)
|
||
{
|
||
cmd_err_t e = need_rail(c);
|
||
if (e != CMD_OK) {
|
||
return e;
|
||
}
|
||
const char *ks = NULL;
|
||
int64_t ms = 0;
|
||
uint32_t keys = 0;
|
||
if (cj_get_str(c->o, "keys", &ks) != 1 || !parse_keys(ks, &keys) ||
|
||
cj_get_int(c->o, "ms", &ms) != 1) {
|
||
return CMD_ERR_ARG;
|
||
}
|
||
if (ms < 1 || ms > ACT_KEY_MAX_MS) {
|
||
return reject(c, CMD_ERR_RANGE, ms); /* 拦「把电源键按死」的手误 */
|
||
}
|
||
e = from_hal(t_actuator_key(c->rail, keys, (uint32_t)ms));
|
||
if (e == CMD_OK && keys == ACT_KEYS_PWR && ms >= ACT_KEY_MS_FORCE) {
|
||
/* 长按电源键 ≥10 s = 两级重启的「一级(软)」(需求 §4.1),记一条重启事件。 */
|
||
evlog_put((uint8_t)c->rail, LOG_EV_REBOOT, CMD_REBOOT_ARG(false, c->link),
|
||
now_mv(c->rail), now_ua(c->rail));
|
||
}
|
||
return e;
|
||
}
|
||
|
||
cmd_err_t do_edl(ctx_t *c)
|
||
{
|
||
const cmd_err_t e = need_rail(c);
|
||
return (e != CMD_OK) ? e : from_hal(t_actuator_edl(c->rail));
|
||
}
|
||
|
||
cmd_err_t do_vbus(ctx_t *c)
|
||
{
|
||
cmd_err_t e = need_rail(c);
|
||
if (e != CMD_OK) {
|
||
return e;
|
||
}
|
||
bool on = false;
|
||
if (cj_get_bool(c->o, "on", &on) != 1) {
|
||
return CMD_ERR_ARG;
|
||
}
|
||
/* EDL 序列自己要动 VBUS,中途插手就把它的时序打乱了。 */
|
||
if (t_actuator_busy(c->rail)) {
|
||
return CMD_ERR_BUSY;
|
||
}
|
||
/* eFuse 关着时开 VBUS = 让手机只靠 USB 供着电 —— `rail_all_off()` 专门
|
||
「先断 VBUS 再关 eFuse」就是为了不留这个窗口,命令也不许把它造出来。 */
|
||
if (on && !rail_efuse_is_on(c->rail)) {
|
||
return reject(c, CMD_ERR_STATE, 1);
|
||
}
|
||
t_actuator_cancel_vbus_back(c->rail); /* 人动了 VBUS:硬重启排的「过会儿接回」作废 */
|
||
return from_bus(slot_vbus_enable(c->rail, on));
|
||
}
|
||
|
||
/* A15:意图出口(cmd.h 的 `cmd_intent_fn`)。 */
|
||
static void note_intent(int rail, bool on)
|
||
{
|
||
const cmd_env_t *env = cmd_env();
|
||
if (env != NULL && env->intent != NULL) {
|
||
env->intent(env->intent_ctx, rail, on);
|
||
}
|
||
}
|
||
|
||
cmd_err_t do_power(ctx_t *c)
|
||
{
|
||
cmd_err_t e = need_rail(c);
|
||
if (e != CMD_OK) {
|
||
return e;
|
||
}
|
||
bool on = false;
|
||
if (cj_get_bool(c->o, "on", &on) != 1) {
|
||
return CMD_ERR_ARG;
|
||
}
|
||
if (t_actuator_busy(c->rail)) {
|
||
return CMD_ERR_BUSY; /* 硬重启走到一半再开关一次是自作主张(t_actuator_abort 同理) */
|
||
}
|
||
t_actuator_cancel_vbus_back(c->rail); /* 人动了电:硬重启排的「过会儿接回 VBUS」作废 */
|
||
if (on) {
|
||
const hal_err_t ee = rail_efuse_enable(c->rail, true);
|
||
if (ee == HAL_OK) {
|
||
note_intent(c->rail, true);
|
||
}
|
||
return from_bus(ee);
|
||
}
|
||
/* 关:与 `rail_all_off()` 同序 —— **先断 VBUS 再关 eFuse**。
|
||
VBUS 走 I²C 可能断不掉,eFuse 是 GPIO 永远关得掉:前者失败也照样关后者,
|
||
回应报 io,上位机据此知道「直供断了,VBUS 可能还通着」。 */
|
||
const hal_err_t ev = slot_vbus_enable(c->rail, false);
|
||
const hal_err_t ee = rail_efuse_enable(c->rail, false);
|
||
if (ee == HAL_OK) {
|
||
note_intent(c->rail, false); /* 直供确实断了:下次开机也别开它 */
|
||
/* 这一路停了,刷机模式也一并退出(不占 CHG_MODE_MAX_RAILS 的名额)。 */
|
||
if (chgmode_active(c->rail, hal_time_now_ms())) {
|
||
(void)chgmode_set(c->rail, 0, hal_time_now_ms());
|
||
evlog_put((uint8_t)c->rail, LOG_EV_CHG_MODE, CHG_EV_OFF, now_mv(c->rail), now_ua(c->rail));
|
||
}
|
||
}
|
||
return from_bus(ee != HAL_OK ? ee : ev);
|
||
}
|
||
|
||
/* 0.1.5 刷机模式(chgmode.h):`{"cmd":"flash_mode","rail":1,"on":true,"min":60}`。
|
||
min 省略 = CHG_MODE_DEFAULT_MIN;关不带 min。回应带 `"left":秒`。
|
||
0.1.6 加钟:`{"cmd":"flash_mode","rail":1,"add":30}`(与 on / min 互斥)—— 只给开着的窗口加,
|
||
剩余最多 CHG_MODE_MAX_MIN 分钟(超出截断);没开着回 state。
|
||
⚠️ 只改判据,**不动 VBUS**:开 VBUS 仍用 `vbus` 命令(顺序无所谓:先开模式再开 VBUS,
|
||
或 VBUS 被 SW-8 断了之后开模式再重开 VBUS)。 */
|
||
cmd_err_t do_flash_mode(ctx_t *c)
|
||
{
|
||
cmd_err_t e = need_rail(c);
|
||
if (e != CMD_OK) {
|
||
return e;
|
||
}
|
||
/* 加钟(0.1.6):`{"cmd":"flash_mode","rail":1,"add":30}`,与 on 二选一。 */
|
||
int64_t add = 0;
|
||
const int ga = cj_get_int(c->o, "add", &add);
|
||
if (ga < 0) {
|
||
return CMD_ERR_ARG;
|
||
}
|
||
if (ga == 1) {
|
||
bool on_probe = false;
|
||
int64_t min_probe = 0;
|
||
if (cj_get_bool(c->o, "on", &on_probe) != 0 || cj_get_int(c->o, "min", &min_probe) != 0) {
|
||
return CMD_ERR_ARG; /* add 不和 on / min 混用 */
|
||
}
|
||
if (add < 1 || add > CHG_MODE_MAX_MIN) {
|
||
return reject(c, CMD_ERR_RANGE, add);
|
||
}
|
||
const uint32_t now = hal_time_now_ms();
|
||
const hal_err_t he = chgmode_extend(c->rail, (uint32_t)add, now);
|
||
if (he == HAL_ERR_STATE) {
|
||
return reject(c, CMD_ERR_STATE, 0); /* 没开着:要重新开,不是加钟 */
|
||
}
|
||
if (he != HAL_OK) {
|
||
return reject(c, CMD_ERR_RANGE, add);
|
||
}
|
||
evlog_put((uint8_t)c->rail, LOG_EV_CHG_MODE, (uint8_t)(CHG_EV_EXTEND | (uint32_t)add),
|
||
now_mv(c->rail), now_ua(c->rail));
|
||
cj_w(c->w, ",\"left\":%lu", (unsigned long)chgmode_left_s(c->rail, now));
|
||
return CMD_OK;
|
||
}
|
||
bool on = false;
|
||
if (cj_get_bool(c->o, "on", &on) != 1) {
|
||
return CMD_ERR_ARG;
|
||
}
|
||
int64_t min = CHG_MODE_DEFAULT_MIN;
|
||
const int g = cj_get_int(c->o, "min", &min);
|
||
if (g < 0 || (!on && g == 1)) {
|
||
return CMD_ERR_ARG;
|
||
}
|
||
const uint32_t now = hal_time_now_ms();
|
||
if (!on) {
|
||
const bool was = chgmode_active(c->rail, now);
|
||
(void)chgmode_set(c->rail, 0, now);
|
||
if (was) {
|
||
evlog_put((uint8_t)c->rail, LOG_EV_CHG_MODE, CHG_EV_OFF, now_mv(c->rail), now_ua(c->rail));
|
||
}
|
||
cj_w(c->w, ",\"left\":0");
|
||
return CMD_OK;
|
||
}
|
||
if (min < 1 || min > CHG_MODE_MAX_MIN) {
|
||
return reject(c, CMD_ERR_RANGE, min);
|
||
}
|
||
/* 关着电的路开刷机模式没有意义(手机没电),与 vbus 命令同一条规矩。 */
|
||
if (!rail_efuse_is_on(c->rail)) {
|
||
return reject(c, CMD_ERR_STATE, 1);
|
||
}
|
||
const hal_err_t he = chgmode_set(c->rail, (uint32_t)min, now);
|
||
if (he == HAL_ERR_STATE) {
|
||
return reject(c, CMD_ERR_STATE, chgmode_count(now)); /* 已有 CHG_MODE_MAX_RAILS 路开着 */
|
||
}
|
||
if (he != HAL_OK) {
|
||
return reject(c, CMD_ERR_RANGE, min);
|
||
}
|
||
evlog_put((uint8_t)c->rail, LOG_EV_CHG_MODE, (uint8_t)min, now_mv(c->rail), now_ua(c->rail));
|
||
cj_w(c->w, ",\"left\":%lu", (unsigned long)chgmode_left_s(c->rail, now));
|
||
return CMD_OK;
|
||
}
|
||
|
||
void cmd_chgmode_tick(uint32_t now)
|
||
{
|
||
const uint32_t ended = chgmode_tick(now);
|
||
for (int r = 1; r <= PHONERAIL_RAIL_COUNT; r++) {
|
||
if ((ended & (1u << (r - 1))) != 0) {
|
||
evlog_put((uint8_t)r, LOG_EV_CHG_MODE, CHG_EV_EXPIRED, now_mv(r), now_ua(r));
|
||
}
|
||
}
|
||
/* 0.1.13:这一路已经断了电(刀片被拔走 / SR-5 等判据关路 / mux 失联全断 —— 都是 t_safety 关的 eFuse,
|
||
不经过 `power` 命令)⇒ 刷机模式一并退出。不退的话它一直占着 CHG_MODE_MAX_RAILS 的名额
|
||
(2026-09-28 板 B:第 2 槽刀片拔走后还挂着 48 分钟),插回来上电时还带着放宽的判据。
|
||
硬重启那几秒 eFuse 本来就关着,不算(执行器忙)。 */
|
||
for (int r = 1; r <= PHONERAIL_RAIL_COUNT; r++) {
|
||
if (chgmode_active(r, now) && !rail_efuse_is_on(r) && !t_actuator_busy(r)) {
|
||
(void)chgmode_set(r, 0, now);
|
||
evlog_put((uint8_t)r, LOG_EV_CHG_MODE, CHG_EV_DROPPED, now_mv(r), now_ua(r));
|
||
}
|
||
}
|
||
}
|
||
|
||
/* 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)
|
||
{
|
||
int64_t duty = 0;
|
||
if (cj_get_int(c->o, "duty", &duty) != 1) {
|
||
return CMD_ERR_ARG;
|
||
}
|
||
if (duty < 0 || duty > 1000) {
|
||
return reject(c, CMD_ERR_RANGE, duty);
|
||
}
|
||
/* 手动值只是**下限**:温度曲线要得更高时照曲线来(t_thermal.h)。0 = 交回自动。 */
|
||
t_thermal_set_floor((uint16_t)duty);
|
||
cj_w(c->w, ",\"floor\":%u", (unsigned)t_thermal_floor());
|
||
return CMD_OK;
|
||
}
|