@canboat/canboatjs
Version:
Native javascript version of canboat
159 lines (156 loc) • 6.11 kB
JavaScript
;
/**
* Stream Maretron IPG100 binary frames as canboat plain CSV.
*
* Connects, completes the CONNECT handshake, sends SET_MODE\tBINARY\0 on
* receipt of CONNECTED, and writes one CSV line per inbound 0xA5 frame to
* stdout in the format canboat's `analyzer` accepts:
*
* YYYY-MM-DD-HH:MM:SS.mmm,<prio>,<pgn>,<src>,<dst>,<len>,<hex>,<hex>,...
*
* Control text frames (handshake replies, license-pool announces) go to
* stderr, so stdout can be piped directly into `analyzerjs`:
*
* maretron-ipgjs ipg100-ip | analyzerjs -json -nv
*
* Outbound traffic: each line on stdin is sent to the bus. canboat plain
* CSV is interpreted as a string; a line that starts with `{` is parsed
* as a JSON PGN object (matching cansendjs's convention).
*
* cat commands.txt | maretron-ipgjs ipg100-ip
*
* stdin is not consumed until the CONNECT handshake completes; the kernel
* pipe buffer applies backpressure to the producer so nothing is dropped.
*/
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
const node_events_1 = require("node:events");
const readline_1 = __importDefault(require("readline"));
const minimist_1 = __importDefault(require("minimist"));
const utils_1 = require("./utils");
const utilities_1 = require("../utilities");
const index_1 = require("../index");
const maretron_ipg_1 = require("../maretron-ipg");
const debug = (0, utilities_1.createDebug)('canboatjs:maretron-ipg-cli');
const argv = (0, minimist_1.default)(process.argv.slice(2), {
alias: { h: 'help' },
string: ['port', 'password'],
boolean: ['help', 'version'],
default: {
port: String(maretron_ipg_1.IPG_PORT),
password: ''
}
});
(0, utils_1.printVersion)(argv);
if (argv['help'] || argv['_'].length === 0) {
console.error(`Usage: ${process.argv[1]?.split('/').pop() ?? 'maretron-ipgjs'} [options] host
Streams N2K traffic from a Maretron IPG100 as canboat plain CSV on
stdout. Control/status text frames go to stderr. Lines read on stdin
are sent to the bus after the CONNECT handshake completes (canboat
plain CSV or a single-line JSON PGN object).
Options:
--port <n> TCP port (default 6543)
--password <str> CONNECT password (default empty)
--version print version and exit
-h, --help show this help
Examples:
maretron-ipgjs ipg100-ip | analyzerjs -json -nv
cat commands.txt | maretron-ipgjs ipg100-ip > frames.log 2> control.log
echo '2026-05-15-10:00:00.000,6,59904,0,255,3,00,ee,01' | maretron-ipgjs ipg100-ip`);
process.exit(argv['help'] ? 0 : 1);
}
const host = argv['_'][0];
const port = Number(argv['port']);
if (!Number.isFinite(port) || port <= 0 || port > 65535) {
console.error(`Invalid --port value: ${argv['port']}`);
process.exit(1);
}
debug(`connecting to ${host}:${port}`);
// An app-like EventEmitter — the driver listens on `nmea2000out` /
// `nmea2000JsonOut`, so emitting those events here is how we send.
const app = new node_events_1.EventEmitter();
const stream = new index_1.MaretronIPG({
app,
host,
port,
password: argv['password'],
reconnect: true,
// The CLI passes `app` only for event wiring (nmea2000out / JsonOut
// from stdin), not as a SignalK provider. Re-assert the standalone
// fail-fast policy explicitly so a typo'd hostname exits 1 instead
// of looping forever.
failFastOnInitialConnect: true
});
let stdinAttached = false;
function attachStdin() {
if (stdinAttached)
return;
stdinAttached = true;
const rl = readline_1.default.createInterface({
input: process.stdin,
terminal: false
});
rl.on('line', (line) => {
if (line.length === 0)
return;
// Mirrors cansendjs convention: JSON-line PGN objects or canboat CSV.
if (line[0] === '{') {
try {
const obj = JSON.parse(line);
app.emit('nmea2000JsonOut', obj);
}
catch (err) {
const message = err instanceof Error ? err.message : String(err);
console.error(`-- bad JSON on stdin: ${message}`);
}
}
else {
app.emit('nmea2000out', line);
}
});
rl.on('close', () => {
debug('stdin closed');
});
console.error('-- stdin ready (canboat CSV or JSON, one per line)');
}
stream.on('connected', (info) => {
console.error(`-- connected (serial=${info?.serial ?? '?'})`);
// Attach stdin only now. Until this point the kernel pipe buffer holds
// input from any upstream `cat`/echo, applying backpressure rather than
// discarding lines — so the very first line in commands.txt still gets
// sent. Reconnects don't re-attach (the rl is already live).
attachStdin();
});
stream.on('version', (version, product) => {
console.error(`-- ${product} ${version}`);
});
stream.on('instance', (busAddress, instance) => {
console.error(`-- IPG bus address=${busAddress}, client instance=${instance}`);
});
stream.on('authfail', () => {
console.error('-- authentication failed (NO)');
process.exit(2);
});
// Driver pushes canboat plain CSV strings on the readable side — drain
// them to stdout with newline terminators so `analyzer` can parse the
// output directly.
stream.on('data', (line) => {
process.stdout.write(line + '\n');
});
stream.on('error', () => {
// The driver only emits a stream-level 'error' on a fatal initial-connect
// failure. By the time we get here, the driver has already written the
// underlying socket error to stderr via setProviderError. In-session
// socket errors don't reach this handler — they take the setProviderError
// path and are followed by an automatic reconnect.
process.exit(1);
});
process.on('SIGINT', () => {
console.error('-- SIGINT, closing');
stream.end();
process.exit(0);
});
//# sourceMappingURL=maretron-ipgjs.js.map