add slcan serial support
This commit is contained in:
132
src/slcan.js
Normal file
132
src/slcan.js
Normal file
@@ -0,0 +1,132 @@
|
||||
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;
|
||||
Reference in New Issue
Block a user