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