diff --git a/README.md b/README.md
index 8badd19..df7ef57 100644
--- a/README.md
+++ b/README.md
@@ -12,7 +12,7 @@ ESP32-C3FH4 OTA 中继固件,通过 WiFi 接收上位机 OTA 固件包,通
| 模块 | 描述 |
|------|------|
-| `health_monitor` | 温度 + 电压 + WiFi RSSI 监控 |
+| `health_monitor` | 温度 + WiFi RSSI 监控 |
| `ota_relay` | OTA 数据包接收与转发 |
| `wifi_ota` | WiFi OTA HTTP 服务器 + CAN 帧转发 |
| `spi_master` | SPI 主站,与 GD32 通信 |
diff --git a/_assistant_backups/2026-05-05/app.js.bak_2026-05-05_0451 b/_assistant_backups/2026-05-05/app.js.bak_2026-05-05_0451
new file mode 100644
index 0000000..2ca7e4d
--- /dev/null
+++ b/_assistant_backups/2026-05-05/app.js.bak_2026-05-05_0451
@@ -0,0 +1,557 @@
+// ==================== 全局状态 ====================
+let currentFile = null;
+let uploadProgress = 0;
+let isUploading = false;
+let logFilter = 'all';
+let logEntries = [];
+let logSocket = null;
+let latestSystemStatus = null;
+
+// ==================== 工具函数 ====================
+function formatBytes(bytes) {
+ if (bytes < 1024) return bytes + ' B';
+ if (bytes < 1024 * 1024) return (bytes / 1024).toFixed(1) + ' KB';
+ return (bytes / (1024 * 1024)).toFixed(2) + ' MB';
+}
+
+function formatTime(date) {
+ return date.toTimeString().split(' ')[0];
+}
+
+function showToast(message, type = 'success') {
+ const toast = document.getElementById('toast');
+ const icon = document.getElementById('toastIcon');
+ const msg = document.getElementById('toastMessage');
+
+ toast.className = `toast ${type} show`;
+ icon.textContent = type === 'success' ? '✓' : type === 'error' ? '✕' : '⚠';
+ msg.textContent = message;
+
+ setTimeout(() => toast.classList.remove('show'), 3000);
+}
+
+function getEspIp() {
+ return document.getElementById('espIp').value.trim() || '192.168.31.127';
+}
+
+function getBaseUrl() {
+ return `http://${getEspIp()}`;
+}
+
+// ==================== 日志系统 ====================
+function addLog(level, tag, message) {
+ const logContent = document.getElementById('logContent');
+ const time = formatTime(new Date());
+
+ const entry = { time, level, tag, message };
+ logEntries.push(entry);
+
+ if (logFilter !== 'all' && level !== logFilter) {
+ return;
+ }
+
+ const div = document.createElement('div');
+ div.className = 'log-entry';
+ div.innerHTML = `
+ [${time}]
+ ${level}
+ [${tag}]
+ ${message}
+ `;
+
+ logContent.appendChild(div);
+ logContent.scrollTop = logContent.scrollHeight;
+ document.getElementById('logCount').textContent = logEntries.length;
+}
+
+function clearLogs() {
+ document.getElementById('logContent').innerHTML = '';
+ logEntries = [];
+ document.getElementById('logCount').textContent = '0';
+}
+
+function toggleFilter(level, btn) {
+ document.querySelectorAll('.filter-btn').forEach(b => b.classList.remove('active'));
+ btn.classList.add('active');
+ logFilter = level;
+
+ const logContent = document.getElementById('logContent');
+ logContent.innerHTML = '';
+ logEntries.forEach(entry => {
+ if (level === 'all' || entry.level === level) {
+ const div = document.createElement('div');
+ div.className = 'log-entry';
+ div.innerHTML = `
+ [${entry.time}]
+ ${entry.level}
+ [${entry.tag}]
+ ${entry.message}
+ `;
+ logContent.appendChild(div);
+ }
+ });
+ logContent.scrollTop = logContent.scrollHeight;
+}
+
+// ==================== 连接管理 ====================
+function updateConnectionUi(connected, text = null) {
+ const badge = document.getElementById('connectionBadge');
+ const label = document.getElementById('connectionText');
+ badge.className = connected ? 'badge connected' : 'badge disconnected';
+ label.textContent = text || (connected ? '已连接' : '未连接');
+}
+
+function updateHealthUi(healthCode, freeHeap) {
+ const healthNames = ['正常', '警告', '危险', '关机'];
+ const health = healthCode !== undefined ? (healthNames[healthCode] || '未知') : '未知';
+ document.getElementById('healthText').textContent = `系统${health}`;
+ document.getElementById('healthBadge').className = healthCode === 0 ? 'badge connected' : 'badge warning';
+ if (freeHeap !== undefined) {
+ document.getElementById('statHeap').textContent = formatBytes(freeHeap);
+ }
+}
+
+function updateSystemStatusUi(data) {
+ if (!data) return;
+
+ latestSystemStatus = data;
+
+ const wifi = data.wifi || {};
+ const ota = data.ota || {};
+ const healthCode = data.health;
+ const relayStates = {
+ 0: '待机',
+ 1: '等待握手',
+ 2: '握手已发',
+ 3: '挑战已发',
+ 4: '收到握手响应',
+ 5: '会话密钥已发',
+ 6: '认证完成',
+ 7: '大小已发',
+ 8: '大小确认',
+ 9: '接收中',
+ 10: '转发中',
+ 11: '数据包已发',
+ 12: 'ACK正常',
+ 13: '等待重发',
+ 14: '接收完成',
+ 15: '传输完成',
+ 16: '结束已发',
+ 17: 'OTA完成',
+ 255: '错误'
+ };
+
+ updateConnectionUi(!!wifi.connected, wifi.connected ? `${wifi.mode || 'STA'} 已连接` : `${wifi.mode || 'STA'} 未连接`);
+ updateHealthUi(healthCode, data.free_heap);
+
+ // 显示固件版本
+ if (data.version) {
+ document.getElementById('fwVersion').textContent = `v${data.version}`;
+ }
+
+ document.getElementById('statTemperature').textContent = Number.isFinite(data.temperature) ? `${data.temperature.toFixed(1)}°C` : '--';
+ document.getElementById('statVoltage').textContent = Number.isFinite(data.voltage) ? `${data.voltage.toFixed(2)}V` : '--';
+ document.getElementById('statWifi').textContent = wifi.connected ? (wifi.ip || '已连接') : '未连接';
+ document.getElementById('statSignal').textContent = (wifi.rssi !== undefined && wifi.rssi !== -127) ? `${wifi.rssi} dBm` : '--';
+ document.getElementById('statHeap').textContent = data.free_heap !== undefined ? formatBytes(data.free_heap) : '--';
+ document.getElementById('statMinHeap').textContent = data.min_free_heap !== undefined ? formatBytes(data.min_free_heap) : '--';
+
+ const total = ota.total_size || 0;
+ const stored = ota.stored_size || 0;
+ const progress = total > 0 ? Math.min(100, Math.round(stored * 100 / total)) : 0;
+ document.getElementById('statProgress').textContent = `${progress}%`;
+ document.getElementById('statRelayState').textContent = relayStates[ota.relay_state] ?? '待机';
+}
+
+async function testConnection() {
+ if (!latestSystemStatus) {
+ addLog('W', '连接', '暂无同步数据,请等待 1 秒自动同步');
+ showToast('暂无同步数据,请稍等 1 秒', 'warning');
+ return;
+ }
+
+ const wifi = latestSystemStatus.wifi || {};
+ const connected = !!wifi.connected;
+ updateSystemStatusUi(latestSystemStatus);
+ addLog('I', '连接', connected
+ ? `当前已连接:${wifi.mode || 'STA'} ${wifi.ip || ''}`.trim()
+ : `当前未连接:${wifi.mode || 'STA'}`);
+ showToast(connected ? 'ESP32 当前在线' : 'ESP32 当前未连接', connected ? 'success' : 'warning');
+}
+
+async function healthCheck() {
+ if (!latestSystemStatus) {
+ addLog('W', '健康', '暂无同步数据,请等待 1 秒自动同步');
+ showToast('暂无同步数据,请稍等 1 秒', 'warning');
+ return;
+ }
+
+ updateSystemStatusUi(latestSystemStatus);
+ const data = latestSystemStatus;
+ const healthNames = ['正常', '警告', '危险', '关机'];
+ const health = data.health !== undefined ? (healthNames[data.health] || '未知') : '未知';
+ addLog('I', '健康', `当前状态: ${health}, 空闲堆: ${data.free_heap} bytes, 温度: ${data.temperature}°C, 电压: ${data.voltage}V`);
+ showToast(`当前健康状态: ${health}`, 'success');
+}
+
+// ==================== CAN 帧发送 ====================
+async function sendCanFrame() {
+ const ch = parseInt(document.getElementById('canChannel').value);
+ const idStr = document.getElementById('canId').value.trim();
+ const dlc = parseInt(document.getElementById('canDlc').value) || 8;
+ const dataStr = document.getElementById('canData').value.trim();
+
+ let extId = 0;
+ if (idStr.startsWith('0x') || idStr.startsWith('0X')) {
+ extId = parseInt(idStr, 16);
+ } else {
+ extId = parseInt(idStr, 10);
+ if (isNaN(extId)) extId = parseInt(idStr, 16);
+ }
+
+ if (isNaN(extId)) {
+ showToast('ID 格式错误', 'error');
+ return;
+ }
+
+ let dataHex = dataStr.replace(/\s/g, '');
+ if (dataHex.length % 2 !== 0) {
+ showToast('数据长度必须为偶数', 'error');
+ return;
+ }
+
+ addLog('I', 'CAN', `发送 CH${ch} ID=0x${extId.toString(16).toUpperCase()} DLC=${dlc}`);
+
+ try {
+ const response = await fetch(`${getBaseUrl()}/can/send`, {
+ method: 'POST',
+ headers: { 'Content-Type': 'application/json' },
+ body: JSON.stringify({
+ ch: ch,
+ id: extId,
+ dlc: dlc,
+ data: dataHex
+ }),
+ signal: AbortSignal.timeout(5000)
+ });
+
+ const result = await response.json();
+
+ if (result.status === 'ok') {
+ addLog('I', 'CAN', `响应: ${result.recv || '(无数据)'} (${result.recv_len || 0}B)`);
+
+ const respDiv = document.getElementById('canResponse');
+ const respContent = document.getElementById('canRespContent');
+ respDiv.style.display = 'block';
+ respContent.textContent = result.recv
+ ? `0x${result.recv} (${result.recv_len} 字节)`
+ : '无响应数据';
+
+ showToast(`CH${ch} 发送成功`, 'success');
+ } else {
+ addLog('E', 'CAN', `发送失败: code=${result.code}`);
+ showToast(`发送失败: code=${result.code}`, 'error');
+ }
+ } catch (e) {
+ addLog('E', 'CAN', `发送失败: ${e.message}`);
+ showToast('发送失败', 'error');
+ }
+}
+
+function onTargetChipChange() {
+ const chip = document.getElementById('targetChip').value;
+ const relayBtn = document.getElementById('relayBtn');
+ const esp32Actions = document.getElementById('esp32Actions');
+
+ if (chip === 'esp32') {
+ relayBtn.style.display = 'none';
+ esp32Actions.style.display = 'block';
+ } else {
+ relayBtn.style.display = '';
+ esp32Actions.style.display = 'none';
+ }
+ addLog('I', '目标', `切换到: ${chip === 'esp32' ? 'ESP32 自身升级' : 'GD32 Flash 中继'}`);
+}
+
+function clearCanFields() {
+ document.getElementById('canId').value = '0x18DA00F1';
+ document.getElementById('canDlc').value = '8';
+ document.getElementById('canData').value = '0102030405060708';
+ document.getElementById('canResponse').style.display = 'none';
+}
+
+// ==================== 文件处理 ====================
+function handleFileSelect(event) {
+ const file = event.target.files[0];
+ if (file) processFile(file);
+}
+
+document.getElementById('uploadZone').addEventListener('dragover', (e) => {
+ e.preventDefault();
+ e.currentTarget.classList.add('dragover');
+});
+
+document.getElementById('uploadZone').addEventListener('dragleave', (e) => {
+ e.currentTarget.classList.remove('dragover');
+});
+
+document.getElementById('uploadZone').addEventListener('drop', (e) => {
+ e.preventDefault();
+ e.currentTarget.classList.remove('dragover');
+ const file = e.dataTransfer.files[0];
+ if (file) processFile(file);
+});
+
+function processFile(file) {
+ currentFile = file;
+ const fileInfo = document.getElementById('fileInfo');
+ const uploadBtn = document.getElementById('uploadBtn');
+
+ document.getElementById('fileName').textContent = file.name;
+ document.getElementById('fileType').textContent = file.name.endsWith('.hex') ? 'Intel HEX' : 'Binary';
+ document.getElementById('fileSize').textContent = formatBytes(file.size);
+
+ const chunks = Math.ceil(file.size / 4096);
+ document.getElementById('fileChunks').textContent = chunks + ' 包';
+
+ fileInfo.classList.add('visible');
+ uploadBtn.disabled = false;
+
+ addLog('I', '文件', `已选择: ${file.name} (${formatBytes(file.size)})`);
+ showToast('文件已选择', 'success');
+}
+
+function clearFile() {
+ currentFile = null;
+ document.getElementById('fileInput').value = '';
+ document.getElementById('fileInfo').classList.remove('visible');
+ document.getElementById('uploadBtn').disabled = true;
+ document.getElementById('progressSection').classList.remove('visible');
+ addLog('I', '文件', '文件已清除');
+}
+
+// ==================== 上传固件 ====================
+async function startUpload() {
+ if (!currentFile || isUploading) return;
+
+ const chip = document.getElementById('targetChip').value;
+ isUploading = true;
+ const uploadBtn = document.getElementById('uploadBtn');
+ uploadBtn.disabled = true;
+ uploadBtn.textContent = '上传中...';
+
+ const progressSection = document.getElementById('progressSection');
+ progressSection.classList.add('visible');
+
+ addLog('I', '上传', `开始上传: ${currentFile.name} → ${chip.toUpperCase()}`);
+ showToast(`开始上传固件到 ${chip.toUpperCase()}`, 'success');
+
+ try {
+ const arrayBuffer = await currentFile.arrayBuffer();
+ const fileSize = currentFile.size;
+
+ let body;
+ let url = `${getBaseUrl()}/ota/upload`;
+
+ if (chip === 'esp32') {
+ const headerJson = JSON.stringify({
+ type: 'esp32',
+ total_size: fileSize
+ });
+ const encoder = new TextEncoder();
+ const headerBytes = encoder.encode(headerJson);
+ const fileBytes = new Uint8Array(arrayBuffer);
+
+ const combined = new Uint8Array(headerBytes.length + fileSize);
+ combined.set(headerBytes, 0);
+ combined.set(fileBytes, headerBytes.length);
+
+ body = combined.buffer;
+ } else {
+ body = arrayBuffer;
+ }
+
+ const xhr = new XMLHttpRequest();
+
+ xhr.upload.addEventListener('progress', (e) => {
+ if (e.lengthComputable) {
+ const percent = Math.round((e.loaded / e.total) * 100);
+ uploadProgress = percent;
+ document.getElementById('progressBar').style.width = percent + '%';
+ document.getElementById('progressText').textContent =
+ `已上传 ${formatBytes(e.loaded)} / ${formatBytes(e.total)}`;
+ document.getElementById('progressPercent').textContent = percent + '%';
+ document.getElementById('statProgress').textContent = percent + '%';
+ }
+ });
+
+ xhr.addEventListener('load', () => {
+ isUploading = false;
+ uploadBtn.disabled = false;
+ uploadBtn.textContent = '⬆️ 上传固件';
+
+ if (xhr.status === 200) {
+ addLog('I', '上传', '固件上传完成!');
+ showToast('固件上传成功', 'success');
+
+ if (chip === 'esp32') {
+ document.getElementById('esp32Actions').style.display = 'block';
+ // 安全间隔:3 秒后自动调用刷写,留时间确认状态
+ let countdown = 3;
+ addLog('I', 'OTA', `${countdown} 秒后自动调用 POST /ota/update 完成升级并重启...`);
+ const timer = setInterval(() => {
+ countdown--;
+ if (countdown > 0) {
+ addLog('I', 'OTA', `${countdown} 秒后开始刷写...`);
+ } else {
+ clearInterval(timer);
+ addLog('I', 'OTA', '开始刷写!');
+ triggerEsp32Update();
+ }
+ }, 1000);
+ }
+ } else {
+ addLog('E', '上传', `上传失败: HTTP ${xhr.status}`);
+ showToast('上传失败', 'error');
+ }
+ });
+
+ xhr.addEventListener('error', () => {
+ isUploading = false;
+ uploadBtn.disabled = false;
+ uploadBtn.textContent = '⬆️ 上传固件';
+ addLog('E', '上传', '上传过程发生错误');
+ showToast('上传出错', 'error');
+ });
+
+ xhr.open('POST', url);
+ xhr.send(body);
+
+ } catch (e) {
+ addLog('E', '上传', `上传失败: ${e.message}`);
+ showToast('上传失败', 'error');
+ isUploading = false;
+ uploadBtn.disabled = false;
+ uploadBtn.textContent = '⬆️ 上传固件';
+ }
+}
+
+// ==================== ESP32 OTA 完成升级 ====================
+async function triggerEsp32Update() {
+ addLog('I', 'OTA', '=== ESP32 OTA 完成,开始重启 ===');
+ showToast('ESP32 正在重启...', 'success');
+
+ try {
+ const response = await fetch(`${getBaseUrl()}/ota/update`, {
+ method: 'POST',
+ signal: AbortSignal.timeout(5000)
+ });
+
+ if (response.ok) {
+ const data = await response.json();
+ addLog('I', 'OTA', `升级完成: ${data.message}`);
+ addLog('I', 'OTA', 'ESP32 即将重启,新固件将生效,页面将继续通过 1 秒状态同步自动恢复显示');
+ showToast('ESP32 重启中...', 'success');
+ }
+ } catch (e) {
+ addLog('E', 'OTA', `升级失败: ${e.message}`);
+ showToast('升级失败', 'error');
+ }
+}
+
+// ==================== 触发 OTA 中继 ====================
+async function triggerRelay() {
+ addLog('I', 'OTA', '触发 GD32 OTA 中继...');
+ showToast('正在触发 OTA 中继', 'success');
+
+ document.getElementById('stage1').className = 'stage active';
+ document.getElementById('statRelayState').textContent = '进行中';
+
+ try {
+ const response = await fetch(`${getBaseUrl()}/ota/relay`, {
+ method: 'POST',
+ signal: AbortSignal.timeout(5000)
+ });
+
+ if (response.ok) {
+ addLog('I', 'OTA', 'OTA 中继已触发,正在等待 GD32 响应...');
+
+ setTimeout(() => {
+ document.getElementById('stage1').className = 'stage completed';
+ document.getElementById('stage2').className = 'stage active';
+ }, 2000);
+
+ setTimeout(() => {
+ document.getElementById('stage2').className = 'stage completed';
+ document.getElementById('stage3').className = 'stage active';
+ }, 4000);
+
+ setTimeout(() => {
+ document.getElementById('stage3').className = 'stage completed';
+ document.getElementById('stage4').className = 'stage active';
+ }, 6000);
+
+ setTimeout(() => {
+ document.getElementById('stage4').className = 'stage completed';
+ document.getElementById('statRelayState').textContent = '完成';
+ addLog('I', 'OTA', '=== OTA 中继完成 ===');
+ showToast('OTA 流程完成!', 'success');
+ }, 8000);
+
+ } else {
+ addLog('E', 'OTA', `触发失败: HTTP ${response.status}`);
+ showToast('触发失败', 'error');
+ }
+ } catch (e) {
+ addLog('E', 'OTA', `触发失败: ${e.message}`);
+ showToast('触发失败', 'error');
+ document.getElementById('statRelayState').textContent = '失败';
+ }
+}
+
+// ==================== 轮询状态 ====================
+async function pollStatus() {
+ try {
+ const response = await fetch(`${getBaseUrl()}/system/status`, {
+ signal: AbortSignal.timeout(3000)
+ });
+ if (response.ok) {
+ const data = await response.json();
+ updateSystemStatusUi(data);
+ }
+ } catch (e) {
+ updateConnectionUi(false, '未连接');
+ }
+}
+
+setInterval(pollStatus, 1000);
+
+// ==================== ESP32 实时日志拉取 ====================
+async function pollLogs() {
+ try {
+ const response = await fetch(`${getBaseUrl()}/logs`, {
+ signal: AbortSignal.timeout(2000)
+ });
+ if (!response.ok) return;
+ const logs = await response.json();
+ if (!Array.isArray(logs) || logs.length === 0) return;
+
+ // 解析 [I][WIFI] xxx 格式
+ const levelMap = { 'E': 'E', 'W': 'W', 'I': 'I', 'D': 'D' };
+ logs.forEach(line => {
+ // 格式: [I][WIFI] message 或 [E][OTA] message
+ const m = line.match(/^\[([EWID])\]\[([^\]]+)\]\s*(.*)$/);
+ if (m) {
+ addLog(levelMap[m[1]] || 'I', m[2], m[3].trimEnd());
+ } else {
+ addLog('I', 'ESP32', line.trimEnd());
+ }
+ });
+ } catch (e) {
+ // 静默失败,不打扰用户
+ }
+}
+
+setInterval(pollLogs, 1000);
+
+setTimeout(() => addLog('I', '系统', '上位机初始化完成,等待固件文件...'), 500);
diff --git a/_assistant_backups/2026-05-05/app.js.bak_2026-05-05_0522 b/_assistant_backups/2026-05-05/app.js.bak_2026-05-05_0522
new file mode 100644
index 0000000..f4e14e7
--- /dev/null
+++ b/_assistant_backups/2026-05-05/app.js.bak_2026-05-05_0522
@@ -0,0 +1,556 @@
+// ==================== 全局状态 ====================
+let currentFile = null;
+let uploadProgress = 0;
+let isUploading = false;
+let logFilter = 'all';
+let logEntries = [];
+let logSocket = null;
+let latestSystemStatus = null;
+
+// ==================== 工具函数 ====================
+function formatBytes(bytes) {
+ if (bytes < 1024) return bytes + ' B';
+ if (bytes < 1024 * 1024) return (bytes / 1024).toFixed(1) + ' KB';
+ return (bytes / (1024 * 1024)).toFixed(2) + ' MB';
+}
+
+function formatTime(date) {
+ return date.toTimeString().split(' ')[0];
+}
+
+function showToast(message, type = 'success') {
+ const toast = document.getElementById('toast');
+ const icon = document.getElementById('toastIcon');
+ const msg = document.getElementById('toastMessage');
+
+ toast.className = `toast ${type} show`;
+ icon.textContent = type === 'success' ? '✓' : type === 'error' ? '✕' : '⚠';
+ msg.textContent = message;
+
+ setTimeout(() => toast.classList.remove('show'), 3000);
+}
+
+function getEspIp() {
+ return document.getElementById('espIp').value.trim() || '192.168.31.127';
+}
+
+function getBaseUrl() {
+ return `http://${getEspIp()}`;
+}
+
+// ==================== 日志系统 ====================
+function addLog(level, tag, message) {
+ const logContent = document.getElementById('logContent');
+ const time = formatTime(new Date());
+
+ const entry = { time, level, tag, message };
+ logEntries.push(entry);
+
+ if (logFilter !== 'all' && level !== logFilter) {
+ return;
+ }
+
+ const div = document.createElement('div');
+ div.className = 'log-entry';
+ div.innerHTML = `
+ [${time}]
+ ${level}
+ [${tag}]
+ ${message}
+ `;
+
+ logContent.appendChild(div);
+ logContent.scrollTop = logContent.scrollHeight;
+ document.getElementById('logCount').textContent = logEntries.length;
+}
+
+function clearLogs() {
+ document.getElementById('logContent').innerHTML = '';
+ logEntries = [];
+ document.getElementById('logCount').textContent = '0';
+}
+
+function toggleFilter(level, btn) {
+ document.querySelectorAll('.filter-btn').forEach(b => b.classList.remove('active'));
+ btn.classList.add('active');
+ logFilter = level;
+
+ const logContent = document.getElementById('logContent');
+ logContent.innerHTML = '';
+ logEntries.forEach(entry => {
+ if (level === 'all' || entry.level === level) {
+ const div = document.createElement('div');
+ div.className = 'log-entry';
+ div.innerHTML = `
+ [${entry.time}]
+ ${entry.level}
+ [${entry.tag}]
+ ${entry.message}
+ `;
+ logContent.appendChild(div);
+ }
+ });
+ logContent.scrollTop = logContent.scrollHeight;
+}
+
+// ==================== 连接管理 ====================
+function updateConnectionUi(connected, text = null) {
+ const badge = document.getElementById('connectionBadge');
+ const label = document.getElementById('connectionText');
+ badge.className = connected ? 'badge connected' : 'badge disconnected';
+ label.textContent = text || (connected ? '已连接' : '未连接');
+}
+
+function updateHealthUi(healthCode, freeHeap) {
+ const healthNames = ['正常', '警告', '危险', '关机'];
+ const health = healthCode !== undefined ? (healthNames[healthCode] || '未知') : '未知';
+ document.getElementById('healthText').textContent = `系统${health}`;
+ document.getElementById('healthBadge').className = healthCode === 0 ? 'badge connected' : 'badge warning';
+ if (freeHeap !== undefined) {
+ document.getElementById('statHeap').textContent = formatBytes(freeHeap);
+ }
+}
+
+function updateSystemStatusUi(data) {
+ if (!data) return;
+
+ latestSystemStatus = data;
+
+ const wifi = data.wifi || {};
+ const ota = data.ota || {};
+ const healthCode = data.health;
+ const relayStates = {
+ 0: '待机',
+ 1: '等待握手',
+ 2: '握手已发',
+ 3: '挑战已发',
+ 4: '收到握手响应',
+ 5: '会话密钥已发',
+ 6: '认证完成',
+ 7: '大小已发',
+ 8: '大小确认',
+ 9: '接收中',
+ 10: '转发中',
+ 11: '数据包已发',
+ 12: 'ACK正常',
+ 13: '等待重发',
+ 14: '接收完成',
+ 15: '传输完成',
+ 16: '结束已发',
+ 17: 'OTA完成',
+ 255: '错误'
+ };
+
+ updateConnectionUi(!!wifi.connected, wifi.connected ? `${wifi.mode || 'STA'} 已连接` : `${wifi.mode || 'STA'} 未连接`);
+ updateHealthUi(healthCode, data.free_heap);
+
+ // 显示固件版本
+ if (data.version) {
+ document.getElementById('fwVersion').textContent = `v${data.version}`;
+ }
+
+ document.getElementById('statTemperature').textContent = Number.isFinite(data.temperature) ? `${data.temperature.toFixed(1)}°C` : '--';
+ document.getElementById('statWifi').textContent = wifi.connected ? (wifi.ip || '已连接') : '未连接';
+ document.getElementById('statSignal').textContent = (wifi.rssi !== undefined && wifi.rssi !== -127) ? `${wifi.rssi} dBm` : '--';
+ document.getElementById('statHeap').textContent = data.free_heap !== undefined ? formatBytes(data.free_heap) : '--';
+ document.getElementById('statMinHeap').textContent = data.min_free_heap !== undefined ? formatBytes(data.min_free_heap) : '--';
+
+ const total = ota.total_size || 0;
+ const stored = ota.stored_size || 0;
+ const progress = total > 0 ? Math.min(100, Math.round(stored * 100 / total)) : 0;
+ document.getElementById('statProgress').textContent = `${progress}%`;
+ document.getElementById('statRelayState').textContent = relayStates[ota.relay_state] ?? '待机';
+}
+
+async function testConnection() {
+ if (!latestSystemStatus) {
+ addLog('W', '连接', '暂无同步数据,请等待 1 秒自动同步');
+ showToast('暂无同步数据,请稍等 1 秒', 'warning');
+ return;
+ }
+
+ const wifi = latestSystemStatus.wifi || {};
+ const connected = !!wifi.connected;
+ updateSystemStatusUi(latestSystemStatus);
+ addLog('I', '连接', connected
+ ? `当前已连接:${wifi.mode || 'STA'} ${wifi.ip || ''}`.trim()
+ : `当前未连接:${wifi.mode || 'STA'}`);
+ showToast(connected ? 'ESP32 当前在线' : 'ESP32 当前未连接', connected ? 'success' : 'warning');
+}
+
+async function healthCheck() {
+ if (!latestSystemStatus) {
+ addLog('W', '健康', '暂无同步数据,请等待 1 秒自动同步');
+ showToast('暂无同步数据,请稍等 1 秒', 'warning');
+ return;
+ }
+
+ updateSystemStatusUi(latestSystemStatus);
+ const data = latestSystemStatus;
+ const healthNames = ['正常', '警告', '危险', '关机'];
+ const health = data.health !== undefined ? (healthNames[data.health] || '未知') : '未知';
+ addLog('I', '健康', `当前状态: ${health}, 空闲堆: ${data.free_heap} bytes, 温度: ${data.temperature}°C`);
+ showToast(`当前健康状态: ${health}`, 'success');
+}
+
+// ==================== CAN 帧发送 ====================
+async function sendCanFrame() {
+ const ch = parseInt(document.getElementById('canChannel').value);
+ const idStr = document.getElementById('canId').value.trim();
+ const dlc = parseInt(document.getElementById('canDlc').value) || 8;
+ const dataStr = document.getElementById('canData').value.trim();
+
+ let extId = 0;
+ if (idStr.startsWith('0x') || idStr.startsWith('0X')) {
+ extId = parseInt(idStr, 16);
+ } else {
+ extId = parseInt(idStr, 10);
+ if (isNaN(extId)) extId = parseInt(idStr, 16);
+ }
+
+ if (isNaN(extId)) {
+ showToast('ID 格式错误', 'error');
+ return;
+ }
+
+ let dataHex = dataStr.replace(/\s/g, '');
+ if (dataHex.length % 2 !== 0) {
+ showToast('数据长度必须为偶数', 'error');
+ return;
+ }
+
+ addLog('I', 'CAN', `发送 CH${ch} ID=0x${extId.toString(16).toUpperCase()} DLC=${dlc}`);
+
+ try {
+ const response = await fetch(`${getBaseUrl()}/can/send`, {
+ method: 'POST',
+ headers: { 'Content-Type': 'application/json' },
+ body: JSON.stringify({
+ ch: ch,
+ id: extId,
+ dlc: dlc,
+ data: dataHex
+ }),
+ signal: AbortSignal.timeout(5000)
+ });
+
+ const result = await response.json();
+
+ if (result.status === 'ok') {
+ addLog('I', 'CAN', `响应: ${result.recv || '(无数据)'} (${result.recv_len || 0}B)`);
+
+ const respDiv = document.getElementById('canResponse');
+ const respContent = document.getElementById('canRespContent');
+ respDiv.style.display = 'block';
+ respContent.textContent = result.recv
+ ? `0x${result.recv} (${result.recv_len} 字节)`
+ : '无响应数据';
+
+ showToast(`CH${ch} 发送成功`, 'success');
+ } else {
+ addLog('E', 'CAN', `发送失败: code=${result.code}`);
+ showToast(`发送失败: code=${result.code}`, 'error');
+ }
+ } catch (e) {
+ addLog('E', 'CAN', `发送失败: ${e.message}`);
+ showToast('发送失败', 'error');
+ }
+}
+
+function onTargetChipChange() {
+ const chip = document.getElementById('targetChip').value;
+ const relayBtn = document.getElementById('relayBtn');
+ const esp32Actions = document.getElementById('esp32Actions');
+
+ if (chip === 'esp32') {
+ relayBtn.style.display = 'none';
+ esp32Actions.style.display = 'block';
+ } else {
+ relayBtn.style.display = '';
+ esp32Actions.style.display = 'none';
+ }
+ addLog('I', '目标', `切换到: ${chip === 'esp32' ? 'ESP32 自身升级' : 'GD32 Flash 中继'}`);
+}
+
+function clearCanFields() {
+ document.getElementById('canId').value = '0x18DA00F1';
+ document.getElementById('canDlc').value = '8';
+ document.getElementById('canData').value = '0102030405060708';
+ document.getElementById('canResponse').style.display = 'none';
+}
+
+// ==================== 文件处理 ====================
+function handleFileSelect(event) {
+ const file = event.target.files[0];
+ if (file) processFile(file);
+}
+
+document.getElementById('uploadZone').addEventListener('dragover', (e) => {
+ e.preventDefault();
+ e.currentTarget.classList.add('dragover');
+});
+
+document.getElementById('uploadZone').addEventListener('dragleave', (e) => {
+ e.currentTarget.classList.remove('dragover');
+});
+
+document.getElementById('uploadZone').addEventListener('drop', (e) => {
+ e.preventDefault();
+ e.currentTarget.classList.remove('dragover');
+ const file = e.dataTransfer.files[0];
+ if (file) processFile(file);
+});
+
+function processFile(file) {
+ currentFile = file;
+ const fileInfo = document.getElementById('fileInfo');
+ const uploadBtn = document.getElementById('uploadBtn');
+
+ document.getElementById('fileName').textContent = file.name;
+ document.getElementById('fileType').textContent = file.name.endsWith('.hex') ? 'Intel HEX' : 'Binary';
+ document.getElementById('fileSize').textContent = formatBytes(file.size);
+
+ const chunks = Math.ceil(file.size / 4096);
+ document.getElementById('fileChunks').textContent = chunks + ' 包';
+
+ fileInfo.classList.add('visible');
+ uploadBtn.disabled = false;
+
+ addLog('I', '文件', `已选择: ${file.name} (${formatBytes(file.size)})`);
+ showToast('文件已选择', 'success');
+}
+
+function clearFile() {
+ currentFile = null;
+ document.getElementById('fileInput').value = '';
+ document.getElementById('fileInfo').classList.remove('visible');
+ document.getElementById('uploadBtn').disabled = true;
+ document.getElementById('progressSection').classList.remove('visible');
+ addLog('I', '文件', '文件已清除');
+}
+
+// ==================== 上传固件 ====================
+async function startUpload() {
+ if (!currentFile || isUploading) return;
+
+ const chip = document.getElementById('targetChip').value;
+ isUploading = true;
+ const uploadBtn = document.getElementById('uploadBtn');
+ uploadBtn.disabled = true;
+ uploadBtn.textContent = '上传中...';
+
+ const progressSection = document.getElementById('progressSection');
+ progressSection.classList.add('visible');
+
+ addLog('I', '上传', `开始上传: ${currentFile.name} → ${chip.toUpperCase()}`);
+ showToast(`开始上传固件到 ${chip.toUpperCase()}`, 'success');
+
+ try {
+ const arrayBuffer = await currentFile.arrayBuffer();
+ const fileSize = currentFile.size;
+
+ let body;
+ let url = `${getBaseUrl()}/ota/upload`;
+
+ if (chip === 'esp32') {
+ const headerJson = JSON.stringify({
+ type: 'esp32',
+ total_size: fileSize
+ });
+ const encoder = new TextEncoder();
+ const headerBytes = encoder.encode(headerJson);
+ const fileBytes = new Uint8Array(arrayBuffer);
+
+ const combined = new Uint8Array(headerBytes.length + fileSize);
+ combined.set(headerBytes, 0);
+ combined.set(fileBytes, headerBytes.length);
+
+ body = combined.buffer;
+ } else {
+ body = arrayBuffer;
+ }
+
+ const xhr = new XMLHttpRequest();
+
+ xhr.upload.addEventListener('progress', (e) => {
+ if (e.lengthComputable) {
+ const percent = Math.round((e.loaded / e.total) * 100);
+ uploadProgress = percent;
+ document.getElementById('progressBar').style.width = percent + '%';
+ document.getElementById('progressText').textContent =
+ `已上传 ${formatBytes(e.loaded)} / ${formatBytes(e.total)}`;
+ document.getElementById('progressPercent').textContent = percent + '%';
+ document.getElementById('statProgress').textContent = percent + '%';
+ }
+ });
+
+ xhr.addEventListener('load', () => {
+ isUploading = false;
+ uploadBtn.disabled = false;
+ uploadBtn.textContent = '⬆️ 上传固件';
+
+ if (xhr.status === 200) {
+ addLog('I', '上传', '固件上传完成!');
+ showToast('固件上传成功', 'success');
+
+ if (chip === 'esp32') {
+ document.getElementById('esp32Actions').style.display = 'block';
+ // 安全间隔:3 秒后自动调用刷写,留时间确认状态
+ let countdown = 3;
+ addLog('I', 'OTA', `${countdown} 秒后自动调用 POST /ota/update 完成升级并重启...`);
+ const timer = setInterval(() => {
+ countdown--;
+ if (countdown > 0) {
+ addLog('I', 'OTA', `${countdown} 秒后开始刷写...`);
+ } else {
+ clearInterval(timer);
+ addLog('I', 'OTA', '开始刷写!');
+ triggerEsp32Update();
+ }
+ }, 1000);
+ }
+ } else {
+ addLog('E', '上传', `上传失败: HTTP ${xhr.status}`);
+ showToast('上传失败', 'error');
+ }
+ });
+
+ xhr.addEventListener('error', () => {
+ isUploading = false;
+ uploadBtn.disabled = false;
+ uploadBtn.textContent = '⬆️ 上传固件';
+ addLog('E', '上传', '上传过程发生错误');
+ showToast('上传出错', 'error');
+ });
+
+ xhr.open('POST', url);
+ xhr.send(body);
+
+ } catch (e) {
+ addLog('E', '上传', `上传失败: ${e.message}`);
+ showToast('上传失败', 'error');
+ isUploading = false;
+ uploadBtn.disabled = false;
+ uploadBtn.textContent = '⬆️ 上传固件';
+ }
+}
+
+// ==================== ESP32 OTA 完成升级 ====================
+async function triggerEsp32Update() {
+ addLog('I', 'OTA', '=== ESP32 OTA 完成,开始重启 ===');
+ showToast('ESP32 正在重启...', 'success');
+
+ try {
+ const response = await fetch(`${getBaseUrl()}/ota/update`, {
+ method: 'POST',
+ signal: AbortSignal.timeout(5000)
+ });
+
+ if (response.ok) {
+ const data = await response.json();
+ addLog('I', 'OTA', `升级完成: ${data.message}`);
+ addLog('I', 'OTA', 'ESP32 即将重启,新固件将生效,页面将继续通过 1 秒状态同步自动恢复显示');
+ showToast('ESP32 重启中...', 'success');
+ }
+ } catch (e) {
+ addLog('E', 'OTA', `升级失败: ${e.message}`);
+ showToast('升级失败', 'error');
+ }
+}
+
+// ==================== 触发 OTA 中继 ====================
+async function triggerRelay() {
+ addLog('I', 'OTA', '触发 GD32 OTA 中继...');
+ showToast('正在触发 OTA 中继', 'success');
+
+ document.getElementById('stage1').className = 'stage active';
+ document.getElementById('statRelayState').textContent = '进行中';
+
+ try {
+ const response = await fetch(`${getBaseUrl()}/ota/relay`, {
+ method: 'POST',
+ signal: AbortSignal.timeout(5000)
+ });
+
+ if (response.ok) {
+ addLog('I', 'OTA', 'OTA 中继已触发,正在等待 GD32 响应...');
+
+ setTimeout(() => {
+ document.getElementById('stage1').className = 'stage completed';
+ document.getElementById('stage2').className = 'stage active';
+ }, 2000);
+
+ setTimeout(() => {
+ document.getElementById('stage2').className = 'stage completed';
+ document.getElementById('stage3').className = 'stage active';
+ }, 4000);
+
+ setTimeout(() => {
+ document.getElementById('stage3').className = 'stage completed';
+ document.getElementById('stage4').className = 'stage active';
+ }, 6000);
+
+ setTimeout(() => {
+ document.getElementById('stage4').className = 'stage completed';
+ document.getElementById('statRelayState').textContent = '完成';
+ addLog('I', 'OTA', '=== OTA 中继完成 ===');
+ showToast('OTA 流程完成!', 'success');
+ }, 8000);
+
+ } else {
+ addLog('E', 'OTA', `触发失败: HTTP ${response.status}`);
+ showToast('触发失败', 'error');
+ }
+ } catch (e) {
+ addLog('E', 'OTA', `触发失败: ${e.message}`);
+ showToast('触发失败', 'error');
+ document.getElementById('statRelayState').textContent = '失败';
+ }
+}
+
+// ==================== 轮询状态 ====================
+async function pollStatus() {
+ try {
+ const response = await fetch(`${getBaseUrl()}/system/status`, {
+ signal: AbortSignal.timeout(3000)
+ });
+ if (response.ok) {
+ const data = await response.json();
+ updateSystemStatusUi(data);
+ }
+ } catch (e) {
+ updateConnectionUi(false, '未连接');
+ }
+}
+
+setInterval(pollStatus, 1000);
+
+// ==================== ESP32 实时日志拉取 ====================
+async function pollLogs() {
+ try {
+ const response = await fetch(`${getBaseUrl()}/logs`, {
+ signal: AbortSignal.timeout(2000)
+ });
+ if (!response.ok) return;
+ const logs = await response.json();
+ if (!Array.isArray(logs) || logs.length === 0) return;
+
+ // 解析 [I][WIFI] xxx 格式
+ const levelMap = { 'E': 'E', 'W': 'W', 'I': 'I', 'D': 'D' };
+ logs.forEach(line => {
+ // 格式: [I][WIFI] message 或 [E][OTA] message
+ const m = line.match(/^\[([EWID])\]\[([^\]]+)\]\s*(.*)$/);
+ if (m) {
+ addLog(levelMap[m[1]] || 'I', m[2], m[3].trimEnd());
+ } else {
+ addLog('I', 'ESP32', line.trimEnd());
+ }
+ });
+ } catch (e) {
+ // 静默失败,不打扰用户
+ }
+}
+
+setInterval(pollLogs, 1000);
+
+setTimeout(() => addLog('I', '系统', '上位机初始化完成,等待固件文件...'), 500);
diff --git a/_assistant_backups/2026-05-05/app_config.h.bak_2026-05-05_0516 b/_assistant_backups/2026-05-05/app_config.h.bak_2026-05-05_0516
new file mode 100644
index 0000000..43586fc
--- /dev/null
+++ b/_assistant_backups/2026-05-05/app_config.h.bak_2026-05-05_0516
@@ -0,0 +1,105 @@
+/*
+ * app_config.h - 统一配置文件(C++ 版本)
+ * ESP32-C3 OTA 中继项目
+ *
+ * 使用 constexpr 代替 #define,提供类型安全
+ */
+#pragma once
+#include
+
+// ============================================================
+// 固件版本
+// ============================================================
+inline constexpr const char *FW_VERSION = "0.1.1";
+
+// ============================================================
+// WiFi 工作模式选择
+// ============================================================
+// 0 = AP 模式(设备当热点)
+// 1 = STA 模式(连接路由器)
+inline constexpr uint8_t CONFIG_WIFI_MODE = 1;
+
+// ============================================================
+// WiFi STA 模式配置(连接路由器)
+// ============================================================
+inline constexpr const char* WIFI_STA_SSID = "004舰桥";
+inline constexpr const char* WIFI_STA_PASSWORD = "q13362875622";
+inline constexpr uint32_t WIFI_STA_CONNECT_TIMEOUT_MS = 30000;
+inline constexpr uint8_t WIFI_STA_MAX_RETRY = 5; // 最大重连次数,超过后切换 AP 模式
+
+// STA 静态 IP
+inline constexpr uint8_t WIFI_STA_USE_STATIC_IP = 0; // 先关闭静态 IP,使用 DHCP 排查连接问题
+
+// 静态 IP 配置(始终定义,由 WIFI_STA_USE_STATIC_IP 在运行时决定是否使用)
+inline constexpr const char* WIFI_STA_STATIC_IP = "192.168.31.32";
+inline constexpr const char* WIFI_STA_STATIC_GW = "192.168.31.1";
+inline constexpr const char* WIFI_STA_STATIC_NETMASK = "255.255.255.0";
+inline constexpr const char* WIFI_STA_STATIC_DNS = "192.168.31.1";
+
+// ============================================================
+// WiFi AP 模式配置(设备当热点)
+// ============================================================
+inline constexpr const char* WIFI_AP_SSID = "ESP32_OTA_RELAY";
+inline constexpr const char* WIFI_AP_PASSWORD = "12345678";
+inline constexpr uint8_t WIFI_AP_CHANNEL = 6;
+inline constexpr uint8_t WIFI_AP_MAX_CONNECT = 4;
+
+// ============================================================
+// HTTP 服务器配置
+// ============================================================
+inline constexpr uint16_t HTTP_SERVER_PORT = 80;
+inline constexpr uint32_t HTTP_SERVER_STACK_SIZE = 8192;
+
+// ============================================================
+// OTA 配置
+// ============================================================
+inline constexpr size_t OTA_BLOCK_SIZE = 4096;
+inline constexpr size_t ESP32_OTA_RECV_BLOCK_SIZE = 8192;
+inline constexpr size_t OTA_MAX_FIRMWARE_SIZE = 2 * 1024 * 1024; // 2MB
+
+// ============================================================
+// 健康监控阈值
+// ============================================================
+inline constexpr int32_t WIFI_RSSI_CRITICAL = -85;
+inline constexpr int32_t WIFI_RSSI_WARNING = -75;
+inline constexpr int32_t TEMP_WARNING = 80;
+inline constexpr int32_t TEMP_CRITICAL = 100;
+inline constexpr size_t HEAP_WARNING_MIN = 10240;
+
+// ============================================================
+// 调试配置(UDP 日志)
+// ============================================================
+inline constexpr uint8_t DEBUG_LOG_ENABLED = 1; // 设为 0 禁用调试日志
+inline constexpr const char* DEBUG_LOG_IP = "192.168.31.3"; // 上位机 IP
+inline constexpr uint16_t DEBUG_LOG_PORT = 6666;
+
+// ============================================================
+// BLE OTA 配置
+// ============================================================
+inline constexpr const char* BLE_DEVICE_NAME = "ESP32-OTA-RELAY";
+inline constexpr uint16_t BLE_MTU_SIZE = 512;
+inline constexpr uint16_t BLE_OTA_SERVICE_UUID = 0xFFFF;
+inline constexpr uint16_t BLE_OTA_CTRL_UUID = 0x0001;
+inline constexpr uint16_t BLE_OTA_DATA_IN_UUID = 0x0002;
+inline constexpr uint16_t BLE_OTA_DATA_OUT_UUID = 0x0003;
+inline constexpr uint16_t BLE_OTA_VERSION_UUID = 0x0004;
+
+// ============================================================
+// 安全常量(需与 GD32 固件团队确认)
+// ⚠️ 请填入实际密钥
+// ============================================================
+inline constexpr uint8_t MASTER_KEY[] = {
+ 0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07,
+ 0x08, 0x09, 0x0A, 0x0B, 0x0C, 0x0D, 0x0E, 0x0F
+};
+
+inline constexpr uint8_t SCRAMBLE_CONST[] = {
+ 0x11, 0x22, 0x33, 0x44, 0x55, 0x66, 0x77, 0x88,
+ 0x99, 0xAA, 0xBB, 0xCC, 0xDD, 0xEE, 0xFF, 0x00
+};
+
+// ⚠️ 请填入实际的 GD32 芯片唯一码(12字节)
+inline constexpr uint8_t GD32_CHIP_UID[] = {
+ 0x00, 0x01, 0x02, 0x03, 0x04, 0x05,
+ 0x06, 0x07, 0x08, 0x09, 0x0A, 0x0B
+};
diff --git a/_assistant_backups/2026-05-05/app_config.h.bak_2026-05-05_0522 b/_assistant_backups/2026-05-05/app_config.h.bak_2026-05-05_0522
new file mode 100644
index 0000000..a5f1529
--- /dev/null
+++ b/_assistant_backups/2026-05-05/app_config.h.bak_2026-05-05_0522
@@ -0,0 +1,108 @@
+/*
+ * app_config.h - 统一配置文件(C++ 版本)
+ * ESP32-C3 OTA 中继项目
+ *
+ * 使用 constexpr 代替 #define,提供类型安全
+ */
+#pragma once
+#include
+
+// ============================================================
+// 固件版本
+// ============================================================
+inline constexpr const char *FW_VERSION = "0.1.1";
+
+// ============================================================
+// WiFi 工作模式选择
+// ============================================================
+// 0 = AP 模式(设备当热点)
+// 1 = STA 模式(连接路由器)
+inline constexpr uint8_t CONFIG_WIFI_MODE = 1;
+
+// ============================================================
+// WiFi STA 模式配置(连接路由器)
+// ============================================================
+inline constexpr const char* WIFI_STA_SSID = "004舰桥";
+inline constexpr const char* WIFI_STA_PASSWORD = "q13362875622";
+inline constexpr uint32_t WIFI_STA_CONNECT_TIMEOUT_MS = 30000;
+inline constexpr uint8_t WIFI_STA_MAX_RETRY = 5; // 最大重连次数,超过后切换 AP 模式
+
+// STA 静态 IP
+inline constexpr uint8_t WIFI_STA_USE_STATIC_IP = 0; // 先关闭静态 IP,使用 DHCP 排查连接问题
+
+// 静态 IP 配置(始终定义,由 WIFI_STA_USE_STATIC_IP 在运行时决定是否使用)
+inline constexpr const char* WIFI_STA_STATIC_IP = "192.168.31.32";
+inline constexpr const char* WIFI_STA_STATIC_GW = "192.168.31.1";
+inline constexpr const char* WIFI_STA_STATIC_NETMASK = "255.255.255.0";
+inline constexpr const char* WIFI_STA_STATIC_DNS = "192.168.31.1";
+
+// ============================================================
+// WiFi AP 模式配置(设备当热点)
+// ============================================================
+inline constexpr const char* WIFI_AP_SSID = "ESP32_OTA_RELAY";
+inline constexpr const char* WIFI_AP_PASSWORD = "12345678";
+inline constexpr uint8_t WIFI_AP_CHANNEL = 6;
+inline constexpr uint8_t WIFI_AP_MAX_CONNECT = 4;
+
+// ============================================================
+// HTTP 服务器配置
+// ============================================================
+inline constexpr uint16_t HTTP_SERVER_PORT = 80;
+inline constexpr uint32_t HTTP_SERVER_STACK_SIZE = 8192;
+
+// ============================================================
+// OTA 配置
+// ============================================================
+inline constexpr size_t OTA_BLOCK_SIZE = 4096;
+inline constexpr size_t ESP32_OTA_RECV_BLOCK_SIZE = 8192;
+inline constexpr size_t OTA_MAX_FIRMWARE_SIZE = 2 * 1024 * 1024; // 2MB
+
+// ============================================================
+// 健康监控阈值
+// ============================================================
+inline constexpr int32_t WIFI_RSSI_CRITICAL = -85;
+inline constexpr int32_t WIFI_RSSI_WARNING = -75;
+inline constexpr int32_t OTA_WIFI_RSSI_CRITICAL = -88; // OTA 期间更宽松,避免瞬时波动误判
+inline constexpr uint32_t OTA_WIFI_RSSI_SAMPLE_MAX_AGE_MS = 15000; // OTA 最多接受 15 秒内的 RSSI 样本
+inline constexpr uint32_t OTA_PACKET_HEALTH_CHECK_INTERVAL = 32; // 每 32 个 48B 包做一次 OTA 健康检查
+inline constexpr int32_t TEMP_WARNING = 80;
+inline constexpr int32_t TEMP_CRITICAL = 100;
+inline constexpr size_t HEAP_WARNING_MIN = 10240;
+
+// ============================================================
+// 调试配置(UDP 日志)
+// ============================================================
+inline constexpr uint8_t DEBUG_LOG_ENABLED = 1; // 设为 0 禁用调试日志
+inline constexpr const char* DEBUG_LOG_IP = "192.168.31.3"; // 上位机 IP
+inline constexpr uint16_t DEBUG_LOG_PORT = 6666;
+
+// ============================================================
+// BLE OTA 配置
+// ============================================================
+inline constexpr const char* BLE_DEVICE_NAME = "ESP32-OTA-RELAY";
+inline constexpr uint16_t BLE_MTU_SIZE = 512;
+inline constexpr uint16_t BLE_OTA_SERVICE_UUID = 0xFFFF;
+inline constexpr uint16_t BLE_OTA_CTRL_UUID = 0x0001;
+inline constexpr uint16_t BLE_OTA_DATA_IN_UUID = 0x0002;
+inline constexpr uint16_t BLE_OTA_DATA_OUT_UUID = 0x0003;
+inline constexpr uint16_t BLE_OTA_VERSION_UUID = 0x0004;
+
+// ============================================================
+// 安全常量(需与 GD32 固件团队确认)
+// ⚠️ 请填入实际密钥
+// ============================================================
+inline constexpr uint8_t MASTER_KEY[] = {
+ 0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07,
+ 0x08, 0x09, 0x0A, 0x0B, 0x0C, 0x0D, 0x0E, 0x0F
+};
+
+inline constexpr uint8_t SCRAMBLE_CONST[] = {
+ 0x11, 0x22, 0x33, 0x44, 0x55, 0x66, 0x77, 0x88,
+ 0x99, 0xAA, 0xBB, 0xCC, 0xDD, 0xEE, 0xFF, 0x00
+};
+
+// ⚠️ 请填入实际的 GD32 芯片唯一码(12字节)
+inline constexpr uint8_t GD32_CHIP_UID[] = {
+ 0x00, 0x01, 0x02, 0x03, 0x04, 0x05,
+ 0x06, 0x07, 0x08, 0x09, 0x0A, 0x0B
+};
diff --git a/_assistant_backups/2026-05-05/app_config.h.bak_2026-05-05_0525 b/_assistant_backups/2026-05-05/app_config.h.bak_2026-05-05_0525
new file mode 100644
index 0000000..3e44c94
--- /dev/null
+++ b/_assistant_backups/2026-05-05/app_config.h.bak_2026-05-05_0525
@@ -0,0 +1,108 @@
+/*
+ * app_config.h - 统一配置文件(C++ 版本)
+ * ESP32-C3 OTA 中继项目
+ *
+ * 使用 constexpr 代替 #define,提供类型安全
+ */
+#pragma once
+#include
+
+// ============================================================
+// 固件版本
+// ============================================================
+inline constexpr const char *FW_VERSION = "0.1.1";
+
+// ============================================================
+// WiFi 工作模式选择
+// ============================================================
+// 0 = AP 模式(设备当热点)
+// 1 = STA 模式(连接路由器)
+inline constexpr uint8_t CONFIG_WIFI_MODE = 1;
+
+// ============================================================
+// WiFi STA 模式配置(连接路由器)
+// ============================================================
+inline constexpr const char* WIFI_STA_SSID = "004舰桥";
+inline constexpr const char* WIFI_STA_PASSWORD = "q13362875622";
+inline constexpr uint32_t WIFI_STA_CONNECT_TIMEOUT_MS = 30000;
+inline constexpr uint8_t WIFI_STA_MAX_RETRY = 5; // 最大重连次数,超过后切换 AP 模式
+
+// STA 静态 IP
+inline constexpr uint8_t WIFI_STA_USE_STATIC_IP = 0; // 先关闭静态 IP,使用 DHCP 排查连接问题
+
+// 静态 IP 配置(始终定义,由 WIFI_STA_USE_STATIC_IP 在运行时决定是否使用)
+inline constexpr const char* WIFI_STA_STATIC_IP = "192.168.31.32";
+inline constexpr const char* WIFI_STA_STATIC_GW = "192.168.31.1";
+inline constexpr const char* WIFI_STA_STATIC_NETMASK = "255.255.255.0";
+inline constexpr const char* WIFI_STA_STATIC_DNS = "192.168.31.1";
+
+// ============================================================
+// WiFi AP 模式配置(设备当热点)
+// ============================================================
+inline constexpr const char* WIFI_AP_SSID = "ESP32_OTA_RELAY";
+inline constexpr const char* WIFI_AP_PASSWORD = "12345678";
+inline constexpr uint8_t WIFI_AP_CHANNEL = 6;
+inline constexpr uint8_t WIFI_AP_MAX_CONNECT = 4;
+
+// ============================================================
+// HTTP 服务器配置
+// ============================================================
+inline constexpr uint16_t HTTP_SERVER_PORT = 80;
+inline constexpr uint32_t HTTP_SERVER_STACK_SIZE = 8192;
+
+// ============================================================
+// OTA 配置
+// ============================================================
+inline constexpr size_t OTA_BLOCK_SIZE = 8192;
+inline constexpr size_t ESP32_OTA_RECV_BLOCK_SIZE = 8192;
+inline constexpr size_t OTA_MAX_FIRMWARE_SIZE = 2 * 1024 * 1024; // 2MB
+
+// ============================================================
+// 健康监控阈值
+// ============================================================
+inline constexpr int32_t WIFI_RSSI_CRITICAL = -85;
+inline constexpr int32_t WIFI_RSSI_WARNING = -75;
+inline constexpr int32_t OTA_WIFI_RSSI_CRITICAL = -88; // OTA 期间更宽松,避免瞬时波动误判
+inline constexpr uint32_t OTA_WIFI_RSSI_SAMPLE_MAX_AGE_MS = 15000; // OTA 最多接受 15 秒内的 RSSI 样本
+inline constexpr uint32_t OTA_PACKET_HEALTH_CHECK_INTERVAL = 32; // 每 32 个 48B 包做一次 OTA 健康检查
+inline constexpr int32_t TEMP_WARNING = 80;
+inline constexpr int32_t TEMP_CRITICAL = 100;
+inline constexpr size_t HEAP_WARNING_MIN = 10240;
+
+// ============================================================
+// 调试配置(UDP 日志)
+// ============================================================
+inline constexpr uint8_t DEBUG_LOG_ENABLED = 1; // 设为 0 禁用调试日志
+inline constexpr const char* DEBUG_LOG_IP = "192.168.31.3"; // 上位机 IP
+inline constexpr uint16_t DEBUG_LOG_PORT = 6666;
+
+// ============================================================
+// BLE OTA 配置
+// ============================================================
+inline constexpr const char* BLE_DEVICE_NAME = "ESP32-OTA-RELAY";
+inline constexpr uint16_t BLE_MTU_SIZE = 512;
+inline constexpr uint16_t BLE_OTA_SERVICE_UUID = 0xFFFF;
+inline constexpr uint16_t BLE_OTA_CTRL_UUID = 0x0001;
+inline constexpr uint16_t BLE_OTA_DATA_IN_UUID = 0x0002;
+inline constexpr uint16_t BLE_OTA_DATA_OUT_UUID = 0x0003;
+inline constexpr uint16_t BLE_OTA_VERSION_UUID = 0x0004;
+
+// ============================================================
+// 安全常量(需与 GD32 固件团队确认)
+// ⚠️ 请填入实际密钥
+// ============================================================
+inline constexpr uint8_t MASTER_KEY[] = {
+ 0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07,
+ 0x08, 0x09, 0x0A, 0x0B, 0x0C, 0x0D, 0x0E, 0x0F
+};
+
+inline constexpr uint8_t SCRAMBLE_CONST[] = {
+ 0x11, 0x22, 0x33, 0x44, 0x55, 0x66, 0x77, 0x88,
+ 0x99, 0xAA, 0xBB, 0xCC, 0xDD, 0xEE, 0xFF, 0x00
+};
+
+// ⚠️ 请填入实际的 GD32 芯片唯一码(12字节)
+inline constexpr uint8_t GD32_CHIP_UID[] = {
+ 0x00, 0x01, 0x02, 0x03, 0x04, 0x05,
+ 0x06, 0x07, 0x08, 0x09, 0x0A, 0x0B
+};
diff --git a/_assistant_backups/2026-05-05/ble_ota.cpp.bak_2026-05-05_0522 b/_assistant_backups/2026-05-05/ble_ota.cpp.bak_2026-05-05_0522
new file mode 100644
index 0000000..44b07c4
--- /dev/null
+++ b/_assistant_backups/2026-05-05/ble_ota.cpp.bak_2026-05-05_0522
@@ -0,0 +1,643 @@
+/*
+ * ble_ota.cpp - BLE OTA GATT 服务器实现 (ESP-IDF 6.0 NimBLE)
+ * T5.1 - T5.5
+ *
+ * 通过 BLE GATT 接收上位机固件,存储到 Flash (gd32_ota 分区),
+ * 接收完成后触发 OTA 中继任务转发到 GD32。
+ *
+ * GATT 服务:
+ * Service UUID: 0xFFFF
+ * - CTRL (0x0001, Write): 写入 0xF7 触发中继
+ * - DATA_IN (0x0002, Write): 接收固件数据块
+ * - DATA_OUT (0x0003, Notify): 推送 GD32 响应
+ * - VERSION (0x0004, Read): 固件版本
+ */
+#include
+#include
+#include "freertos/FreeRTOS.h"
+#include "freertos/task.h"
+#include "freertos/semphr.h"
+#include "freertos/queue.h"
+#include "esp_err.h"
+
+// NimBLE 协议栈
+#include "nimble/nimble_port.h"
+#include "nimble/nimble_port_freertos.h"
+#include "host/ble_hs.h"
+#include "host/ble_gap.h"
+#include "services/gap/ble_svc_gap.h"
+#include "services/gatt/ble_svc_gatt.h"
+
+#include "app_config.h"
+#include "ble_ota.h"
+#include "ota_relay.h"
+#include "health_monitor.h"
+#include "monitor/debug_log.h"
+
+#define TAG "BLE_OTA"
+
+// ============================================================
+// 静态状态
+// ============================================================
+
+static bool s_ble_ready = false; // NimBLE 同步完成
+static bool s_ble_connected = false; // GATT 客户端已连接
+/** @brief 当前 BLE 连接句柄。 */
+static uint16_t s_conn_handle = BLE_HS_CONN_HANDLE_NONE;
+static uint16_t s_data_out_val_handle = 0; // DATA_OUT 值句柄 (供通知用)
+
+// ============================================================
+// NimBLE 工具: 从 mbuf 读取原始数据
+/**
+ * @brief 将 NimBLE mbuf 中的数据拷贝到普通字节缓冲区。
+ * @param om NimBLE mbuf 数据指针。
+ * @param buf 输出缓冲区。
+ * @param buf_len 缓冲区最大长度。
+ * @return 拷贝的字节数,失败返回 -1。
+ */
+static int ble_ota_mbuf_to_buf(struct os_mbuf *om, uint8_t *buf, size_t buf_len)
+{
+ if (!om || !buf) return -1;
+ uint16_t om_len = OS_MBUF_PKTLEN(om);
+ uint16_t copy_len = (om_len > buf_len) ? (uint16_t)buf_len : om_len;
+ os_mbuf_copydata(om, 0, copy_len, buf);
+ return (int)copy_len;
+}
+
+// ============================================================
+// GATT 访问回调
+// ============================================================
+
+/*
+ * CTRL (0x0001) - Write
+ * 写入 0xF7 触发 OTA 中继
+ */
+static int ble_ota_ctrl_access(uint16_t conn_handle, uint16_t attr_handle,
+ struct ble_gatt_access_ctxt *ctxt, void *arg)
+{
+ (void)conn_handle;
+ (void)attr_handle;
+ (void)arg;
+
+ if (ctxt->op != BLE_GATT_ACCESS_OP_WRITE_CHR) {
+ return BLE_ATT_ERR_REQ_NOT_SUPPORTED;
+ }
+
+ uint8_t buf[4] = {0};
+ int len = ble_ota_mbuf_to_buf(ctxt->om, buf, sizeof(buf));
+ if (len < 1) {
+ DEBUG_WARN(TAG, "CTRL 数据为空");
+ return BLE_ATT_ERR_INVALID_ATTR_VALUE_LEN;
+ }
+
+ DEBUG_INFO(TAG, "CTRL 写入: cmd=0x%02X", buf[0]);
+
+ if (buf[0] == 0xF7) {
+ // 检查固件是否存储完毕
+ ota_meta_t meta;
+ esp_err_t err = meta_load(&meta);
+ if (err == ESP_OK &&
+ meta.total_size > 0 &&
+ meta.stored_size >= meta.total_size) {
+ DEBUG_INFO(TAG, "固件就绪 (%" PRIu32 " / %" PRIu32 " 字节),触发中继",
+ meta.stored_size, meta.total_size);
+ ble_ota_signal_relay();
+ } else {
+ DEBUG_WARN(TAG, "固件未就绪, 未触发中继 (stored=%" PRIu32 ", total=%" PRIu32 ")",
+ meta.stored_size, meta.total_size);
+ }
+ }
+
+ return 0; // BLE_ATT_ERR_OK 在 NimBLE 中就是 0
+}
+
+/*
+ * DATA_IN (0x0002) - Write
+ * 数据块格式:
+ * [0] cmd (0x00=数据)
+ * [1-2] seq uint16 LE - 分块序号
+ * [3-6] total_size uint32 LE - 固件总大小 (仅 seq=0 时有效)
+ * [7..n-4] payload 固件数据
+ * [n-4..n] crc 校验 (预留)
+ */
+static int ble_ota_data_in_access(uint16_t conn_handle, uint16_t attr_handle,
+ struct ble_gatt_access_ctxt *ctxt, void *arg)
+{
+ (void)conn_handle;
+ (void)attr_handle;
+ (void)arg;
+
+ if (ctxt->op != BLE_GATT_ACCESS_OP_WRITE_CHR) {
+ return BLE_ATT_ERR_REQ_NOT_SUPPORTED;
+ }
+
+ uint8_t buf[BLE_MTU_SIZE] = {0};
+ int recv_len = ble_ota_mbuf_to_buf(ctxt->om, buf, sizeof(buf));
+
+ if (recv_len < 12) {
+ DEBUG_WARN(TAG, "DATA_IN 数据太短: %d 字节 (最少 12)", recv_len);
+ return BLE_ATT_ERR_INVALID_ATTR_VALUE_LEN;
+ }
+
+ // 解析包头
+ uint8_t cmd = buf[0];
+ uint16_t seq = (uint16_t)(buf[1] | (buf[2] << 8));
+ uint32_t total_size = (uint32_t)(buf[3] | (buf[4] << 8) |
+ (buf[5] << 16) | (buf[6] << 24));
+
+ size_t data_offset = 7;
+ size_t data_len = (size_t)recv_len - data_offset;
+
+ // 尾 4 字节 CRC 预留,实际 payload = data_len - 4
+ if (data_len < 4) {
+ DEBUG_WARN(TAG, "DATA_IN 负载不足: data_len=%zu", data_len);
+ return BLE_ATT_ERR_INVALID_ATTR_VALUE_LEN;
+ }
+ data_len -= 4; // 减去尾部 CRC 预留
+
+ DEBUG_DBG(TAG, "DATA_IN: cmd=0x%02X seq=%u total=%" PRIu32 " payload=%zu",
+ cmd, seq, total_size, data_len);
+
+ // --- 首个分块: 创建存储元数据 ---
+ if (seq == 0 && total_size > 0) {
+ if (total_size > OTA_MAX_FIRMWARE_SIZE) {
+ DEBUG_ERR(TAG, "固件太大: %" PRIu32 " > %zu",
+ total_size, OTA_MAX_FIRMWARE_SIZE);
+ return BLE_ATT_ERR_UNLIKELY;
+ }
+
+ ota_meta_t meta = {};
+ meta.total_size = total_size;
+ meta.stored_size = 0;
+ meta.last_seq = 0;
+ meta.chunk_count = (total_size + (OTA_BLOCK_SIZE - 1)) / OTA_BLOCK_SIZE;
+
+ esp_err_t err = meta_save(&meta);
+ if (err != ESP_OK) {
+ DEBUG_ERR(TAG, "元数据保存失败");
+ return BLE_ATT_ERR_UNLIKELY;
+ }
+
+ DEBUG_INFO(TAG, "固件存储开始: total=%" PRIu32 " chunk_count=%" PRIu32,
+ meta.total_size, meta.chunk_count);
+ }
+
+ // --- 写入 Flash ---
+ uint32_t flash_offset = seq * OTA_BLOCK_SIZE;
+ esp_err_t err = flash_write_chunk(flash_offset, &buf[data_offset], data_len);
+ if (err != ESP_OK) {
+ DEBUG_ERR(TAG, "Flash 写入失败: seq=%u offset=%" PRIu32 " err=%s",
+ seq, flash_offset, esp_err_to_name(err));
+ return BLE_ATT_ERR_UNLIKELY;
+ }
+
+ err = meta_update(flash_offset + data_len, seq);
+ if (err != ESP_OK) {
+ DEBUG_WARN(TAG, "元数据更新失败 (seq=%u)", seq);
+ }
+
+ // --- 检查是否接收完毕 ---
+ ota_meta_t meta;
+ if (meta_load(&meta) == ESP_OK &&
+ meta.total_size > 0 &&
+ meta.stored_size >= meta.total_size) {
+ DEBUG_INFO(TAG, "★ 固件接收完毕: %" PRIu32 " / %" PRIu32 " 字节 (共 %u 块)",
+ meta.stored_size, meta.total_size, seq + 1);
+ }
+
+ return 0;
+}
+
+/*
+ * VERSION (0x0004) - Read
+ * 返回固件版本字符串
+ */
+static int ble_ota_version_access(uint16_t conn_handle, uint16_t attr_handle,
+ struct ble_gatt_access_ctxt *ctxt, void *arg)
+{
+ (void)conn_handle;
+ (void)attr_handle;
+ (void)arg;
+
+ if (ctxt->op != BLE_GATT_ACCESS_OP_READ_CHR) {
+ return BLE_ATT_ERR_REQ_NOT_SUPPORTED;
+ }
+
+ const char *version = "1.0.0";
+ int rc = os_mbuf_append(ctxt->om, version, strlen(version));
+ return (rc == 0) ? 0 : BLE_ATT_ERR_UNLIKELY;
+}
+
+/*
+ * DATA_OUT (0x0003) - Notify only
+ * 访问回调不做读写处理,通知由 ble_ota_notify_response() 发送
+ */
+static int ble_ota_data_out_access(uint16_t conn_handle, uint16_t attr_handle,
+ struct ble_gatt_access_ctxt *ctxt, void *arg)
+{
+ (void)conn_handle;
+ (void)attr_handle;
+ (void)ctxt;
+ (void)arg;
+ return 0;
+}
+
+// ============================================================
+// GATT 服务定义
+// ============================================================
+
+// BLE_UUID16_DECLARE 在 C++ 中产生 "taking address of rvalue" 编译错误。
+// 改用 static ble_uuid16_t 变量,再取 .u 地址。
+/** @brief OTA GATT 服务 UUID */
+static const ble_uuid16_t GATT_SVC_UUID = BLE_UUID16_INIT(BLE_OTA_SERVICE_UUID);
+/** @brief CTRL 特征 UUID,写入 0xF7 触发中继 */
+static const ble_uuid16_t GATT_CTRL_UUID = BLE_UUID16_INIT(BLE_OTA_CTRL_UUID);
+/** @brief DATA_IN 特征 UUID,接收固件数据块 */
+static const ble_uuid16_t GATT_DATA_IN_UUID = BLE_UUID16_INIT(BLE_OTA_DATA_IN_UUID);
+/** @brief DATA_OUT 特征 UUID,Notify 推送 GD32 响应 */
+static const ble_uuid16_t GATT_DATA_OUT_UUID = BLE_UUID16_INIT(BLE_OTA_DATA_OUT_UUID);
+/** @brief VERSION 特征 UUID,返回固件版本字符串 */
+static const ble_uuid16_t GATT_VERSION_UUID = BLE_UUID16_INIT(BLE_OTA_VERSION_UUID);
+
+// NimBLE C 结构体在 C++ designated initializer 不能显式初始化全部字段。
+// 用 pragma 局部关闭 -Wmissing-field-initializers 警告。
+#ifdef __cplusplus
+#pragma GCC diagnostic push
+#pragma GCC diagnostic ignored "-Wmissing-field-initializers"
+#endif
+
+static const struct ble_gatt_chr_def gatt_ota_chrs[] = {
+ {
+ .uuid = &GATT_CTRL_UUID.u,
+ .access_cb = ble_ota_ctrl_access,
+ .flags = BLE_GATT_CHR_F_WRITE,
+ },
+ {
+ .uuid = &GATT_DATA_IN_UUID.u,
+ .access_cb = ble_ota_data_in_access,
+ .flags = BLE_GATT_CHR_F_WRITE,
+ },
+ {
+ .uuid = &GATT_DATA_OUT_UUID.u,
+ .access_cb = ble_ota_data_out_access,
+ .flags = BLE_GATT_CHR_F_NOTIFY,
+ .val_handle = &s_data_out_val_handle,
+ },
+ {
+ .uuid = &GATT_VERSION_UUID.u,
+ .access_cb = ble_ota_version_access,
+ .flags = BLE_GATT_CHR_F_READ,
+ },
+ {} // 终止符
+};
+
+static const struct ble_gatt_svc_def gatt_ota_svcs[] = {
+ {
+ .type = BLE_GATT_SVC_TYPE_PRIMARY,
+ .uuid = &GATT_SVC_UUID.u,
+ .characteristics = (struct ble_gatt_chr_def *)gatt_ota_chrs,
+ },
+ {} // 终止符
+};
+
+#ifdef __cplusplus
+#pragma GCC diagnostic pop
+#endif
+
+// ============================================================
+// GAP 事件回调
+// ============================================================
+/**
+ * @brief BLE GAP 事件回调,处理连接和断开事件。
+ * @param event
+ * @param arg
+ * @return 0 成功。
+ */
+static int ble_ota_gap_event(struct ble_gap_event *event, void *arg)
+{
+ (void)arg;
+
+ switch (event->type) {
+
+ case BLE_GAP_EVENT_CONNECT:
+ if (event->connect.status == 0) {
+ // 连接成功
+ s_ble_connected = true;
+ /** @brief 当前 BLE 连接句柄。 */
+ s_conn_handle = event->connect.conn_handle;
+ DEBUG_INFO(TAG, "BLE 客户端已连接: handle=%d", s_conn_handle);
+ } else {
+ DEBUG_WARN(TAG, "BLE 连接失败: status=%d", event->connect.status);
+ /** @brief BLE GATT 客户端连接状态。 */
+ s_ble_connected = false;
+ }
+ return 0;
+
+ case BLE_GAP_EVENT_DISCONNECT:
+ DEBUG_INFO(TAG, "BLE 客户端已断开: reason=%d",
+ event->disconnect.reason);
+ /** @brief BLE GATT 客户端连接状态。 */
+ s_ble_connected = false;
+ /** @brief 当前 BLE 连接句柄。 */
+ s_conn_handle = BLE_HS_CONN_HANDLE_NONE;
+ // 恢复广播以接受新连接
+ ble_gap_adv_start(BLE_OWN_ADDR_PUBLIC, NULL, BLE_HS_FOREVER,
+ NULL, ble_ota_gap_event, NULL);
+ return 0;
+
+ case BLE_GAP_EVENT_ADV_COMPLETE:
+ DEBUG_INFO(TAG, "广播周期结束,重新开始");
+ ble_gap_adv_start(BLE_OWN_ADDR_PUBLIC, NULL, BLE_HS_FOREVER,
+ NULL, ble_ota_gap_event, NULL);
+ return 0;
+
+ case BLE_GAP_EVENT_SUBSCRIBE:
+ DEBUG_INFO(TAG, "CCC 描述符更新: conn=%d attr=%d notify=%d indicate=%d",
+ event->subscribe.conn_handle,
+ event->subscribe.attr_handle,
+ event->subscribe.cur_notify,
+ event->subscribe.cur_indicate);
+ return 0;
+
+ case BLE_GAP_EVENT_MTU:
+ DEBUG_INFO(TAG, "MTU 协商: conn=%d mtu=%d",
+ event->mtu.conn_handle, event->mtu.value);
+ return 0;
+
+ default:
+ return 0;
+ }
+}
+
+// ============================================================
+// BLE 广播
+// ============================================================
+/**
+ * @brief 启动 BLE 广播,使设备可被发现和连接。
+ */
+static void ble_ota_start_advertising(void)
+{
+ uint8_t adv_data[31] = {0};
+ int adv_pos = 0;
+
+ // AD Flags: LE General Discoverable + BR/EDR Not Supported
+ adv_data[adv_pos++] = 0x02; // 长度
+ adv_data[adv_pos++] = 0x01; // 类型 = Flags
+ adv_data[adv_pos++] = 0x06; // 值
+
+ // Complete Local Name
+ const char *name = BLE_DEVICE_NAME;
+ size_t name_len = strlen(name);
+ if (name_len > 29 - adv_pos - 2) {
+ name_len = 29 - adv_pos - 2; // 截断以适配广播包
+ }
+ adv_data[adv_pos++] = (uint8_t)(name_len + 1);
+ adv_data[adv_pos++] = 0x09; // 类型 = Complete Local Name
+ memcpy(&adv_data[adv_pos], name, name_len);
+ adv_pos += name_len;
+
+ int rc = ble_gap_adv_set_data(adv_data, adv_pos);
+ if (rc != 0) {
+ DEBUG_ERR(TAG, "设置广播数据失败: %d", rc);
+ return;
+ }
+
+ // 扫描响应: 包含 Service UUID 以帮助发现
+ uint8_t scan_rsp[31] = {0};
+ int rsp_pos = 0;
+ scan_rsp[rsp_pos++] = 0x11; // 长度 (2 + 15)
+ scan_rsp[rsp_pos++] = 0x09; // 类型 = Complete Local Name
+ name_len = strlen(name);
+ if (name_len > (size_t)(31 - rsp_pos)) {
+ name_len = 31 - rsp_pos;
+ }
+ memcpy(&scan_rsp[rsp_pos], name, name_len);
+ rsp_pos += name_len;
+
+ rc = ble_gap_adv_rsp_set_data(scan_rsp, rsp_pos);
+ if (rc != 0) {
+ DEBUG_WARN(TAG, "设置扫描响应数据失败: %d", rc);
+ }
+
+ // 开始广播
+ struct ble_gap_adv_params adv_params = {};
+ adv_params.conn_mode = BLE_GAP_CONN_MODE_UND;
+ adv_params.disc_mode = BLE_GAP_DISC_MODE_GEN;
+ // 从 kconfig 获取 BLE 地址类型,默认 Public
+#ifdef CONFIG_BLE_ADDR_TYPE_PUBLIC
+ uint8_t own_addr_type = BLE_OWN_ADDR_PUBLIC;
+#else
+ uint8_t own_addr_type = BLE_OWN_ADDR_RANDOM;
+#endif
+
+ rc = ble_gap_adv_start(own_addr_type, NULL, BLE_HS_FOREVER,
+ &adv_params, ble_ota_gap_event, NULL);
+ if (rc != 0) {
+ DEBUG_ERR(TAG, "启动广播失败: %d", rc);
+ } else {
+ DEBUG_INFO(TAG, "BLE 广播已启动: \"%s\"", name);
+ }
+}
+
+// ============================================================
+// NimBLE 同步/重置回调
+// ============================================================
+/**
+ * @brief NimBLE 协议栈同步完成回调,初始化广播。
+ */
+static void ble_ota_on_sync(void)
+{
+ DEBUG_INFO(TAG, "NimBLE 协议栈同步完成");
+
+ // GATT 服务已在 ble_ota_server_start() 中注册,不再重复注册
+ // 重复调用 ble_gatts_count_cfg / ble_gatts_add_svcs 会导致 BLE_HS_EBUSY
+
+ s_ble_ready = true;
+ ble_ota_start_advertising();
+}
+
+/**
+ * @brief NimBLE 协议栈重置回调。
+ * @param reason
+ */
+static void ble_ota_on_reset(int reason)
+{
+ DEBUG_WARN(TAG, "NimBLE 协议栈重置: reason=%d", reason);
+ /** @brief NimBLE 协议栈同步完成标志。 */
+ s_ble_ready = false;
+}
+
+// ============================================================
+// NimBLE Host 任务(独立 FreeRTOS 任务)
+// ============================================================
+/**
+ * @brief NimBLE 主机任务入口,处理协议栈事件循环。
+ * @param param
+ */
+static void ble_ota_nimble_host_task(void *param)
+{
+ (void)param;
+ DEBUG_INFO(TAG, "NimBLE Host 任务启动");
+ nimble_port_run();
+ // 正常情况下不会到达这里
+ DEBUG_INFO(TAG, "NimBLE Host 任务退出");
+}
+
+// ============================================================
+// 公开 API
+// ============================================================
+/**
+ * @brief 启动 BLE GATT 服务器并注册 OTA 服务。
+ * @return ESP_OK 成功,其他失败。
+ */
+esp_err_t ble_ota_server_start(void)
+{
+ int rc;
+
+ DEBUG_INFO(TAG, "=== 初始化 BLE OTA GATT 服务器 ===");
+ DEBUG_INFO(TAG, " Device: %s", BLE_DEVICE_NAME);
+ DEBUG_INFO(TAG, " MTU: %" PRIu16, BLE_MTU_SIZE);
+ DEBUG_INFO(TAG, " Svc: 0x%04X", BLE_OTA_SERVICE_UUID);
+
+ esp_err_t err = nimble_port_init();
+ if (err != ESP_OK) {
+ DEBUG_ERR(TAG, "nimble_port_init 失败: %s", esp_err_to_name(err));
+ return err;
+ }
+ DEBUG_INFO(TAG, "nimble_port_init 完成");
+
+ ble_svc_gap_init();
+ ble_svc_gatt_init();
+
+ rc = ble_svc_gap_device_name_set(BLE_DEVICE_NAME);
+ if (rc != 0) {
+ DEBUG_WARN(TAG, "设置设备名称失败: %d", rc);
+ }
+
+ // 5. 配置 Host 回调
+ // sync_cb: NimBLE 事件循环就绪时调用 (开始广播)
+ // reset_cb: NimBLE 重置时调用
+ ble_hs_cfg.sync_cb = ble_ota_on_sync;
+ ble_hs_cfg.reset_cb = ble_ota_on_reset;
+ // 不做 bonding,不需要 store 回调
+
+ rc = ble_gatts_count_cfg(gatt_ota_svcs);
+ if (rc != 0) {
+ DEBUG_ERR(TAG, "GATT 服务计数失败: %d", rc);
+ return ESP_FAIL;
+ }
+
+ rc = ble_gatts_add_svcs(gatt_ota_svcs);
+ if (rc != 0) {
+ DEBUG_ERR(TAG, "GATT 服务注册失败: %d", rc);
+ return ESP_FAIL;
+ }
+
+ nimble_port_freertos_init(ble_ota_nimble_host_task);
+
+ DEBUG_INFO(TAG, "BLE OTA 服务器初始化完成");
+ return ESP_OK;
+}
+
+/**
+ * @brief 通过 BLE Notify 将 GD32 响应推送给上位机。
+ * @param data 待推送的数据指针。
+ * @param len 数据长度(字节)。
+ * @return ESP_OK 成功,其他失败。
+ */
+esp_err_t ble_ota_notify_response(const uint8_t *data, size_t len)
+{
+ if (!s_ble_connected) {
+ return ESP_ERR_NOT_SUPPORTED;
+ }
+ if (s_data_out_val_handle == 0) {
+ DEBUG_WARN(TAG, "DATA_OUT 值句柄未设置");
+ return ESP_ERR_INVALID_STATE;
+ }
+
+ if (len == 0) return ESP_OK;
+
+ struct os_mbuf *om = ble_hs_mbuf_from_flat(data, len);
+ if (om == NULL) {
+ DEBUG_ERR(TAG, "分配 mbuf 失败 (%zu 字节)", len);
+ return ESP_ERR_NO_MEM;
+ }
+
+ int rc = ble_gattc_notify_custom(s_conn_handle, s_data_out_val_handle, om);
+ if (rc != 0) {
+ DEBUG_WARN(TAG, "通知发送失败: rc=%d", rc);
+ return ESP_FAIL;
+ }
+
+ DEBUG_DBG(TAG, "通知已发送: %zu 字节", len);
+ return ESP_OK;
+}
+
+/**
+ * @brief 检查当前是否有 BLE 客户端连接。
+ * @return true 已连接,false 未连接。
+ */
+bool ble_ota_is_connected(void)
+{
+ /** @brief BLE GATT 客户端连接状态。 */
+ return s_ble_connected;
+}
+
+/**
+ * @brief 通知中继模块开始向 GD32 转发固件。
+ */
+void ble_ota_signal_relay(void)
+{
+ if (g_ota_dispatch_queue == NULL) {
+ DEBUG_ERR(TAG, "OTA 调度队列未初始化");
+ return;
+ }
+
+ uint32_t msg = 1;
+ BaseType_t ret = xQueueSend(g_ota_dispatch_queue, &msg, pdMS_TO_TICKS(200));
+ if (ret != pdTRUE) {
+ DEBUG_ERR(TAG, "OTA 调度信号发送失败: 队列满");
+ return;
+ }
+
+ DEBUG_INFO(TAG, "OTA 调度信号已发送 → ota_relay_task");
+}
+
+// ============================================================
+// BLE OTA FreeRTOS 任务入口
+//
+// 注意: 此任务的职责是初始化 BLE 服务器并保持存活。
+// 实际的 BLE 事件循环在独立的 NimBLE Host 任务中运行。
+// 为避免与 ota_relay_task 竞争,此任务不从 g_ota_dispatch_queue 消费。
+// ============================================================
+/**
+ * @brief BLE OTA FreeRTOS 任务,初始化 NimBLE 并运行 GATT 服务。
+ * @param pvParameters FreeRTOS 任务参数,未使用。
+ */
+void ble_ota_task(void *pvParameters)
+{
+ (void)pvParameters;
+ DEBUG_INFO(TAG, "=== BLE OTA 任务启动 ===");
+
+ esp_err_t err = ble_ota_server_start();
+ if (err != ESP_OK) {
+ DEBUG_ERR(TAG, "BLE 服务器启动失败: %s → BLE OTA 不可用",
+ esp_err_to_name(err));
+ }
+
+ // 保持任务存活 — 所有 BLE 事件由 NimBLE Host 任务处理
+ uint32_t heartbeat = 0;
+ while (1) {
+ heartbeat++;
+
+ // 每 60 秒打印一次心跳(确认任务存活)
+ if (heartbeat % 6 == 0) {
+ DEBUG_DBG(TAG, "心跳 #%lu | BLE %s | 连接: %s",
+ (unsigned long)(heartbeat / 6),
+ s_ble_ready ? "就绪" : "未就绪",
+ s_ble_connected ? "已连接" : "未连接");
+ }
+
+ vTaskDelay(pdMS_TO_TICKS(10000));
+ }
+}
diff --git a/_assistant_backups/2026-05-05/health_monitor.cpp.bak_2026-05-05_0451 b/_assistant_backups/2026-05-05/health_monitor.cpp.bak_2026-05-05_0451
new file mode 100644
index 0000000..f48355b
--- /dev/null
+++ b/_assistant_backups/2026-05-05/health_monitor.cpp.bak_2026-05-05_0451
@@ -0,0 +1,289 @@
+/*
+ * health_monitor.cpp - 系统健康监控
+ * 包含:健康状态管理 + WiFi RSSI 监控 + 温度监控 + 电压监控
+ */
+#include
+#include
+#include "freertos/FreeRTOS.h"
+#include "freertos/task.h"
+#include "freertos/semphr.h"
+#include "esp_wifi.h"
+#include "esp_timer.h"
+#include "esp_err.h"
+#include "driver/temperature_sensor.h"
+
+#include "health_monitor.h"
+#include "debug_log.h"
+
+/** @brief 全局健康状态,受互斥锁保护。 */
+health_state_t g_health_state = SYS_NORMAL;
+
+/** @brief 健康状态互斥锁。 */
+static SemaphoreHandle_t g_health_mutex = NULL;
+/** @brief 内置温度传感器句柄。 */
+static temperature_sensor_handle_t s_temp_handle = NULL;
+/** @brief 最近一次采样的芯片温度(摄氏度)。 */
+static float s_last_temperature = 0.0f;
+/** @brief 最近一次估算的系统电压(伏特)。 */
+static float s_last_voltage = 3.30f;
+
+// ----------------------------------------
+// 健康状态管理
+/**
+ * @brief 初始化健康监控模块,创建互斥锁和基础状态。
+ */
+void health_monitor_init(void)
+{
+ /** @brief 健康状态互斥锁。 */
+ g_health_mutex = xSemaphoreCreateMutex();
+ if (g_health_mutex == NULL) {
+ DEBUG_ERR("HEALTH", "创建健康状态互斥锁失败");
+ }
+}
+
+/**
+ * @brief 获取当前系统健康状态。
+ * @return 当前健康状态枚举值。
+ */
+health_state_t health_get_current(void)
+{
+ health_state_t state;
+ if (g_health_mutex) {
+ xSemaphoreTake(g_health_mutex, portMAX_DELAY);
+ }
+ state = g_health_state;
+ if (g_health_mutex) {
+ xSemaphoreGive(g_health_mutex);
+ }
+ return state;
+}
+
+/**
+ * @brief 返回最近一次采样到的芯片温度(摄氏度)。
+ * @return 最近一次温度采样值(摄氏度)。
+ */
+float health_get_last_temperature(void)
+{
+ /** @brief 最近一次采样的芯片温度(摄氏度)。 */
+ return s_last_temperature;
+}
+
+/**
+ * @brief 返回最近一次记录的系统电压估算值。
+ * @return 最近一次电压估算值(伏特)。
+ */
+float health_get_last_voltage(void)
+{
+ /** @brief 最近一次估算的系统电压(伏特)。 */
+ return s_last_voltage;
+}
+
+// ----------------------------------------
+// 温度监控(定时器回调)
+/**
+ * @brief 温度定时回调,采样并根据阈值更新健康状态。
+ * @param arg
+ */
+static void temp_monitor_callback(void *arg)
+{
+ float temp = 0.0f;
+ esp_err_t err = temperature_sensor_get_celsius(s_temp_handle, &temp);
+ if (err != ESP_OK) {
+ DEBUG_WARN("TEMP", "读取温度失败: %s", esp_err_to_name(err));
+ return;
+ }
+
+ /** @brief 最近一次采样的芯片温度(摄氏度)。 */
+ s_last_temperature = temp;
+
+ if (temp >= TEMP_CRITICAL) {
+ /** @brief 全局健康状态,受互斥锁保护。 */
+ g_health_state = SYS_SAFE_SHUTDOWN;
+ DEBUG_ERR("TEMP", "!!! 危险 !!! 温度 %.1f°C >= %d°C,触发安全关机", temp, TEMP_CRITICAL);
+ DEBUG_ERR("TEMP", "健康状态变更为: SYS_SAFE_SHUTDOWN");
+ } else if (temp >= TEMP_WARNING) {
+ if (g_health_state < SYS_WARNING) {
+ /** @brief 全局健康状态,受互斥锁保护。 */
+ g_health_state = SYS_WARNING;
+ DEBUG_WARN("TEMP", "警告: 温度 %.1f°C >= %d°C", temp, TEMP_WARNING);
+ DEBUG_WARN("TEMP", "健康状态变更为: SYS_WARNING");
+ }
+ }
+}
+
+// ----------------------------------------
+// 电压监控(启动时调用一次)
+/**
+ * @brief 电压定时回调,采样估算电压并根据阈值更新健康状态。
+ * @param arg
+ */
+static void voltage_monitor_callback(void *arg)
+{
+ float voltage = 3.30f; // ESP32-C3 无内置电压传感器,使用估算
+ /** @brief 最近一次估算的系统电压(伏特)。 */
+ s_last_voltage = voltage;
+
+ if (voltage < 2.3f) {
+ /** @brief 全局健康状态,受互斥锁保护。 */
+ g_health_state = SYS_SAFE_SHUTDOWN;
+ DEBUG_ERR("VOLT", "!!! 危险 !!! 电压 %.2fV < 2.3V,触发安全关机", voltage);
+ DEBUG_ERR("VOLT", "健康状态变更为: SYS_SAFE_SHUTDOWN");
+ } else if (voltage < 2.5f) {
+ if (g_health_state < SYS_WARNING) {
+ /** @brief 全局健康状态,受互斥锁保护。 */
+ g_health_state = SYS_WARNING;
+ DEBUG_WARN("VOLT", "警告: 电压 %.2fV < 2.5V", voltage);
+ DEBUG_WARN("VOLT", "健康状态变更为: SYS_WARNING");
+ }
+ }
+}
+
+// ----------------------------------------
+// 温度电压监控任务入口
+/**
+ * @brief 启动温度和电压监控定时器,周期性巡检系统健康。
+ * @param pvParameters FreeRTOS 任务参数,未使用。
+ */
+void health_monitor_task(void *pvParameters)
+{
+ DEBUG_INFO("MON", "=== 健康监控任务启动 ===");
+
+ temperature_sensor_config_t temp_config = TEMPERATURE_SENSOR_CONFIG_DEFAULT(20, 80);
+ esp_err_t err = temperature_sensor_install(&temp_config, &s_temp_handle);
+ if (err != ESP_OK) {
+ DEBUG_ERR("MON", "温度传感器初始化失败: %s", esp_err_to_name(err));
+ } else {
+ ESP_ERROR_CHECK(temperature_sensor_enable(s_temp_handle));
+ DEBUG_INFO("MON", "温度传感器已启用 (范围 20°C ~ 80°C)");
+ }
+
+ // 定时器,每1秒读取一次温度
+ esp_timer_handle_t temp_timer;
+ esp_timer_create_args_t timer_args = {};
+ timer_args.callback = &temp_monitor_callback;
+ timer_args.arg = nullptr;
+ timer_args.name = "temp_monitor";
+ timer_args.dispatch_method = ESP_TIMER_TASK;
+ timer_args.skip_unhandled_events = false;
+ esp_timer_create(&timer_args, &temp_timer);
+ esp_timer_start_periodic(temp_timer, 1 * 1000000); // 1秒
+
+ DEBUG_INFO("MON", "温度监控定时器已启动 (间隔 1 秒)");
+
+ voltage_monitor_callback(NULL);
+
+ // 每1秒刷新一次估算电压
+ esp_timer_handle_t volt_timer;
+ esp_timer_create_args_t volt_timer_args = {};
+ volt_timer_args.callback = &voltage_monitor_callback;
+ volt_timer_args.arg = nullptr;
+ volt_timer_args.name = "volt_monitor";
+ volt_timer_args.dispatch_method = ESP_TIMER_TASK;
+ volt_timer_args.skip_unhandled_events = false;
+ esp_timer_create(&volt_timer_args, &volt_timer);
+ esp_timer_start_periodic(volt_timer, 1 * 1000000); // 1秒
+ DEBUG_INFO("MON", "电压监控定时器已启动 (间隔 1 秒)");
+
+ // 保活循环:定时器由 esp_timer 任务驱动,此任务只需保持存活
+ // 状态汇报已由 status_monitor_task 负责,此处只在异常时告警
+ while (1) {
+ uint32_t free_heap = esp_get_free_heap_size();
+ if (free_heap < HEAP_WARNING_MIN) {
+ DEBUG_WARN("MON", "空闲堆内存不足: %lu < %d", (unsigned long)free_heap, HEAP_WARNING_MIN);
+ }
+ if (g_health_state >= SYS_CRITICAL) {
+ DEBUG_ERR("MON", "健康状态危险: %d", g_health_state);
+ }
+ vTaskDelay(pdMS_TO_TICKS(5000));
+ }
+}
+
+// ----------------------------------------
+// 健康事件日志
+/**
+ * @brief 记录一条健康监控事件日志。
+ * @param event 事件名称字符串。
+ * @param value 事件关联的数值。
+ */
+void health_log_event(const char *event, int value)
+{
+ DEBUG_INFO("HEALTH", "[健康事件] %s: %d", event, value);
+}
+
+// ----------------------------------------
+// OTA 传输前的健康检查
+/**
+ * @brief OTA 传输前执行健康检查,决定是否允许传输。
+ * @return 健康检查结果,决定是否允许 OTA。
+ */
+health_status_t health_check_before_ota(void)
+{
+ health_status_t status = HEALTH_MON_OK;
+
+ if (g_health_state >= SYS_SAFE_SHUTDOWN) {
+ DEBUG_WARN("HEALTH", "OTA 前检查: SYS_SAFE_SHUTDOWN,禁止传输");
+ return HEALTH_MON_SHUTDOWN;
+ } else if (g_health_state >= SYS_CRITICAL) {
+ DEBUG_WARN("HEALTH", "OTA 前检查: SYS_CRITICAL,禁止传输");
+ return HEALTH_MON_CRITICAL;
+ } else if (g_health_state >= SYS_WARNING) {
+ status = HEALTH_MON_WARNING;
+ DEBUG_WARN("HEALTH", "OTA 前检查: SYS_WARNING,继续传输但需注意");
+ }
+
+ wifi_ap_record_t ap_info;
+ if (esp_wifi_sta_get_ap_info(&ap_info) == ESP_OK) {
+ if (ap_info.rssi < WIFI_RSSI_CRITICAL) {
+ /** @brief 全局健康状态,受互斥锁保护。 */
+ g_health_state = SYS_CRITICAL;
+ DEBUG_WARN("HEALTH", "OTA 前检查: WiFi RSSI %d < %d (危险阈值)",
+ ap_info.rssi, WIFI_RSSI_CRITICAL);
+ return HEALTH_MON_CRITICAL;
+ } else if (ap_info.rssi < WIFI_RSSI_NORMAL) {
+ if (status < HEALTH_MON_WARNING) {
+ status = HEALTH_MON_WARNING;
+ }
+ DEBUG_WARN("HEALTH", "OTA 前检查: WiFi RSSI %d < %d (警告阈值)",
+ ap_info.rssi, WIFI_RSSI_NORMAL);
+ }
+ }
+
+ if (status == HEALTH_MON_OK) {
+ DEBUG_DBG("HEALTH", "OTA 前检查: 正常");
+ }
+
+ return status;
+}
+
+/**
+ * @brief 根据 WiFi RSSI 更新健康状态并返回检查结果。
+ * @param rssi 当前 WiFi 信号强度(dBm)。
+ * @return 健康检查结果。
+ */
+health_status_t health_update_from_wifi_rssi(int8_t rssi)
+{
+ if (rssi < WIFI_RSSI_CRITICAL) {
+ /** @brief 全局健康状态,受互斥锁保护。 */
+ g_health_state = SYS_CRITICAL;
+ DEBUG_WARN("HEALTH", "WiFi RSSI 危险: %d dBm < %d dBm", rssi, WIFI_RSSI_CRITICAL);
+ health_log_event("WIFI危险", rssi);
+ return HEALTH_MON_CRITICAL;
+ } else if (rssi < WIFI_RSSI_NORMAL) {
+ if (g_health_state < SYS_WARNING) {
+ /** @brief 全局健康状态,受互斥锁保护。 */
+ g_health_state = SYS_WARNING;
+ DEBUG_WARN("HEALTH", "WiFi RSSI 警告: %d dBm < %d dBm", rssi, WIFI_RSSI_NORMAL);
+ health_log_event("WIFI警告", rssi);
+ }
+ return HEALTH_MON_WARNING;
+ }
+
+ if (g_health_state == SYS_WARNING && rssi >= WIFI_RSSI_NORMAL) {
+ /** @brief 全局健康状态,受互斥锁保护。 */
+ g_health_state = SYS_NORMAL;
+ DEBUG_INFO("HEALTH", "WiFi RSSI 恢复: %d dBm >= %d dBm", rssi, WIFI_RSSI_NORMAL);
+ health_log_event("WIFI恢复", rssi);
+ }
+
+ return HEALTH_MON_OK;
+}
diff --git a/_assistant_backups/2026-05-05/health_monitor.cpp.bak_2026-05-05_0516 b/_assistant_backups/2026-05-05/health_monitor.cpp.bak_2026-05-05_0516
new file mode 100644
index 0000000..863ffe8
--- /dev/null
+++ b/_assistant_backups/2026-05-05/health_monitor.cpp.bak_2026-05-05_0516
@@ -0,0 +1,236 @@
+/*
+ * health_monitor.cpp - 系统健康监控
+ * 包含:健康状态管理 + WiFi RSSI 监控 + 温度监控
+ */
+#include
+#include
+#include "freertos/FreeRTOS.h"
+#include "freertos/task.h"
+#include "freertos/semphr.h"
+#include "esp_wifi.h"
+#include "esp_timer.h"
+#include "esp_err.h"
+#include "driver/temperature_sensor.h"
+
+#include "health_monitor.h"
+#include "debug_log.h"
+
+/** @brief 全局健康状态,受互斥锁保护。 */
+health_state_t g_health_state = SYS_NORMAL;
+
+/** @brief 健康状态互斥锁。 */
+static SemaphoreHandle_t g_health_mutex = NULL;
+/** @brief 内置温度传感器句柄。 */
+static temperature_sensor_handle_t s_temp_handle = NULL;
+/** @brief 最近一次采样的芯片温度(摄氏度)。 */
+static float s_last_temperature = 0.0f;
+
+// ----------------------------------------
+// 健康状态管理
+/**
+ * @brief 初始化健康监控模块,创建互斥锁和基础状态。
+ */
+void health_monitor_init(void)
+{
+ /** @brief 健康状态互斥锁。 */
+ g_health_mutex = xSemaphoreCreateMutex();
+ if (g_health_mutex == NULL) {
+ DEBUG_ERR("HEALTH", "创建健康状态互斥锁失败");
+ }
+}
+
+/**
+ * @brief 获取当前系统健康状态。
+ * @return 当前健康状态枚举值。
+ */
+health_state_t health_get_current(void)
+{
+ health_state_t state;
+ if (g_health_mutex) {
+ xSemaphoreTake(g_health_mutex, portMAX_DELAY);
+ }
+ state = g_health_state;
+ if (g_health_mutex) {
+ xSemaphoreGive(g_health_mutex);
+ }
+ return state;
+}
+
+/**
+ * @brief 返回最近一次采样到的芯片温度(摄氏度)。
+ * @return 最近一次温度采样值(摄氏度)。
+ */
+float health_get_last_temperature(void)
+{
+ /** @brief 最近一次采样的芯片温度(摄氏度)。 */
+ return s_last_temperature;
+}
+
+// ----------------------------------------
+// 温度监控(定时器回调)
+/**
+ * @brief 温度定时回调,采样并根据阈值更新健康状态。
+ * @param arg
+ */
+static void temp_monitor_callback(void *arg)
+{
+ float temp = 0.0f;
+ esp_err_t err = temperature_sensor_get_celsius(s_temp_handle, &temp);
+ if (err != ESP_OK) {
+ DEBUG_WARN("TEMP", "读取温度失败: %s", esp_err_to_name(err));
+ return;
+ }
+
+ /** @brief 最近一次采样的芯片温度(摄氏度)。 */
+ s_last_temperature = temp;
+
+ if (temp >= TEMP_CRITICAL) {
+ /** @brief 全局健康状态,受互斥锁保护。 */
+ g_health_state = SYS_SAFE_SHUTDOWN;
+ DEBUG_ERR("TEMP", "!!! 危险 !!! 温度 %.1f°C >= %d°C,触发安全关机", temp, TEMP_CRITICAL);
+ DEBUG_ERR("TEMP", "健康状态变更为: SYS_SAFE_SHUTDOWN");
+ } else if (temp >= TEMP_WARNING) {
+ if (g_health_state < SYS_WARNING) {
+ /** @brief 全局健康状态,受互斥锁保护。 */
+ g_health_state = SYS_WARNING;
+ DEBUG_WARN("TEMP", "警告: 温度 %.1f°C >= %d°C", temp, TEMP_WARNING);
+ DEBUG_WARN("TEMP", "健康状态变更为: SYS_WARNING");
+ }
+ }
+}
+
+// ----------------------------------------
+// 温度监控任务入口
+/**
+ * @brief 启动温度监控定时器,周期性巡检系统健康。
+ * @param pvParameters FreeRTOS 任务参数,未使用。
+ */
+void health_monitor_task(void *pvParameters)
+{
+ DEBUG_INFO("MON", "=== 健康监控任务启动 ===");
+
+ temperature_sensor_config_t temp_config = TEMPERATURE_SENSOR_CONFIG_DEFAULT(20, 80);
+ esp_err_t err = temperature_sensor_install(&temp_config, &s_temp_handle);
+ if (err != ESP_OK) {
+ DEBUG_ERR("MON", "温度传感器初始化失败: %s", esp_err_to_name(err));
+ } else {
+ ESP_ERROR_CHECK(temperature_sensor_enable(s_temp_handle));
+ DEBUG_INFO("MON", "温度传感器已启用 (范围 20°C ~ 80°C)");
+ }
+
+ // 定时器,每1秒读取一次温度
+ esp_timer_handle_t temp_timer;
+ esp_timer_create_args_t timer_args = {};
+ timer_args.callback = &temp_monitor_callback;
+ timer_args.arg = nullptr;
+ timer_args.name = "temp_monitor";
+ timer_args.dispatch_method = ESP_TIMER_TASK;
+ timer_args.skip_unhandled_events = false;
+ esp_timer_create(&timer_args, &temp_timer);
+ esp_timer_start_periodic(temp_timer, 1 * 1000000); // 1秒
+
+ DEBUG_INFO("MON", "温度监控定时器已启动 (间隔 1 秒)");
+
+ // 保活循环:定时器由 esp_timer 任务驱动,此任务只需保持存活
+ // 状态汇报已由 status_monitor_task 负责,此处只在异常时告警
+ while (1) {
+ uint32_t free_heap = esp_get_free_heap_size();
+ if (free_heap < HEAP_WARNING_MIN) {
+ DEBUG_WARN("MON", "空闲堆内存不足: %lu < %d", (unsigned long)free_heap, HEAP_WARNING_MIN);
+ }
+ if (g_health_state >= SYS_CRITICAL) {
+ DEBUG_ERR("MON", "健康状态危险: %d", g_health_state);
+ }
+ vTaskDelay(pdMS_TO_TICKS(5000));
+ }
+}
+
+// ----------------------------------------
+// 健康事件日志
+/**
+ * @brief 记录一条健康监控事件日志。
+ * @param event 事件名称字符串。
+ * @param value 事件关联的数值。
+ */
+void health_log_event(const char *event, int value)
+{
+ DEBUG_INFO("HEALTH", "[健康事件] %s: %d", event, value);
+}
+
+// ----------------------------------------
+// OTA 传输前的健康检查
+/**
+ * @brief OTA 传输前执行健康检查,决定是否允许传输。
+ * @return 健康检查结果,决定是否允许 OTA。
+ */
+health_status_t health_check_before_ota(void)
+{
+ health_status_t status = HEALTH_MON_OK;
+
+ if (g_health_state >= SYS_SAFE_SHUTDOWN) {
+ DEBUG_WARN("HEALTH", "OTA 前检查: SYS_SAFE_SHUTDOWN,禁止传输");
+ return HEALTH_MON_SHUTDOWN;
+ } else if (g_health_state >= SYS_CRITICAL) {
+ DEBUG_WARN("HEALTH", "OTA 前检查: SYS_CRITICAL,禁止传输");
+ return HEALTH_MON_CRITICAL;
+ } else if (g_health_state >= SYS_WARNING) {
+ status = HEALTH_MON_WARNING;
+ DEBUG_WARN("HEALTH", "OTA 前检查: SYS_WARNING,继续传输但需注意");
+ }
+
+ wifi_ap_record_t ap_info;
+ if (esp_wifi_sta_get_ap_info(&ap_info) == ESP_OK) {
+ if (ap_info.rssi < WIFI_RSSI_CRITICAL) {
+ /** @brief 全局健康状态,受互斥锁保护。 */
+ g_health_state = SYS_CRITICAL;
+ DEBUG_WARN("HEALTH", "OTA 前检查: WiFi RSSI %d < %d (危险阈值)",
+ ap_info.rssi, WIFI_RSSI_CRITICAL);
+ return HEALTH_MON_CRITICAL;
+ } else if (ap_info.rssi < WIFI_RSSI_NORMAL) {
+ if (status < HEALTH_MON_WARNING) {
+ status = HEALTH_MON_WARNING;
+ }
+ DEBUG_WARN("HEALTH", "OTA 前检查: WiFi RSSI %d < %d (警告阈值)",
+ ap_info.rssi, WIFI_RSSI_NORMAL);
+ }
+ }
+
+ if (status == HEALTH_MON_OK) {
+ DEBUG_DBG("HEALTH", "OTA 前检查: 正常");
+ }
+
+ return status;
+}
+
+/**
+ * @brief 根据 WiFi RSSI 更新健康状态并返回检查结果。
+ * @param rssi 当前 WiFi 信号强度(dBm)。
+ * @return 健康检查结果。
+ */
+health_status_t health_update_from_wifi_rssi(int8_t rssi)
+{
+ if (rssi < WIFI_RSSI_CRITICAL) {
+ /** @brief 全局健康状态,受互斥锁保护。 */
+ g_health_state = SYS_CRITICAL;
+ DEBUG_WARN("HEALTH", "WiFi RSSI 危险: %d dBm < %d dBm", rssi, WIFI_RSSI_CRITICAL);
+ health_log_event("WIFI危险", rssi);
+ return HEALTH_MON_CRITICAL;
+ } else if (rssi < WIFI_RSSI_NORMAL) {
+ if (g_health_state < SYS_WARNING) {
+ /** @brief 全局健康状态,受互斥锁保护。 */
+ g_health_state = SYS_WARNING;
+ DEBUG_WARN("HEALTH", "WiFi RSSI 警告: %d dBm < %d dBm", rssi, WIFI_RSSI_NORMAL);
+ health_log_event("WIFI警告", rssi);
+ }
+ return HEALTH_MON_WARNING;
+ }
+
+ if (g_health_state == SYS_WARNING && rssi >= WIFI_RSSI_NORMAL) {
+ /** @brief 全局健康状态,受互斥锁保护。 */
+ g_health_state = SYS_NORMAL;
+ DEBUG_INFO("HEALTH", "WiFi RSSI 恢复: %d dBm >= %d dBm", rssi, WIFI_RSSI_NORMAL);
+ health_log_event("WIFI恢复", rssi);
+ }
+
+ return HEALTH_MON_OK;
+}
diff --git a/_assistant_backups/2026-05-05/health_monitor.h.bak_2026-05-05_0451 b/_assistant_backups/2026-05-05/health_monitor.h.bak_2026-05-05_0451
new file mode 100644
index 0000000..d248225
--- /dev/null
+++ b/_assistant_backups/2026-05-05/health_monitor.h.bak_2026-05-05_0451
@@ -0,0 +1,78 @@
+/*
+ * health_monitor.h - 系统健康监控头文件
+ */
+#pragma once
+
+#include
+#include "app_config.h"
+
+// T9.3: 健康状态等级
+typedef enum {
+ SYS_NORMAL = 0, // 正常
+ SYS_WARNING = 1, // 警告
+ SYS_CRITICAL = 2, // 危险
+ SYS_SAFE_SHUTDOWN = 3 // 安全关机
+} health_state_t;
+
+typedef enum {
+ HEALTH_MON_OK = 0,
+ HEALTH_MON_WARNING = 1,
+ HEALTH_MON_CRITICAL = 2,
+ HEALTH_MON_SHUTDOWN = 3
+} health_status_t;
+
+// 外部共享状态(互斥锁保护)
+extern health_state_t g_health_state;
+
+// WiFi RSSI 阈值(来自 app_config.h)
+#define WIFI_RSSI_NORMAL WIFI_RSSI_WARNING
+#define WIFI_RSSI_WEAK WIFI_RSSI_CRITICAL
+
+/**
+ * @brief 初始化健康监控模块,创建内部互斥锁与基础状态。
+ */
+void health_monitor_init(void);
+
+/**
+ * @brief 健康监控任务入口,负责启动温度/电压定时检测并周期性巡检系统状态。
+ * @param pvParameters FreeRTOS 任务参数,未使用。
+ */
+void health_monitor_task(void *pvParameters);
+
+/**
+ * @brief 在 OTA 传输开始前执行健康检查,返回当前是否允许传输。
+ * @return 健康检查结果,决定是否允许 OTA。
+ */
+health_status_t health_check_before_ota(void);
+
+/**
+ * @brief 记录一条健康相关事件日志,便于故障追踪与状态回溯。
+ * @param event 事件名称字符串。
+ * @param value 事件关联的数值。
+ */
+void health_log_event(const char *event, int value);
+
+/**
+ * @brief 获取当前系统健康状态。
+ * @return 当前健康状态枚举值。
+ */
+health_state_t health_get_current(void);
+
+/**
+ * @brief 根据最新 WiFi RSSI 更新健康状态,并返回对应健康检查结果。
+ * @param rssi 当前 WiFi 信号强度(dBm)。
+ * @return 健康检查结果。
+ */
+health_status_t health_update_from_wifi_rssi(int8_t rssi);
+
+/**
+ * @brief 获取最近一次采样到的芯片温度(摄氏度)。
+ * @return 最近一次温度采样值(摄氏度)。
+ */
+float health_get_last_temperature(void);
+
+/**
+ * @brief 获取最近一次记录的系统电压估算值(伏特)。
+ * @return 最近一次电压估算值(伏特)。
+ */
+float health_get_last_voltage(void);
diff --git a/_assistant_backups/2026-05-05/health_monitor.h.bak_2026-05-05_0516 b/_assistant_backups/2026-05-05/health_monitor.h.bak_2026-05-05_0516
new file mode 100644
index 0000000..898adef
--- /dev/null
+++ b/_assistant_backups/2026-05-05/health_monitor.h.bak_2026-05-05_0516
@@ -0,0 +1,72 @@
+/*
+ * health_monitor.h - 系统健康监控头文件
+ */
+#pragma once
+
+#include
+#include "app_config.h"
+
+// T9.3: 健康状态等级
+typedef enum {
+ SYS_NORMAL = 0, // 正常
+ SYS_WARNING = 1, // 警告
+ SYS_CRITICAL = 2, // 危险
+ SYS_SAFE_SHUTDOWN = 3 // 安全关机
+} health_state_t;
+
+typedef enum {
+ HEALTH_MON_OK = 0,
+ HEALTH_MON_WARNING = 1,
+ HEALTH_MON_CRITICAL = 2,
+ HEALTH_MON_SHUTDOWN = 3
+} health_status_t;
+
+// 外部共享状态(互斥锁保护)
+extern health_state_t g_health_state;
+
+// WiFi RSSI 阈值(来自 app_config.h)
+#define WIFI_RSSI_NORMAL WIFI_RSSI_WARNING
+#define WIFI_RSSI_WEAK WIFI_RSSI_CRITICAL
+
+/**
+ * @brief 初始化健康监控模块,创建内部互斥锁与基础状态。
+ */
+void health_monitor_init(void);
+
+/**
+ * @brief 健康监控任务入口,负责启动温度定时检测并周期性巡检系统状态。
+ * @param pvParameters FreeRTOS 任务参数,未使用。
+ */
+void health_monitor_task(void *pvParameters);
+
+/**
+ * @brief 在 OTA 传输开始前执行健康检查,返回当前是否允许传输。
+ * @return 健康检查结果,决定是否允许 OTA。
+ */
+health_status_t health_check_before_ota(void);
+
+/**
+ * @brief 记录一条健康相关事件日志,便于故障追踪与状态回溯。
+ * @param event 事件名称字符串。
+ * @param value 事件关联的数值。
+ */
+void health_log_event(const char *event, int value);
+
+/**
+ * @brief 获取当前系统健康状态。
+ * @return 当前健康状态枚举值。
+ */
+health_state_t health_get_current(void);
+
+/**
+ * @brief 根据最新 WiFi RSSI 更新健康状态,并返回对应健康检查结果。
+ * @param rssi 当前 WiFi 信号强度(dBm)。
+ * @return 健康检查结果。
+ */
+health_status_t health_update_from_wifi_rssi(int8_t rssi);
+
+/**
+ * @brief 获取最近一次采样到的芯片温度(摄氏度)。
+ * @return 最近一次温度采样值(摄氏度)。
+ */
+float health_get_last_temperature(void);
diff --git a/_assistant_backups/2026-05-05/ota_relay.cpp.bak_2026-05-05_0516 b/_assistant_backups/2026-05-05/ota_relay.cpp.bak_2026-05-05_0516
new file mode 100644
index 0000000..5f09a73
--- /dev/null
+++ b/_assistant_backups/2026-05-05/ota_relay.cpp.bak_2026-05-05_0516
@@ -0,0 +1,587 @@
+/*
+ * ota_relay.c - OTA 中继核心状态机实现
+ * T2.3, T2.4, T3.1 - T3.7
+ */
+#include
+#include
+#include "freertos/FreeRTOS.h"
+#include "freertos/task.h"
+#include "freertos/queue.h"
+#include "esp_err.h"
+#include "esp_partition.h"
+#include "cJSON.h"
+
+#include "ota_relay.h"
+#include "spi_master.h"
+#include "protocol.h"
+#include "health_monitor.h"
+#include "monitor/debug_log.h"
+
+/** @brief 当前 OTA 中继状态机状态。 */
+relay_state_t g_relay_state = RELAY_IDLE;
+
+static const esp_partition_t *g_gd32_partition = NULL;
+/** @brief OTA 认证会话上下文,包含会话密钥和 IV。 */
+static session_ctx_t g_session_ctx;
+
+/** @brief OTA 命令分发队列,从 WiFi/BLE 任务接收指令。 */
+QueueHandle_t g_ota_dispatch_queue = NULL;
+/** @brief OTA 结果回传队列,将执行结果返回给调用方。 */
+QueueHandle_t g_ota_result_queue = NULL;
+
+/**
+ * @brief OTA 握手阶段:向 GD32 发送握手请求并等待进入认证流程。
+ * @return 操作结果码。
+ */
+static ota_result_t send_handshake_req(void);
+
+/**
+ * @brief 认证阶段:生成随机数并完成与 GD32 的双向认证。
+ * @return 操作结果码。
+ */
+static ota_result_t send_nonce_and_authenticate(void);
+
+/**
+ * @brief 向 GD32 发送待升级固件的总大小。
+ * @param size 固件总字节数。
+ * @return 操作结果码。
+ */
+static ota_result_t send_firmware_size(uint32_t size);
+
+/**
+ * @brief 从 Flash 逐包读取固件并中继转发给 GD32。
+ * @return 操作结果码。
+ */
+static ota_result_t relay_packets_from_flash(void);
+
+/**
+ * @brief OTA 收尾阶段:发送结束命令并等待 GD32 确认。
+ * @return 操作结果码。
+ */
+static ota_result_t finish_ota(void);
+
+/**
+ * @brief 从 Flash 加载元数据
+ * @param meta 输出参数,加载后的元数据结构体。
+ * @return ESP_OK 成功,其他失败。
+ */
+esp_err_t meta_load(ota_meta_t *meta)
+{
+ if (!meta) return ESP_ERR_INVALID_ARG;
+
+ memset(meta, 0, sizeof(ota_meta_t));
+
+ FILE *f = fopen(META_FILE, "r");
+ if (!f) {
+ // 文件不存在是正常状态(首次使用,未刷过 GD32)
+ return ESP_OK;
+ }
+
+ char buf[256];
+ size_t n = fread(buf, 1, sizeof(buf) - 1, f);
+ buf[n] = '\0';
+ fclose(f);
+
+ cJSON *json = cJSON_Parse(buf);
+ if (!json) {
+ DEBUG_ERR("OTA", "元数据 JSON 解析失败");
+ return ESP_ERR_INVALID_RESPONSE;
+ }
+
+ cJSON *item = cJSON_GetObjectItem(json, "total_size");
+ if (item) meta->total_size = (uint32_t)item->valueint;
+
+ item = cJSON_GetObjectItem(json, "stored_size");
+ if (item) meta->stored_size = (uint32_t)item->valueint;
+
+ item = cJSON_GetObjectItem(json, "last_seq");
+ if (item) meta->last_seq = (uint32_t)item->valueint;
+
+ item = cJSON_GetObjectItem(json, "chunk_count");
+ if (item) meta->chunk_count = (uint32_t)item->valueint;
+
+ cJSON_Delete(json);
+ DEBUG_INFO("OTA", "元数据已加载: total=%u, stored=%u, seq=%u",
+ meta->total_size, meta->stored_size, meta->last_seq);
+ return ESP_OK;
+}
+
+/**
+ * @brief 保存元数据到 Flash
+ * @param meta 待保存的元数据结构体。
+ * @return ESP_OK 成功,其他失败。
+ */
+esp_err_t meta_save(const ota_meta_t *meta)
+{
+ if (!meta) return ESP_ERR_INVALID_ARG;
+
+ FILE *f = fopen(META_FILE, "w");
+ if (!f) {
+ DEBUG_ERR("OTA", "无法打开元数据文件写入");
+ return ESP_FAIL;
+ }
+
+ fprintf(f,
+ "{\n"
+ " \"total_size\": %" PRIu32 ",\n"
+ " \"stored_size\": %" PRIu32 ",\n"
+ " \"last_seq\": %" PRIu32 ",\n"
+ " \"chunk_count\": %" PRIu32 "\n"
+ "}",
+ meta->total_size, meta->stored_size, meta->last_seq, meta->chunk_count);
+
+ fclose(f);
+ DEBUG_INFO("OTA", "元数据已保存到 Flash");
+ return ESP_OK;
+}
+
+/**
+ * @brief 传输过程中更新元数据
+ * @param stored_size 已存储的固件大小(字节)。
+ * @param last_seq 最后成功的分块序号。
+ * @return ESP_OK 成功,其他失败。
+ */
+esp_err_t meta_update(uint32_t stored_size, uint32_t last_seq)
+{
+ ota_meta_t meta;
+ esp_err_t err = meta_load(&meta);
+ if (err != ESP_OK) return err;
+
+ meta.stored_size = stored_size;
+ meta.last_seq = last_seq;
+
+ return meta_save(&meta);
+}
+
+/**
+ * @brief OTA 完成后或中止时清除元数据
+ * @return ESP_OK 成功,其他失败。
+ */
+esp_err_t meta_clear(void)
+{
+ DEBUG_INFO("OTA", "清除 OTA 元数据");
+ ota_meta_t meta = {};
+ return meta_save(&meta);
+}
+
+/**
+ * @brief Flash 写入接口
+ * @param offset 分区内写入偏移。
+ * @param data 待写入的数据指针。
+ * @param len 数据长度(字节)。
+ * @return ESP_OK 成功,其他失败。
+ */
+esp_err_t flash_write_chunk(uint32_t offset, const uint8_t *data, size_t len)
+{
+ if (!g_gd32_partition) {
+ g_gd32_partition = esp_partition_find_first(
+ ESP_PARTITION_TYPE_DATA, ESP_PARTITION_SUBTYPE_DATA_SPIFFS, "gd32_ota");
+ if (!g_gd32_partition) {
+ DEBUG_ERR("OTA", "找不到 gd32_ota 分区");
+ return ESP_ERR_NOT_FOUND;
+ }
+ }
+
+ return esp_partition_write(g_gd32_partition, offset, data, len);
+}
+
+/**
+ * @brief 从 OTA 缓存分区读取指定偏移和长度的数据块。
+ * @param offset 分区内读取偏移。
+ * @param data 读取缓冲区指针。
+ * @param len 读取长度(字节)。
+ * @return ESP_OK 成功,其他失败。
+ */
+esp_err_t flash_read_chunk(uint32_t offset, uint8_t *data, size_t len)
+{
+ if (!g_gd32_partition) {
+ g_gd32_partition = esp_partition_find_first(
+ ESP_PARTITION_TYPE_DATA, ESP_PARTITION_SUBTYPE_DATA_SPIFFS, "gd32_ota");
+ if (!g_gd32_partition) {
+ DEBUG_ERR("OTA", "找不到 gd32_ota 分区");
+ return ESP_ERR_NOT_FOUND;
+ }
+ }
+
+ return esp_partition_read(g_gd32_partition, offset, data, len);
+}
+
+/**
+ * @brief 阶段1 - 发起与 GD32 的握手
+ * @return 操作结果码。
+ */
+static ota_result_t send_handshake_req(void)
+{
+ DEBUG_INFO("OTA", "=== 阶段1: 握手开始 ===");
+ DEBUG_INFO("OTA", "发送 HANDSHAKE_REQ (0x00 0xAC)");
+
+ uint8_t tx_frame[32];
+ uint8_t rx_frame[32];
+
+ uint8_t payload[] = {0x00, 0xAC};
+ frame_build(CMD_HANDSHAKE_REQ, payload, sizeof(payload), tx_frame);
+
+ /** @brief 当前 OTA 中继状态机状态。 */
+ g_relay_state = RELAY_HANDSHAKE_SENT;
+ DEBUG_INFO("OTA", "状态: RELAY_HANDSHAKE_SENT");
+
+ esp_err_t err = spi_sync_transfer(CMD_HANDSHAKE_REQ, payload, sizeof(payload),
+ rx_frame, sizeof(rx_frame), 3000);
+ if (err != ESP_OK) {
+ DEBUG_ERR("OTA", "HANDSHAKE_REQ 失败, err=%d", err);
+ /** @brief 当前 OTA 中继状态机状态。 */
+ g_relay_state = RELAY_ERROR;
+ return OTA_RESULT_ERR_TIMEOUT;
+ }
+
+ DEBUG_INFO("OTA", "握手请求已发送,等待 GD32 响应...");
+ vTaskDelay(pdMS_TO_TICKS(100));
+
+ return OTA_RESULT_OK;
+}
+
+/**
+ * @brief 发送随机数并与 GD32 相互认证
+ * @return 操作结果码。
+ */
+static ota_result_t send_nonce_and_authenticate(void)
+{
+ uint8_t tx_frame[64];
+ uint8_t rx_frame[64];
+ uint8_t nonce_server[16];
+
+ uint32_t timestamp = xTaskGetTickCount();
+ uint32_t mem_addr = (uint32_t)nonce_server;
+ uint32_t rand_seed = 0x12345678;
+
+ scramble_generate(timestamp, mem_addr, rand_seed, nonce_server);
+
+ DEBUG_INFO("OTA", "--- 认证步骤 1: 发送 CHALLENGE ---");
+ DEBUG_INFO("OTA", "发送 nonce_server (16B), timestamp=0x%08X", timestamp);
+ /** @brief 当前 OTA 中继状态机状态。 */
+ g_relay_state = RELAY_CHALLENGE_SENT;
+ DEBUG_INFO("OTA", "状态: RELAY_CHALLENGE_SENT");
+
+ frame_build(CMD_CHALLENGE, nonce_server, 16, tx_frame);
+
+ esp_err_t err = spi_sync_transfer(CMD_CHALLENGE, nonce_server, 16,
+ rx_frame, sizeof(rx_frame), 5000);
+ if (err != ESP_OK) {
+ DEBUG_ERR("OTA", "CHALLENGE 发送失败, err=%d", err);
+ /** @brief 当前 OTA 中继状态机状态。 */
+ g_relay_state = RELAY_ERROR;
+ return OTA_RESULT_ERR_TIMEOUT;
+ }
+
+ /** @brief 当前 OTA 中继状态机状态。 */
+ g_relay_state = RELAY_CHALLENGE_RESPOND;
+ DEBUG_INFO("OTA", "已收到 GD32 握手数据包");
+ DEBUG_INFO("OTA", "状态: RELAY_CHALLENGE_RESPOND");
+
+ // 发送 SESSION_KEY(带变换后的随机数)
+ uint8_t transformed_nonce[16];
+ memcpy(transformed_nonce, nonce_server, 16);
+
+ DEBUG_INFO("OTA", "--- 认证步骤 2: 发送 SESSION_KEY ---");
+ DEBUG_INFO("OTA", "发送变换后的随机数 (16B)");
+ /** @brief 当前 OTA 中继状态机状态。 */
+ g_relay_state = RELAY_SESSION_KEY_SENT;
+ DEBUG_INFO("OTA", "状态: RELAY_SESSION_KEY_SENT");
+
+ frame_build(CMD_SESSION_KEY, transformed_nonce, 16, tx_frame);
+ esp_err_t err2 = spi_sync_transfer(CMD_SESSION_KEY, transformed_nonce, 16,
+ rx_frame, sizeof(rx_frame), 3000);
+ if (err2 != ESP_OK) {
+ DEBUG_ERR("OTA", "SESSION_KEY 发送失败, err=%d", err2);
+ /** @brief 当前 OTA 中继状态机状态。 */
+ g_relay_state = RELAY_ERROR;
+ return OTA_RESULT_ERR_TIMEOUT;
+ }
+
+ /** @brief 当前 OTA 中继状态机状态。 */
+ g_relay_state = RELAY_AUTH_COMPLETE;
+ DEBUG_INFO("OTA", "状态: RELAY_AUTH_COMPLETE");
+ DEBUG_INFO("OTA", "=== 阶段1 认证完成: 安全通道已建立 ===");
+
+ uint8_t k_session[16] = {0};
+ session_key_init(&g_session_ctx, k_session);
+
+ return OTA_RESULT_OK;
+}
+
+/**
+ * @brief 向 GD32 发送固件总大小
+ * @param size 固件总字节数。
+ * @return 操作结果码。
+ */
+static ota_result_t send_firmware_size(uint32_t size)
+{
+ uint8_t tx_frame[32];
+ uint8_t rx_frame[32];
+
+ DEBUG_INFO("OTA", "=== 阶段2: 发送固件大小 ===");
+ DEBUG_INFO("OTA", "固件大小: %u 字节", size);
+
+ uint8_t size_payload[4];
+ size_payload[0] = (uint8_t)(size & 0xFF);
+ size_payload[1] = (uint8_t)((size >> 8) & 0xFF);
+ size_payload[2] = (uint8_t)((size >> 16) & 0xFF);
+ size_payload[3] = (uint8_t)((size >> 24) & 0xFF);
+
+ /** @brief 当前 OTA 中继状态机状态。 */
+ g_relay_state = RELAY_SIZE_SENT;
+ DEBUG_INFO("OTA", "状态: RELAY_SIZE_SENT");
+
+ frame_build(CMD_SEND_SIZE, size_payload, 4, tx_frame);
+ esp_err_t err = spi_sync_transfer(CMD_SEND_SIZE, size_payload, 4,
+ rx_frame, sizeof(rx_frame), 3000);
+ if (err != ESP_OK) {
+ DEBUG_ERR("OTA", "SEND_SIZE 失败, err=%d", err);
+ /** @brief 当前 OTA 中继状态机状态。 */
+ g_relay_state = RELAY_ERROR;
+ return OTA_RESULT_ERR_TIMEOUT;
+ }
+
+ DEBUG_INFO("OTA", "大小已发送: %u 字节,等待 GD32 确认...", size);
+ /** @brief 当前 OTA 中继状态机状态。 */
+ g_relay_state = RELAY_SIZE_ACK;
+ DEBUG_INFO("OTA", "状态: RELAY_SIZE_ACK");
+
+ return OTA_RESULT_OK;
+}
+
+/**
+ * @brief 从 Flash 向 GD32 转发存储的固件(48B/包)
+ * @return 操作结果码。
+ */
+static ota_result_t relay_packets_from_flash(void)
+{
+ DEBUG_INFO("OTA", "=== 阶段3: 从 Flash 转发固件 ===");
+
+ ota_meta_t meta;
+ if (meta_load(&meta) != ESP_OK) {
+ DEBUG_ERR("OTA", "元数据加载失败");
+ return OTA_RESULT_ERR_STORAGE;
+ }
+
+ uint32_t total_packets = (meta.total_size + 47) / 48;
+ uint32_t flash_offset = 0;
+ uint8_t tx_buf[64];
+ uint8_t rx_buf[32];
+ ota_result_t result = OTA_RESULT_OK;
+
+ DEBUG_INFO("OTA", "总包数: %u, 每包 48 字节", total_packets);
+ DEBUG_INFO("OTA", "状态: RELAY_RELAY_ACTIVE");
+ /** @brief 当前 OTA 中继状态机状态。 */
+ g_relay_state = RELAY_RELAY_ACTIVE;
+
+ for (uint32_t seq = 0; seq < total_packets; seq++) {
+ health_status_t health = health_check_before_ota();
+ if (health == HEALTH_MON_SHUTDOWN || health == HEALTH_MON_CRITICAL) {
+ DEBUG_ERR("OTA", "健康检查危险,中止 OTA");
+ return OTA_RESULT_ERR_HEALTH;
+ }
+
+ size_t chunk_size = 48;
+ if (seq == total_packets - 1) {
+ chunk_size = meta.total_size - (seq * 48);
+ }
+
+ esp_err_t err = flash_read_chunk(flash_offset, &tx_buf[2], chunk_size);
+ if (err != ESP_OK) {
+ DEBUG_ERR("OTA", "Flash 读取失败, offset=%u, err=%d", flash_offset, err);
+ return OTA_RESULT_ERR_STORAGE;
+ }
+
+ tx_buf[0] = (uint8_t)(seq & 0xFF);
+ tx_buf[1] = (uint8_t)((seq >> 8) & 0xFF);
+
+ uint16_t crc = crc16_modbus(tx_buf, 2 + chunk_size);
+ tx_buf[2 + chunk_size] = (uint8_t)(crc & 0xFF);
+ tx_buf[2 + chunk_size + 1] = (uint8_t)((crc >> 8) & 0xFF);
+
+ /** @brief 当前 OTA 中继状态机状态。 */
+ g_relay_state = RELAY_PACKET_SENT;
+
+ uint8_t cmd = (seq == total_packets - 1) ? CMD_DATA_END : CMD_DATA_PACKET;
+ size_t tx_len = 2 + chunk_size + 2;
+
+ err = spi_sync_transfer(cmd, tx_buf, tx_len, rx_buf, sizeof(rx_buf), 3000);
+ if (err != ESP_OK) {
+ DEBUG_WARN("OTA", "数据包 %u 发送失败, err=%d, 重试中...", seq, err);
+ result = OTA_RESULT_ERR_TIMEOUT;
+ break;
+ }
+
+ flash_offset += chunk_size;
+ /** @brief 当前 OTA 中继状态机状态。 */
+ g_relay_state = RELAY_PACKET_ACK_OK;
+
+ // 每 10% 记录一次进度
+ if (seq % (total_packets / 10 + 1) == 0) {
+ uint32_t progress = (seq * 100) / total_packets;
+ DEBUG_INFO("OTA", "转发进度: %u/%u 包 (%u%%)",
+ seq, total_packets, progress);
+ }
+ }
+
+ /** @brief 当前 OTA 中继状态机状态。 */
+ g_relay_state = RELAY_RECEIVE_DONE;
+ DEBUG_INFO("OTA", "状态: RELAY_RECEIVE_DONE");
+ return result;
+}
+
+/**
+ * @brief 结束 OTA - 发送 0x0A AC 并清理
+ * @return 操作结果码。
+ */
+static ota_result_t finish_ota(void)
+{
+ DEBUG_INFO("OTA", "=== 阶段4: 结束 OTA ===");
+ DEBUG_INFO("OTA", "发送 FINISH_OTA (0x0A 0xAC)");
+
+ uint8_t tx_frame[16];
+ uint8_t rx_frame[16];
+
+ uint8_t payload[] = {0x0A, 0xAC};
+ frame_build(CMD_FINISH_OTA, payload, sizeof(payload), tx_frame);
+
+ /** @brief 当前 OTA 中继状态机状态。 */
+ g_relay_state = RELAY_FINISH_SENT;
+ DEBUG_INFO("OTA", "状态: RELAY_FINISH_SENT");
+
+ esp_err_t err = spi_sync_transfer(CMD_FINISH_OTA, payload, sizeof(payload),
+ rx_frame, sizeof(rx_frame), 5000);
+ if (err != ESP_OK) {
+ DEBUG_ERR("OTA", "FINISH_OTA 失败, err=%d", err);
+ /** @brief 当前 OTA 中继状态机状态。 */
+ g_relay_state = RELAY_ERROR;
+ return OTA_RESULT_ERR_TIMEOUT;
+ }
+
+ DEBUG_INFO("OTA", "等待 GD32 跳转确认...");
+
+ session_key_clear(&g_session_ctx);
+ memset(&g_session_ctx, 0, sizeof(session_ctx_t));
+
+ /** @brief 当前 OTA 中继状态机状态。 */
+ g_relay_state = RELAY_DONE;
+ DEBUG_INFO("OTA", "状态: RELAY_DONE");
+ DEBUG_INFO("OTA", "=== OTA 完成 ===");
+
+ meta_clear();
+
+ return OTA_RESULT_OK;
+}
+
+/**
+ * @brief 启动完整 OTA 流程的主入口
+ * @return 操作结果码。
+ */
+ota_result_t ota_relay_start_handshake(void)
+{
+ ota_result_t res;
+
+ res = send_handshake_req();
+ if (res != OTA_RESULT_OK) return res;
+
+ res = send_nonce_and_authenticate();
+ if (res != OTA_RESULT_OK) return res;
+
+ ota_meta_t meta;
+ meta_load(&meta);
+ res = send_firmware_size(meta.total_size);
+ if (res != OTA_RESULT_OK) return res;
+
+ res = relay_packets_from_flash();
+ if (res != OTA_RESULT_OK) return res;
+
+ res = finish_ota();
+
+ return res;
+}
+
+/**
+ * @brief 从 Flash 向 GD32 转发固件
+ * @return 操作结果码。
+ */
+ota_result_t ota_relay_forward_firmware(void)
+{
+ ota_meta_t meta;
+ if (meta_load(&meta) != ESP_OK) {
+ return OTA_RESULT_ERR_STORAGE;
+ }
+
+ DEBUG_INFO("OTA", "开始固件转发: %u 字节, %u 块",
+ meta.total_size, meta.chunk_count);
+
+ ota_result_t res = send_handshake_req();
+ if (res != OTA_RESULT_OK) return res;
+
+ res = send_nonce_and_authenticate();
+ if (res != OTA_RESULT_OK) return res;
+
+ res = send_firmware_size(meta.total_size);
+ if (res != OTA_RESULT_OK) return res;
+
+ res = relay_packets_from_flash();
+ if (res != OTA_RESULT_OK) return res;
+
+ res = finish_ota();
+
+ return res;
+}
+
+/**
+ * @brief 获取当前状态
+ * @return 当前中继状态枚举值。
+ */
+relay_state_t ota_relay_get_state(void)
+{
+ /** @brief 当前 OTA 中继状态机状态。 */
+ return g_relay_state;
+}
+
+/**
+ * @brief 重置状态机
+ */
+void ota_relay_reset(void)
+{
+ /** @brief 当前 OTA 中继状态机状态。 */
+ g_relay_state = RELAY_IDLE;
+ session_key_clear(&g_session_ctx);
+}
+
+/**
+ * @brief OTA 中继 FreeRTOS 任务
+ * @param pvParameters FreeRTOS 任务参数,未使用。
+ */
+void ota_relay_task(void *pvParameters)
+{
+ DEBUG_INFO("OTA", "OTA 中继任务启动");
+
+ /** @brief OTA 命令分发队列,从 WiFi/BLE 任务接收指令。 */
+ g_ota_dispatch_queue = xQueueCreate(4, sizeof(uint32_t));
+ /** @brief OTA 结果回传队列,将执行结果返回给调用方。 */
+ g_ota_result_queue = xQueueCreate(4, sizeof(uint32_t));
+
+ uint32_t msg;
+ BaseType_t ret = xQueueReceive(g_ota_dispatch_queue, &msg, portMAX_DELAY);
+
+ if (ret == pdTRUE) {
+ DEBUG_INFO("OTA", "收到 OTA 调度信号,开始转发...");
+ ota_result_t result = ota_relay_forward_firmware();
+
+ uint32_t result_msg = (uint32_t)result;
+ xQueueSend(g_ota_result_queue, &result_msg, pdMS_TO_TICKS(100));
+
+ if (result == OTA_RESULT_OK) {
+ DEBUG_INFO("OTA", "OTA 转发成功完成");
+ } else {
+ DEBUG_ERR("OTA", "OTA 转发失败,错误码: %d", result);
+ }
+ }
+
+ DEBUG_INFO("OTA", "OTA 中继任务结束");
+ vTaskDelete(NULL);
+}
\ No newline at end of file
diff --git a/_assistant_backups/2026-05-05/wifi_ota.cpp.bak_2026-05-05_0451 b/_assistant_backups/2026-05-05/wifi_ota.cpp.bak_2026-05-05_0451
new file mode 100644
index 0000000..23657a3
--- /dev/null
+++ b/_assistant_backups/2026-05-05/wifi_ota.cpp.bak_2026-05-05_0451
@@ -0,0 +1,1369 @@
+/*
+ * wifi_ota.c - WiFi OTA 服务器实现
+ * T4.1 - T4.5
+ */
+#include
+#include "freertos/FreeRTOS.h"
+#include "freertos/task.h"
+#include "freertos/semphr.h"
+#include "esp_wifi.h"
+#include "esp_netif.h"
+#include "esp_http_server.h"
+#include "esp_system.h"
+#include "esp_chip_info.h"
+#include "esp_timer.h"
+#include "esp_ota_ops.h"
+#include "nvs_flash.h"
+#include "lwip/netdb.h"
+#include "esp_netif.h"
+#include "esp_netif_types.h"
+#include "esp_event.h"
+#include "esp_mac.h"
+
+#include "app_config.h"
+#include "wifi_ota.h"
+#include "ota_relay.h"
+#include "health_monitor.h"
+#include "monitor/debug_log.h"
+#include "spi_master.h"
+#include "cJSON.h"
+
+/** @brief 当前 WiFi 工作模式(AP 或 STA)。 */
+static ota_wifi_mode_t s_wifi_mode = OTA_WIFI_MODE_AP;
+/** @brief OTA 中继是否已触发标志。 */
+static bool s_relay_triggered = false;
+/** @brief STA 模式连接状态。 */
+static bool s_sta_connected = false;
+/** @brief STA 连接重试次数。 */
+static int s_sta_retry_count = 0;
+/** @brief STA 连接成功信号量。 */
+static SemaphoreHandle_t s_sta_connected_sem = NULL;
+/** @brief STA 获取 IP 信号量。 */
+static SemaphoreHandle_t s_sta_got_ip_sem = NULL;
+
+// ESP32 OTA 状态
+static bool s_esp32_ota_active = false;
+/** @brief ESP32 OTA 写入句柄。 */
+static esp_ota_handle_t s_esp32_ota_handle = 0;
+static const esp_partition_t *s_esp32_update_partition = NULL;
+/** @brief ESP32 OTA 已接收字节数。 */
+static size_t s_esp32_received = 0;
+
+// GD32 OTA 存储状态
+static bool s_gd32_storage_done = false;
+
+/** @brief HTTP 服务器句柄。 */
+static httpd_handle_t s_http_server = NULL;
+
+/**
+ * @brief 预先声明:为 STA 模式配置静态 IP、网关和子网掩码。
+ */
+static void wifi_set_static_ip(void);
+
+/**
+ * @brief 为 HTTP 响应添加跨域访问控制头。
+ * @param req HTTP 请求对象。
+ */
+static void http_set_cors_headers(httpd_req_t *req)
+{
+ httpd_resp_set_hdr(req, "Access-Control-Allow-Origin", "*");
+ httpd_resp_set_hdr(req, "Access-Control-Allow-Methods", "GET, POST, OPTIONS");
+ httpd_resp_set_hdr(req, "Access-Control-Allow-Headers", "Content-Type");
+ httpd_resp_set_hdr(req, "Access-Control-Max-Age", "600");
+}
+
+/**
+ * @brief 处理 HTTP OPTIONS 预检请求,返回 204。
+ * @param req HTTP 请求对象。
+ * @return ESP_OK。
+ */
+static esp_err_t http_options_handler(httpd_req_t *req)
+{
+ http_set_cors_headers(req);
+ httpd_resp_set_status(req, "204 No Content");
+ httpd_resp_send(req, NULL, 0);
+ return ESP_OK;
+}
+
+// ----------------------------------------
+// WiFi/IP 事件回调(事件驱动连接)
+// ----------------------------------------
+static void wifi_event_handler(void* arg, esp_event_base_t event_base,
+ int32_t event_id, void* event_data)
+{
+ if (event_base == WIFI_EVENT) {
+ switch (event_id) {
+ case WIFI_EVENT_STA_START:
+ DEBUG_INFO("WIFI", "STA 已启动,开始连接...");
+ esp_wifi_connect();
+ break;
+
+ case WIFI_EVENT_STA_CONNECTED:
+ DEBUG_INFO("WIFI", "STA 已关联到 AP");
+ /** @brief STA 模式连接状态。 */
+ s_sta_connected = true;
+ /** @brief STA 连接重试次数。 */
+ s_sta_retry_count = 0;
+ if (s_sta_connected_sem) {
+ xSemaphoreGive(s_sta_connected_sem);
+ }
+ break;
+
+ case WIFI_EVENT_STA_DISCONNECTED: {
+ wifi_event_sta_disconnected_t* disconn = (wifi_event_sta_disconnected_t*)event_data;
+ DEBUG_WARN("WIFI", "STA 断开, reason=%d, retry=%d", disconn->reason, s_sta_retry_count);
+ /** @brief STA 模式连接状态。 */
+ s_sta_connected = false;
+
+ if (s_sta_retry_count < WIFI_STA_MAX_RETRY) {
+ s_sta_retry_count++;
+ DEBUG_INFO("WIFI", "重连第 %d 次...", s_sta_retry_count);
+ esp_wifi_connect();
+ } else {
+ DEBUG_ERR("WIFI", "STA 重连失败 %d 次,放弃", s_sta_retry_count);
+ }
+ break;
+ }
+
+ case WIFI_EVENT_AP_STACONNECTED: {
+ wifi_event_ap_staconnected_t* event = (wifi_event_ap_staconnected_t*)event_data;
+ DEBUG_INFO("WIFI", "AP: 客户端 " MACSTR " 连入, AID=%d", MAC2STR(event->mac), event->aid);
+ break;
+ }
+
+ case WIFI_EVENT_AP_STADISCONNECTED: {
+ wifi_event_ap_stadisconnected_t* event = (wifi_event_ap_stadisconnected_t*)event_data;
+ DEBUG_INFO("WIFI", "AP: 客户端 " MACSTR " 离开, AID=%d, reason=%d",
+ MAC2STR(event->mac), event->aid, event->reason);
+ break;
+ }
+
+ default:
+ break;
+ }
+ } else if (event_base == IP_EVENT) {
+ switch (event_id) {
+ case IP_EVENT_STA_GOT_IP: {
+ ip_event_got_ip_t* event = (ip_event_got_ip_t*)event_data;
+ DEBUG_INFO("WIFI", "STA 获取 IP: " IPSTR, IP2STR(&event->ip_info.ip));
+ if (s_sta_got_ip_sem) {
+ xSemaphoreGive(s_sta_got_ip_sem);
+ }
+ break;
+ }
+
+ default:
+ break;
+ }
+ }
+}
+
+// ----------------------------------------
+// WiFi AP 模式初始化
+/**
+ * @brief 初始化 SoftAP 模式,供局域网 OTA 使用。
+ * @return ESP_OK 成功,其他失败。
+ */
+static esp_err_t wifi_init_ap(void)
+{
+ DEBUG_INFO("WIFI", "=== 初始化 WiFi AP 模式 ===");
+
+ /** @brief 当前 WiFi 工作模式(AP 或 STA)。 */
+ s_wifi_mode = OTA_WIFI_MODE_AP;
+
+ esp_netif_t *ap_netif = esp_netif_get_handle_from_ifkey("WIFI_AP_DEF");
+ if (ap_netif == NULL) {
+ ap_netif = esp_netif_create_default_wifi_ap();
+ }
+ if (ap_netif != NULL) {
+ esp_netif_dhcps_stop(ap_netif);
+
+ esp_netif_ip_info_t ip_info;
+ memset(&ip_info, 0, sizeof(ip_info));
+ // 方案 A:SoftAP 使用 192.168.31.0/24 网段
+ inet_pton(AF_INET, "192.168.31.1", &ip_info.ip);
+ inet_pton(AF_INET, "192.168.31.1", &ip_info.gw);
+ inet_pton(AF_INET, "255.255.255.0", &ip_info.netmask);
+ ESP_ERROR_CHECK(esp_netif_set_ip_info(ap_netif, &ip_info));
+
+ esp_netif_dhcps_start(ap_netif);
+ DEBUG_INFO("WIFI", "AP IP 已设置: 192.168.31.1");
+ }
+
+ wifi_config_t wifi_config = {};
+ memcpy(wifi_config.ap.ssid, WIFI_AP_SSID, strlen(WIFI_AP_SSID) + 1);
+ wifi_config.ap.ssid_len = strlen(WIFI_AP_SSID);
+ memcpy(wifi_config.ap.password, WIFI_AP_PASSWORD, strlen(WIFI_AP_PASSWORD) + 1);
+ wifi_config.ap.max_connection = WIFI_AP_MAX_CONNECT;
+ wifi_config.ap.authmode = WIFI_AUTH_WPA_WPA2_PSK;
+ wifi_config.ap.channel = WIFI_AP_CHANNEL;
+
+ DEBUG_INFO("WIFI", "AP SSID: %s, Channel: %d", WIFI_AP_SSID, WIFI_AP_CHANNEL);
+
+ ESP_ERROR_CHECK(esp_wifi_set_mode(WIFI_MODE_AP));
+ ESP_ERROR_CHECK(esp_wifi_set_config(WIFI_IF_AP, &wifi_config));
+ ESP_ERROR_CHECK(esp_wifi_start());
+
+ DEBUG_INFO("WIFI", "WiFi AP 已启动: %s", WIFI_AP_SSID);
+ DEBUG_INFO("WIFI", "AP IP 地址: 192.168.31.1");
+ DEBUG_INFO("WIFI", "AP 网关地址: 192.168.31.1");
+ DEBUG_INFO("WIFI", "最大连接数: %d", WIFI_AP_MAX_CONNECT);
+
+ return ESP_OK;
+}
+
+// ----------------------------------------
+// WiFi STA 模式初始化
+/**
+ * @brief 初始化 STA 模式并连接预配置的路由器。
+ * @return ESP_OK 成功,其他失败。
+ */
+static esp_err_t wifi_init_sta(void)
+{
+ DEBUG_INFO("WIFI", "=== 初始化 WiFi STA 模式 ===");
+
+ /** @brief 当前 WiFi 工作模式(AP 或 STA)。 */
+ s_wifi_mode = OTA_WIFI_MODE_STA;
+ /** @brief STA 连接重试次数。 */
+ s_sta_retry_count = 0;
+
+ // ★ 设置国家代码:中国,允许信道 1-13
+ // ESP32 默认国家是 US(只允许 1-11),中国路由可能用 12/13
+ wifi_country_t country = {};
+ memcpy(country.cc, "CN", 3);
+ country.schan = 1;
+ country.nchan = 13;
+ country.policy = WIFI_COUNTRY_POLICY_MANUAL;
+
+ wifi_config_t wifi_config = {};
+ wifi_config.sta.scan_method = WIFI_ALL_CHANNEL_SCAN;
+ wifi_config.sta.sort_method = WIFI_CONNECT_AP_BY_SIGNAL;
+ wifi_config.sta.threshold.authmode = WIFI_AUTH_WPA2_PSK;
+ wifi_config.sta.pmf_cfg.capable = true;
+ wifi_config.sta.pmf_cfg.required = false;
+ wifi_config.sta.listen_interval = 3;
+ memcpy(wifi_config.sta.ssid, WIFI_STA_SSID, strlen(WIFI_STA_SSID) + 1);
+ memcpy(wifi_config.sta.password, WIFI_STA_PASSWORD, strlen(WIFI_STA_PASSWORD) + 1);
+
+ DEBUG_INFO("WIFI", "STA SSID: %s (全信道扫描, CN 国家代码)", WIFI_STA_SSID);
+
+ ESP_ERROR_CHECK(esp_wifi_set_country(&country));
+ ESP_ERROR_CHECK(esp_wifi_set_mode(WIFI_MODE_STA));
+ ESP_ERROR_CHECK(esp_wifi_set_config(WIFI_IF_STA, &wifi_config));
+
+ wifi_set_static_ip();
+
+ ESP_ERROR_CHECK(esp_wifi_start());
+ ESP_ERROR_CHECK(esp_wifi_set_ps(WIFI_PS_NONE));
+
+ DEBUG_INFO("WIFI", "WiFi STA 启动, SSID: %s, 事件驱动连接...", WIFI_STA_SSID);
+
+ return ESP_OK;
+}
+
+/**
+ * @brief 等待 STA 连接成功并获取 IP(静态 IP 等 CONNECTED,DHCP 等 GOT_IP)
+ * @param ip_str 输出缓冲区,存放获取到的 IP 字符串。
+ * @param len 缓冲区长度。
+ * @return ESP_OK 连接成功,其他失败。
+ */
+static esp_err_t wifi_wait_sta_connected(char *ip_str, size_t len)
+{
+ SemaphoreHandle_t wait_sem = WIFI_STA_USE_STATIC_IP ? s_sta_connected_sem : s_sta_got_ip_sem;
+ const char *wait_name = WIFI_STA_USE_STATIC_IP ? "STA_CONNECTED" : "STA_GOT_IP";
+
+ if (wait_sem == NULL) {
+ DEBUG_ERR("WIFI", "%s 信号量未创建", wait_name);
+ return ESP_ERR_INVALID_STATE;
+ }
+
+ TickType_t timeout_ticks = pdMS_TO_TICKS(WIFI_STA_CONNECT_TIMEOUT_MS);
+ if (xSemaphoreTake(wait_sem, timeout_ticks) == pdTRUE) {
+ esp_netif_ip_info_t ip_info;
+ esp_netif_t *netif = esp_netif_get_handle_from_ifkey("WIFI_STA_DEF");
+ if (netif != NULL && esp_netif_get_ip_info(netif, &ip_info) == ESP_OK) {
+ if (ip_str) {
+ snprintf(ip_str, len, IPSTR, IP2STR(&ip_info.ip));
+ }
+ DEBUG_INFO("WIFI", "%s 等待成功, 当前 IP: " IPSTR, wait_name, IP2STR(&ip_info.ip));
+ }
+ /** @brief STA 模式连接状态。 */
+ s_sta_connected = true;
+ return ESP_OK;
+ }
+
+ DEBUG_ERR("WIFI", "等待 %s 超时 (%d ms), 重连次数: %d", wait_name, WIFI_STA_CONNECT_TIMEOUT_MS, s_sta_retry_count);
+ return ESP_ERR_TIMEOUT;
+}
+
+/**
+ * @brief 设置 STA 静态 IP
+ */
+static void wifi_set_static_ip(void)
+{
+ if (WIFI_STA_USE_STATIC_IP) {
+ esp_netif_t *sta_netif = esp_netif_get_handle_from_ifkey("WIFI_STA_DEF");
+ if (sta_netif) {
+ DEBUG_INFO("WIFI", "STA 使用静态 IP 模式,停止 DHCP Client");
+ ESP_ERROR_CHECK(esp_netif_dhcpc_stop(sta_netif));
+
+ esp_netif_ip_info_t info;
+ memset(&info, 0, sizeof(info));
+ inet_pton(AF_INET, WIFI_STA_STATIC_IP, &info.ip);
+ inet_pton(AF_INET, WIFI_STA_STATIC_GW, &info.gw);
+ inet_pton(AF_INET, WIFI_STA_STATIC_NETMASK, &info.netmask);
+ ESP_ERROR_CHECK(esp_netif_set_ip_info(sta_netif, &info));
+ DEBUG_INFO("WIFI", "静态 IP 已设置: %s", WIFI_STA_STATIC_IP);
+
+ // 设置 DNS
+ esp_netif_dns_info_t dns_info;
+ memset(&dns_info, 0, sizeof(dns_info));
+ inet_pton(AF_INET, WIFI_STA_STATIC_DNS, &dns_info.ip.u_addr.ip4.addr);
+ ESP_ERROR_CHECK(esp_netif_set_dns_info(sta_netif, ESP_NETIF_DNS_MAIN, &dns_info));
+ DEBUG_INFO("WIFI", "DNS 已设置: %s", WIFI_STA_STATIC_DNS);
+ }
+ } else {
+ DEBUG_INFO("WIFI", "使用 DHCP 获取 IP");
+ }
+}
+
+// ----------------------------------------
+// ESP32 OTA 内部函数
+/**
+ * @brief 初始化 ESP32 自身的 OTA 写入流程。
+ * @param total_size
+ * @return ESP_OK 成功,其他失败。
+ */
+static esp_err_t esp32_ota_begin(size_t total_size)
+{
+ const esp_partition_t *boot_partition = esp_ota_get_running_partition();
+ DEBUG_INFO("OTA", "当前启动分区: %s, size=%u, address=0x%lx",
+ boot_partition->label, boot_partition->size, boot_partition->address);
+
+ s_esp32_update_partition = esp_partition_find_first(
+ ESP_PARTITION_TYPE_APP, ESP_PARTITION_SUBTYPE_APP_OTA_0, NULL);
+ if (s_esp32_update_partition == NULL ||
+ s_esp32_update_partition->address == boot_partition->address) {
+ s_esp32_update_partition = esp_partition_find_first(
+ ESP_PARTITION_TYPE_APP, ESP_PARTITION_SUBTYPE_APP_OTA_1, NULL);
+ }
+
+ if (s_esp32_update_partition == NULL) {
+ DEBUG_ERR("OTA", "找不到可用的 OTA 分区");
+ return ESP_FAIL;
+ }
+
+ DEBUG_INFO("OTA", "OTA 目标分区: %s, size=%u, address=0x%lx",
+ s_esp32_update_partition->label,
+ s_esp32_update_partition->size,
+ s_esp32_update_partition->address);
+
+ if (total_size > s_esp32_update_partition->size) {
+ DEBUG_ERR("OTA", "固件太大: %u > 分区大小: %u",
+ total_size, s_esp32_update_partition->size);
+ return ESP_FAIL;
+ }
+
+ esp_err_t err = esp_ota_begin(s_esp32_update_partition, total_size, &s_esp32_ota_handle);
+ if (err != ESP_OK) {
+ DEBUG_ERR("OTA", "esp_ota_begin 失败: %s", esp_err_to_name(err));
+ /** @brief ESP32 OTA 目标分区。 */
+ s_esp32_update_partition = NULL;
+ return err;
+ }
+
+ /** @brief ESP32 OTA 已接收字节数。 */
+ s_esp32_received = 0;
+ /** @brief ESP32 自身 OTA 写入进行中标志。 */
+ s_esp32_ota_active = true;
+ DEBUG_INFO("OTA", "ESP32 OTA 开始,目标分区: %s", s_esp32_update_partition->label);
+ return ESP_OK;
+}
+
+/**
+ * @brief 将接收到的数据块写入 ESP32 OTA 分区。
+ * @param data
+ * @param len
+ * @return ESP_OK 成功,其他失败。
+ */
+static esp_err_t esp32_ota_write(const uint8_t *data, size_t len)
+{
+ if (!s_esp32_ota_active || s_esp32_update_partition == NULL) {
+ return ESP_ERR_INVALID_STATE;
+ }
+ esp_err_t err = esp_ota_write(s_esp32_ota_handle, data, len);
+ if (err == ESP_OK) {
+ s_esp32_received += len;
+ }
+ return err;
+}
+
+/**
+ * @brief HTTP POST /ota/update - 完成 ESP32 OTA 并重启
+ * @param req
+ * @return
+ */
+static esp_err_t esp32_ota_update_handler(httpd_req_t *req)
+{
+ (void)req;
+ http_set_cors_headers(req);
+ if (!s_esp32_ota_active) {
+ httpd_resp_send_err(req, HTTPD_400_BAD_REQUEST, "OTA not started");
+ return ESP_FAIL;
+ }
+
+ DEBUG_INFO("OTA", "ESP32 OTA 完成, 共 %u 字节, 设置启动分区...", s_esp32_received);
+
+ esp_err_t err = esp_ota_end(s_esp32_ota_handle);
+ if (err != ESP_OK) {
+ DEBUG_ERR("OTA", "esp_ota_end 失败: %s", esp_err_to_name(err));
+ /** @brief ESP32 自身 OTA 写入进行中标志。 */
+ s_esp32_ota_active = false;
+ httpd_resp_send_err(req, HTTPD_500_INTERNAL_SERVER_ERROR, esp_err_to_name(err));
+ return ESP_FAIL;
+ }
+
+ err = esp_ota_set_boot_partition(s_esp32_update_partition);
+ if (err != ESP_OK) {
+ DEBUG_ERR("OTA", "esp_ota_set_boot_partition 失败: %s", esp_err_to_name(err));
+ /** @brief ESP32 自身 OTA 写入进行中标志。 */
+ s_esp32_ota_active = false;
+ httpd_resp_send_err(req, HTTPD_500_INTERNAL_SERVER_ERROR, esp_err_to_name(err));
+ return ESP_FAIL;
+ }
+
+ /** @brief ESP32 自身 OTA 写入进行中标志。 */
+ s_esp32_ota_active = false;
+ DEBUG_INFO("OTA", "ESP32 OTA 完成,重启中...");
+
+ const char resp[] = "{\"status\": \"ok\", \"message\": \"OTA finished, rebooting\"}";
+ httpd_resp_set_type(req, "application/json");
+ httpd_resp_send(req, resp, strlen(resp));
+
+ vTaskDelay(pdMS_TO_TICKS(500));
+ esp_restart();
+ return ESP_OK;
+}
+
+/**
+ * @brief HTTP GET /ota/update - 查询 ESP32 OTA 状态
+ * @param req
+ * @return
+ */
+static esp_err_t esp32_ota_status_handler(httpd_req_t *req)
+{
+ http_set_cors_headers(req);
+ char resp[128];
+ if (s_esp32_ota_active) {
+ snprintf(resp, sizeof(resp),
+ "{\"active\": true, \"received\": %u, \"partition\": \"%s\"}",
+ (unsigned int)s_esp32_received,
+ s_esp32_update_partition ? s_esp32_update_partition->label : "null");
+ } else {
+ snprintf(resp, sizeof(resp), "{\"active\": false}");
+ }
+ httpd_resp_set_type(req, "application/json");
+ httpd_resp_send(req, resp, strlen(resp));
+ return ESP_OK;
+}
+
+// 从 HTTP 流中读取完整的 JSON 头(以 } 结尾)
+/**
+ * @brief 返回 JSON 字符串长度,失败返回 0
+ * @param req
+ * @param buf
+ * @param buf_size
+ * @return
+ */
+static int recv_json_header(httpd_req_t *req, char *buf, int buf_size)
+{
+ int total = 0;
+ int depth = 1; // 开头的 '{' 已被 peek 消费,从 1 开始
+ while (total < buf_size - 1) {
+ int ret = httpd_req_recv(req, buf + total, 1); // 逐字节读,精确找到 JSON 结尾
+ if (ret <= 0) return 0;
+ total += ret;
+ buf[total] = '\0';
+ if (buf[total - 1] == '{') depth++;
+ else if (buf[total - 1] == '}') {
+ depth--;
+ if (depth == 0) return total; // JSON 头读完
+ }
+ }
+ return 0; // JSON 头太长,超出缓冲区
+}
+
+// ----------------------------------------
+// 统一 /ota/upload 处理器(自动识别目标)
+/**
+ * @brief HTTP 文件上传处理器,接收固件并写入 Flash。
+ * @param req
+ * @return ESP_OK 成功,其他失败。
+ */
+static esp_err_t ota_upload_handler(httpd_req_t *req)
+{
+ http_set_cors_headers(req);
+ DEBUG_INFO("WIFI", "=== HTTP: POST /ota/upload ===");
+ DEBUG_INFO("WIFI", "收到固件上传请求, 内容长度: %d 字节", req->content_len);
+
+ // 预读 1 字节判断是 JSON 还是 binary
+ char peek;
+ int peek_ret = httpd_req_recv(req, &peek, 1);
+ if (peek_ret <= 0) {
+ httpd_resp_send_err(req, HTTPD_500_INTERNAL_SERVER_ERROR, "接收失败");
+ return ESP_FAIL;
+ }
+
+ bool is_esp32_ota = false;
+ uint32_t total_size = 0;
+ int json_header_len = 0;
+
+ if (peek == '{') {
+ // JSON 模式:逐字节读完 JSON 头
+ char json_buf[256];
+ json_buf[0] = peek;
+ json_header_len = recv_json_header(req, json_buf + 1, sizeof(json_buf) - 1);
+ if (json_header_len > 0) {
+ json_header_len++; // 加上已读的 peek 字节
+ json_buf[json_header_len] = '\0';
+
+ cJSON *root = cJSON_Parse(json_buf);
+ if (root) {
+ cJSON *type_item = cJSON_GetObjectItem(root, "type");
+ if (type_item && cJSON_IsString(type_item) &&
+ strcmp(cJSON_GetStringValue(type_item), "esp32") == 0) {
+ is_esp32_ota = true;
+ DEBUG_INFO("WIFI", "检测到 ESP32 OTA, JSON 头长度: %d 字节", json_header_len);
+ }
+ cJSON *size_item = cJSON_GetObjectItem(root, "total_size");
+ if (size_item) {
+ total_size = (uint32_t)cJSON_GetNumberValue(size_item);
+ }
+ cJSON_Delete(root);
+ }
+ }
+ }
+
+ if (is_esp32_ota) {
+ // --- ESP32 OTA ---
+ if (total_size == 0) {
+ httpd_resp_send_err(req, HTTPD_400_BAD_REQUEST, "missing total_size");
+ return ESP_FAIL;
+ }
+
+ esp_err_t err = esp32_ota_begin(total_size);
+ if (err != ESP_OK) {
+ httpd_resp_send_err(req, HTTPD_500_INTERNAL_SERVER_ERROR, "OTA begin failed");
+ return ESP_FAIL;
+ }
+
+ // 堆分配缓冲区,避免栈溢出(HTTP server 栈仅 8KB)
+ uint8_t *fw_buf = (uint8_t *)malloc(1024);
+ if (!fw_buf) {
+ httpd_resp_send_err(req, HTTPD_500_INTERNAL_SERVER_ERROR, "内存分配失败");
+ return ESP_FAIL;
+ }
+
+ size_t total_firmware_written = 0;
+ size_t total_received = json_header_len; // 已消费的字节数
+ int last_percent = 0;
+
+ while (total_received < req->content_len) {
+ size_t to_read = req->content_len - total_received;
+ if (to_read > 1024) to_read = 1024;
+
+ int ret = httpd_req_recv(req, (char *)fw_buf, to_read);
+ if (ret <= 0) break;
+
+ err = esp32_ota_write(fw_buf, ret);
+ if (err != ESP_OK) {
+ DEBUG_ERR("WIFI", "OTA write 失败");
+ break;
+ }
+ total_received += ret;
+ total_firmware_written += ret;
+
+ if (total_size > 0) {
+ int percent = (int)((total_firmware_written * 100) / total_size);
+ if (percent / 10 > last_percent / 10) {
+ last_percent = percent;
+ DEBUG_INFO("WIFI", "ESP32 OTA 进度: %u / %u (%d%%)",
+ (unsigned int)total_firmware_written, total_size, percent);
+ }
+ }
+ }
+
+ free(fw_buf);
+ DEBUG_INFO("WIFI", "ESP32 OTA 存储完成: %u 字节 (跳过 JSON 头 %d 字节)", (unsigned int)s_esp32_received, json_header_len);
+ const char resp[] = "{\"status\": \"ok\", \"message\": \"stored, call POST /ota/update to finish\", \"target\": \"esp32\"}";
+ httpd_resp_set_type(req, "application/json");
+ httpd_resp_send(req, resp, strlen(resp));
+ return ESP_OK;
+ }
+
+ // --- GD32 OTA(原逻辑)---
+ if (req->content_len > OTA_MAX_FIRMWARE_SIZE) {
+ DEBUG_ERR("WIFI", "固件太大: %d > %d", req->content_len, OTA_MAX_FIRMWARE_SIZE);
+ httpd_resp_send_err(req, HTTPD_500_INTERNAL_SERVER_ERROR, "固件太大");
+ return ESP_FAIL;
+ }
+
+ uint8_t *buf = (uint8_t *)malloc(OTA_BLOCK_SIZE);
+ int received = 0;
+
+ // peek 字节是 GD32 数据的第一字节,先放回缓冲区
+ uint8_t gd32_header[6];
+ gd32_header[0] = (uint8_t)peek;
+ int header_read = 1;
+ // 读剩余 5 字节凑齐 6 字节 GD32 头
+ if (req->content_len >= 6) {
+ int need = 6 - header_read;
+ int got = httpd_req_recv(req, (char *)gd32_header + header_read, need);
+ if (got > 0) header_read += got;
+ }
+
+ // 先处理已读的 header
+ if (header_read >= 6) {
+ uint16_t seq = gd32_header[0] | (gd32_header[1] << 8);
+ uint32_t fw_size = gd32_header[2] | (gd32_header[3] << 8) | (gd32_header[4] << 16) | (gd32_header[5] << 24);
+ if (seq == 0) {
+ ota_meta_t meta = { .total_size = fw_size, .stored_size = 0, .last_seq = 0,
+ .chunk_count = (fw_size + 4095) / 4096, .checksum = {0} };
+ meta_save(&meta);
+ DEBUG_INFO("WIFI", "开始 GD32 固件存储: %u 字节", fw_size);
+ }
+ }
+
+ // 继续读剩余数据
+ while (received + header_read < req->content_len) {
+ size_t to_read = req->content_len - received - header_read;
+ if (to_read > OTA_BLOCK_SIZE) to_read = OTA_BLOCK_SIZE;
+
+ int ret = httpd_req_recv(req, (char *)buf, to_read);
+ if (ret <= 0) break;
+
+ uint32_t flash_offset = ((received + header_read) / OTA_BLOCK_SIZE) * OTA_BLOCK_SIZE;
+ esp_err_t err = flash_write_chunk(flash_offset, buf, ret);
+ if (err == ESP_OK) {
+ meta_update(flash_offset + ret, (received + header_read) / OTA_BLOCK_SIZE);
+ }
+ received += ret;
+
+ uint32_t progress = ((received + header_read) * 100) / req->content_len;
+ if (progress % 10 == 0) {
+ DEBUG_INFO("WIFI", "GD32 上传进度: %d / %d (%u%%)",
+ received + header_read, req->content_len, progress);
+ }
+ }
+
+ free(buf);
+ DEBUG_INFO("WIFI", "GD32 固件存储完成: %d 字节", received + header_read);
+
+ ota_meta_t meta;
+ meta_load(&meta);
+ if (meta.stored_size >= meta.total_size) {
+ /** @brief GD32 固件存储完成标志。 */
+ s_gd32_storage_done = true;
+ DEBUG_INFO("WIFI", "GD32 固件已存储,触发转发...");
+ wifi_ota_signal_relay();
+ }
+
+ const char resp[] = "{\"status\": \"ok\", \"progress\": 100, \"target\": \"gd32\"}";
+ httpd_resp_set_type(req, "application/json");
+ httpd_resp_send(req, resp, strlen(resp));
+ return ESP_OK;
+}
+
+/**
+ * @brief HTTP POST /ota/relay 处理器
+ * @param req
+ * @return
+ */
+static esp_err_t ota_relay_trigger_handler(httpd_req_t *req)
+{
+ (void)req;
+ http_set_cors_headers(req);
+ DEBUG_INFO("WIFI", "=== HTTP: POST /ota/relay ===");
+ DEBUG_INFO("WIFI", "收到触发 OTA 中继请求");
+ wifi_ota_signal_relay();
+
+ const char resp[] = "{\"status\": \"ok\", \"message\": \"OTA relay triggered\"}";
+ httpd_resp_set_type(req, "application/json");
+ httpd_resp_send(req, resp, strlen(resp));
+
+ return ESP_OK;
+}
+
+/**
+ * @brief HTTP GET /ota/status 处理器
+ * @param req
+ * @return
+ */
+static esp_err_t ota_status_handler(httpd_req_t *req)
+{
+ http_set_cors_headers(req);
+ DEBUG_INFO("WIFI", "=== HTTP: GET /ota/status ===");
+
+ ota_meta_t meta;
+ meta_load(&meta);
+
+ char resp[256];
+ int len = snprintf(resp, sizeof(resp),
+ "{\"total_size\": %lu, \"stored_size\": %lu, \"last_seq\": %lu, "
+ "\"storage_done\": %s, \"relay_state\": %d}",
+ (unsigned long)meta.total_size, (unsigned long)meta.stored_size,
+ (unsigned long)meta.last_seq,
+ s_gd32_storage_done ? "true" : "false",
+ g_relay_state);
+
+ DEBUG_INFO("WIFI", "OTA 状态: total=%lu, stored=%lu, done=%s",
+ (unsigned long)meta.total_size, (unsigned long)meta.stored_size,
+ s_gd32_storage_done ? "true" : "false");
+
+ httpd_resp_set_type(req, "application/json");
+ httpd_resp_send(req, resp, len);
+
+ return ESP_OK;
+}
+
+/**
+ * @brief HTTP GET /health 处理器
+ * @param req
+ * @return
+ */
+static esp_err_t health_handler(httpd_req_t *req)
+{
+ http_set_cors_headers(req);
+ DEBUG_INFO("WIFI", "=== HTTP: GET /health ===");
+
+ health_state_t state = health_get_current();
+ uint32_t free_heap = esp_get_free_heap_size();
+
+ char resp[128];
+ int len = snprintf(resp, sizeof(resp),
+ "{\"health\": %d, \"free_heap\": %lu}",
+ state, (unsigned long)free_heap);
+
+ DEBUG_INFO("WIFI", "健康状态: %d, 空闲堆: %lu", state, (unsigned long)free_heap);
+
+ httpd_resp_set_type(req, "application/json");
+ httpd_resp_send(req, resp, len);
+
+ return ESP_OK;
+}
+
+/**
+ * @brief HTTP GET /logs - 拉取并清空日志缓冲区
+ * @param req
+ * @return
+ */
+static esp_err_t logs_handler(httpd_req_t *req)
+{
+ http_set_cors_headers(req);
+
+ int json_len = 0;
+ char *json = debug_log_ring_drain_json(&json_len);
+
+ if (json && json_len > 0) {
+ httpd_resp_set_type(req, "application/json");
+ httpd_resp_send(req, json, json_len);
+ free(json);
+ } else {
+ httpd_resp_set_type(req, "application/json");
+ httpd_resp_send(req, "[]", 2);
+ }
+ return ESP_OK;
+}
+
+// HTTP POST /can/send 处理器
+// 上位机发送 CAN 帧,经 ESP32 转发到 GD32
+// 请求体: {"ch": 0, "id": 123456, "data": "0102030405060708", "dlc": 8}
+/**
+ * @brief data 为 hex 字符串(可不填,自动用 dlc 决定实际发送长度)
+ * @param req
+ * @return
+ */
+static esp_err_t can_send_handler(httpd_req_t *req)
+{
+ http_set_cors_headers(req);
+ char buf[256] = {0};
+ int ret = httpd_req_recv(req, buf, sizeof(buf) - 1);
+ if (ret <= 0) {
+ httpd_resp_send_err(req, HTTPD_400_BAD_REQUEST, "接收数据失败");
+ return ESP_FAIL;
+ }
+ buf[ret] = '\0';
+
+ cJSON *root = cJSON_Parse(buf);
+ if (!root) {
+ httpd_resp_send_err(req, HTTPD_400_BAD_REQUEST, "JSON 解析失败");
+ return ESP_FAIL;
+ }
+
+ cJSON *ch_item = cJSON_GetObjectItem(root, "ch");
+ cJSON *id_item = cJSON_GetObjectItem(root, "id");
+ cJSON *data_item = cJSON_GetObjectItem(root, "data");
+ cJSON *dlc_item = cJSON_GetObjectItem(root, "dlc");
+
+ if (!ch_item || !id_item) {
+ cJSON_Delete(root);
+ httpd_resp_send_err(req, HTTPD_400_BAD_REQUEST, "缺少 ch 或 id 字段");
+ return ESP_FAIL;
+ }
+
+ can_channel_t ch = (can_channel_t)cJSON_GetNumberValue(ch_item);
+ uint32_t ext_id = (uint32_t)cJSON_GetNumberValue(id_item);
+ uint8_t dlc = dlc_item ? (uint8_t)cJSON_GetNumberValue(dlc_item) : 0;
+
+ uint8_t tx_data[CAN_MAX_DLC] = {0};
+ uint8_t tx_dlc = 0;
+
+ if (data_item && cJSON_IsString(data_item)) {
+ const char *hex_str = cJSON_GetStringValue(data_item);
+ size_t hex_len = strlen(hex_str);
+ for (size_t i = 0; i < hex_len / 2 && i < CAN_MAX_DLC; i++) {
+ char byte_str[3] = { hex_str[i * 2], hex_str[i * 2 + 1], '\0' };
+ tx_data[i] = (uint8_t)strtol(byte_str, NULL, 16);
+ tx_dlc++;
+ }
+ }
+
+ if (tx_dlc == 0 && dlc > 0) {
+ tx_dlc = (dlc > CAN_MAX_DLC) ? CAN_MAX_DLC : dlc;
+ }
+
+ cJSON_Delete(root);
+
+ // 发送 CAN 帧到 GD32
+ uint8_t resp_data[CAN_MAX_DLC] = {0};
+ int resp_len = gd32_can_send(ch, ext_id, tx_data, tx_dlc,
+ resp_data, sizeof(resp_data), 1000);
+
+ char resp[512];
+ if (resp_len >= 0) {
+ // 将响应 data 转为 hex 字符串
+ char resp_hex[CAN_MAX_DLC * 2 + 1] = {0};
+ for (int i = 0; i < resp_len; i++) {
+ sprintf(&resp_hex[i * 2], "%02X", resp_data[i]);
+ }
+ snprintf(resp, sizeof(resp),
+ "{\"status\": \"ok\", \"ch\": %d, \"id\": %lu, \"sent\": %d, \"recv_len\": %d, \"recv\": \"%s\"}",
+ ch, (unsigned long)ext_id, tx_dlc, resp_len, resp_hex);
+ } else {
+ snprintf(resp, sizeof(resp),
+ "{\"status\": \"error\", \"code\": %d}", resp_len);
+ }
+
+ httpd_resp_set_type(req, "application/json");
+ httpd_resp_send(req, resp, strlen(resp));
+ return ESP_OK;
+}
+
+/**
+ * @brief HTTP GET /can/status 处理器
+ * @param req
+ * @return
+ */
+static esp_err_t can_status_handler(httpd_req_t *req)
+{
+ http_set_cors_headers(req);
+ char resp[128];
+ snprintf(resp, sizeof(resp),
+ "{\"channels\": 3, \"max_dlc\": %d, \"spi_init\": %s}",
+ CAN_MAX_DLC,
+ g_spi_device != NULL ? "true" : "false");
+ httpd_resp_set_type(req, "application/json");
+ httpd_resp_send(req, resp, strlen(resp));
+ return ESP_OK;
+}
+
+/**
+ * @brief HTTP GET /wifi/status 处理器
+ * @param req
+ * @return
+ */
+static esp_err_t wifi_status_handler(httpd_req_t *req)
+{
+ http_set_cors_headers(req);
+ char resp[256];
+ char ip_str[16] = "0.0.0.0";
+ int8_t rssi = -127;
+
+ if (s_wifi_mode == OTA_WIFI_MODE_STA && s_sta_connected) {
+ esp_netif_ip_info_t ip_info;
+ esp_netif_t *sta_netif = esp_netif_get_handle_from_ifkey("WIFI_STA_DEF");
+ if (sta_netif && esp_netif_get_ip_info(sta_netif, &ip_info) == ESP_OK) {
+ snprintf(ip_str, sizeof(ip_str), IPSTR, IP2STR(&ip_info.ip));
+ }
+ wifi_ap_record_t ap_info;
+ if (esp_wifi_sta_get_ap_info(&ap_info) == ESP_OK) {
+ rssi = ap_info.rssi;
+ }
+ }
+
+ int len = snprintf(resp, sizeof(resp),
+ "{\"mode\": \"%s\", \"ip\": \"%s\", \"rssi\": %d, \"connected\": %s}",
+ s_wifi_mode == OTA_WIFI_MODE_AP ? "AP" : "STA",
+ ip_str, rssi,
+ (s_wifi_mode == OTA_WIFI_MODE_STA) ? (s_sta_connected ? "true" : "false") : "N/A");
+
+ httpd_resp_set_type(req, "application/json");
+ httpd_resp_send(req, resp, len);
+
+ return ESP_OK;
+}
+
+/**
+ * @brief HTTP GET /system/status 处理器
+ * @param req
+ * @return
+ */
+static esp_err_t system_status_handler(httpd_req_t *req)
+{
+ http_set_cors_headers(req);
+
+ char ip_str[16] = "0.0.0.0";
+ int8_t rssi = -127;
+ bool connected = false;
+
+ if (s_wifi_mode == OTA_WIFI_MODE_STA && s_sta_connected) {
+ esp_netif_ip_info_t ip_info;
+ esp_netif_t *sta_netif = esp_netif_get_handle_from_ifkey("WIFI_STA_DEF");
+ if (sta_netif && esp_netif_get_ip_info(sta_netif, &ip_info) == ESP_OK) {
+ snprintf(ip_str, sizeof(ip_str), IPSTR, IP2STR(&ip_info.ip));
+ }
+ wifi_ap_record_t ap_info;
+ if (esp_wifi_sta_get_ap_info(&ap_info) == ESP_OK) {
+ rssi = ap_info.rssi;
+ }
+ connected = true;
+ }
+
+ ota_meta_t meta;
+ memset(&meta, 0, sizeof(meta));
+ meta_load(&meta);
+
+ char resp[640];
+ int len = snprintf(resp, sizeof(resp),
+ "{\"version\": \"%s\", \"health\": %d, \"free_heap\": %lu, \"min_free_heap\": %lu, \"temperature\": %.1f, \"voltage\": %.2f, \"wifi\": {\"mode\": \"%s\", \"ip\": \"%s\", \"rssi\": %d, \"connected\": %s}, \"ota\": {\"total_size\": %lu, \"stored_size\": %lu, \"storage_done\": %s, \"relay_state\": %d}}",
+ FW_VERSION,health_get_current(),
+ (unsigned long)esp_get_free_heap_size(),
+ (unsigned long)esp_get_minimum_free_heap_size(),
+ (double)health_get_last_temperature(),
+ (double)health_get_last_voltage(),
+ s_wifi_mode == OTA_WIFI_MODE_AP ? "AP" : "STA",
+ ip_str,
+ rssi,
+ connected ? "true" : "false",
+ (unsigned long)meta.total_size,
+ (unsigned long)meta.stored_size,
+ s_gd32_storage_done ? "true" : "false",
+ g_relay_state);
+
+ httpd_resp_set_type(req, "application/json");
+ httpd_resp_send(req, resp, len);
+ return ESP_OK;
+}
+
+// ----------------------------------------
+// 启动 HTTP OTA 服务器
+/**
+ * @brief 启动 HTTP OTA 服务器,注册各接口路由。
+ * @return ESP_OK 成功,其他失败。
+ */
+static esp_err_t http_ota_server_start(void)
+{
+ httpd_config_t config = HTTPD_DEFAULT_CONFIG();
+ config.server_port = HTTP_SERVER_PORT;
+ config.stack_size = HTTP_SERVER_STACK_SIZE;
+ config.max_uri_handlers = 32; // 现有业务端点 + 对应 OPTIONS 预检
+
+ DEBUG_INFO("WIFI", "=== 启动 HTTP OTA 服务器 ===");
+ DEBUG_INFO("WIFI", "HTTP 服务器端口: %d", config.server_port);
+
+ esp_err_t err = httpd_start(&s_http_server, &config);
+ if (err != ESP_OK) {
+ DEBUG_ERR("WIFI", "HTTP 服务器启动失败: %s", esp_err_to_name(err));
+ return err;
+ }
+
+ httpd_uri_t upload_uri = {
+ .uri = "/ota/upload",
+ .method = HTTP_POST,
+ .handler = ota_upload_handler,
+ .user_ctx = NULL
+ };
+ httpd_register_uri_handler(s_http_server, &upload_uri);
+ httpd_uri_t upload_options_uri = {
+ .uri = "/ota/upload",
+ .method = HTTP_OPTIONS,
+ .handler = http_options_handler,
+ .user_ctx = NULL
+ };
+ httpd_register_uri_handler(s_http_server, &upload_options_uri);
+
+ httpd_uri_t relay_uri = {
+ .uri = "/ota/relay",
+ .method = HTTP_POST,
+ .handler = ota_relay_trigger_handler,
+ .user_ctx = NULL
+ };
+ httpd_register_uri_handler(s_http_server, &relay_uri);
+ httpd_uri_t relay_options_uri = {
+ .uri = "/ota/relay",
+ .method = HTTP_OPTIONS,
+ .handler = http_options_handler,
+ .user_ctx = NULL
+ };
+ httpd_register_uri_handler(s_http_server, &relay_options_uri);
+
+ httpd_uri_t status_uri = {
+ .uri = "/ota/status",
+ .method = HTTP_GET,
+ .handler = ota_status_handler,
+ .user_ctx = NULL
+ };
+ httpd_register_uri_handler(s_http_server, &status_uri);
+ httpd_uri_t status_options_uri = {
+ .uri = "/ota/status",
+ .method = HTTP_OPTIONS,
+ .handler = http_options_handler,
+ .user_ctx = NULL
+ };
+ httpd_register_uri_handler(s_http_server, &status_options_uri);
+
+ httpd_uri_t esp32_update_uri = {
+ .uri = "/ota/update",
+ .method = HTTP_POST,
+ .handler = esp32_ota_update_handler,
+ .user_ctx = NULL
+ };
+ httpd_register_uri_handler(s_http_server, &esp32_update_uri);
+ httpd_uri_t esp32_update_options_uri = {
+ .uri = "/ota/update",
+ .method = HTTP_OPTIONS,
+ .handler = http_options_handler,
+ .user_ctx = NULL
+ };
+ httpd_register_uri_handler(s_http_server, &esp32_update_options_uri);
+
+ httpd_uri_t esp32_status_uri = {
+ .uri = "/ota/esp32_status",
+ .method = HTTP_GET,
+ .handler = esp32_ota_status_handler,
+ .user_ctx = NULL
+ };
+ httpd_register_uri_handler(s_http_server, &esp32_status_uri);
+ httpd_uri_t esp32_status_options_uri = {
+ .uri = "/ota/esp32_status",
+ .method = HTTP_OPTIONS,
+ .handler = http_options_handler,
+ .user_ctx = NULL
+ };
+ httpd_register_uri_handler(s_http_server, &esp32_status_options_uri);
+
+ httpd_uri_t health_uri = {
+ .uri = "/health",
+ .method = HTTP_GET,
+ .handler = health_handler,
+ .user_ctx = NULL
+ };
+ httpd_register_uri_handler(s_http_server, &health_uri);
+ httpd_uri_t health_options_uri = {
+ .uri = "/health",
+ .method = HTTP_OPTIONS,
+ .handler = http_options_handler,
+ .user_ctx = NULL
+ };
+ httpd_register_uri_handler(s_http_server, &health_options_uri);
+
+ httpd_uri_t logs_uri = {
+ .uri = "/logs",
+ .method = HTTP_GET,
+ .handler = logs_handler,
+ .user_ctx = NULL
+ };
+ httpd_register_uri_handler(s_http_server, &logs_uri);
+ httpd_uri_t logs_options_uri = {
+ .uri = "/logs",
+ .method = HTTP_OPTIONS,
+ .handler = http_options_handler,
+ .user_ctx = NULL
+ };
+ httpd_register_uri_handler(s_http_server, &logs_options_uri);
+
+ httpd_uri_t wifi_status_uri = {
+ .uri = "/wifi/status",
+ .method = HTTP_GET,
+ .handler = wifi_status_handler,
+ .user_ctx = NULL
+ };
+ httpd_register_uri_handler(s_http_server, &wifi_status_uri);
+ httpd_uri_t wifi_status_options_uri = {
+ .uri = "/wifi/status",
+ .method = HTTP_OPTIONS,
+ .handler = http_options_handler,
+ .user_ctx = NULL
+ };
+ httpd_register_uri_handler(s_http_server, &wifi_status_options_uri);
+
+ httpd_uri_t system_status_uri = {
+ .uri = "/system/status",
+ .method = HTTP_GET,
+ .handler = system_status_handler,
+ .user_ctx = NULL
+ };
+ httpd_register_uri_handler(s_http_server, &system_status_uri);
+ httpd_uri_t system_status_options_uri = {
+ .uri = "/system/status",
+ .method = HTTP_OPTIONS,
+ .handler = http_options_handler,
+ .user_ctx = NULL
+ };
+ httpd_register_uri_handler(s_http_server, &system_status_options_uri);
+
+ // CAN 帧转发
+ httpd_uri_t can_send_uri = {
+ .uri = "/can/send",
+ .method = HTTP_POST,
+ .handler = can_send_handler,
+ .user_ctx = NULL
+ };
+ httpd_register_uri_handler(s_http_server, &can_send_uri);
+ httpd_uri_t can_send_options_uri = {
+ .uri = "/can/send",
+ .method = HTTP_OPTIONS,
+ .handler = http_options_handler,
+ .user_ctx = NULL
+ };
+ httpd_register_uri_handler(s_http_server, &can_send_options_uri);
+
+ httpd_uri_t can_status_uri = {
+ .uri = "/can/status",
+ .method = HTTP_GET,
+ .handler = can_status_handler,
+ .user_ctx = NULL
+ };
+ httpd_register_uri_handler(s_http_server, &can_status_uri);
+ httpd_uri_t can_status_options_uri = {
+ .uri = "/can/status",
+ .method = HTTP_OPTIONS,
+ .handler = http_options_handler,
+ .user_ctx = NULL
+ };
+ httpd_register_uri_handler(s_http_server, &can_status_options_uri);
+
+ DEBUG_INFO("WIFI", "HTTP 服务器已启动");
+ DEBUG_INFO("WIFI", "注册端点: /ota/upload, /ota/relay, /ota/update, /ota/status, /ota/esp32_status, /health, /wifi/status, /system/status, /can/send, /can/status");
+
+ return ESP_OK;
+}
+
+// ----------------------------------------
+// 公开 API
+/**
+ * @brief 通知 OTA 中继开始向 GD32 转发固件。
+ */
+void wifi_ota_signal_relay(void)
+{
+ if (s_relay_triggered) {
+ DEBUG_WARN("WIFI", "转发已经触发过");
+ return;
+ }
+
+ /** @brief OTA 中继是否已触发标志。 */
+ s_relay_triggered = true;
+
+ if (g_ota_dispatch_queue) {
+ uint32_t msg = 1;
+ xQueueSend(g_ota_dispatch_queue, &msg, pdMS_TO_TICKS(100));
+ DEBUG_INFO("WIFI", "OTA 调度信号已发送到中继任务");
+ }
+}
+
+/**
+ * @brief 获取当前 WiFi 工作模式(AP 或 STA)。
+ * @return 当前 WiFi 模式枚举值。
+ */
+ota_wifi_mode_t wifi_get_mode(void)
+{
+ /** @brief 当前 WiFi 工作模式(AP 或 STA)。 */
+ return s_wifi_mode;
+}
+
+/**
+ * @brief 获取当前 WiFi 信号强度。
+ * @return 信号强度(dBm),未连接时返回无效值。
+ */
+int8_t wifi_get_rssi(void)
+{
+ if (s_wifi_mode != OTA_WIFI_MODE_STA || !s_sta_connected) {
+ return -127;
+ }
+ wifi_ap_record_t ap_info;
+ if (esp_wifi_sta_get_ap_info(&ap_info) == ESP_OK) {
+ return ap_info.rssi;
+ }
+ return -127;
+}
+
+/**
+ * @brief 判断 STA 是否已连接到上级 WiFi。
+ * @return true 已连接,false 未连接。
+ */
+bool wifi_is_connected(void)
+{
+ /** @brief STA 模式连接状态。 */
+ return s_sta_connected;
+}
+
+// ----------------------------------------
+// ★ WiFi 早期初始化(在 app_main 调度器启动前调用)
+// 包括:NVS → netif → event handler → WiFi driver → 配置 → connect
+// 返回:true = STA 模式已启动(正在连接),false = 需要切 AP
+/**
+ * @brief 在调度器启动前初始化 WiFi 并尝试 STA 连接。
+ * @return true STA 初始化中,false 需回退 AP。
+ */
+bool wifi_early_init(void)
+{
+ DEBUG_INFO("WIFI", "=== WiFi 早期初始化 (调度器启动前) ===");
+
+ esp_err_t err = nvs_flash_init();
+ if (err == ESP_ERR_NVS_NO_FREE_PAGES || err == ESP_ERR_NVS_NEW_VERSION_FOUND) {
+ // NVS 分区损坏或版本不匹配,擦除后重试
+ DEBUG_WARN("WIFI", "NVS 分区异常 (%s),擦除后重试", esp_err_to_name(err));
+ ESP_ERROR_CHECK(nvs_flash_erase());
+ err = nvs_flash_init();
+ }
+ if (err != ESP_OK) {
+ DEBUG_ERR("WIFI", "NVS 初始化失败: %s", esp_err_to_name(err));
+ } else {
+ DEBUG_INFO("WIFI", "NVS 初始化成功");
+ }
+
+ /** @brief STA 连接成功信号量。 */
+ s_sta_connected_sem = xSemaphoreCreateBinary();
+ /** @brief STA 获取 IP 信号量。 */
+ s_sta_got_ip_sem = xSemaphoreCreateBinary();
+ if (!s_sta_connected_sem || !s_sta_got_ip_sem) {
+ DEBUG_ERR("WIFI", "创建信号量失败");
+ }
+
+ // 注册 WiFi/IP 事件回调(必须在 esp_wifi_init 之前)
+ ESP_ERROR_CHECK(esp_event_handler_register(WIFI_EVENT, ESP_EVENT_ANY_ID, &wifi_event_handler, NULL));
+ ESP_ERROR_CHECK(esp_event_handler_register(IP_EVENT, ESP_EVENT_ANY_ID, &wifi_event_handler, NULL));
+ DEBUG_INFO("WIFI", "WiFi/IP 事件回调已注册");
+
+ esp_netif_t *sta_netif = esp_netif_get_handle_from_ifkey("WIFI_STA_DEF");
+ if (sta_netif == NULL) {
+ sta_netif = esp_netif_create_default_wifi_sta();
+ if (sta_netif == NULL) {
+ DEBUG_ERR("WIFI", "创建 STA netif 失败");
+ } else {
+ DEBUG_INFO("WIFI", "STA netif 已创建");
+ }
+ } else {
+ DEBUG_INFO("WIFI", "STA netif 已存在,跳过创建");
+ }
+
+ wifi_init_config_t wifi_cfg = WIFI_INIT_CONFIG_DEFAULT();
+ ESP_ERROR_CHECK(esp_wifi_init(&wifi_cfg));
+ DEBUG_INFO("WIFI", "WiFi driver 初始化成功");
+
+ // STA 优先,失败时返回 false 让 task 切 AP
+ if (CONFIG_WIFI_MODE == 1) {
+ DEBUG_INFO("WIFI", "工作模式: STA (优先)");
+ err = wifi_init_sta();
+ if (err == ESP_OK) {
+ // 连接已触发(STA_START 事件驱动),返回 true
+ // 实际连接状态由 wifi_ota_task 等待确认
+ return true;
+ }
+ DEBUG_WARN("WIFI", "STA 初始化失败,将切换 AP");
+ return false;
+ } else {
+ DEBUG_INFO("WIFI", "工作模式: AP");
+ wifi_init_ap();
+ return false; // AP 模式,不需要等待连接
+ }
+}
+
+// ----------------------------------------
+// WiFi OTA FreeRTOS 任务
+// WiFi 初始化已在 app_main 中通过 wifi_early_init() 完成
+// 此任务负责:等待连接结果 → 启动 HTTP 服务器 → 状态监控
+/**
+ * @brief WiFi OTA FreeRTOS 任务,提供 HTTP 上传和中继触发。
+ * @param pvParameters FreeRTOS 任务参数,未使用。
+ */
+void wifi_ota_task(void *pvParameters)
+{
+ (void)pvParameters;
+
+ DEBUG_INFO("WIFI", "=== WiFi OTA 任务启动 ===");
+
+ // 等待 STA 连接结果(wifi_early_init 已触发连接)
+ if (wifi_get_mode() == OTA_WIFI_MODE_STA) {
+ char ip_str[16] = {0};
+ esp_err_t err = wifi_wait_sta_connected(ip_str, sizeof(ip_str));
+ if (err == ESP_OK) {
+ DEBUG_INFO("WIFI", "STA 已连接, IP: %s", ip_str);
+ } else {
+ DEBUG_WARN("WIFI", "STA 连接超时,切换到 AP 模式...");
+ esp_wifi_stop(); // 先停 STA,再切 AP
+ /** @brief 当前 WiFi 工作模式(AP 或 STA)。 */
+ s_wifi_mode = OTA_WIFI_MODE_AP;
+ wifi_init_ap();
+ }
+ }
+ // AP 模式:wifi_early_init 已完成,无需额外操作
+
+ esp_err_t err = http_ota_server_start();
+ if (err != ESP_OK) {
+ DEBUG_ERR("WIFI", "HTTP 服务器启动失败");
+ }
+
+ // ★ WiFi 已连接,UDP 从这里开始能通
+ // 等待网络栈稳定(ARP 表建立、路由就绪),避免首批 UDP 包丢失
+ vTaskDelay(pdMS_TO_TICKS(500));
+
+ // 启动摘要,发两遍确保上位机收到(第一包可能触发 ARP 被丢)
+ esp_chip_info_t chip;
+ esp_chip_info(&chip);
+ for (int i = 0; i < 2; i++) {
+ DEBUG_INFO("WIFI", "========================================");
+ DEBUG_INFO("WIFI", "[启动摘要] ESP32-C3 OTA Relay v%s", FW_VERSION);
+ DEBUG_INFO("WIFI", "[启动摘要] Chip: %s, Rev %d", CONFIG_IDF_TARGET, chip.revision);
+ DEBUG_INFO("WIFI", "[启动摘要] Free heap: %lu bytes", (unsigned long)esp_get_free_heap_size());
+ DEBUG_INFO("WIFI", "[启动摘要] WiFi: %s", wifi_get_mode() == OTA_WIFI_MODE_STA ? "STA" : "AP");
+ DEBUG_INFO("WIFI", "[启动摘要] NVS + WiFi driver + netif + event handler 初始化完成");
+ DEBUG_INFO("WIFI", "[启动摘要] HTTP 服务器端口: %d", HTTP_SERVER_PORT);
+ DEBUG_INFO("WIFI", "========================================");
+ DEBUG_INFO("WIFI", "=== WiFi OTA 服务器就绪 ===");
+ if (i == 0) vTaskDelay(pdMS_TO_TICKS(200));
+ }
+
+ // WiFi 状态监控
+ // 每秒一次循环:每 10 秒检查 RSSI
+ uint32_t tick = 0;
+ while (1) {
+ tick++;
+
+ // 每 10 秒检查一次 RSSI
+ if (tick % 10 == 0) {
+ int8_t rssi = wifi_get_rssi();
+ if (rssi != -127) {
+ health_update_from_wifi_rssi(rssi);
+
+ if (rssi < WIFI_RSSI_CRITICAL) {
+ DEBUG_WARN("WIFI", "WiFi RSSI 危险: %d dBm (阈值: %d)", rssi, WIFI_RSSI_CRITICAL);
+ } else if (rssi < WIFI_RSSI_WARNING) {
+ DEBUG_WARN("WIFI", "WiFi RSSI 较弱: %d dBm", rssi);
+ }
+ }
+ }
+
+ vTaskDelay(pdMS_TO_TICKS(1000));
+ }
+}
diff --git a/_assistant_backups/2026-05-05/wifi_ota.cpp.bak_2026-05-05_0510 b/_assistant_backups/2026-05-05/wifi_ota.cpp.bak_2026-05-05_0510
new file mode 100644
index 0000000..3cdfea9
--- /dev/null
+++ b/_assistant_backups/2026-05-05/wifi_ota.cpp.bak_2026-05-05_0510
@@ -0,0 +1,1368 @@
+/*
+ * wifi_ota.c - WiFi OTA 服务器实现
+ * T4.1 - T4.5
+ */
+#include
+#include "freertos/FreeRTOS.h"
+#include "freertos/task.h"
+#include "freertos/semphr.h"
+#include "esp_wifi.h"
+#include "esp_netif.h"
+#include "esp_http_server.h"
+#include "esp_system.h"
+#include "esp_chip_info.h"
+#include "esp_timer.h"
+#include "esp_ota_ops.h"
+#include "nvs_flash.h"
+#include "lwip/netdb.h"
+#include "esp_netif.h"
+#include "esp_netif_types.h"
+#include "esp_event.h"
+#include "esp_mac.h"
+
+#include "app_config.h"
+#include "wifi_ota.h"
+#include "ota_relay.h"
+#include "health_monitor.h"
+#include "monitor/debug_log.h"
+#include "spi_master.h"
+#include "cJSON.h"
+
+/** @brief 当前 WiFi 工作模式(AP 或 STA)。 */
+static ota_wifi_mode_t s_wifi_mode = OTA_WIFI_MODE_AP;
+/** @brief OTA 中继是否已触发标志。 */
+static bool s_relay_triggered = false;
+/** @brief STA 模式连接状态。 */
+static bool s_sta_connected = false;
+/** @brief STA 连接重试次数。 */
+static int s_sta_retry_count = 0;
+/** @brief STA 连接成功信号量。 */
+static SemaphoreHandle_t s_sta_connected_sem = NULL;
+/** @brief STA 获取 IP 信号量。 */
+static SemaphoreHandle_t s_sta_got_ip_sem = NULL;
+
+// ESP32 OTA 状态
+static bool s_esp32_ota_active = false;
+/** @brief ESP32 OTA 写入句柄。 */
+static esp_ota_handle_t s_esp32_ota_handle = 0;
+static const esp_partition_t *s_esp32_update_partition = NULL;
+/** @brief ESP32 OTA 已接收字节数。 */
+static size_t s_esp32_received = 0;
+
+// GD32 OTA 存储状态
+static bool s_gd32_storage_done = false;
+
+/** @brief HTTP 服务器句柄。 */
+static httpd_handle_t s_http_server = NULL;
+
+/**
+ * @brief 预先声明:为 STA 模式配置静态 IP、网关和子网掩码。
+ */
+static void wifi_set_static_ip(void);
+
+/**
+ * @brief 为 HTTP 响应添加跨域访问控制头。
+ * @param req HTTP 请求对象。
+ */
+static void http_set_cors_headers(httpd_req_t *req)
+{
+ httpd_resp_set_hdr(req, "Access-Control-Allow-Origin", "*");
+ httpd_resp_set_hdr(req, "Access-Control-Allow-Methods", "GET, POST, OPTIONS");
+ httpd_resp_set_hdr(req, "Access-Control-Allow-Headers", "Content-Type");
+ httpd_resp_set_hdr(req, "Access-Control-Max-Age", "600");
+}
+
+/**
+ * @brief 处理 HTTP OPTIONS 预检请求,返回 204。
+ * @param req HTTP 请求对象。
+ * @return ESP_OK。
+ */
+static esp_err_t http_options_handler(httpd_req_t *req)
+{
+ http_set_cors_headers(req);
+ httpd_resp_set_status(req, "204 No Content");
+ httpd_resp_send(req, NULL, 0);
+ return ESP_OK;
+}
+
+// ----------------------------------------
+// WiFi/IP 事件回调(事件驱动连接)
+// ----------------------------------------
+static void wifi_event_handler(void* arg, esp_event_base_t event_base,
+ int32_t event_id, void* event_data)
+{
+ if (event_base == WIFI_EVENT) {
+ switch (event_id) {
+ case WIFI_EVENT_STA_START:
+ DEBUG_INFO("WIFI", "STA 已启动,开始连接...");
+ esp_wifi_connect();
+ break;
+
+ case WIFI_EVENT_STA_CONNECTED:
+ DEBUG_INFO("WIFI", "STA 已关联到 AP");
+ /** @brief STA 模式连接状态。 */
+ s_sta_connected = true;
+ /** @brief STA 连接重试次数。 */
+ s_sta_retry_count = 0;
+ if (s_sta_connected_sem) {
+ xSemaphoreGive(s_sta_connected_sem);
+ }
+ break;
+
+ case WIFI_EVENT_STA_DISCONNECTED: {
+ wifi_event_sta_disconnected_t* disconn = (wifi_event_sta_disconnected_t*)event_data;
+ DEBUG_WARN("WIFI", "STA 断开, reason=%d, retry=%d", disconn->reason, s_sta_retry_count);
+ /** @brief STA 模式连接状态。 */
+ s_sta_connected = false;
+
+ if (s_sta_retry_count < WIFI_STA_MAX_RETRY) {
+ s_sta_retry_count++;
+ DEBUG_INFO("WIFI", "重连第 %d 次...", s_sta_retry_count);
+ esp_wifi_connect();
+ } else {
+ DEBUG_ERR("WIFI", "STA 重连失败 %d 次,放弃", s_sta_retry_count);
+ }
+ break;
+ }
+
+ case WIFI_EVENT_AP_STACONNECTED: {
+ wifi_event_ap_staconnected_t* event = (wifi_event_ap_staconnected_t*)event_data;
+ DEBUG_INFO("WIFI", "AP: 客户端 " MACSTR " 连入, AID=%d", MAC2STR(event->mac), event->aid);
+ break;
+ }
+
+ case WIFI_EVENT_AP_STADISCONNECTED: {
+ wifi_event_ap_stadisconnected_t* event = (wifi_event_ap_stadisconnected_t*)event_data;
+ DEBUG_INFO("WIFI", "AP: 客户端 " MACSTR " 离开, AID=%d, reason=%d",
+ MAC2STR(event->mac), event->aid, event->reason);
+ break;
+ }
+
+ default:
+ break;
+ }
+ } else if (event_base == IP_EVENT) {
+ switch (event_id) {
+ case IP_EVENT_STA_GOT_IP: {
+ ip_event_got_ip_t* event = (ip_event_got_ip_t*)event_data;
+ DEBUG_INFO("WIFI", "STA 获取 IP: " IPSTR, IP2STR(&event->ip_info.ip));
+ if (s_sta_got_ip_sem) {
+ xSemaphoreGive(s_sta_got_ip_sem);
+ }
+ break;
+ }
+
+ default:
+ break;
+ }
+ }
+}
+
+// ----------------------------------------
+// WiFi AP 模式初始化
+/**
+ * @brief 初始化 SoftAP 模式,供局域网 OTA 使用。
+ * @return ESP_OK 成功,其他失败。
+ */
+static esp_err_t wifi_init_ap(void)
+{
+ DEBUG_INFO("WIFI", "=== 初始化 WiFi AP 模式 ===");
+
+ /** @brief 当前 WiFi 工作模式(AP 或 STA)。 */
+ s_wifi_mode = OTA_WIFI_MODE_AP;
+
+ esp_netif_t *ap_netif = esp_netif_get_handle_from_ifkey("WIFI_AP_DEF");
+ if (ap_netif == NULL) {
+ ap_netif = esp_netif_create_default_wifi_ap();
+ }
+ if (ap_netif != NULL) {
+ esp_netif_dhcps_stop(ap_netif);
+
+ esp_netif_ip_info_t ip_info;
+ memset(&ip_info, 0, sizeof(ip_info));
+ // 方案 A:SoftAP 使用 192.168.31.0/24 网段
+ inet_pton(AF_INET, "192.168.31.1", &ip_info.ip);
+ inet_pton(AF_INET, "192.168.31.1", &ip_info.gw);
+ inet_pton(AF_INET, "255.255.255.0", &ip_info.netmask);
+ ESP_ERROR_CHECK(esp_netif_set_ip_info(ap_netif, &ip_info));
+
+ esp_netif_dhcps_start(ap_netif);
+ DEBUG_INFO("WIFI", "AP IP 已设置: 192.168.31.1");
+ }
+
+ wifi_config_t wifi_config = {};
+ memcpy(wifi_config.ap.ssid, WIFI_AP_SSID, strlen(WIFI_AP_SSID) + 1);
+ wifi_config.ap.ssid_len = strlen(WIFI_AP_SSID);
+ memcpy(wifi_config.ap.password, WIFI_AP_PASSWORD, strlen(WIFI_AP_PASSWORD) + 1);
+ wifi_config.ap.max_connection = WIFI_AP_MAX_CONNECT;
+ wifi_config.ap.authmode = WIFI_AUTH_WPA_WPA2_PSK;
+ wifi_config.ap.channel = WIFI_AP_CHANNEL;
+
+ DEBUG_INFO("WIFI", "AP SSID: %s, Channel: %d", WIFI_AP_SSID, WIFI_AP_CHANNEL);
+
+ ESP_ERROR_CHECK(esp_wifi_set_mode(WIFI_MODE_AP));
+ ESP_ERROR_CHECK(esp_wifi_set_config(WIFI_IF_AP, &wifi_config));
+ ESP_ERROR_CHECK(esp_wifi_start());
+
+ DEBUG_INFO("WIFI", "WiFi AP 已启动: %s", WIFI_AP_SSID);
+ DEBUG_INFO("WIFI", "AP IP 地址: 192.168.31.1");
+ DEBUG_INFO("WIFI", "AP 网关地址: 192.168.31.1");
+ DEBUG_INFO("WIFI", "最大连接数: %d", WIFI_AP_MAX_CONNECT);
+
+ return ESP_OK;
+}
+
+// ----------------------------------------
+// WiFi STA 模式初始化
+/**
+ * @brief 初始化 STA 模式并连接预配置的路由器。
+ * @return ESP_OK 成功,其他失败。
+ */
+static esp_err_t wifi_init_sta(void)
+{
+ DEBUG_INFO("WIFI", "=== 初始化 WiFi STA 模式 ===");
+
+ /** @brief 当前 WiFi 工作模式(AP 或 STA)。 */
+ s_wifi_mode = OTA_WIFI_MODE_STA;
+ /** @brief STA 连接重试次数。 */
+ s_sta_retry_count = 0;
+
+ // ★ 设置国家代码:中国,允许信道 1-13
+ // ESP32 默认国家是 US(只允许 1-11),中国路由可能用 12/13
+ wifi_country_t country = {};
+ memcpy(country.cc, "CN", 3);
+ country.schan = 1;
+ country.nchan = 13;
+ country.policy = WIFI_COUNTRY_POLICY_MANUAL;
+
+ wifi_config_t wifi_config = {};
+ wifi_config.sta.scan_method = WIFI_ALL_CHANNEL_SCAN;
+ wifi_config.sta.sort_method = WIFI_CONNECT_AP_BY_SIGNAL;
+ wifi_config.sta.threshold.authmode = WIFI_AUTH_WPA2_PSK;
+ wifi_config.sta.pmf_cfg.capable = true;
+ wifi_config.sta.pmf_cfg.required = false;
+ wifi_config.sta.listen_interval = 3;
+ memcpy(wifi_config.sta.ssid, WIFI_STA_SSID, strlen(WIFI_STA_SSID) + 1);
+ memcpy(wifi_config.sta.password, WIFI_STA_PASSWORD, strlen(WIFI_STA_PASSWORD) + 1);
+
+ DEBUG_INFO("WIFI", "STA SSID: %s (全信道扫描, CN 国家代码)", WIFI_STA_SSID);
+
+ ESP_ERROR_CHECK(esp_wifi_set_country(&country));
+ ESP_ERROR_CHECK(esp_wifi_set_mode(WIFI_MODE_STA));
+ ESP_ERROR_CHECK(esp_wifi_set_config(WIFI_IF_STA, &wifi_config));
+
+ wifi_set_static_ip();
+
+ ESP_ERROR_CHECK(esp_wifi_start());
+ ESP_ERROR_CHECK(esp_wifi_set_ps(WIFI_PS_NONE));
+
+ DEBUG_INFO("WIFI", "WiFi STA 启动, SSID: %s, 事件驱动连接...", WIFI_STA_SSID);
+
+ return ESP_OK;
+}
+
+/**
+ * @brief 等待 STA 连接成功并获取 IP(静态 IP 等 CONNECTED,DHCP 等 GOT_IP)
+ * @param ip_str 输出缓冲区,存放获取到的 IP 字符串。
+ * @param len 缓冲区长度。
+ * @return ESP_OK 连接成功,其他失败。
+ */
+static esp_err_t wifi_wait_sta_connected(char *ip_str, size_t len)
+{
+ SemaphoreHandle_t wait_sem = WIFI_STA_USE_STATIC_IP ? s_sta_connected_sem : s_sta_got_ip_sem;
+ const char *wait_name = WIFI_STA_USE_STATIC_IP ? "STA_CONNECTED" : "STA_GOT_IP";
+
+ if (wait_sem == NULL) {
+ DEBUG_ERR("WIFI", "%s 信号量未创建", wait_name);
+ return ESP_ERR_INVALID_STATE;
+ }
+
+ TickType_t timeout_ticks = pdMS_TO_TICKS(WIFI_STA_CONNECT_TIMEOUT_MS);
+ if (xSemaphoreTake(wait_sem, timeout_ticks) == pdTRUE) {
+ esp_netif_ip_info_t ip_info;
+ esp_netif_t *netif = esp_netif_get_handle_from_ifkey("WIFI_STA_DEF");
+ if (netif != NULL && esp_netif_get_ip_info(netif, &ip_info) == ESP_OK) {
+ if (ip_str) {
+ snprintf(ip_str, len, IPSTR, IP2STR(&ip_info.ip));
+ }
+ DEBUG_INFO("WIFI", "%s 等待成功, 当前 IP: " IPSTR, wait_name, IP2STR(&ip_info.ip));
+ }
+ /** @brief STA 模式连接状态。 */
+ s_sta_connected = true;
+ return ESP_OK;
+ }
+
+ DEBUG_ERR("WIFI", "等待 %s 超时 (%d ms), 重连次数: %d", wait_name, WIFI_STA_CONNECT_TIMEOUT_MS, s_sta_retry_count);
+ return ESP_ERR_TIMEOUT;
+}
+
+/**
+ * @brief 设置 STA 静态 IP
+ */
+static void wifi_set_static_ip(void)
+{
+ if (WIFI_STA_USE_STATIC_IP) {
+ esp_netif_t *sta_netif = esp_netif_get_handle_from_ifkey("WIFI_STA_DEF");
+ if (sta_netif) {
+ DEBUG_INFO("WIFI", "STA 使用静态 IP 模式,停止 DHCP Client");
+ ESP_ERROR_CHECK(esp_netif_dhcpc_stop(sta_netif));
+
+ esp_netif_ip_info_t info;
+ memset(&info, 0, sizeof(info));
+ inet_pton(AF_INET, WIFI_STA_STATIC_IP, &info.ip);
+ inet_pton(AF_INET, WIFI_STA_STATIC_GW, &info.gw);
+ inet_pton(AF_INET, WIFI_STA_STATIC_NETMASK, &info.netmask);
+ ESP_ERROR_CHECK(esp_netif_set_ip_info(sta_netif, &info));
+ DEBUG_INFO("WIFI", "静态 IP 已设置: %s", WIFI_STA_STATIC_IP);
+
+ // 设置 DNS
+ esp_netif_dns_info_t dns_info;
+ memset(&dns_info, 0, sizeof(dns_info));
+ inet_pton(AF_INET, WIFI_STA_STATIC_DNS, &dns_info.ip.u_addr.ip4.addr);
+ ESP_ERROR_CHECK(esp_netif_set_dns_info(sta_netif, ESP_NETIF_DNS_MAIN, &dns_info));
+ DEBUG_INFO("WIFI", "DNS 已设置: %s", WIFI_STA_STATIC_DNS);
+ }
+ } else {
+ DEBUG_INFO("WIFI", "使用 DHCP 获取 IP");
+ }
+}
+
+// ----------------------------------------
+// ESP32 OTA 内部函数
+/**
+ * @brief 初始化 ESP32 自身的 OTA 写入流程。
+ * @param total_size
+ * @return ESP_OK 成功,其他失败。
+ */
+static esp_err_t esp32_ota_begin(size_t total_size)
+{
+ const esp_partition_t *boot_partition = esp_ota_get_running_partition();
+ DEBUG_INFO("OTA", "当前启动分区: %s, size=%u, address=0x%lx",
+ boot_partition->label, boot_partition->size, boot_partition->address);
+
+ s_esp32_update_partition = esp_partition_find_first(
+ ESP_PARTITION_TYPE_APP, ESP_PARTITION_SUBTYPE_APP_OTA_0, NULL);
+ if (s_esp32_update_partition == NULL ||
+ s_esp32_update_partition->address == boot_partition->address) {
+ s_esp32_update_partition = esp_partition_find_first(
+ ESP_PARTITION_TYPE_APP, ESP_PARTITION_SUBTYPE_APP_OTA_1, NULL);
+ }
+
+ if (s_esp32_update_partition == NULL) {
+ DEBUG_ERR("OTA", "找不到可用的 OTA 分区");
+ return ESP_FAIL;
+ }
+
+ DEBUG_INFO("OTA", "OTA 目标分区: %s, size=%u, address=0x%lx",
+ s_esp32_update_partition->label,
+ s_esp32_update_partition->size,
+ s_esp32_update_partition->address);
+
+ if (total_size > s_esp32_update_partition->size) {
+ DEBUG_ERR("OTA", "固件太大: %u > 分区大小: %u",
+ total_size, s_esp32_update_partition->size);
+ return ESP_FAIL;
+ }
+
+ esp_err_t err = esp_ota_begin(s_esp32_update_partition, total_size, &s_esp32_ota_handle);
+ if (err != ESP_OK) {
+ DEBUG_ERR("OTA", "esp_ota_begin 失败: %s", esp_err_to_name(err));
+ /** @brief ESP32 OTA 目标分区。 */
+ s_esp32_update_partition = NULL;
+ return err;
+ }
+
+ /** @brief ESP32 OTA 已接收字节数。 */
+ s_esp32_received = 0;
+ /** @brief ESP32 自身 OTA 写入进行中标志。 */
+ s_esp32_ota_active = true;
+ DEBUG_INFO("OTA", "ESP32 OTA 开始,目标分区: %s", s_esp32_update_partition->label);
+ return ESP_OK;
+}
+
+/**
+ * @brief 将接收到的数据块写入 ESP32 OTA 分区。
+ * @param data
+ * @param len
+ * @return ESP_OK 成功,其他失败。
+ */
+static esp_err_t esp32_ota_write(const uint8_t *data, size_t len)
+{
+ if (!s_esp32_ota_active || s_esp32_update_partition == NULL) {
+ return ESP_ERR_INVALID_STATE;
+ }
+ esp_err_t err = esp_ota_write(s_esp32_ota_handle, data, len);
+ if (err == ESP_OK) {
+ s_esp32_received += len;
+ }
+ return err;
+}
+
+/**
+ * @brief HTTP POST /ota/update - 完成 ESP32 OTA 并重启
+ * @param req
+ * @return
+ */
+static esp_err_t esp32_ota_update_handler(httpd_req_t *req)
+{
+ (void)req;
+ http_set_cors_headers(req);
+ if (!s_esp32_ota_active) {
+ httpd_resp_send_err(req, HTTPD_400_BAD_REQUEST, "OTA not started");
+ return ESP_FAIL;
+ }
+
+ DEBUG_INFO("OTA", "ESP32 OTA 完成, 共 %u 字节, 设置启动分区...", s_esp32_received);
+
+ esp_err_t err = esp_ota_end(s_esp32_ota_handle);
+ if (err != ESP_OK) {
+ DEBUG_ERR("OTA", "esp_ota_end 失败: %s", esp_err_to_name(err));
+ /** @brief ESP32 自身 OTA 写入进行中标志。 */
+ s_esp32_ota_active = false;
+ httpd_resp_send_err(req, HTTPD_500_INTERNAL_SERVER_ERROR, esp_err_to_name(err));
+ return ESP_FAIL;
+ }
+
+ err = esp_ota_set_boot_partition(s_esp32_update_partition);
+ if (err != ESP_OK) {
+ DEBUG_ERR("OTA", "esp_ota_set_boot_partition 失败: %s", esp_err_to_name(err));
+ /** @brief ESP32 自身 OTA 写入进行中标志。 */
+ s_esp32_ota_active = false;
+ httpd_resp_send_err(req, HTTPD_500_INTERNAL_SERVER_ERROR, esp_err_to_name(err));
+ return ESP_FAIL;
+ }
+
+ /** @brief ESP32 自身 OTA 写入进行中标志。 */
+ s_esp32_ota_active = false;
+ DEBUG_INFO("OTA", "ESP32 OTA 完成,重启中...");
+
+ const char resp[] = "{\"status\": \"ok\", \"message\": \"OTA finished, rebooting\"}";
+ httpd_resp_set_type(req, "application/json");
+ httpd_resp_send(req, resp, strlen(resp));
+
+ vTaskDelay(pdMS_TO_TICKS(500));
+ esp_restart();
+ return ESP_OK;
+}
+
+/**
+ * @brief HTTP GET /ota/update - 查询 ESP32 OTA 状态
+ * @param req
+ * @return
+ */
+static esp_err_t esp32_ota_status_handler(httpd_req_t *req)
+{
+ http_set_cors_headers(req);
+ char resp[128];
+ if (s_esp32_ota_active) {
+ snprintf(resp, sizeof(resp),
+ "{\"active\": true, \"received\": %u, \"partition\": \"%s\"}",
+ (unsigned int)s_esp32_received,
+ s_esp32_update_partition ? s_esp32_update_partition->label : "null");
+ } else {
+ snprintf(resp, sizeof(resp), "{\"active\": false}");
+ }
+ httpd_resp_set_type(req, "application/json");
+ httpd_resp_send(req, resp, strlen(resp));
+ return ESP_OK;
+}
+
+// 从 HTTP 流中读取完整的 JSON 头(以 } 结尾)
+/**
+ * @brief 返回 JSON 字符串长度,失败返回 0
+ * @param req
+ * @param buf
+ * @param buf_size
+ * @return
+ */
+static int recv_json_header(httpd_req_t *req, char *buf, int buf_size)
+{
+ int total = 0;
+ int depth = 1; // 开头的 '{' 已被 peek 消费,从 1 开始
+ while (total < buf_size - 1) {
+ int ret = httpd_req_recv(req, buf + total, 1); // 逐字节读,精确找到 JSON 结尾
+ if (ret <= 0) return 0;
+ total += ret;
+ buf[total] = '\0';
+ if (buf[total - 1] == '{') depth++;
+ else if (buf[total - 1] == '}') {
+ depth--;
+ if (depth == 0) return total; // JSON 头读完
+ }
+ }
+ return 0; // JSON 头太长,超出缓冲区
+}
+
+// ----------------------------------------
+// 统一 /ota/upload 处理器(自动识别目标)
+/**
+ * @brief HTTP 文件上传处理器,接收固件并写入 Flash。
+ * @param req
+ * @return ESP_OK 成功,其他失败。
+ */
+static esp_err_t ota_upload_handler(httpd_req_t *req)
+{
+ http_set_cors_headers(req);
+ DEBUG_INFO("WIFI", "=== HTTP: POST /ota/upload ===");
+ DEBUG_INFO("WIFI", "收到固件上传请求, 内容长度: %d 字节", req->content_len);
+
+ // 预读 1 字节判断是 JSON 还是 binary
+ char peek;
+ int peek_ret = httpd_req_recv(req, &peek, 1);
+ if (peek_ret <= 0) {
+ httpd_resp_send_err(req, HTTPD_500_INTERNAL_SERVER_ERROR, "接收失败");
+ return ESP_FAIL;
+ }
+
+ bool is_esp32_ota = false;
+ uint32_t total_size = 0;
+ int json_header_len = 0;
+
+ if (peek == '{') {
+ // JSON 模式:逐字节读完 JSON 头
+ char json_buf[256];
+ json_buf[0] = peek;
+ json_header_len = recv_json_header(req, json_buf + 1, sizeof(json_buf) - 1);
+ if (json_header_len > 0) {
+ json_header_len++; // 加上已读的 peek 字节
+ json_buf[json_header_len] = '\0';
+
+ cJSON *root = cJSON_Parse(json_buf);
+ if (root) {
+ cJSON *type_item = cJSON_GetObjectItem(root, "type");
+ if (type_item && cJSON_IsString(type_item) &&
+ strcmp(cJSON_GetStringValue(type_item), "esp32") == 0) {
+ is_esp32_ota = true;
+ DEBUG_INFO("WIFI", "检测到 ESP32 OTA, JSON 头长度: %d 字节", json_header_len);
+ }
+ cJSON *size_item = cJSON_GetObjectItem(root, "total_size");
+ if (size_item) {
+ total_size = (uint32_t)cJSON_GetNumberValue(size_item);
+ }
+ cJSON_Delete(root);
+ }
+ }
+ }
+
+ if (is_esp32_ota) {
+ // --- ESP32 OTA ---
+ if (total_size == 0) {
+ httpd_resp_send_err(req, HTTPD_400_BAD_REQUEST, "missing total_size");
+ return ESP_FAIL;
+ }
+
+ esp_err_t err = esp32_ota_begin(total_size);
+ if (err != ESP_OK) {
+ httpd_resp_send_err(req, HTTPD_500_INTERNAL_SERVER_ERROR, "OTA begin failed");
+ return ESP_FAIL;
+ }
+
+ // 堆分配缓冲区,避免栈溢出(HTTP server 栈仅 8KB)
+ uint8_t *fw_buf = (uint8_t *)malloc(1024);
+ if (!fw_buf) {
+ httpd_resp_send_err(req, HTTPD_500_INTERNAL_SERVER_ERROR, "内存分配失败");
+ return ESP_FAIL;
+ }
+
+ size_t total_firmware_written = 0;
+ size_t total_received = json_header_len; // 已消费的字节数
+ int last_percent = 0;
+
+ while (total_received < req->content_len) {
+ size_t to_read = req->content_len - total_received;
+ if (to_read > 1024) to_read = 1024;
+
+ int ret = httpd_req_recv(req, (char *)fw_buf, to_read);
+ if (ret <= 0) break;
+
+ err = esp32_ota_write(fw_buf, ret);
+ if (err != ESP_OK) {
+ DEBUG_ERR("WIFI", "OTA write 失败");
+ break;
+ }
+ total_received += ret;
+ total_firmware_written += ret;
+
+ if (total_size > 0) {
+ int percent = (int)((total_firmware_written * 100) / total_size);
+ if (percent / 10 > last_percent / 10) {
+ last_percent = percent;
+ DEBUG_INFO("WIFI", "ESP32 OTA 进度: %u / %u (%d%%)",
+ (unsigned int)total_firmware_written, total_size, percent);
+ }
+ }
+ }
+
+ free(fw_buf);
+ DEBUG_INFO("WIFI", "ESP32 OTA 存储完成: %u 字节 (跳过 JSON 头 %d 字节)", (unsigned int)s_esp32_received, json_header_len);
+ const char resp[] = "{\"status\": \"ok\", \"message\": \"stored, call POST /ota/update to finish\", \"target\": \"esp32\"}";
+ httpd_resp_set_type(req, "application/json");
+ httpd_resp_send(req, resp, strlen(resp));
+ return ESP_OK;
+ }
+
+ // --- GD32 OTA(原逻辑)---
+ if (req->content_len > OTA_MAX_FIRMWARE_SIZE) {
+ DEBUG_ERR("WIFI", "固件太大: %d > %d", req->content_len, OTA_MAX_FIRMWARE_SIZE);
+ httpd_resp_send_err(req, HTTPD_500_INTERNAL_SERVER_ERROR, "固件太大");
+ return ESP_FAIL;
+ }
+
+ uint8_t *buf = (uint8_t *)malloc(OTA_BLOCK_SIZE);
+ int received = 0;
+
+ // peek 字节是 GD32 数据的第一字节,先放回缓冲区
+ uint8_t gd32_header[6];
+ gd32_header[0] = (uint8_t)peek;
+ int header_read = 1;
+ // 读剩余 5 字节凑齐 6 字节 GD32 头
+ if (req->content_len >= 6) {
+ int need = 6 - header_read;
+ int got = httpd_req_recv(req, (char *)gd32_header + header_read, need);
+ if (got > 0) header_read += got;
+ }
+
+ // 先处理已读的 header
+ if (header_read >= 6) {
+ uint16_t seq = gd32_header[0] | (gd32_header[1] << 8);
+ uint32_t fw_size = gd32_header[2] | (gd32_header[3] << 8) | (gd32_header[4] << 16) | (gd32_header[5] << 24);
+ if (seq == 0) {
+ ota_meta_t meta = { .total_size = fw_size, .stored_size = 0, .last_seq = 0,
+ .chunk_count = (fw_size + 4095) / 4096, .checksum = {0} };
+ meta_save(&meta);
+ DEBUG_INFO("WIFI", "开始 GD32 固件存储: %u 字节", fw_size);
+ }
+ }
+
+ // 继续读剩余数据
+ while (received + header_read < req->content_len) {
+ size_t to_read = req->content_len - received - header_read;
+ if (to_read > OTA_BLOCK_SIZE) to_read = OTA_BLOCK_SIZE;
+
+ int ret = httpd_req_recv(req, (char *)buf, to_read);
+ if (ret <= 0) break;
+
+ uint32_t flash_offset = ((received + header_read) / OTA_BLOCK_SIZE) * OTA_BLOCK_SIZE;
+ esp_err_t err = flash_write_chunk(flash_offset, buf, ret);
+ if (err == ESP_OK) {
+ meta_update(flash_offset + ret, (received + header_read) / OTA_BLOCK_SIZE);
+ }
+ received += ret;
+
+ uint32_t progress = ((received + header_read) * 100) / req->content_len;
+ if (progress % 10 == 0) {
+ DEBUG_INFO("WIFI", "GD32 上传进度: %d / %d (%u%%)",
+ received + header_read, req->content_len, progress);
+ }
+ }
+
+ free(buf);
+ DEBUG_INFO("WIFI", "GD32 固件存储完成: %d 字节", received + header_read);
+
+ ota_meta_t meta;
+ meta_load(&meta);
+ if (meta.stored_size >= meta.total_size) {
+ /** @brief GD32 固件存储完成标志。 */
+ s_gd32_storage_done = true;
+ DEBUG_INFO("WIFI", "GD32 固件已存储,触发转发...");
+ wifi_ota_signal_relay();
+ }
+
+ const char resp[] = "{\"status\": \"ok\", \"progress\": 100, \"target\": \"gd32\"}";
+ httpd_resp_set_type(req, "application/json");
+ httpd_resp_send(req, resp, strlen(resp));
+ return ESP_OK;
+}
+
+/**
+ * @brief HTTP POST /ota/relay 处理器
+ * @param req
+ * @return
+ */
+static esp_err_t ota_relay_trigger_handler(httpd_req_t *req)
+{
+ (void)req;
+ http_set_cors_headers(req);
+ DEBUG_INFO("WIFI", "=== HTTP: POST /ota/relay ===");
+ DEBUG_INFO("WIFI", "收到触发 OTA 中继请求");
+ wifi_ota_signal_relay();
+
+ const char resp[] = "{\"status\": \"ok\", \"message\": \"OTA relay triggered\"}";
+ httpd_resp_set_type(req, "application/json");
+ httpd_resp_send(req, resp, strlen(resp));
+
+ return ESP_OK;
+}
+
+/**
+ * @brief HTTP GET /ota/status 处理器
+ * @param req
+ * @return
+ */
+static esp_err_t ota_status_handler(httpd_req_t *req)
+{
+ http_set_cors_headers(req);
+ DEBUG_INFO("WIFI", "=== HTTP: GET /ota/status ===");
+
+ ota_meta_t meta;
+ meta_load(&meta);
+
+ char resp[256];
+ int len = snprintf(resp, sizeof(resp),
+ "{\"total_size\": %lu, \"stored_size\": %lu, \"last_seq\": %lu, "
+ "\"storage_done\": %s, \"relay_state\": %d}",
+ (unsigned long)meta.total_size, (unsigned long)meta.stored_size,
+ (unsigned long)meta.last_seq,
+ s_gd32_storage_done ? "true" : "false",
+ g_relay_state);
+
+ DEBUG_INFO("WIFI", "OTA 状态: total=%lu, stored=%lu, done=%s",
+ (unsigned long)meta.total_size, (unsigned long)meta.stored_size,
+ s_gd32_storage_done ? "true" : "false");
+
+ httpd_resp_set_type(req, "application/json");
+ httpd_resp_send(req, resp, len);
+
+ return ESP_OK;
+}
+
+/**
+ * @brief HTTP GET /health 处理器
+ * @param req
+ * @return
+ */
+static esp_err_t health_handler(httpd_req_t *req)
+{
+ http_set_cors_headers(req);
+ DEBUG_INFO("WIFI", "=== HTTP: GET /health ===");
+
+ health_state_t state = health_get_current();
+ uint32_t free_heap = esp_get_free_heap_size();
+
+ char resp[128];
+ int len = snprintf(resp, sizeof(resp),
+ "{\"health\": %d, \"free_heap\": %lu}",
+ state, (unsigned long)free_heap);
+
+ DEBUG_INFO("WIFI", "健康状态: %d, 空闲堆: %lu", state, (unsigned long)free_heap);
+
+ httpd_resp_set_type(req, "application/json");
+ httpd_resp_send(req, resp, len);
+
+ return ESP_OK;
+}
+
+/**
+ * @brief HTTP GET /logs - 拉取并清空日志缓冲区
+ * @param req
+ * @return
+ */
+static esp_err_t logs_handler(httpd_req_t *req)
+{
+ http_set_cors_headers(req);
+
+ int json_len = 0;
+ char *json = debug_log_ring_drain_json(&json_len);
+
+ if (json && json_len > 0) {
+ httpd_resp_set_type(req, "application/json");
+ httpd_resp_send(req, json, json_len);
+ free(json);
+ } else {
+ httpd_resp_set_type(req, "application/json");
+ httpd_resp_send(req, "[]", 2);
+ }
+ return ESP_OK;
+}
+
+// HTTP POST /can/send 处理器
+// 上位机发送 CAN 帧,经 ESP32 转发到 GD32
+// 请求体: {"ch": 0, "id": 123456, "data": "0102030405060708", "dlc": 8}
+/**
+ * @brief data 为 hex 字符串(可不填,自动用 dlc 决定实际发送长度)
+ * @param req
+ * @return
+ */
+static esp_err_t can_send_handler(httpd_req_t *req)
+{
+ http_set_cors_headers(req);
+ char buf[256] = {0};
+ int ret = httpd_req_recv(req, buf, sizeof(buf) - 1);
+ if (ret <= 0) {
+ httpd_resp_send_err(req, HTTPD_400_BAD_REQUEST, "接收数据失败");
+ return ESP_FAIL;
+ }
+ buf[ret] = '\0';
+
+ cJSON *root = cJSON_Parse(buf);
+ if (!root) {
+ httpd_resp_send_err(req, HTTPD_400_BAD_REQUEST, "JSON 解析失败");
+ return ESP_FAIL;
+ }
+
+ cJSON *ch_item = cJSON_GetObjectItem(root, "ch");
+ cJSON *id_item = cJSON_GetObjectItem(root, "id");
+ cJSON *data_item = cJSON_GetObjectItem(root, "data");
+ cJSON *dlc_item = cJSON_GetObjectItem(root, "dlc");
+
+ if (!ch_item || !id_item) {
+ cJSON_Delete(root);
+ httpd_resp_send_err(req, HTTPD_400_BAD_REQUEST, "缺少 ch 或 id 字段");
+ return ESP_FAIL;
+ }
+
+ can_channel_t ch = (can_channel_t)cJSON_GetNumberValue(ch_item);
+ uint32_t ext_id = (uint32_t)cJSON_GetNumberValue(id_item);
+ uint8_t dlc = dlc_item ? (uint8_t)cJSON_GetNumberValue(dlc_item) : 0;
+
+ uint8_t tx_data[CAN_MAX_DLC] = {0};
+ uint8_t tx_dlc = 0;
+
+ if (data_item && cJSON_IsString(data_item)) {
+ const char *hex_str = cJSON_GetStringValue(data_item);
+ size_t hex_len = strlen(hex_str);
+ for (size_t i = 0; i < hex_len / 2 && i < CAN_MAX_DLC; i++) {
+ char byte_str[3] = { hex_str[i * 2], hex_str[i * 2 + 1], '\0' };
+ tx_data[i] = (uint8_t)strtol(byte_str, NULL, 16);
+ tx_dlc++;
+ }
+ }
+
+ if (tx_dlc == 0 && dlc > 0) {
+ tx_dlc = (dlc > CAN_MAX_DLC) ? CAN_MAX_DLC : dlc;
+ }
+
+ cJSON_Delete(root);
+
+ // 发送 CAN 帧到 GD32
+ uint8_t resp_data[CAN_MAX_DLC] = {0};
+ int resp_len = gd32_can_send(ch, ext_id, tx_data, tx_dlc,
+ resp_data, sizeof(resp_data), 1000);
+
+ char resp[512];
+ if (resp_len >= 0) {
+ // 将响应 data 转为 hex 字符串
+ char resp_hex[CAN_MAX_DLC * 2 + 1] = {0};
+ for (int i = 0; i < resp_len; i++) {
+ sprintf(&resp_hex[i * 2], "%02X", resp_data[i]);
+ }
+ snprintf(resp, sizeof(resp),
+ "{\"status\": \"ok\", \"ch\": %d, \"id\": %lu, \"sent\": %d, \"recv_len\": %d, \"recv\": \"%s\"}",
+ ch, (unsigned long)ext_id, tx_dlc, resp_len, resp_hex);
+ } else {
+ snprintf(resp, sizeof(resp),
+ "{\"status\": \"error\", \"code\": %d}", resp_len);
+ }
+
+ httpd_resp_set_type(req, "application/json");
+ httpd_resp_send(req, resp, strlen(resp));
+ return ESP_OK;
+}
+
+/**
+ * @brief HTTP GET /can/status 处理器
+ * @param req
+ * @return
+ */
+static esp_err_t can_status_handler(httpd_req_t *req)
+{
+ http_set_cors_headers(req);
+ char resp[128];
+ snprintf(resp, sizeof(resp),
+ "{\"channels\": 3, \"max_dlc\": %d, \"spi_init\": %s}",
+ CAN_MAX_DLC,
+ g_spi_device != NULL ? "true" : "false");
+ httpd_resp_set_type(req, "application/json");
+ httpd_resp_send(req, resp, strlen(resp));
+ return ESP_OK;
+}
+
+/**
+ * @brief HTTP GET /wifi/status 处理器
+ * @param req
+ * @return
+ */
+static esp_err_t wifi_status_handler(httpd_req_t *req)
+{
+ http_set_cors_headers(req);
+ char resp[256];
+ char ip_str[16] = "0.0.0.0";
+ int8_t rssi = -127;
+
+ if (s_wifi_mode == OTA_WIFI_MODE_STA && s_sta_connected) {
+ esp_netif_ip_info_t ip_info;
+ esp_netif_t *sta_netif = esp_netif_get_handle_from_ifkey("WIFI_STA_DEF");
+ if (sta_netif && esp_netif_get_ip_info(sta_netif, &ip_info) == ESP_OK) {
+ snprintf(ip_str, sizeof(ip_str), IPSTR, IP2STR(&ip_info.ip));
+ }
+ wifi_ap_record_t ap_info;
+ if (esp_wifi_sta_get_ap_info(&ap_info) == ESP_OK) {
+ rssi = ap_info.rssi;
+ }
+ }
+
+ int len = snprintf(resp, sizeof(resp),
+ "{\"mode\": \"%s\", \"ip\": \"%s\", \"rssi\": %d, \"connected\": %s}",
+ s_wifi_mode == OTA_WIFI_MODE_AP ? "AP" : "STA",
+ ip_str, rssi,
+ (s_wifi_mode == OTA_WIFI_MODE_STA) ? (s_sta_connected ? "true" : "false") : "N/A");
+
+ httpd_resp_set_type(req, "application/json");
+ httpd_resp_send(req, resp, len);
+
+ return ESP_OK;
+}
+
+/**
+ * @brief HTTP GET /system/status 处理器
+ * @param req
+ * @return
+ */
+static esp_err_t system_status_handler(httpd_req_t *req)
+{
+ http_set_cors_headers(req);
+
+ char ip_str[16] = "0.0.0.0";
+ int8_t rssi = -127;
+ bool connected = false;
+
+ if (s_wifi_mode == OTA_WIFI_MODE_STA && s_sta_connected) {
+ esp_netif_ip_info_t ip_info;
+ esp_netif_t *sta_netif = esp_netif_get_handle_from_ifkey("WIFI_STA_DEF");
+ if (sta_netif && esp_netif_get_ip_info(sta_netif, &ip_info) == ESP_OK) {
+ snprintf(ip_str, sizeof(ip_str), IPSTR, IP2STR(&ip_info.ip));
+ }
+ wifi_ap_record_t ap_info;
+ if (esp_wifi_sta_get_ap_info(&ap_info) == ESP_OK) {
+ rssi = ap_info.rssi;
+ }
+ connected = true;
+ }
+
+ ota_meta_t meta;
+ memset(&meta, 0, sizeof(meta));
+ meta_load(&meta);
+
+ char resp[640];
+ int len = snprintf(resp, sizeof(resp),
+ "{\"version\": \"%s\", \"health\": %d, \"free_heap\": %lu, \"min_free_heap\": %lu, \"temperature\": %.1f, \"wifi\": {\"mode\": \"%s\", \"ip\": \"%s\", \"rssi\": %d, \"connected\": %s}, \"ota\": {\"total_size\": %lu, \"stored_size\": %lu, \"storage_done\": %s, \"relay_state\": %d}}",
+ FW_VERSION,health_get_current(),
+ (unsigned long)esp_get_free_heap_size(),
+ (unsigned long)esp_get_minimum_free_heap_size(),
+ (double)health_get_last_temperature(),
+ s_wifi_mode == OTA_WIFI_MODE_AP ? "AP" : "STA",
+ ip_str,
+ rssi,
+ connected ? "true" : "false",
+ (unsigned long)meta.total_size,
+ (unsigned long)meta.stored_size,
+ s_gd32_storage_done ? "true" : "false",
+ g_relay_state);
+
+ httpd_resp_set_type(req, "application/json");
+ httpd_resp_send(req, resp, len);
+ return ESP_OK;
+}
+
+// ----------------------------------------
+// 启动 HTTP OTA 服务器
+/**
+ * @brief 启动 HTTP OTA 服务器,注册各接口路由。
+ * @return ESP_OK 成功,其他失败。
+ */
+static esp_err_t http_ota_server_start(void)
+{
+ httpd_config_t config = HTTPD_DEFAULT_CONFIG();
+ config.server_port = HTTP_SERVER_PORT;
+ config.stack_size = HTTP_SERVER_STACK_SIZE;
+ config.max_uri_handlers = 32; // 现有业务端点 + 对应 OPTIONS 预检
+
+ DEBUG_INFO("WIFI", "=== 启动 HTTP OTA 服务器 ===");
+ DEBUG_INFO("WIFI", "HTTP 服务器端口: %d", config.server_port);
+
+ esp_err_t err = httpd_start(&s_http_server, &config);
+ if (err != ESP_OK) {
+ DEBUG_ERR("WIFI", "HTTP 服务器启动失败: %s", esp_err_to_name(err));
+ return err;
+ }
+
+ httpd_uri_t upload_uri = {
+ .uri = "/ota/upload",
+ .method = HTTP_POST,
+ .handler = ota_upload_handler,
+ .user_ctx = NULL
+ };
+ httpd_register_uri_handler(s_http_server, &upload_uri);
+ httpd_uri_t upload_options_uri = {
+ .uri = "/ota/upload",
+ .method = HTTP_OPTIONS,
+ .handler = http_options_handler,
+ .user_ctx = NULL
+ };
+ httpd_register_uri_handler(s_http_server, &upload_options_uri);
+
+ httpd_uri_t relay_uri = {
+ .uri = "/ota/relay",
+ .method = HTTP_POST,
+ .handler = ota_relay_trigger_handler,
+ .user_ctx = NULL
+ };
+ httpd_register_uri_handler(s_http_server, &relay_uri);
+ httpd_uri_t relay_options_uri = {
+ .uri = "/ota/relay",
+ .method = HTTP_OPTIONS,
+ .handler = http_options_handler,
+ .user_ctx = NULL
+ };
+ httpd_register_uri_handler(s_http_server, &relay_options_uri);
+
+ httpd_uri_t status_uri = {
+ .uri = "/ota/status",
+ .method = HTTP_GET,
+ .handler = ota_status_handler,
+ .user_ctx = NULL
+ };
+ httpd_register_uri_handler(s_http_server, &status_uri);
+ httpd_uri_t status_options_uri = {
+ .uri = "/ota/status",
+ .method = HTTP_OPTIONS,
+ .handler = http_options_handler,
+ .user_ctx = NULL
+ };
+ httpd_register_uri_handler(s_http_server, &status_options_uri);
+
+ httpd_uri_t esp32_update_uri = {
+ .uri = "/ota/update",
+ .method = HTTP_POST,
+ .handler = esp32_ota_update_handler,
+ .user_ctx = NULL
+ };
+ httpd_register_uri_handler(s_http_server, &esp32_update_uri);
+ httpd_uri_t esp32_update_options_uri = {
+ .uri = "/ota/update",
+ .method = HTTP_OPTIONS,
+ .handler = http_options_handler,
+ .user_ctx = NULL
+ };
+ httpd_register_uri_handler(s_http_server, &esp32_update_options_uri);
+
+ httpd_uri_t esp32_status_uri = {
+ .uri = "/ota/esp32_status",
+ .method = HTTP_GET,
+ .handler = esp32_ota_status_handler,
+ .user_ctx = NULL
+ };
+ httpd_register_uri_handler(s_http_server, &esp32_status_uri);
+ httpd_uri_t esp32_status_options_uri = {
+ .uri = "/ota/esp32_status",
+ .method = HTTP_OPTIONS,
+ .handler = http_options_handler,
+ .user_ctx = NULL
+ };
+ httpd_register_uri_handler(s_http_server, &esp32_status_options_uri);
+
+ httpd_uri_t health_uri = {
+ .uri = "/health",
+ .method = HTTP_GET,
+ .handler = health_handler,
+ .user_ctx = NULL
+ };
+ httpd_register_uri_handler(s_http_server, &health_uri);
+ httpd_uri_t health_options_uri = {
+ .uri = "/health",
+ .method = HTTP_OPTIONS,
+ .handler = http_options_handler,
+ .user_ctx = NULL
+ };
+ httpd_register_uri_handler(s_http_server, &health_options_uri);
+
+ httpd_uri_t logs_uri = {
+ .uri = "/logs",
+ .method = HTTP_GET,
+ .handler = logs_handler,
+ .user_ctx = NULL
+ };
+ httpd_register_uri_handler(s_http_server, &logs_uri);
+ httpd_uri_t logs_options_uri = {
+ .uri = "/logs",
+ .method = HTTP_OPTIONS,
+ .handler = http_options_handler,
+ .user_ctx = NULL
+ };
+ httpd_register_uri_handler(s_http_server, &logs_options_uri);
+
+ httpd_uri_t wifi_status_uri = {
+ .uri = "/wifi/status",
+ .method = HTTP_GET,
+ .handler = wifi_status_handler,
+ .user_ctx = NULL
+ };
+ httpd_register_uri_handler(s_http_server, &wifi_status_uri);
+ httpd_uri_t wifi_status_options_uri = {
+ .uri = "/wifi/status",
+ .method = HTTP_OPTIONS,
+ .handler = http_options_handler,
+ .user_ctx = NULL
+ };
+ httpd_register_uri_handler(s_http_server, &wifi_status_options_uri);
+
+ httpd_uri_t system_status_uri = {
+ .uri = "/system/status",
+ .method = HTTP_GET,
+ .handler = system_status_handler,
+ .user_ctx = NULL
+ };
+ httpd_register_uri_handler(s_http_server, &system_status_uri);
+ httpd_uri_t system_status_options_uri = {
+ .uri = "/system/status",
+ .method = HTTP_OPTIONS,
+ .handler = http_options_handler,
+ .user_ctx = NULL
+ };
+ httpd_register_uri_handler(s_http_server, &system_status_options_uri);
+
+ // CAN 帧转发
+ httpd_uri_t can_send_uri = {
+ .uri = "/can/send",
+ .method = HTTP_POST,
+ .handler = can_send_handler,
+ .user_ctx = NULL
+ };
+ httpd_register_uri_handler(s_http_server, &can_send_uri);
+ httpd_uri_t can_send_options_uri = {
+ .uri = "/can/send",
+ .method = HTTP_OPTIONS,
+ .handler = http_options_handler,
+ .user_ctx = NULL
+ };
+ httpd_register_uri_handler(s_http_server, &can_send_options_uri);
+
+ httpd_uri_t can_status_uri = {
+ .uri = "/can/status",
+ .method = HTTP_GET,
+ .handler = can_status_handler,
+ .user_ctx = NULL
+ };
+ httpd_register_uri_handler(s_http_server, &can_status_uri);
+ httpd_uri_t can_status_options_uri = {
+ .uri = "/can/status",
+ .method = HTTP_OPTIONS,
+ .handler = http_options_handler,
+ .user_ctx = NULL
+ };
+ httpd_register_uri_handler(s_http_server, &can_status_options_uri);
+
+ DEBUG_INFO("WIFI", "HTTP 服务器已启动");
+ DEBUG_INFO("WIFI", "注册端点: /ota/upload, /ota/relay, /ota/update, /ota/status, /ota/esp32_status, /health, /wifi/status, /system/status, /can/send, /can/status");
+
+ return ESP_OK;
+}
+
+// ----------------------------------------
+// 公开 API
+/**
+ * @brief 通知 OTA 中继开始向 GD32 转发固件。
+ */
+void wifi_ota_signal_relay(void)
+{
+ if (s_relay_triggered) {
+ DEBUG_WARN("WIFI", "转发已经触发过");
+ return;
+ }
+
+ /** @brief OTA 中继是否已触发标志。 */
+ s_relay_triggered = true;
+
+ if (g_ota_dispatch_queue) {
+ uint32_t msg = 1;
+ xQueueSend(g_ota_dispatch_queue, &msg, pdMS_TO_TICKS(100));
+ DEBUG_INFO("WIFI", "OTA 调度信号已发送到中继任务");
+ }
+}
+
+/**
+ * @brief 获取当前 WiFi 工作模式(AP 或 STA)。
+ * @return 当前 WiFi 模式枚举值。
+ */
+ota_wifi_mode_t wifi_get_mode(void)
+{
+ /** @brief 当前 WiFi 工作模式(AP 或 STA)。 */
+ return s_wifi_mode;
+}
+
+/**
+ * @brief 获取当前 WiFi 信号强度。
+ * @return 信号强度(dBm),未连接时返回无效值。
+ */
+int8_t wifi_get_rssi(void)
+{
+ if (s_wifi_mode != OTA_WIFI_MODE_STA || !s_sta_connected) {
+ return -127;
+ }
+ wifi_ap_record_t ap_info;
+ if (esp_wifi_sta_get_ap_info(&ap_info) == ESP_OK) {
+ return ap_info.rssi;
+ }
+ return -127;
+}
+
+/**
+ * @brief 判断 STA 是否已连接到上级 WiFi。
+ * @return true 已连接,false 未连接。
+ */
+bool wifi_is_connected(void)
+{
+ /** @brief STA 模式连接状态。 */
+ return s_sta_connected;
+}
+
+// ----------------------------------------
+// ★ WiFi 早期初始化(在 app_main 调度器启动前调用)
+// 包括:NVS → netif → event handler → WiFi driver → 配置 → connect
+// 返回:true = STA 模式已启动(正在连接),false = 需要切 AP
+/**
+ * @brief 在调度器启动前初始化 WiFi 并尝试 STA 连接。
+ * @return true STA 初始化中,false 需回退 AP。
+ */
+bool wifi_early_init(void)
+{
+ DEBUG_INFO("WIFI", "=== WiFi 早期初始化 (调度器启动前) ===");
+
+ esp_err_t err = nvs_flash_init();
+ if (err == ESP_ERR_NVS_NO_FREE_PAGES || err == ESP_ERR_NVS_NEW_VERSION_FOUND) {
+ // NVS 分区损坏或版本不匹配,擦除后重试
+ DEBUG_WARN("WIFI", "NVS 分区异常 (%s),擦除后重试", esp_err_to_name(err));
+ ESP_ERROR_CHECK(nvs_flash_erase());
+ err = nvs_flash_init();
+ }
+ if (err != ESP_OK) {
+ DEBUG_ERR("WIFI", "NVS 初始化失败: %s", esp_err_to_name(err));
+ } else {
+ DEBUG_INFO("WIFI", "NVS 初始化成功");
+ }
+
+ /** @brief STA 连接成功信号量。 */
+ s_sta_connected_sem = xSemaphoreCreateBinary();
+ /** @brief STA 获取 IP 信号量。 */
+ s_sta_got_ip_sem = xSemaphoreCreateBinary();
+ if (!s_sta_connected_sem || !s_sta_got_ip_sem) {
+ DEBUG_ERR("WIFI", "创建信号量失败");
+ }
+
+ // 注册 WiFi/IP 事件回调(必须在 esp_wifi_init 之前)
+ ESP_ERROR_CHECK(esp_event_handler_register(WIFI_EVENT, ESP_EVENT_ANY_ID, &wifi_event_handler, NULL));
+ ESP_ERROR_CHECK(esp_event_handler_register(IP_EVENT, ESP_EVENT_ANY_ID, &wifi_event_handler, NULL));
+ DEBUG_INFO("WIFI", "WiFi/IP 事件回调已注册");
+
+ esp_netif_t *sta_netif = esp_netif_get_handle_from_ifkey("WIFI_STA_DEF");
+ if (sta_netif == NULL) {
+ sta_netif = esp_netif_create_default_wifi_sta();
+ if (sta_netif == NULL) {
+ DEBUG_ERR("WIFI", "创建 STA netif 失败");
+ } else {
+ DEBUG_INFO("WIFI", "STA netif 已创建");
+ }
+ } else {
+ DEBUG_INFO("WIFI", "STA netif 已存在,跳过创建");
+ }
+
+ wifi_init_config_t wifi_cfg = WIFI_INIT_CONFIG_DEFAULT();
+ ESP_ERROR_CHECK(esp_wifi_init(&wifi_cfg));
+ DEBUG_INFO("WIFI", "WiFi driver 初始化成功");
+
+ // STA 优先,失败时返回 false 让 task 切 AP
+ if (CONFIG_WIFI_MODE == 1) {
+ DEBUG_INFO("WIFI", "工作模式: STA (优先)");
+ err = wifi_init_sta();
+ if (err == ESP_OK) {
+ // 连接已触发(STA_START 事件驱动),返回 true
+ // 实际连接状态由 wifi_ota_task 等待确认
+ return true;
+ }
+ DEBUG_WARN("WIFI", "STA 初始化失败,将切换 AP");
+ return false;
+ } else {
+ DEBUG_INFO("WIFI", "工作模式: AP");
+ wifi_init_ap();
+ return false; // AP 模式,不需要等待连接
+ }
+}
+
+// ----------------------------------------
+// WiFi OTA FreeRTOS 任务
+// WiFi 初始化已在 app_main 中通过 wifi_early_init() 完成
+// 此任务负责:等待连接结果 → 启动 HTTP 服务器 → 状态监控
+/**
+ * @brief WiFi OTA FreeRTOS 任务,提供 HTTP 上传和中继触发。
+ * @param pvParameters FreeRTOS 任务参数,未使用。
+ */
+void wifi_ota_task(void *pvParameters)
+{
+ (void)pvParameters;
+
+ DEBUG_INFO("WIFI", "=== WiFi OTA 任务启动 ===");
+
+ // 等待 STA 连接结果(wifi_early_init 已触发连接)
+ if (wifi_get_mode() == OTA_WIFI_MODE_STA) {
+ char ip_str[16] = {0};
+ esp_err_t err = wifi_wait_sta_connected(ip_str, sizeof(ip_str));
+ if (err == ESP_OK) {
+ DEBUG_INFO("WIFI", "STA 已连接, IP: %s", ip_str);
+ } else {
+ DEBUG_WARN("WIFI", "STA 连接超时,切换到 AP 模式...");
+ esp_wifi_stop(); // 先停 STA,再切 AP
+ /** @brief 当前 WiFi 工作模式(AP 或 STA)。 */
+ s_wifi_mode = OTA_WIFI_MODE_AP;
+ wifi_init_ap();
+ }
+ }
+ // AP 模式:wifi_early_init 已完成,无需额外操作
+
+ esp_err_t err = http_ota_server_start();
+ if (err != ESP_OK) {
+ DEBUG_ERR("WIFI", "HTTP 服务器启动失败");
+ }
+
+ // ★ WiFi 已连接,UDP 从这里开始能通
+ // 等待网络栈稳定(ARP 表建立、路由就绪),避免首批 UDP 包丢失
+ vTaskDelay(pdMS_TO_TICKS(500));
+
+ // 启动摘要,发两遍确保上位机收到(第一包可能触发 ARP 被丢)
+ esp_chip_info_t chip;
+ esp_chip_info(&chip);
+ for (int i = 0; i < 2; i++) {
+ DEBUG_INFO("WIFI", "========================================");
+ DEBUG_INFO("WIFI", "[启动摘要] ESP32-C3 OTA Relay v%s", FW_VERSION);
+ DEBUG_INFO("WIFI", "[启动摘要] Chip: %s, Rev %d", CONFIG_IDF_TARGET, chip.revision);
+ DEBUG_INFO("WIFI", "[启动摘要] Free heap: %lu bytes", (unsigned long)esp_get_free_heap_size());
+ DEBUG_INFO("WIFI", "[启动摘要] WiFi: %s", wifi_get_mode() == OTA_WIFI_MODE_STA ? "STA" : "AP");
+ DEBUG_INFO("WIFI", "[启动摘要] NVS + WiFi driver + netif + event handler 初始化完成");
+ DEBUG_INFO("WIFI", "[启动摘要] HTTP 服务器端口: %d", HTTP_SERVER_PORT);
+ DEBUG_INFO("WIFI", "========================================");
+ DEBUG_INFO("WIFI", "=== WiFi OTA 服务器就绪 ===");
+ if (i == 0) vTaskDelay(pdMS_TO_TICKS(200));
+ }
+
+ // WiFi 状态监控
+ // 每秒一次循环:每 10 秒检查 RSSI
+ uint32_t tick = 0;
+ while (1) {
+ tick++;
+
+ // 每 10 秒检查一次 RSSI
+ if (tick % 10 == 0) {
+ int8_t rssi = wifi_get_rssi();
+ if (rssi != -127) {
+ health_update_from_wifi_rssi(rssi);
+
+ if (rssi < WIFI_RSSI_CRITICAL) {
+ DEBUG_WARN("WIFI", "WiFi RSSI 危险: %d dBm (阈值: %d)", rssi, WIFI_RSSI_CRITICAL);
+ } else if (rssi < WIFI_RSSI_WARNING) {
+ DEBUG_WARN("WIFI", "WiFi RSSI 较弱: %d dBm", rssi);
+ }
+ }
+ }
+
+ vTaskDelay(pdMS_TO_TICKS(1000));
+ }
+}
diff --git a/_assistant_backups/2026-05-05/wifi_ota.cpp.bak_2026-05-05_0522 b/_assistant_backups/2026-05-05/wifi_ota.cpp.bak_2026-05-05_0522
new file mode 100644
index 0000000..8528322
--- /dev/null
+++ b/_assistant_backups/2026-05-05/wifi_ota.cpp.bak_2026-05-05_0522
@@ -0,0 +1,1368 @@
+/*
+ * wifi_ota.c - WiFi OTA 服务器实现
+ * T4.1 - T4.5
+ */
+#include
+#include "freertos/FreeRTOS.h"
+#include "freertos/task.h"
+#include "freertos/semphr.h"
+#include "esp_wifi.h"
+#include "esp_netif.h"
+#include "esp_http_server.h"
+#include "esp_system.h"
+#include "esp_chip_info.h"
+#include "esp_timer.h"
+#include "esp_ota_ops.h"
+#include "nvs_flash.h"
+#include "lwip/netdb.h"
+#include "esp_netif.h"
+#include "esp_netif_types.h"
+#include "esp_event.h"
+#include "esp_mac.h"
+
+#include "app_config.h"
+#include "wifi_ota.h"
+#include "ota_relay.h"
+#include "health_monitor.h"
+#include "monitor/debug_log.h"
+#include "spi_master.h"
+#include "cJSON.h"
+
+/** @brief 当前 WiFi 工作模式(AP 或 STA)。 */
+static ota_wifi_mode_t s_wifi_mode = OTA_WIFI_MODE_AP;
+/** @brief OTA 中继是否已触发标志。 */
+static bool s_relay_triggered = false;
+/** @brief STA 模式连接状态。 */
+static bool s_sta_connected = false;
+/** @brief STA 连接重试次数。 */
+static int s_sta_retry_count = 0;
+/** @brief STA 连接成功信号量。 */
+static SemaphoreHandle_t s_sta_connected_sem = NULL;
+/** @brief STA 获取 IP 信号量。 */
+static SemaphoreHandle_t s_sta_got_ip_sem = NULL;
+
+// ESP32 OTA 状态
+static bool s_esp32_ota_active = false;
+/** @brief ESP32 OTA 写入句柄。 */
+static esp_ota_handle_t s_esp32_ota_handle = 0;
+static const esp_partition_t *s_esp32_update_partition = NULL;
+/** @brief ESP32 OTA 已接收字节数。 */
+static size_t s_esp32_received = 0;
+
+// GD32 OTA 存储状态
+static bool s_gd32_storage_done = false;
+
+/** @brief HTTP 服务器句柄。 */
+static httpd_handle_t s_http_server = NULL;
+
+/**
+ * @brief 预先声明:为 STA 模式配置静态 IP、网关和子网掩码。
+ */
+static void wifi_set_static_ip(void);
+
+/**
+ * @brief 为 HTTP 响应添加跨域访问控制头。
+ * @param req HTTP 请求对象。
+ */
+static void http_set_cors_headers(httpd_req_t *req)
+{
+ httpd_resp_set_hdr(req, "Access-Control-Allow-Origin", "*");
+ httpd_resp_set_hdr(req, "Access-Control-Allow-Methods", "GET, POST, OPTIONS");
+ httpd_resp_set_hdr(req, "Access-Control-Allow-Headers", "Content-Type");
+ httpd_resp_set_hdr(req, "Access-Control-Max-Age", "600");
+}
+
+/**
+ * @brief 处理 HTTP OPTIONS 预检请求,返回 204。
+ * @param req HTTP 请求对象。
+ * @return ESP_OK。
+ */
+static esp_err_t http_options_handler(httpd_req_t *req)
+{
+ http_set_cors_headers(req);
+ httpd_resp_set_status(req, "204 No Content");
+ httpd_resp_send(req, NULL, 0);
+ return ESP_OK;
+}
+
+// ----------------------------------------
+// WiFi/IP 事件回调(事件驱动连接)
+// ----------------------------------------
+static void wifi_event_handler(void* arg, esp_event_base_t event_base,
+ int32_t event_id, void* event_data)
+{
+ if (event_base == WIFI_EVENT) {
+ switch (event_id) {
+ case WIFI_EVENT_STA_START:
+ DEBUG_INFO("WIFI", "STA 已启动,开始连接...");
+ esp_wifi_connect();
+ break;
+
+ case WIFI_EVENT_STA_CONNECTED:
+ DEBUG_INFO("WIFI", "STA 已关联到 AP");
+ /** @brief STA 模式连接状态。 */
+ s_sta_connected = true;
+ /** @brief STA 连接重试次数。 */
+ s_sta_retry_count = 0;
+ if (s_sta_connected_sem) {
+ xSemaphoreGive(s_sta_connected_sem);
+ }
+ break;
+
+ case WIFI_EVENT_STA_DISCONNECTED: {
+ wifi_event_sta_disconnected_t* disconn = (wifi_event_sta_disconnected_t*)event_data;
+ DEBUG_WARN("WIFI", "STA 断开, reason=%d, retry=%d", disconn->reason, s_sta_retry_count);
+ /** @brief STA 模式连接状态。 */
+ s_sta_connected = false;
+
+ if (s_sta_retry_count < WIFI_STA_MAX_RETRY) {
+ s_sta_retry_count++;
+ DEBUG_INFO("WIFI", "重连第 %d 次...", s_sta_retry_count);
+ esp_wifi_connect();
+ } else {
+ DEBUG_ERR("WIFI", "STA 重连失败 %d 次,放弃", s_sta_retry_count);
+ }
+ break;
+ }
+
+ case WIFI_EVENT_AP_STACONNECTED: {
+ wifi_event_ap_staconnected_t* event = (wifi_event_ap_staconnected_t*)event_data;
+ DEBUG_INFO("WIFI", "AP: 客户端 " MACSTR " 连入, AID=%d", MAC2STR(event->mac), event->aid);
+ break;
+ }
+
+ case WIFI_EVENT_AP_STADISCONNECTED: {
+ wifi_event_ap_stadisconnected_t* event = (wifi_event_ap_stadisconnected_t*)event_data;
+ DEBUG_INFO("WIFI", "AP: 客户端 " MACSTR " 离开, AID=%d, reason=%d",
+ MAC2STR(event->mac), event->aid, event->reason);
+ break;
+ }
+
+ default:
+ break;
+ }
+ } else if (event_base == IP_EVENT) {
+ switch (event_id) {
+ case IP_EVENT_STA_GOT_IP: {
+ ip_event_got_ip_t* event = (ip_event_got_ip_t*)event_data;
+ DEBUG_INFO("WIFI", "STA 获取 IP: " IPSTR, IP2STR(&event->ip_info.ip));
+ if (s_sta_got_ip_sem) {
+ xSemaphoreGive(s_sta_got_ip_sem);
+ }
+ break;
+ }
+
+ default:
+ break;
+ }
+ }
+}
+
+// ----------------------------------------
+// WiFi AP 模式初始化
+/**
+ * @brief 初始化 SoftAP 模式,供局域网 OTA 使用。
+ * @return ESP_OK 成功,其他失败。
+ */
+static esp_err_t wifi_init_ap(void)
+{
+ DEBUG_INFO("WIFI", "=== 初始化 WiFi AP 模式 ===");
+
+ /** @brief 当前 WiFi 工作模式(AP 或 STA)。 */
+ s_wifi_mode = OTA_WIFI_MODE_AP;
+
+ esp_netif_t *ap_netif = esp_netif_get_handle_from_ifkey("WIFI_AP_DEF");
+ if (ap_netif == NULL) {
+ ap_netif = esp_netif_create_default_wifi_ap();
+ }
+ if (ap_netif != NULL) {
+ esp_netif_dhcps_stop(ap_netif);
+
+ esp_netif_ip_info_t ip_info;
+ memset(&ip_info, 0, sizeof(ip_info));
+ // 方案 A:SoftAP 使用 192.168.31.0/24 网段
+ inet_pton(AF_INET, "192.168.31.1", &ip_info.ip);
+ inet_pton(AF_INET, "192.168.31.1", &ip_info.gw);
+ inet_pton(AF_INET, "255.255.255.0", &ip_info.netmask);
+ ESP_ERROR_CHECK(esp_netif_set_ip_info(ap_netif, &ip_info));
+
+ esp_netif_dhcps_start(ap_netif);
+ DEBUG_INFO("WIFI", "AP IP 已设置: 192.168.31.1");
+ }
+
+ wifi_config_t wifi_config = {};
+ memcpy(wifi_config.ap.ssid, WIFI_AP_SSID, strlen(WIFI_AP_SSID) + 1);
+ wifi_config.ap.ssid_len = strlen(WIFI_AP_SSID);
+ memcpy(wifi_config.ap.password, WIFI_AP_PASSWORD, strlen(WIFI_AP_PASSWORD) + 1);
+ wifi_config.ap.max_connection = WIFI_AP_MAX_CONNECT;
+ wifi_config.ap.authmode = WIFI_AUTH_WPA_WPA2_PSK;
+ wifi_config.ap.channel = WIFI_AP_CHANNEL;
+
+ DEBUG_INFO("WIFI", "AP SSID: %s, Channel: %d", WIFI_AP_SSID, WIFI_AP_CHANNEL);
+
+ ESP_ERROR_CHECK(esp_wifi_set_mode(WIFI_MODE_AP));
+ ESP_ERROR_CHECK(esp_wifi_set_config(WIFI_IF_AP, &wifi_config));
+ ESP_ERROR_CHECK(esp_wifi_start());
+
+ DEBUG_INFO("WIFI", "WiFi AP 已启动: %s", WIFI_AP_SSID);
+ DEBUG_INFO("WIFI", "AP IP 地址: 192.168.31.1");
+ DEBUG_INFO("WIFI", "AP 网关地址: 192.168.31.1");
+ DEBUG_INFO("WIFI", "最大连接数: %d", WIFI_AP_MAX_CONNECT);
+
+ return ESP_OK;
+}
+
+// ----------------------------------------
+// WiFi STA 模式初始化
+/**
+ * @brief 初始化 STA 模式并连接预配置的路由器。
+ * @return ESP_OK 成功,其他失败。
+ */
+static esp_err_t wifi_init_sta(void)
+{
+ DEBUG_INFO("WIFI", "=== 初始化 WiFi STA 模式 ===");
+
+ /** @brief 当前 WiFi 工作模式(AP 或 STA)。 */
+ s_wifi_mode = OTA_WIFI_MODE_STA;
+ /** @brief STA 连接重试次数。 */
+ s_sta_retry_count = 0;
+
+ // ★ 设置国家代码:中国,允许信道 1-13
+ // ESP32 默认国家是 US(只允许 1-11),中国路由可能用 12/13
+ wifi_country_t country = {};
+ memcpy(country.cc, "CN", 3);
+ country.schan = 1;
+ country.nchan = 13;
+ country.policy = WIFI_COUNTRY_POLICY_MANUAL;
+
+ wifi_config_t wifi_config = {};
+ wifi_config.sta.scan_method = WIFI_ALL_CHANNEL_SCAN;
+ wifi_config.sta.sort_method = WIFI_CONNECT_AP_BY_SIGNAL;
+ wifi_config.sta.threshold.authmode = WIFI_AUTH_WPA2_PSK;
+ wifi_config.sta.pmf_cfg.capable = true;
+ wifi_config.sta.pmf_cfg.required = false;
+ wifi_config.sta.listen_interval = 3;
+ memcpy(wifi_config.sta.ssid, WIFI_STA_SSID, strlen(WIFI_STA_SSID) + 1);
+ memcpy(wifi_config.sta.password, WIFI_STA_PASSWORD, strlen(WIFI_STA_PASSWORD) + 1);
+
+ DEBUG_INFO("WIFI", "STA SSID: %s (全信道扫描, CN 国家代码)", WIFI_STA_SSID);
+
+ ESP_ERROR_CHECK(esp_wifi_set_country(&country));
+ ESP_ERROR_CHECK(esp_wifi_set_mode(WIFI_MODE_STA));
+ ESP_ERROR_CHECK(esp_wifi_set_config(WIFI_IF_STA, &wifi_config));
+
+ wifi_set_static_ip();
+
+ ESP_ERROR_CHECK(esp_wifi_start());
+ ESP_ERROR_CHECK(esp_wifi_set_ps(WIFI_PS_NONE));
+
+ DEBUG_INFO("WIFI", "WiFi STA 启动, SSID: %s, 事件驱动连接...", WIFI_STA_SSID);
+
+ return ESP_OK;
+}
+
+/**
+ * @brief 等待 STA 连接成功并获取 IP(静态 IP 等 CONNECTED,DHCP 等 GOT_IP)
+ * @param ip_str 输出缓冲区,存放获取到的 IP 字符串。
+ * @param len 缓冲区长度。
+ * @return ESP_OK 连接成功,其他失败。
+ */
+static esp_err_t wifi_wait_sta_connected(char *ip_str, size_t len)
+{
+ SemaphoreHandle_t wait_sem = WIFI_STA_USE_STATIC_IP ? s_sta_connected_sem : s_sta_got_ip_sem;
+ const char *wait_name = WIFI_STA_USE_STATIC_IP ? "STA_CONNECTED" : "STA_GOT_IP";
+
+ if (wait_sem == NULL) {
+ DEBUG_ERR("WIFI", "%s 信号量未创建", wait_name);
+ return ESP_ERR_INVALID_STATE;
+ }
+
+ TickType_t timeout_ticks = pdMS_TO_TICKS(WIFI_STA_CONNECT_TIMEOUT_MS);
+ if (xSemaphoreTake(wait_sem, timeout_ticks) == pdTRUE) {
+ esp_netif_ip_info_t ip_info;
+ esp_netif_t *netif = esp_netif_get_handle_from_ifkey("WIFI_STA_DEF");
+ if (netif != NULL && esp_netif_get_ip_info(netif, &ip_info) == ESP_OK) {
+ if (ip_str) {
+ snprintf(ip_str, len, IPSTR, IP2STR(&ip_info.ip));
+ }
+ DEBUG_INFO("WIFI", "%s 等待成功, 当前 IP: " IPSTR, wait_name, IP2STR(&ip_info.ip));
+ }
+ /** @brief STA 模式连接状态。 */
+ s_sta_connected = true;
+ return ESP_OK;
+ }
+
+ DEBUG_ERR("WIFI", "等待 %s 超时 (%d ms), 重连次数: %d", wait_name, WIFI_STA_CONNECT_TIMEOUT_MS, s_sta_retry_count);
+ return ESP_ERR_TIMEOUT;
+}
+
+/**
+ * @brief 设置 STA 静态 IP
+ */
+static void wifi_set_static_ip(void)
+{
+ if (WIFI_STA_USE_STATIC_IP) {
+ esp_netif_t *sta_netif = esp_netif_get_handle_from_ifkey("WIFI_STA_DEF");
+ if (sta_netif) {
+ DEBUG_INFO("WIFI", "STA 使用静态 IP 模式,停止 DHCP Client");
+ ESP_ERROR_CHECK(esp_netif_dhcpc_stop(sta_netif));
+
+ esp_netif_ip_info_t info;
+ memset(&info, 0, sizeof(info));
+ inet_pton(AF_INET, WIFI_STA_STATIC_IP, &info.ip);
+ inet_pton(AF_INET, WIFI_STA_STATIC_GW, &info.gw);
+ inet_pton(AF_INET, WIFI_STA_STATIC_NETMASK, &info.netmask);
+ ESP_ERROR_CHECK(esp_netif_set_ip_info(sta_netif, &info));
+ DEBUG_INFO("WIFI", "静态 IP 已设置: %s", WIFI_STA_STATIC_IP);
+
+ // 设置 DNS
+ esp_netif_dns_info_t dns_info;
+ memset(&dns_info, 0, sizeof(dns_info));
+ inet_pton(AF_INET, WIFI_STA_STATIC_DNS, &dns_info.ip.u_addr.ip4.addr);
+ ESP_ERROR_CHECK(esp_netif_set_dns_info(sta_netif, ESP_NETIF_DNS_MAIN, &dns_info));
+ DEBUG_INFO("WIFI", "DNS 已设置: %s", WIFI_STA_STATIC_DNS);
+ }
+ } else {
+ DEBUG_INFO("WIFI", "使用 DHCP 获取 IP");
+ }
+}
+
+// ----------------------------------------
+// ESP32 OTA 内部函数
+/**
+ * @brief 初始化 ESP32 自身的 OTA 写入流程。
+ * @param total_size
+ * @return ESP_OK 成功,其他失败。
+ */
+static esp_err_t esp32_ota_begin(size_t total_size)
+{
+ const esp_partition_t *boot_partition = esp_ota_get_running_partition();
+ DEBUG_INFO("OTA", "当前启动分区: %s, size=%u, address=0x%lx",
+ boot_partition->label, boot_partition->size, boot_partition->address);
+
+ s_esp32_update_partition = esp_partition_find_first(
+ ESP_PARTITION_TYPE_APP, ESP_PARTITION_SUBTYPE_APP_OTA_0, NULL);
+ if (s_esp32_update_partition == NULL ||
+ s_esp32_update_partition->address == boot_partition->address) {
+ s_esp32_update_partition = esp_partition_find_first(
+ ESP_PARTITION_TYPE_APP, ESP_PARTITION_SUBTYPE_APP_OTA_1, NULL);
+ }
+
+ if (s_esp32_update_partition == NULL) {
+ DEBUG_ERR("OTA", "找不到可用的 OTA 分区");
+ return ESP_FAIL;
+ }
+
+ DEBUG_INFO("OTA", "OTA 目标分区: %s, size=%u, address=0x%lx",
+ s_esp32_update_partition->label,
+ s_esp32_update_partition->size,
+ s_esp32_update_partition->address);
+
+ if (total_size > s_esp32_update_partition->size) {
+ DEBUG_ERR("OTA", "固件太大: %u > 分区大小: %u",
+ total_size, s_esp32_update_partition->size);
+ return ESP_FAIL;
+ }
+
+ esp_err_t err = esp_ota_begin(s_esp32_update_partition, total_size, &s_esp32_ota_handle);
+ if (err != ESP_OK) {
+ DEBUG_ERR("OTA", "esp_ota_begin 失败: %s", esp_err_to_name(err));
+ /** @brief ESP32 OTA 目标分区。 */
+ s_esp32_update_partition = NULL;
+ return err;
+ }
+
+ /** @brief ESP32 OTA 已接收字节数。 */
+ s_esp32_received = 0;
+ /** @brief ESP32 自身 OTA 写入进行中标志。 */
+ s_esp32_ota_active = true;
+ DEBUG_INFO("OTA", "ESP32 OTA 开始,目标分区: %s", s_esp32_update_partition->label);
+ return ESP_OK;
+}
+
+/**
+ * @brief 将接收到的数据块写入 ESP32 OTA 分区。
+ * @param data
+ * @param len
+ * @return ESP_OK 成功,其他失败。
+ */
+static esp_err_t esp32_ota_write(const uint8_t *data, size_t len)
+{
+ if (!s_esp32_ota_active || s_esp32_update_partition == NULL) {
+ return ESP_ERR_INVALID_STATE;
+ }
+ esp_err_t err = esp_ota_write(s_esp32_ota_handle, data, len);
+ if (err == ESP_OK) {
+ s_esp32_received += len;
+ }
+ return err;
+}
+
+/**
+ * @brief HTTP POST /ota/update - 完成 ESP32 OTA 并重启
+ * @param req
+ * @return
+ */
+static esp_err_t esp32_ota_update_handler(httpd_req_t *req)
+{
+ (void)req;
+ http_set_cors_headers(req);
+ if (!s_esp32_ota_active) {
+ httpd_resp_send_err(req, HTTPD_400_BAD_REQUEST, "OTA not started");
+ return ESP_FAIL;
+ }
+
+ DEBUG_INFO("OTA", "ESP32 OTA 完成, 共 %u 字节, 设置启动分区...", s_esp32_received);
+
+ esp_err_t err = esp_ota_end(s_esp32_ota_handle);
+ if (err != ESP_OK) {
+ DEBUG_ERR("OTA", "esp_ota_end 失败: %s", esp_err_to_name(err));
+ /** @brief ESP32 自身 OTA 写入进行中标志。 */
+ s_esp32_ota_active = false;
+ httpd_resp_send_err(req, HTTPD_500_INTERNAL_SERVER_ERROR, esp_err_to_name(err));
+ return ESP_FAIL;
+ }
+
+ err = esp_ota_set_boot_partition(s_esp32_update_partition);
+ if (err != ESP_OK) {
+ DEBUG_ERR("OTA", "esp_ota_set_boot_partition 失败: %s", esp_err_to_name(err));
+ /** @brief ESP32 自身 OTA 写入进行中标志。 */
+ s_esp32_ota_active = false;
+ httpd_resp_send_err(req, HTTPD_500_INTERNAL_SERVER_ERROR, esp_err_to_name(err));
+ return ESP_FAIL;
+ }
+
+ /** @brief ESP32 自身 OTA 写入进行中标志。 */
+ s_esp32_ota_active = false;
+ DEBUG_INFO("OTA", "ESP32 OTA 完成,重启中...");
+
+ const char resp[] = "{\"status\": \"ok\", \"message\": \"OTA finished, rebooting\"}";
+ httpd_resp_set_type(req, "application/json");
+ httpd_resp_send(req, resp, strlen(resp));
+
+ vTaskDelay(pdMS_TO_TICKS(500));
+ esp_restart();
+ return ESP_OK;
+}
+
+/**
+ * @brief HTTP GET /ota/update - 查询 ESP32 OTA 状态
+ * @param req
+ * @return
+ */
+static esp_err_t esp32_ota_status_handler(httpd_req_t *req)
+{
+ http_set_cors_headers(req);
+ char resp[128];
+ if (s_esp32_ota_active) {
+ snprintf(resp, sizeof(resp),
+ "{\"active\": true, \"received\": %u, \"partition\": \"%s\"}",
+ (unsigned int)s_esp32_received,
+ s_esp32_update_partition ? s_esp32_update_partition->label : "null");
+ } else {
+ snprintf(resp, sizeof(resp), "{\"active\": false}");
+ }
+ httpd_resp_set_type(req, "application/json");
+ httpd_resp_send(req, resp, strlen(resp));
+ return ESP_OK;
+}
+
+// 从 HTTP 流中读取完整的 JSON 头(以 } 结尾)
+/**
+ * @brief 返回 JSON 字符串长度,失败返回 0
+ * @param req
+ * @param buf
+ * @param buf_size
+ * @return
+ */
+static int recv_json_header(httpd_req_t *req, char *buf, int buf_size)
+{
+ int total = 0;
+ int depth = 1; // 开头的 '{' 已被 peek 消费,从 1 开始
+ while (total < buf_size - 1) {
+ int ret = httpd_req_recv(req, buf + total, 1); // 逐字节读,精确找到 JSON 结尾
+ if (ret <= 0) return 0;
+ total += ret;
+ buf[total] = '\0';
+ if (buf[total - 1] == '{') depth++;
+ else if (buf[total - 1] == '}') {
+ depth--;
+ if (depth == 0) return total; // JSON 头读完
+ }
+ }
+ return 0; // JSON 头太长,超出缓冲区
+}
+
+// ----------------------------------------
+// 统一 /ota/upload 处理器(自动识别目标)
+/**
+ * @brief HTTP 文件上传处理器,接收固件并写入 Flash。
+ * @param req
+ * @return ESP_OK 成功,其他失败。
+ */
+static esp_err_t ota_upload_handler(httpd_req_t *req)
+{
+ http_set_cors_headers(req);
+ DEBUG_INFO("WIFI", "=== HTTP: POST /ota/upload ===");
+ DEBUG_INFO("WIFI", "收到固件上传请求, 内容长度: %d 字节", req->content_len);
+
+ // 预读 1 字节判断是 JSON 还是 binary
+ char peek;
+ int peek_ret = httpd_req_recv(req, &peek, 1);
+ if (peek_ret <= 0) {
+ httpd_resp_send_err(req, HTTPD_500_INTERNAL_SERVER_ERROR, "接收失败");
+ return ESP_FAIL;
+ }
+
+ bool is_esp32_ota = false;
+ uint32_t total_size = 0;
+ int json_header_len = 0;
+
+ if (peek == '{') {
+ // JSON 模式:逐字节读完 JSON 头
+ char json_buf[256];
+ json_buf[0] = peek;
+ json_header_len = recv_json_header(req, json_buf + 1, sizeof(json_buf) - 1);
+ if (json_header_len > 0) {
+ json_header_len++; // 加上已读的 peek 字节
+ json_buf[json_header_len] = '\0';
+
+ cJSON *root = cJSON_Parse(json_buf);
+ if (root) {
+ cJSON *type_item = cJSON_GetObjectItem(root, "type");
+ if (type_item && cJSON_IsString(type_item) &&
+ strcmp(cJSON_GetStringValue(type_item), "esp32") == 0) {
+ is_esp32_ota = true;
+ DEBUG_INFO("WIFI", "检测到 ESP32 OTA, JSON 头长度: %d 字节", json_header_len);
+ }
+ cJSON *size_item = cJSON_GetObjectItem(root, "total_size");
+ if (size_item) {
+ total_size = (uint32_t)cJSON_GetNumberValue(size_item);
+ }
+ cJSON_Delete(root);
+ }
+ }
+ }
+
+ if (is_esp32_ota) {
+ // --- ESP32 OTA ---
+ if (total_size == 0) {
+ httpd_resp_send_err(req, HTTPD_400_BAD_REQUEST, "missing total_size");
+ return ESP_FAIL;
+ }
+
+ esp_err_t err = esp32_ota_begin(total_size);
+ if (err != ESP_OK) {
+ httpd_resp_send_err(req, HTTPD_500_INTERNAL_SERVER_ERROR, "OTA begin failed");
+ return ESP_FAIL;
+ }
+
+ // 堆分配缓冲区,避免栈溢出(HTTP server 栈仅 8KB)
+ uint8_t *fw_buf = (uint8_t *)malloc(ESP32_OTA_RECV_BLOCK_SIZE);
+ if (!fw_buf) {
+ httpd_resp_send_err(req, HTTPD_500_INTERNAL_SERVER_ERROR, "内存分配失败");
+ return ESP_FAIL;
+ }
+
+ size_t total_firmware_written = 0;
+ size_t total_received = json_header_len; // 已消费的字节数
+ int last_percent = 0;
+
+ while (total_received < req->content_len) {
+ size_t to_read = req->content_len - total_received;
+ if (to_read > ESP32_OTA_RECV_BLOCK_SIZE) to_read = ESP32_OTA_RECV_BLOCK_SIZE;
+
+ int ret = httpd_req_recv(req, (char *)fw_buf, to_read);
+ if (ret <= 0) break;
+
+ err = esp32_ota_write(fw_buf, ret);
+ if (err != ESP_OK) {
+ DEBUG_ERR("WIFI", "OTA write 失败");
+ break;
+ }
+ total_received += ret;
+ total_firmware_written += ret;
+
+ if (total_size > 0) {
+ int percent = (int)((total_firmware_written * 100) / total_size);
+ if (percent / 10 > last_percent / 10) {
+ last_percent = percent;
+ DEBUG_INFO("WIFI", "ESP32 OTA 进度: %u / %u (%d%%)",
+ (unsigned int)total_firmware_written, total_size, percent);
+ }
+ }
+ }
+
+ free(fw_buf);
+ DEBUG_INFO("WIFI", "ESP32 OTA 存储完成: %u 字节 (跳过 JSON 头 %d 字节)", (unsigned int)s_esp32_received, json_header_len);
+ const char resp[] = "{\"status\": \"ok\", \"message\": \"stored, call POST /ota/update to finish\", \"target\": \"esp32\"}";
+ httpd_resp_set_type(req, "application/json");
+ httpd_resp_send(req, resp, strlen(resp));
+ return ESP_OK;
+ }
+
+ // --- GD32 OTA(原逻辑)---
+ if (req->content_len > OTA_MAX_FIRMWARE_SIZE) {
+ DEBUG_ERR("WIFI", "固件太大: %d > %d", req->content_len, OTA_MAX_FIRMWARE_SIZE);
+ httpd_resp_send_err(req, HTTPD_500_INTERNAL_SERVER_ERROR, "固件太大");
+ return ESP_FAIL;
+ }
+
+ uint8_t *buf = (uint8_t *)malloc(OTA_BLOCK_SIZE);
+ int received = 0;
+
+ // peek 字节是 GD32 数据的第一字节,先放回缓冲区
+ uint8_t gd32_header[6];
+ gd32_header[0] = (uint8_t)peek;
+ int header_read = 1;
+ // 读剩余 5 字节凑齐 6 字节 GD32 头
+ if (req->content_len >= 6) {
+ int need = 6 - header_read;
+ int got = httpd_req_recv(req, (char *)gd32_header + header_read, need);
+ if (got > 0) header_read += got;
+ }
+
+ // 先处理已读的 header
+ if (header_read >= 6) {
+ uint16_t seq = gd32_header[0] | (gd32_header[1] << 8);
+ uint32_t fw_size = gd32_header[2] | (gd32_header[3] << 8) | (gd32_header[4] << 16) | (gd32_header[5] << 24);
+ if (seq == 0) {
+ ota_meta_t meta = { .total_size = fw_size, .stored_size = 0, .last_seq = 0,
+ .chunk_count = (fw_size + 4095) / 4096, .checksum = {0} };
+ meta_save(&meta);
+ DEBUG_INFO("WIFI", "开始 GD32 固件存储: %u 字节", fw_size);
+ }
+ }
+
+ // 继续读剩余数据
+ while (received + header_read < req->content_len) {
+ size_t to_read = req->content_len - received - header_read;
+ if (to_read > OTA_BLOCK_SIZE) to_read = OTA_BLOCK_SIZE;
+
+ int ret = httpd_req_recv(req, (char *)buf, to_read);
+ if (ret <= 0) break;
+
+ uint32_t flash_offset = ((received + header_read) / OTA_BLOCK_SIZE) * OTA_BLOCK_SIZE;
+ esp_err_t err = flash_write_chunk(flash_offset, buf, ret);
+ if (err == ESP_OK) {
+ meta_update(flash_offset + ret, (received + header_read) / OTA_BLOCK_SIZE);
+ }
+ received += ret;
+
+ uint32_t progress = ((received + header_read) * 100) / req->content_len;
+ if (progress % 10 == 0) {
+ DEBUG_INFO("WIFI", "GD32 上传进度: %d / %d (%u%%)",
+ received + header_read, req->content_len, progress);
+ }
+ }
+
+ free(buf);
+ DEBUG_INFO("WIFI", "GD32 固件存储完成: %d 字节", received + header_read);
+
+ ota_meta_t meta;
+ meta_load(&meta);
+ if (meta.stored_size >= meta.total_size) {
+ /** @brief GD32 固件存储完成标志。 */
+ s_gd32_storage_done = true;
+ DEBUG_INFO("WIFI", "GD32 固件已存储,触发转发...");
+ wifi_ota_signal_relay();
+ }
+
+ const char resp[] = "{\"status\": \"ok\", \"progress\": 100, \"target\": \"gd32\"}";
+ httpd_resp_set_type(req, "application/json");
+ httpd_resp_send(req, resp, strlen(resp));
+ return ESP_OK;
+}
+
+/**
+ * @brief HTTP POST /ota/relay 处理器
+ * @param req
+ * @return
+ */
+static esp_err_t ota_relay_trigger_handler(httpd_req_t *req)
+{
+ (void)req;
+ http_set_cors_headers(req);
+ DEBUG_INFO("WIFI", "=== HTTP: POST /ota/relay ===");
+ DEBUG_INFO("WIFI", "收到触发 OTA 中继请求");
+ wifi_ota_signal_relay();
+
+ const char resp[] = "{\"status\": \"ok\", \"message\": \"OTA relay triggered\"}";
+ httpd_resp_set_type(req, "application/json");
+ httpd_resp_send(req, resp, strlen(resp));
+
+ return ESP_OK;
+}
+
+/**
+ * @brief HTTP GET /ota/status 处理器
+ * @param req
+ * @return
+ */
+static esp_err_t ota_status_handler(httpd_req_t *req)
+{
+ http_set_cors_headers(req);
+ DEBUG_INFO("WIFI", "=== HTTP: GET /ota/status ===");
+
+ ota_meta_t meta;
+ meta_load(&meta);
+
+ char resp[256];
+ int len = snprintf(resp, sizeof(resp),
+ "{\"total_size\": %lu, \"stored_size\": %lu, \"last_seq\": %lu, "
+ "\"storage_done\": %s, \"relay_state\": %d}",
+ (unsigned long)meta.total_size, (unsigned long)meta.stored_size,
+ (unsigned long)meta.last_seq,
+ s_gd32_storage_done ? "true" : "false",
+ g_relay_state);
+
+ DEBUG_INFO("WIFI", "OTA 状态: total=%lu, stored=%lu, done=%s",
+ (unsigned long)meta.total_size, (unsigned long)meta.stored_size,
+ s_gd32_storage_done ? "true" : "false");
+
+ httpd_resp_set_type(req, "application/json");
+ httpd_resp_send(req, resp, len);
+
+ return ESP_OK;
+}
+
+/**
+ * @brief HTTP GET /health 处理器
+ * @param req
+ * @return
+ */
+static esp_err_t health_handler(httpd_req_t *req)
+{
+ http_set_cors_headers(req);
+ DEBUG_INFO("WIFI", "=== HTTP: GET /health ===");
+
+ health_state_t state = health_get_current();
+ uint32_t free_heap = esp_get_free_heap_size();
+
+ char resp[128];
+ int len = snprintf(resp, sizeof(resp),
+ "{\"health\": %d, \"free_heap\": %lu}",
+ state, (unsigned long)free_heap);
+
+ DEBUG_INFO("WIFI", "健康状态: %d, 空闲堆: %lu", state, (unsigned long)free_heap);
+
+ httpd_resp_set_type(req, "application/json");
+ httpd_resp_send(req, resp, len);
+
+ return ESP_OK;
+}
+
+/**
+ * @brief HTTP GET /logs - 拉取并清空日志缓冲区
+ * @param req
+ * @return
+ */
+static esp_err_t logs_handler(httpd_req_t *req)
+{
+ http_set_cors_headers(req);
+
+ int json_len = 0;
+ char *json = debug_log_ring_drain_json(&json_len);
+
+ if (json && json_len > 0) {
+ httpd_resp_set_type(req, "application/json");
+ httpd_resp_send(req, json, json_len);
+ free(json);
+ } else {
+ httpd_resp_set_type(req, "application/json");
+ httpd_resp_send(req, "[]", 2);
+ }
+ return ESP_OK;
+}
+
+// HTTP POST /can/send 处理器
+// 上位机发送 CAN 帧,经 ESP32 转发到 GD32
+// 请求体: {"ch": 0, "id": 123456, "data": "0102030405060708", "dlc": 8}
+/**
+ * @brief data 为 hex 字符串(可不填,自动用 dlc 决定实际发送长度)
+ * @param req
+ * @return
+ */
+static esp_err_t can_send_handler(httpd_req_t *req)
+{
+ http_set_cors_headers(req);
+ char buf[256] = {0};
+ int ret = httpd_req_recv(req, buf, sizeof(buf) - 1);
+ if (ret <= 0) {
+ httpd_resp_send_err(req, HTTPD_400_BAD_REQUEST, "接收数据失败");
+ return ESP_FAIL;
+ }
+ buf[ret] = '\0';
+
+ cJSON *root = cJSON_Parse(buf);
+ if (!root) {
+ httpd_resp_send_err(req, HTTPD_400_BAD_REQUEST, "JSON 解析失败");
+ return ESP_FAIL;
+ }
+
+ cJSON *ch_item = cJSON_GetObjectItem(root, "ch");
+ cJSON *id_item = cJSON_GetObjectItem(root, "id");
+ cJSON *data_item = cJSON_GetObjectItem(root, "data");
+ cJSON *dlc_item = cJSON_GetObjectItem(root, "dlc");
+
+ if (!ch_item || !id_item) {
+ cJSON_Delete(root);
+ httpd_resp_send_err(req, HTTPD_400_BAD_REQUEST, "缺少 ch 或 id 字段");
+ return ESP_FAIL;
+ }
+
+ can_channel_t ch = (can_channel_t)cJSON_GetNumberValue(ch_item);
+ uint32_t ext_id = (uint32_t)cJSON_GetNumberValue(id_item);
+ uint8_t dlc = dlc_item ? (uint8_t)cJSON_GetNumberValue(dlc_item) : 0;
+
+ uint8_t tx_data[CAN_MAX_DLC] = {0};
+ uint8_t tx_dlc = 0;
+
+ if (data_item && cJSON_IsString(data_item)) {
+ const char *hex_str = cJSON_GetStringValue(data_item);
+ size_t hex_len = strlen(hex_str);
+ for (size_t i = 0; i < hex_len / 2 && i < CAN_MAX_DLC; i++) {
+ char byte_str[3] = { hex_str[i * 2], hex_str[i * 2 + 1], '\0' };
+ tx_data[i] = (uint8_t)strtol(byte_str, NULL, 16);
+ tx_dlc++;
+ }
+ }
+
+ if (tx_dlc == 0 && dlc > 0) {
+ tx_dlc = (dlc > CAN_MAX_DLC) ? CAN_MAX_DLC : dlc;
+ }
+
+ cJSON_Delete(root);
+
+ // 发送 CAN 帧到 GD32
+ uint8_t resp_data[CAN_MAX_DLC] = {0};
+ int resp_len = gd32_can_send(ch, ext_id, tx_data, tx_dlc,
+ resp_data, sizeof(resp_data), 1000);
+
+ char resp[512];
+ if (resp_len >= 0) {
+ // 将响应 data 转为 hex 字符串
+ char resp_hex[CAN_MAX_DLC * 2 + 1] = {0};
+ for (int i = 0; i < resp_len; i++) {
+ sprintf(&resp_hex[i * 2], "%02X", resp_data[i]);
+ }
+ snprintf(resp, sizeof(resp),
+ "{\"status\": \"ok\", \"ch\": %d, \"id\": %lu, \"sent\": %d, \"recv_len\": %d, \"recv\": \"%s\"}",
+ ch, (unsigned long)ext_id, tx_dlc, resp_len, resp_hex);
+ } else {
+ snprintf(resp, sizeof(resp),
+ "{\"status\": \"error\", \"code\": %d}", resp_len);
+ }
+
+ httpd_resp_set_type(req, "application/json");
+ httpd_resp_send(req, resp, strlen(resp));
+ return ESP_OK;
+}
+
+/**
+ * @brief HTTP GET /can/status 处理器
+ * @param req
+ * @return
+ */
+static esp_err_t can_status_handler(httpd_req_t *req)
+{
+ http_set_cors_headers(req);
+ char resp[128];
+ snprintf(resp, sizeof(resp),
+ "{\"channels\": 3, \"max_dlc\": %d, \"spi_init\": %s}",
+ CAN_MAX_DLC,
+ g_spi_device != NULL ? "true" : "false");
+ httpd_resp_set_type(req, "application/json");
+ httpd_resp_send(req, resp, strlen(resp));
+ return ESP_OK;
+}
+
+/**
+ * @brief HTTP GET /wifi/status 处理器
+ * @param req
+ * @return
+ */
+static esp_err_t wifi_status_handler(httpd_req_t *req)
+{
+ http_set_cors_headers(req);
+ char resp[256];
+ char ip_str[16] = "0.0.0.0";
+ int8_t rssi = -127;
+
+ if (s_wifi_mode == OTA_WIFI_MODE_STA && s_sta_connected) {
+ esp_netif_ip_info_t ip_info;
+ esp_netif_t *sta_netif = esp_netif_get_handle_from_ifkey("WIFI_STA_DEF");
+ if (sta_netif && esp_netif_get_ip_info(sta_netif, &ip_info) == ESP_OK) {
+ snprintf(ip_str, sizeof(ip_str), IPSTR, IP2STR(&ip_info.ip));
+ }
+ wifi_ap_record_t ap_info;
+ if (esp_wifi_sta_get_ap_info(&ap_info) == ESP_OK) {
+ rssi = ap_info.rssi;
+ }
+ }
+
+ int len = snprintf(resp, sizeof(resp),
+ "{\"mode\": \"%s\", \"ip\": \"%s\", \"rssi\": %d, \"connected\": %s}",
+ s_wifi_mode == OTA_WIFI_MODE_AP ? "AP" : "STA",
+ ip_str, rssi,
+ (s_wifi_mode == OTA_WIFI_MODE_STA) ? (s_sta_connected ? "true" : "false") : "N/A");
+
+ httpd_resp_set_type(req, "application/json");
+ httpd_resp_send(req, resp, len);
+
+ return ESP_OK;
+}
+
+/**
+ * @brief HTTP GET /system/status 处理器
+ * @param req
+ * @return
+ */
+static esp_err_t system_status_handler(httpd_req_t *req)
+{
+ http_set_cors_headers(req);
+
+ char ip_str[16] = "0.0.0.0";
+ int8_t rssi = -127;
+ bool connected = false;
+
+ if (s_wifi_mode == OTA_WIFI_MODE_STA && s_sta_connected) {
+ esp_netif_ip_info_t ip_info;
+ esp_netif_t *sta_netif = esp_netif_get_handle_from_ifkey("WIFI_STA_DEF");
+ if (sta_netif && esp_netif_get_ip_info(sta_netif, &ip_info) == ESP_OK) {
+ snprintf(ip_str, sizeof(ip_str), IPSTR, IP2STR(&ip_info.ip));
+ }
+ wifi_ap_record_t ap_info;
+ if (esp_wifi_sta_get_ap_info(&ap_info) == ESP_OK) {
+ rssi = ap_info.rssi;
+ }
+ connected = true;
+ }
+
+ ota_meta_t meta;
+ memset(&meta, 0, sizeof(meta));
+ meta_load(&meta);
+
+ char resp[640];
+ int len = snprintf(resp, sizeof(resp),
+ "{\"version\": \"%s\", \"health\": %d, \"free_heap\": %lu, \"min_free_heap\": %lu, \"temperature\": %.1f, \"wifi\": {\"mode\": \"%s\", \"ip\": \"%s\", \"rssi\": %d, \"connected\": %s}, \"ota\": {\"total_size\": %lu, \"stored_size\": %lu, \"storage_done\": %s, \"relay_state\": %d}}",
+ FW_VERSION,health_get_current(),
+ (unsigned long)esp_get_free_heap_size(),
+ (unsigned long)esp_get_minimum_free_heap_size(),
+ (double)health_get_last_temperature(),
+ s_wifi_mode == OTA_WIFI_MODE_AP ? "AP" : "STA",
+ ip_str,
+ rssi,
+ connected ? "true" : "false",
+ (unsigned long)meta.total_size,
+ (unsigned long)meta.stored_size,
+ s_gd32_storage_done ? "true" : "false",
+ g_relay_state);
+
+ httpd_resp_set_type(req, "application/json");
+ httpd_resp_send(req, resp, len);
+ return ESP_OK;
+}
+
+// ----------------------------------------
+// 启动 HTTP OTA 服务器
+/**
+ * @brief 启动 HTTP OTA 服务器,注册各接口路由。
+ * @return ESP_OK 成功,其他失败。
+ */
+static esp_err_t http_ota_server_start(void)
+{
+ httpd_config_t config = HTTPD_DEFAULT_CONFIG();
+ config.server_port = HTTP_SERVER_PORT;
+ config.stack_size = HTTP_SERVER_STACK_SIZE;
+ config.max_uri_handlers = 32; // 现有业务端点 + 对应 OPTIONS 预检
+
+ DEBUG_INFO("WIFI", "=== 启动 HTTP OTA 服务器 ===");
+ DEBUG_INFO("WIFI", "HTTP 服务器端口: %d", config.server_port);
+
+ esp_err_t err = httpd_start(&s_http_server, &config);
+ if (err != ESP_OK) {
+ DEBUG_ERR("WIFI", "HTTP 服务器启动失败: %s", esp_err_to_name(err));
+ return err;
+ }
+
+ httpd_uri_t upload_uri = {
+ .uri = "/ota/upload",
+ .method = HTTP_POST,
+ .handler = ota_upload_handler,
+ .user_ctx = NULL
+ };
+ httpd_register_uri_handler(s_http_server, &upload_uri);
+ httpd_uri_t upload_options_uri = {
+ .uri = "/ota/upload",
+ .method = HTTP_OPTIONS,
+ .handler = http_options_handler,
+ .user_ctx = NULL
+ };
+ httpd_register_uri_handler(s_http_server, &upload_options_uri);
+
+ httpd_uri_t relay_uri = {
+ .uri = "/ota/relay",
+ .method = HTTP_POST,
+ .handler = ota_relay_trigger_handler,
+ .user_ctx = NULL
+ };
+ httpd_register_uri_handler(s_http_server, &relay_uri);
+ httpd_uri_t relay_options_uri = {
+ .uri = "/ota/relay",
+ .method = HTTP_OPTIONS,
+ .handler = http_options_handler,
+ .user_ctx = NULL
+ };
+ httpd_register_uri_handler(s_http_server, &relay_options_uri);
+
+ httpd_uri_t status_uri = {
+ .uri = "/ota/status",
+ .method = HTTP_GET,
+ .handler = ota_status_handler,
+ .user_ctx = NULL
+ };
+ httpd_register_uri_handler(s_http_server, &status_uri);
+ httpd_uri_t status_options_uri = {
+ .uri = "/ota/status",
+ .method = HTTP_OPTIONS,
+ .handler = http_options_handler,
+ .user_ctx = NULL
+ };
+ httpd_register_uri_handler(s_http_server, &status_options_uri);
+
+ httpd_uri_t esp32_update_uri = {
+ .uri = "/ota/update",
+ .method = HTTP_POST,
+ .handler = esp32_ota_update_handler,
+ .user_ctx = NULL
+ };
+ httpd_register_uri_handler(s_http_server, &esp32_update_uri);
+ httpd_uri_t esp32_update_options_uri = {
+ .uri = "/ota/update",
+ .method = HTTP_OPTIONS,
+ .handler = http_options_handler,
+ .user_ctx = NULL
+ };
+ httpd_register_uri_handler(s_http_server, &esp32_update_options_uri);
+
+ httpd_uri_t esp32_status_uri = {
+ .uri = "/ota/esp32_status",
+ .method = HTTP_GET,
+ .handler = esp32_ota_status_handler,
+ .user_ctx = NULL
+ };
+ httpd_register_uri_handler(s_http_server, &esp32_status_uri);
+ httpd_uri_t esp32_status_options_uri = {
+ .uri = "/ota/esp32_status",
+ .method = HTTP_OPTIONS,
+ .handler = http_options_handler,
+ .user_ctx = NULL
+ };
+ httpd_register_uri_handler(s_http_server, &esp32_status_options_uri);
+
+ httpd_uri_t health_uri = {
+ .uri = "/health",
+ .method = HTTP_GET,
+ .handler = health_handler,
+ .user_ctx = NULL
+ };
+ httpd_register_uri_handler(s_http_server, &health_uri);
+ httpd_uri_t health_options_uri = {
+ .uri = "/health",
+ .method = HTTP_OPTIONS,
+ .handler = http_options_handler,
+ .user_ctx = NULL
+ };
+ httpd_register_uri_handler(s_http_server, &health_options_uri);
+
+ httpd_uri_t logs_uri = {
+ .uri = "/logs",
+ .method = HTTP_GET,
+ .handler = logs_handler,
+ .user_ctx = NULL
+ };
+ httpd_register_uri_handler(s_http_server, &logs_uri);
+ httpd_uri_t logs_options_uri = {
+ .uri = "/logs",
+ .method = HTTP_OPTIONS,
+ .handler = http_options_handler,
+ .user_ctx = NULL
+ };
+ httpd_register_uri_handler(s_http_server, &logs_options_uri);
+
+ httpd_uri_t wifi_status_uri = {
+ .uri = "/wifi/status",
+ .method = HTTP_GET,
+ .handler = wifi_status_handler,
+ .user_ctx = NULL
+ };
+ httpd_register_uri_handler(s_http_server, &wifi_status_uri);
+ httpd_uri_t wifi_status_options_uri = {
+ .uri = "/wifi/status",
+ .method = HTTP_OPTIONS,
+ .handler = http_options_handler,
+ .user_ctx = NULL
+ };
+ httpd_register_uri_handler(s_http_server, &wifi_status_options_uri);
+
+ httpd_uri_t system_status_uri = {
+ .uri = "/system/status",
+ .method = HTTP_GET,
+ .handler = system_status_handler,
+ .user_ctx = NULL
+ };
+ httpd_register_uri_handler(s_http_server, &system_status_uri);
+ httpd_uri_t system_status_options_uri = {
+ .uri = "/system/status",
+ .method = HTTP_OPTIONS,
+ .handler = http_options_handler,
+ .user_ctx = NULL
+ };
+ httpd_register_uri_handler(s_http_server, &system_status_options_uri);
+
+ // CAN 帧转发
+ httpd_uri_t can_send_uri = {
+ .uri = "/can/send",
+ .method = HTTP_POST,
+ .handler = can_send_handler,
+ .user_ctx = NULL
+ };
+ httpd_register_uri_handler(s_http_server, &can_send_uri);
+ httpd_uri_t can_send_options_uri = {
+ .uri = "/can/send",
+ .method = HTTP_OPTIONS,
+ .handler = http_options_handler,
+ .user_ctx = NULL
+ };
+ httpd_register_uri_handler(s_http_server, &can_send_options_uri);
+
+ httpd_uri_t can_status_uri = {
+ .uri = "/can/status",
+ .method = HTTP_GET,
+ .handler = can_status_handler,
+ .user_ctx = NULL
+ };
+ httpd_register_uri_handler(s_http_server, &can_status_uri);
+ httpd_uri_t can_status_options_uri = {
+ .uri = "/can/status",
+ .method = HTTP_OPTIONS,
+ .handler = http_options_handler,
+ .user_ctx = NULL
+ };
+ httpd_register_uri_handler(s_http_server, &can_status_options_uri);
+
+ DEBUG_INFO("WIFI", "HTTP 服务器已启动");
+ DEBUG_INFO("WIFI", "注册端点: /ota/upload, /ota/relay, /ota/update, /ota/status, /ota/esp32_status, /health, /wifi/status, /system/status, /can/send, /can/status");
+
+ return ESP_OK;
+}
+
+// ----------------------------------------
+// 公开 API
+/**
+ * @brief 通知 OTA 中继开始向 GD32 转发固件。
+ */
+void wifi_ota_signal_relay(void)
+{
+ if (s_relay_triggered) {
+ DEBUG_WARN("WIFI", "转发已经触发过");
+ return;
+ }
+
+ /** @brief OTA 中继是否已触发标志。 */
+ s_relay_triggered = true;
+
+ if (g_ota_dispatch_queue) {
+ uint32_t msg = 1;
+ xQueueSend(g_ota_dispatch_queue, &msg, pdMS_TO_TICKS(100));
+ DEBUG_INFO("WIFI", "OTA 调度信号已发送到中继任务");
+ }
+}
+
+/**
+ * @brief 获取当前 WiFi 工作模式(AP 或 STA)。
+ * @return 当前 WiFi 模式枚举值。
+ */
+ota_wifi_mode_t wifi_get_mode(void)
+{
+ /** @brief 当前 WiFi 工作模式(AP 或 STA)。 */
+ return s_wifi_mode;
+}
+
+/**
+ * @brief 获取当前 WiFi 信号强度。
+ * @return 信号强度(dBm),未连接时返回无效值。
+ */
+int8_t wifi_get_rssi(void)
+{
+ if (s_wifi_mode != OTA_WIFI_MODE_STA || !s_sta_connected) {
+ return -127;
+ }
+ wifi_ap_record_t ap_info;
+ if (esp_wifi_sta_get_ap_info(&ap_info) == ESP_OK) {
+ return ap_info.rssi;
+ }
+ return -127;
+}
+
+/**
+ * @brief 判断 STA 是否已连接到上级 WiFi。
+ * @return true 已连接,false 未连接。
+ */
+bool wifi_is_connected(void)
+{
+ /** @brief STA 模式连接状态。 */
+ return s_sta_connected;
+}
+
+// ----------------------------------------
+// ★ WiFi 早期初始化(在 app_main 调度器启动前调用)
+// 包括:NVS → netif → event handler → WiFi driver → 配置 → connect
+// 返回:true = STA 模式已启动(正在连接),false = 需要切 AP
+/**
+ * @brief 在调度器启动前初始化 WiFi 并尝试 STA 连接。
+ * @return true STA 初始化中,false 需回退 AP。
+ */
+bool wifi_early_init(void)
+{
+ DEBUG_INFO("WIFI", "=== WiFi 早期初始化 (调度器启动前) ===");
+
+ esp_err_t err = nvs_flash_init();
+ if (err == ESP_ERR_NVS_NO_FREE_PAGES || err == ESP_ERR_NVS_NEW_VERSION_FOUND) {
+ // NVS 分区损坏或版本不匹配,擦除后重试
+ DEBUG_WARN("WIFI", "NVS 分区异常 (%s),擦除后重试", esp_err_to_name(err));
+ ESP_ERROR_CHECK(nvs_flash_erase());
+ err = nvs_flash_init();
+ }
+ if (err != ESP_OK) {
+ DEBUG_ERR("WIFI", "NVS 初始化失败: %s", esp_err_to_name(err));
+ } else {
+ DEBUG_INFO("WIFI", "NVS 初始化成功");
+ }
+
+ /** @brief STA 连接成功信号量。 */
+ s_sta_connected_sem = xSemaphoreCreateBinary();
+ /** @brief STA 获取 IP 信号量。 */
+ s_sta_got_ip_sem = xSemaphoreCreateBinary();
+ if (!s_sta_connected_sem || !s_sta_got_ip_sem) {
+ DEBUG_ERR("WIFI", "创建信号量失败");
+ }
+
+ // 注册 WiFi/IP 事件回调(必须在 esp_wifi_init 之前)
+ ESP_ERROR_CHECK(esp_event_handler_register(WIFI_EVENT, ESP_EVENT_ANY_ID, &wifi_event_handler, NULL));
+ ESP_ERROR_CHECK(esp_event_handler_register(IP_EVENT, ESP_EVENT_ANY_ID, &wifi_event_handler, NULL));
+ DEBUG_INFO("WIFI", "WiFi/IP 事件回调已注册");
+
+ esp_netif_t *sta_netif = esp_netif_get_handle_from_ifkey("WIFI_STA_DEF");
+ if (sta_netif == NULL) {
+ sta_netif = esp_netif_create_default_wifi_sta();
+ if (sta_netif == NULL) {
+ DEBUG_ERR("WIFI", "创建 STA netif 失败");
+ } else {
+ DEBUG_INFO("WIFI", "STA netif 已创建");
+ }
+ } else {
+ DEBUG_INFO("WIFI", "STA netif 已存在,跳过创建");
+ }
+
+ wifi_init_config_t wifi_cfg = WIFI_INIT_CONFIG_DEFAULT();
+ ESP_ERROR_CHECK(esp_wifi_init(&wifi_cfg));
+ DEBUG_INFO("WIFI", "WiFi driver 初始化成功");
+
+ // STA 优先,失败时返回 false 让 task 切 AP
+ if (CONFIG_WIFI_MODE == 1) {
+ DEBUG_INFO("WIFI", "工作模式: STA (优先)");
+ err = wifi_init_sta();
+ if (err == ESP_OK) {
+ // 连接已触发(STA_START 事件驱动),返回 true
+ // 实际连接状态由 wifi_ota_task 等待确认
+ return true;
+ }
+ DEBUG_WARN("WIFI", "STA 初始化失败,将切换 AP");
+ return false;
+ } else {
+ DEBUG_INFO("WIFI", "工作模式: AP");
+ wifi_init_ap();
+ return false; // AP 模式,不需要等待连接
+ }
+}
+
+// ----------------------------------------
+// WiFi OTA FreeRTOS 任务
+// WiFi 初始化已在 app_main 中通过 wifi_early_init() 完成
+// 此任务负责:等待连接结果 → 启动 HTTP 服务器 → 状态监控
+/**
+ * @brief WiFi OTA FreeRTOS 任务,提供 HTTP 上传和中继触发。
+ * @param pvParameters FreeRTOS 任务参数,未使用。
+ */
+void wifi_ota_task(void *pvParameters)
+{
+ (void)pvParameters;
+
+ DEBUG_INFO("WIFI", "=== WiFi OTA 任务启动 ===");
+
+ // 等待 STA 连接结果(wifi_early_init 已触发连接)
+ if (wifi_get_mode() == OTA_WIFI_MODE_STA) {
+ char ip_str[16] = {0};
+ esp_err_t err = wifi_wait_sta_connected(ip_str, sizeof(ip_str));
+ if (err == ESP_OK) {
+ DEBUG_INFO("WIFI", "STA 已连接, IP: %s", ip_str);
+ } else {
+ DEBUG_WARN("WIFI", "STA 连接超时,切换到 AP 模式...");
+ esp_wifi_stop(); // 先停 STA,再切 AP
+ /** @brief 当前 WiFi 工作模式(AP 或 STA)。 */
+ s_wifi_mode = OTA_WIFI_MODE_AP;
+ wifi_init_ap();
+ }
+ }
+ // AP 模式:wifi_early_init 已完成,无需额外操作
+
+ esp_err_t err = http_ota_server_start();
+ if (err != ESP_OK) {
+ DEBUG_ERR("WIFI", "HTTP 服务器启动失败");
+ }
+
+ // ★ WiFi 已连接,UDP 从这里开始能通
+ // 等待网络栈稳定(ARP 表建立、路由就绪),避免首批 UDP 包丢失
+ vTaskDelay(pdMS_TO_TICKS(500));
+
+ // 启动摘要,发两遍确保上位机收到(第一包可能触发 ARP 被丢)
+ esp_chip_info_t chip;
+ esp_chip_info(&chip);
+ for (int i = 0; i < 2; i++) {
+ DEBUG_INFO("WIFI", "========================================");
+ DEBUG_INFO("WIFI", "[启动摘要] ESP32-C3 OTA Relay v%s", FW_VERSION);
+ DEBUG_INFO("WIFI", "[启动摘要] Chip: %s, Rev %d", CONFIG_IDF_TARGET, chip.revision);
+ DEBUG_INFO("WIFI", "[启动摘要] Free heap: %lu bytes", (unsigned long)esp_get_free_heap_size());
+ DEBUG_INFO("WIFI", "[启动摘要] WiFi: %s", wifi_get_mode() == OTA_WIFI_MODE_STA ? "STA" : "AP");
+ DEBUG_INFO("WIFI", "[启动摘要] NVS + WiFi driver + netif + event handler 初始化完成");
+ DEBUG_INFO("WIFI", "[启动摘要] HTTP 服务器端口: %d", HTTP_SERVER_PORT);
+ DEBUG_INFO("WIFI", "========================================");
+ DEBUG_INFO("WIFI", "=== WiFi OTA 服务器就绪 ===");
+ if (i == 0) vTaskDelay(pdMS_TO_TICKS(200));
+ }
+
+ // WiFi 状态监控
+ // 每秒一次循环:每 10 秒检查 RSSI
+ uint32_t tick = 0;
+ while (1) {
+ tick++;
+
+ // 每 10 秒检查一次 RSSI
+ if (tick % 10 == 0) {
+ int8_t rssi = wifi_get_rssi();
+ if (rssi != -127) {
+ health_update_from_wifi_rssi(rssi);
+
+ if (rssi < WIFI_RSSI_CRITICAL) {
+ DEBUG_WARN("WIFI", "WiFi RSSI 危险: %d dBm (阈值: %d)", rssi, WIFI_RSSI_CRITICAL);
+ } else if (rssi < WIFI_RSSI_WARNING) {
+ DEBUG_WARN("WIFI", "WiFi RSSI 较弱: %d dBm", rssi);
+ }
+ }
+ }
+
+ vTaskDelay(pdMS_TO_TICKS(1000));
+ }
+}
diff --git a/_assistant_backups/2026-05-05/wifi_ota.cpp.bak_2026-05-05_0525 b/_assistant_backups/2026-05-05/wifi_ota.cpp.bak_2026-05-05_0525
new file mode 100644
index 0000000..715454a
--- /dev/null
+++ b/_assistant_backups/2026-05-05/wifi_ota.cpp.bak_2026-05-05_0525
@@ -0,0 +1,1368 @@
+/*
+ * wifi_ota.c - WiFi OTA 服务器实现
+ * T4.1 - T4.5
+ */
+#include
+#include "freertos/FreeRTOS.h"
+#include "freertos/task.h"
+#include "freertos/semphr.h"
+#include "esp_wifi.h"
+#include "esp_netif.h"
+#include "esp_http_server.h"
+#include "esp_system.h"
+#include "esp_chip_info.h"
+#include "esp_timer.h"
+#include "esp_ota_ops.h"
+#include "nvs_flash.h"
+#include "lwip/netdb.h"
+#include "esp_netif.h"
+#include "esp_netif_types.h"
+#include "esp_event.h"
+#include "esp_mac.h"
+
+#include "app_config.h"
+#include "wifi_ota.h"
+#include "ota_relay.h"
+#include "health_monitor.h"
+#include "monitor/debug_log.h"
+#include "spi_master.h"
+#include "cJSON.h"
+
+/** @brief 当前 WiFi 工作模式(AP 或 STA)。 */
+static ota_wifi_mode_t s_wifi_mode = OTA_WIFI_MODE_AP;
+/** @brief OTA 中继是否已触发标志。 */
+static bool s_relay_triggered = false;
+/** @brief STA 模式连接状态。 */
+static bool s_sta_connected = false;
+/** @brief STA 连接重试次数。 */
+static int s_sta_retry_count = 0;
+/** @brief STA 连接成功信号量。 */
+static SemaphoreHandle_t s_sta_connected_sem = NULL;
+/** @brief STA 获取 IP 信号量。 */
+static SemaphoreHandle_t s_sta_got_ip_sem = NULL;
+
+// ESP32 OTA 状态
+static bool s_esp32_ota_active = false;
+/** @brief ESP32 OTA 写入句柄。 */
+static esp_ota_handle_t s_esp32_ota_handle = 0;
+static const esp_partition_t *s_esp32_update_partition = NULL;
+/** @brief ESP32 OTA 已接收字节数。 */
+static size_t s_esp32_received = 0;
+
+// GD32 OTA 存储状态
+static bool s_gd32_storage_done = false;
+
+/** @brief HTTP 服务器句柄。 */
+static httpd_handle_t s_http_server = NULL;
+
+/**
+ * @brief 预先声明:为 STA 模式配置静态 IP、网关和子网掩码。
+ */
+static void wifi_set_static_ip(void);
+
+/**
+ * @brief 为 HTTP 响应添加跨域访问控制头。
+ * @param req HTTP 请求对象。
+ */
+static void http_set_cors_headers(httpd_req_t *req)
+{
+ httpd_resp_set_hdr(req, "Access-Control-Allow-Origin", "*");
+ httpd_resp_set_hdr(req, "Access-Control-Allow-Methods", "GET, POST, OPTIONS");
+ httpd_resp_set_hdr(req, "Access-Control-Allow-Headers", "Content-Type");
+ httpd_resp_set_hdr(req, "Access-Control-Max-Age", "600");
+}
+
+/**
+ * @brief 处理 HTTP OPTIONS 预检请求,返回 204。
+ * @param req HTTP 请求对象。
+ * @return ESP_OK。
+ */
+static esp_err_t http_options_handler(httpd_req_t *req)
+{
+ http_set_cors_headers(req);
+ httpd_resp_set_status(req, "204 No Content");
+ httpd_resp_send(req, NULL, 0);
+ return ESP_OK;
+}
+
+// ----------------------------------------
+// WiFi/IP 事件回调(事件驱动连接)
+// ----------------------------------------
+static void wifi_event_handler(void* arg, esp_event_base_t event_base,
+ int32_t event_id, void* event_data)
+{
+ if (event_base == WIFI_EVENT) {
+ switch (event_id) {
+ case WIFI_EVENT_STA_START:
+ DEBUG_INFO("WIFI", "STA 已启动,开始连接...");
+ esp_wifi_connect();
+ break;
+
+ case WIFI_EVENT_STA_CONNECTED:
+ DEBUG_INFO("WIFI", "STA 已关联到 AP");
+ /** @brief STA 模式连接状态。 */
+ s_sta_connected = true;
+ /** @brief STA 连接重试次数。 */
+ s_sta_retry_count = 0;
+ if (s_sta_connected_sem) {
+ xSemaphoreGive(s_sta_connected_sem);
+ }
+ break;
+
+ case WIFI_EVENT_STA_DISCONNECTED: {
+ wifi_event_sta_disconnected_t* disconn = (wifi_event_sta_disconnected_t*)event_data;
+ DEBUG_WARN("WIFI", "STA 断开, reason=%d, retry=%d", disconn->reason, s_sta_retry_count);
+ /** @brief STA 模式连接状态。 */
+ s_sta_connected = false;
+
+ if (s_sta_retry_count < WIFI_STA_MAX_RETRY) {
+ s_sta_retry_count++;
+ DEBUG_INFO("WIFI", "重连第 %d 次...", s_sta_retry_count);
+ esp_wifi_connect();
+ } else {
+ DEBUG_ERR("WIFI", "STA 重连失败 %d 次,放弃", s_sta_retry_count);
+ }
+ break;
+ }
+
+ case WIFI_EVENT_AP_STACONNECTED: {
+ wifi_event_ap_staconnected_t* event = (wifi_event_ap_staconnected_t*)event_data;
+ DEBUG_INFO("WIFI", "AP: 客户端 " MACSTR " 连入, AID=%d", MAC2STR(event->mac), event->aid);
+ break;
+ }
+
+ case WIFI_EVENT_AP_STADISCONNECTED: {
+ wifi_event_ap_stadisconnected_t* event = (wifi_event_ap_stadisconnected_t*)event_data;
+ DEBUG_INFO("WIFI", "AP: 客户端 " MACSTR " 离开, AID=%d, reason=%d",
+ MAC2STR(event->mac), event->aid, event->reason);
+ break;
+ }
+
+ default:
+ break;
+ }
+ } else if (event_base == IP_EVENT) {
+ switch (event_id) {
+ case IP_EVENT_STA_GOT_IP: {
+ ip_event_got_ip_t* event = (ip_event_got_ip_t*)event_data;
+ DEBUG_INFO("WIFI", "STA 获取 IP: " IPSTR, IP2STR(&event->ip_info.ip));
+ if (s_sta_got_ip_sem) {
+ xSemaphoreGive(s_sta_got_ip_sem);
+ }
+ break;
+ }
+
+ default:
+ break;
+ }
+ }
+}
+
+// ----------------------------------------
+// WiFi AP 模式初始化
+/**
+ * @brief 初始化 SoftAP 模式,供局域网 OTA 使用。
+ * @return ESP_OK 成功,其他失败。
+ */
+static esp_err_t wifi_init_ap(void)
+{
+ DEBUG_INFO("WIFI", "=== 初始化 WiFi AP 模式 ===");
+
+ /** @brief 当前 WiFi 工作模式(AP 或 STA)。 */
+ s_wifi_mode = OTA_WIFI_MODE_AP;
+
+ esp_netif_t *ap_netif = esp_netif_get_handle_from_ifkey("WIFI_AP_DEF");
+ if (ap_netif == NULL) {
+ ap_netif = esp_netif_create_default_wifi_ap();
+ }
+ if (ap_netif != NULL) {
+ esp_netif_dhcps_stop(ap_netif);
+
+ esp_netif_ip_info_t ip_info;
+ memset(&ip_info, 0, sizeof(ip_info));
+ // 方案 A:SoftAP 使用 192.168.31.0/24 网段
+ inet_pton(AF_INET, "192.168.31.1", &ip_info.ip);
+ inet_pton(AF_INET, "192.168.31.1", &ip_info.gw);
+ inet_pton(AF_INET, "255.255.255.0", &ip_info.netmask);
+ ESP_ERROR_CHECK(esp_netif_set_ip_info(ap_netif, &ip_info));
+
+ esp_netif_dhcps_start(ap_netif);
+ DEBUG_INFO("WIFI", "AP IP 已设置: 192.168.31.1");
+ }
+
+ wifi_config_t wifi_config = {};
+ memcpy(wifi_config.ap.ssid, WIFI_AP_SSID, strlen(WIFI_AP_SSID) + 1);
+ wifi_config.ap.ssid_len = strlen(WIFI_AP_SSID);
+ memcpy(wifi_config.ap.password, WIFI_AP_PASSWORD, strlen(WIFI_AP_PASSWORD) + 1);
+ wifi_config.ap.max_connection = WIFI_AP_MAX_CONNECT;
+ wifi_config.ap.authmode = WIFI_AUTH_WPA_WPA2_PSK;
+ wifi_config.ap.channel = WIFI_AP_CHANNEL;
+
+ DEBUG_INFO("WIFI", "AP SSID: %s, Channel: %d", WIFI_AP_SSID, WIFI_AP_CHANNEL);
+
+ ESP_ERROR_CHECK(esp_wifi_set_mode(WIFI_MODE_AP));
+ ESP_ERROR_CHECK(esp_wifi_set_config(WIFI_IF_AP, &wifi_config));
+ ESP_ERROR_CHECK(esp_wifi_start());
+
+ DEBUG_INFO("WIFI", "WiFi AP 已启动: %s", WIFI_AP_SSID);
+ DEBUG_INFO("WIFI", "AP IP 地址: 192.168.31.1");
+ DEBUG_INFO("WIFI", "AP 网关地址: 192.168.31.1");
+ DEBUG_INFO("WIFI", "最大连接数: %d", WIFI_AP_MAX_CONNECT);
+
+ return ESP_OK;
+}
+
+// ----------------------------------------
+// WiFi STA 模式初始化
+/**
+ * @brief 初始化 STA 模式并连接预配置的路由器。
+ * @return ESP_OK 成功,其他失败。
+ */
+static esp_err_t wifi_init_sta(void)
+{
+ DEBUG_INFO("WIFI", "=== 初始化 WiFi STA 模式 ===");
+
+ /** @brief 当前 WiFi 工作模式(AP 或 STA)。 */
+ s_wifi_mode = OTA_WIFI_MODE_STA;
+ /** @brief STA 连接重试次数。 */
+ s_sta_retry_count = 0;
+
+ // ★ 设置国家代码:中国,允许信道 1-13
+ // ESP32 默认国家是 US(只允许 1-11),中国路由可能用 12/13
+ wifi_country_t country = {};
+ memcpy(country.cc, "CN", 3);
+ country.schan = 1;
+ country.nchan = 13;
+ country.policy = WIFI_COUNTRY_POLICY_MANUAL;
+
+ wifi_config_t wifi_config = {};
+ wifi_config.sta.scan_method = WIFI_ALL_CHANNEL_SCAN;
+ wifi_config.sta.sort_method = WIFI_CONNECT_AP_BY_SIGNAL;
+ wifi_config.sta.threshold.authmode = WIFI_AUTH_WPA2_PSK;
+ wifi_config.sta.pmf_cfg.capable = true;
+ wifi_config.sta.pmf_cfg.required = false;
+ wifi_config.sta.listen_interval = 3;
+ memcpy(wifi_config.sta.ssid, WIFI_STA_SSID, strlen(WIFI_STA_SSID) + 1);
+ memcpy(wifi_config.sta.password, WIFI_STA_PASSWORD, strlen(WIFI_STA_PASSWORD) + 1);
+
+ DEBUG_INFO("WIFI", "STA SSID: %s (全信道扫描, CN 国家代码)", WIFI_STA_SSID);
+
+ ESP_ERROR_CHECK(esp_wifi_set_country(&country));
+ ESP_ERROR_CHECK(esp_wifi_set_mode(WIFI_MODE_STA));
+ ESP_ERROR_CHECK(esp_wifi_set_config(WIFI_IF_STA, &wifi_config));
+
+ wifi_set_static_ip();
+
+ ESP_ERROR_CHECK(esp_wifi_start());
+ ESP_ERROR_CHECK(esp_wifi_set_ps(WIFI_PS_NONE));
+
+ DEBUG_INFO("WIFI", "WiFi STA 启动, SSID: %s, 事件驱动连接...", WIFI_STA_SSID);
+
+ return ESP_OK;
+}
+
+/**
+ * @brief 等待 STA 连接成功并获取 IP(静态 IP 等 CONNECTED,DHCP 等 GOT_IP)
+ * @param ip_str 输出缓冲区,存放获取到的 IP 字符串。
+ * @param len 缓冲区长度。
+ * @return ESP_OK 连接成功,其他失败。
+ */
+static esp_err_t wifi_wait_sta_connected(char *ip_str, size_t len)
+{
+ SemaphoreHandle_t wait_sem = WIFI_STA_USE_STATIC_IP ? s_sta_connected_sem : s_sta_got_ip_sem;
+ const char *wait_name = WIFI_STA_USE_STATIC_IP ? "STA_CONNECTED" : "STA_GOT_IP";
+
+ if (wait_sem == NULL) {
+ DEBUG_ERR("WIFI", "%s 信号量未创建", wait_name);
+ return ESP_ERR_INVALID_STATE;
+ }
+
+ TickType_t timeout_ticks = pdMS_TO_TICKS(WIFI_STA_CONNECT_TIMEOUT_MS);
+ if (xSemaphoreTake(wait_sem, timeout_ticks) == pdTRUE) {
+ esp_netif_ip_info_t ip_info;
+ esp_netif_t *netif = esp_netif_get_handle_from_ifkey("WIFI_STA_DEF");
+ if (netif != NULL && esp_netif_get_ip_info(netif, &ip_info) == ESP_OK) {
+ if (ip_str) {
+ snprintf(ip_str, len, IPSTR, IP2STR(&ip_info.ip));
+ }
+ DEBUG_INFO("WIFI", "%s 等待成功, 当前 IP: " IPSTR, wait_name, IP2STR(&ip_info.ip));
+ }
+ /** @brief STA 模式连接状态。 */
+ s_sta_connected = true;
+ return ESP_OK;
+ }
+
+ DEBUG_ERR("WIFI", "等待 %s 超时 (%d ms), 重连次数: %d", wait_name, WIFI_STA_CONNECT_TIMEOUT_MS, s_sta_retry_count);
+ return ESP_ERR_TIMEOUT;
+}
+
+/**
+ * @brief 设置 STA 静态 IP
+ */
+static void wifi_set_static_ip(void)
+{
+ if (WIFI_STA_USE_STATIC_IP) {
+ esp_netif_t *sta_netif = esp_netif_get_handle_from_ifkey("WIFI_STA_DEF");
+ if (sta_netif) {
+ DEBUG_INFO("WIFI", "STA 使用静态 IP 模式,停止 DHCP Client");
+ ESP_ERROR_CHECK(esp_netif_dhcpc_stop(sta_netif));
+
+ esp_netif_ip_info_t info;
+ memset(&info, 0, sizeof(info));
+ inet_pton(AF_INET, WIFI_STA_STATIC_IP, &info.ip);
+ inet_pton(AF_INET, WIFI_STA_STATIC_GW, &info.gw);
+ inet_pton(AF_INET, WIFI_STA_STATIC_NETMASK, &info.netmask);
+ ESP_ERROR_CHECK(esp_netif_set_ip_info(sta_netif, &info));
+ DEBUG_INFO("WIFI", "静态 IP 已设置: %s", WIFI_STA_STATIC_IP);
+
+ // 设置 DNS
+ esp_netif_dns_info_t dns_info;
+ memset(&dns_info, 0, sizeof(dns_info));
+ inet_pton(AF_INET, WIFI_STA_STATIC_DNS, &dns_info.ip.u_addr.ip4.addr);
+ ESP_ERROR_CHECK(esp_netif_set_dns_info(sta_netif, ESP_NETIF_DNS_MAIN, &dns_info));
+ DEBUG_INFO("WIFI", "DNS 已设置: %s", WIFI_STA_STATIC_DNS);
+ }
+ } else {
+ DEBUG_INFO("WIFI", "使用 DHCP 获取 IP");
+ }
+}
+
+// ----------------------------------------
+// ESP32 OTA 内部函数
+/**
+ * @brief 初始化 ESP32 自身的 OTA 写入流程。
+ * @param total_size
+ * @return ESP_OK 成功,其他失败。
+ */
+static esp_err_t esp32_ota_begin(size_t total_size)
+{
+ const esp_partition_t *boot_partition = esp_ota_get_running_partition();
+ DEBUG_INFO("OTA", "当前启动分区: %s, size=%u, address=0x%lx",
+ boot_partition->label, boot_partition->size, boot_partition->address);
+
+ s_esp32_update_partition = esp_partition_find_first(
+ ESP_PARTITION_TYPE_APP, ESP_PARTITION_SUBTYPE_APP_OTA_0, NULL);
+ if (s_esp32_update_partition == NULL ||
+ s_esp32_update_partition->address == boot_partition->address) {
+ s_esp32_update_partition = esp_partition_find_first(
+ ESP_PARTITION_TYPE_APP, ESP_PARTITION_SUBTYPE_APP_OTA_1, NULL);
+ }
+
+ if (s_esp32_update_partition == NULL) {
+ DEBUG_ERR("OTA", "找不到可用的 OTA 分区");
+ return ESP_FAIL;
+ }
+
+ DEBUG_INFO("OTA", "OTA 目标分区: %s, size=%u, address=0x%lx",
+ s_esp32_update_partition->label,
+ s_esp32_update_partition->size,
+ s_esp32_update_partition->address);
+
+ if (total_size > s_esp32_update_partition->size) {
+ DEBUG_ERR("OTA", "固件太大: %u > 分区大小: %u",
+ total_size, s_esp32_update_partition->size);
+ return ESP_FAIL;
+ }
+
+ esp_err_t err = esp_ota_begin(s_esp32_update_partition, total_size, &s_esp32_ota_handle);
+ if (err != ESP_OK) {
+ DEBUG_ERR("OTA", "esp_ota_begin 失败: %s", esp_err_to_name(err));
+ /** @brief ESP32 OTA 目标分区。 */
+ s_esp32_update_partition = NULL;
+ return err;
+ }
+
+ /** @brief ESP32 OTA 已接收字节数。 */
+ s_esp32_received = 0;
+ /** @brief ESP32 自身 OTA 写入进行中标志。 */
+ s_esp32_ota_active = true;
+ DEBUG_INFO("OTA", "ESP32 OTA 开始,目标分区: %s", s_esp32_update_partition->label);
+ return ESP_OK;
+}
+
+/**
+ * @brief 将接收到的数据块写入 ESP32 OTA 分区。
+ * @param data
+ * @param len
+ * @return ESP_OK 成功,其他失败。
+ */
+static esp_err_t esp32_ota_write(const uint8_t *data, size_t len)
+{
+ if (!s_esp32_ota_active || s_esp32_update_partition == NULL) {
+ return ESP_ERR_INVALID_STATE;
+ }
+ esp_err_t err = esp_ota_write(s_esp32_ota_handle, data, len);
+ if (err == ESP_OK) {
+ s_esp32_received += len;
+ }
+ return err;
+}
+
+/**
+ * @brief HTTP POST /ota/update - 完成 ESP32 OTA 并重启
+ * @param req
+ * @return
+ */
+static esp_err_t esp32_ota_update_handler(httpd_req_t *req)
+{
+ (void)req;
+ http_set_cors_headers(req);
+ if (!s_esp32_ota_active) {
+ httpd_resp_send_err(req, HTTPD_400_BAD_REQUEST, "OTA not started");
+ return ESP_FAIL;
+ }
+
+ DEBUG_INFO("OTA", "ESP32 OTA 完成, 共 %u 字节, 设置启动分区...", s_esp32_received);
+
+ esp_err_t err = esp_ota_end(s_esp32_ota_handle);
+ if (err != ESP_OK) {
+ DEBUG_ERR("OTA", "esp_ota_end 失败: %s", esp_err_to_name(err));
+ /** @brief ESP32 自身 OTA 写入进行中标志。 */
+ s_esp32_ota_active = false;
+ httpd_resp_send_err(req, HTTPD_500_INTERNAL_SERVER_ERROR, esp_err_to_name(err));
+ return ESP_FAIL;
+ }
+
+ err = esp_ota_set_boot_partition(s_esp32_update_partition);
+ if (err != ESP_OK) {
+ DEBUG_ERR("OTA", "esp_ota_set_boot_partition 失败: %s", esp_err_to_name(err));
+ /** @brief ESP32 自身 OTA 写入进行中标志。 */
+ s_esp32_ota_active = false;
+ httpd_resp_send_err(req, HTTPD_500_INTERNAL_SERVER_ERROR, esp_err_to_name(err));
+ return ESP_FAIL;
+ }
+
+ /** @brief ESP32 自身 OTA 写入进行中标志。 */
+ s_esp32_ota_active = false;
+ DEBUG_INFO("OTA", "ESP32 OTA 完成,重启中...");
+
+ const char resp[] = "{\"status\": \"ok\", \"message\": \"OTA finished, rebooting\"}";
+ httpd_resp_set_type(req, "application/json");
+ httpd_resp_send(req, resp, strlen(resp));
+
+ vTaskDelay(pdMS_TO_TICKS(500));
+ esp_restart();
+ return ESP_OK;
+}
+
+/**
+ * @brief HTTP GET /ota/update - 查询 ESP32 OTA 状态
+ * @param req
+ * @return
+ */
+static esp_err_t esp32_ota_status_handler(httpd_req_t *req)
+{
+ http_set_cors_headers(req);
+ char resp[128];
+ if (s_esp32_ota_active) {
+ snprintf(resp, sizeof(resp),
+ "{\"active\": true, \"received\": %u, \"partition\": \"%s\"}",
+ (unsigned int)s_esp32_received,
+ s_esp32_update_partition ? s_esp32_update_partition->label : "null");
+ } else {
+ snprintf(resp, sizeof(resp), "{\"active\": false}");
+ }
+ httpd_resp_set_type(req, "application/json");
+ httpd_resp_send(req, resp, strlen(resp));
+ return ESP_OK;
+}
+
+// 从 HTTP 流中读取完整的 JSON 头(以 } 结尾)
+/**
+ * @brief 返回 JSON 字符串长度,失败返回 0
+ * @param req
+ * @param buf
+ * @param buf_size
+ * @return
+ */
+static int recv_json_header(httpd_req_t *req, char *buf, int buf_size)
+{
+ int total = 0;
+ int depth = 1; // 开头的 '{' 已被 peek 消费,从 1 开始
+ while (total < buf_size - 1) {
+ int ret = httpd_req_recv(req, buf + total, 1); // 逐字节读,精确找到 JSON 结尾
+ if (ret <= 0) return 0;
+ total += ret;
+ buf[total] = '\0';
+ if (buf[total - 1] == '{') depth++;
+ else if (buf[total - 1] == '}') {
+ depth--;
+ if (depth == 0) return total; // JSON 头读完
+ }
+ }
+ return 0; // JSON 头太长,超出缓冲区
+}
+
+// ----------------------------------------
+// 统一 /ota/upload 处理器(自动识别目标)
+/**
+ * @brief HTTP 文件上传处理器,接收固件并写入 Flash。
+ * @param req
+ * @return ESP_OK 成功,其他失败。
+ */
+static esp_err_t ota_upload_handler(httpd_req_t *req)
+{
+ http_set_cors_headers(req);
+ DEBUG_INFO("WIFI", "=== HTTP: POST /ota/upload ===");
+ DEBUG_INFO("WIFI", "收到固件上传请求, 内容长度: %d 字节", req->content_len);
+
+ // 预读 1 字节判断是 JSON 还是 binary
+ char peek;
+ int peek_ret = httpd_req_recv(req, &peek, 1);
+ if (peek_ret <= 0) {
+ httpd_resp_send_err(req, HTTPD_500_INTERNAL_SERVER_ERROR, "接收失败");
+ return ESP_FAIL;
+ }
+
+ bool is_esp32_ota = false;
+ uint32_t total_size = 0;
+ int json_header_len = 0;
+
+ if (peek == '{') {
+ // JSON 模式:逐字节读完 JSON 头
+ char json_buf[256];
+ json_buf[0] = peek;
+ json_header_len = recv_json_header(req, json_buf + 1, sizeof(json_buf) - 1);
+ if (json_header_len > 0) {
+ json_header_len++; // 加上已读的 peek 字节
+ json_buf[json_header_len] = '\0';
+
+ cJSON *root = cJSON_Parse(json_buf);
+ if (root) {
+ cJSON *type_item = cJSON_GetObjectItem(root, "type");
+ if (type_item && cJSON_IsString(type_item) &&
+ strcmp(cJSON_GetStringValue(type_item), "esp32") == 0) {
+ is_esp32_ota = true;
+ DEBUG_INFO("WIFI", "检测到 ESP32 OTA, JSON 头长度: %d 字节", json_header_len);
+ }
+ cJSON *size_item = cJSON_GetObjectItem(root, "total_size");
+ if (size_item) {
+ total_size = (uint32_t)cJSON_GetNumberValue(size_item);
+ }
+ cJSON_Delete(root);
+ }
+ }
+ }
+
+ if (is_esp32_ota) {
+ // --- ESP32 OTA ---
+ if (total_size == 0) {
+ httpd_resp_send_err(req, HTTPD_400_BAD_REQUEST, "missing total_size");
+ return ESP_FAIL;
+ }
+
+ esp_err_t err = esp32_ota_begin(total_size);
+ if (err != ESP_OK) {
+ httpd_resp_send_err(req, HTTPD_500_INTERNAL_SERVER_ERROR, "OTA begin failed");
+ return ESP_FAIL;
+ }
+
+ // 堆分配缓冲区,避免栈溢出(HTTP server 栈仅 8KB)
+ uint8_t *fw_buf = (uint8_t *)malloc(ESP32_OTA_RECV_BLOCK_SIZE);
+ if (!fw_buf) {
+ httpd_resp_send_err(req, HTTPD_500_INTERNAL_SERVER_ERROR, "内存分配失败");
+ return ESP_FAIL;
+ }
+
+ size_t total_firmware_written = 0;
+ size_t total_received = json_header_len; // 已消费的字节数
+ int last_percent = 0;
+
+ while (total_received < req->content_len) {
+ size_t to_read = req->content_len - total_received;
+ if (to_read > ESP32_OTA_RECV_BLOCK_SIZE) to_read = ESP32_OTA_RECV_BLOCK_SIZE;
+
+ int ret = httpd_req_recv(req, (char *)fw_buf, to_read);
+ if (ret <= 0) break;
+
+ err = esp32_ota_write(fw_buf, ret);
+ if (err != ESP_OK) {
+ DEBUG_ERR("WIFI", "OTA write 失败");
+ break;
+ }
+ total_received += ret;
+ total_firmware_written += ret;
+
+ if (total_size > 0) {
+ int percent = (int)((total_firmware_written * 100) / total_size);
+ if (percent / 10 > last_percent / 10) {
+ last_percent = percent;
+ DEBUG_INFO("WIFI", "ESP32 OTA 进度: %u / %u (%d%%)",
+ (unsigned int)total_firmware_written, total_size, percent);
+ }
+ }
+ }
+
+ free(fw_buf);
+ DEBUG_INFO("WIFI", "ESP32 OTA 存储完成: %u 字节 (跳过 JSON 头 %d 字节)", (unsigned int)s_esp32_received, json_header_len);
+ const char resp[] = "{\"status\": \"ok\", \"message\": \"stored, call POST /ota/update to finish\", \"target\": \"esp32\"}";
+ httpd_resp_set_type(req, "application/json");
+ httpd_resp_send(req, resp, strlen(resp));
+ return ESP_OK;
+ }
+
+ // --- GD32 OTA(原逻辑)---
+ if (req->content_len > OTA_MAX_FIRMWARE_SIZE) {
+ DEBUG_ERR("WIFI", "固件太大: %d > %d", req->content_len, OTA_MAX_FIRMWARE_SIZE);
+ httpd_resp_send_err(req, HTTPD_500_INTERNAL_SERVER_ERROR, "固件太大");
+ return ESP_FAIL;
+ }
+
+ uint8_t *buf = (uint8_t *)malloc(OTA_BLOCK_SIZE);
+ int received = 0;
+
+ // peek 字节是 GD32 数据的第一字节,先放回缓冲区
+ uint8_t gd32_header[6];
+ gd32_header[0] = (uint8_t)peek;
+ int header_read = 1;
+ // 读剩余 5 字节凑齐 6 字节 GD32 头
+ if (req->content_len >= 6) {
+ int need = 6 - header_read;
+ int got = httpd_req_recv(req, (char *)gd32_header + header_read, need);
+ if (got > 0) header_read += got;
+ }
+
+ // 先处理已读的 header
+ if (header_read >= 6) {
+ uint16_t seq = gd32_header[0] | (gd32_header[1] << 8);
+ uint32_t fw_size = gd32_header[2] | (gd32_header[3] << 8) | (gd32_header[4] << 16) | (gd32_header[5] << 24);
+ if (seq == 0) {
+ ota_meta_t meta = { .total_size = fw_size, .stored_size = 0, .last_seq = 0,
+ .chunk_count = (fw_size + OTA_BLOCK_SIZE - 1) / OTA_BLOCK_SIZE, .checksum = {0} };
+ meta_save(&meta);
+ DEBUG_INFO("WIFI", "开始 GD32 固件存储: %u 字节", fw_size);
+ }
+ }
+
+ // 继续读剩余数据
+ while (received + header_read < req->content_len) {
+ size_t to_read = req->content_len - received - header_read;
+ if (to_read > OTA_BLOCK_SIZE) to_read = OTA_BLOCK_SIZE;
+
+ int ret = httpd_req_recv(req, (char *)buf, to_read);
+ if (ret <= 0) break;
+
+ uint32_t flash_offset = ((received + header_read) / OTA_BLOCK_SIZE) * OTA_BLOCK_SIZE;
+ esp_err_t err = flash_write_chunk(flash_offset, buf, ret);
+ if (err == ESP_OK) {
+ meta_update(flash_offset + ret, (received + header_read) / OTA_BLOCK_SIZE);
+ }
+ received += ret;
+
+ uint32_t progress = ((received + header_read) * 100) / req->content_len;
+ if (progress % 10 == 0) {
+ DEBUG_INFO("WIFI", "GD32 上传进度: %d / %d (%u%%)",
+ received + header_read, req->content_len, progress);
+ }
+ }
+
+ free(buf);
+ DEBUG_INFO("WIFI", "GD32 固件存储完成: %d 字节", received + header_read);
+
+ ota_meta_t meta;
+ meta_load(&meta);
+ if (meta.stored_size >= meta.total_size) {
+ /** @brief GD32 固件存储完成标志。 */
+ s_gd32_storage_done = true;
+ DEBUG_INFO("WIFI", "GD32 固件已存储,触发转发...");
+ wifi_ota_signal_relay();
+ }
+
+ const char resp[] = "{\"status\": \"ok\", \"progress\": 100, \"target\": \"gd32\"}";
+ httpd_resp_set_type(req, "application/json");
+ httpd_resp_send(req, resp, strlen(resp));
+ return ESP_OK;
+}
+
+/**
+ * @brief HTTP POST /ota/relay 处理器
+ * @param req
+ * @return
+ */
+static esp_err_t ota_relay_trigger_handler(httpd_req_t *req)
+{
+ (void)req;
+ http_set_cors_headers(req);
+ DEBUG_INFO("WIFI", "=== HTTP: POST /ota/relay ===");
+ DEBUG_INFO("WIFI", "收到触发 OTA 中继请求");
+ wifi_ota_signal_relay();
+
+ const char resp[] = "{\"status\": \"ok\", \"message\": \"OTA relay triggered\"}";
+ httpd_resp_set_type(req, "application/json");
+ httpd_resp_send(req, resp, strlen(resp));
+
+ return ESP_OK;
+}
+
+/**
+ * @brief HTTP GET /ota/status 处理器
+ * @param req
+ * @return
+ */
+static esp_err_t ota_status_handler(httpd_req_t *req)
+{
+ http_set_cors_headers(req);
+ DEBUG_INFO("WIFI", "=== HTTP: GET /ota/status ===");
+
+ ota_meta_t meta;
+ meta_load(&meta);
+
+ char resp[256];
+ int len = snprintf(resp, sizeof(resp),
+ "{\"total_size\": %lu, \"stored_size\": %lu, \"last_seq\": %lu, "
+ "\"storage_done\": %s, \"relay_state\": %d}",
+ (unsigned long)meta.total_size, (unsigned long)meta.stored_size,
+ (unsigned long)meta.last_seq,
+ s_gd32_storage_done ? "true" : "false",
+ g_relay_state);
+
+ DEBUG_INFO("WIFI", "OTA 状态: total=%lu, stored=%lu, done=%s",
+ (unsigned long)meta.total_size, (unsigned long)meta.stored_size,
+ s_gd32_storage_done ? "true" : "false");
+
+ httpd_resp_set_type(req, "application/json");
+ httpd_resp_send(req, resp, len);
+
+ return ESP_OK;
+}
+
+/**
+ * @brief HTTP GET /health 处理器
+ * @param req
+ * @return
+ */
+static esp_err_t health_handler(httpd_req_t *req)
+{
+ http_set_cors_headers(req);
+ DEBUG_INFO("WIFI", "=== HTTP: GET /health ===");
+
+ health_state_t state = health_get_current();
+ uint32_t free_heap = esp_get_free_heap_size();
+
+ char resp[128];
+ int len = snprintf(resp, sizeof(resp),
+ "{\"health\": %d, \"free_heap\": %lu}",
+ state, (unsigned long)free_heap);
+
+ DEBUG_INFO("WIFI", "健康状态: %d, 空闲堆: %lu", state, (unsigned long)free_heap);
+
+ httpd_resp_set_type(req, "application/json");
+ httpd_resp_send(req, resp, len);
+
+ return ESP_OK;
+}
+
+/**
+ * @brief HTTP GET /logs - 拉取并清空日志缓冲区
+ * @param req
+ * @return
+ */
+static esp_err_t logs_handler(httpd_req_t *req)
+{
+ http_set_cors_headers(req);
+
+ int json_len = 0;
+ char *json = debug_log_ring_drain_json(&json_len);
+
+ if (json && json_len > 0) {
+ httpd_resp_set_type(req, "application/json");
+ httpd_resp_send(req, json, json_len);
+ free(json);
+ } else {
+ httpd_resp_set_type(req, "application/json");
+ httpd_resp_send(req, "[]", 2);
+ }
+ return ESP_OK;
+}
+
+// HTTP POST /can/send 处理器
+// 上位机发送 CAN 帧,经 ESP32 转发到 GD32
+// 请求体: {"ch": 0, "id": 123456, "data": "0102030405060708", "dlc": 8}
+/**
+ * @brief data 为 hex 字符串(可不填,自动用 dlc 决定实际发送长度)
+ * @param req
+ * @return
+ */
+static esp_err_t can_send_handler(httpd_req_t *req)
+{
+ http_set_cors_headers(req);
+ char buf[256] = {0};
+ int ret = httpd_req_recv(req, buf, sizeof(buf) - 1);
+ if (ret <= 0) {
+ httpd_resp_send_err(req, HTTPD_400_BAD_REQUEST, "接收数据失败");
+ return ESP_FAIL;
+ }
+ buf[ret] = '\0';
+
+ cJSON *root = cJSON_Parse(buf);
+ if (!root) {
+ httpd_resp_send_err(req, HTTPD_400_BAD_REQUEST, "JSON 解析失败");
+ return ESP_FAIL;
+ }
+
+ cJSON *ch_item = cJSON_GetObjectItem(root, "ch");
+ cJSON *id_item = cJSON_GetObjectItem(root, "id");
+ cJSON *data_item = cJSON_GetObjectItem(root, "data");
+ cJSON *dlc_item = cJSON_GetObjectItem(root, "dlc");
+
+ if (!ch_item || !id_item) {
+ cJSON_Delete(root);
+ httpd_resp_send_err(req, HTTPD_400_BAD_REQUEST, "缺少 ch 或 id 字段");
+ return ESP_FAIL;
+ }
+
+ can_channel_t ch = (can_channel_t)cJSON_GetNumberValue(ch_item);
+ uint32_t ext_id = (uint32_t)cJSON_GetNumberValue(id_item);
+ uint8_t dlc = dlc_item ? (uint8_t)cJSON_GetNumberValue(dlc_item) : 0;
+
+ uint8_t tx_data[CAN_MAX_DLC] = {0};
+ uint8_t tx_dlc = 0;
+
+ if (data_item && cJSON_IsString(data_item)) {
+ const char *hex_str = cJSON_GetStringValue(data_item);
+ size_t hex_len = strlen(hex_str);
+ for (size_t i = 0; i < hex_len / 2 && i < CAN_MAX_DLC; i++) {
+ char byte_str[3] = { hex_str[i * 2], hex_str[i * 2 + 1], '\0' };
+ tx_data[i] = (uint8_t)strtol(byte_str, NULL, 16);
+ tx_dlc++;
+ }
+ }
+
+ if (tx_dlc == 0 && dlc > 0) {
+ tx_dlc = (dlc > CAN_MAX_DLC) ? CAN_MAX_DLC : dlc;
+ }
+
+ cJSON_Delete(root);
+
+ // 发送 CAN 帧到 GD32
+ uint8_t resp_data[CAN_MAX_DLC] = {0};
+ int resp_len = gd32_can_send(ch, ext_id, tx_data, tx_dlc,
+ resp_data, sizeof(resp_data), 1000);
+
+ char resp[512];
+ if (resp_len >= 0) {
+ // 将响应 data 转为 hex 字符串
+ char resp_hex[CAN_MAX_DLC * 2 + 1] = {0};
+ for (int i = 0; i < resp_len; i++) {
+ sprintf(&resp_hex[i * 2], "%02X", resp_data[i]);
+ }
+ snprintf(resp, sizeof(resp),
+ "{\"status\": \"ok\", \"ch\": %d, \"id\": %lu, \"sent\": %d, \"recv_len\": %d, \"recv\": \"%s\"}",
+ ch, (unsigned long)ext_id, tx_dlc, resp_len, resp_hex);
+ } else {
+ snprintf(resp, sizeof(resp),
+ "{\"status\": \"error\", \"code\": %d}", resp_len);
+ }
+
+ httpd_resp_set_type(req, "application/json");
+ httpd_resp_send(req, resp, strlen(resp));
+ return ESP_OK;
+}
+
+/**
+ * @brief HTTP GET /can/status 处理器
+ * @param req
+ * @return
+ */
+static esp_err_t can_status_handler(httpd_req_t *req)
+{
+ http_set_cors_headers(req);
+ char resp[128];
+ snprintf(resp, sizeof(resp),
+ "{\"channels\": 3, \"max_dlc\": %d, \"spi_init\": %s}",
+ CAN_MAX_DLC,
+ g_spi_device != NULL ? "true" : "false");
+ httpd_resp_set_type(req, "application/json");
+ httpd_resp_send(req, resp, strlen(resp));
+ return ESP_OK;
+}
+
+/**
+ * @brief HTTP GET /wifi/status 处理器
+ * @param req
+ * @return
+ */
+static esp_err_t wifi_status_handler(httpd_req_t *req)
+{
+ http_set_cors_headers(req);
+ char resp[256];
+ char ip_str[16] = "0.0.0.0";
+ int8_t rssi = -127;
+
+ if (s_wifi_mode == OTA_WIFI_MODE_STA && s_sta_connected) {
+ esp_netif_ip_info_t ip_info;
+ esp_netif_t *sta_netif = esp_netif_get_handle_from_ifkey("WIFI_STA_DEF");
+ if (sta_netif && esp_netif_get_ip_info(sta_netif, &ip_info) == ESP_OK) {
+ snprintf(ip_str, sizeof(ip_str), IPSTR, IP2STR(&ip_info.ip));
+ }
+ wifi_ap_record_t ap_info;
+ if (esp_wifi_sta_get_ap_info(&ap_info) == ESP_OK) {
+ rssi = ap_info.rssi;
+ }
+ }
+
+ int len = snprintf(resp, sizeof(resp),
+ "{\"mode\": \"%s\", \"ip\": \"%s\", \"rssi\": %d, \"connected\": %s}",
+ s_wifi_mode == OTA_WIFI_MODE_AP ? "AP" : "STA",
+ ip_str, rssi,
+ (s_wifi_mode == OTA_WIFI_MODE_STA) ? (s_sta_connected ? "true" : "false") : "N/A");
+
+ httpd_resp_set_type(req, "application/json");
+ httpd_resp_send(req, resp, len);
+
+ return ESP_OK;
+}
+
+/**
+ * @brief HTTP GET /system/status 处理器
+ * @param req
+ * @return
+ */
+static esp_err_t system_status_handler(httpd_req_t *req)
+{
+ http_set_cors_headers(req);
+
+ char ip_str[16] = "0.0.0.0";
+ int8_t rssi = -127;
+ bool connected = false;
+
+ if (s_wifi_mode == OTA_WIFI_MODE_STA && s_sta_connected) {
+ esp_netif_ip_info_t ip_info;
+ esp_netif_t *sta_netif = esp_netif_get_handle_from_ifkey("WIFI_STA_DEF");
+ if (sta_netif && esp_netif_get_ip_info(sta_netif, &ip_info) == ESP_OK) {
+ snprintf(ip_str, sizeof(ip_str), IPSTR, IP2STR(&ip_info.ip));
+ }
+ wifi_ap_record_t ap_info;
+ if (esp_wifi_sta_get_ap_info(&ap_info) == ESP_OK) {
+ rssi = ap_info.rssi;
+ }
+ connected = true;
+ }
+
+ ota_meta_t meta;
+ memset(&meta, 0, sizeof(meta));
+ meta_load(&meta);
+
+ char resp[640];
+ int len = snprintf(resp, sizeof(resp),
+ "{\"version\": \"%s\", \"health\": %d, \"free_heap\": %lu, \"min_free_heap\": %lu, \"temperature\": %.1f, \"wifi\": {\"mode\": \"%s\", \"ip\": \"%s\", \"rssi\": %d, \"connected\": %s}, \"ota\": {\"total_size\": %lu, \"stored_size\": %lu, \"storage_done\": %s, \"relay_state\": %d}}",
+ FW_VERSION,health_get_current(),
+ (unsigned long)esp_get_free_heap_size(),
+ (unsigned long)esp_get_minimum_free_heap_size(),
+ (double)health_get_last_temperature(),
+ s_wifi_mode == OTA_WIFI_MODE_AP ? "AP" : "STA",
+ ip_str,
+ rssi,
+ connected ? "true" : "false",
+ (unsigned long)meta.total_size,
+ (unsigned long)meta.stored_size,
+ s_gd32_storage_done ? "true" : "false",
+ g_relay_state);
+
+ httpd_resp_set_type(req, "application/json");
+ httpd_resp_send(req, resp, len);
+ return ESP_OK;
+}
+
+// ----------------------------------------
+// 启动 HTTP OTA 服务器
+/**
+ * @brief 启动 HTTP OTA 服务器,注册各接口路由。
+ * @return ESP_OK 成功,其他失败。
+ */
+static esp_err_t http_ota_server_start(void)
+{
+ httpd_config_t config = HTTPD_DEFAULT_CONFIG();
+ config.server_port = HTTP_SERVER_PORT;
+ config.stack_size = HTTP_SERVER_STACK_SIZE;
+ config.max_uri_handlers = 32; // 现有业务端点 + 对应 OPTIONS 预检
+
+ DEBUG_INFO("WIFI", "=== 启动 HTTP OTA 服务器 ===");
+ DEBUG_INFO("WIFI", "HTTP 服务器端口: %d", config.server_port);
+
+ esp_err_t err = httpd_start(&s_http_server, &config);
+ if (err != ESP_OK) {
+ DEBUG_ERR("WIFI", "HTTP 服务器启动失败: %s", esp_err_to_name(err));
+ return err;
+ }
+
+ httpd_uri_t upload_uri = {
+ .uri = "/ota/upload",
+ .method = HTTP_POST,
+ .handler = ota_upload_handler,
+ .user_ctx = NULL
+ };
+ httpd_register_uri_handler(s_http_server, &upload_uri);
+ httpd_uri_t upload_options_uri = {
+ .uri = "/ota/upload",
+ .method = HTTP_OPTIONS,
+ .handler = http_options_handler,
+ .user_ctx = NULL
+ };
+ httpd_register_uri_handler(s_http_server, &upload_options_uri);
+
+ httpd_uri_t relay_uri = {
+ .uri = "/ota/relay",
+ .method = HTTP_POST,
+ .handler = ota_relay_trigger_handler,
+ .user_ctx = NULL
+ };
+ httpd_register_uri_handler(s_http_server, &relay_uri);
+ httpd_uri_t relay_options_uri = {
+ .uri = "/ota/relay",
+ .method = HTTP_OPTIONS,
+ .handler = http_options_handler,
+ .user_ctx = NULL
+ };
+ httpd_register_uri_handler(s_http_server, &relay_options_uri);
+
+ httpd_uri_t status_uri = {
+ .uri = "/ota/status",
+ .method = HTTP_GET,
+ .handler = ota_status_handler,
+ .user_ctx = NULL
+ };
+ httpd_register_uri_handler(s_http_server, &status_uri);
+ httpd_uri_t status_options_uri = {
+ .uri = "/ota/status",
+ .method = HTTP_OPTIONS,
+ .handler = http_options_handler,
+ .user_ctx = NULL
+ };
+ httpd_register_uri_handler(s_http_server, &status_options_uri);
+
+ httpd_uri_t esp32_update_uri = {
+ .uri = "/ota/update",
+ .method = HTTP_POST,
+ .handler = esp32_ota_update_handler,
+ .user_ctx = NULL
+ };
+ httpd_register_uri_handler(s_http_server, &esp32_update_uri);
+ httpd_uri_t esp32_update_options_uri = {
+ .uri = "/ota/update",
+ .method = HTTP_OPTIONS,
+ .handler = http_options_handler,
+ .user_ctx = NULL
+ };
+ httpd_register_uri_handler(s_http_server, &esp32_update_options_uri);
+
+ httpd_uri_t esp32_status_uri = {
+ .uri = "/ota/esp32_status",
+ .method = HTTP_GET,
+ .handler = esp32_ota_status_handler,
+ .user_ctx = NULL
+ };
+ httpd_register_uri_handler(s_http_server, &esp32_status_uri);
+ httpd_uri_t esp32_status_options_uri = {
+ .uri = "/ota/esp32_status",
+ .method = HTTP_OPTIONS,
+ .handler = http_options_handler,
+ .user_ctx = NULL
+ };
+ httpd_register_uri_handler(s_http_server, &esp32_status_options_uri);
+
+ httpd_uri_t health_uri = {
+ .uri = "/health",
+ .method = HTTP_GET,
+ .handler = health_handler,
+ .user_ctx = NULL
+ };
+ httpd_register_uri_handler(s_http_server, &health_uri);
+ httpd_uri_t health_options_uri = {
+ .uri = "/health",
+ .method = HTTP_OPTIONS,
+ .handler = http_options_handler,
+ .user_ctx = NULL
+ };
+ httpd_register_uri_handler(s_http_server, &health_options_uri);
+
+ httpd_uri_t logs_uri = {
+ .uri = "/logs",
+ .method = HTTP_GET,
+ .handler = logs_handler,
+ .user_ctx = NULL
+ };
+ httpd_register_uri_handler(s_http_server, &logs_uri);
+ httpd_uri_t logs_options_uri = {
+ .uri = "/logs",
+ .method = HTTP_OPTIONS,
+ .handler = http_options_handler,
+ .user_ctx = NULL
+ };
+ httpd_register_uri_handler(s_http_server, &logs_options_uri);
+
+ httpd_uri_t wifi_status_uri = {
+ .uri = "/wifi/status",
+ .method = HTTP_GET,
+ .handler = wifi_status_handler,
+ .user_ctx = NULL
+ };
+ httpd_register_uri_handler(s_http_server, &wifi_status_uri);
+ httpd_uri_t wifi_status_options_uri = {
+ .uri = "/wifi/status",
+ .method = HTTP_OPTIONS,
+ .handler = http_options_handler,
+ .user_ctx = NULL
+ };
+ httpd_register_uri_handler(s_http_server, &wifi_status_options_uri);
+
+ httpd_uri_t system_status_uri = {
+ .uri = "/system/status",
+ .method = HTTP_GET,
+ .handler = system_status_handler,
+ .user_ctx = NULL
+ };
+ httpd_register_uri_handler(s_http_server, &system_status_uri);
+ httpd_uri_t system_status_options_uri = {
+ .uri = "/system/status",
+ .method = HTTP_OPTIONS,
+ .handler = http_options_handler,
+ .user_ctx = NULL
+ };
+ httpd_register_uri_handler(s_http_server, &system_status_options_uri);
+
+ // CAN 帧转发
+ httpd_uri_t can_send_uri = {
+ .uri = "/can/send",
+ .method = HTTP_POST,
+ .handler = can_send_handler,
+ .user_ctx = NULL
+ };
+ httpd_register_uri_handler(s_http_server, &can_send_uri);
+ httpd_uri_t can_send_options_uri = {
+ .uri = "/can/send",
+ .method = HTTP_OPTIONS,
+ .handler = http_options_handler,
+ .user_ctx = NULL
+ };
+ httpd_register_uri_handler(s_http_server, &can_send_options_uri);
+
+ httpd_uri_t can_status_uri = {
+ .uri = "/can/status",
+ .method = HTTP_GET,
+ .handler = can_status_handler,
+ .user_ctx = NULL
+ };
+ httpd_register_uri_handler(s_http_server, &can_status_uri);
+ httpd_uri_t can_status_options_uri = {
+ .uri = "/can/status",
+ .method = HTTP_OPTIONS,
+ .handler = http_options_handler,
+ .user_ctx = NULL
+ };
+ httpd_register_uri_handler(s_http_server, &can_status_options_uri);
+
+ DEBUG_INFO("WIFI", "HTTP 服务器已启动");
+ DEBUG_INFO("WIFI", "注册端点: /ota/upload, /ota/relay, /ota/update, /ota/status, /ota/esp32_status, /health, /wifi/status, /system/status, /can/send, /can/status");
+
+ return ESP_OK;
+}
+
+// ----------------------------------------
+// 公开 API
+/**
+ * @brief 通知 OTA 中继开始向 GD32 转发固件。
+ */
+void wifi_ota_signal_relay(void)
+{
+ if (s_relay_triggered) {
+ DEBUG_WARN("WIFI", "转发已经触发过");
+ return;
+ }
+
+ /** @brief OTA 中继是否已触发标志。 */
+ s_relay_triggered = true;
+
+ if (g_ota_dispatch_queue) {
+ uint32_t msg = 1;
+ xQueueSend(g_ota_dispatch_queue, &msg, pdMS_TO_TICKS(100));
+ DEBUG_INFO("WIFI", "OTA 调度信号已发送到中继任务");
+ }
+}
+
+/**
+ * @brief 获取当前 WiFi 工作模式(AP 或 STA)。
+ * @return 当前 WiFi 模式枚举值。
+ */
+ota_wifi_mode_t wifi_get_mode(void)
+{
+ /** @brief 当前 WiFi 工作模式(AP 或 STA)。 */
+ return s_wifi_mode;
+}
+
+/**
+ * @brief 获取当前 WiFi 信号强度。
+ * @return 信号强度(dBm),未连接时返回无效值。
+ */
+int8_t wifi_get_rssi(void)
+{
+ if (s_wifi_mode != OTA_WIFI_MODE_STA || !s_sta_connected) {
+ return -127;
+ }
+ wifi_ap_record_t ap_info;
+ if (esp_wifi_sta_get_ap_info(&ap_info) == ESP_OK) {
+ return ap_info.rssi;
+ }
+ return -127;
+}
+
+/**
+ * @brief 判断 STA 是否已连接到上级 WiFi。
+ * @return true 已连接,false 未连接。
+ */
+bool wifi_is_connected(void)
+{
+ /** @brief STA 模式连接状态。 */
+ return s_sta_connected;
+}
+
+// ----------------------------------------
+// ★ WiFi 早期初始化(在 app_main 调度器启动前调用)
+// 包括:NVS → netif → event handler → WiFi driver → 配置 → connect
+// 返回:true = STA 模式已启动(正在连接),false = 需要切 AP
+/**
+ * @brief 在调度器启动前初始化 WiFi 并尝试 STA 连接。
+ * @return true STA 初始化中,false 需回退 AP。
+ */
+bool wifi_early_init(void)
+{
+ DEBUG_INFO("WIFI", "=== WiFi 早期初始化 (调度器启动前) ===");
+
+ esp_err_t err = nvs_flash_init();
+ if (err == ESP_ERR_NVS_NO_FREE_PAGES || err == ESP_ERR_NVS_NEW_VERSION_FOUND) {
+ // NVS 分区损坏或版本不匹配,擦除后重试
+ DEBUG_WARN("WIFI", "NVS 分区异常 (%s),擦除后重试", esp_err_to_name(err));
+ ESP_ERROR_CHECK(nvs_flash_erase());
+ err = nvs_flash_init();
+ }
+ if (err != ESP_OK) {
+ DEBUG_ERR("WIFI", "NVS 初始化失败: %s", esp_err_to_name(err));
+ } else {
+ DEBUG_INFO("WIFI", "NVS 初始化成功");
+ }
+
+ /** @brief STA 连接成功信号量。 */
+ s_sta_connected_sem = xSemaphoreCreateBinary();
+ /** @brief STA 获取 IP 信号量。 */
+ s_sta_got_ip_sem = xSemaphoreCreateBinary();
+ if (!s_sta_connected_sem || !s_sta_got_ip_sem) {
+ DEBUG_ERR("WIFI", "创建信号量失败");
+ }
+
+ // 注册 WiFi/IP 事件回调(必须在 esp_wifi_init 之前)
+ ESP_ERROR_CHECK(esp_event_handler_register(WIFI_EVENT, ESP_EVENT_ANY_ID, &wifi_event_handler, NULL));
+ ESP_ERROR_CHECK(esp_event_handler_register(IP_EVENT, ESP_EVENT_ANY_ID, &wifi_event_handler, NULL));
+ DEBUG_INFO("WIFI", "WiFi/IP 事件回调已注册");
+
+ esp_netif_t *sta_netif = esp_netif_get_handle_from_ifkey("WIFI_STA_DEF");
+ if (sta_netif == NULL) {
+ sta_netif = esp_netif_create_default_wifi_sta();
+ if (sta_netif == NULL) {
+ DEBUG_ERR("WIFI", "创建 STA netif 失败");
+ } else {
+ DEBUG_INFO("WIFI", "STA netif 已创建");
+ }
+ } else {
+ DEBUG_INFO("WIFI", "STA netif 已存在,跳过创建");
+ }
+
+ wifi_init_config_t wifi_cfg = WIFI_INIT_CONFIG_DEFAULT();
+ ESP_ERROR_CHECK(esp_wifi_init(&wifi_cfg));
+ DEBUG_INFO("WIFI", "WiFi driver 初始化成功");
+
+ // STA 优先,失败时返回 false 让 task 切 AP
+ if (CONFIG_WIFI_MODE == 1) {
+ DEBUG_INFO("WIFI", "工作模式: STA (优先)");
+ err = wifi_init_sta();
+ if (err == ESP_OK) {
+ // 连接已触发(STA_START 事件驱动),返回 true
+ // 实际连接状态由 wifi_ota_task 等待确认
+ return true;
+ }
+ DEBUG_WARN("WIFI", "STA 初始化失败,将切换 AP");
+ return false;
+ } else {
+ DEBUG_INFO("WIFI", "工作模式: AP");
+ wifi_init_ap();
+ return false; // AP 模式,不需要等待连接
+ }
+}
+
+// ----------------------------------------
+// WiFi OTA FreeRTOS 任务
+// WiFi 初始化已在 app_main 中通过 wifi_early_init() 完成
+// 此任务负责:等待连接结果 → 启动 HTTP 服务器 → 状态监控
+/**
+ * @brief WiFi OTA FreeRTOS 任务,提供 HTTP 上传和中继触发。
+ * @param pvParameters FreeRTOS 任务参数,未使用。
+ */
+void wifi_ota_task(void *pvParameters)
+{
+ (void)pvParameters;
+
+ DEBUG_INFO("WIFI", "=== WiFi OTA 任务启动 ===");
+
+ // 等待 STA 连接结果(wifi_early_init 已触发连接)
+ if (wifi_get_mode() == OTA_WIFI_MODE_STA) {
+ char ip_str[16] = {0};
+ esp_err_t err = wifi_wait_sta_connected(ip_str, sizeof(ip_str));
+ if (err == ESP_OK) {
+ DEBUG_INFO("WIFI", "STA 已连接, IP: %s", ip_str);
+ } else {
+ DEBUG_WARN("WIFI", "STA 连接超时,切换到 AP 模式...");
+ esp_wifi_stop(); // 先停 STA,再切 AP
+ /** @brief 当前 WiFi 工作模式(AP 或 STA)。 */
+ s_wifi_mode = OTA_WIFI_MODE_AP;
+ wifi_init_ap();
+ }
+ }
+ // AP 模式:wifi_early_init 已完成,无需额外操作
+
+ esp_err_t err = http_ota_server_start();
+ if (err != ESP_OK) {
+ DEBUG_ERR("WIFI", "HTTP 服务器启动失败");
+ }
+
+ // ★ WiFi 已连接,UDP 从这里开始能通
+ // 等待网络栈稳定(ARP 表建立、路由就绪),避免首批 UDP 包丢失
+ vTaskDelay(pdMS_TO_TICKS(500));
+
+ // 启动摘要,发两遍确保上位机收到(第一包可能触发 ARP 被丢)
+ esp_chip_info_t chip;
+ esp_chip_info(&chip);
+ for (int i = 0; i < 2; i++) {
+ DEBUG_INFO("WIFI", "========================================");
+ DEBUG_INFO("WIFI", "[启动摘要] ESP32-C3 OTA Relay v%s", FW_VERSION);
+ DEBUG_INFO("WIFI", "[启动摘要] Chip: %s, Rev %d", CONFIG_IDF_TARGET, chip.revision);
+ DEBUG_INFO("WIFI", "[启动摘要] Free heap: %lu bytes", (unsigned long)esp_get_free_heap_size());
+ DEBUG_INFO("WIFI", "[启动摘要] WiFi: %s", wifi_get_mode() == OTA_WIFI_MODE_STA ? "STA" : "AP");
+ DEBUG_INFO("WIFI", "[启动摘要] NVS + WiFi driver + netif + event handler 初始化完成");
+ DEBUG_INFO("WIFI", "[启动摘要] HTTP 服务器端口: %d", HTTP_SERVER_PORT);
+ DEBUG_INFO("WIFI", "========================================");
+ DEBUG_INFO("WIFI", "=== WiFi OTA 服务器就绪 ===");
+ if (i == 0) vTaskDelay(pdMS_TO_TICKS(200));
+ }
+
+ // WiFi 状态监控
+ // 每秒一次循环:每 10 秒检查 RSSI
+ uint32_t tick = 0;
+ while (1) {
+ tick++;
+
+ // 每 10 秒检查一次 RSSI
+ if (tick % 10 == 0) {
+ int8_t rssi = wifi_get_rssi();
+ if (rssi != -127) {
+ health_update_from_wifi_rssi(rssi);
+
+ if (rssi < WIFI_RSSI_CRITICAL) {
+ DEBUG_WARN("WIFI", "WiFi RSSI 危险: %d dBm (阈值: %d)", rssi, WIFI_RSSI_CRITICAL);
+ } else if (rssi < WIFI_RSSI_WARNING) {
+ DEBUG_WARN("WIFI", "WiFi RSSI 较弱: %d dBm", rssi);
+ }
+ }
+ }
+
+ vTaskDelay(pdMS_TO_TICKS(1000));
+ }
+}
diff --git a/docs/COMMUNICATION_AUDIT_2026-05-05.md b/docs/COMMUNICATION_AUDIT_2026-05-05.md
index e8eb630..dcca2df 100644
--- a/docs/COMMUNICATION_AUDIT_2026-05-05.md
+++ b/docs/COMMUNICATION_AUDIT_2026-05-05.md
@@ -6,7 +6,7 @@
### WebUI 实际在用的接口
-- `GET /system/status`:每 1 秒轮询一次,用于在线状态、WiFi、heap、温度、电压、OTA 状态展示
+- `GET /system/status`:每 1 秒轮询一次,用于在线状态、WiFi、heap、温度、OTA 状态展示
- `GET /logs`:每 1 秒轮询一次,用于拉取并清空日志环形缓冲区
- `POST /can/send`:发送 CAN 请求到 GD32
- `POST /ota/upload` / `POST /ota/relay` / `POST /ota/update`:OTA 相关
diff --git a/docs/DEVELOPMENT_GUIDE.md b/docs/DEVELOPMENT_GUIDE.md
index 081d6e1..bac07d1 100644
--- a/docs/DEVELOPMENT_GUIDE.md
+++ b/docs/DEVELOPMENT_GUIDE.md
@@ -28,7 +28,7 @@
|
+-- 存储 GD32 固件到 Flash
+-- 四阶段安全握手协议
- +-- 内置温度/电压/WiFi 自检
+ +-- 内置温度/WiFi 自检
```
ESP32 的角色是**数据透传 + 存储中继**,GD32 OTA 的加密握手和数据传输逻辑全部由 ESP32 透传到 GD32,ESP32 本身不解析固件内容。
@@ -74,7 +74,6 @@ ESP32 的角色是**数据透传 + 存储中继**,GD32 OTA 的加密握手和
不外挂传感器,使用 ESP32 内置传感器:
- **芯片温度**(正常 < 80°C / 警告 80-100°C / 停机 > 100°C)— 1s 轮询
-- **供电电压**(正常 2.5-3.6V / 警告 < 2.5V / 停机 < 2.3V)— 5s 轮询
- **WiFi RSSI**(正常 > -70 / 弱 -70~-85 / 断开 < -85 dBm)— 10s 轮询
- **BLE 连接状态** — 事件驱动
diff --git a/docs/esp32_ota_relay_design.md b/docs/esp32_ota_relay_design.md
index b68c75a..d01475e 100644
--- a/docs/esp32_ota_relay_design.md
+++ b/docs/esp32_ota_relay_design.md
@@ -604,7 +604,6 @@ ESP32-C3FH4 利用芯片内置传感器实现运行安全监控,不外挂额
| 检测项 | 来源 | 阈值范围 | 处理动作 |
|---|---|---|---|
| 芯片温度 | 内部温度传感器 | 正常: < 80°C / 警告: 80-100°C / 停机: > 100°C | 警告时降频,停机时中止 OTA |
-| 供电电压 | VDD 电压传感器 | 正常: 2.5V - 3.6V / 警告: < 2.5V / 停机: < 2.3V | 警告时提示,停机时挂起任务 |
| Flash 空间 | 系统 API | 可用 > 16KB(存储分区元数据) | 空间不足时拒绝 OTA 请求 |
| WiFi 信号强度 | WiFi RSSI | 正常: > -70 dBm / 弱: -70 ~ -85 dBm / 断开: < -85 dBm | 弱时降低 OTA 速度,断开时暂停 |
| BLE 连接状态 | BLE stack | 连接中 / 断开 | 断开时通知 APP 重连 |
@@ -614,7 +613,6 @@ ESP32-C3FH4 利用芯片内置传感器实现运行安全监控,不外挂额
| 检测项 | 频率 | 方式 |
|---|---|---|
| 芯片温度 | 每 1 秒 | FreeRTOS Timer |
-| 供电电压 | 每 5 秒 | FreeRTOS Timer |
| Flash 空间 | OTA 开始前检查 | 一次性 |
| WiFi RSSI | 每 10 秒 | 轮询 |
| BLE 连接 | 事件驱动 | BLE GAP 回调 |
@@ -648,7 +646,6 @@ typedef enum {
```
[TIMESTAMP] TEMP_WARNING: 85°C
-[TIMESTAMP] VOLT_WARNING: 2.45V
[TIMESTAMP] OTA_PAUSED: temp_critical
[TIMESTAMP] OTA_RESUMED
```
@@ -663,7 +660,6 @@ typedef enum {
│ (FreeRTOS Task, Priority High) │
├──────────────────────────────────┤
│ temp_monitor (1s timer) │
-│ voltage_monitor (5s timer) │
│ wifi_monitor (10s timer) │
│ ble_monitor (event-driven) │
├──────────────────────────────────┤
diff --git a/main/core/main.cpp b/main/core/main.cpp
index cef0fb1..37e4c40 100644
--- a/main/core/main.cpp
+++ b/main/core/main.cpp
@@ -55,7 +55,7 @@ extern "C" void app_main(void)
DEBUG_INFO("MAIN", "WiFi: %s", wifi_sta_ok ? "STA 连接中" : "将使用 AP 模式");
DEBUG_INFO("MAIN", "========================================");
- // 健康监控任务(温度 + 电压 + WiFi RSSI)
+ // 健康监控任务(温度 + WiFi RSSI)
BaseType_t xRet;
xRet = xTaskCreatePinnedToCore(
health_monitor_task,
diff --git a/main/include/app_config.h b/main/include/app_config.h
index 663bdcd..8a59a28 100644
--- a/main/include/app_config.h
+++ b/main/include/app_config.h
@@ -10,7 +10,7 @@
// ============================================================
// 固件版本
// ============================================================
-inline constexpr const char *FW_VERSION = "0.0.6";
+inline constexpr const char *FW_VERSION = "0.1.4";
// ============================================================
// WiFi 工作模式选择
@@ -53,7 +53,7 @@ inline constexpr uint32_t HTTP_SERVER_STACK_SIZE = 8192;
// ============================================================
// OTA 配置
// ============================================================
-inline constexpr size_t OTA_BLOCK_SIZE = 4096;
+inline constexpr size_t OTA_BLOCK_SIZE = 8192;
inline constexpr size_t OTA_MAX_FIRMWARE_SIZE = 2 * 1024 * 1024; // 2MB
// ============================================================
@@ -61,6 +61,9 @@ inline constexpr size_t OTA_MAX_FIRMWARE_SIZE = 2 * 1024 * 1024; // 2MB
// ============================================================
inline constexpr int32_t WIFI_RSSI_CRITICAL = -85;
inline constexpr int32_t WIFI_RSSI_WARNING = -75;
+inline constexpr int32_t OTA_WIFI_RSSI_CRITICAL = -88; // OTA 期间更宽松,避免瞬时波动误判
+inline constexpr uint32_t OTA_WIFI_RSSI_SAMPLE_MAX_AGE_MS = 15000; // OTA 最多接受 15 秒内的 RSSI 样本
+inline constexpr uint32_t OTA_PACKET_HEALTH_CHECK_INTERVAL = 32; // 每 32 个 48B 包做一次 OTA 健康检查
inline constexpr int32_t TEMP_WARNING = 80;
inline constexpr int32_t TEMP_CRITICAL = 100;
inline constexpr size_t HEAP_WARNING_MIN = 10240;
diff --git a/main/monitor/health_monitor.cpp b/main/monitor/health_monitor.cpp
index f48355b..1ba9634 100644
--- a/main/monitor/health_monitor.cpp
+++ b/main/monitor/health_monitor.cpp
@@ -1,6 +1,6 @@
/*
* health_monitor.cpp - 系统健康监控
- * 包含:健康状态管理 + WiFi RSSI 监控 + 温度监控 + 电压监控
+ * 包含:健康状态管理 + WiFi RSSI 监控 + 温度监控
*/
#include
#include
@@ -14,6 +14,7 @@
#include "health_monitor.h"
#include "debug_log.h"
+#include "server/wifi_ota.h"
/** @brief 全局健康状态,受互斥锁保护。 */
health_state_t g_health_state = SYS_NORMAL;
@@ -24,8 +25,10 @@ static SemaphoreHandle_t g_health_mutex = NULL;
static temperature_sensor_handle_t s_temp_handle = NULL;
/** @brief 最近一次采样的芯片温度(摄氏度)。 */
static float s_last_temperature = 0.0f;
-/** @brief 最近一次估算的系统电压(伏特)。 */
-static float s_last_voltage = 3.30f;
+/** @brief 最近一次健康监控采样得到的 WiFi RSSI。 */
+static int8_t s_last_wifi_rssi = -127;
+/** @brief 最近一次 WiFi RSSI 采样时间(毫秒)。 */
+static int64_t s_last_wifi_rssi_ms = 0;
// ----------------------------------------
// 健康状态管理
@@ -68,14 +71,18 @@ float health_get_last_temperature(void)
return s_last_temperature;
}
-/**
- * @brief 返回最近一次记录的系统电压估算值。
- * @return 最近一次电压估算值(伏特)。
- */
-float health_get_last_voltage(void)
+bool health_get_last_wifi_rssi(int8_t *out_rssi, uint32_t *out_age_ms)
{
- /** @brief 最近一次估算的系统电压(伏特)。 */
- return s_last_voltage;
+ if (s_last_wifi_rssi_ms <= 0) {
+ return false;
+ }
+
+ int64_t now_ms = esp_timer_get_time() / 1000;
+ uint32_t age_ms = (now_ms > s_last_wifi_rssi_ms) ? (uint32_t)(now_ms - s_last_wifi_rssi_ms) : 0;
+
+ if (out_rssi) *out_rssi = s_last_wifi_rssi;
+ if (out_age_ms) *out_age_ms = age_ms;
+ return true;
}
// ----------------------------------------
@@ -112,36 +119,9 @@ static void temp_monitor_callback(void *arg)
}
// ----------------------------------------
-// 电压监控(启动时调用一次)
+// 温度监控任务入口
/**
- * @brief 电压定时回调,采样估算电压并根据阈值更新健康状态。
- * @param arg
- */
-static void voltage_monitor_callback(void *arg)
-{
- float voltage = 3.30f; // ESP32-C3 无内置电压传感器,使用估算
- /** @brief 最近一次估算的系统电压(伏特)。 */
- s_last_voltage = voltage;
-
- if (voltage < 2.3f) {
- /** @brief 全局健康状态,受互斥锁保护。 */
- g_health_state = SYS_SAFE_SHUTDOWN;
- DEBUG_ERR("VOLT", "!!! 危险 !!! 电压 %.2fV < 2.3V,触发安全关机", voltage);
- DEBUG_ERR("VOLT", "健康状态变更为: SYS_SAFE_SHUTDOWN");
- } else if (voltage < 2.5f) {
- if (g_health_state < SYS_WARNING) {
- /** @brief 全局健康状态,受互斥锁保护。 */
- g_health_state = SYS_WARNING;
- DEBUG_WARN("VOLT", "警告: 电压 %.2fV < 2.5V", voltage);
- DEBUG_WARN("VOLT", "健康状态变更为: SYS_WARNING");
- }
- }
-}
-
-// ----------------------------------------
-// 温度电压监控任务入口
-/**
- * @brief 启动温度和电压监控定时器,周期性巡检系统健康。
+ * @brief 启动温度监控定时器,周期性巡检系统健康。
* @param pvParameters FreeRTOS 任务参数,未使用。
*/
void health_monitor_task(void *pvParameters)
@@ -170,23 +150,15 @@ void health_monitor_task(void *pvParameters)
DEBUG_INFO("MON", "温度监控定时器已启动 (间隔 1 秒)");
- voltage_monitor_callback(NULL);
-
- // 每1秒刷新一次估算电压
- esp_timer_handle_t volt_timer;
- esp_timer_create_args_t volt_timer_args = {};
- volt_timer_args.callback = &voltage_monitor_callback;
- volt_timer_args.arg = nullptr;
- volt_timer_args.name = "volt_monitor";
- volt_timer_args.dispatch_method = ESP_TIMER_TASK;
- volt_timer_args.skip_unhandled_events = false;
- esp_timer_create(&volt_timer_args, &volt_timer);
- esp_timer_start_periodic(volt_timer, 1 * 1000000); // 1秒
- DEBUG_INFO("MON", "电压监控定时器已启动 (间隔 1 秒)");
-
// 保活循环:定时器由 esp_timer 任务驱动,此任务只需保持存活
// 状态汇报已由 status_monitor_task 负责,此处只在异常时告警
while (1) {
+ int8_t rssi = wifi_get_rssi();
+ if (wifi_is_connected() && rssi != -127) {
+ s_last_wifi_rssi = rssi;
+ s_last_wifi_rssi_ms = esp_timer_get_time() / 1000;
+ }
+
uint32_t free_heap = esp_get_free_heap_size();
if (free_heap < HEAP_WARNING_MIN) {
DEBUG_WARN("MON", "空闲堆内存不足: %lu < %d", (unsigned long)free_heap, HEAP_WARNING_MIN);
@@ -231,20 +203,26 @@ health_status_t health_check_before_ota(void)
DEBUG_WARN("HEALTH", "OTA 前检查: SYS_WARNING,继续传输但需注意");
}
- wifi_ap_record_t ap_info;
- if (esp_wifi_sta_get_ap_info(&ap_info) == ESP_OK) {
- if (ap_info.rssi < WIFI_RSSI_CRITICAL) {
- /** @brief 全局健康状态,受互斥锁保护。 */
- g_health_state = SYS_CRITICAL;
- DEBUG_WARN("HEALTH", "OTA 前检查: WiFi RSSI %d < %d (危险阈值)",
- ap_info.rssi, WIFI_RSSI_CRITICAL);
- return HEALTH_MON_CRITICAL;
- } else if (ap_info.rssi < WIFI_RSSI_NORMAL) {
- if (status < HEALTH_MON_WARNING) {
- status = HEALTH_MON_WARNING;
+ if (!wifi_is_connected()) {
+ DEBUG_WARN("HEALTH", "OTA 前检查: WiFi 未连接");
+ return HEALTH_MON_CRITICAL;
+ }
+
+ int8_t last_rssi = -127;
+ uint32_t age_ms = 0;
+ if (health_get_last_wifi_rssi(&last_rssi, &age_ms)) {
+ if (age_ms <= OTA_WIFI_RSSI_SAMPLE_MAX_AGE_MS) {
+ if (last_rssi < OTA_WIFI_RSSI_CRITICAL) {
+ DEBUG_WARN("HEALTH", "OTA 前检查: 最近 RSSI %d < %d (样本年龄 %lu ms)",
+ last_rssi, OTA_WIFI_RSSI_CRITICAL, (unsigned long)age_ms);
+ return HEALTH_MON_CRITICAL;
+ } else if (last_rssi < WIFI_RSSI_NORMAL) {
+ if (status < HEALTH_MON_WARNING) {
+ status = HEALTH_MON_WARNING;
+ }
+ DEBUG_WARN("HEALTH", "OTA 前检查: 最近 RSSI %d < %d (样本年龄 %lu ms)",
+ last_rssi, WIFI_RSSI_NORMAL, (unsigned long)age_ms);
}
- DEBUG_WARN("HEALTH", "OTA 前检查: WiFi RSSI %d < %d (警告阈值)",
- ap_info.rssi, WIFI_RSSI_NORMAL);
}
}
diff --git a/main/monitor/health_monitor.h b/main/monitor/health_monitor.h
index d248225..0aa851b 100644
--- a/main/monitor/health_monitor.h
+++ b/main/monitor/health_monitor.h
@@ -34,7 +34,7 @@ extern health_state_t g_health_state;
void health_monitor_init(void);
/**
- * @brief 健康监控任务入口,负责启动温度/电压定时检测并周期性巡检系统状态。
+ * @brief 健康监控任务入口,负责启动温度定时检测并周期性巡检系统状态。
* @param pvParameters FreeRTOS 任务参数,未使用。
*/
void health_monitor_task(void *pvParameters);
@@ -72,7 +72,9 @@ health_status_t health_update_from_wifi_rssi(int8_t rssi);
float health_get_last_temperature(void);
/**
- * @brief 获取最近一次记录的系统电压估算值(伏特)。
- * @return 最近一次电压估算值(伏特)。
+ * @brief 获取最近一次健康监控采样得到的 WiFi RSSI。
+ * @param out_rssi 输出 RSSI。
+ * @param out_age_ms 输出样本距今时长(毫秒)。
+ * @return true 有有效样本,false 当前无有效样本。
*/
-float health_get_last_voltage(void);
+bool health_get_last_wifi_rssi(int8_t *out_rssi, uint32_t *out_age_ms);
diff --git a/main/relay/ota_relay.cpp b/main/relay/ota_relay.cpp
index 5f09a73..ce7f0cc 100644
--- a/main/relay/ota_relay.cpp
+++ b/main/relay/ota_relay.cpp
@@ -376,10 +376,12 @@ static ota_result_t relay_packets_from_flash(void)
g_relay_state = RELAY_RELAY_ACTIVE;
for (uint32_t seq = 0; seq < total_packets; seq++) {
- health_status_t health = health_check_before_ota();
- if (health == HEALTH_MON_SHUTDOWN || health == HEALTH_MON_CRITICAL) {
- DEBUG_ERR("OTA", "健康检查危险,中止 OTA");
- return OTA_RESULT_ERR_HEALTH;
+ if ((seq % OTA_PACKET_HEALTH_CHECK_INTERVAL) == 0 || seq == 0 || seq == total_packets - 1) {
+ health_status_t health = health_check_before_ota();
+ if (health == HEALTH_MON_SHUTDOWN || health == HEALTH_MON_CRITICAL) {
+ DEBUG_ERR("OTA", "健康检查危险,中止 OTA");
+ return OTA_RESULT_ERR_HEALTH;
+ }
}
size_t chunk_size = 48;
diff --git a/main/server/wifi_ota.cpp b/main/server/wifi_ota.cpp
index 23657a3..005275b 100644
--- a/main/server/wifi_ota.cpp
+++ b/main/server/wifi_ota.cpp
@@ -445,6 +445,27 @@ static esp_err_t esp32_ota_update_handler(httpd_req_t *req)
return ESP_OK;
}
+/**
+ * @brief HTTP POST /restart - 立即重启 ESP32
+ * @param req
+ * @return
+ */
+static esp_err_t restart_handler(httpd_req_t *req)
+{
+ (void)req;
+ http_set_cors_headers(req);
+
+ DEBUG_INFO("OTA", "收到重启请求,500ms 后重启...");
+
+ const char resp[] = "{\"status\": \"ok\", \"message\": \"rebooting\"}";
+ httpd_resp_set_type(req, "application/json");
+ httpd_resp_send(req, resp, strlen(resp));
+
+ vTaskDelay(pdMS_TO_TICKS(500));
+ esp_restart();
+ return ESP_OK;
+}
+
/**
* @brief HTTP GET /ota/update - 查询 ESP32 OTA 状态
* @param req
@@ -558,7 +579,7 @@ static esp_err_t ota_upload_handler(httpd_req_t *req)
}
// 堆分配缓冲区,避免栈溢出(HTTP server 栈仅 8KB)
- uint8_t *fw_buf = (uint8_t *)malloc(1024);
+ uint8_t *fw_buf = (uint8_t *)malloc(OTA_BLOCK_SIZE);
if (!fw_buf) {
httpd_resp_send_err(req, HTTPD_500_INTERNAL_SERVER_ERROR, "内存分配失败");
return ESP_FAIL;
@@ -570,7 +591,7 @@ static esp_err_t ota_upload_handler(httpd_req_t *req)
while (total_received < req->content_len) {
size_t to_read = req->content_len - total_received;
- if (to_read > 1024) to_read = 1024;
+ if (to_read > OTA_BLOCK_SIZE) to_read = OTA_BLOCK_SIZE;
int ret = httpd_req_recv(req, (char *)fw_buf, to_read);
if (ret <= 0) break;
@@ -628,7 +649,7 @@ static esp_err_t ota_upload_handler(httpd_req_t *req)
uint32_t fw_size = gd32_header[2] | (gd32_header[3] << 8) | (gd32_header[4] << 16) | (gd32_header[5] << 24);
if (seq == 0) {
ota_meta_t meta = { .total_size = fw_size, .stored_size = 0, .last_seq = 0,
- .chunk_count = (fw_size + 4095) / 4096, .checksum = {0} };
+ .chunk_count = (fw_size + OTA_BLOCK_SIZE - 1) / OTA_BLOCK_SIZE, .checksum = {0} };
meta_save(&meta);
DEBUG_INFO("WIFI", "开始 GD32 固件存储: %u 字节", fw_size);
}
@@ -943,14 +964,14 @@ static esp_err_t system_status_handler(httpd_req_t *req)
memset(&meta, 0, sizeof(meta));
meta_load(&meta);
- char resp[640];
+ char resp[700];
int len = snprintf(resp, sizeof(resp),
- "{\"version\": \"%s\", \"health\": %d, \"free_heap\": %lu, \"min_free_heap\": %lu, \"temperature\": %.1f, \"voltage\": %.2f, \"wifi\": {\"mode\": \"%s\", \"ip\": \"%s\", \"rssi\": %d, \"connected\": %s}, \"ota\": {\"total_size\": %lu, \"stored_size\": %lu, \"storage_done\": %s, \"relay_state\": %d}}",
+ "{\"version\": \"%s\", \"health\": %d, \"free_heap\": %lu, \"min_free_heap\": %lu, \"temperature\": %.1f, \"uptime_sec\": %lu, \"wifi\": {\"mode\": \"%s\", \"ip\": \"%s\", \"rssi\": %d, \"connected\": %s}, \"ota\": {\"total_size\": %lu, \"stored_size\": %lu, \"storage_done\": %s, \"relay_state\": %d}}",
FW_VERSION,health_get_current(),
(unsigned long)esp_get_free_heap_size(),
(unsigned long)esp_get_minimum_free_heap_size(),
(double)health_get_last_temperature(),
- (double)health_get_last_voltage(),
+ (unsigned long)(esp_timer_get_time() / 1000000),
s_wifi_mode == OTA_WIFI_MODE_AP ? "AP" : "STA",
ip_str,
rssi,
@@ -1047,6 +1068,21 @@ static esp_err_t http_ota_server_start(void)
};
httpd_register_uri_handler(s_http_server, &esp32_update_options_uri);
+ httpd_uri_t restart_uri = {
+ .uri = "/restart",
+ .method = HTTP_POST,
+ .handler = restart_handler,
+ .user_ctx = NULL
+ };
+ httpd_register_uri_handler(s_http_server, &restart_uri);
+ httpd_uri_t restart_options_uri = {
+ .uri = "/restart",
+ .method = HTTP_OPTIONS,
+ .handler = http_options_handler,
+ .user_ctx = NULL
+ };
+ httpd_register_uri_handler(s_http_server, &restart_options_uri);
+
httpd_uri_t esp32_status_uri = {
.uri = "/ota/esp32_status",
.method = HTTP_GET,
diff --git a/tools/app.js b/tools/app.js
index 2ca7e4d..4c307da 100644
--- a/tools/app.js
+++ b/tools/app.js
@@ -18,6 +18,18 @@ function formatTime(date) {
return date.toTimeString().split(' ')[0];
}
+function formatUptime(sec) {
+ if (!Number.isFinite(sec) || sec < 0) return '--';
+ const d = Math.floor(sec / 86400);
+ const h = Math.floor((sec % 86400) / 3600);
+ const m = Math.floor((sec % 3600) / 60);
+ const s = Math.floor(sec % 60);
+ if (d > 0) return `${d}d ${h}h ${m}m`;
+ if (h > 0) return `${h}h ${m}m ${s}s`;
+ if (m > 0) return `${m}m ${s}s`;
+ return `${s}s`;
+}
+
function showToast(message, type = 'success') {
const toast = document.getElementById('toast');
const icon = document.getElementById('toastIcon');
@@ -150,7 +162,8 @@ function updateSystemStatusUi(data) {
}
document.getElementById('statTemperature').textContent = Number.isFinite(data.temperature) ? `${data.temperature.toFixed(1)}°C` : '--';
- document.getElementById('statVoltage').textContent = Number.isFinite(data.voltage) ? `${data.voltage.toFixed(2)}V` : '--';
+ const uptimeEl = document.getElementById('statUptime');
+ if (uptimeEl) uptimeEl.textContent = formatUptime(data.uptime_sec);
document.getElementById('statWifi').textContent = wifi.connected ? (wifi.ip || '已连接') : '未连接';
document.getElementById('statSignal').textContent = (wifi.rssi !== undefined && wifi.rssi !== -127) ? `${wifi.rssi} dBm` : '--';
document.getElementById('statHeap').textContent = data.free_heap !== undefined ? formatBytes(data.free_heap) : '--';
@@ -179,20 +192,6 @@ async function testConnection() {
showToast(connected ? 'ESP32 当前在线' : 'ESP32 当前未连接', connected ? 'success' : 'warning');
}
-async function healthCheck() {
- if (!latestSystemStatus) {
- addLog('W', '健康', '暂无同步数据,请等待 1 秒自动同步');
- showToast('暂无同步数据,请稍等 1 秒', 'warning');
- return;
- }
-
- updateSystemStatusUi(latestSystemStatus);
- const data = latestSystemStatus;
- const healthNames = ['正常', '警告', '危险', '关机'];
- const health = data.health !== undefined ? (healthNames[data.health] || '未知') : '未知';
- addLog('I', '健康', `当前状态: ${health}, 空闲堆: ${data.free_heap} bytes, 温度: ${data.temperature}°C, 电压: ${data.voltage}V`);
- showToast(`当前健康状态: ${health}`, 'success');
-}
// ==================== CAN 帧发送 ====================
async function sendCanFrame() {
@@ -311,7 +310,7 @@ function processFile(file) {
document.getElementById('fileType').textContent = file.name.endsWith('.hex') ? 'Intel HEX' : 'Binary';
document.getElementById('fileSize').textContent = formatBytes(file.size);
- const chunks = Math.ceil(file.size / 4096);
+ const chunks = Math.ceil(file.size / 8192);
document.getElementById('fileChunks').textContent = chunks + ' 包';
fileInfo.classList.add('visible');
@@ -450,7 +449,6 @@ async function triggerEsp32Update() {
if (response.ok) {
const data = await response.json();
addLog('I', 'OTA', `升级完成: ${data.message}`);
- addLog('I', 'OTA', 'ESP32 即将重启,新固件将生效,页面将继续通过 1 秒状态同步自动恢复显示');
showToast('ESP32 重启中...', 'success');
}
} catch (e) {
@@ -459,6 +457,29 @@ async function triggerEsp32Update() {
}
}
+async function restartEsp32() {
+ if (!confirm('确定要重启 ESP32 吗?')) return;
+
+ addLog('I', '系统', '=== 请求重启 ESP32 ===');
+ showToast('ESP32 正在重启...', 'success');
+
+ try {
+ const response = await fetch(`${getBaseUrl()}/restart`, {
+ method: 'POST',
+ signal: AbortSignal.timeout(5000)
+ });
+
+ if (response.ok) {
+ const data = await response.json();
+ addLog('I', '系统', `重启已触发: ${data.message}`);
+ showToast('ESP32 重启中,页面将自动恢复...', 'success');
+ }
+ } catch (e) {
+ addLog('E', '系统', `重启请求失败: ${e.message}`);
+ showToast('重启失败', 'error');
+ }
+}
+
// ==================== 触发 OTA 中继 ====================
async function triggerRelay() {
addLog('I', 'OTA', '触发 GD32 OTA 中继...');
diff --git a/tools/index.html b/tools/index.html
index b7923b1..077d607 100644
--- a/tools/index.html
+++ b/tools/index.html
@@ -58,8 +58,8 @@
-
-
- 🔄 完成 ESP32 升级(重启)
-
@@ -156,6 +153,7 @@
@@ -163,10 +161,6 @@
--
芯片温度
-