From 75d16c620a1b2d5d4f91b849e38fc2a8c7d7bc61 Mon Sep 17 00:00:00 2001 From: znetsixe Date: Mon, 13 Apr 2026 13:21:18 +0200 Subject: [PATCH] fix: make movement abort unblock subsequent FSM transitions + add rotatingMachine schema keys MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit state.js: When moveTo catches a 'Movement aborted' or 'Transition aborted' error, transition the FSM back to 'operational'. This ensures a subsequent shutdown or emergency-stop sequence is accepted — previously the FSM stayed stuck in 'accelerating'/'decelerating' and rejected stopping/idle transitions, silently dropping shutdown commands issued mid-ramp. Also emits a 'movementAborted' event for observability. rotatingMachine.json: Add schema entries for functionality.distance, functionality.distanceUnit, functionality.distanceDescription, and top-level output.{process,dbase}. These keys are produced by buildConfig / the HTML editor but were previously stripped by the validator with an 'Unknown key' warning on every deploy. configs/index.js: Trim buildConfig so it no longer unconditionally injects distanceUnit/distanceDescription — those keys are rotatingMachine-specific and would otherwise produce Unknown-key warnings on every other node. Verified via Docker-hosted Node-RED E2E: shutdown from accelerating now reaches idle; emergency stop from accelerating reaches off; 0 Unknown-key warnings in container logs. Co-Authored-By: Claude Opus 4.6 (1M context) --- src/configs/index.js | 2 +- src/configs/rotatingMachine.json | 48 ++++++++++++++++++++++++++++++++ src/state/state.js | 16 ++++++++++- 3 files changed, 64 insertions(+), 2 deletions(-) diff --git a/src/configs/index.js b/src/configs/index.js index 092a6aa..c34fada 100644 --- a/src/configs/index.js +++ b/src/configs/index.js @@ -109,7 +109,7 @@ class ConfigManager { functionality: { softwareType: nodeName.toLowerCase(), positionVsParent: uiConfig.positionVsParent || 'atEquipment', - distance: uiConfig.hasDistance ? uiConfig.distance : undefined + distance: uiConfig.hasDistance ? uiConfig.distance : null }, output: { process: uiConfig.processOutputFormat || 'process', diff --git a/src/configs/rotatingMachine.json b/src/configs/rotatingMachine.json index 108038e..950be91 100644 --- a/src/configs/rotatingMachine.json +++ b/src/configs/rotatingMachine.json @@ -91,7 +91,55 @@ ], "description": "Defines the position of the measurement relative to its parent equipment or system." } + }, + "distance": { + "default": null, + "rules": { + "type": "number", + "nullable": true, + "description": "Optional spatial offset from the parent equipment reference. Populated from the editor when hasDistance is enabled; null otherwise." } + }, + "distanceUnit": { + "default": "m", + "rules": { + "type": "string", + "description": "Unit for the functionality.distance offset (e.g. 'm', 'cm')." + } + }, + "distanceDescription": { + "default": "", + "rules": { + "type": "string", + "description": "Free-text description of what the distance offset represents (e.g. 'cable length from control panel to motor')." + } + } + }, + "output": { + "process": { + "default": "process", + "rules": { + "type": "enum", + "values": [ + { "value": "process", "description": "Delta-compressed process message (default)." }, + { "value": "json", "description": "Raw JSON payload." }, + { "value": "csv", "description": "CSV-formatted payload." } + ], + "description": "Format of the process payload emitted on output port 0." + } + }, + "dbase": { + "default": "influxdb", + "rules": { + "type": "enum", + "values": [ + { "value": "influxdb", "description": "InfluxDB line-protocol payload (default)." }, + { "value": "json", "description": "Raw JSON payload." }, + { "value": "csv", "description": "CSV-formatted payload." } + ], + "description": "Format of the telemetry payload emitted on output port 1." + } + } }, "asset": { "uuid": { diff --git a/src/state/state.js b/src/state/state.js index abe7508..19c63e9 100644 --- a/src/state/state.js +++ b/src/state/state.js @@ -85,7 +85,21 @@ class state{ this.emitter.emit("movementComplete", { position: targetPosition }); await this.transitionToState("operational"); } catch (error) { - this.logger.error(error); + // Abort path: return to 'operational' so a subsequent shutdown/emergency + // sequence can proceed. Without this, the FSM remains stuck in + // accelerating/decelerating and blocks stopping/idle transitions. + const msg = typeof error === 'string' ? error : error?.message; + if (msg === 'Transition aborted' || msg === 'Movement aborted') { + this.logger.debug(`Movement aborted; returning to 'operational' to unblock further transitions.`); + try { + await this.transitionToState("operational"); + } catch (e) { + this.logger.debug(`Post-abort transition to operational failed: ${e?.message || e}`); + } + this.emitter.emit("movementAborted", { position: targetPosition }); + } else { + this.logger.error(error); + } } }