max485-raspberry-nodejs
v4.0.0
Published
Node.js library for Modbus RTU communication over RS485 using MAX485 transceivers on Raspberry Pi
Downloads
220
Maintainers
Readme
max485-raspberry-nodejs
Node.js library for Modbus RTU communication over RS485 on Raspberry Pi.
v4.0.0 — kompletny architectural refactor. Wszystkie operacje są
prawdziwie asynchroniczne (napi_async_work, off main thread), z natywnymi
JS Errors zamiast string sentinels, structured ModbusException, typed arrays,
opcjonalnym retry, observability counters i finalize_cb chroniącym przed
resource leak przy GC bez explicit close().
Installation
npm install max485-raspberry-nodejsWymaga Linux + Go 1.23+ + make + node-gyp toolchain (gcc, g++,
libnode-dev). Auto-build odpalany przez npm install.
Quick start
const ModbusRTU = require('max485-raspberry-nodejs');
async function main() {
const device = await ModbusRTU.open({
port: '/dev/serial0',
baudRate: 9600,
transceiver: 'isl43485', // 'isl43485' | 'max485' | 'auto'
dePin: 17, // BCM numbering
rePin: 27, // tylko dla isl43485
});
// Opcjonalne: włącz automatyczny retry dla transient errors (timeout/CRC).
// ModbusException (illegal address etc.) NIE jest retry'owany.
device.setRetryConfig({ maxRetries: 2, backoffMs: 50 });
try {
const coils = await device.readCoils(21, 0, 4);
// -> [true, false, true, false] (natywny Array<boolean>)
await device.writeCoil(21, 0, true);
await device.writeMultipleCoils(21, 0, [true, false, true, false]);
const regs = await device.readHoldingRegisters(21, 0, 4);
// -> [42, 100, 0, 65535] (natywny Array<number>)
await device.writeRegister(21, 0, 123);
await device.writeMultipleRegisters(21, 0, [50, 100, 150, 200]);
} catch (e) {
if (e.code === 'MODBUS_EXCEPTION') {
// Structured fields — bez parse'owania message string!
console.error('Modbus exception:', {
slaveID: e.slaveID, // np. 21
functionCode: e.functionCode, // np. 0x05
exceptionCode: e.exceptionCode, // np. 0x02 = illegal data address
});
} else {
console.error('Bus error:', e.message);
}
} finally {
await device.close();
}
}
main();Transceiver types
| Type | Description | Pins |
|------|-------------|------|
| isl43485 | ISL43485IBZ (HAT Power Shield v2). Driver enable order chroni przed SHUTDOWN trap. | dePin, rePin |
| max485 | Klasyczny MAX485 (DE i /RE złączone w 1 pin). | dePin only |
| auto | USB↔RS485 z auto-direction (CH340, FTDI w RS485 mode). | — |
API
static async ModbusRTU.open(opts) → Promise<ModbusRTU>
Async factory — JEDYNY supported sposób tworzenia instancji w v4.x.
new ModbusRTU(...) rzuca.
Opts: { port, baudRate, transceiver='isl43485', dePin=17, rePin=27 }.
Read ops (Promise → natywny Array)
readCoils(slaveID, startAddr, count) → Promise<boolean[]>readDiscreteInputs(slaveID, startAddr, count) → Promise<boolean[]>readHoldingRegisters(slaveID, startAddr, count) → Promise<number[]>readInputRegisters(slaveID, startAddr, count) → Promise<number[]>
Write ops (Promise)
writeCoil(slaveID, coilAddr, value: boolean)writeRegister(slaveID, regAddr, value: number)writeMultipleCoils(slaveID, startAddr, values: boolean[])writeMultipleRegisters(slaveID, startAddr, values: number[])
Configuration
setDebug(level: 0|1|2)— per-instance debug.0=off,1=basic events,2=basic + hex TX/RX dump. Wcześniej globalny env varMAX485_DEBUG; teraz consumer sam wybiera per device:if (process.env.MAX485_DEBUG) device.setDebug(parseInt(process.env.MAX485_DEBUG, 10) || 1);setRetryConfig({ maxRetries, backoffMs })— włącz retry dla transient.setRetryConfig(null)lub{ maxRetries: 0 }wyłącza.
Observability
stats() → object(sync, czysty snapshot atomic counters)
{
opsTotal: 12345n, // BigInt
opsByResult: {
success: 12000n, timeout: 12n, crc_error: 3n,
exception: 5n, io_error: 0n
},
opsBySlave: {
'21': { ops: 5000n, successes: 4990n, timeouts: 5n, ..., sumLatencyMicro: 12340567n },
'31': { ... }
},
lastTxUnixNano: 1748100000000000000n,
lastRxUnixNano: 1748100000003200000n,
}Lifecycle
async close()— zwalnia port, GPIO bus-idle, lockfile removed, rpio refcount decremented. Idempotent. Jeśli pominiesz, napi finalize_cb przy GC i tak posprząta (F1.1 / A4) — ale explicit close = deterministic timing.
Error model
Wszystkie operacje bus mogą rzucić:
| Error.code | Pola | Kiedy |
|------------|------|-------|
| MODBUS_EXCEPTION | slaveID, functionCode, exceptionCode | Slave zwrócił FC | 0x80 (permanent) |
| undefined | message zawiera "modbus timeout", "CRC error", "write:", "drain:" etc. | Transient bus error (retry-able) |
MODBUS_EXCEPTION NIE jest retry'owany przez setRetryConfig (permanent
application error). Wszystko inne (timeout, CRC, IO) traktujemy jako
transient → retry kicks in jeśli skonfigurowane.
v3.x → v4.0.0 migration
| v3.x | v4.0.0 |
|------|--------|
| new ModbusRTU(port, baud, de, re) | await ModbusRTU.open({ port, baudRate, transceiver: 'isl43485', dePin, rePin }) |
| device.close() (sync) | await device.close() |
| result.startsWith('Error:') then throw | try/catch (natywny throw) |
| error.message.includes('exception') (parse) | error.code === 'MODBUS_EXCEPTION', fields slaveID/functionCode/exceptionCode |
| JSON.parse(await readCoils(...)) | await readCoils(...) (natywny array) |
| await writeCoil(...) === 'success' | await writeCoil(...) (Promise) |
| MAX485_DEBUG=1 env var | device.setDebug(1) |
License
MIT
