feat(esp32): 添加多电机OTA中继功能并优化调试日志系统
- 新增多电机状态、版本和OTA中继监控模块(monitor/motor_state.cpp, monitor/motor_version.cpp, monitor/motor_ota_relay.cpp) - 在CAN反馈任务中添加0xC5/0xC6/0xD2类型消息处理,支持多电机状态推送 - 重构调试日志系统,将一次性JSON分配改为512B栈缓冲流式发送,降低内存峰值 - 调试UART波特率从115200提升至30000 - 修改版本管理方式,从自动生version.h改为手动维护 - 更新VSCode设置以支持ESP-IDF开发环境配置 - 在OTA协调器中添加电机OTA互斥机制,防止并发冲突 - 完善中继状态机异常处理,添加远程取消通知机制
This commit is contained in:
@@ -397,7 +397,14 @@ export function setupDropZone(zoneId, chip) {
|
||||
zone.addEventListener('drop', function (e) {
|
||||
e.preventDefault(); e.currentTarget.classList.remove('dragover');
|
||||
var file = e.dataTransfer.files[0];
|
||||
if (file) processFile(file, chip);
|
||||
if (!file) return;
|
||||
/* 拖拽拿不到 FSAPI 句柄:必须清掉旧句柄,否则热重载会把上一个文件读回来
|
||||
覆盖本次拖入的文件(表现为"刷错固件"),并同步删除 IDB 持久化句柄 */
|
||||
if (chip === 'esp32') window.esp32FileHandle = null;
|
||||
else window.gd32FileHandle = null;
|
||||
window._pendingFwHandles[chip] = null;
|
||||
persistFileHandle(chip, null);
|
||||
processFile(file, chip);
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -171,12 +171,15 @@ export function cleanupAfterOtaFailure(reason) {
|
||||
setActivePhase(null);
|
||||
window.currentOtaTarget = null;
|
||||
|
||||
// 4. 恢复上传按钮(两枚都解禁并恢复原文案,避免 chip 判错或一侧残留"上传中")
|
||||
['uploadEsp32Btn', 'uploadGd32Btn'].forEach(function (btnId) {
|
||||
// 4. 恢复上传按钮(两枚都解禁并恢复各自原文案,避免 chip 判错或一侧残留"上传中")
|
||||
// ★ 按钮文案是固定的,绝不能用共享的 uploadBtnOrigText 回写两枚,否则最后
|
||||
// 刷 GD32 后 ESP32 按钮会被错写成 "GD32 OTA"(反之亦然)。
|
||||
var uploadBtnLabels = { uploadEsp32Btn: '上传并升级 ESP32', uploadGd32Btn: 'GD32 OTA' };
|
||||
Object.keys(uploadBtnLabels).forEach(function (btnId) {
|
||||
var btn = document.getElementById(btnId);
|
||||
if (!btn) return;
|
||||
btn.disabled = false;
|
||||
if (window.uploadBtnOrigText) btn.textContent = window.uploadBtnOrigText;
|
||||
btn.textContent = uploadBtnLabels[btnId];
|
||||
});
|
||||
window.activeUploadChip = null;
|
||||
window.otaSessionTarget = null;
|
||||
|
||||
+37
-24
@@ -106,6 +106,18 @@ function setDisplayedRelayState(rs) {
|
||||
updateStagesFromRelayState(rs);
|
||||
}
|
||||
|
||||
/* 写前比较小工具:每秒轮询里大部分仪表盘字段不变,
|
||||
* 无条件写 textContent/className/title 会白白触发样式失效。 */
|
||||
function setNodeText(el, txt) {
|
||||
if (el && el.textContent !== txt) el.textContent = txt;
|
||||
}
|
||||
function setNodeClass(el, cls) {
|
||||
if (el && el.className !== cls) el.className = cls;
|
||||
}
|
||||
function setNodeTitle(el, title) {
|
||||
if (el && el.title !== title) el.title = title;
|
||||
}
|
||||
|
||||
/**
|
||||
* 常驻仪表盘:堆、连接、健康、版本、温度、IP/RSSI。
|
||||
* @param {object} data /system/status JSON
|
||||
@@ -122,28 +134,29 @@ function applyDashboardUi(data, ota, wifi) {
|
||||
if (data.version) {
|
||||
var pillVerEl = document.getElementById('pillVer');
|
||||
if (pillVerEl) {
|
||||
pillVerEl.textContent = 'v' + data.version;
|
||||
pillVerEl.dataset.tooltip = data.build_time ? ('编译: ' + data.build_time) : '';
|
||||
setNodeText(pillVerEl, 'v' + data.version);
|
||||
var verTip = data.build_time ? ('编译: ' + data.build_time) : '';
|
||||
if (pillVerEl.dataset.tooltip !== verTip) pillVerEl.dataset.tooltip = verTip;
|
||||
}
|
||||
var mGd32App = document.getElementById('mGd32App');
|
||||
var mGd32Boot = document.getElementById('mGd32Boot');
|
||||
if (mGd32App) {
|
||||
if (ota.gd32_app && ota.gd32_app !== 'v0.0.0.0') {
|
||||
mGd32App.textContent = ota.gd32_app;
|
||||
mGd32App.className = 'v ok';
|
||||
setNodeText(mGd32App, ota.gd32_app);
|
||||
setNodeClass(mGd32App, 'v ok');
|
||||
} else {
|
||||
mGd32App.textContent = ota.gd32_app_alive ? '未知' : '离线';
|
||||
mGd32App.className = 'v';
|
||||
setNodeText(mGd32App, ota.gd32_app_alive ? '未知' : '离线');
|
||||
setNodeClass(mGd32App, 'v');
|
||||
}
|
||||
}
|
||||
if (mGd32Boot) {
|
||||
if (ota.gd32_boot && ota.gd32_boot !== 'v0.0.0.0') {
|
||||
mGd32Boot.textContent = ota.gd32_boot;
|
||||
mGd32Boot.className = 'v ok';
|
||||
setNodeText(mGd32Boot, ota.gd32_boot);
|
||||
setNodeClass(mGd32Boot, 'v ok');
|
||||
} else {
|
||||
/* BOOT 版本与 APP alive 无直接关系,无版本时统一「未知」 */
|
||||
mGd32Boot.textContent = '未知';
|
||||
mGd32Boot.className = 'v';
|
||||
setNodeText(mGd32Boot, '未知');
|
||||
setNodeClass(mGd32Boot, 'v');
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -153,19 +166,19 @@ function applyDashboardUi(data, ota, wifi) {
|
||||
/* 与固件 app_config:TEMP_WARNING=80 / TEMP_CRITICAL=100 对齐 */
|
||||
if (Number.isFinite(data.temperature)) {
|
||||
var tC = data.temperature;
|
||||
mTempEl.textContent = tC.toFixed(1) + '\u00B0C';
|
||||
setNodeText(mTempEl, tC.toFixed(1) + '\u00B0C');
|
||||
if (tC >= 100) {
|
||||
mTempEl.className = 'v err';
|
||||
setNodeClass(mTempEl, 'v err');
|
||||
} else if (tC >= 80) {
|
||||
mTempEl.className = 'v warn';
|
||||
setNodeClass(mTempEl, 'v warn');
|
||||
} else {
|
||||
mTempEl.className = 'v ok';
|
||||
setNodeClass(mTempEl, 'v ok');
|
||||
}
|
||||
mTempEl.title = 'ESP32 片内温度传感器(警告≥80°C,危险≥100°C)';
|
||||
setNodeTitle(mTempEl, 'ESP32 片内温度传感器(警告≥80°C,危险≥100°C)');
|
||||
} else {
|
||||
mTempEl.textContent = '--';
|
||||
mTempEl.className = 'v';
|
||||
mTempEl.title = '温度无效或未采样';
|
||||
setNodeText(mTempEl, '--');
|
||||
setNodeClass(mTempEl, 'v');
|
||||
setNodeTitle(mTempEl, '温度无效或未采样');
|
||||
}
|
||||
}
|
||||
/* GD32 ADC:enabled 来自 HELLO_ACK 功能位(GD32 编译开关),ESP32 无本地开关 */
|
||||
@@ -177,9 +190,9 @@ function applyDashboardUi(data, ota, wifi) {
|
||||
|
||||
function setMetricUi(el, text, cls, title) {
|
||||
if (!el) return;
|
||||
el.textContent = text;
|
||||
el.className = cls || 'v';
|
||||
if (title) el.title = title;
|
||||
setNodeText(el, text);
|
||||
setNodeClass(el, cls || 'v');
|
||||
if (title) setNodeTitle(el, title);
|
||||
}
|
||||
|
||||
if (mGd32TempEl || mVccEl || mVddaEl) {
|
||||
@@ -233,12 +246,12 @@ function applyDashboardUi(data, ota, wifi) {
|
||||
}
|
||||
}
|
||||
var uptimeEl = document.getElementById('pillUptime');
|
||||
if (uptimeEl) uptimeEl.textContent = '\u23F1 ' + formatUptime(data.uptime_sec);
|
||||
if (uptimeEl) setNodeText(uptimeEl, '\u23F1 ' + formatUptime(data.uptime_sec));
|
||||
var pillIPEl = document.getElementById('pillIP');
|
||||
if (pillIPEl) pillIPEl.textContent = wifi.ip || '--';
|
||||
if (pillIPEl) setNodeText(pillIPEl, wifi.ip || '--');
|
||||
var rssi = (wifi.rssi !== undefined && wifi.rssi !== -127) ? wifi.rssi + ' dBm' : '--';
|
||||
var pillRSSIEl = document.getElementById('pillRSSI');
|
||||
if (pillRSSIEl) pillRSSIEl.textContent = rssi;
|
||||
if (pillRSSIEl) setNodeText(pillRSSIEl, rssi);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -164,11 +164,23 @@ export async function startUpload(chip) {
|
||||
if (chip === 'esp32') window.esp32File = freshFile;
|
||||
else window.gd32File = freshFile;
|
||||
file = freshFile;
|
||||
/* .bin 直读分支此前无任何日志,热重载是否生效不可观察,补一条 */
|
||||
addLog('I', '文件', '热重载: 已重新读取 ' + freshFile.name +
|
||||
' (' + formatBytes(freshFile.size) + ')');
|
||||
}
|
||||
} catch (e) {
|
||||
addLog('W', '文件', '重新读取文件失败: ' + e.message + ',将使用缓存版本');
|
||||
}
|
||||
} else {
|
||||
/* 无 FSAPI 句柄(拖拽/传统方式选择):无法读盘,只能用缓存。
|
||||
GD32 缓存是内存中转换出的 Blob,arrayBuffer 校验永远通过,
|
||||
若不显式告知,本地重新编译后点上传会静默刷入旧固件 */
|
||||
var cachedAt = file.lastModified
|
||||
? new Date(file.lastModified).toLocaleTimeString('zh-CN', { hour12: false })
|
||||
: '未知时间';
|
||||
addLog('W', '文件', '热重载不可用(无 FSAPI 句柄):本次上传缓存文件 ' + file.name +
|
||||
'(磁盘修改于 ' + cachedAt + ')。如需自动读取最新编译产物,请点击上传区重新选择');
|
||||
showToast('热重载不可用,上传的是缓存文件', 'warning');
|
||||
try {
|
||||
await file.arrayBuffer();
|
||||
} catch (e) {
|
||||
|
||||
Reference in New Issue
Block a user