Files
DroneCan_WebTools/src/slcan.js
2026-05-23 09:02:26 +08:00

133 lines
4.3 KiB
JavaScript

class SlcanCodec {
constructor() {
this.buffer = '';
this.decoder = new TextDecoder('ascii');
}
feed(chunk, onFrame, onError) {
const text = this.decoder.decode(chunk, { stream: true });
if (text.includes('\x07')) {
console.warn('SLCAN adapter returned NACK');
}
this.buffer += text.replace(/\x07/g, '');
this.parseBuffer(onFrame);
}
parseBuffer(onFrame) {
while (this.buffer.length > 0) {
this.buffer = this.buffer.replace(/^[\r\n]+/, '');
if (!this.buffer) return;
const frameStart = this.buffer.search(/[TtRr]/);
if (frameStart === -1) {
this.buffer = '';
return;
}
if (frameStart > 0) {
this.buffer = this.buffer.slice(frameStart);
}
const type = this.buffer[0];
if (type === 'T') {
if (this.buffer.length < 10) return;
const dlcText = this.buffer[9];
if (!/^[0-8]$/.test(dlcText)) {
console.warn(`Malformed SLCAN frame header: ${this.buffer.slice(0, 10)}`);
this.buffer = this.buffer.slice(1);
continue;
}
const dlc = parseInt(dlcText, 16);
const frameLength = 10 + dlc * 2;
if (this.buffer.length < frameLength) return;
const line = this.buffer.slice(0, frameLength);
this.buffer = this.buffer.slice(frameLength).replace(/^[\r\n]+/, '');
this.parseLine(line, onFrame);
continue;
}
if (type === 't') {
if (this.buffer.length < 5) return;
const dlcText = this.buffer[4];
if (!/^[0-8]$/.test(dlcText)) {
this.buffer = this.buffer.slice(1);
continue;
}
const frameLength = 5 + parseInt(dlcText, 16) * 2;
if (this.buffer.length < frameLength) return;
this.buffer = this.buffer.slice(frameLength).replace(/^[\r\n]+/, '');
continue;
}
const lineEndIndex = this.findLineEnd();
if (lineEndIndex === -1) return;
this.buffer = this.buffer.slice(lineEndIndex + 1);
}
}
findLineEnd() {
const crIndex = this.buffer.indexOf('\r');
const lfIndex = this.buffer.indexOf('\n');
if (crIndex === -1) return lfIndex;
if (lfIndex === -1) return crIndex;
return Math.min(crIndex, lfIndex);
}
parseLine(line, onFrame) {
if (!line) return;
const type = line[0];
if (type === 't' || type === 'r' || type === 'R') return;
if (type !== 'T') return;
if (line.length < 10) {
console.warn(`Malformed SLCAN frame: ${line}`);
return;
}
const idText = line.slice(1, 9);
const dlcText = line[9];
if (!/^[0-9a-fA-F]{8}$/.test(idText) || !/^[0-8]$/.test(dlcText)) {
console.warn(`Malformed SLCAN frame: ${line}`);
return;
}
const dlc = parseInt(dlcText, 16);
const dataText = line.slice(10, 10 + dlc * 2);
if (dataText.length !== dlc * 2 || !/^[0-9a-fA-F]*$/.test(dataText)) {
console.warn(`Malformed SLCAN payload: ${line}`);
return;
}
const data = new Uint8Array(dlc);
for (let i = 0; i < dlc; i++) {
data[i] = parseInt(dataText.slice(i * 2, i * 2 + 2), 16);
}
onFrame?.({
id: parseInt(idText, 16) & 0x1FFFFFFF,
data,
len: dlc,
});
}
encodeExtendedFrame(messageId, data, len) {
const canId = messageId & 0x1FFFFFFF;
const frameLength = Math.min(len, data.length ?? len);
if (frameLength > 8) {
throw new Error('SLCAN classic CAN frames support a maximum DLC of 8');
}
let dataHex = '';
for (let i = 0; i < frameLength; i++) {
dataHex += data[i].toString(16).toUpperCase().padStart(2, '0');
}
return `T${canId.toString(16).toUpperCase().padStart(8, '0')}${frameLength.toString(16).toUpperCase()}${dataHex}\r`;
}
}
export default SlcanCodec;