修复协议解析器, 打通与 MPU 的双向通信

- protocol: 修复 edc_cycle_parse_byte 状态机死代码 (switch 恒 return, 收帧逻辑不可达), 并加 LEN 未就绪保护
- protocol: UART 默认引脚改为 GPIO11/12 (UART0)
- sdkconfig.defaults: 控制台切至 USB Serial/JTAG, 避免与协议 UART0 冲突
- web_ctrl: 新增协议状态 API 与 Cycle 快照; on_cycle 回 DONE 并按 cycle_id 去重
- main: 接入协议初始化与接收任务, 更新接线注释
- 实测: MPU 每秒下发 Cycle, I2C/UART 双通道 12/12 收, DONE 12/12 回
This commit is contained in:
kirl committed 2026-07-29 23:24:31 +08:00
1 parent 40ee976236
commit af4f0edc08
9 files changed
+218 -20

No files matched your search

+174
View File
@@ -7,6 +7,7 @@
#include "emm_v5.h"
#include "magnet.h"
#include "servo.h"
#include "protocol.h"
#include <string.h>
#include <stdlib.h>
@@ -175,6 +176,20 @@ button:active{opacity:.7}
</div>
<div class="card" id="proto-card">
<h2><i class="tag amber"></i>通讯协议</h2>
<div class="row" style="font-size:.82rem">
<span>I2C: <b id="pi2c">-</b></span>
<span>UART: <b id="puart">-</b></span>
<span>通道: <b id="pch">-</b></span>
</div>
<div id="pcycle" style="font-size:.78rem;color:var(--sub);margin-top:6px">等待 Cycle...</div>
<div class="btn-row" style="margin-top:10px">
<button class="btn-go" onclick="var c=document.getElementById('pcid');api('proto/done?id='+(c?c.value:0))">Done</button>
<button class="btn-stop" onclick="api('proto/request')">Request</button>
</div>
</div>
<div class="card log-card">
<h2><i class="tag green"></i>运行日志</h2>
<div class="log" id="log">等待指令...</div>
@@ -197,6 +212,30 @@ fetch('/api/status').then(function(r){return r.json()}).then(function(d){
document.getElementById('wifi').innerHTML='<i class="dot"></i>WiFi: '+d.ssid;
document.getElementById('ip').textContent='IP: '+d.ip
})
/* 每秒刷新协议状态 */
function pollProto(){
fetch('/api/proto/status').then(function(r){return r.json()}).then(function(d){
document.getElementById('pi2c').textContent = d.i2c_ok + '/' + (d.i2c_ok+d.i2c_err);
document.getElementById('puart').textContent = d.uart_ok + '/' + (d.uart_ok+d.uart_err);
document.getElementById('pch').textContent = d.last_ch;
if(d.cycle){
document.getElementById('pcid').value = d.cycle.id;
var html = 'Cycle #'+d.cycle.id + ' ('+d.cycle.frags+' frags)<br>';
for(var i=0;i<d.cycle.data.length;i++){
var f=d.cycle.data[i];
html += 'F'+i+': ('+f.sx+','+f.sy+' '+f.sa+'°) → ('+f.tx+','+f.ty+' '+f.ta+'°)<br>';
}
document.getElementById('pcycle').innerHTML = html;
}
}).catch(function(){});
}
/* 隐藏的 cycle_id 存储 */
(function(){
var inp=document.createElement('input');
inp.type='hidden';inp.id='pcid';inp.value='0';
document.getElementById('proto-card').appendChild(inp);
})();
pollProto();setInterval(pollProto,1000);
</script>
</body>
</html>)raw";
@@ -448,6 +487,116 @@ static esp_err_t favicon_handler(httpd_req_t *req)
return ESP_OK;
}
/* ============================================================
* 协议状态 API
* ============================================================ */
/* 最近收到的 Cycle 快照 (由 edc_proto_on_cycle 更新, web 读取) */
static edc_cycle_t s_web_cycle;
static volatile bool s_web_cycle_valid;
static portMUX_TYPE s_web_lock = portMUX_INITIALIZER_UNLOCKED;
void edc_proto_on_cycle(const edc_cycle_t *cycle, edc_channel_t ch)
{
static uint16_t s_last_done_id = 0xFFFF;
portENTER_CRITICAL(&s_web_lock);
memcpy(&s_web_cycle, cycle, sizeof(edc_cycle_t));
s_web_cycle_valid = true;
portEXIT_CRITICAL(&s_web_lock);
ESP_LOGI(TAG, "Cycle #%u via %s, %u frags",
cycle->cycle_id, ch == EDC_CH_I2C ? "I2C" : "UART",
cycle->frag_count);
/* I2C / UART 冗余下发, 同一 cycle_id 只回一次 DONE */
if (cycle->cycle_id == s_last_done_id) {
return;
}
s_last_done_id = cycle->cycle_id;
/* 回复 MPU: Cycle 已收到 (后续实际执行电机后应移到执行完毕处) */
edc_proto_send_done(cycle->cycle_id);
}
static esp_err_t api_proto_status_handler(httpd_req_t *req)
{
edc_proto_stat_t stat;
edc_proto_get_stats(&stat);
char json[1024];
int off = snprintf(json, sizeof(json),
"{\"i2c_ok\":%lu,\"i2c_err\":%lu,\"uart_ok\":%lu,\"uart_err\":%lu,"
"\"last_ch\":\"%s\",\"done\":%lu,\"req\":%lu",
stat.i2c.cycles_ok, stat.i2c.cycles_err,
stat.uart.cycles_ok, stat.uart.cycles_err,
(stat.last_channel == EDC_CH_I2C) ? "I2C" :
(stat.last_channel == EDC_CH_UART) ? "UART" : "none",
stat.done_sent, stat.request_sent);
portENTER_CRITICAL(&s_web_lock);
bool valid = s_web_cycle_valid;
edc_cycle_t cycle;
if (valid) memcpy(&cycle, &s_web_cycle, sizeof(edc_cycle_t));
portEXIT_CRITICAL(&s_web_lock);
if (valid) {
off += snprintf(json + off, sizeof(json) - off,
",\"cycle\":{\"id\":%u,\"frags\":%u,\"data\":[",
cycle.cycle_id, cycle.frag_count);
for (int i = 0; i < cycle.frag_count; i++) {
edc_fragment_t *f = &cycle.fragments[i];
off += snprintf(json + off, sizeof(json) - off,
"%s{\"sx\":%u,\"sy\":%u,\"sa\":%.1f,\"tx\":%u,\"ty\":%u,\"ta\":%.1f}",
(i > 0) ? "," : "",
f->source.x, f->source.y, f->source.angle_x10 / 10.0f,
f->target.x, f->target.y, f->target.angle_x10 / 10.0f);
}
off += snprintf(json + off, sizeof(json) - off, "]}");
}
off += snprintf(json + off, sizeof(json) - off, "}");
httpd_resp_set_type(req, "application/json");
httpd_resp_send(req, json, HTTPD_RESP_USE_STRLEN);
return ESP_OK;
}
static esp_err_t api_proto_request_handler(httpd_req_t *req)
{
edc_proto_send_request();
httpd_resp_set_type(req, "text/plain; charset=utf-8");
httpd_resp_send(req, "已发送 CYCLE_REQUEST", HTTPD_RESP_USE_STRLEN);
return ESP_OK;
}
static esp_err_t api_proto_done_handler(httpd_req_t *req)
{
edc_cycle_t cycle;
edc_proto_stat_t stat;
edc_proto_get_stats(&stat);
/* 对最近收到的 cycle 发送 DONE */
if (stat.last_channel != EDC_CH_NONE) {
char buf[128];
size_t len = httpd_req_get_url_query_len(req) + 1;
if (len > sizeof(buf)) len = sizeof(buf);
uint16_t cid = 0;
if (httpd_req_get_url_query_str(req, buf, len) == ESP_OK) {
char val[16];
if (httpd_query_key_value(buf, "id", val, sizeof(val)) == ESP_OK) {
cid = (uint16_t)atoi(val);
}
}
edc_proto_send_done(cid);
char resp[64];
snprintf(resp, sizeof(resp), "已发送 CYCLE_DONE (id=%u)", cid);
httpd_resp_set_type(req, "text/plain; charset=utf-8");
httpd_resp_send(req, resp, HTTPD_RESP_USE_STRLEN);
return ESP_OK;
}
httpd_resp_send_err(req, HTTPD_400_BAD_REQUEST, "尚无收到 Cycle");
return ESP_FAIL;
}
static void http_server_init(void)
{
httpd_handle_t server = NULL;
@@ -532,6 +681,31 @@ static void http_server_init(void)
};
httpd_register_uri_handler(server, &uri_favicon);
/* ---- 协议状态 API ---- */
httpd_uri_t uri_proto_status = {
.uri = "/api/proto/status",
.method = HTTP_GET,
.handler = api_proto_status_handler,
.user_ctx = NULL
};
httpd_register_uri_handler(server, &uri_proto_status);
httpd_uri_t uri_proto_request = {
.uri = "/api/proto/request",
.method = HTTP_GET,
.handler = api_proto_request_handler,
.user_ctx = NULL
};
httpd_register_uri_handler(server, &uri_proto_request);
httpd_uri_t uri_proto_done = {
.uri = "/api/proto/done",
.method = HTTP_GET,
.handler = api_proto_done_handler,
.user_ctx = NULL
};
httpd_register_uri_handler(server, &uri_proto_done);
ESP_LOGI(TAG, "HTTP 服务器已启动 (端口 %d)", cfg.server_port);
}