- 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]>
156 行
5.6 KiB
C
156 行
5.6 KiB
C
/* USB-Serial-JTAG 链路的 ESP 胶水(A13)。**只进 IDF 目标**,分帧逻辑在 ulink.c(宿主机可测)。
|
||
|
||
---- 这根线归谁 ----------------------------------------------------------
|
||
sdkconfig 里**关掉了副控制台**(`CONFIG_ESP_CONSOLE_SECONDARY_NONE`):
|
||
IDF 的副控制台会绕开驱动直接往 USB FIFO 写 stdout,和这里的写入互不同步,
|
||
一条回应中间插进半行 printf 就被拆坏了(CRC 能兜住,但那是每条都可能坏)。
|
||
现在这根线上的字节**只有两个来源**,都经同一把互斥锁、同一个 `ulink_tx_t`:
|
||
· `esp_log` 钩子:每条日志复制一份到 USB(UART0 / J33 照旧拿全量);
|
||
· 命令回应:执行器任务经 `cmd_queue` 的回调写。
|
||
`printf()` 与 panic 输出只去 UART0。代价:USB 上看不到 panic 回溯 ——
|
||
复位原因会在下一次启动日志里报,回溯要接 J33。
|
||
|
||
---- 两个写超时 ----------------------------------------------------------
|
||
上位机没开串口时 USB 发不出去,`usb_serial_jtag_write_bytes()` 会等到超时。
|
||
· 日志:**0 等待**,写不下就丢(UART 上还有一份)。日志钩子在**任何任务**里被调,
|
||
包括 20 ms 节拍的 `t_safety`,等一下就是把巡检拖慢一下;
|
||
· 回应:等 `ULINK_ESP_FRAME_WAIT_MS`。回应在执行器任务里写,等太久键会松得晚。
|
||
拿锁同理:日志拿不到锁就不等(丢 USB 那份),回应等一小会儿。 */
|
||
#include <stdarg.h>
|
||
#include <stdio.h>
|
||
#include <string.h>
|
||
|
||
#include "cmd_queue.h"
|
||
#include "driver/usb_serial_jtag.h"
|
||
#include "esp_log.h"
|
||
#include "freertos/FreeRTOS.h"
|
||
#include "freertos/semphr.h"
|
||
#include "ulink.h"
|
||
#include "ulink_esp.h"
|
||
|
||
static const char *TAG = "ulink";
|
||
|
||
#define ULINK_ESP_TX_BUF 4096 /* 装得下两条满配 status(≈1 KB)+ 一堆日志 */
|
||
#define ULINK_ESP_RX_BUF 1024
|
||
#define ULINK_ESP_FRAME_WAIT_MS 20
|
||
#define ULINK_ESP_LOG_LINE 256
|
||
|
||
static SemaphoreHandle_t s_mtx;
|
||
static ulink_tx_t s_tx;
|
||
static ulink_rx_t s_rx;
|
||
static vprintf_like_t s_prev_vprintf;
|
||
static bool s_up;
|
||
static TickType_t s_wait; /* 本次写允许等多久:由调用者在持锁时设 */
|
||
|
||
static size_t usj_sink(void *ctx, const char *data, size_t len)
|
||
{
|
||
(void)ctx;
|
||
if (!usb_serial_jtag_is_connected()) {
|
||
return 0; /* 没人听:一字节都不写(写进缓冲的会在下次连上时吐出一段陈年日志) */
|
||
}
|
||
const int n = usb_serial_jtag_write_bytes(data, len, s_wait);
|
||
return n > 0 ? (size_t)n : 0u;
|
||
}
|
||
|
||
static int log_vprintf(const char *fmt, va_list ap)
|
||
{
|
||
if (s_up && xSemaphoreTake(s_mtx, 0) == pdTRUE) {
|
||
static char line[ULINK_ESP_LOG_LINE]; /* 锁内用,静态即可,不占调用者的栈 */
|
||
va_list ap2;
|
||
va_copy(ap2, ap);
|
||
const int n = vsnprintf(line, sizeof(line), fmt, ap2);
|
||
va_end(ap2);
|
||
if (n > 0) {
|
||
s_wait = 0;
|
||
ulink_tx_log(&s_tx, line, (size_t)n < sizeof(line) ? (size_t)n : sizeof(line) - 1);
|
||
}
|
||
xSemaphoreGive(s_mtx);
|
||
}
|
||
return s_prev_vprintf(fmt, ap); /* UART0 照旧 */
|
||
}
|
||
|
||
static void send_frame(const char *json, size_t len)
|
||
{
|
||
if (!s_up || xSemaphoreTake(s_mtx, pdMS_TO_TICKS(ULINK_ESP_FRAME_WAIT_MS)) != pdTRUE) {
|
||
return; /* 上位机那一侧表现为超时,按同一个 seq 重发(判重兜住) */
|
||
}
|
||
s_wait = pdMS_TO_TICKS(ULINK_ESP_FRAME_WAIT_MS);
|
||
(void)ulink_tx_frame(&s_tx, json, len);
|
||
xSemaphoreGive(s_mtx);
|
||
}
|
||
|
||
/* cmd_queue 的回调:在**执行器任务**里被调(cmd_queue.h)。 */
|
||
static void reply_usb(void *ctx, const char *line, size_t len)
|
||
{
|
||
(void)ctx;
|
||
send_frame(line, len);
|
||
}
|
||
|
||
static void link_error(const char *payload, const char *err)
|
||
{
|
||
char out[96];
|
||
const size_t n = ulink_link_error(payload, err, out, sizeof(out));
|
||
if (n > 0) {
|
||
send_frame(out, n);
|
||
}
|
||
}
|
||
|
||
/* ulink_rx_feed 的回调:在**链路任务**里被调。 */
|
||
static void on_line(void *ctx, ulink_rx_kind_t kind, const char *payload)
|
||
{
|
||
(void)ctx;
|
||
if (kind != ULINK_RX_CMD) {
|
||
link_error(payload, ulink_rx_err_str(kind));
|
||
return;
|
||
}
|
||
if (!cmd_queue_submit(CMD_LINK_USB, payload, reply_usb, NULL)) {
|
||
link_error(payload, "queue_full");
|
||
}
|
||
}
|
||
|
||
hal_err_t ulink_esp_init(void)
|
||
{
|
||
s_mtx = xSemaphoreCreateMutex();
|
||
if (s_mtx == NULL) {
|
||
return HAL_ERR_STATE;
|
||
}
|
||
usb_serial_jtag_driver_config_t cfg = {
|
||
.tx_buffer_size = ULINK_ESP_TX_BUF,
|
||
.rx_buffer_size = ULINK_ESP_RX_BUF,
|
||
};
|
||
const esp_err_t e = usb_serial_jtag_driver_install(&cfg);
|
||
if (e != ESP_OK) {
|
||
ESP_LOGE(TAG, "USB-Serial-JTAG 驱动装不上:%s —— USB 命令链路不可用", esp_err_to_name(e));
|
||
return HAL_ERR_BUS;
|
||
}
|
||
ulink_tx_init(&s_tx, usj_sink, NULL);
|
||
ulink_rx_init(&s_rx);
|
||
s_up = true;
|
||
s_prev_vprintf = esp_log_set_vprintf(log_vprintf);
|
||
ESP_LOGI(TAG, "USB 命令链路就绪:请求 %s<json>[*CRC],回应 %s<json>*CRC", ULINK_CMD_PREFIX,
|
||
ULINK_RSP_PREFIX);
|
||
return HAL_OK;
|
||
}
|
||
|
||
void ulink_esp_poll(uint32_t wait_ms)
|
||
{
|
||
if (!s_up) {
|
||
vTaskDelay(pdMS_TO_TICKS(wait_ms));
|
||
return;
|
||
}
|
||
uint8_t buf[64];
|
||
const int n = usb_serial_jtag_read_bytes(buf, sizeof(buf), pdMS_TO_TICKS(wait_ms));
|
||
if (n > 0) {
|
||
ulink_rx_feed(&s_rx, buf, (size_t)n, on_line, NULL);
|
||
}
|
||
}
|
||
|
||
void ulink_esp_stats(ulink_esp_stats_t *st)
|
||
{
|
||
st->rx_cmds = s_rx.cmds;
|
||
st->rx_ignored = s_rx.ignored;
|
||
st->rx_errors = s_rx.too_long + s_rx.bad_crc + s_rx.bad_bytes;
|
||
st->tx_frames = s_tx.frames;
|
||
st->tx_short = s_tx.short_writes;
|
||
}
|