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
JavaScript
"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 };