add slcan serial support
This commit is contained in:
@@ -64,6 +64,11 @@ const CONNECTION_TYPES = {
|
||||
WEBSOCKET: 'websocket'
|
||||
};
|
||||
|
||||
const SERIAL_PROTOCOLS = {
|
||||
MAVLINK: 'mavlink',
|
||||
SLCAN: 'slcan'
|
||||
};
|
||||
|
||||
// Add this constant inside the ConnectionSettingsModal.js file, outside the component
|
||||
const INTERFACE_BUS_LIST = [0, 1]; //BUS 1, BUS 2
|
||||
|
||||
@@ -85,6 +90,7 @@ const ConnectionSettingsModal = ({
|
||||
const [ports, setPorts] = useState([]);
|
||||
const [selectedPort, setSelectedPort] = useState(null);
|
||||
const [baudRate, setBaudRate] = useState(DEFAULT_BAUD_RATE);
|
||||
const [serialProtocol, setSerialProtocol] = useState(SERIAL_PROTOCOLS.MAVLINK);
|
||||
const [wsHost, setWsHost] = useState(DEFAULT_WS_HOST);
|
||||
const [wsPort, setWsPort] = useState(DEFAULT_WS_PORT);
|
||||
|
||||
@@ -93,6 +99,7 @@ const ConnectionSettingsModal = ({
|
||||
|
||||
// Track active connection
|
||||
const [activeConnection, setActiveConnection] = useState(null); // null, 'serial', or 'websocket'
|
||||
const [activeSerialProtocol, setActiveSerialProtocol] = useState(null);
|
||||
|
||||
// Add these state variables after your other state declarations
|
||||
const [hostError, setHostError] = useState('');
|
||||
@@ -240,6 +247,7 @@ const ConnectionSettingsModal = ({
|
||||
|
||||
// Update the UI regardless of connection state
|
||||
setActiveConnection(null);
|
||||
setActiveSerialProtocol(null);
|
||||
onConnectionStatusChange(false);
|
||||
showMessage('Serial connection closed', 'info');
|
||||
|
||||
@@ -264,25 +272,28 @@ const ConnectionSettingsModal = ({
|
||||
}
|
||||
}
|
||||
|
||||
window.mavlinkSession.initWebSerialConnection(port, baudRate);
|
||||
window.mavlinkSession.initWebSerialConnection(port, baudRate, { protocol: serialProtocol });
|
||||
window.mavlinkSession.addWebSerialOpenHandler(() => {
|
||||
// Set Node ID and Bus for the local node
|
||||
window.localNode.setNodeId(parseInt(nodeId, 10));
|
||||
window.localNode.setBus(selectedBus);
|
||||
|
||||
|
||||
// Start the mavlinkCanForward interval
|
||||
if (serialProtocol === SERIAL_PROTOCOLS.MAVLINK) {
|
||||
const intervalId = setInterval(() => {
|
||||
if (window.mavlinkSession) {
|
||||
window.mavlinkSession.enableMavlinkCanForward(window.localNode.bus);
|
||||
}
|
||||
}, 1000);
|
||||
|
||||
setForwardingInterval(intervalId);
|
||||
} else {
|
||||
setForwardingInterval(null);
|
||||
}
|
||||
|
||||
setActiveConnection('serial');
|
||||
setActiveSerialProtocol(serialProtocol);
|
||||
setConnectionInProgress(false);
|
||||
onConnectionStatusChange(true);
|
||||
showMessage('Serial connection established', 'success');
|
||||
showMessage(`Serial ${serialProtocol === SERIAL_PROTOCOLS.SLCAN ? 'SLCAN' : 'MAVLink'} connection established`, 'success');
|
||||
})
|
||||
|
||||
window.mavlinkSession.addWebSerialErrorHandler((error) => {
|
||||
@@ -404,6 +415,7 @@ const ConnectionSettingsModal = ({
|
||||
// Force close the connection
|
||||
window.mavlinkSession.close();
|
||||
setActiveConnection(null);
|
||||
setActiveSerialProtocol(null);
|
||||
onConnectionStatusChange(false);
|
||||
showMessage('WebSocket connection closed', 'info');
|
||||
if (forwardingInterval) {
|
||||
@@ -447,6 +459,7 @@ const ConnectionSettingsModal = ({
|
||||
}, 1000);
|
||||
setForwardingInterval(intervalId);
|
||||
setActiveConnection('websocket');
|
||||
setActiveSerialProtocol(null);
|
||||
onConnectionStatusChange(true);
|
||||
setConnectionInProgress(false);
|
||||
showMessage('WebSocket connection established', 'success');
|
||||
@@ -512,7 +525,7 @@ const ConnectionSettingsModal = ({
|
||||
<Box display="flex" alignItems="center" gap={1}>
|
||||
{activeConnection && (
|
||||
<Chip
|
||||
label={`Connected via ${activeConnection}`}
|
||||
label={activeConnection === 'serial' && activeSerialProtocol ? `Connected via serial (${activeSerialProtocol === SERIAL_PROTOCOLS.SLCAN ? 'SLCAN' : 'MAVLink'})` : `Connected via ${activeConnection}`}
|
||||
color="success"
|
||||
size="small"
|
||||
variant="outlined"
|
||||
@@ -587,6 +600,18 @@ const ConnectionSettingsModal = ({
|
||||
</Box>
|
||||
</Box>
|
||||
|
||||
<FormControl fullWidth size="small" disabled={activeConnection !== null}>
|
||||
<InputLabel>Serial Protocol</InputLabel>
|
||||
<Select
|
||||
value={serialProtocol}
|
||||
onChange={(e) => setSerialProtocol(e.target.value)}
|
||||
label="Serial Protocol"
|
||||
>
|
||||
<MenuItem value={SERIAL_PROTOCOLS.MAVLINK}>MAVLink tunnel</MenuItem>
|
||||
<MenuItem value={SERIAL_PROTOCOLS.SLCAN}>SLCAN / LAWICEL</MenuItem>
|
||||
</Select>
|
||||
</FormControl>
|
||||
|
||||
{/* Port action buttons */}
|
||||
<Box sx={{ display: 'flex', gap: 1 }}>
|
||||
<Button
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { EventEmitter } from 'events';
|
||||
import WebSocketClient from './ws_client';
|
||||
import WebSerial from './web_serial';
|
||||
import SlcanCodec from './slcan';
|
||||
import dronecan from './dronecan';
|
||||
import './mavlink';
|
||||
|
||||
@@ -12,6 +13,8 @@ class MavlinkSession extends EventEmitter {
|
||||
this.mavlinkProcessor = new MAVLink20Processor(null, this.targetSystem, this.targetComponent);
|
||||
this.wsClient = null;
|
||||
this.serial = null;
|
||||
this.serialProtocol = 'mavlink';
|
||||
this.slcanCodec = null;
|
||||
this.parseBuffer = this.parseBuffer.bind(this);
|
||||
}
|
||||
|
||||
@@ -28,6 +31,9 @@ class MavlinkSession extends EventEmitter {
|
||||
}
|
||||
|
||||
initWebSocketConnection(ip, port, mavlinkSigning='') {
|
||||
this.serialProtocol = 'mavlink';
|
||||
this.slcanCodec = null;
|
||||
|
||||
if (mavlinkSigning) {
|
||||
const enc = new TextEncoder();
|
||||
const data = enc.encode(mavlinkSigning);
|
||||
@@ -49,13 +55,21 @@ class MavlinkSession extends EventEmitter {
|
||||
this.wsClient.connect();
|
||||
}
|
||||
|
||||
initWebSerialConnection(port, baudRate) {
|
||||
initWebSerialConnection(port, baudRate, options = {}) {
|
||||
this.serial = new WebSerial(port, baudRate);
|
||||
this.serialProtocol = options.protocol || 'mavlink';
|
||||
this.slcanCodec = this.serialProtocol === 'slcan' ? new SlcanCodec() : null;
|
||||
|
||||
if (this.serialProtocol === 'mavlink') {
|
||||
this.mavlinkProcessor.file = this.serial;
|
||||
}
|
||||
|
||||
this.serial.addMessageHandler((buffer) => {
|
||||
// console.log('Received buffer:', buffer);
|
||||
if (this.serialProtocol === 'slcan') {
|
||||
this.parseSlcanBuffer(buffer);
|
||||
} else {
|
||||
this.parseBuffer(buffer);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@@ -72,7 +86,11 @@ class MavlinkSession extends EventEmitter {
|
||||
}
|
||||
|
||||
webSerialConnect() {
|
||||
this.serial.connect();
|
||||
this.serial.connect().then(() => {
|
||||
if (this.serialProtocol === 'slcan') {
|
||||
this.openSlcanChannel();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
close() {
|
||||
@@ -84,6 +102,17 @@ class MavlinkSession extends EventEmitter {
|
||||
}
|
||||
}
|
||||
|
||||
openSlcanChannel() {
|
||||
if (!this.serial || this.serialProtocol !== 'slcan') return;
|
||||
|
||||
console.log('SLCAN: opening CAN channel');
|
||||
setTimeout(() => {
|
||||
if (this.serial && this.serial.connected) {
|
||||
this.serial.write('O\r');
|
||||
}
|
||||
}, 0);
|
||||
}
|
||||
|
||||
parseBuffer(buffer) {
|
||||
// console.log('Parsing buffer:', buffer);
|
||||
const messages = this.mavlinkProcessor.parseBuffer(buffer);
|
||||
@@ -94,6 +123,21 @@ class MavlinkSession extends EventEmitter {
|
||||
}
|
||||
}
|
||||
|
||||
parseSlcanBuffer(buffer) {
|
||||
this.slcanCodec.feed(
|
||||
buffer,
|
||||
({ id, data, len }) => {
|
||||
console.log('SLCAN RX frame:', { id: id.toString(16), len, data });
|
||||
if (typeof localNode !== 'undefined' && localNode) {
|
||||
localNode.emit('can-frame', (id | dronecan.TransferManager.FlagEFF) >>> 0, data, len);
|
||||
}
|
||||
if (this.listenerCount('mav-rx') > 0) {
|
||||
this.emit('mav-rx', { protocol: 'slcan', id, data, len });
|
||||
}
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
handleMavlinkMsg(message) {
|
||||
switch (message._id) {
|
||||
case mavlink20.MAVLINK_MSG_ID_HEARTBEAT:
|
||||
@@ -119,21 +163,49 @@ class MavlinkSession extends EventEmitter {
|
||||
default:
|
||||
}
|
||||
|
||||
if (this.getMaxListeners('mav-rx') > 0) {
|
||||
if (this.listenerCount('mav-rx') > 0) {
|
||||
this.emit('mav-rx', message);
|
||||
}
|
||||
}
|
||||
|
||||
sendMavlinkMsg(msg) {
|
||||
if (this.serialProtocol === 'slcan') return;
|
||||
|
||||
if ((this.wsClient && this.wsClient.connected) || (this.serial && this.serial.connected)) {
|
||||
this.mavlinkProcessor.send(msg);
|
||||
if (this.getMaxListeners('mav-tx') > 0) {
|
||||
if (this.listenerCount('mav-tx') > 0) {
|
||||
this.emit('mav-tx', msg);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
sendCanFrame(bus, messageId, data, len) {
|
||||
if (this.serialProtocol === 'slcan') {
|
||||
if (this.serial && this.serial.connected && this.slcanCodec) {
|
||||
const line = this.slcanCodec.encodeExtendedFrame(messageId, data, len);
|
||||
console.log('SLCAN TX frame:', line.trim());
|
||||
this.serial.write(line);
|
||||
if (this.listenerCount('mav-tx') > 0) {
|
||||
this.emit('mav-tx', { protocol: 'slcan', id: messageId, data, len });
|
||||
}
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
const msg = new mavlink20.messages.can_frame(
|
||||
this.targetSystem,
|
||||
this.targetComponent,
|
||||
bus,
|
||||
len,
|
||||
messageId,
|
||||
data.toString('binary')
|
||||
);
|
||||
this.sendMavlinkMsg(msg);
|
||||
}
|
||||
|
||||
enableMavlinkCanForward(bus) {
|
||||
if (this.serialProtocol === 'slcan') return;
|
||||
|
||||
// console.log('Enabling CAN forward on bus:', bus);
|
||||
const msg = new mavlink20.messages.command_long(
|
||||
this.targetSystem, // target_system
|
||||
|
||||
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