/* `status` 与 `events` 两条只读命令的正文(A12,需求 §10.8)。 两条都是**只读、幂等**的,所以不进判重窗口(cmd.h)。 输出一律是整数(mV / µA / m°C / 千分比 / ms),不引浮点,和固件内部口径一致; 读不到的量写 `null`,**不写 0** —— 0 mA 与「这一圈没读到」是两回事, 上位机的卡死判据(电流曲线)拿 0 当真值会直接误判。 */ #include #include "board_pins.h" #include "chgmode.h" #include "cmd_priv.h" #include "evlog.h" #include "hal_gpio.h" #include "hal_time.h" #include "ota.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"; } /* C2:`"ota":{"st":"dl","got":N,"size":N,"err":null,"pend":false,"bad":null}`。 `err` 是上一次失败的原因(成功 / 没做过是 null);`pend` = 正在跑的固件还没自证健康; `bad` = 被判无效、回滚掉的那份固件的版本号。版本号来自镜像头,按 ASCII 过滤一遍再写。 */ static void ota_obj(cj_writer_t *w) { ota_status_t st; ota_status_get(&st); cj_w(w, ",\"ota\":{\"st\":\"%s\",\"got\":%lu,\"size\":%lu,\"err\":", ota_phase_str(st.phase), (unsigned long)st.got, (unsigned long)st.size); if (st.err == OTA_E_NONE) { cj_w(w, "null"); } else { cj_w(w, "\"%s\"", ota_err_str(st.err)); } cj_w(w, ",\"pend\":%s,\"bad\":", b(st.pending)); if (st.bad[0] == '\0') { cj_w(w, "null}"); return; } char v[sizeof(st.bad)]; size_t n = 0; for (const char *s = st.bad; *s != '\0' && n < sizeof(v) - 1; s++) { const unsigned char ch = (unsigned char)*s; v[n++] = (ch < 0x20 || ch >= 0x7F || ch == '"' || ch == '\\') ? '?' : (char)ch; } v[n] = '\0'; cj_w(w, "\"%s\"}", v); } 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"); } } /* 0.1.5:刷机模式剩余秒数(0 = 没开)。 */ cj_w(w, ",\"chg\":%lu", (unsigned long)chgmode_left_s(rail, hal_time_now_ms())); 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()); /* 上游 USB-C 插没插电脑(`UP_VBUS_DET`,bringup 里配的输入)。读不到写 null。 ⚠️ 这是 VBUS 在不在,不是 Hub 有没有枚举 / 挂起:电脑休眠时 VBUS 多半还在。 */ int host = 0; if (hal_gpio_read(BOARD_PIN_UP_VBUS_DET, &host) == HAL_OK) { cj_w(w, ",\"host\":%s", b(host != 0)); } else { cj_w(w, ",\"host\":null"); } 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,\"inits\":%lu}", (unsigned long)s->cycles, (unsigned long)s->skipped, (unsigned long)s->i2c_errors, (unsigned long)s->events, (unsigned long)s->blade_inits); } ota_obj(w); 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, "]"); }