modbus-webserial
Version:
Tiny TypeScript library for speaking Modbus-RTU from the browser via Web Serial
628 lines (622 loc) • 19.5 kB
JavaScript
// 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
};