固件: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]>
这个提交包含在:
@@ -0,0 +1,155 @@
|
||||
/* 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;
|
||||
}
|
||||
在新工单中引用
屏蔽一个用户