feat(wifi_ota): 添加CAN帧转发功能

添加了HTTP接口用于转发CAN帧,包括/can/send POST接口用于发送CAN帧,
以及/can/status GET接口用于查询CAN通道状态。同时集成了SPI master驱动
以支持与GD32之间的CANFD通信,并定义了完整的CAN帧格式和通道管理机制。

BREAKING CHANGE: 移除了OTA上位机工具脚本
This commit is contained in:
小龙虾
2026-04-27 03:22:49 +08:00
parent df27ef3a98
commit 47ffe1bd54
5 changed files with 358 additions and 660 deletions
+113
View File
@@ -868,6 +868,46 @@
</div>
</div>
<!-- CAN 帧发送面板 -->
<div class="card">
<div class="card-header">
<div class="card-title">
<div class="icon">📨</div>
CAN 帧发送
</div>
</div>
<div style="display:grid; grid-template-columns:80px 1fr 80px 1fr; gap:12px; margin-bottom:16px;">
<div class="form-group" style="margin:0;">
<label style="font-size:11px;color:var(--text-muted);margin-bottom:6px;display:block;">通道</label>
<select id="canChannel" style="width:100%;background:var(--bg-secondary);border:1px solid var(--border);border-radius:8px;padding:10px 12px;color:var(--text-primary);font-size:13px;">
<option value="0">CH0</option>
<option value="1">CH1</option>
<option value="2">CH2</option>
</select>
</div>
<div class="form-group" style="margin:0;">
<label style="font-size:11px;color:var(--text-muted);margin-bottom:6px;display:block;">扩展帧 ID</label>
<input type="text" id="canId" value="0x18DA00F1" placeholder="例如: 0x18DA00F1" style="width:100%;background:var(--bg-secondary);border:1px solid var(--border);border-radius:8px;padding:10px 12px;color:var(--text-primary);font-size:13px;">
</div>
<div class="form-group" style="margin:0;">
<label style="font-size:11px;color:var(--text-muted);margin-bottom:6px;display:block;">DLC</label>
<input type="number" id="canDlc" value="8" min="1" max="64" style="width:100%;background:var(--bg-secondary);border:1px solid var(--border);border-radius:8px;padding:10px 12px;color:var(--text-primary);font-size:13px;">
</div>
<div class="form-group" style="margin:0;">
<label style="font-size:11px;color:var(--text-muted);margin-bottom:6px;display:block;">数据 (HEX)</label>
<input type="text" id="canData" value="0102030405060708" placeholder="01020304..." style="width:100%;background:var(--bg-secondary);border:1px solid var(--border);border-radius:8px;padding:10px 12px;color:var(--text-primary);font-size:13px;">
</div>
</div>
<div style="display:flex;gap:12px;">
<button class="btn btn-primary" style="flex:1;" onclick="sendCanFrame()">📤 发送</button>
<button class="btn btn-secondary" onclick="clearCanFields()">清空</button>
</div>
<div id="canResponse" style="margin-top:16px;background:var(--bg-secondary);border-radius:10px;padding:12px 16px;font-family:monospace;font-size:12px;display:none;">
<div style="color:var(--text-muted);margin-bottom:6px;">响应:</div>
<div id="canRespContent" style="color:var(--accent);"></div>
</div>
</div>
<!-- 调试日志 -->
<div class="card log-panel">
<div class="card-header">
@@ -1051,6 +1091,79 @@ async function healthCheck() {
}
}
// ==================== 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();
// 解析 ID (支持 0x 前缀)
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;
}
// 解析 data (hex 字符串)
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 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];