UNPKG

modbus-webserial

Version:

Tiny TypeScript library for speaking Modbus-RTU from the browser via Web Serial

628 lines (622 loc) 19.5 kB
// src/core/errors.ts var EXCEPTION_MESSAGES = { 1: "Illegal Function", 2: "Illegal Data Address", 3: "Illegal Data Value", 4: "Slave Device Failure", 5: "Acknowledge", 6: "Slaver Device Busy", 8: "Memory Parity Error", 10: "Gateway Path Unavailable", 11: "Gateway Target Device Failed to Respond" }; var CrcError = class extends Error { constructor() { super("CRC check failed"); } }; var TimeoutError = class extends Error { constructor() { super("Modbus response timed out"); } }; var ExceptionError = class extends Error { constructor(code) { var _a; super((_a = EXCEPTION_MESSAGES[code]) != null ? _a : `Modbus exception 0x${code.toString(16)}`); this.code = code; } }; // src/core/crc16.ts function crc16(buf) { let crc = 65535; for (let b of buf) { crc ^= b; for (let i = 0; i < 8; i++) { crc = crc >> 1 ^ (crc & 1 ? 40961 : 0); } } return crc; } // src/transport/webserial.ts var WebSerialTransport = class _WebSerialTransport { constructor() { this.timeout = 500; this.rxBuf = new Uint8Array(0); } // rolling buffer across calls // timeout gelpers setTimeout(ms) { this.timeout = ms; } getTimeout() { return this.timeout; } getPort() { return this.port; } // ---------------------------------------------------------------- // Factory // ---------------------------------------------------------------- static async open(opts = {}) { const t = new _WebSerialTransport(); await t.init(opts); return t; } async init(opts) { var _a, _b, _c, _d, _e, _f; this.timeout = (_a = opts.timeout) != null ? _a : 500; this.port = await navigator.serial.requestPort({ filters: (_b = opts.requestFilters) != null ? _b : [] }); await this.port.open({ baudRate: (_c = opts.baudRate) != null ? _c : 9600, dataBits: (_d = opts.dataBits) != null ? _d : 8, stopBits: (_e = opts.stopBits) != null ? _e : 1, parity: (_f = opts.parity) != null ? _f : "none" }); this.reader = this.port.readable.getReader(); this.writer = this.port.writable.getWriter(); } // ---------------------------------------------------------------- // transact(): Send `req` and await a response whose function-code matches the request // ---------------------------------------------------------------- */ async transact(req) { await this.writer.write(req); const expectedFC = req[1] & 127; const deadline = Date.now() + this.timeout; while (true) { while (this.rxBuf.length < 3) { if (Date.now() > deadline) throw new TimeoutError(); const { value } = await this.reader.read(); if (!value) throw new TimeoutError(); this.rxBuf = concat(this.rxBuf, value); } const need = this.frameLengthIfComplete(this.rxBuf); if (need === 0) { if (Date.now() > deadline) throw new TimeoutError(); const { value } = await this.reader.read(); if (!value) throw new TimeoutError(); this.rxBuf = concat(this.rxBuf, value); continue; } const frame = this.rxBuf.slice(0, need); this.rxBuf = this.rxBuf.slice(need); const crc = crc16(frame.subarray(0, frame.length - 2)); const crcOk = (crc & 255) === frame[frame.length - 2] && crc >> 8 === frame[frame.length - 1]; if (!crcOk) { this.rxBuf = this.rxBuf.slice(1); throw new CrcError(); } const fc = frame[1] & 127; if (fc === expectedFC) { return frame; } else { continue; } } } // ---------------------------------------------------------------- // Determine expected length; return 0 if we still need more bytes // ---------------------------------------------------------------- frameLengthIfComplete(buf) { if (buf.length < 3) return 0; const fc = buf[1]; if (fc & 128) return buf.length >= 5 ? 5 : 0; if (fc === 1 || fc === 2 || fc === 3 || fc === 4) { const need = 3 + buf[2] + 2; return buf.length >= need ? need : 0; } if (fc === 5 || fc === 6 || fc === 15 || fc === 16) return buf.length >= 8 ? 8 : 0; return buf.length >= 8 ? 8 : 0; } // -- close port -- async close() { var _a, _b, _c; await ((_a = this.reader) == null ? void 0 : _a.cancel()); await ((_b = this.writer) == null ? void 0 : _b.close()); await ((_c = this.port) == null ? void 0 : _c.close()); } }; function concat(a, b) { const out = new Uint8Array(a.length + b.length); out.set(a, 0); out.set(b, a.length); return out; } // src/core/types.ts var FC_READ_HOLDING_REGISTERS = 3; var FC_WRITE_SINGLE_HOLDING_REGISTER = 6; var FC_WRITE_MULTIPLE_HOLDING_REGISTERS = 16; var FC_READ_COILS = 1; var FC_WRITE_SINGLE_COIL = 5; var FC_WRITE_MULTIPLE_COILS = 15; var FC_READ_INPUT_REGISTERS = 4; var FC_READ_DISCRETE_INPUTS = 2; var FC_READ_FILE_RECORD = 20; var FC_WRITE_FILE_RECORD = 21; var FC_MASK_WRITE_REGISTER = 22; var FC_READ_WRITE_MULTIPLE_REGISTERS = 23; var FC_READ_FIFO_QUEUE = 24; // src/core/frames.ts function buildReadHolding(id, addr, len) { const frame = new Uint8Array(8); frame[0] = id; frame[1] = FC_READ_HOLDING_REGISTERS; frame[2] = addr >> 8; frame[3] = addr & 255; frame[4] = len >> 8; frame[5] = len & 255; const crc = crc16(frame.subarray(0, 6)); frame[6] = crc & 255; frame[7] = crc >> 8; return frame; } function buildWriteSingle(id, addr, value) { const frame = new Uint8Array(8); frame[0] = id; frame[1] = FC_WRITE_SINGLE_HOLDING_REGISTER; frame[2] = addr >> 8; frame[3] = addr & 255; frame[4] = value >> 8; frame[5] = value & 255; const crc = crc16(frame.subarray(0, 6)); frame[6] = crc & 255; frame[7] = crc >> 8; return frame; } function buildWriteMultiple(id, addr, values) { const qty = values.length; if (qty < 1 || qty > 123) throw new Error("Invalid number of registers"); const byteCount = qty * 2; const frame = new Uint8Array(7 + byteCount + 2); frame[0] = id; frame[1] = FC_WRITE_MULTIPLE_HOLDING_REGISTERS; frame[2] = addr >> 8; frame[3] = addr & 255; frame[4] = qty >> 8; frame[5] = qty & 255; frame[6] = byteCount; for (let i = 0; i < qty; i++) { const val = values[i] & 65535; frame[7 + i * 2] = val >> 8; frame[7 + i * 2 + 1] = val & 255; } const crc = crc16(frame.subarray(0, frame.length - 2)); frame[frame.length - 2] = crc & 255; frame[frame.length - 1] = crc >> 8; return frame; } function buildReadCoils(id, addr, qty) { if (qty < 1 || qty > 2e3) throw new Error("Invalid coil quantity"); const frame = new Uint8Array(8); frame[0] = id; frame[1] = FC_READ_COILS; frame[2] = addr >> 8; frame[3] = addr & 255; frame[4] = qty >> 8; frame[5] = qty & 255; const crc = crc16(frame.subarray(0, 6)); frame[6] = crc & 255; frame[7] = crc >> 8; return frame; } function buildWriteSingleCoil(id, addr, value) { const frame = new Uint8Array(8); frame[0] = id; frame[1] = FC_WRITE_SINGLE_COIL; frame[2] = addr >> 8; frame[3] = addr & 255; frame[4] = value ? 255 : 0; frame[5] = 0; const crc = crc16(frame.subarray(0, 6)); frame[6] = crc & 255; frame[7] = crc >> 8; return frame; } function buildWriteMultipleCoils(id, addr, values) { const qty = values.length; if (qty < 1 || qty > 1968) throw new Error("Invalid coil quantity"); const byteCount = Math.ceil(qty / 8); const frame = new Uint8Array(7 + byteCount + 2); frame[0] = id; frame[1] = FC_WRITE_MULTIPLE_COILS; frame[2] = addr >> 8; frame[3] = addr & 255; frame[4] = qty >> 8; frame[5] = qty & 255; frame[6] = byteCount; for (let i = 0; i < qty; i++) { if (values[i]) { const byteIndex = i >> 3; const bitIndex = i & 7; frame[7 + byteIndex] |= 1 << bitIndex; } } const crc = crc16(frame.subarray(0, frame.length - 2)); frame[frame.length - 2] = crc & 255; frame[frame.length - 1] = crc >> 8; return frame; } function buildReadInputRegisters(id, addr, qty) { if (qty < 1 || qty > 125) throw new Error("Invalid register quantity"); const frame = new Uint8Array(8); frame[0] = id; frame[1] = FC_READ_INPUT_REGISTERS; frame[2] = addr >> 8; frame[3] = addr & 255; frame[4] = qty >> 8; frame[5] = qty & 255; const crc = crc16(frame.subarray(0, 6)); frame[6] = crc & 255; frame[7] = crc >> 8; return frame; } function buildReadDiscreteInputs(id, addr, qty) { if (qty < 1 || qty > 2e3) throw new Error("Invalid discrete-input quantity"); const frame = new Uint8Array(8); frame[0] = id; frame[1] = FC_READ_DISCRETE_INPUTS; frame[2] = addr >> 8; frame[3] = addr & 255; frame[4] = qty >> 8; frame[5] = qty & 255; const crc = crc16(frame.subarray(0, 6)); frame[6] = crc & 255; frame[7] = crc >> 8; return frame; } function buildMaskWriteRegister(id, addr, andMask, orMask) { const frame = new Uint8Array(10); frame[0] = id; frame[1] = FC_MASK_WRITE_REGISTER; frame[2] = addr >> 8; frame[3] = addr & 255; frame[4] = andMask >> 8; frame[5] = andMask & 255; frame[6] = orMask >> 8; frame[7] = orMask & 255; const crc = crc16(frame.subarray(0, 8)); frame[8] = crc & 255; frame[9] = crc >> 8; return frame; } function buildReadWriteMultiple(id, readAddr, readQty, writeAddr, values) { const writeQty = values.length; if (readQty < 1 || readQty > 125) throw new Error("Invalid read quantity"); if (writeQty < 1 || writeQty > 121) throw new Error("Invalid write quantity"); const byteCount = writeQty * 2; const frame = new Uint8Array(11 + byteCount + 2); frame[0] = id; frame[1] = FC_READ_WRITE_MULTIPLE_REGISTERS; frame[2] = readAddr >> 8; frame[3] = readAddr & 255; frame[4] = readQty >> 8; frame[5] = readQty & 255; frame[6] = writeAddr >> 8; frame[7] = writeAddr & 255; frame[8] = writeQty >> 8; frame[9] = writeQty & 255; frame[10] = byteCount; for (let i = 0; i < writeQty; i++) { const v = values[i] & 65535; frame[11 + i * 2] = v >> 8; frame[12 + i * 2] = v & 255; } const crc = crc16(frame.subarray(0, frame.length - 2)); frame[frame.length - 2] = crc & 255; frame[frame.length - 1] = crc >> 8; return frame; } function buildReadFileRecord(id, file, record, length) { if (length < 1 || length > 120) throw new Error("Invalid record length"); const frame = new Uint8Array(12); frame[0] = id; frame[1] = FC_READ_FILE_RECORD; frame[2] = 7; frame[3] = 6; frame[4] = file >> 8; frame[5] = file & 255; frame[6] = record >> 8; frame[7] = record & 255; frame[8] = length >> 8; frame[9] = length & 255; const crc = crc16(frame.subarray(0, 10)); frame[10] = crc & 255; frame[11] = crc >> 8; return frame; } function buildWriteFileRecord(id, file, record, values) { const len = values.length; if (len < 1 || len > 120) throw new Error("Invalid record length"); const byteCount = 7 + len * 2; const frame = new Uint8Array(3 + byteCount + 2); frame[0] = id; frame[1] = FC_WRITE_FILE_RECORD; frame[2] = byteCount; frame[3] = 6; frame[4] = file >> 8; frame[5] = file & 255; frame[6] = record >> 8; frame[7] = record & 255; frame[8] = len >> 8; frame[9] = len & 255; for (let i = 0; i < len; i++) { const v = values[i] & 65535; frame[10 + i * 2] = v >> 8; frame[11 + i * 2] = v & 255; } const crc = crc16(frame.subarray(0, frame.length - 2)); frame[frame.length - 2] = crc & 255; frame[frame.length - 1] = crc >> 8; return frame; } function buildReadFifoQueue(id, addr) { const frame = new Uint8Array(6); frame[0] = id; frame[1] = FC_READ_FIFO_QUEUE; frame[2] = addr >> 8; frame[3] = addr & 255; const crc = crc16(frame.subarray(0, 4)); frame[4] = crc & 255; frame[5] = crc >> 8; return frame; } function parseReadHolding(resp) { basicChecks(resp, FC_READ_HOLDING_REGISTERS); const byteCount = resp[2]; const words = []; for (let i = 0; i < byteCount; i += 2) { words.push(resp[3 + i] << 8 | resp[4 + i]); } return words; } function parseWriteSingle(resp) { basicChecks(resp, FC_WRITE_SINGLE_HOLDING_REGISTER); const addr = resp[2] << 8 | resp[3]; const value = resp[4] << 8 | resp[5]; return { address: addr, value }; } var parseReadCoils = _parseBits(FC_READ_COILS); var parseReadDiscreteInputs = _parseBits(FC_READ_DISCRETE_INPUTS); function parseReadInputRegisters(resp) { basicChecks(resp, FC_READ_INPUT_REGISTERS); const byteCount = resp[2]; const words = []; for (let i = 0; i < byteCount; i += 2) { const hi = resp[3 + i], lo = resp[4 + i]; words.push(hi << 8 | lo); } return words; } function parseWriteSingleCoil(resp) { basicChecks(resp, FC_WRITE_SINGLE_COIL); const addr = resp[2] << 8 | resp[3]; const val = resp[4] << 8 | resp[5]; return { address: addr, state: val === 65280 }; } function parseMaskWriteRegister(resp) { basicChecks(resp, FC_MASK_WRITE_REGISTER); const addr = resp[2] << 8 | resp[3]; const andMask = resp[4] << 8 | resp[5]; const orMask = resp[6] << 8 | resp[7]; return { address: addr, andMask, orMask }; } function parseReadWriteMultiple(resp) { basicChecks(resp, FC_READ_WRITE_MULTIPLE_REGISTERS); const byteCount = resp[2]; const words = []; for (let i = 0; i < byteCount; i += 2) { words.push(resp[3 + i] << 8 | resp[4 + i]); } return words; } function parseReadFileRecord(resp) { basicChecks(resp, FC_READ_FILE_RECORD); const dataLen = resp[3] - 1; const words = []; for (let i = 0; i < dataLen; i += 2) { words.push(resp[5 + i] << 8 | resp[6 + i]); } return words; } function parseReadFifoQueue(resp) { basicChecks(resp, FC_READ_FIFO_QUEUE); const count = resp[4] << 8 | resp[5]; const words = []; for (let i = 0; i < count; i++) { const hi = resp[6 + i * 2]; const lo = resp[7 + i * 2]; words.push(hi << 8 | lo); } return words; } function _parseBits(expectedFC) { return (_frame) => { basicChecks(_frame, expectedFC); const byteCount = _frame[2]; const bits = []; for (let i = 0; i < byteCount; i++) { const byte = _frame[3 + i]; for (let j = 0; j < 8; j++) bits.push(Boolean(byte & 1 << j)); } return bits; }; } function basicChecks(frame, expectedFC) { const crcExpected = crc16(frame.subarray(0, frame.length - 2)); const crcGot = frame[frame.length - 2] | frame[frame.length - 1] << 8; if (crcExpected !== crcGot) throw new CrcError(); const fc = frame[1] & 127; const isException = (frame[1] & 128) !== 0; if (isException) throw new ExceptionError(frame[2]); if (fc !== expectedFC) throw new Error("Unexpected function code"); } // src/client.ts var ModbusRTU = class _ModbusRTU { constructor() { this.id = 1; } /* ---------- static factory ---------- */ static async openWebSerial(opts) { const cli = new _ModbusRTU(); cli.transport = await WebSerialTransport.open(opts); return cli; } /* ---------- housekeeping ---------- */ async close() { await this.transport.close(); } isOpen() { return !!this.transport; } /* ---------- config ---------- */ setID(id) { this.id = id; } getID() { return this.id; } setTimeout(ms) { this.transport.setTimeout(ms); } getTimeout() { return this.transport.getTimeout(); } getPort() { return this.transport.getPort(); } /* ========================= READ =============================== */ /** FC 01 – coils */ async readCoils(addr, qty) { const raw = await this.transport.transact(buildReadCoils(this.id, addr, qty)); return { data: parseReadCoils(raw), raw }; } /** FC 02 – discrete inputs */ async readDiscreteInputs(addr, qty) { const raw = await this.transport.transact( buildReadDiscreteInputs(this.id, addr, qty) ); const full = parseReadDiscreteInputs(raw); return { data: full.slice(0, qty), raw }; } /** FC 03 – holding registers (already existed) */ async readHoldingRegisters(addr, qty) { const raw = await this.transport.transact(buildReadHolding(this.id, addr, qty)); return { data: parseReadHolding(raw), raw }; } /** FC 04 – input registers */ async readInputRegisters(addr, qty) { const raw = await this.transport.transact(buildReadInputRegisters(this.id, addr, qty)); return { data: parseReadInputRegisters(raw), raw }; } /* ========================= WRITE ============================== */ /** FC 05 – single coil */ async writeCoil(addr, state) { const raw = await this.transport.transact(buildWriteSingleCoil(this.id, addr, state)); const { address, state: s } = parseWriteSingleCoil(raw); return { address, state: s, raw }; } /** FC 0F – multiple coils */ async writeCoils(addr, states) { const raw = await this.transport.transact(buildWriteMultipleCoils(this.id, addr, states)); const length = states.length; return { address: addr, length, raw }; } /** FC 06 – single holding register (already existed) */ async writeRegister(addr, value) { const raw = await this.transport.transact(buildWriteSingle(this.id, addr, value)); const { address, value: v } = parseWriteSingle(raw); return { address, value: v, raw }; } /** FC 16 – multiple holding registers */ async writeRegisters(addr, values) { const raw = await this.transport.transact(buildWriteMultiple(this.id, addr, values)); const length = values.length; return { address: addr, length, raw }; } /* ================== OPTIONAL DATA-ACCESS FUNCTIONS ================== */ /** FC 22 – mask write register */ async maskWriteRegister(addr, andMask, orMask) { const raw = await this.transport.transact(buildMaskWriteRegister(this.id, addr, andMask, orMask)); const { address, andMask: a, orMask: o } = parseMaskWriteRegister(raw); return { address, andMask: a, orMask: o, raw }; } /** FC 23 – read/write multiple registers */ async readWriteRegisters(readAddr, readQty, writeAddr, values) { const raw = await this.transport.transact( buildReadWriteMultiple(this.id, readAddr, readQty, writeAddr, values) ); return { data: parseReadWriteMultiple(raw), raw }; } /** FC 20 – read file record (single reference) */ async readFileRecord(file, record, length) { const raw = await this.transport.transact( buildReadFileRecord(this.id, file, record, length) ); return { data: parseReadFileRecord(raw), raw }; } /** FC 21 – write file record (single reference) */ async writeFileRecord(file, record, values) { const raw = await this.transport.transact( buildWriteFileRecord(this.id, file, record, values) ); return { file, record, length: values.length, raw }; } /** FC 24 – read FIFO queue */ async readFifoQueue(addr) { const raw = await this.transport.transact( buildReadFifoQueue(this.id, addr) ); return { data: parseReadFifoQueue(raw), raw }; } }; export { CrcError, ExceptionError, ModbusRTU, TimeoutError, buildMaskWriteRegister, buildReadCoils, buildReadDiscreteInputs, buildReadFifoQueue, buildReadFileRecord, buildReadHolding, buildReadInputRegisters, buildReadWriteMultiple, buildWriteFileRecord, buildWriteMultiple, buildWriteMultipleCoils, buildWriteSingle, buildWriteSingleCoil, crc16, parseMaskWriteRegister, parseReadCoils, parseReadDiscreteInputs, parseReadFifoQueue, parseReadFileRecord, parseReadHolding, parseReadInputRegisters, parseReadWriteMultiple, parseWriteSingle, parseWriteSingleCoil };