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;