@canboat/canboatjs
Version:
Native javascript version of canboat
142 lines • 4.73 kB
JavaScript
"use strict";
/**
* Minimal CAN socket wrapper using uv_poll_t-based reads.
*
* The native addon (native/canSocket.cpp) opens PF_CAN sockets in non-blocking
* mode and exposes a CanPoller class that registers a uv_poll_t watcher on the
* read fd. When frames are readable, the watcher invokes a JS callback that
* drains all available frames via a non-blocking native read. Writes also use
* a non-blocking native write so neither direction ever occupies a libuv
* threadpool worker — this keeps the threadpool free for fs operations and
* lets process.exit() terminate cleanly without hanging on a blocked syscall.
*
* Reads and writes use separate CAN sockets bound to the same interface:
* the read socket has the default (full) RX filter, the write socket has an
* empty RX filter so the kernel doesn't queue frames into a buffer nobody
* reads.
*
* Copyright 2025 Signal K contributors
* Licensed under the Apache License, Version 2.0
*/
Object.defineProperty(exports, "__esModule", { value: true });
exports.CanChannel = void 0;
const events_1 = require("events");
const fs_1 = require("fs");
const CAN_EFF_FLAG = 0x80000000;
const CAN_EFF_MASK = 0x1fffffff;
const CAN_FRAME_SIZE = 16; // sizeof(struct can_frame)
const CAN_DATA_OFFSET = 8;
let native;
let nativeLoadError;
try {
// eslint-disable-next-line @typescript-eslint/no-require-imports
native = require('../build/Release/canSocket.node');
}
catch (releaseErr) {
try {
// eslint-disable-next-line @typescript-eslint/no-require-imports
native = require('../build/Debug/canSocket.node');
}
catch (_debugErr) {
nativeLoadError = releaseErr;
}
}
/**
* Drop-in replacement for socketcan's channel object.
* Same API: addListener('onMessage'), addListener('onStopped'),
* start(), stop(), send(), removeAllListeners().
*/
class CanChannel extends events_1.EventEmitter {
readFd;
writeFd;
poller = null;
stopped = false;
exitHandler = null;
constructor(ifname) {
super();
if (native === undefined) {
const detail = nativeLoadError ? `: ${nativeLoadError.message}` : '';
throw new Error(`Failed to load native canSocket module${detail}`);
}
this.readFd = native.openCanReadSocket(ifname);
try {
this.writeFd = native.openCanWriteSocket(ifname);
}
catch (e) {
(0, fs_1.closeSync)(this.readFd);
throw e;
}
// Defensive: if the process exits without an explicit stop(), the
// CanPoller's uv_poll_t handle will keep the libuv loop alive and
// process.exit() may hesitate. Clean up on 'exit' as a safety net.
this.exitHandler = () => this.stop();
process.on('exit', this.exitHandler);
}
start() {
this.stopped = false;
this.poller = new native.CanPoller(this.readFd, () => this.onReadable());
}
onReadable() {
if (this.stopped) {
return;
}
let frames;
try {
frames = native.readCanFrames(this.readFd);
}
catch (err) {
if (!this.stopped) {
this.emit('onStopped', err.message);
}
return;
}
for (const frame of frames) {
if (frame.length < CAN_FRAME_SIZE) {
continue;
}
const rawId = frame.readUInt32LE(0);
const id = rawId & CAN_EFF_MASK;
const dlc = frame[4];
const data = Buffer.from(frame.subarray(CAN_DATA_OFFSET, CAN_DATA_OFFSET + dlc));
this.emit('onMessage', { id, data });
}
}
stop() {
if (this.stopped) {
return;
}
this.stopped = true;
if (this.exitHandler) {
process.removeListener('exit', this.exitHandler);
this.exitHandler = null;
}
if (this.poller) {
this.poller.close();
this.poller = null;
}
try {
(0, fs_1.closeSync)(this.readFd);
}
catch (_e) { }
try {
(0, fs_1.closeSync)(this.writeFd);
}
catch (_e) { }
}
send(msg) {
if (this.stopped) {
return;
}
const frame = Buffer.alloc(CAN_FRAME_SIZE);
let canId = msg.id;
if (msg.ext) {
canId |= CAN_EFF_FLAG;
}
frame.writeUInt32LE(canId >>> 0, 0);
frame[4] = msg.data.length;
msg.data.copy(frame, CAN_DATA_OFFSET, 0, Math.min(msg.data.length, 8));
native.writeCanFrame(this.writeFd, frame);
}
}
exports.CanChannel = CanChannel;
//# sourceMappingURL=canSocket.js.map