From f18ae78376ad8a56b1c4f7fb0680b4857b543205 Mon Sep 17 00:00:00 2001 From: Abasz <32517724+Abasz@users.noreply.github.com> Date: Thu, 9 Apr 2026 09:43:06 +0000 Subject: [PATCH 1/2] Add CLI args for session simulation replay Replace the commented-out simulation block in server.js with CLI arguments that enable replay of recorded rowing sessions without code changes. New flags: --simulate Enable session replay (default: off) --simulateFile Path to CSV recording (default: Concept2) --simulateDelay Startup delay in ms (default: 30000) --simulateOnce Run replay once instead of looping --simulateFast Replay at max speed instead of realtime Parsing logic extracted to SimulationArgs.js with full test coverage. Adds npm run simulate convenience script. --- app/server.js | 23 ++++++----- app/tools/SimulationArgs.js | 39 +++++++++++++++++++ app/tools/SimulationArgs.test.js | 67 ++++++++++++++++++++++++++++++++ package.json | 1 + 4 files changed, 120 insertions(+), 10 deletions(-) create mode 100644 app/tools/SimulationArgs.js create mode 100644 app/tools/SimulationArgs.test.js diff --git a/app/server.js b/app/server.js index 5ec746a545..ea8fd49a33 100644 --- a/app/server.js +++ b/app/server.js @@ -12,8 +12,10 @@ import { createSessionManager } from './engine/SessionManager.js' import { createWebServer } from './WebServer.js' import { createPeripheralManager } from './peripherals/PeripheralManager.js' import { createRecordingManager } from './recorders/recordingManager.js' -/* eslint-disable-next-line no-unused-vars -- replayRowingSession shouldn't be used in a production environments */ import { replayRowingSession } from './recorders/RowingReplayer.js' +import { parseSimulationArgs } from './tools/SimulationArgs.js' + +const simulationArgs = parseSimulationArgs(process.argv.slice(2)) const exec = promisify(child_process.exec) @@ -147,12 +149,13 @@ async function shutdownApp () { peripheralManager.handleCommand('shutdown') } -/* Uncomment the following lines to simulate a session -setTimeout(function() { - replayRowingSession(handleRotationImpulse, { - filename: 'recordings/Concept2_RowErg_Session_2000meters.csv', // Concept 2, 2000 meter session - realtime: true, - loop: true - }) -}, 30000) -*/ +if (simulationArgs.simulate) { + log.info(`Simulation mode enabled, replaying '${simulationArgs.simulateFile}' after ${simulationArgs.simulateDelay}ms delay (realtime: ${simulationArgs.realtime}, loop: ${simulationArgs.loop})`) + setTimeout(() => { + replayRowingSession(handleRotationImpulse, { + filename: simulationArgs.simulateFile, + realtime: simulationArgs.realtime, + loop: simulationArgs.loop + }) + }, simulationArgs.simulateDelay) +} diff --git a/app/tools/SimulationArgs.js b/app/tools/SimulationArgs.js new file mode 100644 index 0000000000..a245ca5598 --- /dev/null +++ b/app/tools/SimulationArgs.js @@ -0,0 +1,39 @@ +'use strict' +/* + Open Rowing Monitor, https://github.com/JaapvanEkris/openrowingmonitor + + Parses command-line arguments for session simulation. +*/ + +import { parseArgs } from 'util' + +const simulationArgOptions = /** @type {const} */ ({ + simulate: { type: 'boolean', default: false }, + simulateFile: { type: 'string', default: 'recordings/Concept2_RowErg_Session_2000meters.csv' }, + simulateDelay: { type: 'string', default: '30000' }, + simulateOnce: { type: 'boolean', default: false }, + simulateFast: { type: 'boolean', default: false } +}) + +/** + * @param {string[]} argv + */ +function parseSimulationArgs (argv) { + const { values } = parseArgs({ + options: simulationArgOptions, + args: argv, + strict: false + }) + + return { + simulate: !!values.simulate, + simulateFile: String(values.simulateFile ?? 'recordings/Concept2_RowErg_Session_2000meters.csv'), + simulateDelay: parseInt(String(values.simulateDelay ?? '30000'), 10), + realtime: !values.simulateFast, + loop: !values.simulateOnce + } +} + +export { + parseSimulationArgs +} diff --git a/app/tools/SimulationArgs.test.js b/app/tools/SimulationArgs.test.js new file mode 100644 index 0000000000..77ccd16be3 --- /dev/null +++ b/app/tools/SimulationArgs.test.js @@ -0,0 +1,67 @@ +'use strict' +/* + Open Rowing Monitor, https://github.com/JaapvanEkris/openrowingmonitor + + Tests for SimulationArgs parsing +*/ +import { test } from 'uvu' +import * as assert from 'uvu/assert' +import { parseSimulationArgs } from './SimulationArgs.js' + +test('defaults when no args are passed', () => { + const result = parseSimulationArgs([]) + assert.is(result.simulate, false) + assert.is(result.simulateFile, 'recordings/Concept2_RowErg_Session_2000meters.csv') + assert.is(result.simulateDelay, 30000) + assert.is(result.realtime, true) + assert.is(result.loop, true) +}) + +test('--simulate enables simulation', () => { + const result = parseSimulationArgs(['--simulate']) + assert.is(result.simulate, true) + assert.is(result.simulateFile, 'recordings/Concept2_RowErg_Session_2000meters.csv') + assert.is(result.simulateDelay, 30000) + assert.is(result.realtime, true) + assert.is(result.loop, true) +}) + +test('--simulateFile overrides default file', () => { + const result = parseSimulationArgs(['--simulate', '--simulateFile', 'recordings/WRX700_2magnets.csv']) + assert.is(result.simulate, true) + assert.is(result.simulateFile, 'recordings/WRX700_2magnets.csv') +}) + +test('--simulateDelay overrides default delay', () => { + const result = parseSimulationArgs(['--simulate', '--simulateDelay', '5000']) + assert.is(result.simulateDelay, 5000) +}) + +test('--simulateOnce disables looping', () => { + const result = parseSimulationArgs(['--simulate', '--simulateOnce']) + assert.is(result.loop, false) + assert.is(result.realtime, true) +}) + +test('--simulateFast disables realtime', () => { + const result = parseSimulationArgs(['--simulate', '--simulateFast']) + assert.is(result.realtime, false) + assert.is(result.loop, true) +}) + +test('all flags combined', () => { + const result = parseSimulationArgs([ + '--simulate', + '--simulateFile', 'recordings/RX800.csv', + '--simulateDelay', '1000', + '--simulateOnce', + '--simulateFast' + ]) + assert.is(result.simulate, true) + assert.is(result.simulateFile, 'recordings/RX800.csv') + assert.is(result.simulateDelay, 1000) + assert.is(result.realtime, false) + assert.is(result.loop, false) +}) + +test.run() diff --git a/package.json b/package.json index 41b1efbda9..6657e2f409 100644 --- a/package.json +++ b/package.json @@ -20,6 +20,7 @@ "scripts": { "lint": "eslint ./app ./config && markdownlint-cli2 '**/*.md' '#node_modules'", "start": "node app/server.js", + "simulate": "node app/server.js --simulate", "dev": "vite", "build": "vite build", "build:watch": "vite build --watch", From 518a4a33f2f2515178d9a6586dc979d38723b2e7 Mon Sep 17 00:00:00 2001 From: Abasz <32517724+Abasz@users.noreply.github.com> Date: Thu, 9 Apr 2026 10:04:51 +0000 Subject: [PATCH 2/2] Handle invalid simulateDelay values gracefully Fall back to the default 30000ms delay when a non-numeric, empty, or negative value is passed via --simulateDelay. Add edge case tests for invalid delay, empty string, negative values, and unknown flags. --- app/tools/SimulationArgs.js | 4 +++- app/tools/SimulationArgs.test.js | 21 +++++++++++++++++++++ 2 files changed, 24 insertions(+), 1 deletion(-) diff --git a/app/tools/SimulationArgs.js b/app/tools/SimulationArgs.js index a245ca5598..af09e37bb0 100644 --- a/app/tools/SimulationArgs.js +++ b/app/tools/SimulationArgs.js @@ -25,10 +25,12 @@ function parseSimulationArgs (argv) { strict: false }) + const parsedDelay = parseInt(String(values.simulateDelay ?? '30000'), 10) + return { simulate: !!values.simulate, simulateFile: String(values.simulateFile ?? 'recordings/Concept2_RowErg_Session_2000meters.csv'), - simulateDelay: parseInt(String(values.simulateDelay ?? '30000'), 10), + simulateDelay: Number.isFinite(parsedDelay) && parsedDelay >= 0 ? parsedDelay : 30000, realtime: !values.simulateFast, loop: !values.simulateOnce } diff --git a/app/tools/SimulationArgs.test.js b/app/tools/SimulationArgs.test.js index 77ccd16be3..696333eea6 100644 --- a/app/tools/SimulationArgs.test.js +++ b/app/tools/SimulationArgs.test.js @@ -64,4 +64,25 @@ test('all flags combined', () => { assert.is(result.loop, false) }) +test('non-numeric delay falls back to default', () => { + const result = parseSimulationArgs(['--simulate', '--simulateDelay', 'abc']) + assert.is(result.simulateDelay, 30000) +}) + +test('empty delay falls back to default', () => { + const result = parseSimulationArgs(['--simulate', '--simulateDelay', '']) + assert.is(result.simulateDelay, 30000) +}) + +test('negative delay falls back to default', () => { + const result = parseSimulationArgs(['--simulate', '--simulateDelay', '-5000']) + assert.is(result.simulateDelay, 30000) +}) + +test('unknown flags are ignored', () => { + const result = parseSimulationArgs(['--simulate', '--unknownFlag']) + assert.is(result.simulate, true) + assert.is(result.simulateDelay, 30000) +}) + test.run()