changed the folder and added index.js
This commit is contained in:
277
src/state/movementManager.js
Normal file
277
src/state/movementManager.js
Normal file
@@ -0,0 +1,277 @@
|
||||
//const EventEmitter = require('events');
|
||||
|
||||
class movementManager {
|
||||
constructor(config, logger, emitter) {
|
||||
this.emitter = emitter; //new EventEmitter(); //state class emitter
|
||||
|
||||
const { min, max, initial } = config.position;
|
||||
const { speed, maxSpeed, interval } = config.movement;
|
||||
|
||||
this.minPosition = min;
|
||||
this.maxPosition = max;
|
||||
this.currentPosition = initial;
|
||||
|
||||
this.speed = speed;
|
||||
this.maxSpeed = maxSpeed;
|
||||
this.interval = interval;
|
||||
this.timeleft = 0; // timeleft of current movement
|
||||
|
||||
this.logger = logger;
|
||||
this.movementMode = config.movement.mode;
|
||||
}
|
||||
|
||||
getCurrentPosition() {
|
||||
return this.currentPosition;
|
||||
}
|
||||
|
||||
async moveTo(targetPosition, signal) {
|
||||
// Constrain target position if necessary
|
||||
if (
|
||||
targetPosition < this.minPosition ||
|
||||
targetPosition > this.maxPosition
|
||||
) {
|
||||
targetPosition = this.constrain(targetPosition);
|
||||
this.logger.warn(
|
||||
`New target position=${targetPosition} is constrained to fit between min=${this.minPosition} and max=${this.maxPosition}`
|
||||
);
|
||||
}
|
||||
|
||||
this.logger.info(
|
||||
`Starting movement to position ${targetPosition} in ${this.movementMode} with avg speed=${this.speed}%/s.`
|
||||
);
|
||||
|
||||
if (signal && signal.aborted) {
|
||||
this.logger.debug("Movement aborted.");
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
// Execute the movement logic based on the mode
|
||||
switch (this.movementMode) {
|
||||
case "staticspeed":
|
||||
const movelinFeedback = await this.moveLinear(targetPosition,signal);
|
||||
this.logger.info(`Linear move: ${movelinFeedback} `);
|
||||
break;
|
||||
|
||||
case "dynspeed":
|
||||
const moveDynFeedback = await this.moveEaseInOut(targetPosition,signal);
|
||||
this.logger.info(`Dynamic move : ${moveDynFeedback}`);
|
||||
break;
|
||||
|
||||
default:
|
||||
throw new Error(`Unsupported movement mode: ${this.movementMode}`);
|
||||
}
|
||||
} catch (error) {
|
||||
this.logger.error(error);
|
||||
}
|
||||
}
|
||||
|
||||
moveLinear(targetPosition, signal) {
|
||||
return new Promise((resolve, reject) => {
|
||||
// Immediate abort if already signalled
|
||||
if (signal?.aborted) {
|
||||
return reject(new Error("Movement aborted"));
|
||||
}
|
||||
|
||||
// Clamp the final target into [minPosition, maxPosition]
|
||||
targetPosition = this.constrain(targetPosition);
|
||||
|
||||
// Compute direction and remaining distance
|
||||
const direction = targetPosition > this.currentPosition ? 1 : -1;
|
||||
const distance = Math.abs(targetPosition - this.currentPosition);
|
||||
|
||||
// Speed is a fraction [0,1] of full-range per second
|
||||
this.speed = Math.min(Math.max(this.speed, 0), 1);
|
||||
const fullRange = this.maxPosition - this.minPosition;
|
||||
const velocity = this.speed * fullRange; // units per second
|
||||
if (velocity === 0) {
|
||||
return reject(new Error("Movement aborted: zero speed"));
|
||||
}
|
||||
|
||||
// Duration and bookkeeping
|
||||
const duration = distance / velocity; // seconds to go the remaining distance
|
||||
this.timeleft = duration;
|
||||
this.logger.debug(
|
||||
`Linear move: dir=${direction}, dist=${distance}, vel=${velocity.toFixed(2)} u/s, dur=${duration.toFixed(2)}s`
|
||||
);
|
||||
|
||||
// Compute how much to move each tick
|
||||
const intervalMs = this.interval;
|
||||
const intervalSec = intervalMs / 1000;
|
||||
const stepSize = direction * velocity * intervalSec;
|
||||
|
||||
const startTime = Date.now();
|
||||
|
||||
// Kick off the loop
|
||||
const intervalId = setInterval(() => {
|
||||
// 7a) Abort check
|
||||
if (signal?.aborted) {
|
||||
clearInterval(intervalId);
|
||||
return reject(new Error("Movement aborted"));
|
||||
}
|
||||
|
||||
// Advance position and clamp
|
||||
this.currentPosition += stepSize;
|
||||
this.currentPosition = this.constrain(this.currentPosition);
|
||||
this.emitPos(this.currentPosition);
|
||||
|
||||
// Update timeleft
|
||||
const elapsed = (Date.now() - startTime) / 1000;
|
||||
this.timeleft = Math.max(0, duration - elapsed);
|
||||
|
||||
this.logger.debug(
|
||||
`pos=${this.currentPosition.toFixed(2)}, timeleft=${this.timeleft.toFixed(2)}`
|
||||
);
|
||||
|
||||
// Completed the move?
|
||||
if (
|
||||
(direction > 0 && this.currentPosition >= targetPosition) ||
|
||||
(direction < 0 && this.currentPosition <= targetPosition)
|
||||
) {
|
||||
clearInterval(intervalId);
|
||||
this.currentPosition = targetPosition;
|
||||
this.emitPos(this.currentPosition);
|
||||
return resolve("Reached target move.");
|
||||
}
|
||||
}, intervalMs);
|
||||
|
||||
// 8) Also catch aborts that happen before the first tick
|
||||
signal?.addEventListener("abort", () => {
|
||||
clearInterval(intervalId);
|
||||
reject(new Error("Movement aborted"));
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
moveLinearinTime(targetPosition,signal) {
|
||||
return new Promise((resolve, reject) => {
|
||||
// Abort immediately if already signalled
|
||||
if (signal?.aborted) {
|
||||
return reject(new Error("Movement aborted"));
|
||||
}
|
||||
|
||||
const direction = targetPosition > this.currentPosition ? 1 : -1;
|
||||
const distance = Math.abs(targetPosition - this.currentPosition);
|
||||
|
||||
// Ensure speed is a percentage [0, 1]
|
||||
this.speed = Math.min(Math.max(this.speed, 0), 1);
|
||||
|
||||
// Calculate duration based on percentage of distance per second
|
||||
const duration = 1 / this.speed; // 1 second for 100% of the distance
|
||||
|
||||
this.timeleft = duration; //set this so other classes can use it
|
||||
this.logger.debug(
|
||||
`Linear movement: Direction=${direction}, Distance=${distance}, Duration=${duration}s`
|
||||
);
|
||||
|
||||
let elapsedTime = 0;
|
||||
const interval = this.interval; // Update every x ms
|
||||
const totalSteps = Math.ceil((duration * 1000) / interval);
|
||||
const stepSize = direction * (distance / totalSteps);
|
||||
|
||||
// 2) Set up the abort listener once
|
||||
const intervalId = setInterval(() => {
|
||||
// 3) Check for abort on each tick
|
||||
if (signal?.aborted) {
|
||||
clearInterval(intervalId);
|
||||
return reject(new Error("Movement aborted"));
|
||||
}
|
||||
// Update elapsed time
|
||||
elapsedTime += interval / 1000;
|
||||
|
||||
this.timeleft = duration - elapsedTime; //set this so other classes can use it
|
||||
|
||||
// Update the position incrementally
|
||||
this.currentPosition += stepSize;
|
||||
this.emitPos(this.currentPosition);
|
||||
this.logger.debug(
|
||||
`Using ${this.movementMode} => Current position ${this.currentPosition}`
|
||||
);
|
||||
|
||||
// Check if the target position has been reached
|
||||
if (
|
||||
(direction > 0 && this.currentPosition >= targetPosition) ||
|
||||
(direction < 0 && this.currentPosition <= targetPosition)
|
||||
) {
|
||||
clearInterval(intervalId);
|
||||
this.currentPosition = targetPosition;
|
||||
resolve(`Reached target move.`);
|
||||
}
|
||||
}, interval);
|
||||
// Also attach abort outside the interval in case it fires before the first tick:
|
||||
signal?.addEventListener("abort", () => {
|
||||
clearInterval(intervalId);
|
||||
reject(new Error("Movement aborted"));
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
moveEaseInOut(targetPosition, signal) {
|
||||
return new Promise((resolve, reject) => {
|
||||
// 1) Bail immediately if already aborted
|
||||
if (signal?.aborted) {
|
||||
return reject(new Error("Movement aborted"));
|
||||
}
|
||||
|
||||
const direction = targetPosition > this.currentPosition ? 1 : -1;
|
||||
const totalDistance = Math.abs(targetPosition - this.currentPosition);
|
||||
const startPosition = this.currentPosition;
|
||||
this.speed = Math.min(Math.max(this.speed, 0), 1);
|
||||
|
||||
const easeFunction = (t) =>
|
||||
t < 0.5 ? 4 * t * t * t : 1 - Math.pow(-2 * t + 2, 3) / 2;
|
||||
|
||||
let elapsedTime = 0;
|
||||
const duration = totalDistance / this.speed;
|
||||
this.timeleft = duration;
|
||||
const interval = this.interval;
|
||||
|
||||
// 2) Start the moving loop
|
||||
const intervalId = setInterval(() => {
|
||||
// 3) Check for abort on each tick
|
||||
if (signal?.aborted) {
|
||||
clearInterval(intervalId);
|
||||
return reject(new Error("Movement aborted"));
|
||||
}
|
||||
|
||||
elapsedTime += interval / 1000;
|
||||
const progress = Math.min(elapsedTime / duration, 1);
|
||||
this.timeleft = duration - elapsedTime;
|
||||
const easedProgress = easeFunction(progress);
|
||||
const newPosition =
|
||||
startPosition + (targetPosition - startPosition) * easedProgress;
|
||||
|
||||
this.emitPos(newPosition);
|
||||
this.logger.debug(
|
||||
`Using ${this.movementMode} => Progress=${progress.toFixed(
|
||||
2
|
||||
)}, Eased=${easedProgress.toFixed(2)}`
|
||||
);
|
||||
|
||||
if (progress >= 1) {
|
||||
clearInterval(intervalId);
|
||||
this.currentPosition = targetPosition;
|
||||
resolve(`Reached target move.`);
|
||||
} else {
|
||||
this.currentPosition = newPosition;
|
||||
}
|
||||
}, interval);
|
||||
|
||||
// 4) Also listen once for abort before first tick
|
||||
signal?.addEventListener("abort", () => {
|
||||
clearInterval(intervalId);
|
||||
reject(new Error("Movement aborted"));
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
emitPos(newPosition) {
|
||||
this.emitter.emit("positionChange", newPosition);
|
||||
}
|
||||
|
||||
constrain(value) {
|
||||
return Math.min(Math.max(value, this.minPosition), this.maxPosition);
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = movementManager;
|
||||
131
src/state/state.js
Normal file
131
src/state/state.js
Normal file
@@ -0,0 +1,131 @@
|
||||
//load local dependencies
|
||||
const EventEmitter = require('events');
|
||||
const StateManager = require('./stateManager');
|
||||
const MovementManager = require('./movementManager');
|
||||
|
||||
//load all config modules
|
||||
const defaultConfig = require('./stateConfig.json');
|
||||
const ConfigUtils = require('../../../generalFunctions/helper/configUtils');
|
||||
|
||||
class state{
|
||||
constructor(config = {}, logger) {
|
||||
|
||||
this.emitter = new EventEmitter(); // Own EventEmitter
|
||||
this.configUtils = new ConfigUtils(defaultConfig);
|
||||
this.config = this.configUtils.initConfig(config);
|
||||
this.abortController = null; // new abort controller for aborting async tasks
|
||||
// Init after config is set
|
||||
this.logger = logger;
|
||||
|
||||
// Initialize StateManager for state handling
|
||||
this.stateManager = new StateManager(this.config,this.logger);
|
||||
this.movementManager = new MovementManager(this.config, this.logger, this.emitter);
|
||||
|
||||
this.delayedMove = null;
|
||||
this.mode = this.config.mode.current;
|
||||
|
||||
// Log initialization
|
||||
this.logger.info("State class initialized.");
|
||||
|
||||
}
|
||||
|
||||
// -------- Delegate State Management -------- //
|
||||
|
||||
getMoveTimeLeft() {
|
||||
return this.movementManager.timeleft;
|
||||
}
|
||||
|
||||
getCurrentState() {
|
||||
return this.stateManager.currentState;
|
||||
}
|
||||
|
||||
getStateDescription() {
|
||||
return this.stateManager.getStateDescription();
|
||||
}
|
||||
|
||||
// -------- Movement Methods -------- //
|
||||
getCurrentPosition() {
|
||||
return this.movementManager.getCurrentPosition();
|
||||
}
|
||||
|
||||
getRunTimeHours() {
|
||||
return this.stateManager.getRunTimeHours();
|
||||
}
|
||||
|
||||
async moveTo(targetPosition) {
|
||||
|
||||
// Check for invalid conditions and throw errors
|
||||
if (targetPosition === this.getCurrentPosition()) {
|
||||
this.logger.warn(`Target position=${targetPosition} is the same as the current position ${this.getCurrentPosition()}. Not executing move.`);
|
||||
return;
|
||||
}
|
||||
|
||||
if (this.stateManager.getCurrentState() !== "operational") {
|
||||
if (this.config.mode.current === "auto") {
|
||||
this.delayedMove = targetPosition;
|
||||
this.logger.warn(`Saving setpoint=${targetPosition} to execute once back in 'operational' state.`);
|
||||
}
|
||||
else{
|
||||
this.logger.warn(`Not able to accept setpoint=${targetPosition} while not in ${this.stateManager.getCurrentState()} state`);
|
||||
}
|
||||
//return early
|
||||
return;
|
||||
}
|
||||
this.abortController = new AbortController();
|
||||
const { signal } = this.abortController;
|
||||
try {
|
||||
const newState = targetPosition < this.getCurrentPosition() ? "decelerating" : "accelerating";
|
||||
await this.transitionToState(newState,signal); // awaits transition
|
||||
await this.movementManager.moveTo(targetPosition,signal); // awaits moving
|
||||
this.emitter.emit("movementComplete", { position: targetPosition });
|
||||
await this.transitionToState("operational");
|
||||
} catch (error) {
|
||||
this.logger.error(error);
|
||||
}
|
||||
}
|
||||
|
||||
// -------- State Transition Methods -------- //
|
||||
|
||||
async transitionToState(targetState, signal) {
|
||||
|
||||
const fromState = this.getCurrentState();
|
||||
const position = this.getCurrentPosition();
|
||||
|
||||
try {
|
||||
|
||||
this.logger.debug(`Starting transition from ${fromState} to ${targetState}.`);
|
||||
const feedback = await this.stateManager.transitionTo(targetState,signal);
|
||||
this.logger.info(`Statemanager: ${feedback}`);
|
||||
|
||||
/* -- Auto pick setpoints in auto mode when operational--*/
|
||||
if (
|
||||
targetState === "operational" &&
|
||||
this.config.mode.current === "auto" &&
|
||||
this.delayedMove !== position &&
|
||||
this.delayedMove
|
||||
) {
|
||||
this.logger.info(`Automatically picking up on last requested setpoint ${this.delayedMove}`);
|
||||
//trigger move
|
||||
await this.moveTo(this.delayedMove,signal);
|
||||
this.delayedMove = null;
|
||||
|
||||
this.logger.info(`moveTo : ${feedback} `);
|
||||
}
|
||||
|
||||
this.logger.info(`State change to ${targetState} completed.`);
|
||||
this.emitter.emit('stateChange', targetState); // <-- Implement Here
|
||||
} catch (error) {
|
||||
if (
|
||||
error.message === "Transition aborted" ||
|
||||
error.message === "Movement aborted"
|
||||
) {
|
||||
throw error;
|
||||
}
|
||||
this.logger.error(error);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
module.exports = state;
|
||||
|
||||
331
src/state/stateConfig.json
Normal file
331
src/state/stateConfig.json
Normal file
@@ -0,0 +1,331 @@
|
||||
{
|
||||
"general": {
|
||||
"name": {
|
||||
"default": "State Configuration",
|
||||
"rules": {
|
||||
"type": "string",
|
||||
"description": "A human-readable name for the state configuration."
|
||||
}
|
||||
},
|
||||
"id": {
|
||||
"default": null,
|
||||
"rules": {
|
||||
"type": "string",
|
||||
"nullable": true,
|
||||
"description": "A unique identifier for this configuration, assigned dynamically when needed."
|
||||
}
|
||||
},
|
||||
"unit": {
|
||||
"default": "unitless",
|
||||
"rules": {
|
||||
"type": "string",
|
||||
"description": "The unit used for the state values (e.g., 'meters', 'seconds', 'unitless')."
|
||||
}
|
||||
},
|
||||
"logging": {
|
||||
"logLevel": {
|
||||
"default": "info",
|
||||
"rules": {
|
||||
"type": "enum",
|
||||
"values": [
|
||||
{
|
||||
"value": "debug",
|
||||
"description": "Log messages are printed for debugging purposes."
|
||||
},
|
||||
{
|
||||
"value": "info",
|
||||
"description": "Informational messages are printed."
|
||||
},
|
||||
{
|
||||
"value": "warn",
|
||||
"description": "Warning messages are printed."
|
||||
},
|
||||
{
|
||||
"value": "error",
|
||||
"description": "Error messages are printed."
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"enabled": {
|
||||
"default": true,
|
||||
"rules": {
|
||||
"type": "boolean",
|
||||
"description": "Indicates whether logging is active. If true, log messages will be generated."
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"functionality": {
|
||||
"softwareType": {
|
||||
"default": "state class",
|
||||
"rules": {
|
||||
"type": "string",
|
||||
"description": "Logical name identifying the software type."
|
||||
}
|
||||
},
|
||||
"role": {
|
||||
"default": "StateController",
|
||||
"rules": {
|
||||
"type": "string",
|
||||
"description": "Functional role within the system."
|
||||
}
|
||||
}
|
||||
},
|
||||
"time": {
|
||||
"starting": {
|
||||
"default": 10,
|
||||
"rules": {
|
||||
"type": "number",
|
||||
"description": "Time in seconds for the starting phase."
|
||||
}
|
||||
},
|
||||
"warmingup": {
|
||||
"default": 5,
|
||||
"rules": {
|
||||
"type": "number",
|
||||
"description": "Time in seconds for the warming-up phase."
|
||||
}
|
||||
},
|
||||
"stopping": {
|
||||
"default": 5,
|
||||
"rules": {
|
||||
"type": "number",
|
||||
"description": "Time in seconds for the stopping phase."
|
||||
}
|
||||
},
|
||||
"coolingdown": {
|
||||
"default": 10,
|
||||
"rules": {
|
||||
"type": "number",
|
||||
"description": "Time in seconds for the cooling-down phase."
|
||||
}
|
||||
}
|
||||
},
|
||||
"movement": {
|
||||
"mode": {
|
||||
"default": "dynspeed",
|
||||
"rules": {
|
||||
"type": "enum",
|
||||
"values": [
|
||||
{
|
||||
"value": "staticspeed",
|
||||
"description": "Linear movement to setpoint."
|
||||
},
|
||||
{
|
||||
"value": "dynspeed",
|
||||
"description": "Ease-in and ease-out to setpoint."
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"speed": {
|
||||
"default": 1,
|
||||
"rules": {
|
||||
"type": "number",
|
||||
"description": "Current speed setting."
|
||||
}
|
||||
},
|
||||
"maxSpeed": {
|
||||
"default": 10,
|
||||
"rules": {
|
||||
"type": "number",
|
||||
"description": "Maximum speed setting."
|
||||
}
|
||||
},
|
||||
"interval": {
|
||||
"default": 1000,
|
||||
"rules": {
|
||||
"type": "number",
|
||||
"description": "Feedback interval in milliseconds."
|
||||
}
|
||||
}
|
||||
},
|
||||
"position": {
|
||||
"min": {
|
||||
"default": 0,
|
||||
"rules": {
|
||||
"type": "number",
|
||||
"description": "Minimum position value."
|
||||
}
|
||||
},
|
||||
"max": {
|
||||
"default": 100,
|
||||
"rules": {
|
||||
"type": "number",
|
||||
"description": "Maximum position value."
|
||||
}
|
||||
},
|
||||
"initial": {
|
||||
"default": 0,
|
||||
"rules": {
|
||||
"type": "number",
|
||||
"description": "Initial position value."
|
||||
}
|
||||
}
|
||||
},
|
||||
"state": {
|
||||
"current": {
|
||||
"default": "idle",
|
||||
"rules": {
|
||||
"type": "enum",
|
||||
"values": [
|
||||
{
|
||||
"value": "idle",
|
||||
"description": "Machine is idle."
|
||||
},
|
||||
{
|
||||
"value": "starting",
|
||||
"description": "Machine is starting up."
|
||||
},
|
||||
{
|
||||
"value": "warmingup",
|
||||
"description": "Machine is warming up."
|
||||
},
|
||||
{
|
||||
"value": "operational",
|
||||
"description": "Machine is running."
|
||||
},
|
||||
{
|
||||
"value": "accelerating",
|
||||
"description": "Machine is accelerating."
|
||||
},
|
||||
{
|
||||
"value": "decelerating",
|
||||
"description": "Machine is decelerating."
|
||||
},
|
||||
{
|
||||
"value": "stopping",
|
||||
"description": "Machine is stopping."
|
||||
},
|
||||
{
|
||||
"value": "coolingdown",
|
||||
"description": "Machine is cooling down."
|
||||
},
|
||||
{
|
||||
"value": "off",
|
||||
"description": "Machine is off."
|
||||
}
|
||||
],
|
||||
"description": "Current state of the machine."
|
||||
}
|
||||
},
|
||||
"allowedTransitions":{
|
||||
"default": {},
|
||||
"rules": {
|
||||
"type": "object",
|
||||
"schema": {
|
||||
"idle": {
|
||||
"default": ["starting", "off","emergencystop"],
|
||||
"rules":{
|
||||
"type": "set",
|
||||
"itemType": "string",
|
||||
"description": "Allowed transitions from idle state."
|
||||
}
|
||||
},
|
||||
"starting": {
|
||||
"default": ["starting","warmingup","emergencystop"],
|
||||
"rules":{
|
||||
"type": "set",
|
||||
"itemType": "string",
|
||||
"description": "Allowed transitions from starting state."
|
||||
}
|
||||
},
|
||||
"warmingup": {
|
||||
"default": ["operational","emergencystop"],
|
||||
"rules":{
|
||||
"type": "set",
|
||||
"itemType": "string",
|
||||
"description": "Allowed transitions from warmingup state."
|
||||
}
|
||||
},
|
||||
"operational": {
|
||||
"default": ["accelerating", "decelerating", "stopping","emergencystop"],
|
||||
"rules":{
|
||||
"type": "set",
|
||||
"itemType": "string",
|
||||
"description": "Allowed transitions from operational state."
|
||||
}
|
||||
},
|
||||
"accelerating": {
|
||||
"default": ["operational","emergencystop"],
|
||||
"rules":{
|
||||
"type": "set",
|
||||
"itemType": "string",
|
||||
"description": "Allowed transitions from accelerating state."
|
||||
}
|
||||
},
|
||||
"decelerating": {
|
||||
"default": ["operational","emergencystop"],
|
||||
"rules":{
|
||||
"type": "set",
|
||||
"itemType": "string",
|
||||
"description": "Allowed transitions from decelerating state."
|
||||
}
|
||||
},
|
||||
"stopping": {
|
||||
"default": ["idle","coolingdown","emergencystop"],
|
||||
"rules":{
|
||||
"type": "set",
|
||||
"itemType": "string",
|
||||
"description": "Allowed transitions from stopping state."
|
||||
}
|
||||
},
|
||||
"coolingdown": {
|
||||
"default": ["idle","off","emergencystop"],
|
||||
"rules":{
|
||||
"type": "set",
|
||||
"itemType": "string",
|
||||
"description": "Allowed transitions from coolingDown state."
|
||||
}
|
||||
},
|
||||
"off": {
|
||||
"default": ["idle","emergencystop"],
|
||||
"rules":{
|
||||
"type": "set",
|
||||
"itemType": "string",
|
||||
"description": "Allowed transitions from off state."
|
||||
}
|
||||
},
|
||||
"emergencystop": {
|
||||
"default": ["idle","off"],
|
||||
"rules":{
|
||||
"type": "set",
|
||||
"itemType": "string",
|
||||
"description": "Allowed transitions from emergency stop state."
|
||||
}
|
||||
}
|
||||
},
|
||||
"description": "Allowed transitions between states."
|
||||
}
|
||||
},
|
||||
"activeStates":{
|
||||
"default": ["operational", "starting", "warmingup", "accelerating", "decelerating"],
|
||||
"rules": {
|
||||
"type": "set",
|
||||
"itemType": "string",
|
||||
"description": "Active states."
|
||||
}
|
||||
}
|
||||
},
|
||||
"mode": {
|
||||
"current": {
|
||||
"default": "auto",
|
||||
"rules": {
|
||||
"type": "enum",
|
||||
"values": [
|
||||
{
|
||||
"value": "auto",
|
||||
"description": "Automatically tracks and handles delayed commands for setpoints > 0."
|
||||
},
|
||||
{
|
||||
"value": "manual",
|
||||
"description": "Requires explicit commands to start."
|
||||
}
|
||||
],
|
||||
"description": "Current mode of the machine."
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
164
src/state/stateManager.js
Normal file
164
src/state/stateManager.js
Normal file
@@ -0,0 +1,164 @@
|
||||
/**
|
||||
* @file stateManager.js
|
||||
*
|
||||
* Permission is hereby granted to any person obtaining a copy of this software
|
||||
* and associated documentation files (the "Software"), to use it for personal
|
||||
* or non-commercial purposes, with the following restrictions:
|
||||
*
|
||||
* 1. **No Copying or Redistribution**: The Software or any of its parts may not
|
||||
* be copied, merged, distributed, sublicensed, or sold without explicit
|
||||
* prior written permission from the author.
|
||||
*
|
||||
* 2. **Commercial Use**: Any use of the Software for commercial purposes requires
|
||||
* a valid license, obtainable only with the explicit consent of the author.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
* FITNESS FOR A PARTICULAR PURPOSE, AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES, OR OTHER
|
||||
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT, OR OTHERWISE, ARISING FROM,
|
||||
* OUT OF, OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
* SOFTWARE.
|
||||
*
|
||||
* Ownership of this code remains solely with the original author. Unauthorized
|
||||
* use of this Software is strictly prohibited.
|
||||
*
|
||||
* @summary Class for managing state transitions and state descriptions.
|
||||
* @description Class for managing state transitions and state descriptions.
|
||||
* @module stateManager
|
||||
* @exports stateManager
|
||||
* @version 0.1.0
|
||||
* @since 0.1.0
|
||||
*
|
||||
* Author:
|
||||
* - Rene De Ren
|
||||
* Email:
|
||||
* - rene@thegoldenbasket.nl
|
||||
*/
|
||||
|
||||
class stateManager {
|
||||
constructor(config, logger) {
|
||||
this.currentState = config.state.current;
|
||||
this.availableStates = config.state.available;
|
||||
this.descriptions = config.state.descriptions;
|
||||
this.logger = logger;
|
||||
this.transitionTimeleft = 0;
|
||||
this.transitionTimes = config.time;
|
||||
|
||||
// Define valid transitions (can be extended dynamically if needed)
|
||||
this.validTransitions = config.state.allowedTransitions;
|
||||
|
||||
// NEW: Initialize runtime tracking
|
||||
this.runTimeHours = 0; // cumulative runtime in hours
|
||||
this.runTimeStart = null; // timestamp when active state began
|
||||
|
||||
// Define active states (runtime counts only in these states)
|
||||
this.activeStates = config.state.activeStates;
|
||||
}
|
||||
|
||||
getCurrentState() {
|
||||
return this.currentState;
|
||||
}
|
||||
|
||||
transitionTo(newState,signal) {
|
||||
return new Promise((resolve, reject) => {
|
||||
if (signal && signal.aborted) {
|
||||
this.logger.debug("Transition aborted.");
|
||||
return reject("Transition aborted.");
|
||||
}
|
||||
|
||||
if (!this.isValidTransition(newState)) {
|
||||
return reject(
|
||||
`Invalid transition from ${this.currentState} to ${newState}. Transition not executed.`
|
||||
); //go back early and reject promise
|
||||
}
|
||||
|
||||
// NEW: Handle runtime tracking based on active states
|
||||
this.handleRuntimeTracking(newState);
|
||||
|
||||
const transitionDuration = this.transitionTimes[this.currentState] || 0; // Default to 0 if no transition time
|
||||
this.logger.debug(
|
||||
`Transition from ${this.currentState} to ${newState} will take ${transitionDuration}s.`
|
||||
);
|
||||
|
||||
if (transitionDuration > 0) {
|
||||
const timeoutId = setTimeout(() => {
|
||||
this.currentState = newState;
|
||||
resolve(`Transition from ${this.currentState} to ${newState} completed in ${transitionDuration}s.`);
|
||||
}, transitionDuration * 1000);
|
||||
if (signal) {
|
||||
signal.addEventListener('abort', () => {
|
||||
clearTimeout(timeoutId);
|
||||
reject(new Error('Transition aborted'));
|
||||
});
|
||||
}
|
||||
} else {
|
||||
this.currentState = newState;
|
||||
resolve(`Immediate transition to ${this.currentState} completed.`);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
handleRuntimeTracking(newState) {
|
||||
// NEW: Handle runtime tracking based on active states
|
||||
const wasActive = this.activeStates.has(this.currentState);
|
||||
const willBeActive = this.activeStates.has(newState);
|
||||
if (wasActive && !willBeActive && this.runTimeStart) {
|
||||
// stop runtime timer and accumulate elapsed time
|
||||
const elapsed = (Date.now() - this.runTimeStart) / 3600000; // hours
|
||||
this.runTimeHours += elapsed;
|
||||
this.runTimeStart = null;
|
||||
this.logger.debug(
|
||||
`Runtime timer stopped; elapsed=${elapsed.toFixed(
|
||||
3
|
||||
)}h, total=${this.runTimeHours.toFixed(3)}h.`
|
||||
);
|
||||
} else if (!wasActive && willBeActive && !this.runTimeStart) {
|
||||
// starting new runtime
|
||||
this.runTimeStart = Date.now();
|
||||
this.logger.debug("Runtime timer started.");
|
||||
}
|
||||
}
|
||||
|
||||
isValidTransition(newState) {
|
||||
this.logger.debug(
|
||||
`Check 1 Transition valid ? From ${
|
||||
this.currentState
|
||||
} To ${newState} => ${this.validTransitions[this.currentState]?.has(
|
||||
newState
|
||||
)} `
|
||||
);
|
||||
this.logger.debug(
|
||||
`Check 2 Transition valid ? ${
|
||||
this.currentState
|
||||
} is not equal to ${newState} => ${this.currentState !== newState}`
|
||||
);
|
||||
// check if transition is valid and not the same as before
|
||||
const valid =
|
||||
this.validTransitions[this.currentState]?.has(newState) &&
|
||||
this.currentState !== newState;
|
||||
|
||||
//if not valid
|
||||
if (!valid) {
|
||||
return false;
|
||||
} else {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
getStateDescription(state = this.currentState) {
|
||||
return this.descriptions[state] || "No description available.";
|
||||
}
|
||||
|
||||
// NEW: Getter to retrieve current cumulative runtime (active time) in hours.
|
||||
getRunTimeHours() {
|
||||
// If currently active add the ongoing duration.
|
||||
let currentElapsed = 0;
|
||||
if (this.runTimeStart) {
|
||||
currentElapsed = (Date.now() - this.runTimeStart) / 3600000;
|
||||
}
|
||||
return this.runTimeHours + currentElapsed;
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = stateManager;
|
||||
Reference in New Issue
Block a user