UNPKG

bt-seneca-msc

Version:

A pure Javascript API for the Seneca Multi Smart Calibrator (MSC) device, using web bluetooth.

265 lines (224 loc) 7.29 kB
"use strict"; /******************************** MODBUS RTU handling ***********************************************/ var log = require("loglevel"); const SENECA_MB_SLAVE_ID = 25; // Modbus RTU slave ID class ModbusError extends Error { /** * Creates a new modbus error * @param {String} message message * @param {number} fc function code */ constructor(message, fc) { super(message); this.message = message; this.fc = fc; } } /** * Returns the 4 bytes CRC code from the buffer contents * @param {ArrayBuffer} buffer */ function crc16(buffer) { var crc = 0xFFFF; var odd; for (var i = 0; i < buffer.length; i++) { crc = crc ^ buffer[i]; for (var j = 0; j < 8; j++) { odd = crc & 0x0001; crc = crc >> 1; if (odd) { crc = crc ^ 0xA001; } } } return crc; } /** * Make a Modbus Read Holding Registers (FC=03) to serial port * * @param {number} ID slave ID * @param {number} count number of registers to read * @param {number} register starting register */ function makeFC3(ID, count, register) { const buffer = new ArrayBuffer(8); const view = new DataView(buffer); view.setUint8(0, ID); view.setUint8(1, 3); view.setUint16(2, register, false); view.setUint16(4, count, false); var crc = crc16(new Uint8Array(buffer.slice(0, -2))); view.setUint16(6, crc, true); return buffer; } /** * Write a Modbus "Preset Multiple Registers" (FC=16) to serial port. * * @param {number} address the slave unit address. * @param {number} dataAddress the Data Address of the first register. * @param {Array} array the array of values to write to registers. */ function makeFC16(address, dataAddress, array) { const code = 16; // sanity check if (typeof address === "undefined" || typeof dataAddress === "undefined") { return; } let dataLength = array.length; const codeLength = 7 + 2 * dataLength; const buf = new ArrayBuffer(codeLength + 2); // add 2 crc bytes const dv = new DataView(buf); dv.setUint8(0, address); dv.setUint8(1, code); dv.setUint16(2, dataAddress, false); dv.setUint16(4, dataLength, false); dv.setUint8(6, dataLength * 2); // copy content of array to buf for (let i = 0; i < dataLength; i++) { dv.setUint16(7 + 2 * i, array[i], false); } const crc = crc16(new Uint8Array(buf.slice(0, -2))); // add crc bytes to buffer dv.setUint16(codeLength, crc, true); return buf; } /** * Returns the registers values from a FC03 answer by RTU slave * * @param {ArrayBuffer} response */ function parseFC3(response) { if (!(response instanceof ArrayBuffer)) { return null; } const view = new DataView(response); // Invalid modbus packet if (response.length < 5) return; var computed_crc = crc16(new Uint8Array(response.slice(0, -2))); var actual_crc = view.getUint16(view.byteLength - 2, true); if (computed_crc != actual_crc) { throw new ModbusError("Wrong CRC (expected:" + computed_crc + ",got:" + actual_crc + ")", 3); } var address = view.getUint8(0); if (address != SENECA_MB_SLAVE_ID) { throw new ModbusError("Wrong slave ID :" + address, 3); } var fc = view.getUint8(1); if (fc > 128) { var exp = view.getUint8(2); throw new ModbusError("Exception by slave:" + exp, 3); } if (fc != 3) { throw new ModbusError("Wrong FC :" + fc, fc); } // Length in bytes from slave answer var length = view.getUint8(2); const buffer = new ArrayBuffer(length); const registers = new DataView(buffer); for (var i = 3; i < view.byteLength - 2; i += 2) { var reg = view.getInt16(i, false); registers.setInt16(i - 3, reg, false); var idx = ((i - 3) / 2 + 1); log.debug("\t\tRegister " + idx + "/" + (length / 2) + " = " + reg); } return registers; } /** * Check if the FC16 response is correct (CRC, return code) AND optionally matching the register length expected * @param {ArrayBuffer} response modbus rtu raw output * @param {number} expected number of expected written registers from slave. If <=0, it will not be checked. * @returns {boolean} true if all registers have been written */ function parseFC16checked(response, expected) { try { const result = parseFC16(response); return (expected <= 0 || result[1] === expected); // check if length is matching } catch (err) { log.error("FC16 answer error", err); return false; } } /** * Parse the answer to the write multiple registers from the slave * @param {ArrayBuffer} response */ function parseFC16(response) { const view = new DataView(response); if (response.length < 3) return; var slave = view.getUint8(0); if (slave != SENECA_MB_SLAVE_ID) { return; } var fc = view.getUint8(1); if (fc > 128) { var exp = view.getUint8(2); throw new ModbusError("Exception :" + exp, 16); } if (fc != 16) { throw new ModbusError("Wrong FC :" + fc, fc); } var computed_crc = crc16(new Uint8Array(response.slice(0, -2))); var actual_crc = view.getUint16(view.byteLength - 2, true); if (computed_crc != actual_crc) { throw new ModbusError("Wrong CRC (expected:" + computed_crc + ",got:" + actual_crc + ")", 16); } var address = view.getUint16(2, false); var length = view.getUint16(4, false); return [address, length]; } /** * Converts with byte swap AB CD -> CD AB -> float * @param {DataView} dataView buffer view to process * @param {number} offset byte number where float into the buffer * @returns {number} converted value */ function getFloat32LEBS(dataView, offset) { const buff = new ArrayBuffer(4); const dv = new DataView(buff); dv.setInt16(0, dataView.getInt16(offset + 2, false), false); dv.setInt16(2, dataView.getInt16(offset, false), false); return dv.getFloat32(0, false); } /** * Converts with byte swap AB CD -> CD AB -> Uint32 * @param {DataView} dataView buffer view to process * @param {number} offset byte number where float into the buffer * @returns {number} converted value */ function getUint32LEBS(dataView, offset) { const buff = new ArrayBuffer(4); const dv = new DataView(buff); dv.setInt16(0, dataView.getInt16(offset + 2, false), false); dv.setInt16(2, dataView.getInt16(offset, false), false); return dv.getUint32(0, false); } /** * Converts with byte swap AB CD -> CD AB -> float * @param {DataView} dataView buffer view to process * @param {number} offset byte number where float into the buffer * @param {value} number value to set */ function setFloat32LEBS(dataView, offset, value) { const buff = new ArrayBuffer(4); const dv = new DataView(buff); dv.setFloat32(0, value, false); dataView.setInt16(offset, dv.getInt16(2, false), false); dataView.setInt16(offset + 2, dv.getInt16(0, false), false); } /** * Converts with byte swap AB CD -> CD AB * @param {DataView} dataView buffer view to process * @param {number} offset byte number where uint32 into the buffer * @param {number} value value to set */ function setUint32LEBS(dataView, offset, value) { const buff = new ArrayBuffer(4); const dv = new DataView(buff); dv.setUint32(0, value, false); dataView.setInt16(offset, dv.getInt16(2, false), false); dataView.setInt16(offset + 2, dv.getInt16(0, false), false); } module.exports = { makeFC3, getFloat32LEBS, makeFC16, setFloat32LEBS, setUint32LEBS, parseFC3, parseFC16, parseFC16checked, ModbusError, SENECA_MB_SLAVE_ID, getUint32LEBS, crc16 };