From adb7dc65caea937eb39408c7ced6fa0eee54192c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=B0=8F=E9=BE=99=E8=99=BE?= Date: Thu, 7 May 2026 23:50:04 +0800 Subject: [PATCH] =?UTF-8?q?refactor(app):=20=E5=88=A0=E9=99=A4=E8=BF=87?= =?UTF-8?q?=E6=97=B6=E7=9A=84app.js=E5=A4=87=E4=BB=BD=E6=96=87=E4=BB=B6?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 移除 _assistant_backups 目录中的 app.js.bak_2026-05-05_0451 备份文件, 该文件包含完整的JavaScript应用程序代码,包括全局状态管理、工具函数、 日志系统、连接管理、CAN帧发送、文件处理、固件上传等功能模块。 --- .../2026-05-05/app.js.bak_2026-05-05_0451 | 557 ------- .../2026-05-05/app.js.bak_2026-05-05_0522 | 556 ------- .../app_config.h.bak_2026-05-05_0516 | 105 -- .../app_config.h.bak_2026-05-05_0522 | 108 -- .../app_config.h.bak_2026-05-05_0525 | 108 -- .../ble_ota.cpp.bak_2026-05-05_0522 | 643 -------- .../health_monitor.cpp.bak_2026-05-05_0451 | 289 ---- .../health_monitor.cpp.bak_2026-05-05_0516 | 236 --- .../health_monitor.h.bak_2026-05-05_0451 | 78 - .../health_monitor.h.bak_2026-05-05_0516 | 72 - .../ota_relay.cpp.bak_2026-05-05_0516 | 587 ------- .../wifi_ota.cpp.bak_2026-05-05_0451 | 1369 ----------------- .../wifi_ota.cpp.bak_2026-05-05_0510 | 1368 ---------------- .../wifi_ota.cpp.bak_2026-05-05_0522 | 1368 ---------------- .../wifi_ota.cpp.bak_2026-05-05_0525 | 1368 ---------------- main/include/app_config.h | 11 +- main/relay/ota_relay.cpp | 337 ++-- main/server/ble_ota.cpp | 444 +++++- main/server/wifi_ota.cpp | 80 +- main/transport/spi_master.cpp | 151 ++ main/transport/spi_master.h | 33 +- tools/ble_ota.html | 831 ++++++++++ tools/index.html | 3 + tools/serve.bat | 21 + 24 files changed, 1754 insertions(+), 8969 deletions(-) delete mode 100644 _assistant_backups/2026-05-05/app.js.bak_2026-05-05_0451 delete mode 100644 _assistant_backups/2026-05-05/app.js.bak_2026-05-05_0522 delete mode 100644 _assistant_backups/2026-05-05/app_config.h.bak_2026-05-05_0516 delete mode 100644 _assistant_backups/2026-05-05/app_config.h.bak_2026-05-05_0522 delete mode 100644 _assistant_backups/2026-05-05/app_config.h.bak_2026-05-05_0525 delete mode 100644 _assistant_backups/2026-05-05/ble_ota.cpp.bak_2026-05-05_0522 delete mode 100644 _assistant_backups/2026-05-05/health_monitor.cpp.bak_2026-05-05_0451 delete mode 100644 _assistant_backups/2026-05-05/health_monitor.cpp.bak_2026-05-05_0516 delete mode 100644 _assistant_backups/2026-05-05/health_monitor.h.bak_2026-05-05_0451 delete mode 100644 _assistant_backups/2026-05-05/health_monitor.h.bak_2026-05-05_0516 delete mode 100644 _assistant_backups/2026-05-05/ota_relay.cpp.bak_2026-05-05_0516 delete mode 100644 _assistant_backups/2026-05-05/wifi_ota.cpp.bak_2026-05-05_0451 delete mode 100644 _assistant_backups/2026-05-05/wifi_ota.cpp.bak_2026-05-05_0510 delete mode 100644 _assistant_backups/2026-05-05/wifi_ota.cpp.bak_2026-05-05_0522 delete mode 100644 _assistant_backups/2026-05-05/wifi_ota.cpp.bak_2026-05-05_0525 create mode 100644 tools/ble_ota.html create mode 100644 tools/serve.bat 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 deleted file mode 100644 index 2ca7e4d..0000000 --- a/_assistant_backups/2026-05-05/app.js.bak_2026-05-05_0451 +++ /dev/null @@ -1,557 +0,0 @@ -// ==================== 全局状态 ==================== -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 deleted file mode 100644 index f4e14e7..0000000 --- a/_assistant_backups/2026-05-05/app.js.bak_2026-05-05_0522 +++ /dev/null @@ -1,556 +0,0 @@ -// ==================== 全局状态 ==================== -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 deleted file mode 100644 index 43586fc..0000000 --- a/_assistant_backups/2026-05-05/app_config.h.bak_2026-05-05_0516 +++ /dev/null @@ -1,105 +0,0 @@ -/* - * 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 deleted file mode 100644 index a5f1529..0000000 --- a/_assistant_backups/2026-05-05/app_config.h.bak_2026-05-05_0522 +++ /dev/null @@ -1,108 +0,0 @@ -/* - * 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 deleted file mode 100644 index 3e44c94..0000000 --- a/_assistant_backups/2026-05-05/app_config.h.bak_2026-05-05_0525 +++ /dev/null @@ -1,108 +0,0 @@ -/* - * 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 deleted file mode 100644 index 44b07c4..0000000 --- a/_assistant_backups/2026-05-05/ble_ota.cpp.bak_2026-05-05_0522 +++ /dev/null @@ -1,643 +0,0 @@ -/* - * 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 deleted file mode 100644 index f48355b..0000000 --- a/_assistant_backups/2026-05-05/health_monitor.cpp.bak_2026-05-05_0451 +++ /dev/null @@ -1,289 +0,0 @@ -/* - * 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 deleted file mode 100644 index 863ffe8..0000000 --- a/_assistant_backups/2026-05-05/health_monitor.cpp.bak_2026-05-05_0516 +++ /dev/null @@ -1,236 +0,0 @@ -/* - * 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 deleted file mode 100644 index d248225..0000000 --- a/_assistant_backups/2026-05-05/health_monitor.h.bak_2026-05-05_0451 +++ /dev/null @@ -1,78 +0,0 @@ -/* - * 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 deleted file mode 100644 index 898adef..0000000 --- a/_assistant_backups/2026-05-05/health_monitor.h.bak_2026-05-05_0516 +++ /dev/null @@ -1,72 +0,0 @@ -/* - * 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 deleted file mode 100644 index 5f09a73..0000000 --- a/_assistant_backups/2026-05-05/ota_relay.cpp.bak_2026-05-05_0516 +++ /dev/null @@ -1,587 +0,0 @@ -/* - * 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 deleted file mode 100644 index 23657a3..0000000 --- a/_assistant_backups/2026-05-05/wifi_ota.cpp.bak_2026-05-05_0451 +++ /dev/null @@ -1,1369 +0,0 @@ -/* - * 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 deleted file mode 100644 index 3cdfea9..0000000 --- a/_assistant_backups/2026-05-05/wifi_ota.cpp.bak_2026-05-05_0510 +++ /dev/null @@ -1,1368 +0,0 @@ -/* - * 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 deleted file mode 100644 index 8528322..0000000 --- a/_assistant_backups/2026-05-05/wifi_ota.cpp.bak_2026-05-05_0522 +++ /dev/null @@ -1,1368 +0,0 @@ -/* - * 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 deleted file mode 100644 index 715454a..0000000 --- a/_assistant_backups/2026-05-05/wifi_ota.cpp.bak_2026-05-05_0525 +++ /dev/null @@ -1,1368 +0,0 @@ -/* - * 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/main/include/app_config.h b/main/include/app_config.h index 8a59a28..7500375 100644 --- a/main/include/app_config.h +++ b/main/include/app_config.h @@ -10,7 +10,7 @@ // ============================================================ // 固件版本 // ============================================================ -inline constexpr const char *FW_VERSION = "0.1.4"; +inline constexpr const char *FW_VERSION = "0.2.0"; // ============================================================ // WiFi 工作模式选择 @@ -54,7 +54,8 @@ inline constexpr uint32_t HTTP_SERVER_STACK_SIZE = 8192; // OTA 配置 // ============================================================ inline constexpr size_t OTA_BLOCK_SIZE = 8192; -inline constexpr size_t OTA_MAX_FIRMWARE_SIZE = 2 * 1024 * 1024; // 2MB +inline constexpr size_t OTA_MAX_FIRMWARE_SIZE = 216 * 1024; // 216KB,对齐 GD32 APP_MAX_SIZE (0x36000) +inline constexpr size_t GD32_OTA_PACKET_SIZE = 48; // GD32 OTA 实际转发包载荷 // ============================================================ // 健康监控阈值 @@ -85,6 +86,12 @@ 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; +inline constexpr uint8_t BLE_OTA_CTRL_TRIGGER_GD32 = 0xF7; +inline constexpr uint8_t BLE_OTA_CTRL_TRIGGER_ESP32 = 0xF8; +inline constexpr uint8_t BLE_OTA_CMD_GD32_DATA = 0x00; +inline constexpr uint8_t BLE_OTA_CMD_ESP32_DATA = 0x01; +inline constexpr size_t BLE_OTA_CHUNK_PAYLOAD = 480; // 默认目标 payload;ATT Write Command 可写值≈(MTU-3),当前 512 MTU 下协议 payload 建议 ≤ 498B +inline constexpr size_t BLE_OTA_PACKET_OVERHEAD = 11; // cmd(1)+seq(2)+size(4)+crc(4) // ============================================================ // 安全常量(需与 GD32 固件团队确认) diff --git a/main/relay/ota_relay.cpp b/main/relay/ota_relay.cpp index ce7f0cc..b609e78 100644 --- a/main/relay/ota_relay.cpp +++ b/main/relay/ota_relay.cpp @@ -10,6 +10,7 @@ #include "esp_err.h" #include "esp_partition.h" #include "cJSON.h" +#include "aes/esp_aes.h" #include "ota_relay.h" #include "spi_master.h" @@ -23,6 +24,8 @@ 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; +static uint8_t g_nonce_server[16] = {0}; +static uint8_t g_k_session[16] = {0}; /** @brief OTA 命令分发队列,从 WiFi/BLE 任务接收指令。 */ QueueHandle_t g_ota_dispatch_queue = NULL; @@ -60,6 +63,93 @@ static ota_result_t relay_packets_from_flash(void); */ static ota_result_t finish_ota(void); +static int aes128_ecb_encrypt_block_local(const uint8_t *key, const uint8_t *in, uint8_t *out) +{ + esp_aes_context aes; + esp_aes_init(&aes); + int ret = esp_aes_setkey(&aes, key, 128); + if (ret == 0) { + ret = esp_aes_crypt_ecb(&aes, ESP_AES_ENCRYPT, in, out); + } + esp_aes_free(&aes); + return ret; +} + +static int aes128_ecb_decrypt_block_local(const uint8_t *key, const uint8_t *in, uint8_t *out) +{ + esp_aes_context aes; + esp_aes_init(&aes); + int ret = esp_aes_setkey(&aes, key, 128); + if (ret == 0) { + ret = esp_aes_crypt_ecb(&aes, ESP_AES_DECRYPT, in, out); + } + esp_aes_free(&aes); + return ret; +} + +static bool aes128_ecb_encrypt_48bytes_local(const uint8_t *key, const uint8_t *in, uint8_t *out) +{ + for (int i = 0; i < 3; ++i) { + if (aes128_ecb_encrypt_block_local(key, in + i * 16, out + i * 16) != 0) { + return false; + } + } + return true; +} + +static void calculate_crc32_with_key_local(const uint8_t *key, const uint8_t *data48, uint8_t *output) +{ + uint32_t crc = 0xFFFFFFFFU; + for (int i = 0; i < 16; ++i) { + crc ^= key[i]; + for (int j = 0; j < 8; ++j) { + crc = (crc >> 1) ^ (0xEDB88320U & (0U - (crc & 1U))); + } + } + for (size_t i = 0; i < GD32_OTA_PACKET_SIZE; ++i) { + crc ^= data48[i]; + for (int j = 0; j < 8; ++j) { + crc = (crc >> 1) ^ (0xEDB88320U & (0U - (crc & 1U))); + } + } + crc ^= 0xFFFFFFFFU; + output[0] = (uint8_t)(crc & 0xFFU); + output[1] = (uint8_t)((crc >> 8) & 0xFFU); + output[2] = (uint8_t)((crc >> 16) & 0xFFU); + output[3] = (uint8_t)((crc >> 24) & 0xFFU); +} + +static void log_resp_payload(const char *stage, uint8_t resp_type, const uint8_t *payload, size_t len) +{ + char hex[193] = {0}; + size_t n = (len > 32U) ? 32U : len; + for (size_t i = 0; i < n; ++i) { + snprintf(&hex[i * 2], sizeof(hex) - i * 2, "%02X", payload[i]); + } + DEBUG_INFO("OTA", "%s 响应: type=0x%02X len=%u payload[%u]=%s%s", + stage, resp_type, (unsigned)len, (unsigned)n, + n > 0 ? hex : "", + len > n ? "..." : ""); +} + +static esp_err_t wait_until_not_wait(const char *stage, uint8_t *resp_type, + uint8_t *resp_payload, size_t resp_max, + size_t *resp_len, uint32_t timeout_ms, uint32_t max_wait_rounds) +{ + if (!resp_type) return ESP_ERR_INVALID_ARG; + uint32_t round = 0; + while (*resp_type == SPI_OTA_RSP_WAIT && round < max_wait_rounds) { + ++round; + DEBUG_WARN("OTA", "%s 收到 WAIT,开始第 %u 次只读轮询...", stage, round); + esp_err_t err = spi_ota_poll_response(resp_type, resp_payload, resp_max, resp_len, timeout_ms); + if (err != ESP_OK) { + return err; + } + log_resp_payload(stage, *resp_type, resp_payload, resp_len ? *resp_len : 0); + } + return ESP_OK; +} + /** * @brief 从 Flash 加载元数据 * @param meta 输出参数,加载后的元数据结构体。 @@ -215,28 +305,29 @@ 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); + uint8_t resp_type = 0; + uint8_t resp_payload[64] = {0}; + size_t resp_len = 0; /** @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 中继状态机状态。 */ + esp_err_t err = spi_ota_transfer(SPI_OTA_CMD_REQ, payload, sizeof(payload), + &resp_type, resp_payload, sizeof(resp_payload), &resp_len, 3000); + log_resp_payload("HANDSHAKE_REQ", resp_type, resp_payload, resp_len); + if (err == ESP_OK) { + err = wait_until_not_wait("HANDSHAKE_REQ", &resp_type, resp_payload, sizeof(resp_payload), &resp_len, 1000, 8); + } + if (err != ESP_OK || resp_type != SPI_OTA_RSP_ACK || resp_len < 2 || + resp_payload[0] != 0x00 || resp_payload[1] != 0xAC) { + DEBUG_ERR("OTA", "HANDSHAKE_REQ 失败, err=%d, resp_type=0x%02X, resp_len=%zu", err, resp_type, resp_len); g_relay_state = RELAY_ERROR; return OTA_RESULT_ERR_TIMEOUT; } - DEBUG_INFO("OTA", "握手请求已发送,等待 GD32 响应..."); - vTaskDelay(pdMS_TO_TICKS(100)); - + DEBUG_INFO("OTA", "握手请求已确认"); return OTA_RESULT_OK; } @@ -246,66 +337,77 @@ static ota_result_t send_handshake_req(void) */ static ota_result_t send_nonce_and_authenticate(void) { - uint8_t tx_frame[64]; - uint8_t rx_frame[64]; - uint8_t nonce_server[16]; + uint8_t resp_type = 0; + uint8_t resp_payload[64] = {0}; + size_t resp_len = 0; uint32_t timestamp = xTaskGetTickCount(); - uint32_t mem_addr = (uint32_t)nonce_server; + uint32_t mem_addr = (uint32_t)(uintptr_t)g_nonce_server; uint32_t rand_seed = 0x12345678; - scramble_generate(timestamp, mem_addr, rand_seed, nonce_server); + scramble_generate(timestamp, mem_addr, rand_seed, g_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 中继状态机状态。 */ + esp_err_t err = spi_ota_transfer(SPI_OTA_CMD_REQ, g_nonce_server, 16, + &resp_type, resp_payload, sizeof(resp_payload), &resp_len, 5000); + log_resp_payload("CHALLENGE", resp_type, resp_payload, resp_len); + if (err == ESP_OK) { + err = wait_until_not_wait("CHALLENGE", &resp_type, resp_payload, sizeof(resp_payload), &resp_len, 1000, 12); + } + if (err != ESP_OK || resp_type != SPI_OTA_RSP_ACK || resp_len != 36) { + DEBUG_ERR("OTA", "CHALLENGE 发送失败, err=%d, resp_type=0x%02X, resp_len=%zu", err, resp_type, resp_len); 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); + if (aes128_ecb_encrypt_block_local(MASTER_KEY, g_nonce_server, g_k_session) != 0) { + DEBUG_ERR("OTA", "计算 K_session 失败"); + g_relay_state = RELAY_ERROR; + return OTA_RESULT_ERR_AUTH; + } - DEBUG_INFO("OTA", "--- 认证步骤 2: 发送 SESSION_KEY ---"); - DEBUG_INFO("OTA", "发送变换后的随机数 (16B)"); - /** @brief 当前 OTA 中继状态机状态。 */ + uint8_t plaintext[16]; + for (int i = 0; i < 16; ++i) { + plaintext[i] = g_nonce_server[i] ^ 0x55U; + } + uint8_t encrypted_confirm[16] = {0}; + if (aes128_ecb_encrypt_block_local(g_k_session, plaintext, encrypted_confirm) != 0) { + DEBUG_ERR("OTA", "加密确认块失败"); + g_relay_state = RELAY_ERROR; + return OTA_RESULT_ERR_AUTH; + } + + DEBUG_INFO("OTA", "--- 认证步骤 2: 发送 SESSION_KEY/CONFIRM ---"); 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 中继状态机状态。 */ + resp_len = 0; + err = spi_ota_transfer(SPI_OTA_CMD_REQ, encrypted_confirm, 16, + &resp_type, resp_payload, sizeof(resp_payload), &resp_len, 3000); + log_resp_payload("CONFIRM", resp_type, resp_payload, resp_len); + if (err == ESP_OK) { + err = wait_until_not_wait("CONFIRM", &resp_type, resp_payload, sizeof(resp_payload), &resp_len, 1000, 8); + } + if (err != ESP_OK || resp_type != SPI_OTA_RSP_ACK || resp_len < 2 || + resp_payload[0] != 0xBA || resp_payload[1] != 0xC0) { + DEBUG_ERR("OTA", "SESSION_KEY/CONFIRM 失败, err=%d, resp_type=0x%02X, resp_len=%zu", err, resp_type, resp_len); 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); - + session_key_init(&g_session_ctx, g_k_session); return OTA_RESULT_OK; } @@ -316,8 +418,9 @@ static ota_result_t send_nonce_and_authenticate(void) */ static ota_result_t send_firmware_size(uint32_t size) { - uint8_t tx_frame[32]; - uint8_t rx_frame[32]; + uint8_t resp_type = 0; + uint8_t resp_payload[32] = {0}; + size_t resp_len = 0; DEBUG_INFO("OTA", "=== 阶段2: 发送固件大小 ==="); DEBUG_INFO("OTA", "固件大小: %u 字节", size); @@ -328,22 +431,23 @@ static ota_result_t send_firmware_size(uint32_t size) 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 中继状态机状态。 */ + esp_err_t err = spi_ota_transfer(SPI_OTA_CMD_DATA, size_payload, sizeof(size_payload), + &resp_type, resp_payload, sizeof(resp_payload), &resp_len, 3000); + log_resp_payload("SEND_SIZE", resp_type, resp_payload, resp_len); + if (err == ESP_OK) { + err = wait_until_not_wait("SEND_SIZE", &resp_type, resp_payload, sizeof(resp_payload), &resp_len, 1500, 12); + } + if (err != ESP_OK || resp_type != SPI_OTA_RSP_ACK || resp_len < 2 || + resp_payload[0] != 0xBA || resp_payload[1] != 0xC0) { + DEBUG_ERR("OTA", "SEND_SIZE 失败, err=%d, resp_type=0x%02X, resp_len=%zu", err, resp_type, resp_len); 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"); @@ -364,15 +468,16 @@ static ota_result_t relay_packets_from_flash(void) return OTA_RESULT_ERR_STORAGE; } - uint32_t total_packets = (meta.total_size + 47) / 48; + uint32_t total_packets = (meta.total_size + GD32_OTA_PACKET_SIZE - 1U) / GD32_OTA_PACKET_SIZE; uint32_t flash_offset = 0; - uint8_t tx_buf[64]; - uint8_t rx_buf[32]; + uint8_t tx_buf[64] = {0}; + uint8_t resp_type = 0; + uint8_t resp_payload[32] = {0}; + size_t resp_len = 0; ota_result_t result = OTA_RESULT_OK; - DEBUG_INFO("OTA", "总包数: %u, 每包 48 字节", total_packets); + DEBUG_INFO("OTA", "总包数: %u, 每包 %u 字节", total_packets, (unsigned)GD32_OTA_PACKET_SIZE); DEBUG_INFO("OTA", "状态: RELAY_RELAY_ACTIVE"); - /** @brief 当前 OTA 中继状态机状态。 */ g_relay_state = RELAY_RELAY_ACTIVE; for (uint32_t seq = 0; seq < total_packets; seq++) { @@ -384,50 +489,76 @@ static ota_result_t relay_packets_from_flash(void) } } - size_t chunk_size = 48; + uint8_t plain48[GD32_OTA_PACKET_SIZE]; + memset(plain48, 0xFF, sizeof(plain48)); + size_t chunk_size = GD32_OTA_PACKET_SIZE; if (seq == total_packets - 1) { - chunk_size = meta.total_size - (seq * 48); + chunk_size = meta.total_size - (seq * GD32_OTA_PACKET_SIZE); } - esp_err_t err = flash_read_chunk(flash_offset, &tx_buf[2], chunk_size); + esp_err_t err = flash_read_chunk(flash_offset, plain48, 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); + tx_buf[0] = 0x00; tx_buf[1] = 0x00; tx_buf[2] = 0x00; tx_buf[3] = 0x00; /* session_num 预留 */ + tx_buf[4] = (uint8_t)(seq & 0xFFU); + tx_buf[5] = (uint8_t)((seq >> 8) & 0xFFU); + tx_buf[6] = 0x00; + tx_buf[7] = 0x00; - 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); + if (!aes128_ecb_encrypt_48bytes_local(g_k_session, plain48, &tx_buf[8])) { + DEBUG_ERR("OTA", "包 %u 加密失败", seq); + return OTA_RESULT_ERR_AUTH; + } + + uint32_t if_end = (seq == total_packets - 1) ? 0xFFFFFFFFU : 0x00000000U; + tx_buf[56] = (uint8_t)(if_end & 0xFFU); + tx_buf[57] = (uint8_t)((if_end >> 8) & 0xFFU); + tx_buf[58] = (uint8_t)((if_end >> 16) & 0xFFU); + tx_buf[59] = (uint8_t)((if_end >> 24) & 0xFFU); + calculate_crc32_with_key_local(g_k_session, plain48, &tx_buf[60]); + + DEBUG_INFO("OTA", "包 %u: chunk=%u if_end=0x%08X crc32=%02X%02X%02X%02X", + seq, (unsigned)chunk_size, (unsigned)if_end, + tx_buf[60], tx_buf[61], tx_buf[62], tx_buf[63]); - /** @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); + resp_len = 0; + err = spi_ota_transfer(SPI_OTA_CMD_DATA, tx_buf, sizeof(tx_buf), + &resp_type, resp_payload, sizeof(resp_payload), &resp_len, 3000); + log_resp_payload("DATA_PACKET", resp_type, resp_payload, resp_len); + if (err == ESP_OK) { + err = wait_until_not_wait("DATA_PACKET", &resp_type, resp_payload, sizeof(resp_payload), &resp_len, 1000, 10); + } + if (err != ESP_OK || resp_type != SPI_OTA_RSP_ACK || resp_len < 4) { + DEBUG_WARN("OTA", "数据包 %u 发送失败, err=%d, resp_type=0x%02X, resp_len=%zu", seq, err, resp_type, resp_len); result = OTA_RESULT_ERR_TIMEOUT; break; } - flash_offset += chunk_size; - /** @brief 当前 OTA 中继状态机状态。 */ + uint32_t ack_value = (uint32_t)resp_payload[0] | + ((uint32_t)resp_payload[1] << 8) | + ((uint32_t)resp_payload[2] << 16) | + ((uint32_t)resp_payload[3] << 24); + uint32_t expected_ack = (seq == total_packets - 1) ? 0xFFFFFFFFU : (seq + 1U); + if (ack_value != expected_ack) { + DEBUG_ERR("OTA", "包 %u ACK 不匹配: got=0x%08X expected=0x%08X", seq, ack_value, expected_ack); + result = OTA_RESULT_ERR_GD32; + break; + } + + flash_offset += (uint32_t)chunk_size; 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); + uint32_t progress = (seq * 100U) / 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; @@ -440,39 +571,18 @@ static ota_result_t relay_packets_from_flash(void) 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 跳转确认..."); + DEBUG_INFO("OTA", "GD32 当前实现已在最后一个 DATA 包后自行 ACK 并跳转,无需额外 FINISH 命令"); session_key_clear(&g_session_ctx); memset(&g_session_ctx, 0, sizeof(session_ctx_t)); + memset(g_k_session, 0, sizeof(g_k_session)); + memset(g_nonce_server, 0, sizeof(g_nonce_server)); - /** @brief 当前 OTA 中继状态机状态。 */ g_relay_state = RELAY_DONE; DEBUG_INFO("OTA", "状态: RELAY_DONE"); DEBUG_INFO("OTA", "=== OTA 完成 ==="); meta_clear(); - return OTA_RESULT_OK; } @@ -567,11 +677,20 @@ void ota_relay_task(void *pvParameters) /** @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 (!g_ota_dispatch_queue || !g_ota_result_queue) { + DEBUG_ERR("OTA", "OTA 队列创建失败"); + vTaskDelete(NULL); + return; + } - if (ret == pdTRUE) { - DEBUG_INFO("OTA", "收到 OTA 调度信号,开始转发..."); + while (1) { + uint32_t msg = 0; + BaseType_t ret = xQueueReceive(g_ota_dispatch_queue, &msg, portMAX_DELAY); + if (ret != pdTRUE) { + continue; + } + + DEBUG_INFO("OTA", "收到 OTA 调度信号: %lu,开始转发...", (unsigned long)msg); ota_result_t result = ota_relay_forward_firmware(); uint32_t result_msg = (uint32_t)result; @@ -581,9 +700,7 @@ void ota_relay_task(void *pvParameters) DEBUG_INFO("OTA", "OTA 转发成功完成"); } else { DEBUG_ERR("OTA", "OTA 转发失败,错误码: %d", result); + ota_relay_reset(); } } - - DEBUG_INFO("OTA", "OTA 中继任务结束"); - vTaskDelete(NULL); } \ No newline at end of file diff --git a/main/server/ble_ota.cpp b/main/server/ble_ota.cpp index 44b07c4..e6b26f3 100644 --- a/main/server/ble_ota.cpp +++ b/main/server/ble_ota.cpp @@ -2,14 +2,15 @@ * ble_ota.cpp - BLE OTA GATT 服务器实现 (ESP-IDF 6.0 NimBLE) * T5.1 - T5.5 * - * 通过 BLE GATT 接收上位机固件,存储到 Flash (gd32_ota 分区), - * 接收完成后触发 OTA 中继任务转发到 GD32。 + * 通过 BLE GATT 接收上位机固件,支持两种目标: + * - GD32: 存入 Flash → 触发 SPI 中继转发到 GD32 + * - ESP32: 直接写入 OTA 分区 → 触发重启升级自身 * * GATT 服务: * Service UUID: 0xFFFF - * - CTRL (0x0001, Write): 写入 0xF7 触发中继 - * - DATA_IN (0x0002, Write): 接收固件数据块 - * - DATA_OUT (0x0003, Notify): 推送 GD32 响应 + * - CTRL (0x0001, Write): 0xF7=触发GD32中继, 0xF8=触发ESP32 OTA升级 + * - DATA_IN (0x0002, Write): cmd=0x00=GD32数据, cmd=0x01=ESP32数据 + * - DATA_OUT (0x0003, Notify): 推送响应 * - VERSION (0x0004, Read): 固件版本 */ #include @@ -19,6 +20,9 @@ #include "freertos/semphr.h" #include "freertos/queue.h" #include "esp_err.h" +#include "esp_ota_ops.h" +#include "esp_system.h" +#include "esp_timer.h" // NimBLE 协议栈 #include "nimble/nimble_port.h" @@ -27,6 +31,7 @@ #include "host/ble_gap.h" #include "services/gap/ble_svc_gap.h" #include "services/gatt/ble_svc_gatt.h" +#include "host/ble_att.h" #include "app_config.h" #include "ble_ota.h" @@ -35,6 +40,25 @@ #include "monitor/debug_log.h" #define TAG "BLE_OTA" +#define BLE_OTA_NOTIFY_ACK 0xA1 +#define BLE_OTA_NOTIFY_FINISH 0xB1 +#define BLE_OTA_FINISH_BEGIN_OTA_END 0x01 +#define BLE_OTA_FINISH_DONE_OTA_END 0x02 +#define BLE_OTA_FINISH_BEGIN_SET_BOOT 0x03 +#define BLE_OTA_FINISH_DONE_SET_BOOT 0x04 +#define BLE_OTA_FINISH_BEGIN_DELAY 0x05 +#define BLE_OTA_FINISH_DONE_DELAY 0x06 +#define BLE_OTA_FINISH_REBOOT_NOW 0x07 +#define BLE_OTA_FINISH_ERR_OTA_END 0x81 +#define BLE_OTA_FINISH_ERR_SET_BOOT 0x82 +#define BLE_OTA_ACK_WINDOW 8 + +// ============================================================ +// 前向声明(GAP event handler 在广播函数之前定义,需要声明) +// ============================================================ +static void ble_ota_start_advertising(void); +static void ble_ota_send_window_ack(uint8_t cmd, uint16_t seq, uint32_t completed_bytes); +static void ble_ota_send_finish_event(uint8_t stage, uint32_t value); // ============================================================ // 静态状态 @@ -46,6 +70,97 @@ static bool s_ble_connected = false; // GATT 客户端已连接 static uint16_t s_conn_handle = BLE_HS_CONN_HANDLE_NONE; static uint16_t s_data_out_val_handle = 0; // DATA_OUT 值句柄 (供通知用) +// ESP32 自身 OTA 状态 +static bool s_esp32_ota_active = false; +static esp_ota_handle_t s_esp32_ota_handle = 0; +static const esp_partition_t *s_esp32_update_partition = NULL; +static size_t s_esp32_received = 0; +static bool s_esp32_reboot_pending = false; +static uint8_t s_esp32_last_cmd = 0; +static uint16_t s_esp32_last_seq = 0xFFFFU; + +static void ble_esp32_finish_and_reboot_task(void *pvParameters) +{ + (void)pvParameters; + DEBUG_INFO(TAG, "ESP32 OTA 异步收尾任务启动"); + + if (s_esp32_last_seq != 0xFFFFU) { + ble_ota_send_window_ack(s_esp32_last_cmd, s_esp32_last_seq, (uint32_t)s_esp32_received); + DEBUG_INFO(TAG, "ESP32 最终 ACK 已补发: seq=%u", s_esp32_last_seq); + } + vTaskDelay(pdMS_TO_TICKS(150)); + + if (!s_esp32_ota_active || s_esp32_update_partition == NULL) { + DEBUG_WARN(TAG, "异步收尾时 OTA 状态无效,放弃重启"); + s_esp32_reboot_pending = false; + vTaskDelete(NULL); + return; + } + + DEBUG_INFO(TAG, "ESP32 OTA 收尾: 共 %u 字节, 设置启动分区...", (unsigned int)s_esp32_received); + + int64_t t0_us = esp_timer_get_time(); + DEBUG_INFO(TAG, "[FINISH] 即将执行 esp_ota_end()"); + ble_ota_send_finish_event(BLE_OTA_FINISH_BEGIN_OTA_END, 0); + esp_err_t err = esp_ota_end(s_esp32_ota_handle); + int64_t t1_us = esp_timer_get_time(); + if (err != ESP_OK) { + DEBUG_ERR(TAG, "[FINISH] esp_ota_end 失败: %s (耗时=%lld ms)", + esp_err_to_name(err), (long long)((t1_us - t0_us) / 1000)); + ble_ota_send_finish_event(BLE_OTA_FINISH_ERR_OTA_END, (uint32_t)err); + esp_ota_abort(s_esp32_ota_handle); + s_esp32_ota_active = false; + s_esp32_ota_handle = 0; + s_esp32_update_partition = NULL; + s_esp32_received = 0; + s_esp32_reboot_pending = false; + vTaskDelete(NULL); + return; + } + DEBUG_INFO(TAG, "[FINISH] esp_ota_end 完成 (耗时=%lld ms)", + (long long)((t1_us - t0_us) / 1000)); + ble_ota_send_finish_event(BLE_OTA_FINISH_DONE_OTA_END, (uint32_t)((t1_us - t0_us) / 1000)); + + int64_t t2_us = esp_timer_get_time(); + DEBUG_INFO(TAG, "[FINISH] 即将执行 esp_ota_set_boot_partition()"); + ble_ota_send_finish_event(BLE_OTA_FINISH_BEGIN_SET_BOOT, 0); + err = esp_ota_set_boot_partition(s_esp32_update_partition); + int64_t t3_us = esp_timer_get_time(); + if (err != ESP_OK) { + DEBUG_ERR(TAG, "[FINISH] 设置启动分区失败: %s (耗时=%lld ms)", + esp_err_to_name(err), (long long)((t3_us - t2_us) / 1000)); + ble_ota_send_finish_event(BLE_OTA_FINISH_ERR_SET_BOOT, (uint32_t)err); + s_esp32_ota_active = false; + s_esp32_ota_handle = 0; + s_esp32_update_partition = NULL; + s_esp32_received = 0; + s_esp32_reboot_pending = false; + vTaskDelete(NULL); + return; + } + DEBUG_INFO(TAG, "[FINISH] esp_ota_set_boot_partition 完成 (耗时=%lld ms)", + (long long)((t3_us - t2_us) / 1000)); + ble_ota_send_finish_event(BLE_OTA_FINISH_DONE_SET_BOOT, (uint32_t)((t3_us - t2_us) / 1000)); + + s_esp32_ota_active = false; + s_esp32_ota_handle = 0; + s_esp32_update_partition = NULL; + s_esp32_received = 0; + + DEBUG_INFO(TAG, "ESP32 OTA 收尾完成,800ms 后重启..."); + int64_t t4_us = esp_timer_get_time(); + DEBUG_INFO(TAG, "[FINISH] 即将进入 800ms 重启前等待"); + ble_ota_send_finish_event(BLE_OTA_FINISH_BEGIN_DELAY, 800); + vTaskDelay(pdMS_TO_TICKS(800)); + int64_t t5_us = esp_timer_get_time(); + DEBUG_INFO(TAG, "[FINISH] 800ms 等待结束 (实际耗时=%lld ms)", + (long long)((t5_us - t4_us) / 1000)); + ble_ota_send_finish_event(BLE_OTA_FINISH_DONE_DELAY, (uint32_t)((t5_us - t4_us) / 1000)); + DEBUG_INFO(TAG, "ESP32 即将重启"); + ble_ota_send_finish_event(BLE_OTA_FINISH_REBOOT_NOW, 0); + esp_restart(); +} + // ============================================================ // NimBLE 工具: 从 mbuf 读取原始数据 /** @@ -64,6 +179,116 @@ static int ble_ota_mbuf_to_buf(struct os_mbuf *om, uint8_t *buf, size_t buf_len) return (int)copy_len; } +static esp_err_t ble_esp32_ota_begin(size_t total_size) +{ + const esp_partition_t *boot_partition = esp_ota_get_running_partition(); + DEBUG_INFO(TAG, "当前启动分区: %s, size=%u, address=0x%lx", + boot_partition->label, boot_partition->size, boot_partition->address); + + if (s_esp32_ota_active) { + DEBUG_WARN(TAG, "检测到未完成的 ESP32 OTA,会先中止旧会话"); + esp_ota_abort(s_esp32_ota_handle); + s_esp32_ota_active = false; + s_esp32_ota_handle = 0; + s_esp32_update_partition = NULL; + s_esp32_received = 0; + s_esp32_reboot_pending = false; + s_esp32_last_seq = 0xFFFFU; + } + + 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(TAG, "找不到可用的 OTA 分区"); + return ESP_FAIL; + } + + DEBUG_INFO(TAG, "ESP32 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(TAG, "ESP32 固件太大: %u > 分区大小: %u", + total_size, s_esp32_update_partition->size); + s_esp32_update_partition = NULL; + 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(TAG, "esp_ota_begin 失败: %s", esp_err_to_name(err)); + s_esp32_update_partition = NULL; + s_esp32_ota_handle = 0; + return err; + } + + s_esp32_received = 0; + s_esp32_ota_active = true; + s_esp32_reboot_pending = false; + s_esp32_last_cmd = 0; + s_esp32_last_seq = 0xFFFFU; + DEBUG_INFO(TAG, "ESP32 OTA 开始,目标分区: %s", s_esp32_update_partition->label); + return ESP_OK; +} + +static esp_err_t ble_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; +} + +static void ble_ota_send_window_ack(uint8_t cmd, uint16_t seq, uint32_t completed_bytes) +{ + uint8_t ack[8] = {0}; + ack[0] = BLE_OTA_NOTIFY_ACK; + ack[1] = cmd; + ack[2] = (uint8_t)(seq & 0xFFU); + ack[3] = (uint8_t)((seq >> 8) & 0xFFU); + ack[4] = (uint8_t)(completed_bytes & 0xFFU); + ack[5] = (uint8_t)((completed_bytes >> 8) & 0xFFU); + ack[6] = (uint8_t)((completed_bytes >> 16) & 0xFFU); + ack[7] = (uint8_t)((completed_bytes >> 24) & 0xFFU); + + esp_err_t err = ble_ota_notify_response(ack, sizeof(ack)); + if (err != ESP_OK) { + DEBUG_WARN(TAG, "发送 ACK 失败: cmd=0x%02X seq=%u err=%s", + cmd, seq, esp_err_to_name(err)); + } else { + DEBUG_DBG(TAG, "已发送 ACK: cmd=0x%02X seq=%u bytes=%lu", + cmd, seq, (unsigned long)completed_bytes); + } +} + +static void ble_ota_send_finish_event(uint8_t stage, uint32_t value) +{ + uint8_t pkt[6] = {0}; + pkt[0] = BLE_OTA_NOTIFY_FINISH; + pkt[1] = stage; + pkt[2] = (uint8_t)(value & 0xFFU); + pkt[3] = (uint8_t)((value >> 8) & 0xFFU); + pkt[4] = (uint8_t)((value >> 16) & 0xFFU); + pkt[5] = (uint8_t)((value >> 24) & 0xFFU); + esp_err_t err = ble_ota_notify_response(pkt, sizeof(pkt)); + if (err != ESP_OK) { + DEBUG_WARN(TAG, "发送 FINISH 事件失败: stage=0x%02X err=%s", + stage, esp_err_to_name(err)); + } +} + + // ============================================================ // GATT 访问回调 // ============================================================ @@ -92,20 +317,43 @@ static int ble_ota_ctrl_access(uint16_t conn_handle, uint16_t attr_handle, DEBUG_INFO(TAG, "CTRL 写入: cmd=0x%02X", buf[0]); - if (buf[0] == 0xF7) { - // 检查固件是否存储完毕 + if (buf[0] == BLE_OTA_CTRL_TRIGGER_GD32) { + // 检查固件是否存储完毕 → 触发 GD32 OTA 中继 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 " 字节),触发中继", + DEBUG_INFO(TAG, "GD32 固件就绪 (%" PRIu32 " / %" PRIu32 " 字节),触发中继", meta.stored_size, meta.total_size); ble_ota_signal_relay(); } else { - DEBUG_WARN(TAG, "固件未就绪, 未触发中继 (stored=%" PRIu32 ", total=%" PRIu32 ")", + DEBUG_WARN(TAG, "GD32 固件未就绪 (stored=%" PRIu32 ", total=%" PRIu32 ")", meta.stored_size, meta.total_size); } + } else if (buf[0] == BLE_OTA_CTRL_TRIGGER_ESP32) { + if (!s_esp32_ota_active || s_esp32_update_partition == NULL) { + DEBUG_WARN(TAG, "ESP32 OTA 未激活,无法触发升级"); + return 0; + } + if (s_esp32_reboot_pending) { + DEBUG_WARN(TAG, "ESP32 OTA 收尾已在进行中,忽略重复 CTRL"); + return 0; + } + + s_esp32_reboot_pending = true; + BaseType_t ok = xTaskCreate(ble_esp32_finish_and_reboot_task, + "ble_ota_finish", + 4096, + NULL, + 5, + NULL); + if (ok != pdPASS) { + DEBUG_ERR(TAG, "创建 ESP32 OTA 收尾任务失败"); + s_esp32_reboot_pending = false; + } else { + DEBUG_INFO(TAG, "ESP32 OTA 收尾任务已创建(由设备端异步完成)"); + } } return 0; // BLE_ATT_ERR_OK 在 NimBLE 中就是 0 @@ -158,7 +406,78 @@ static int ble_ota_data_in_access(uint16_t conn_handle, uint16_t attr_handle, DEBUG_DBG(TAG, "DATA_IN: cmd=0x%02X seq=%u total=%" PRIu32 " payload=%zu", cmd, seq, total_size, data_len); - // --- 首个分块: 创建存储元数据 --- + // --- ESP32 自身 OTA:直接写 OTA 分区 --- + if (cmd == BLE_OTA_CMD_ESP32_DATA) { + if (seq == 0) { + if (total_size == 0) { + DEBUG_ERR(TAG, "ESP32 固件大小非法: %" PRIu32, total_size); + return BLE_ATT_ERR_UNLIKELY; + } + const esp_partition_t *boot_partition = esp_ota_get_running_partition(); + const esp_partition_t *check_partition = esp_partition_find_first( + ESP_PARTITION_TYPE_APP, ESP_PARTITION_SUBTYPE_APP_OTA_0, NULL); + if (check_partition == NULL || check_partition->address == boot_partition->address) { + check_partition = esp_partition_find_first( + ESP_PARTITION_TYPE_APP, ESP_PARTITION_SUBTYPE_APP_OTA_1, NULL); + } + if (check_partition == NULL) { + DEBUG_ERR(TAG, "找不到 ESP32 OTA 目标分区"); + return BLE_ATT_ERR_UNLIKELY; + } + if (total_size > check_partition->size) { + DEBUG_ERR(TAG, "ESP32 固件太大: %" PRIu32 " > OTA分区 %u", + total_size, check_partition->size); + return BLE_ATT_ERR_UNLIKELY; + } + esp_err_t err = ble_esp32_ota_begin(total_size); + if (err != ESP_OK) { + return BLE_ATT_ERR_UNLIKELY; + } + } + + esp_err_t err = ble_esp32_ota_write(&buf[data_offset], data_len); + if (err != ESP_OK) { + DEBUG_ERR(TAG, "ESP32 OTA 写入失败: seq=%u err=%s", + seq, esp_err_to_name(err)); + return BLE_ATT_ERR_UNLIKELY; + } + + DEBUG_DBG(TAG, "ESP32 OTA 写入成功: seq=%u received=%u", + seq, (unsigned int)s_esp32_received); + + if ((((uint32_t)seq + 1U) % BLE_OTA_ACK_WINDOW) == 0U || + (total_size > 0 && s_esp32_received >= total_size)) { + ble_ota_send_window_ack(cmd, seq, (uint32_t)s_esp32_received); + } + + if (total_size > 0 && s_esp32_received >= total_size) { + DEBUG_INFO(TAG, "★ ESP32 固件接收完毕: %u / %" PRIu32 " 字节 (共 %u 块)", + (unsigned int)s_esp32_received, total_size, seq + 1); + s_esp32_last_cmd = cmd; + s_esp32_last_seq = seq; + + if (!s_esp32_reboot_pending) { + s_esp32_reboot_pending = true; + BaseType_t ok = xTaskCreate(ble_esp32_finish_and_reboot_task, + "ble_ota_finish", + 4096, + NULL, + 5, + NULL); + if (ok != pdPASS) { + DEBUG_ERR(TAG, "创建 ESP32 OTA 收尾任务失败"); + s_esp32_reboot_pending = false; + return BLE_ATT_ERR_UNLIKELY; + } + DEBUG_INFO(TAG, "ESP32 OTA 收尾任务已创建,等待设备端自动重启"); + } else { + DEBUG_WARN(TAG, "ESP32 OTA 收尾任务已存在,忽略重复完成触发"); + } + } + return 0; + } + + // --- GD32 OTA:首个分块时创建存储元数据 --- if (seq == 0 && total_size > 0) { if (total_size > OTA_MAX_FIRMWARE_SIZE) { DEBUG_ERR(TAG, "固件太大: %" PRIu32 " > %zu", @@ -170,7 +489,7 @@ static int ble_ota_data_in_access(uint16_t conn_handle, uint16_t attr_handle, 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; + meta.chunk_count = (total_size + (uint32_t)data_len - 1U) / (uint32_t)data_len; esp_err_t err = meta_save(&meta); if (err != ESP_OK) { @@ -178,12 +497,31 @@ static int ble_ota_data_in_access(uint16_t conn_handle, uint16_t attr_handle, return BLE_ATT_ERR_UNLIKELY; } - DEBUG_INFO(TAG, "固件存储开始: total=%" PRIu32 " chunk_count=%" PRIu32, - meta.total_size, meta.chunk_count); + DEBUG_INFO(TAG, "GD32 固件存储开始: total=%" PRIu32 " chunk_count=%" PRIu32 ", payload=%zu", + meta.total_size, meta.chunk_count, data_len); + } + + ota_meta_t meta = {}; + if (meta_load(&meta) != ESP_OK || meta.total_size == 0) { + DEBUG_ERR(TAG, "GD32 元数据无效,拒绝写入: seq=%u", seq); + return BLE_ATT_ERR_UNLIKELY; + } + + if (seq > 0 && meta.last_seq != (uint32_t)(seq - 1)) { + DEBUG_ERR(TAG, "GD32 分包序号不连续: seq=%u last_seq=%" PRIu32, + seq, meta.last_seq); + return BLE_ATT_ERR_UNLIKELY; + } + + // --- GD32 写入 Flash --- + // 按已存储字节数顺序追加,兼容更大的 BLE payload,避免把步长硬编码死。 + uint32_t flash_offset = meta.stored_size; + if (flash_offset + (uint32_t)data_len > meta.total_size) { + DEBUG_ERR(TAG, "GD32 写入越界: offset=%" PRIu32 " len=%zu total=%" PRIu32, + flash_offset, data_len, meta.total_size); + return BLE_ATT_ERR_UNLIKELY; } - // --- 写入 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", @@ -191,17 +529,25 @@ static int ble_ota_data_in_access(uint16_t conn_handle, uint16_t attr_handle, return BLE_ATT_ERR_UNLIKELY; } - err = meta_update(flash_offset + data_len, seq); + uint32_t stored_size = flash_offset + (uint32_t)data_len; + err = meta_update(stored_size, seq); if (err != ESP_OK) { DEBUG_WARN(TAG, "元数据更新失败 (seq=%u)", seq); } - // --- 检查是否接收完毕 --- - ota_meta_t meta; + if ((((uint32_t)seq + 1U) % BLE_OTA_ACK_WINDOW) == 0U || + (meta.total_size > 0 && stored_size >= meta.total_size)) { + ble_ota_send_window_ack(cmd, seq, stored_size); + } + + DEBUG_DBG(TAG, "Flash 写入成功: seq=%u offset=%" PRIu32 " stored=%" PRIu32, + seq, flash_offset, stored_size); + + // --- 检查 GD32 固件是否接收完毕 --- if (meta_load(&meta) == ESP_OK && meta.total_size > 0 && meta.stored_size >= meta.total_size) { - DEBUG_INFO(TAG, "★ 固件接收完毕: %" PRIu32 " / %" PRIu32 " 字节 (共 %u 块)", + DEBUG_INFO(TAG, "★ GD32 固件接收完毕: %" PRIu32 " / %" PRIu32 " 字节 (共 %u 块)", meta.stored_size, meta.total_size, seq + 1); } @@ -223,7 +569,7 @@ static int ble_ota_version_access(uint16_t conn_handle, uint16_t attr_handle, return BLE_ATT_ERR_REQ_NOT_SUPPORTED; } - const char *version = "1.0.0"; + const char *version = FW_VERSION; int rc = os_mbuf_append(ctxt->om, version, strlen(version)); return (rc == 0) ? 0 : BLE_ATT_ERR_UNLIKELY; } @@ -326,6 +672,22 @@ static int ble_ota_gap_event(struct ble_gap_event *event, void *arg) /** @brief 当前 BLE 连接句柄。 */ s_conn_handle = event->connect.conn_handle; DEBUG_INFO(TAG, "BLE 客户端已连接: handle=%d", s_conn_handle); + + // 向对端请求更快的连接参数,提升 OTA 吞吐 + struct ble_gap_upd_params up = {}; + up.itvl_min = 6; // 7.5 ms 最小连接间隔 + up.itvl_max = 12; // 15 ms 最大连接间隔 + up.latency = 0; // 无延迟 + up.supervision_timeout = 2000; // 2 秒监控超时 + up.min_ce_len = 0; // 控制器自行决定 + up.max_ce_len = 0; + int rc = ble_gap_update_params(s_conn_handle, &up); + if (rc != 0) { + DEBUG_WARN(TAG, "请求更新连接参数失败: rc=%d(对端不一定接受,继续运行)", rc); + } else { + DEBUG_INFO(TAG, "已请求更快的连接参数: itvl=%.1f-%.1fms latency=0", + up.itvl_min * 1.25f, up.itvl_max * 1.25f); + } } else { DEBUG_WARN(TAG, "BLE 连接失败: status=%d", event->connect.status); /** @brief BLE GATT 客户端连接状态。 */ @@ -341,14 +703,12 @@ static int ble_ota_gap_event(struct ble_gap_event *event, void *arg) /** @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); + ble_ota_start_advertising(); 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); + ble_ota_start_advertising(); return 0; case BLE_GAP_EVENT_SUBSCRIBE: @@ -377,6 +737,26 @@ static int ble_ota_gap_event(struct ble_gap_event *event, void *arg) */ static void ble_ota_start_advertising(void) { + int rc; + + // 自动推断可用地址类型(优先 Public,不可用则生成 Random) + uint8_t own_addr_type; + rc = ble_hs_id_infer_auto(0, &own_addr_type); + if (rc != 0) { + DEBUG_ERR(TAG, "推断地址类型失败: rc=%d, 尝试生成随机地址", rc); + // 生成不可解析随机地址作为 fallback + ble_addr_t rnd_addr; + rc = ble_hs_id_gen_rnd(0, &rnd_addr); + if (rc != 0) { + DEBUG_ERR(TAG, "生成随机地址也失败: rc=%d, BLE 广播无法启动", rc); + return; + } + own_addr_type = rnd_addr.type; + } + + DEBUG_INFO(TAG, "使用地址类型: %s", own_addr_type == BLE_OWN_ADDR_PUBLIC ? "Public" : "Random"); + + // --- 构建广播数据 --- uint8_t adv_data[31] = {0}; int adv_pos = 0; @@ -396,7 +776,7 @@ static void ble_ota_start_advertising(void) memcpy(&adv_data[adv_pos], name, name_len); adv_pos += name_len; - int rc = ble_gap_adv_set_data(adv_data, adv_pos); + rc = ble_gap_adv_set_data(adv_data, adv_pos); if (rc != 0) { DEBUG_ERR(TAG, "设置广播数据失败: %d", rc); return; @@ -423,12 +803,7 @@ static void ble_ota_start_advertising(void) 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 + // own_addr_type 已在上面由 ble_hs_id_infer_auto 推断 rc = ble_gap_adv_start(own_addr_type, NULL, BLE_HS_FOREVER, &adv_params, ble_ota_gap_event, NULL); @@ -514,6 +889,13 @@ esp_err_t ble_ota_server_start(void) DEBUG_WARN(TAG, "设置设备名称失败: %d", rc); } + rc = ble_att_set_preferred_mtu(BLE_MTU_SIZE); + if (rc != 0) { + DEBUG_WARN(TAG, "设置首选 MTU 失败: %d", rc); + } else { + DEBUG_INFO(TAG, "首选 ATT MTU 已设置: %u", (unsigned)BLE_MTU_SIZE); + } + // 5. 配置 Host 回调 // sync_cb: NimBLE 事件循环就绪时调用 (开始广播) // reset_cb: NimBLE 重置时调用 diff --git a/main/server/wifi_ota.cpp b/main/server/wifi_ota.cpp index 005275b..6343a43 100644 --- a/main/server/wifi_ota.cpp +++ b/main/server/wifi_ota.cpp @@ -629,7 +629,34 @@ static esp_err_t ota_upload_handler(httpd_req_t *req) return ESP_FAIL; } + if (g_relay_state != RELAY_IDLE) { + DEBUG_WARN("WIFI", "GD32 OTA 开始前复位中继状态: %d", g_relay_state); + ota_relay_reset(); + } + s_gd32_storage_done = false; + s_relay_triggered = false; + + const esp_partition_t *gd32_partition = esp_partition_find_first( + ESP_PARTITION_TYPE_DATA, ESP_PARTITION_SUBTYPE_DATA_SPIFFS, "gd32_ota"); + if (!gd32_partition) { + DEBUG_ERR("WIFI", "找不到 gd32_ota 分区"); + httpd_resp_send_err(req, HTTPD_500_INTERNAL_SERVER_ERROR, "找不到 gd32_ota 分区"); + return ESP_FAIL; + } + esp_err_t erase_err = esp_partition_erase_range(gd32_partition, 0, gd32_partition->size); + if (erase_err != ESP_OK) { + DEBUG_ERR("WIFI", "擦除 gd32_ota 分区失败: %s", esp_err_to_name(erase_err)); + httpd_resp_send_err(req, HTTPD_500_INTERNAL_SERVER_ERROR, "擦除分区失败"); + return ESP_FAIL; + } + meta_clear(); + uint8_t *buf = (uint8_t *)malloc(OTA_BLOCK_SIZE); + if (!buf) { + httpd_resp_send_err(req, HTTPD_500_INTERNAL_SERVER_ERROR, "内存分配失败"); + return ESP_FAIL; + } + int received = 0; // peek 字节是 GD32 数据的第一字节,先放回缓冲区 @@ -643,10 +670,17 @@ static esp_err_t ota_upload_handler(httpd_req_t *req) if (got > 0) header_read += got; } - // 先处理已读的 header + uint32_t expected_fw_size = 0; 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); + expected_fw_size = fw_size; + if (fw_size > OTA_MAX_FIRMWARE_SIZE) { + DEBUG_ERR("WIFI", "GD32 固件头声明大小超限: %u > %u", fw_size, OTA_MAX_FIRMWARE_SIZE); + free(buf); + httpd_resp_send_err(req, HTTPD_400_BAD_REQUEST, "GD32 固件大小超限"); + return ESP_FAIL; + } 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} }; @@ -655,7 +689,7 @@ static esp_err_t ota_upload_handler(httpd_req_t *req) } } - // 继续读剩余数据 + // 首包已经读走 6 字节头,因此后续 body 全部按 header_read 为偏移基准顺序落盘 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; @@ -663,30 +697,39 @@ static esp_err_t ota_upload_handler(httpd_req_t *req) 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; + uint32_t flash_offset = (uint32_t)received; 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); + if (err != ESP_OK) { + DEBUG_ERR("WIFI", "Flash 写入失败: offset=%u len=%d err=%s", + flash_offset, ret, esp_err_to_name(err)); + free(buf); + httpd_resp_send_err(req, HTTPD_500_INTERNAL_SERVER_ERROR, "写入 gd32_ota 失败"); + return ESP_FAIL; } - received += ret; - uint32_t progress = ((received + header_read) * 100) / req->content_len; + received += ret; + if (expected_fw_size > 0) { + meta_update((uint32_t)received, (uint32_t)(received / OTA_BLOCK_SIZE)); + } + + uint32_t progress = expected_fw_size > 0 + ? ((uint32_t)received * 100U) / expected_fw_size + : ((uint32_t)(received + header_read) * 100U) / (uint32_t)req->content_len; if (progress % 10 == 0) { - DEBUG_INFO("WIFI", "GD32 上传进度: %d / %d (%u%%)", - received + header_read, req->content_len, progress); + DEBUG_INFO("WIFI", "GD32 上传进度: %d / %u (%u%%)", + received, expected_fw_size, progress); } } free(buf); - DEBUG_INFO("WIFI", "GD32 固件存储完成: %d 字节", received + header_read); + DEBUG_INFO("WIFI", "GD32 固件存储完成: payload=%d 字节, 头=%d 字节", received, header_read); ota_meta_t meta; meta_load(&meta); - if (meta.stored_size >= meta.total_size) { + if (meta.total_size > 0 && meta.stored_size >= meta.total_size) { /** @brief GD32 固件存储完成标志。 */ s_gd32_storage_done = true; - DEBUG_INFO("WIFI", "GD32 固件已存储,触发转发..."); - wifi_ota_signal_relay(); + DEBUG_INFO("WIFI", "GD32 固件已存储完成,等待触发转发..."); } const char resp[] = "{\"status\": \"ok\", \"progress\": 100, \"target\": \"gd32\"}"; @@ -1202,6 +1245,14 @@ static esp_err_t http_ota_server_start(void) */ void wifi_ota_signal_relay(void) { + ota_meta_t meta; + memset(&meta, 0, sizeof(meta)); + if (meta_load(&meta) != ESP_OK || meta.total_size == 0 || meta.stored_size < meta.total_size) { + DEBUG_WARN("WIFI", "GD32 固件未完整存储,拒绝触发转发: stored=%lu total=%lu", + (unsigned long)meta.stored_size, (unsigned long)meta.total_size); + return; + } + if (s_relay_triggered) { DEBUG_WARN("WIFI", "转发已经触发过"); return; @@ -1214,6 +1265,9 @@ void wifi_ota_signal_relay(void) uint32_t msg = 1; xQueueSend(g_ota_dispatch_queue, &msg, pdMS_TO_TICKS(100)); DEBUG_INFO("WIFI", "OTA 调度信号已发送到中继任务"); + } else { + DEBUG_ERR("WIFI", "OTA 中继队列尚未就绪"); + s_relay_triggered = false; } } diff --git a/main/transport/spi_master.cpp b/main/transport/spi_master.cpp index 946c818..15b1bbb 100644 --- a/main/transport/spi_master.cpp +++ b/main/transport/spi_master.cpp @@ -15,6 +15,63 @@ #include "spi_master.h" #include "monitor/debug_log.h" +static uint8_t spi_ota_crc8(const uint8_t *data, size_t len) +{ + uint8_t crc = 0x00; + for (size_t i = 0; i < len; ++i) { + crc ^= data[i]; + } + return crc; +} + +static bool spi_ota_build_frame(uint8_t type, const uint8_t *payload, size_t payload_len, + uint8_t *out, size_t *out_len) +{ + if (!out || !out_len) return false; + size_t total = 5 + payload_len; + if (total > SPI_OTA_FRAME_MAX) return false; + out[0] = SPI_OTA_SOF; + out[1] = type; + out[2] = (uint8_t)(payload_len & 0xFF); + out[3] = (uint8_t)((payload_len >> 8) & 0xFF); + if (payload && payload_len > 0) { + memcpy(&out[4], payload, payload_len); + } + out[4 + payload_len] = spi_ota_crc8(out, 4 + payload_len); + *out_len = total; + return true; +} + +static bool spi_ota_parse_frame(const uint8_t *frame, size_t frame_len, + uint8_t *type, const uint8_t **payload, size_t *payload_len) +{ + if (!frame || frame_len < 5 || frame[0] != SPI_OTA_SOF) return false; + size_t len = (size_t)frame[2] | ((size_t)frame[3] << 8); + size_t expected = 5 + len; + if (expected > frame_len) return false; + uint8_t crc = spi_ota_crc8(frame, 4 + len); + if (crc != frame[4 + len]) return false; + if (type) *type = frame[1]; + if (payload) *payload = &frame[4]; + if (payload_len) *payload_len = len; + return true; +} + +static bool spi_ota_find_frame(const uint8_t *buf, size_t buf_len, + uint8_t *type, const uint8_t **payload, size_t *payload_len, + size_t *frame_offset) +{ + if (!buf || buf_len < 5) return false; + for (size_t i = 0; i + 5 <= buf_len; ++i) { + if (buf[i] != SPI_OTA_SOF) continue; + if (spi_ota_parse_frame(&buf[i], buf_len - i, type, payload, payload_len)) { + if (frame_offset) *frame_offset = i; + return true; + } + } + return false; +} + /** @brief SPI2 从设备句柄。 */ spi_device_handle_t g_spi_device = NULL; @@ -132,6 +189,100 @@ esp_err_t spi_recv(uint8_t cmd, uint8_t *recv_buf, size_t len, uint32_t timeout_ return spi_sync_transfer(cmd, dummy, len, recv_buf, len, timeout_ms); } +esp_err_t spi_ota_poll_response(uint8_t *resp_type, uint8_t *resp_payload, + size_t resp_max, size_t *resp_len, uint32_t timeout_ms) +{ + if (g_spi_device == NULL) { + return ESP_ERR_INVALID_STATE; + } + if (resp_len) *resp_len = 0; + if (resp_type) *resp_type = 0; + + uint8_t rx_raw[SPI_OTA_FRAME_MAX] = {0}; + uint8_t dummy[SPI_OTA_FRAME_MAX]; + memset(dummy, 0xFF, sizeof(dummy)); + + TickType_t deadline = xTaskGetTickCount() + pdMS_TO_TICKS(timeout_ms); + uint32_t attempt = 0; + while (xTaskGetTickCount() <= deadline) { + ++attempt; + memset(rx_raw, 0xFF, sizeof(rx_raw)); + spi_transaction_t rsp = {}; + rsp.length = SPI_OTA_FRAME_MAX * 8; + rsp.rxlength = SPI_OTA_FRAME_MAX * 8; + rsp.tx_buffer = dummy; + rsp.rx_buffer = rx_raw; + esp_err_t ret = spi_device_transmit(g_spi_device, &rsp); + if (ret != ESP_OK) { + DEBUG_WARN("SPI", "SPI OTA 响应读取失败[%u]: %s", attempt, esp_err_to_name(ret)); + vTaskDelay(pdMS_TO_TICKS(10)); + continue; + } + + uint8_t parsed_type = 0; + const uint8_t *parsed_payload = nullptr; + size_t parsed_len = 0; + size_t frame_offset = 0; + if (spi_ota_find_frame(rx_raw, sizeof(rx_raw), &parsed_type, &parsed_payload, &parsed_len, &frame_offset)) { + DEBUG_INFO("SPI", "OTA响应: type=0x%02X len=%u offset=%u attempt=%u", + parsed_type, (unsigned)parsed_len, (unsigned)frame_offset, (unsigned)attempt); + if (resp_type) *resp_type = parsed_type; + if (resp_payload && resp_max > 0) { + size_t copy_len = (parsed_len < resp_max) ? parsed_len : resp_max; + memcpy(resp_payload, parsed_payload, copy_len); + if (resp_len) *resp_len = copy_len; + } else if (resp_len) { + *resp_len = parsed_len; + } + return ESP_OK; + } + + bool all_ff = true; + for (size_t i = 0; i < sizeof(rx_raw); ++i) { + if (rx_raw[i] != 0xFF) { + all_ff = false; + break; + } + } + DEBUG_DBG("SPI", "OTA响应未就绪[%u]: %s", attempt, all_ff ? "all-FF" : "no-valid-SOF"); + vTaskDelay(pdMS_TO_TICKS(10)); + } + + DEBUG_ERR("SPI", "OTA响应超时: timeout=%ums", (unsigned)timeout_ms); + return ESP_ERR_TIMEOUT; +} + +esp_err_t spi_ota_transfer(uint8_t type, const uint8_t *payload, size_t payload_len, + uint8_t *resp_type, uint8_t *resp_payload, + size_t resp_max, size_t *resp_len, uint32_t timeout_ms) +{ + if (g_spi_device == NULL) { + return ESP_ERR_INVALID_STATE; + } + if (resp_len) *resp_len = 0; + if (resp_type) *resp_type = 0; + + uint8_t tx_frame[SPI_OTA_FRAME_MAX] = {0}; + size_t tx_len = 0; + if (!spi_ota_build_frame(type, payload, payload_len, tx_frame, &tx_len)) { + return ESP_ERR_INVALID_ARG; + } + + DEBUG_INFO("SPI", "OTA事务: req_type=0x%02X req_len=%u", type, (unsigned)payload_len); + + spi_transaction_t req = {}; + req.length = tx_len * 8; + req.tx_buffer = tx_frame; + esp_err_t ret = spi_device_transmit(g_spi_device, &req); + if (ret != ESP_OK) { + DEBUG_ERR("SPI", "SPI OTA 请求事务失败: %s", esp_err_to_name(ret)); + return ret; + } + + vTaskDelay(pdMS_TO_TICKS(2)); + return spi_ota_poll_response(resp_type, resp_payload, resp_max, resp_len, timeout_ms); +} + // CANFD 帧发送 // 帧格式: [通道(1B)] [扩展帧ID(4B BE)] [数据(1~64B)] static int can_frame_send(can_channel_t channel, uint32_t ext_id, diff --git a/main/transport/spi_master.h b/main/transport/spi_master.h index 20f4c93..9db61b7 100644 --- a/main/transport/spi_master.h +++ b/main/transport/spi_master.h @@ -10,9 +10,19 @@ // SPI 配置常量 #define SPI_MASTER_CS_GPIO (4) // CS 片选引脚 -#define SPI_CLOCK_HZ (40 * 1000 * 1000) // 40MHz +#define SPI_CLOCK_HZ (4 * 1000 * 1000) // 先降到 4MHz 便于联调 #define SPI_MODE (0) // CPOL=0, CPHA=0 #define SPI_QUEUE_SIZE (1) +#define SPI_OTA_FRAME_MAX (96) + +// GD32 SPI OTA 帧类型 +#define SPI_OTA_SOF (0xA5) +#define SPI_OTA_CMD_REQ (0x01) +#define SPI_OTA_CMD_DATA (0x02) +#define SPI_OTA_CMD_CANCEL (0x03) +#define SPI_OTA_RSP_ACK (0x80) +#define SPI_OTA_RSP_WAIT (0x81) +#define SPI_OTA_RSP_ERROR (0x82) // SPI 设备句柄(外部引用) extern spi_device_handle_t g_spi_device; @@ -108,4 +118,25 @@ esp_err_t spi_send(uint8_t cmd, const uint8_t *payload, size_t len); */ esp_err_t spi_recv(uint8_t cmd, uint8_t *recv_buf, size_t len, uint32_t timeout_ms); +/** + * @brief 按 GD32 SPI OTA 最小协议执行一次请求事务 + 一次响应读取事务。 + * @param type 请求帧类型(REQ/DATA/CANCEL)。 + * @param payload 请求负载。 + * @param payload_len 负载长度。 + * @param resp_type 输出响应类型(ACK/WAIT/ERROR)。 + * @param resp_payload 输出响应负载缓冲区。 + * @param resp_max 输出响应负载最大长度。 + * @param resp_len 输出实际响应负载长度。 + * @param timeout_ms 超时。 + */ +esp_err_t spi_ota_transfer(uint8_t type, const uint8_t *payload, size_t payload_len, + uint8_t *resp_type, uint8_t *resp_payload, + size_t resp_max, size_t *resp_len, uint32_t timeout_ms); + +/** + * @brief 仅执行 SPI OTA 响应读取事务,不重复发送请求帧。 + */ +esp_err_t spi_ota_poll_response(uint8_t *resp_type, uint8_t *resp_payload, + size_t resp_max, size_t *resp_len, uint32_t timeout_ms); + #endif // SPI_MASTER_H \ No newline at end of file diff --git a/tools/ble_ota.html b/tools/ble_ota.html new file mode 100644 index 0000000..2e0835b --- /dev/null +++ b/tools/ble_ota.html @@ -0,0 +1,831 @@ + + + + + +ESP32 BLE OTA 刷写台 + + + +
+
+
+
BLE • OTA • Dual Target
+

ESP32 Web Bluetooth 刷写台

+

这个页面专门走 BLE GATT,不依赖 WiFi。它能扫描并连接 ESP32-OTA-RELAY,读取版本,订阅通知,把固件按协议分包写入 DATA_IN,并在完成后自动向 CTRL 写入对应命令:GD32 = 0xF7,ESP32 = 0xF8。

+
+ + + + + 回 WiFi 页面 +
+
+
GD32 / ESP32双目标 BLE 刷写
+
480B默认 Payload
+
一键刷写写入完成后自动触发
+
+
+
+
连接状态
未连接
offline
+
设备名称
-
-
+
GATT/MTU 提示
未就绪
浏览器决定
+
版本
未读取
notify off
+
+
+ +
+
+

刷写控制台

+
+ + +
+
+
+ + +
+
+ + +
+
+ + +
+
+
+ 当前目标:GD32。页面会使用 cmd=0x00 发送数据,最后自动写 CTRL=0xF7。协议格式:cmd(1) + seq(2) + total_size(4) + payload + crc占位4。 +
+
+
+
+ 等待选择文件 + 0% +
+
+
+ + + + +
+ +
+ +
+

文件与进度

+
+
文件名
-
+
文件大小
0 B
+
总包数
0
+
已写包
0
+
+
+ +
暂无通知
+
+
+
+ +
+

运行日志

+
+
+
+ + + + diff --git a/tools/index.html b/tools/index.html index 077d607..869fa49 100644 --- a/tools/index.html +++ b/tools/index.html @@ -61,6 +61,9 @@ + + 📶 打开 BLE 刷写页面 + diff --git a/tools/serve.bat b/tools/serve.bat new file mode 100644 index 0000000..7447dba --- /dev/null +++ b/tools/serve.bat @@ -0,0 +1,21 @@ +@echo off +:: ESP32 COMM Host Server Launcher (Windows) +:: Double-click to start, then visit http://localhost:8080 + +set PORT=8080 + +echo ======================================== +echo ESP32 COMM Host Server +echo PORT: %PORT% +echo ======================================== +echo. +echo WiFi OTA: http://localhost:%PORT%/index.html +echo BLE OTA: http://localhost:%PORT%/ble_ota.html +echo. +echo Press Ctrl+C to stop +echo ======================================== +echo. + +cd /d "%~dp0" +python -m http.server %PORT% +pause