/system/status 检查设备状态...';
}
try {
var resp = await fetch(baseUrl + '/system/status', { signal: AbortSignal.timeout(3000) });
if (!resp.ok) {
setConnectionState(false, 'HTTP ' + resp.status, '', '');
if (el.connectionNote) {
el.connectionNote.className = 'helper-note err';
el.connectionNote.innerHTML = '状态接口返回 HTTP ' + resp.status + ',请确认设备地址和 HTTP 服务。';
}
showToast('设备响应异常: HTTP ' + resp.status, 'error');
return;
}
var data = await resp.json();
state.latestStatus = data;
var wifi = data.wifi || {};
var connected = !!wifi.connected;
var mode = wifi.mode || 'STA';
var text = connected ? (mode + ' 已连接') : (mode + ' 未连接');
setConnectionState(connected, text, data.version, data.build_time);
if (el.connectionNote) {
el.connectionNote.className = 'helper-note ' + (connected ? 'ok' : 'warn');
el.connectionNote.innerHTML =
'IP: ' + sanitizeHtml(wifi.ip || '--') + ',RSSI: ' +
sanitizeHtml(wifi.rssi !== undefined && wifi.rssi !== -127 ? (wifi.rssi + ' dBm') : '--') +
',运行: ' + sanitizeHtml(formatUptime(data.uptime_sec)) + '';
}
showToast(connected ? 'ESP32 当前在线' : '设备已响应,但 WiFi 未连接', connected ? 'success' : 'warning');
} catch (err) {
setConnectionState(false, '连接失败', '', '');
if (el.connectionNote) {
el.connectionNote.className = 'helper-note err';
el.connectionNote.innerHTML = '无法访问 /system/status:' + sanitizeHtml(err.message);
}
showToast('连接失败: ' + err.message, 'error');
}
}
function formatUptime(sec) {
if (!Number.isFinite(sec) || sec < 0) return '--';
var s = Math.floor(sec);
var days = Math.floor(s / 86400);
s %= 86400;
var hours = Math.floor(s / 3600);
s %= 3600;
var mins = Math.floor(s / 60);
s %= 60;
if (days > 0) return days + 'd ' + hours + 'h';
if (hours > 0) return hours + 'h ' + mins + 'm';
if (mins > 0) return mins + 'm ' + s + 's';
return s + 's';
}
function normalizeWhitespace(text) {
return String(text || '').replace(/\r/g, '').trim();
}
function parseDbc(text) {
var lines = String(text || '').replace(/\r/g, '').split('\n');
var messages = [];
var messageMap = {};
var comments = { messages: {}, signals: {} };
var valueTables = {};
var sigDefaults = {};
var nodeNames = [];
var current = null;
for (var i = 0; i < lines.length; i++) {
var line = lines[i].trim();
if (!line) continue;
var nodeMatch = line.match(/^BU_\s*:\s*(.*)$/);
if (nodeMatch) {
nodeNames = nodeMatch[1].trim() ? nodeMatch[1].trim().split(/\s+/) : [];
continue;
}
var messageMatch = line.match(/^BO_\s+(\d+)\s+([A-Za-z0-9_]+)\s*:\s*(\d+)\s+(.+)$/);
if (messageMatch) {
current = {
idRaw: parseInt(messageMatch[1], 10),
name: messageMatch[2],
dlc: parseInt(messageMatch[3], 10),
transmitter: messageMatch[4].trim(),
signals: [],
comment: ''
};
current.id = current.idRaw & 0x1FFFFFFF;
current.isExtended = current.idRaw > 0x7FF;
current.key = String(current.idRaw) + ':' + current.name;
messages.push(current);
messageMap[current.idRaw] = current;
continue;
}
var signalMatch = line.match(/^SG_\s+([A-Za-z0-9_]+)(?:\s+((?:M|m-?\d+)))?\s*:\s*(\d+)\|(\d+)@([01])([+-])\s+\(([^\),]+),([^\)]+)\)\s+\[([^\|]*)\|([^\]]*)\]\s+"([^"]*)"\s+(.+)$/);
if (signalMatch && current) {
var muxInfo = parseMultiplexToken(signalMatch[2]);
current.signals.push({
name: signalMatch[1],
startBit: parseInt(signalMatch[3], 10),
length: parseInt(signalMatch[4], 10),
byteOrder: signalMatch[5] === '1' ? 'little' : 'big',
isSigned: signalMatch[6] === '-',
factor: parseFloat(signalMatch[7]),
offset: parseFloat(signalMatch[8]),
min: signalMatch[9].trim() === '' ? null : parseFloat(signalMatch[9]),
max: signalMatch[10].trim() === '' ? null : parseFloat(signalMatch[10]),
unit: signalMatch[11],
receivers: signalMatch[12].split(/\s*,\s*/),
comment: '',
valueType: 'int',
enumMap: {},
defaultValue: null,
multiplexRole: muxInfo.role,
multiplexValue: muxInfo.value
});
continue;
}
var messageCommentMatch = line.match(/^CM_\s+BO_\s+(\d+)\s+"([\s\S]*)";$/);
if (messageCommentMatch) {
comments.messages[messageCommentMatch[1]] = messageCommentMatch[2];
continue;
}
var signalCommentMatch = line.match(/^CM_\s+SG_\s+(\d+)\s+([A-Za-z0-9_]+)\s+"([\s\S]*)";$/);
if (signalCommentMatch) {
comments.signals[signalCommentMatch[1] + ':' + signalCommentMatch[2]] = signalCommentMatch[3];
continue;
}
var valueMatch = line.match(/^VAL_\s+(\d+)\s+([A-Za-z0-9_]+)\s+(.+);$/);
if (valueMatch) {
var table = {};
var pairRe = /(-?\d+)\s+"([^"]*)"/g;
var m;
while ((m = pairRe.exec(valueMatch[3])) !== null) {
table[m[1]] = m[2];
}
valueTables[valueMatch[1] + ':' + valueMatch[2]] = table;
continue;
}
var valTypeMatch = line.match(/^SIG_VALTYPE_\s+(\d+)\s+([A-Za-z0-9_]+)\s*:\s*(\d+)\s*;$/);
if (valTypeMatch) {
var msg = messageMap[valTypeMatch[1]];
if (!msg) continue;
var signal = findSignal(msg, valTypeMatch[2]);
if (signal) {
signal.valueType = valTypeMatch[3] === '1' ? 'float' : valTypeMatch[3] === '2' ? 'double' : 'int';
}
continue;
}
/* BA_ "GenSigDefaultValue" SG_ BO_、SG_、VAL_、CM_ 和 SIG_VALTYPE_。';
}
}
function bindEvents() {
el.testConnBtn.addEventListener('click', testConnection);
el.defaultChannel.addEventListener('change', function () {
el.manualChannel.value = el.defaultChannel.value;
el.canChannelPill.textContent = 'CH' + el.defaultChannel.value;
renderMessageList();
renderSelectedMessage();
updateEncodedPreview();
});
el.dbcDropZone.addEventListener('click', function () { el.dbcFileInput.click(); });
el.dbcDropZone.addEventListener('dragover', function (event) {
event.preventDefault();
el.dbcDropZone.classList.add('dragover');
});
el.dbcDropZone.addEventListener('dragleave', function () {
el.dbcDropZone.classList.remove('dragover');
});
el.dbcDropZone.addEventListener('drop', function (event) {
event.preventDefault();
el.dbcDropZone.classList.remove('dragover');
var file = event.dataTransfer && event.dataTransfer.files ? event.dataTransfer.files[0] : null;
if (file) handleDbcFile(file).catch(handleDbcError);
});
el.dbcFileInput.addEventListener('change', function (event) {
var file = event.target && event.target.files ? event.target.files[0] : null;
if (file) handleDbcFile(file).catch(handleDbcError);
});
el.clearDbcBtn.addEventListener('click', clearDbc);
el.dbcSearch.addEventListener('input', filterMessages);
el.sendSelectedBtn.addEventListener('click', sendSelectedMessage);
el.loadPreviewToRawBtn.addEventListener('click', loadPreviewToManual);
el.sendManualBtn.addEventListener('click', sendManualFrame);
el.resetManualBtn.addEventListener('click', resetManualFields);
el.manualData.addEventListener('input', refreshManualHint);
el.manualDlc.addEventListener('input', refreshManualHint);
el.manualId.addEventListener('input', refreshManualHint);
el.toolIp.addEventListener('change', testConnection);
if (el.sendCheckedBtn) el.sendCheckedBtn.addEventListener('click', sendCheckedMessages);
if (el.selectAllBox) el.selectAllBox.addEventListener('change', function () {
var visible = state.filteredMessages || [];
if (this.checked) {
visible.forEach(function (m) { state.checkedKeys[m.key] = true; });
} else {
visible.forEach(function (m) { delete state.checkedKeys[m.key]; });
}
renderMessageList();
});
if (el.expandAllBtn) el.expandAllBtn.addEventListener('click', function () {
var visible = state.filteredMessages || [];
visible.forEach(function (m) { state.expandedKeys[m.key] = true; ensureMessageDefaults(m); });
renderMessageList();
});
if (el.collapseAllBtn) el.collapseAllBtn.addEventListener('click', function () {
var visible = state.filteredMessages || [];
visible.forEach(function (m) { delete state.expandedKeys[m.key]; });
renderMessageList();
});
bindNodeFilterEvents();
}
function handleDbcError(err) {
if (el.dbcHint) {
el.dbcHint.className = 'helper-note err';
el.dbcHint.innerHTML = 'DBC 解析失败:' + sanitizeHtml(err.message);
}
showToast('DBC 解析失败: ' + err.message, 'error');
}
function init() {
cacheElements();
bindEvents();
setConnectionState(false, '未连接', '', '');
if (el.canChannelPill) el.canChannelPill.textContent = 'CH' + el.defaultChannel.value;
if (el.manualChannel) el.manualChannel.value = el.defaultChannel.value;
buildSummary();
renderMessageList();
renderSelectedMessage();
refreshManualHint();
renderHistory();
setTimeout(testConnection, 150);
}
var CAN_SEND_ERR_DESC = {
'-1': '参数错误(通道/ID/DLC 无效)',
'-2': 'SPI 通信失败(ESP32 与 GD32 通信异常)',
'-3': 'GD32 CAN 总线拒绝',
};
var GD32_ERR_DESC = {
0xE0: 'payload 过短',
0xE1: '通道号非法',
0xE2: 'BUS_OFF(总线离线,检查接线/终端电阻/对端上电)',
0xE3: 'TEC 过高(错误计数 >96,对端 ACK 失败率高)',
0xE4: '邮箱满(3 个硬件邮箱全 PENDING,对端不 ACK)',
0xE5: '参数无效(通道未启用/DLC 越界)',
0xEE: 'SPI 帧解析失败(GD32 收到的帧 CRC/SOF 校验失败,通常是 SPI 字节丢失或 CS 时序冲突)'
};
function buildSendResponseText(result) {
if (result.status === 'ok') {
return result.recv ? ('0x' + result.recv + ' (' + result.recv_len + ' 字节)') : '无响应数据';
}
var code = String(result.code);
var desc = CAN_SEND_ERR_DESC[code] || '未知错误';
/* 用 GD32 细分错误码补充具体原因 */
if (result.gd32_err != null && result.gd32_err !== 0) {
var gd32Desc = GD32_ERR_DESC[result.gd32_err];
if (gd32Desc) {
desc = desc + ' · ' + gd32Desc;
} else {
desc = desc + ' · gd32_err=0x' + result.gd32_err.toString(16).toUpperCase();
}
}
return '发送失败: ' + desc + ' (code=' + result.code + ')';
}
init();
})();