- A12 单一命令处理器(cmd*.c):一行 JSON 进出、seq 全局判重(32 条按值)、
限幅 / 空槽一律拒 / 拒绝记事件;命令号与错误码冻结(进 flash)。新增 power 命令,
fan 改为只抬不压的下限(t_thermal_set_floor)。rail_set_mv 注明不拿锁,调用方包 xact。
- A13 USB-Serial-JTAG 分帧(ulink*.c):@C / @R 前缀 + CRC-16,日志行首 @ 垫空格,
任意切块可切;关掉 USB 副控制台(它绕开驱动写 FIFO)。PC 端客户端 tools/prlink.py。
- A14 + C1(netcfg / mlink / cmd_net):topic phonerail/<mac>/{cmd,rsp,tele,state},
mqtts(evse-ca 编进固件)、1 s 遥测、遗嘱离线;wifi / mqtt 配置只收 USB、存 NVS,
net 永不回显密码。
- 协议定稿写进需求文档 §10.8(A12 / A13 / A14 三节);N7 撤销改 D7(机架管理前后端)。
宿主机 23 个 ctest / 400 例全绿;板 B 已烧 0.1.0-a14,经 WiFi → 公网 → 8883 TLS 真连网验过。
固件 0xE2220 / 1 MB 分区只剩 12%,C2 前要重排分区表。
Co-Authored-By: Claude Opus 5.5 <[email protected]>
133 行
5.3 KiB
C
133 行
5.3 KiB
C
/* `status` 与 `events` 两条只读命令的正文(A12,需求 §10.8)。
|
|
|
|
两条都是**只读、幂等**的,所以不进判重窗口(cmd.h)。
|
|
输出一律是整数(mV / µA / m°C / 千分比 / ms),不引浮点,和固件内部口径一致;
|
|
读不到的量写 `null`,**不写 0** —— 0 mA 与「这一圈没读到」是两回事,
|
|
上位机的卡死判据(电流曲线)拿 0 当真值会直接误判。 */
|
|
#include <stdio.h>
|
|
|
|
#include "cmd_priv.h"
|
|
#include "evlog.h"
|
|
#include "hal_time.h"
|
|
#include "pca9555.h"
|
|
#include "phonerail.h"
|
|
#include "rail.h"
|
|
#include "rail_cfg.h"
|
|
#include "slot.h"
|
|
#include "t_actuator.h"
|
|
|
|
static const char *b(bool v) { return v ? "true" : "false"; }
|
|
|
|
static void rail_obj(cj_writer_t *w, int rail, const safety_snap_t *s)
|
|
{
|
|
const int i = rail - 1;
|
|
cj_w(w, "{\"r\":%d,\"pr\":%s,\"en\":%s,\"vb\":%s,\"set\":%d", rail, b(slot_present(rail)),
|
|
b(rail_efuse_is_on(rail)), b(slot_vbus_is_on(rail)), rail_setpoint_mv(rail));
|
|
if (s != NULL) {
|
|
if (s->rail_ok[i]) {
|
|
cj_w(w, ",\"mv\":%ld,\"ua\":%ld", (long)s->rail[i].bus_mv,
|
|
(long)s->rail[i].current_ua);
|
|
} else {
|
|
cj_w(w, ",\"mv\":null,\"ua\":null");
|
|
}
|
|
if (s->io_ok[i]) {
|
|
cj_w(w, ",\"pg\":%s,\"flt\":%s", b(pca9555_efuse_pg(&s->io[i])),
|
|
b(pca9555_efuse_fault(&s->io[i])));
|
|
} else {
|
|
cj_w(w, ",\"pg\":null,\"flt\":null");
|
|
}
|
|
}
|
|
cj_w(w, ",\"st\":\"%s\",\"res\":\"%s\"}", t_actuator_state_str(t_actuator_state(rail)),
|
|
t_actuator_result_str(t_actuator_last_result(rail)));
|
|
}
|
|
|
|
static void thermal_obj(cj_writer_t *w, const thermal_snap_t *t)
|
|
{
|
|
cj_w(w, ",\"temp\":[");
|
|
for (int i = 0; i < T_THERMAL_NTC_COUNT; i++) {
|
|
if (t->temp_ok[i]) {
|
|
cj_w(w, "%s%ld", i ? "," : "", (long)t->temp_mc[i]);
|
|
} else {
|
|
cj_w(w, "%snull", i ? "," : "");
|
|
}
|
|
}
|
|
cj_w(w, "],\"fan\":{\"duty\":%u,\"floor\":%u,\"en\":%s,\"rpm\":[", (unsigned)t->duty,
|
|
(unsigned)t_thermal_floor(), b(t->fan_en));
|
|
for (int i = 0; i < T_THERMAL_FAN_COUNT; i++) {
|
|
if (t->fan[i].rpm_valid) {
|
|
cj_w(w, "%s%lu", i ? "," : "", (unsigned long)t->fan[i].rpm);
|
|
} else {
|
|
cj_w(w, "%snull", i ? "," : "");
|
|
}
|
|
}
|
|
cj_w(w, "],\"stall\":[");
|
|
for (int i = 0; i < T_THERMAL_FAN_COUNT; i++) {
|
|
cj_w(w, "%s%s", i ? "," : "", b(t->fan[i].stalled));
|
|
}
|
|
cj_w(w, "]},\"derate\":%s", b(t->derate));
|
|
}
|
|
|
|
void cmd_report_status(cj_writer_t *w, const cmd_env_t *env)
|
|
{
|
|
const safety_snap_t *s = env->safety;
|
|
cj_w(w, ",\"fw\":\"%s\",\"up\":%lu", phonerail_version(), (unsigned long)hal_time_now_ms());
|
|
if (s != NULL) {
|
|
if (s->input_ok) {
|
|
cj_w(w, ",\"in\":{\"mv\":%ld,\"ua\":%ld}", (long)s->input.bus_mv,
|
|
(long)s->input.current_ua);
|
|
} else {
|
|
cj_w(w, ",\"in\":null");
|
|
}
|
|
}
|
|
cj_w(w, ",\"rails\":[");
|
|
for (int rail = 1; rail <= PHONERAIL_RAIL_COUNT; rail++) {
|
|
if (rail > 1) {
|
|
cj_w(w, ",");
|
|
}
|
|
rail_obj(w, rail, s);
|
|
}
|
|
cj_w(w, "]");
|
|
if (env->thermal != NULL) {
|
|
thermal_obj(w, env->thermal);
|
|
}
|
|
if (s != NULL) {
|
|
cj_w(w, ",\"safety\":{\"cycles\":%lu,\"skipped\":%lu,\"i2c_err\":%lu,\"events\":%lu}",
|
|
(unsigned long)s->cycles, (unsigned long)s->skipped,
|
|
(unsigned long)s->i2c_errors, (unsigned long)s->events);
|
|
}
|
|
evlog_stats_t es;
|
|
evlog_stats(&es);
|
|
cmd_stats_t cs;
|
|
cmd_stats(&cs);
|
|
/* `ev_next` 是**已落盘**的下一个序号;还在 RAM 里的 `ev_pending` 条要等 events 触发落盘。
|
|
上位机拿 `ev_next` 与自己的游标比,就知道要不要拉日志,不用每次都拉。 */
|
|
cj_w(w, ",\"ev_next\":%lu,\"ev_pending\":%lu,\"cmd\":{\"lines\":%lu,\"dups\":%lu,\"rej\":%lu}",
|
|
(unsigned long)es.next_seq, (unsigned long)es.pending, (unsigned long)cs.lines,
|
|
(unsigned long)cs.dups, (unsigned long)cs.rejects);
|
|
}
|
|
|
|
void cmd_report_events(cj_writer_t *w, uint32_t from_seq)
|
|
{
|
|
(void)evlog_flush(0);
|
|
evlog_stats_t es;
|
|
evlog_stats(&es);
|
|
|
|
evlog_rec_t rec[CMD_EVENTS_MAX];
|
|
const uint32_t n = evlog_read(from_seq, rec, CMD_EVENTS_MAX);
|
|
/* `evlog_read()` 从 max(from, oldest) 开始读、序号连续(洞也占位),
|
|
所以每条的序号可以这样算回来 —— 回应里逐条带上,上位机不用自己推。 */
|
|
const uint32_t start = (from_seq < es.oldest_seq) ? es.oldest_seq : from_seq;
|
|
|
|
/* `oldest` 大于上位机的游标 ⇒ 中间那一段已经被环形缓冲盖掉了(evlog.h)。 */
|
|
cj_w(w, ",\"oldest\":%lu,\"next\":%lu,\"ev\":[", (unsigned long)es.oldest_seq,
|
|
(unsigned long)es.next_seq);
|
|
for (uint32_t i = 0; i < n; i++) {
|
|
/* [序号, 上电毫秒, 路, 类型, arg, mV, µA]。类型是 `log_ev_t` 的冻结数字,
|
|
名字表在上位机(中文名进不了 ASCII 行,也没必要每条都带)。 */
|
|
cj_w(w, "%s[%lu,%lu,%u,%u,%u,%ld,%ld]", i ? "," : "", (unsigned long)(start + i),
|
|
(unsigned long)rec[i].t_ms, (unsigned)rec[i].rail, (unsigned)rec[i].type,
|
|
(unsigned)rec[i].arg, (long)rec[i].bus_mv, (long)rec[i].current_ua);
|
|
}
|
|
cj_w(w, "]");
|
|
}
|