文件
phonerail/firmware/main/ulink.c
T
zhou yang和Claude Opus 5.5 a32fa06c43 固件:A12 命令处理器 + A13 USB 分帧 + A14 WiFi/MQTT(含 C1 配网)
- 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]>
2026-09-27 12:33:18 +08:00

244 行
7.0 KiB
C

/* USB 链路分帧。格式与两条「为什么」在 ulink.h。 */
#include "ulink.h"
#include <stdio.h>
#include <string.h>
#include "cmd_json.h"
uint16_t ulink_crc16(const char *data, size_t len)
{
uint16_t crc = 0xFFFF;
for (size_t i = 0; i < len; i++) {
crc ^= (uint16_t)((uint8_t)data[i] << 8);
for (int b = 0; b < 8; b++) {
crc = (crc & 0x8000u) ? (uint16_t)((crc << 1) ^ 0x1021u) : (uint16_t)(crc << 1);
}
}
return crc;
}
static int hexval(char c)
{
if (c >= '0' && c <= '9') {
return c - '0';
}
if (c >= 'A' && c <= 'F') {
return c - 'A' + 10;
}
if (c >= 'a' && c <= 'f') {
return c - 'a' + 10;
}
return -1;
}
/* s[0..len) 末尾是不是 "*XXXX"。是则返回 1 并给出 CRC 值。 */
static int crc_suffix(const char *s, size_t len, uint16_t *crc)
{
if (len < ULINK_CRC_LEN || s[len - ULINK_CRC_LEN] != '*') {
return 0;
}
uint16_t v = 0;
for (size_t i = len - 4; i < len; i++) {
const int h = hexval(s[i]);
if (h < 0) {
return 0;
}
v = (uint16_t)((v << 4) | (uint16_t)h);
}
*crc = v;
return 1;
}
/* ---- 接收 ---------------------------------------------------------------- */
void ulink_rx_init(ulink_rx_t *rx) { memset(rx, 0, sizeof(*rx)); }
static bool is_cmd_line(const ulink_rx_t *rx)
{
return rx->n >= ULINK_PREFIX_LEN && memcmp(rx->buf, ULINK_CMD_PREFIX, ULINK_PREFIX_LEN) == 0;
}
static void end_of_line(ulink_rx_t *rx, ulink_rx_fn fn, void *ctx)
{
if (rx->n > 0 && rx->buf[rx->n - 1] == '\r') {
rx->n--;
}
rx->buf[rx->n] = '\0';
if (!is_cmd_line(rx)) {
rx->ignored++;
} else if (rx->overflow) {
rx->too_long++;
fn(ctx, ULINK_RX_TOO_LONG, "");
} else {
char *json = rx->buf + ULINK_PREFIX_LEN;
size_t len = rx->n - ULINK_PREFIX_LEN;
uint16_t want = 0;
const int has_crc = crc_suffix(json, len, &want);
if (has_crc) {
len -= ULINK_CRC_LEN;
json[len] = '\0';
}
if (rx->bad) {
rx->bad_bytes++;
fn(ctx, ULINK_RX_BAD_BYTES, json);
} else if (has_crc && ulink_crc16(json, len) != want) {
rx->bad_crc++;
fn(ctx, ULINK_RX_BAD_CRC, json);
} else {
rx->cmds++;
fn(ctx, ULINK_RX_CMD, json);
}
}
rx->n = 0;
rx->overflow = false;
rx->bad = false;
}
void ulink_rx_feed(ulink_rx_t *rx, const uint8_t *data, size_t len, ulink_rx_fn fn, void *ctx)
{
for (size_t i = 0; i < len; i++) {
const uint8_t c = data[i];
if (c == '\n') {
end_of_line(rx, fn, ctx);
continue;
}
/* NUL 与控制字符:记下来,行尾整行拒。⚠️ 不能只是跳过 —— NUL 会让后面的
JSON 解析在 NUL 处提前结束,`{"cmd":"status"}\0垃圾` 就被当成一条合法命令了。 */
if ((c < 0x20 && c != '\r' && c != '\t') || c == 0x7F) {
rx->bad = true;
}
if (rx->n >= ULINK_RX_LINE_MAX) {
rx->overflow = true; /* 前缀已经在 buf 里了,行尾还认得出它是不是命令 */
continue;
}
rx->buf[rx->n++] = (char)c;
}
}
/* ---- 发送 ---------------------------------------------------------------- */
void ulink_tx_init(ulink_tx_t *tx, ulink_sink_fn sink, void *ctx)
{
memset(tx, 0, sizeof(*tx));
tx->sink = sink;
tx->ctx = ctx;
tx->at_bol = true;
}
/* 写一段,按**真正写出去的**最后一个字节更新 at_bol。 */
static bool put(ulink_tx_t *tx, const char *d, size_t n)
{
if (n == 0) {
return true;
}
const size_t w = tx->sink(tx->ctx, d, n);
if (w > 0) {
tx->at_bol = (d[w - 1] == '\n');
}
if (w < n) {
tx->short_writes++;
return false;
}
return true;
}
void ulink_tx_log(ulink_tx_t *tx, const char *data, size_t len)
{
size_t start = 0;
for (size_t i = 0; i <= len; i++) {
const bool eol = (i < len && data[i] == '\n');
if (i < len && !eol) {
continue;
}
const size_t seg = i - start + (eol ? 1u : 0u); /* 带上这个 \n */
if (seg > 0) {
if (tx->at_bol && data[start] == '@') {
if (!put(tx, " ", 1)) {
return;
}
}
if (!put(tx, data + start, seg)) {
return; /* 写不下就丢掉剩下的日志:日志 UART 上还有一份 */
}
}
start = i + 1;
}
}
bool ulink_tx_frame(ulink_tx_t *tx, const char *json, size_t len)
{
if (!tx->at_bol && !put(tx, "\n", 1)) {
return false;
}
char tail[ULINK_CRC_LEN + 2];
snprintf(tail, sizeof(tail), "*%04X\n", (unsigned)ulink_crc16(json, len));
/* 三段分开写:前缀、JSON、CRC。中间被别的写者插进来的话 CRC 会对不上,
上位机丢弃这一行 —— 这正是必带 CRC 的理由(ulink.h)。 */
const bool ok = put(tx, ULINK_RSP_PREFIX, ULINK_PREFIX_LEN) && put(tx, json, len) &&
put(tx, tail, ULINK_CRC_LEN + 1);
if (ok) {
tx->frames++;
}
return ok;
}
/* ---- 链路层错误 ---------------------------------------------------------- */
const char *ulink_rx_err_str(ulink_rx_kind_t kind)
{
switch (kind) {
case ULINK_RX_CMD: return NULL;
case ULINK_RX_TOO_LONG: return "too_long";
case ULINK_RX_BAD_CRC: return "bad_crc";
case ULINK_RX_BAD_BYTES: return "bad_frame";
}
return "bad_frame";
}
size_t ulink_link_error(const char *payload, const char *err, char *out, size_t cap)
{
/* `cj_parse()` 是纯函数(不碰任何静态量),在链路任务里调是安全的 ——
与只许在执行器任务里调的 `cmd_handle_line()` 不同。 */
cj_obj_t o;
int64_t seq = 0;
const bool has_seq = payload != NULL && cj_parse(payload, &o) &&
cj_get_int(&o, "seq", &seq) == 1 && seq >= 1 && seq <= (int64_t)UINT32_MAX;
cj_writer_t w;
cj_w_init(&w, out, cap);
if (has_seq) {
cj_w(&w, "{\"seq\":%lu,\"ok\":false,\"err\":\"%s\"}", (unsigned long)seq, err);
} else {
cj_w(&w, "{\"ok\":false,\"err\":\"%s\"}", err);
}
return w.overflow ? 0 : w.pos;
}
/* ---- 参考解帧器 ---------------------------------------------------------- */
int ulink_deframe(const char *line, char *out, size_t cap)
{
size_t len = strlen(line);
if (len > 0 && line[len - 1] == '\r') {
len--;
}
if (len < ULINK_PREFIX_LEN + ULINK_CRC_LEN ||
memcmp(line, ULINK_RSP_PREFIX, ULINK_PREFIX_LEN) != 0) {
return -1;
}
const char *json = line + ULINK_PREFIX_LEN;
size_t jlen = len - ULINK_PREFIX_LEN;
uint16_t want = 0;
if (!crc_suffix(json, jlen, &want)) {
return -1; /* 回应必带 CRC */
}
jlen -= ULINK_CRC_LEN;
if (ulink_crc16(json, jlen) != want || jlen + 1 > cap) {
return -1;
}
memcpy(out, json, jlen);
out[jlen] = '\0';
return (int)jlen;
}