bt-seneca-msc
Version:
A pure Javascript API for the Seneca Multi Smart Calibrator (MSC) device, using web bluetooth.
1,747 lines (1,554 loc) • 404 kB
JavaScript
(function(f){if(typeof exports==="object"&&typeof module!=="undefined"){module.exports=f()}else if(typeof define==="function"&&define.amd){define([],f)}else{var g;if(typeof window!=="undefined"){g=window}else if(typeof global!=="undefined"){g=global}else if(typeof self!=="undefined"){g=self}else{g=this}g.MSC = f()}})(function(){var define,module,exports;return (function(){function r(e,n,t){function o(i,f){if(!n[i]){if(!e[i]){var c="function"==typeof require&&require;if(!f&&c)return c(i,!0);if(u)return u(i,!0);var a=new Error("Cannot find module '"+i+"'");throw a.code="MODULE_NOT_FOUND",a}var p=n[i]={exports:{}};e[i][0].call(p.exports,function(r){var n=e[i][1][r];return o(n||r)},p,p.exports,r,e,n,t)}return n[i].exports}for(var u="function"==typeof require&&require,i=0;i<t.length;i++)o(t[i]);return o}return r})()({1:[function(require,module,exports){
"use strict";
/**
* Bluetooth handling module, including main state machine loop.
* This module interacts with browser for bluetooth comunications and pairing, and with SenecaMSC object.
*/
var APIState = require("./classes/APIState");
var log = require("loglevel");
var constants = require("./constants");
var utils = require("./utils");
var senecaModule = require("./classes/SenecaMSC");
var modbus = require("./modbusRtu");
var testData = require("./modbusTestData");
var btState = APIState.btState;
var State = constants.State;
var CommandType = constants.CommandType;
var ResultCode = constants.ResultCode;
var simulation = false;
var logging = false;
/*
* Bluetooth constants
*/
const BlueToothMSC = {
ServiceUuid: "0003cdd0-0000-1000-8000-00805f9b0131", // bluetooth modbus RTU service for Seneca MSC
ModbusAnswerUuid: "0003cdd1-0000-1000-8000-00805f9b0131", // modbus RTU answers
ModbusRequestUuid: "0003cdd2-0000-1000-8000-00805f9b0131" // modbus RTU requests
};
/**
* Send the message using Bluetooth and wait for an answer
* @param {ArrayBuffer} command modbus RTU packet to send
* @returns {ArrayBuffer} the modbus RTU answer
*/
async function SendAndResponse(command) {
if (command == null)
return null;
log.debug(">> " + utils.buf2hex(command));
btState.response = null;
btState.stats["requests"]++;
var startTime = new Date().getTime();
if (simulation) {
btState.response = fakeResponse(command);
await utils.sleep(5);
}
else {
await btState.charWrite.writeValueWithoutResponse(command);
while (btState.state == State.METER_INITIALIZING ||
btState.state == State.BUSY) {
if (btState.response != null) break;
await new Promise(resolve => setTimeout(resolve, 35));
}
}
var endTime = new Date().getTime();
var answer = btState.response?.slice();
btState.response = null;
// Log the packets
if (logging) {
var packet = { "request": utils.buf2hex(command), "answer": utils.buf2hex(answer) };
var packets = window.localStorage.getItem("ModbusRTUtrace");
if (packets == null) {
packets = []; // initialize array
}
else {
packets = JSON.parse(packets); // Restore the json persisted object
}
packets.push(packet); // Add the new object
window.localStorage.setItem("ModbusRTUtrace", JSON.stringify(packets));
}
btState.stats["responseTime"] = Math.round((1.0 * btState.stats["responseTime"] * (btState.stats["responses"] % 500) + (endTime - startTime)) / ((btState.stats["responses"] % 500) + 1));
btState.stats["lastResponseTime"] = Math.round(endTime - startTime) + " ms";
btState.stats["responses"]++;
return answer;
}
let senecaMSC = new senecaModule.SenecaMSC(SendAndResponse);
/**
* Main loop of the meter handler.
* */
async function stateMachine() {
var nextAction;
var DELAY_MS = (simulation ? 20 : 750); // Update the status every X ms.
var TIMEOUT_MS = (simulation ? 1000 : 30000); // Give up some operations after X ms.
btState.started = true;
log.debug("Current state:" + btState.state);
// Consecutive state counted. Can be used to timeout.
if (btState.state == btState.prev_state) {
btState.state_cpt++;
} else {
btState.state_cpt = 0;
}
// Stop request from API
if (btState.stopRequest) {
btState.state = State.STOPPING;
}
log.debug("State:" + btState.state);
switch (btState.state) {
case State.NOT_CONNECTED: // initial state on Start()
if (simulation) {
nextAction = fakePairDevice;
} else {
nextAction = btPairDevice;
}
break;
case State.CONNECTING: // waiting for connection to complete
nextAction = undefined;
break;
case State.DEVICE_PAIRED: // connection complete, acquire meter state
if (simulation) {
nextAction = fakeSubscribe;
} else {
nextAction = btSubscribe;
}
break;
case State.SUBSCRIBING: // waiting for Bluetooth interfaces
nextAction = undefined;
if (btState.state_cpt > Math.floor(TIMEOUT_MS / DELAY_MS)) {
// Timeout, try to resubscribe
log.warn("Timeout in SUBSCRIBING");
btState.state = State.DEVICE_PAIRED;
btState.state_cpt = 0;
}
break;
case State.METER_INIT: // ready to communicate, acquire meter status
nextAction = meterInit;
break;
case State.METER_INITIALIZING: // reading the meter status
if (btState.state_cpt > Math.floor(TIMEOUT_MS / DELAY_MS)) {
log.warn("Timeout in METER_INITIALIZING");
// Timeout, try to resubscribe
if (simulation) {
nextAction = fakeSubscribe;
} else {
nextAction = btSubscribe;
}
btState.state_cpt = 0;
}
nextAction = undefined;
break;
case State.IDLE: // ready to process commands from API
if (btState.command != null)
nextAction = processCommand;
else {
nextAction = refresh;
}
break;
case State.ERROR: // anytime an error happens
nextAction = disconnect;
break;
case State.BUSY: // while a command in going on
if (btState.state_cpt > Math.floor(TIMEOUT_MS / DELAY_MS)) {
log.warn("Timeout in BUSY");
// Timeout, try to resubscribe
if (simulation) {
nextAction = fakeSubscribe;
} else {
nextAction = btSubscribe;
}
btState.state_cpt = 0;
}
nextAction = undefined;
break;
case State.STOPPING:
nextAction = disconnect;
break;
case State.STOPPED: // after a disconnector or Stop() request, stops the state machine.
nextAction = undefined;
break;
default:
break;
}
btState.prev_state = btState.state;
if (nextAction != undefined) {
log.debug("\tExecuting:" + nextAction.name);
try {
await nextAction();
}
catch (e) {
log.error("Exception in state machine", e);
}
}
if (btState.state != State.STOPPED) {
utils.sleep(DELAY_MS).then(() => stateMachine()).catch((err) => {
log.error("State machine error:", err);
btState.state = State.ERROR;
}); // Recheck status in DELAY_MS ms
}
else {
log.debug("\tTerminating State machine");
btState.started = false;
}
}
/**
* Called from state machine to execute a single command from btState.command property
* */
async function processCommand() {
try {
var command = btState.command;
var result;
if (command == null) {
return;
}
btState.state = State.BUSY;
btState.stats["commands"]++;
log.info("\t\tExecuting command :" + command);
// First set NONE because we don't want to write new setpoints with active generation
result = await senecaMSC.switchOff();
if (result != ResultCode.SUCCESS) {
throw new Error("Cannot switch meter off before command write!");
}
// Now write the setpoint or setting
if (utils.isGeneration(command.type) || utils.isSetting(command.type) && command.type != CommandType.OFF) {
result = await senecaMSC.writeSetpoints(command.type, command.setpoint, command.setpoint2);
if (result != ResultCode.SUCCESS) {
throw new Error("Failure to write setpoints!");
}
}
if (!utils.isSetting(command.type) &&
utils.isValid(command.type) && command.type != CommandType.OFF) // IF this is a setting, we're done.
{
// Now write the mode set
result = await senecaMSC.changeMode(command.type);
if (result != ResultCode.SUCCESS) {
throw new Error("Failure to change meter mode!");
}
}
// Caller expects a valid property in GetState() once command is executed.
log.debug("\t\tRefreshing current state");
await refresh();
command.error = false;
command.pending = false;
btState.command = null;
btState.state = State.IDLE;
log.debug("\t\tCompleted command executed");
}
catch (err) {
log.error("** error while executing command: " + err);
btState.state = State.METER_INIT;
btState.stats["exceptions"]++;
if (err instanceof modbus.ModbusError)
btState.stats["modbus_errors"]++;
return;
}
}
function getExpectedStateHex() {
// Simulate current mode answer according to last command.
var stateHex = (CommandType.OFF).toString(16);
if (btState.command?.type != null) {
stateHex = (btState.command.type).toString(16);
}
// Add trailing 0
while (stateHex.length < 2)
stateHex = "0" + stateHex;
return stateHex;
}
/**
* Used to simulate RTU answers
* @param {ArrayBuffer} command real request
* @returns {ArrayBuffer} fake answer
*/
function fakeResponse(command) {
var commandHex = utils.buf2hex(command);
var forgedAnswers = {
"19 03 00 64 00 01 c6 0d": "19 03 02 00" + getExpectedStateHex() + " $$$$", // Current state
"default 03": "19 03 06 0001 0001 0001 $$$$", // default answer for FC3
"default 10": "19 10 00 d4 00 02 0001 0001 $$$$"
}; // default answer for FC10
// Start with the default answer
var responseHex = forgedAnswers["default " + commandHex.split(" ")[1]];
// Do we have a forged answer?
if (forgedAnswers[commandHex] != undefined) {
responseHex = forgedAnswers[commandHex];
}
else {
// Look into registered traces
var found = [];
for (const trace of testData.testTraces) {
if (trace["request"] === commandHex) {
found.push(trace["answer"]);
}
}
if (found.length > 0) {
// Select a random answer from the registered trace
responseHex = found[Math.floor((Math.random() * found.length))];
}
else {
console.info(commandHex + " not found in test traces");
}
}
// Compute CRC if needed
if (responseHex.includes("$$$$")) {
responseHex = responseHex.replace("$$$$", "");
var crc = modbus.crc16(new Uint8Array(utils.hex2buf(responseHex))).toString(16);
while (crc.length < 4)
crc = "0" + crc;
responseHex = responseHex + crc.substring(2, 4) + crc.substring(0, 2);
}
log.debug("<< " + responseHex);
return utils.hex2buf(responseHex);
}
/**
* Acquire the current mode and serial number of the device.
* */
async function meterInit() {
try {
btState.state = State.METER_INITIALIZING;
btState.meter.serial = await senecaMSC.getSerialNumber();
log.info("\t\tSerial number:" + btState.meter.serial);
btState.meter.mode = await senecaMSC.getCurrentMode();
log.debug("\t\tCurrent mode:" + btState.meter.mode);
btState.meter.battery = await senecaMSC.getBatteryVoltage();
log.debug("\t\tBattery (V):" + btState.meter.battery);
btState.state = State.IDLE;
}
catch (err) {
log.warn("Error while initializing meter :" + err);
btState.stats["exceptions"]++;
btState.state = State.DEVICE_PAIRED;
if (err instanceof modbus.ModbusError)
btState.stats["modbus_errors"]++;
}
}
/*
* Close the bluetooth interface (unpair)
* */
async function disconnect() {
btState.command = null;
try {
if (btState.btDevice != null) {
if (btState.btDevice?.gatt?.connected) {
log.warn("* Calling disconnect on btdevice");
// Avoid the event firing which may lead to auto-reconnect
btState.btDevice.removeEventListener("gattserverdisconnected", onDisconnected);
btState.btDevice.gatt.disconnect();
}
}
btState.btService = null;
}
catch { }
btState.state = State.STOPPED;
}
/**
* Event called by browser BT api when the device disconnect
* */
async function onDisconnected() {
log.warn("* GATT Server disconnected event, will try to reconnect *");
btState.btService = null;
btState.stats["GATT disconnects"]++;
btState.state = State.DEVICE_PAIRED; // Try to auto-reconnect the interfaces without pairing
}
/**
* Joins the arguments into a single buffer
* @returns {Buffer} concatenated buffer
*/
function arrayBufferConcat() {
var length = 0;
var buffer = null;
for (var i in arguments) {
buffer = arguments[i];
length += buffer.byteLength;
}
var joined = new Uint8Array(length);
var offset = 0;
for (i in arguments) {
buffer = arguments[i];
joined.set(new Uint8Array(buffer), offset);
offset += buffer.byteLength;
}
return joined.buffer;
}
/**
* Event called by bluetooth characteristics when receiving data
* @param {any} event
*/
function handleNotifications(event) {
let value = event.target.value;
if (value != null) {
log.debug("<< " + utils.buf2hex(value.buffer));
if (btState.response != null) {
// Prevent memory leak by limiting maximum response buffer size
const MAX_RESPONSE_SIZE = 1024; // 1KB limit for modbus responses
const newSize = btState.response.byteLength + value.buffer.byteLength;
if (newSize > MAX_RESPONSE_SIZE) {
log.warn("Response buffer too large, resetting");
btState.response = value.buffer.slice();
} else {
btState.response = arrayBufferConcat(btState.response, value.buffer);
}
} else {
btState.response = value.buffer.slice();
}
}
}
/**
* This function will succeed only if called as a consequence of a user-gesture
* E.g. button click. This is due to BlueTooth API security model.
* */
async function btPairDevice() {
btState.state = State.CONNECTING;
var forceSelection = btState.options["forceDeviceSelection"];
log.debug("btPairDevice(" + forceSelection + ")");
try {
if (typeof (navigator.bluetooth?.getAvailability) == "function") {
const availability = await navigator.bluetooth.getAvailability();
if (!availability) {
log.error("Bluetooth not available in browser.");
throw new Error("Browser does not provide bluetooth");
}
}
var device = null;
// Do we already have permission?
if (typeof (navigator.bluetooth?.getDevices) == "function"
&& !forceSelection) {
const availableDevices = await navigator.bluetooth.getDevices();
availableDevices.forEach(function (dev, index) {
log.debug("Found authorized device :" + dev.name);
if (dev.name.startsWith("MSC"))
device = dev;
});
log.debug("navigator.bluetooth.getDevices()=" + device);
}
// If not, request from user
if (device == null) {
device = await navigator.bluetooth
.requestDevice({
acceptAllDevices: false,
filters: [{ namePrefix: "MSC" }],
optionalServices: [BlueToothMSC.ServiceUuid]
});
}
btState.btDevice = device;
btState.state = State.DEVICE_PAIRED;
log.info("Bluetooth device " + device.name + " connected.");
await utils.sleep(500);
}
catch (err) {
log.warn("** error while connecting: " + err.message);
btState.btService = null;
if (btState.charRead != null) {
try {
btState.charRead.stopNotifications();
} catch (error) { }
}
btState.charRead = null;
btState.charWrite = null;
btState.state = State.ERROR;
btState.stats["exceptions"]++;
}
}
async function fakePairDevice() {
btState.state = State.CONNECTING;
var forceSelection = btState.options["forceDeviceSelection"];
log.debug("fakePairDevice(" + forceSelection + ")");
try {
var device = { name: "FakeBTDevice", gatt: { connected: true } };
btState.btDevice = device;
btState.state = State.DEVICE_PAIRED;
log.info("Bluetooth device " + device.name + " connected.");
await utils.sleep(50);
}
catch (err) {
log.warn("** error while connecting: " + err.message);
btState.btService = null;
btState.charRead = null;
btState.charWrite = null;
btState.state = State.ERROR;
btState.stats["exceptions"]++;
}
}
/**
* Once the device is available, initialize the service and the 2 characteristics needed.
* */
async function btSubscribe() {
try {
btState.state = State.SUBSCRIBING;
btState.stats["subcribes"]++;
let device = btState.btDevice;
let server = null;
if (!device?.gatt?.connected) {
log.debug(`Connecting to GATT Server on ${device.name}...`);
device.addEventListener("gattserverdisconnected", onDisconnected);
try {
if (btState.btService?.connected) {
btState.btService.disconnect();
btState.btService = null;
await utils.sleep(100);
}
} catch (err) { }
server = await device.gatt.connect();
log.debug("> Found GATT server");
}
else {
log.debug("GATT already connected");
server = device.gatt;
}
btState.btService = await server.getPrimaryService(BlueToothMSC.ServiceUuid);
if (btState.btService == null)
throw new Error("GATT Service request failed");
log.debug("> Found Serial service");
btState.charWrite = await btState.btService.getCharacteristic(BlueToothMSC.ModbusRequestUuid);
log.debug("> Found write characteristic");
btState.charRead = await btState.btService.getCharacteristic(BlueToothMSC.ModbusAnswerUuid);
log.debug("> Found read characteristic");
btState.response = null;
btState.charRead.addEventListener("characteristicvaluechanged", handleNotifications);
btState.charRead.startNotifications();
log.info("> Bluetooth interfaces ready.");
btState.stats["last_connect"] = new Date().toISOString();
await utils.sleep(50);
btState.state = State.METER_INIT;
}
catch (err) {
log.warn("** error while subscribing: " + err.message);
if (btState.charRead != null) {
try {
if (btState.btDevice?.gatt?.connected) {
btState.charRead.stopNotifications();
}
btState.btDevice?.gatt.disconnect();
} catch (error) { }
}
btState.charRead = null;
btState.charWrite = null;
btState.state = State.DEVICE_PAIRED;
btState.stats["exceptions"]++;
}
}
async function fakeSubscribe() {
try {
btState.state = State.SUBSCRIBING;
btState.stats["subcribes"]++;
let device = btState.btDevice;
let server = null;
if (!device?.gatt?.connected) {
log.debug(`Connecting to GATT Server on ${device.name}...`);
device["gatt"]["connected"] = true;
log.debug("> Found GATT server");
}
else {
log.debug("GATT already connected");
server = device.gatt;
}
btState.btService = {};
log.debug("> Found Serial service");
btState.charWrite = {};
log.debug("> Found write characteristic");
btState.charRead = {};
log.debug("> Found read characteristic");
btState.response = null;
log.info("> Bluetooth interfaces ready.");
btState.stats["last_connect"] = new Date().toISOString();
await utils.sleep(10);
btState.state = State.METER_INIT;
}
catch (err) {
log.warn("** error while subscribing: " + err.message);
btState.charRead = null;
btState.charWrite = null;
btState.state = State.DEVICE_PAIRED;
btState.stats["exceptions"]++;
}
}
/**
* When idle, this function is called
* */
async function refresh() {
btState.state = State.BUSY;
try {
// Check the mode first
var mode = await senecaMSC.getCurrentMode();
if (mode != CommandType.NONE_UNKNOWN) {
btState.meter.mode = mode;
if (btState.meter.isGeneration()) {
var setpoints = await senecaMSC.getSetpoints(btState.meter.mode);
btState.lastSetpoint = setpoints;
}
if (btState.meter.isMeasurement()) {
var meas = await senecaMSC.getMeasures(btState.meter.mode);
btState.lastMeasure = meas;
}
}
// Refresh battery status regularly (every 10 refresh cycles to avoid excessive communication)
if (!btState.batteryRefreshCounter) {
btState.batteryRefreshCounter = 0;
}
btState.batteryRefreshCounter++;
if (btState.batteryRefreshCounter >= 10) {
btState.meter.battery = await senecaMSC.getBatteryVoltage();
log.debug("\t\tBattery refreshed: " + btState.meter.battery + "V");
btState.batteryRefreshCounter = 0;
}
log.debug("\t\tFinished refreshing current state");
btState.state = State.IDLE;
}
catch (err) {
log.warn("Error while refreshing measure" + err);
btState.state = State.DEVICE_PAIRED;
btState.stats["exceptions"]++;
if (err instanceof modbus.ModbusError)
btState.stats["modbus_errors"]++;
}
}
function SetSimulation(value) {
simulation = value;
}
module.exports = { stateMachine, SendAndResponse, SetSimulation };
},{"./classes/APIState":2,"./classes/SenecaMSC":6,"./constants":7,"./modbusRtu":10,"./modbusTestData":11,"./utils":14,"loglevel":12}],2:[function(require,module,exports){
var constants = require("../constants");
var MeterState = require("./MeterState");
// Current state of the bluetooth
class APIState {
constructor() {
this.state = constants.State.NOT_CONNECTED;
this.prev_state = constants.State.NOT_CONNECTED;
this.state_cpt = 0;
this.started = false; // State machine status
this.stopRequest = false; // To request disconnect
this.lastMeasure = {}; // Array with "MeasureName" : value
this.lastSetpoint = {}; // Array with "SetpointType" : value
// state of connected meter
this.meter = new MeterState();
// last modbus RTU command
this.command = null;
// last modbus RTU answer
this.response = null;
// bluetooth properties
this.charRead = null;
this.charWrite = null;
this.btService = null;
this.btDevice = null;
// emulated continuity checker
this.continuity = false;
// battery refresh counter for regular battery status updates
this.batteryRefreshCounter = 0;
// general statistics for debugging
this.stats = {
"requests": 0,
"responses": 0,
"modbus_errors": 0,
"GATT disconnects": 0,
"exceptions": 0,
"subcribes": 0,
"commands": 0,
"responseTime": 0.0,
"lastResponseTime": "",
"last_connect": new Date(2020, 1, 1).toISOString()
};
this.options = {
"forceDeviceSelection": true
};
}
}
let btState = new APIState();
module.exports = { APIState, btState };
},{"../constants":7,"./MeterState":5}],3:[function(require,module,exports){
var constants = require("../constants");
var utils = require("../utils");
var CommandType = constants.CommandType;
/**
* Command to the meter, may include setpoint
* */
class Command {
/**
* Creates a new command
* @param {CommandType} ctype
*/
constructor(ctype = CommandType.NONE_UNKNOWN) {
this.type = parseInt(ctype);
this.setpoint = null;
this.setpoint2 = null;
this.error = false;
this.pending = true;
this.request = null;
this.response = null;
}
static CreateNoSP(ctype) {
var cmd = new Command(ctype);
return cmd;
}
static CreateOneSP(ctype, setpoint) {
var cmd = new Command(ctype);
cmd.setpoint = parseFloat(setpoint);
return cmd;
}
static CreateTwoSP(ctype, set1, set2) {
var cmd = new Command(ctype);
cmd.setpoint = parseFloat(set1);
cmd.setpoint2 = parseFloat(set2);
return cmd;
}
toString() {
return "Type: " + utils.Parse(CommandType, this.type) + ", setpoint:" + this.setpoint + ", setpoint2: " + this.setpoint2 + ", pending:" + this.pending + ", error:" + this.error;
}
/**
* Gets the default setpoint for this command type
* @returns {Array} setpoint(s) expected
*/
defaultSetpoint() {
switch (this.type) {
case CommandType.GEN_THERMO_B:
case CommandType.GEN_THERMO_E:
case CommandType.GEN_THERMO_J:
case CommandType.GEN_THERMO_K:
case CommandType.GEN_THERMO_L:
case CommandType.GEN_THERMO_N:
case CommandType.GEN_THERMO_R:
case CommandType.GEN_THERMO_S:
case CommandType.GEN_THERMO_T:
case CommandType.GEN_Cu50_3W:
case CommandType.GEN_Cu50_2W:
case CommandType.GEN_Cu100_2W:
case CommandType.GEN_Ni100_2W:
case CommandType.GEN_Ni120_2W:
case CommandType.GEN_PT100_2W:
case CommandType.GEN_PT500_2W:
case CommandType.GEN_PT1000_2W:
return { "Temperature (°C)": 0.0 };
case CommandType.GEN_V:
return { "Voltage (V)": 0.0 };
case CommandType.GEN_mV:
return { "Voltage (mV)": 0.0 };
case CommandType.GEN_mA_active:
case CommandType.GEN_mA_passive:
return { "Current (mA)": 0.0 };
case CommandType.GEN_LoadCell:
return { "Imbalance (mV/V)": 0.0 };
case CommandType.GEN_Frequency:
return { "Frequency (Hz)": 0.0 };
case CommandType.GEN_PulseTrain:
return { "Pulses count": 0, "Frequency (Hz)": 0.0 };
case CommandType.SET_UThreshold_F:
return { "Uthreshold (V)": 2.0 };
case CommandType.SET_Sensitivity_uS:
return { "Sensibility (uS)": 2.0 };
case CommandType.SET_ColdJunction:
return { "Cold junction compensation": 0.0 };
case CommandType.SET_Ulow:
return { "U low (V)": 0.0 / constants.MAX_U_GEN };
case CommandType.SET_Uhigh:
return { "U high (V)": 5.0 / constants.MAX_U_GEN };
case CommandType.SET_ShutdownDelay:
return { "Delay (s)": 60 * 5 };
default:
return {};
}
}
isGeneration() {
return utils.isGeneration(this.type);
}
isMeasurement() {
return utils.isMeasurement(this.type);
}
isSetting() {
return utils.isSetting(this.type);
}
isValid() {
return (utils.isMeasurement(this.type) || utils.isGeneration(this.type) || utils.isSetting(this.type));
}
}
module.exports = Command;
},{"../constants":7,"../utils":14}],4:[function(require,module,exports){
class CommandResult {
value = 0.0;
success = false;
message = "";
unit = "";
secondary_value = 0.0;
secondary_unit = "";
}
module.exports = CommandResult;
},{}],5:[function(require,module,exports){
var constants = require("../constants");
/**
* Current state of the meter
* */
class MeterState {
constructor() {
this.firmware = ""; // Firmware version
this.serial = ""; // Serial number
this.mode = constants.CommandType.NONE_UNKNOWN;
this.battery = 0.0;
}
isMeasurement() {
return this.mode > constants.CommandType.NONE_UNKNOWN && this.mode < constants.CommandType.OFF;
}
isGeneration() {
return this.mode > constants.CommandType.OFF && this.mode < constants.CommandType.GEN_RESERVED;
}
}
module.exports = MeterState;
},{"../constants":7}],6:[function(require,module,exports){
"use strict";
/**
* This module contains the SenecaMSC object, which provides the main operations for bluetooth module.
* It uses the modbus helper functions from senecaModbus / modbusRtu to interact with the meter with SendAndResponse function
*/
var log = require("loglevel");
var utils = require("../utils");
var senecaMB = require("../senecaModbus");
var modbus = require("../modbusRtu");
var constants = require("../constants");
const { btState } = require("./APIState");
var CommandType = constants.CommandType;
var ResultCode = constants.ResultCode;
const RESET_POWER_OFF = 6;
const SET_POWER_OFF = 7;
const CLEAR_AVG_MIN_MAX = 5;
const PULSE_CMD = 9;
class SenecaMSC {
constructor(fnSendAndResponse) {
this.SendAndResponse = fnSendAndResponse;
}
/**
* Gets the meter serial number (12345_1234)
* May throw ModbusError
* @returns {string}
*/
async getSerialNumber() {
log.debug("\t\tReading serial number");
var response = await this.SendAndResponse(senecaMB.makeSerialNumber());
return senecaMB.parseSerialNumber(response);
}
/**
* Gets the current mode set on the MSC device
* May throw ModbusError
* @returns {CommandType} active mode
*/
async getCurrentMode() {
log.debug("\t\tReading current mode");
var response = await this.SendAndResponse(senecaMB.makeCurrentMode());
return senecaMB.parseCurrentMode(response, CommandType.NONE_UNKNOWN);
}
/**
* Gets the battery voltage from the meter for battery level indication
* May throw ModbusError
* @returns {number} voltage (V)
*/
async getBatteryVoltage() {
log.debug("\t\tReading battery voltage");
var response = await this.SendAndResponse(senecaMB.makeBatteryLevel());
return Math.round(senecaMB.parseBattery(response) * 100) / 100;
}
/**
* Check measurement error flags from meter
* May throw ModbusError
* @returns {boolean}
*/
async getQualityValid() {
log.debug("\t\tReading measure quality bit");
var response = await this.SendAndResponse(senecaMB.makeQualityBitRequest());
return senecaMB.isQualityValid(response);
}
/**
* Check generation error flags from meter
* May throw ModbusError
* @returns {boolean}
*/
async getGenQualityValid(current_mode) {
log.debug("\t\tReading generation quality bit");
var response = await this.SendAndResponse(senecaMB.makeGenStatusRead());
return senecaMB.parseGenStatus(response, current_mode);
}
/**
* Reads the measurements from the meter, including error flags
* May throw ModbusError
* @param {CommandType} mode current meter mode
* @returns {array|null} measurement array (units, values, error flag)
*/
async getMeasures(mode) {
log.debug("\t\tReading measures");
var valid = await this.getQualityValid();
var response = await this.SendAndResponse(senecaMB.makeMeasureRequest(mode));
if (response != null) {
var meas = senecaMB.parseMeasure(response, mode);
meas["error"] = !valid;
return meas;
}
return null;
}
/**
* Reads the active setpoints from the meter, including error flags
* May throw ModbusError
* @param {CommandType} mode current meter mode
* @returns {array|null} setpoints array (units, values, error flag)
*/
async getSetpoints(mode) {
log.debug("\t\tReading setpoints");
var valid = await this.getGenQualityValid(mode);
var response = await this.SendAndResponse(senecaMB.makeSetpointRead(mode));
if (response != null) {
var results = senecaMB.parseSetpointRead(response, mode);
results["error"] = !valid;
return results;
}
return null;
}
/**
* Puts the meter in OFF mode
* May throw ModbusError
* @returns {ResultCode} result of the operation
*/
async switchOff() {
log.debug("\t\tSetting meter to OFF");
var packet = senecaMB.makeModeRequest(CommandType.OFF);
if (packet == null)
return ResultCode.FAILED_NO_RETRY;
await this.SendAndResponse(packet);
await utils.sleep(100);
return ResultCode.SUCCESS;
}
/**
* Write the setpoints to the meter
* May throw ModbusError
* @param {CommandType} command_type type of generation command
* @param {number} setpoint setpoint of generation
* @param {number} setpoint2 facultative, second setpoint
* @returns {ResultCode} result of the operation
*/
async writeSetpoints(command_type, setpoint, setpoint2) {
var startGen;
log.debug("\t\tSetting command:"+ command_type + ", setpoint: " + setpoint + ", setpoint 2: " + setpoint2);
var packets = senecaMB.makeSetpointRequest(command_type, setpoint, setpoint2);
for(const p of packets) {
var response = await this.SendAndResponse(p);
if (response != null && !modbus.parseFC16checked(response, 0)) {
return ResultCode.FAILED_SHOULD_RETRY;
}
}
// Special handling of the SET Delay command
switch (command_type) {
case CommandType.SET_ShutdownDelay:
startGen = modbus.makeFC16(modbus.SENECA_MB_SLAVE_ID, senecaMB.MSCRegisters.CMD, [RESET_POWER_OFF]);
response = await this.SendAndResponse(startGen);
if (!modbus.parseFC16checked(response, 1)) {
return ResultCode.FAILED_NO_RETRY;
}
break;
default:
break;
}
return ResultCode.SUCCESS;
}
/**
* Clear Avg/Min/Max statistics
* May throw ModbusError
* @returns {ResultCode} result of the operation
*/
async clearStatistics() {
log.debug("\t\tResetting statistics");
var startGen = modbus.makeFC16(modbus.SENECA_MB_SLAVE_ID, senecaMB.MSCRegisters.CMD, [CLEAR_AVG_MIN_MAX]);
var response = await this.SendAndResponse(startGen);
if (!modbus.parseFC16checked(response, 1)) {
return ResultCode.FAILED_NO_RETRY;
}
return ResultCode.SUCCESS;
}
/**
* Begins the pulse generation
* May throw ModbusError
* @returns {ResultCode} result of the operation
*/
async startPulseGen() {
log.debug("\t\tStarting pulse generation");
var startGen = modbus.makeFC16(modbus.SENECA_MB_SLAVE_ID, senecaMB.MSCRegisters.GEN_CMD, [PULSE_CMD, 2]); // Start with low
var response = await this.SendAndResponse(startGen);
if (!modbus.parseFC16checked(response, 2)) {
return ResultCode.FAILED_NO_RETRY;
}
return ResultCode.SUCCESS;
}
/**
* Begins the frequency generation
* May throw ModbusError
* @returns {ResultCode} result of the operation
*/
async startFreqGen() {
log.debug("\t\tStarting freq gen");
var startGen = modbus.makeFC16(modbus.SENECA_MB_SLAVE_ID, senecaMB.MSCRegisters.GEN_CMD, [PULSE_CMD, 1]); // start gen
var response = await this.SendAndResponse(startGen);
if (!modbus.parseFC16checked(response, 2)) {
return ResultCode.FAILED_NO_RETRY;
}
return ResultCode.SUCCESS;
}
/**
* Disable auto power off to the meter
* May throw ModbusError
* @returns {ResultCode} result of the operation
*/
async disablePowerOff() {
log.debug("\t\tDisabling power off");
var startGen = modbus.makeFC16(modbus.SENECA_MB_SLAVE_ID, senecaMB.MSCRegisters.CMD, [RESET_POWER_OFF]);
await this.SendAndResponse(startGen);
return ResultCode.SUCCESS;
}
/**
* Changes the current mode on the meter
* May throw ModbusError
* @param {CommandType} command_type the new mode to set the meter in
* @returns {ResultCode} result of the operation
*/
async changeMode(command_type) {
log.debug("\t\tSetting meter mode to :" + command_type);
var packet = senecaMB.makeModeRequest(command_type);
if (packet == null) {
log.error("Could not generate modbus packet for command type", command_type);
return ResultCode.FAILED_NO_RETRY;
}
var response = await this.SendAndResponse(packet);
if (!modbus.parseFC16checked(response, 0)) {
log.error("Could not generate modbus packet for command type", command_type);
return ResultCode.FAILED_NO_RETRY;
}
var result = ResultCode.SUCCESS;
// Some commands require additional command to be given to work properly, after a slight delay
switch (command_type) {
case CommandType.Continuity:
btState.continuity = true;
break;
case CommandType.V:
case CommandType.mV:
case CommandType.mA_active:
case CommandType.mA_passive:
case CommandType.PulseTrain:
await utils.sleep(1000);
result = await this.clearStatistics();
break;
case CommandType.GEN_PulseTrain:
await utils.sleep(1000);
result = await this.startPulseGen();
break;
case CommandType.GEN_Frequency:
await utils.sleep(1000);
result = await this.startFreqGen();
break;
}
if (result == ResultCode.SUCCESS) {
result = await this.disablePowerOff();
}
return result;
}
}
module.exports = { SenecaMSC };
},{"../constants":7,"../modbusRtu":10,"../senecaModbus":13,"../utils":14,"./APIState":2,"loglevel":12}],7:[function(require,module,exports){
/**
* Command type, aka mode value to be written into MSC current state register
* */
const CommandType = {
NONE_UNKNOWN: 0, /*** MEASURING FEATURES AFTER THIS POINT *******/
mA_passive: 1,
mA_active: 2,
V: 3,
mV: 4,
THERMO_J: 5, // Termocoppie
THERMO_K: 6,
THERMO_T: 7,
THERMO_E: 8,
THERMO_L: 9,
THERMO_N: 10,
THERMO_R: 11,
THERMO_S: 12,
THERMO_B: 13,
PT100_2W: 14, // RTD 2 fili
PT100_3W: 15,
PT100_4W: 16,
PT500_2W: 17,
PT500_3W: 18,
PT500_4W: 19,
PT1000_2W: 20,
PT1000_3W: 21,
PT1000_4W: 22,
Cu50_2W: 23,
Cu50_3W: 24,
Cu50_4W: 25,
Cu100_2W: 26,
Cu100_3W: 27,
Cu100_4W: 28,
Ni100_2W: 29,
Ni100_3W: 30,
Ni100_4W: 31,
Ni120_2W: 32,
Ni120_3W: 33,
Ni120_4W: 34,
LoadCell: 35, // Celle di carico
Frequency: 36, // Frequenza
PulseTrain: 37, // Conteggio impulsi
RESERVED: 38,
RESERVED_2: 40,
Continuity: 41,
OFF: 100, // ********* GENERATION AFTER THIS POINT *****************/
GEN_mA_passive: 101,
GEN_mA_active: 102,
GEN_V: 103,
GEN_mV: 104,
GEN_THERMO_J: 105,
GEN_THERMO_K: 106,
GEN_THERMO_T: 107,
GEN_THERMO_E: 108,
GEN_THERMO_L: 109,
GEN_THERMO_N: 110,
GEN_THERMO_R: 111,
GEN_THERMO_S: 112,
GEN_THERMO_B: 113,
GEN_PT100_2W: 114,
GEN_PT500_2W: 117,
GEN_PT1000_2W: 120,
GEN_Cu50_2W: 123,
GEN_Cu100_2W: 126,
GEN_Ni100_2W: 129,
GEN_Ni120_2W: 132,
GEN_LoadCell: 135,
GEN_Frequency: 136,
GEN_PulseTrain: 137,
GEN_RESERVED: 138,
// Special settings below this points
SETTING_RESERVED: 1000,
SET_UThreshold_F: 1001,
SET_Sensitivity_uS: 1002,
SET_ColdJunction: 1003,
SET_Ulow: 1004,
SET_Uhigh: 1005,
SET_ShutdownDelay: 1006
};
const ContinuityImpl = CommandType.Cu50_2W;
const ContinuityThresholdOhms = 75;
/*
* Internal state machine descriptions
*/
const State = {
NOT_CONNECTED: "Not connected",
CONNECTING: "Bluetooth device pairing...",
DEVICE_PAIRED: "Device paired",
SUBSCRIBING: "Bluetooth interfaces connecting...",
IDLE: "Idle",
BUSY: "Busy",
ERROR: "Error",
STOPPING: "Closing BT interfaces...",
STOPPED: "Stopped",
METER_INIT: "Meter connected",
METER_INITIALIZING: "Reading meter state..."
};
const ResultCode = {
FAILED_NO_RETRY: 1,
FAILED_SHOULD_RETRY: 2,
SUCCESS: 0
};
const MAX_U_GEN = 27.0; // maximum voltage
module.exports = {State, CommandType, ResultCode, MAX_U_GEN, ContinuityImpl, ContinuityThresholdOhms};
},{}],8:[function(require,module,exports){
"use strict";
const log = require("loglevel");
const constants = require("./constants");
const APIState = require("./classes/APIState");
const Command = require("./classes/Command");
const PublicAPI = require("./meterPublicAPI");
const TestData = require("./modbusTestData");
log.setLevel(log.levels.ERROR, true);
exports.Stop = PublicAPI.Stop;
exports.Pair = PublicAPI.Pair;
exports.Execute = PublicAPI.Execute;
exports.SimpleExecute = PublicAPI.SimpleExecute;
exports.GetState = PublicAPI.GetState;
exports.State = constants.State;
exports.CommandType = constants.CommandType;
exports.Command = Command;
exports.Parse = PublicAPI.Parse;
exports.log = log;
exports.GetStateJSON = PublicAPI.GetStateJSON;
exports.ExecuteJSON = PublicAPI.ExecuteJSON;
exports.SimpleExecuteJSON = PublicAPI.SimpleExecuteJSON;
exports.GetJsonTraces = TestData.GetJsonTraces;
},{"./classes/APIState":2,"./classes/Command":3,"./constants":7,"./meterPublicAPI":9,"./modbusTestData":11,"loglevel":12}],9:[function(require,module,exports){
/*
* This file contains the public API of the meter, i.e. the functions designed
* to be called from third party code.
* 1- Pair() : bool
* 2- Execute(Command) : bool + JSON version
* 3- Stop() : bool
* 4- GetState() : array + JSON version
* 5- SimpleExecute(Command) : returns the updated measurement or null
*/
var CommandResult = require("./classes/CommandResult");
var APIState = require("./classes/APIState");
var constants = require("./constants");
var bluetooth = require("./bluetooth");
var utils = require("./utils");
var log = require("loglevel");
var meterApi = require("./meterApi");
var btState = APIState.btState;
var State = constants.State;
/**
* Returns a copy of the current state
* @returns {array} status of meter
*/
async function GetState() {
let ready = false;
let initializing = false;
switch (btState.state) {
// States requiring user input
case State.ERROR:
case State.STOPPED:
case State.NOT_CONNECTED:
ready = false;
initializing = false;
break;
case State.BUSY:
case State.IDLE:
ready = true;
initializing = false;
break;
case State.CONNECTING:
case State.DEVICE_PAIRED:
case State.METER_INIT:
case State.METER_INITIALIZING:
case State.SUBSCRIBING:
initializing = true;
ready = false;
break;
default:
ready = false;
initializing = false;
}
return {
"lastSetpoint": btState.lastSetpoint,
"lastMeasure": btState.lastMeasure,
"deviceName": btState.btDevice ? btState.btDevice.name : "",
"deviceSerial": btState.meter?.serial,
"stats": btState.stats,
"deviceMode": btState.meter?.mode,
"status": btState.state,
"batteryLevel": btState.meter?.battery,
"ready": ready,
"initializing": initializing
};
}
/**
* Provided for compatibility with Blazor
* @returns {string} JSON state object
*/
async function GetStateJSON() {
return JSON.stringify(await GetState());
}
/**
* Execute command with setpoints, JSON version
* @param {string} jsonCommand the command to execute
* @returns {string} JSON command object
*/
async function ExecuteJSON(jsonCommand) {
let command = JSON.parse(jsonCommand);
// deserialized object has lost its methods, let's recreate a complete one.
let command2 = meterApi.Command.CreateTwoSP(command.type, command.setpoint, command.setpoint2);
return JSON.stringify(await Execute(command2));
}
async function SimpleExecuteJSON(jsonCommand) {
let command = JSON.parse(jsonCommand);
// deserialized object has lost its methods, let's recreate a complete one.
let command2 = meterApi.Command.CreateTwoSP(command.type, command.setpoint, command.setpoint2);
return JSON.stringify(await SimpleExecute(command2));
}
/**
* Execute a command and returns the measurement or setpoint with error flag and message
* @param {Command} command
*/
async function SimpleExecute(command) {
const SIMPLE_EXECUTE_TIMEOUT_S = 5;
var cr = new CommandResult();
log.info("SimpleExecute called...");
if (command == null) {
cr.success = false;
cr.message = "Invalid command";
return cr;
}
command.pending = true; // In case caller does not set pending flag
// Fail immediately if not paired.
if (!btState.started) {
cr.success = false;
cr.message = "Device is not paired";
log.warn(cr.message);
return cr;
}
// Another command may be pending.
if (btState.command != null && btState.command.pending) {
cr.success = false;
cr.message = "Another command is pending";
log.warn(cr.message);
return cr;
}
// Wait for completion of the command, or halt of the state machine
btState.command = command;
if (command != null) {
await utils.waitForTimeout(() => !command.pending || btState.state == State.STOPPED, SIMPLE_EXECUTE_TIMEOUT_S);
}
// Check if error or timeouts
if (command.error || command.pending) {
cr.success = false;
cr.message = "Error while executing the command.";
log.warn(cr.message);
// Reset the active command
btState.command = null;
return cr;
}
// State is updated by execute command, so we can use btState right away
if (utils.isGeneration(command.type)) {
cr.value = btState.lastSetpoint["Value"];
cr.unit = btState.lastSetpoint["Unit"];
}
else if (utils.isMeasurement(command.type)) {
cr.value = btState.lastMeasure["Value"];
cr.unit = btState.lastMeasure["Unit"];
cr.secondary_value = btState.lastMeasure["SecondaryValue"];
cr.secondary_unit = btState.lastMeasure["SecondaryUnit"];
}
else {
cr.value = 0.0; // Settings commands;
}
cr.success = true;
cr.message = "Command executed successfully";
return cr;
}
/**
* External interface to require a command to be executed.
* The bluetooth device pairing window will open if device is not connected.
* This may fail if called outside a user gesture.
* @param {Command} command
*/
async function Execute(command) {
log.info("Execute called...");
if (command == null)
return null;
command.pending = true;
var cpt = 0;
while (btState.command != null && btState.command.pending && cpt < 300) {
log.debug("Waiting for current command to complete...");
await utils.sleep(100);
cpt++;
}
log.info("Setting new command :" + command);
btState.command = command;
// Start the regular state machine
if (!btState.started) {
btState.state = State.NOT_CONNECTED;
try {
await bluetooth.stateMachine();
} catch (err) {
log.error("Failed to start state machine:", err);
command.error = true;
command.pending = false;
return command;
}
}
// Wait for completion of the command, or halt of the state machine
if (command != null) {
await utils.waitFor(() => !command.pending || btState.state == State.STOPPED);
}
// Return the command object result
return command;
}
/**
* MUST BE CALLED FROM A USER GESTURE EVENT HANDLER
* @returns {boolean} true if meter is ready to execute command
* */
async function Pair(forceSelection = false) {
log.info("Pair(" + forceSelection + ") called...");
btState.options["forceDeviceSelection"] = forceSelection;
if (!btState.started) {
btState.state = State.NOT_CONNECTED;
bluetooth.stateMachine().catch((err) => {
log.error("State machine failed during pairing:", err);
btState.state = State.ERROR;
}); // Start it
}
else if (btState.state == State.ERROR) {
btState.state = State.NOT_CONNECTED; // Try to restart
}
await utils.waitFor(() => btState.state == State.IDLE || btState.state == State.STOPPED);
log.info("Pairing completed, state :", btState.state);
return (btState.state != State.STOPPED);
}
/**
* Stops the state machine and disconnects bluetooth.
* */
async function Stop() {
log.info("Stop request received");
btState.stopRequest = true;
await utils.sleep(100);
while (btState.started || (btState.state != State.STOPPED && btState.state != State.NOT_CONNECTED)) {
btState.stopRequest = true;
await utils.sleep(100);
}
btState.command = null;
btState.stopRequest = false;
log.warn("Stopped on request.");
return true;
}
module.exports = { Stop, Pair, Execute, ExecuteJSON, SimpleExecute, SimpleExecuteJSON, GetState, GetStateJSON, log };
},{"./bluetooth":1,"./classes/APIState":2,"./classes/CommandResult":4,"./constants":7,"./meterApi":8,"./utils":14,"loglevel":12}],10:[function(require,module,exports){
"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 Mo