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