bt-seneca-msc
Version:
A pure Javascript API for the Seneca Multi Smart Calibrator (MSC) device, using web bluetooth.
255 lines (223 loc) • 7.04 kB
JavaScript
/*
* 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 };