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().
npm install max485-raspberry-nodejsWymaga Linux + Go 1.23+ + make + node-gyp toolchain (gcc, g++,
libnode-dev). Auto-build odpalany przez npm install.
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();| 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). | — |
Async factory — JEDYNY supported sposób tworzenia instancji w v4.x.
new ModbusRTU(...) rzuca.
Opts: { port, baudRate, transceiver='isl43485', dePin=17, rePin=27 }.
readCoils(slaveID, startAddr, count) → Promise<boolean[]>readDiscreteInputs(slaveID, startAddr, count) → Promise<boolean[]>readHoldingRegisters(slaveID, startAddr, count) → Promise<number[]>readInputRegisters(slaveID, startAddr, count) → Promise<number[]>
writeCoil(slaveID, coilAddr, value: boolean)writeRegister(slaveID, regAddr, value: number)writeMultipleCoils(slaveID, startAddr, values: boolean[])writeMultipleRegisters(slaveID, startAddr, values: number[])
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.
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,
}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.
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 |
|---|---|
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) |
MIT