diff --git a/frontend/src/pages/engineering/developer/ApiReferencePage.jsx b/frontend/src/pages/engineering/developer/ApiReferencePage.jsx index 15b996d..3a5d516 100644 --- a/frontend/src/pages/engineering/developer/ApiReferencePage.jsx +++ b/frontend/src/pages/engineering/developer/ApiReferencePage.jsx @@ -32,14 +32,49 @@ const UART_WHITELIST = [ 'bells.enable', 'bells.disable', ] +// Every command has: +// response — exact wire-format success JSON (what the data topic carries on SUCCESS) +// errors — array of { message, condition } matching every CommandResult::err() path +// +// Wire envelope for all replies: +// { "status": "SUCCESS"|"ERROR", "type": "", "message": "...", "data": {...} } +// "message" is present on plain-text ok(); "data" is present on ok(doc). +// Errors always have "message". + const NAMESPACES = [ { id: 'root', label: 'Root', description: 'Top-level commands — no namespace prefix.', commands: [ - { cmd: 'ping', handler: 'SystemHandler', transports: ['All'], description: 'Connectivity check. No contents required.', contents: null, returns: '{ "status": "SUCCESS", "type": "pong" }', example: '{ "v": 2, "cmd": "ping" }', warning: null }, - { cmd: 'identify', handler: 'SystemHandler', transports: ['WebSocket'], description: 'Register client type with the device. Must be sent immediately after connecting on WebSocket or targeted responses will not be received.', contents: [{ field: 'device_type', type: 'string', required: true, notes: '"master" (app/console) or "secondary" (slave board)' }], returns: null, example: '{ "v": 2, "cmd": "identify", "contents": { "device_type": "master" } }', warning: null }, + { + cmd: 'ping', + handler: 'SystemHandler', + transports: ['All'], + description: 'Connectivity check. No contents required.', + contents: null, + response: '{ "status": "SUCCESS", "type": "pong" }', + errors: [], + example: '{ "v": 2, "cmd": "ping" }', + warning: null, + }, + { + cmd: 'identify', + handler: 'SystemHandler', + transports: ['WebSocket'], + description: 'Register client type with the device. Must be sent immediately after connecting on WebSocket.', + contents: [ + { field: 'device_type', type: 'string', required: true, notes: '"master" (app/console) or "secondary" (slave board)' }, + ], + response: '{ "status": "SUCCESS", "type": "identify", "message": "Device identified as master" }', + errors: [ + { message: 'identify is only available via WebSocket', condition: 'Command sent over non-WebSocket transport' }, + { message: 'Missing required field: device_type', condition: 'contents.device_type is absent' }, + { message: 'Invalid device_type. Use \'master\' or \'secondary\'', condition: 'device_type is any value other than "master" or "secondary"' }, + ], + example: '{ "v": 2, "cmd": "identify", "contents": { "device_type": "master" } }', + warning: null, + }, ], }, { @@ -57,14 +92,17 @@ const NAMESPACES = [ { field: 'uid', type: 'string', required: false, notes: 'Alias for pid. Use pid in new code.' }, { field: 'name', type: 'string', required: false, notes: 'Human-readable label, used in status broadcasts.' }, { field: 'url', type: 'string', required: false, notes: 'Download URL. Required only if the melody is not yet on the SD card and is not a built-in.' }, - { field: 'speed', type: 'int', required: false, notes: 'Inter-note delay in ms. Default 300. Controls overall tempo.' }, - { field: 'note_assignments', type: 'int[]', required: false, notes: 'Array of up to 16 relay output indices, mapping melody note 1–16 → physical output. E.g. [1,2,3,4,5,6,0,0,…]' }, - { field: 'mode', type: 'string', required: false, notes: '"single" (default) — play one loop and stop. "timed" — loop for duration ms. "interval" — timed segments with pauses between. "infinite" — loop until explicitly stopped.' }, - { field: 'duration', type: 'int', required: false, notes: 'Segment duration in ms. Required when mode is "timed" or "interval".' }, + { field: 'speed', type: 'int', required: false, notes: 'Inter-note delay in ms. Default 300.' }, + { field: 'note_assignments', type: 'int[]', required: false, notes: 'Array of up to 16 relay output indices, mapping melody note 1–16 → physical output.' }, + { field: 'mode', type: 'string', required: false, notes: '"single" (default) | "timed" | "interval" | "infinite"' }, + { field: 'duration', type: 'int', required: false, notes: 'Segment duration in ms. Required for mode "timed" or "interval".' }, { field: 'pause_duration', type: 'int', required: false, notes: 'Pause between segments in ms. Used only with mode "interval".' }, - { field: 'total_duration', type: 'int', required: false, notes: 'Total playback time in ms. Used only with mode "interval" to cap the overall run.' }, + { field: 'total_duration', type: 'int', required: false, notes: 'Total playback time in ms. Used only with mode "interval".' }, + ], + response: '{ "status": "SUCCESS", "type": "playback", "message": "Playback command executed" }', + errors: [ + { message: 'Playback command failed', condition: 'Player rejected the command (e.g. already playing, missing pid, melody not found)' }, ], - returns: null, example: '// Single play\n{ "v": 2, "cmd": "playback.play", "contents": { "pid": "westminster", "speed": 400, "mode": "single" } }\n\n// Timed loop for 2 minutes\n{ "v": 2, "cmd": "playback.play", "contents": { "pid": "ABC123", "mode": "timed", "duration": 120000 } }\n\n// Interval: play 15 s, pause 5 s, repeat for 3 min total\n{ "v": 2, "cmd": "playback.play", "contents": { "pid": "ABC123", "mode": "interval", "duration": 15000, "pause_duration": 5000, "total_duration": 180000 } }', warning: null, }, @@ -74,7 +112,10 @@ const NAMESPACES = [ transports: ['All'], description: 'Stop playback immediately. Safe to call even when player is already stopped.', contents: null, - returns: null, + response: '{ "status": "SUCCESS", "type": "playback", "message": "Playback command executed" }', + errors: [ + { message: 'Playback command failed', condition: 'Player rejected the stop (internal error)' }, + ], example: '{ "v": 2, "cmd": "playback.stop" }', warning: null, }, @@ -85,19 +126,84 @@ const NAMESPACES = [ label: 'relay', description: 'Bell relay channel configuration and testing. Handler: RelayHandler.', commands: [ - { cmd: 'relay.set_durations', handler: 'RelayHandler', transports: ['All'], description: 'Set strike duration (ms) for one or more bell channels. Partial updates accepted — omitted channels are unchanged.', contents: [{ field: 'bells', type: 'object', required: true, notes: 'Map of channel index → duration ms. e.g. { "0": 95, "2": 110 }. At least one entry required.' }], returns: null, example: '{\n "v": 2,\n "cmd": "relay.set_durations",\n "contents": { "bells": { "0": 95, "1": 100, "2": 110 } }\n}', warning: null }, - { cmd: 'relay.set_outputs', handler: 'RelayHandler', transports: ['All'], description: 'Map bell channels to physical relay outputs. Partial updates accepted.', contents: [{ field: 'bells', type: 'object', required: true, notes: 'Map of channel index → relay output index. e.g. { "0": 2, "1": 3 }' }], returns: null, example: '{\n "v": 2,\n "cmd": "relay.set_outputs",\n "contents": { "bells": { "0": 2, "1": 3, "2": 4 } }\n}', warning: null }, - { cmd: 'relay.get_config', handler: 'RelayHandler', transports: ['All'], description: 'Read current bell durations and relay output assignments.', contents: null, returns: '{ "durations": { "0": 95, ... }, "outputs": { "0": 2, ... } }', example: '{ "v": 2, "cmd": "relay.get_config" }', warning: null }, + { + cmd: 'relay.set_durations', + handler: 'RelayHandler', + transports: ['All'], + description: 'Set strike duration (ms) for one or more bell channels. Partial updates accepted.', + contents: [ + { field: 'bells', type: 'object', required: true, notes: 'Map of channel index → duration ms. e.g. { "0": 95, "2": 110 }' }, + ], + response: '{ "status": "SUCCESS", "type": "relay.set_durations", "message": "Bell durations updated and saved" }', + errors: [ + { message: 'Missing or invalid \'bells\' object', condition: 'contents.bells is absent or not an object' }, + { message: 'Durations updated but failed to save to SD', condition: 'SD card write failed — values are applied in RAM but will not survive reboot' }, + ], + example: '{\n "v": 2,\n "cmd": "relay.set_durations",\n "contents": { "bells": { "0": 95, "1": 100, "2": 110 } }\n}', + warning: null, + }, + { + cmd: 'relay.set_outputs', + handler: 'RelayHandler', + transports: ['All'], + description: 'Map bell channels to physical relay outputs. Partial updates accepted.', + contents: [ + { field: 'bells', type: 'object', required: true, notes: 'Map of bell index (0-based) → relay output number (1-based, 1 = first relay). 0 = disabled. e.g. { "0": 1, "1": 2 }' }, + ], + response: '{ "status": "SUCCESS", "type": "relay.set_outputs", "message": "Bell outputs updated and saved" }', + errors: [ + { message: 'Missing or invalid \'bells\' object', condition: 'contents.bells is absent or not an object' }, + { message: 'Outputs updated but failed to save to SD', condition: 'SD card write failed' }, + ], + example: '{\n "v": 2,\n "cmd": "relay.set_outputs",\n "contents": { "bells": { "0": 2, "1": 3, "2": 4 } }\n}', + warning: null, + }, + { + cmd: 'relay.set_config', + handler: 'RelayHandler', + transports: ['All'], + description: 'Set durations and/or output assignments in a single command. Both fields are optional — omit whichever you do not need to change. Partial updates are accepted.', + contents: [ + { field: 'durations', type: 'object', required: false, notes: 'Map of bell index (0-based) → duration ms. e.g. { "0": 95, "2": 110 }' }, + { field: 'outputs', type: 'object', required: false, notes: 'Map of bell index (0-based) → relay output number (1-based, 1 = first relay). 0 = disabled. e.g. { "0": 1, "1": 2 }' }, + ], + response: '{ "status": "SUCCESS", "type": "relay.set_config", "message": "Bell configuration updated and saved" }', + errors: [ + { message: 'Provide \'durations\' and/or \'outputs\' objects', condition: 'Neither durations nor outputs is present in contents' }, + { message: 'Durations applied but failed to save to SD', condition: 'SD write failed after applying durations' }, + { message: 'Outputs applied but failed to save to SD', condition: 'SD write failed after applying outputs' }, + ], + example: '// Set both at once\n{\n "v": 2,\n "cmd": "relay.set_config",\n "contents": {\n "durations": { "0": 95, "1": 100, "2": 110 },\n "outputs": { "0": 2, "1": 3, "2": 4 }\n }\n}\n\n// Update only durations\n{ "v": 2, "cmd": "relay.set_config", "contents": { "durations": { "0": 95 } } }\n\n// Update only outputs\n{ "v": 2, "cmd": "relay.set_config", "contents": { "outputs": { "0": 2 } } }', + warning: null, + }, + { + cmd: 'relay.get_config', + handler: 'RelayHandler', + transports: ['All'], + description: 'Read current bell durations and relay output assignments for all 16 channels.', + contents: null, + response: '{\n "status": "SUCCESS",\n "type": "relay.get_config",\n "data": {\n "durations": { "0": 95, "1": 100, ... },\n "outputs": { "0": 1, "1": 2, ... } // 1-based: 1=first relay, 0=disabled\n }\n}', + errors: [], + example: '{ "v": 2, "cmd": "relay.get_config" }', + warning: null, + }, { cmd: 'relay.test_output', handler: 'RelayHandler', transports: ['All'], - description: 'Fire a raw relay output for a caller-specified duration. Bypasses bell assignments — fires the physical relay directly. Player must be stopped.', + description: 'Fire a raw relay output for a caller-specified duration. Bypasses bell assignments. Player must be stopped.', contents: [ - { field: 'output', type: 'int', required: true, notes: 'Physical relay output index (0–31)' }, - { field: 'duration_ms', type: 'int', required: true, notes: 'Duration in ms (1–5000). No default.' }, + { field: 'output', type: 'int', required: true, notes: 'Raw chip pin index (0-based, 0–31). This is the only command that uses 0-based indexing — all other commands use 1-based output numbers.' }, + { field: 'duration_ms', type: 'int', required: true, notes: 'Duration in ms (1–5000)' }, + ], + response: '{ "status": "SUCCESS", "type": "relay.test_output", "message": "Output 2 fired for 95 ms" }', + errors: [ + { message: 'Missing required field: output', condition: 'contents.output is absent' }, + { message: 'Missing required field: duration_ms', condition: 'contents.duration_ms is absent' }, + { message: 'Player must be stopped before testing outputs', condition: 'Player is currently PLAYING, PAUSED, or STOPPING' }, + { message: 'output must be 0-31', condition: 'output is outside 0–31 range' }, + { message: 'duration_ms must be 1-5000', condition: 'duration_ms is 0 or greater than 5000' }, ], - returns: null, example: '{\n "v": 2,\n "cmd": "relay.test_output",\n "contents": { "output": 2, "duration_ms": 95 }\n}', warning: null, }, @@ -105,12 +211,16 @@ const NAMESPACES = [ cmd: 'relay.test_bell', handler: 'RelayHandler', transports: ['All'], - description: 'Fire bell channel N using its configured output and duration. Non-blocking. Can be sent repeatedly in quick succession like a piano key. Bypasses bellsEnabled guard — test commands always fire.', + description: 'Fire bell channel N using its configured output and duration. Non-blocking. Can be sent in rapid succession like piano keys. Bypasses bellsEnabled guard.', contents: [ - { field: 'bell', type: 'int', required: true, notes: 'Bell channel index (0–15, 0-based). Firmware looks up the configured output and duration automatically.' }, + { field: 'bell', type: 'int', required: true, notes: 'Bell channel index (0–15, 0-based)' }, ], - returns: '{ "bell": 0, "output": 2, "duration_ms": 95 }', - example: '// Fire bell 0\n{ "v": 2, "cmd": "relay.test_bell", "contents": { "bell": 0 } }\n\n// Rapid succession — fire bell 0, then bell 1 immediately\n{ "v": 2, "cmd": "relay.test_bell", "contents": { "bell": 0 } }\n{ "v": 2, "cmd": "relay.test_bell", "contents": { "bell": 1 } }', + response: '{\n "status": "SUCCESS",\n "type": "relay.test_bell",\n "data": { "bell": 0, "output": 1, "duration_ms": 95 }\n} // output is 1-based', + errors: [ + { message: 'Missing required field: bell (0-based bell index)', condition: 'contents.bell is absent' }, + { message: 'bell must be 0-15', condition: 'bell is outside 0–15 range' }, + ], + example: '// Fire bell 0\n{ "v": 2, "cmd": "relay.test_bell", "contents": { "bell": 0 } }\n\n// Rapid succession\n{ "v": 2, "cmd": "relay.test_bell", "contents": { "bell": 0 } }\n{ "v": 2, "cmd": "relay.test_bell", "contents": { "bell": 1 } }', warning: null, }, { @@ -119,10 +229,15 @@ const NAMESPACES = [ transports: ['All'], description: 'Fire a list of bell channels simultaneously, like a single melody step. Each bell fires for its own configured duration. Non-blocking. Bypasses bellsEnabled guard.', contents: [ - { field: 'bells', type: 'int[]', required: true, notes: 'Array of bell channel indices (0–15, 0-based). All fire simultaneously.' }, + { field: 'bells', type: 'int[]', required: true, notes: 'Array of bell channel indices (0–15). All fire simultaneously.' }, ], - returns: '{ "fired": 3 }', - example: '// Fire bells 0, 2, and 4 at the same time\n{\n "v": 2,\n "cmd": "relay.test_batch",\n "contents": { "bells": [0, 2, 4] }\n}', + response: '{\n "status": "SUCCESS",\n "type": "relay.test_batch",\n "data": { "fired": 3 }\n}', + errors: [ + { message: 'Missing required field: bells (array of 0-based bell indices)', condition: 'contents.bells is absent or not an array' }, + { message: 'bells array must not be empty', condition: 'contents.bells is an empty array' }, + { message: 'No valid bell indices in array', condition: 'All entries in the array are out of range (0–15) or non-integer' }, + ], + example: '{\n "v": 2,\n "cmd": "relay.test_batch",\n "contents": { "bells": [0, 2, 4] }\n}', warning: null, }, ], @@ -132,29 +247,299 @@ const NAMESPACES = [ label: 'clock', description: 'Clock strike configuration, RTC time, face position, timezone, and silence periods. Handler: ClockHandler.', commands: [ - { cmd: 'clock.set_outputs', handler: 'ClockHandler', transports: ['All'], description: 'Set which relay outputs the clock strikes use. Partial updates accepted.', contents: [{ field: 'c1', type: 'int', required: false, notes: 'Relay output for the first clock channel' }, { field: 'c2', type: 'int', required: false, notes: 'Relay output for the second clock channel. At least one of c1/c2 required.' }], returns: null, example: '{ "v": 2, "cmd": "clock.set_outputs", "contents": { "c1": 4, "c2": 5 } }', warning: null }, - { cmd: 'clock.set_timings', handler: 'ClockHandler', transports: ['All'], description: 'Set clock strike timing configuration. At least one field required.', contents: [{ field: 'pulse_duration', type: 'int', required: false, notes: 'Duration of each clock output pulse in ms. Typical: 80–150 ms.' }, { field: 'pause_duration', type: 'int', required: false, notes: 'Pause between consecutive pulses (e.g. hour chimes) in ms.' }], returns: null, example: '{ "v": 2, "cmd": "clock.set_timings", "contents": { "pulse_duration": 100, "pause_duration": 500 } }', warning: null }, - { cmd: 'clock.set_alerts', handler: 'ClockHandler', transports: ['All'], description: 'Configure hourly/quarter alert behavior. Partial updates accepted — omitted fields are unchanged.', contents: [{ field: 'alert_type', type: 'string', required: false, notes: '"OFF" | "SINGLE" | "HOURS" — OFF disables alerts; SINGLE fires once per trigger; HOURS fires N times equal to the current hour count.' }, { field: 'alert_interval', type: 'int', required: false, notes: 'Number of times the alert bell rings per trigger (used with SINGLE mode).' }, { field: 'hour_bell', type: 'int', required: false, notes: 'Output index for the hour bell. 255 = disabled.' }, { field: 'half_bell', type: 'int', required: false, notes: 'Output index for the half-hour bell. 255 = disabled.' }, { field: 'quarter_bell', type: 'int', required: false, notes: 'Output index for the quarter-hour bell. 255 = disabled.' }], returns: null, example: '{ "v": 2, "cmd": "clock.set_alerts", "contents": { "alert_type": "HOURS", "hour_bell": 5 } }', warning: null }, - { cmd: 'clock.set_backlight', handler: 'ClockHandler', transports: ['All'], description: 'Configure LCD backlight behavior. Partial updates accepted.', contents: [{ field: 'backlight', type: 'bool', required: false, notes: 'Enable or disable automatic backlight control.' }, { field: 'backlight_output', type: 'int', required: false, notes: 'Relay output index used for the backlight.' }, { field: 'backlight_on', type: 'string', required: false, notes: 'Time to turn backlight on, format "HH:MM".' }, { field: 'backlight_off', type: 'string', required: false, notes: 'Time to turn backlight off, format "HH:MM".' }], returns: null, example: '{ "v": 2, "cmd": "clock.set_backlight", "contents": { "backlight": true, "backlight_output": 6, "backlight_on": "07:00", "backlight_off": "22:00" } }', warning: null }, - { cmd: 'clock.set_silence', handler: 'ClockHandler', transports: ['All'], description: 'Configure silence periods (quiet hours). Daytime and nighttime windows are independent. Partial updates accepted.', contents: [{ field: 'daytime_silence', type: 'bool', required: false, notes: 'Enable daytime silence window.' }, { field: 'daytime_on', type: 'string', required: false, notes: 'Start of daytime silence, format "HH:MM".' }, { field: 'daytime_off', type: 'string', required: false, notes: 'End of daytime silence, format "HH:MM".' }, { field: 'night_silence', type: 'bool', required: false, notes: 'Enable nighttime silence window.' }, { field: 'night_on', type: 'string', required: false, notes: 'Start of nighttime silence, format "HH:MM".' }, { field: 'night_off', type: 'string', required: false, notes: 'End of nighttime silence, format "HH:MM".' }], returns: null, example: '{ "v": 2, "cmd": "clock.set_silence", "contents": { "night_silence": true, "night_on": "22:00", "night_off": "07:00" } }', warning: null }, - { cmd: 'clock.set_time', handler: 'ClockHandler', transports: ['All'], description: 'Set the RTC time.', contents: [{ field: 'timestamp', type: 'int', required: true, notes: 'Unix epoch timestamp' }, { field: 'timezone_offset', type: 'int', required: false, notes: 'Offset in seconds (e.g. 7200 for UTC+2)' }, { field: 'dst_offset', type: 'int', required: false, notes: 'DST offset in seconds' }], returns: null, example: '{\n "v": 2,\n "cmd": "clock.set_time",\n "contents": { "timestamp": 1740000000, "timezone_offset": 7200 }\n}', warning: null }, - { cmd: 'clock.set_face', handler: 'ClockHandler', transports: ['All'], description: 'Set the physical analog clock face position.', contents: [{ field: 'hour', type: 'int', required: true, notes: 'Hour hand position (0–11)' }, { field: 'minute', type: 'int', required: true, notes: 'Minute hand position (0–59)' }], returns: null, example: '{ "v": 2, "cmd": "clock.set_face", "contents": { "hour": 10, "minute": 10 } }', warning: null }, - { cmd: 'clock.set_timezone', handler: 'ClockHandler', transports: ['All'], description: 'Set timezone independently of RTC time.', contents: [{ field: 'gmt_offset_sec', type: 'int', required: true, notes: 'GMT offset in seconds (e.g. 7200 for UTC+2)' }, { field: 'dst_offset_sec', type: 'int', required: false, notes: 'DST offset in seconds' }, { field: 'timezone_name', type: 'string', required: false, notes: 'IANA timezone name, e.g. "Europe/Athens"' }], returns: null, example: '{\n "v": 2,\n "cmd": "clock.set_timezone",\n "contents": { "gmt_offset_sec": 7200, "dst_offset_sec": 3600, "timezone_name": "Europe/Athens" }\n}', warning: null }, - { cmd: 'clock.get_timezone', handler: 'ClockHandler', transports: ['All'], description: 'Read current timezone settings.', contents: null, returns: '{ "gmt_offset_sec": 7200, "dst_offset_sec": 3600, "ntp_server": "pool.ntp.org" }', example: '{ "v": 2, "cmd": "clock.get_timezone" }', warning: null }, - { cmd: 'clock.sync_ntp', handler: 'ClockHandler', transports: ['All'], description: 'Force an immediate NTP time sync.', contents: null, returns: null, example: '{ "v": 2, "cmd": "clock.sync_ntp" }', warning: null }, - { cmd: 'clock.enable', handler: 'ClockHandler', transports: ['All'], description: 'Enable clock strike output.', contents: null, returns: null, example: '{ "v": 2, "cmd": "clock.enable" }', warning: null }, - { cmd: 'clock.disable', handler: 'ClockHandler', transports: ['All'], description: 'Disable clock strike output.', contents: null, returns: null, example: '{ "v": 2, "cmd": "clock.disable" }', warning: null }, - { cmd: 'clock.pause', handler: 'ClockHandler', transports: ['All'], description: 'Pause clock updates.', contents: null, returns: null, example: '{ "v": 2, "cmd": "clock.pause" }', warning: null }, - { cmd: 'clock.resume', handler: 'ClockHandler', transports: ['All'], description: 'Resume clock updates.', contents: null, returns: null, example: '{ "v": 2, "cmd": "clock.resume" }', warning: null }, - { cmd: 'clock.get_face', handler: 'ClockHandler', transports: ['All'], description: 'Read current analog clock face position.', contents: null, returns: '{ "clock_hour": 10, "clock_minute": 30, "last_sync_time": 1740000000, "next_output_is_c1": true }', example: '{ "v": 2, "cmd": "clock.get_face" }', warning: null }, - { cmd: 'clock.get_config', handler: 'ClockHandler', transports: ['All'], description: 'Read full clock configuration — outputs, timings, alerts, backlight, silence, enabled state.', contents: null, returns: '{\n "enabled": true,\n "c1output": 4, "c2output": 5,\n "pulse_duration": 100, "pause_duration": 500,\n "physical_hour": 10, "physical_minute": 30,\n "next_is_c1": true, "last_sync_time": 1740000000,\n "alert_type": "HOURS", "alert_interval": 1,\n "hour_bell": 5, "half_bell": 255, "quarter_bell": 255,\n "backlight": true, "backlight_output": 6,\n "backlight_on": "07:00", "backlight_off": "22:00",\n "daytime_silence": false, "daytime_on": "00:00", "daytime_off": "00:00",\n "night_silence": true, "night_on": "22:00", "night_off": "07:00"\n}', example: '{ "v": 2, "cmd": "clock.get_config" }', warning: null }, + { + cmd: 'clock.set_outputs', + handler: 'ClockHandler', + transports: ['All'], + description: 'Set which relay outputs the clock strikes use.', + contents: [ + { field: 'c1', type: 'int', required: false, notes: 'Relay output for C1 (ODD strikes)' }, + { field: 'c2', type: 'int', required: false, notes: 'Relay output for C2 (EVEN strikes). At least one of c1/c2 required.' }, + ], + response: '{ "status": "SUCCESS", "type": "clock.set_outputs", "message": "Clock outputs updated and saved" }', + errors: [ + { message: 'Updated but failed to save to SD', condition: 'SD card write failed' }, + ], + example: '{ "v": 2, "cmd": "clock.set_outputs", "contents": { "c1": 4, "c2": 5 } }', + warning: null, + }, + { + cmd: 'clock.set_timings', + handler: 'ClockHandler', + transports: ['All'], + description: 'Set clock pulse and pause durations. At least one field required.', + contents: [ + { field: 'pulse_duration', type: 'int', required: false, notes: 'Duration of each clock output pulse in ms. Typical: 80–150 ms.' }, + { field: 'pause_duration', type: 'int', required: false, notes: 'Pause between consecutive pulses (e.g. hour chimes) in ms.' }, + ], + response: '{ "status": "SUCCESS", "type": "clock.set_timings", "message": "Clock timings updated and saved" }', + errors: [ + { message: 'No timing fields provided (pulse_duration, pause_duration)', condition: 'Neither pulse_duration nor pause_duration present in contents' }, + { message: 'Updated but failed to save to SD', condition: 'SD card write failed' }, + ], + example: '{ "v": 2, "cmd": "clock.set_timings", "contents": { "pulse_duration": 100, "pause_duration": 500 } }', + warning: null, + }, + { + cmd: 'clock.set_alerts', + handler: 'ClockHandler', + transports: ['All'], + description: 'Configure hourly/quarter alert behavior. Partial updates accepted.', + contents: [ + { field: 'alert_type', type: 'string', required: false, notes: '"OFF" | "SINGLE" | "HOURS" — OFF disables alerts; SINGLE fires once per trigger; HOURS fires N times equal to the current hour count.' }, + { field: 'alert_interval', type: 'int', required: false, notes: 'Ring count per trigger (SINGLE mode).' }, + { field: 'hour_bell', type: 'int', required: false, notes: 'Bell number to ring on the hour (1-based). The bell fires on whichever output it is assigned to in relay config. 255 = disabled.' }, + { field: 'half_bell', type: 'int', required: false, notes: 'Bell number to ring at half past (1-based). 255 = disabled.' }, + { field: 'quarter_bell', type: 'int', required: false, notes: 'Bell number to ring at quarter past/to (1-based). 255 = disabled.' }, + ], + response: '{ "status": "SUCCESS", "type": "clock.set_alerts", "message": "Clock alerts updated and saved" }', + errors: [ + { message: 'Updated but failed to save to SD', condition: 'SD card write failed' }, + ], + example: '{ "v": 2, "cmd": "clock.set_alerts", "contents": { "alert_type": "HOURS", "hour_bell": 5 } }', + warning: null, + }, + { + cmd: 'clock.set_backlight', + handler: 'ClockHandler', + transports: ['All'], + description: 'Configure LCD backlight behavior. Partial updates accepted.', + contents: [ + { field: 'backlight', type: 'bool', required: false, notes: 'Enable automatic backlight control.' }, + { field: 'backlight_output', type: 'int', required: false, notes: 'Relay output index for backlight.' }, + { field: 'backlight_on', type: 'string', required: false, notes: 'Time to turn on, format "HH:MM".' }, + { field: 'backlight_off', type: 'string', required: false, notes: 'Time to turn off, format "HH:MM".' }, + ], + response: '{ "status": "SUCCESS", "type": "clock.set_backlight", "message": "Clock backlight updated and saved" }', + errors: [ + { message: 'Updated but failed to save to SD', condition: 'SD card write failed' }, + ], + example: '{ "v": 2, "cmd": "clock.set_backlight", "contents": { "backlight": true, "backlight_output": 6, "backlight_on": "07:00", "backlight_off": "22:00" } }', + warning: null, + }, + { + cmd: 'clock.set_silence', + handler: 'ClockHandler', + transports: ['All'], + description: 'Configure silence periods (quiet hours). Daytime and nighttime windows are independent. Partial updates accepted.', + contents: [ + { field: 'daytime_silence', type: 'bool', required: false, notes: 'Enable daytime silence window.' }, + { field: 'daytime_on', type: 'string', required: false, notes: 'Start of daytime silence "HH:MM".' }, + { field: 'daytime_off', type: 'string', required: false, notes: 'End of daytime silence "HH:MM".' }, + { field: 'night_silence', type: 'bool', required: false, notes: 'Enable nighttime silence window.' }, + { field: 'night_on', type: 'string', required: false, notes: 'Start of nighttime silence "HH:MM".' }, + { field: 'night_off', type: 'string', required: false, notes: 'End of nighttime silence "HH:MM".' }, + ], + response: '{ "status": "SUCCESS", "type": "clock.set_silence", "message": "Clock silence periods updated and saved" }', + errors: [ + { message: 'Updated but failed to save to SD', condition: 'SD card write failed' }, + ], + example: '{ "v": 2, "cmd": "clock.set_silence", "contents": { "night_silence": true, "night_on": "22:00", "night_off": "07:00" } }', + warning: null, + }, + { + cmd: 'clock.set_config', + handler: 'ClockHandler', + transports: ['All'], + description: 'Combined command to set clock hardware settings in one round trip. Replaces sending clock.enable/disable + clock.set_outputs + clock.set_timings separately. All fields optional — at least one required.', + contents: [ + { field: 'enabled', type: 'bool', required: false, notes: 'Enable or disable the physical clock motor.' }, + { field: 'c1', type: 'int', required: false, notes: 'Relay output for clock pulse 1 (1-based). 0 = disabled.' }, + { field: 'c2', type: 'int', required: false, notes: 'Relay output for clock pulse 2 (1-based). 0 = disabled.' }, + { field: 'pulse_duration', type: 'int', required: false, notes: 'Duration of each clock pulse in milliseconds.' }, + { field: 'pause_duration', type: 'int', required: false, notes: 'Pause between C1 and C2 pulses in milliseconds.' }, + ], + response: '{ "status": "SUCCESS", "type": "clock.set_config", "message": "Clock configuration updated and saved" }', + errors: [ + { message: 'Provide at least one field: enabled, c1, c2, pulse_duration, pause_duration', condition: 'No known fields present in contents' }, + { message: 'Settings applied but failed to save to SD', condition: 'SD card write failed after applying values' }, + ], + example: '{ "v": 2, "cmd": "clock.set_config", "contents": { "enabled": true, "c1": 5, "c2": 6, "pulse_duration": 100, "pause_duration": 500 } }', + warning: null, + }, + { + cmd: 'clock.set_alerts_config', + handler: 'ClockHandler', + transports: ['All'], + description: 'Combined command to set alert behavior and silence periods in one round trip. Replaces sending clock.set_alerts + clock.set_silence separately. All fields optional — at least one required.', + contents: [ + { field: 'alert_type', type: 'string', required: false, notes: '"OFF" | "SINGLE" | "HOURS" — OFF disables alerts; SINGLE fires once per trigger; HOURS fires N times equal to the current hour count.' }, + { field: 'alert_interval', type: 'int', required: false, notes: 'Ring count per trigger (SINGLE mode).' }, + { field: 'hour_bell', type: 'int', required: false, notes: 'Bell number to ring on the hour (1-based). The bell fires on whichever output it is assigned to in relay config. 255 = disabled.' }, + { field: 'half_bell', type: 'int', required: false, notes: 'Bell number to ring at half past (1-based). 255 = disabled.' }, + { field: 'quarter_bell', type: 'int', required: false, notes: 'Bell number to ring at quarter past/to (1-based). 255 = disabled.' }, + { field: 'daytime_silence', type: 'bool', required: false, notes: 'Enable daytime silence window.' }, + { field: 'daytime_on', type: 'string', required: false, notes: 'Start of daytime silence "HH:MM".' }, + { field: 'daytime_off', type: 'string', required: false, notes: 'End of daytime silence "HH:MM".' }, + { field: 'night_silence', type: 'bool', required: false, notes: 'Enable nighttime silence window.' }, + { field: 'night_on', type: 'string', required: false, notes: 'Start of nighttime silence "HH:MM".' }, + { field: 'night_off', type: 'string', required: false, notes: 'End of nighttime silence "HH:MM".' }, + ], + response: '{ "status": "SUCCESS", "type": "clock.set_alerts_config", "message": "Clock alerts and silence configuration updated and saved" }', + errors: [ + { message: 'Provide at least one of: alert_type, alert_interval, hour_bell, half_bell, quarter_bell, daytime_silence, daytime_on, daytime_off, night_silence, night_on, night_off', condition: 'No known fields present in contents' }, + { message: 'Settings applied but failed to save to SD', condition: 'SD card write failed after applying values' }, + ], + example: '{\n "v": 2,\n "cmd": "clock.set_alerts_config",\n "contents": {\n "alert_type": "HOURS",\n "hour_bell": 3,\n "half_bell": 255,\n "quarter_bell": 255,\n "night_silence": true,\n "night_on": "22:00",\n "night_off": "07:00"\n }\n}', + warning: null, + }, + { + cmd: 'clock.set_time', + handler: 'ClockHandler', + transports: ['All'], + description: 'Set the RTC time directly.', + contents: [ + { field: 'timestamp', type: 'int', required: true, notes: 'Unix epoch timestamp (UTC or local depending on whether offsets are provided)' }, + { field: 'timezone_offset', type: 'int', required: false, notes: 'Total UTC offset in seconds (e.g. 7200 for UTC+2). Provide with dst_offset to use the combined-offset path.' }, + { field: 'dst_offset', type: 'int', required: false, notes: 'DST offset in seconds. Provide together with timezone_offset.' }, + ], + response: '{ "status": "SUCCESS", "type": "clock.set_time", "message": "RTC time updated successfully" }', + errors: [ + { message: 'Missing required field: timestamp', condition: 'contents.timestamp is absent' }, + { message: 'Invalid timestamp value', condition: 'timestamp is 0' }, + { message: 'RTC time set but verification failed', condition: 'RTC read-back returned 0 after the write — hardware issue' }, + ], + example: '{\n "v": 2,\n "cmd": "clock.set_time",\n "contents": { "timestamp": 1740000000, "timezone_offset": 7200, "dst_offset": 3600 }\n}', + warning: null, + }, + { + cmd: 'clock.set_face', + handler: 'ClockHandler', + transports: ['All'], + description: 'Set the physical analog clock face position (for clocktower synchronization).', + contents: [ + { field: 'hour', type: 'int', required: true, notes: 'Hour hand (0–23). Firmware converts to 12-h internally.' }, + { field: 'minute', type: 'int', required: true, notes: 'Minute hand (0–59).' }, + ], + response: '{ "status": "SUCCESS", "type": "clock.set_face", "message": "Physical clock face updated and saved" }', + errors: [ + { message: 'Missing required fields: hour, minute', condition: 'Either hour or minute is absent' }, + { message: 'hour must be 0-23', condition: 'hour is outside 0–23' }, + { message: 'minute must be 0-59', condition: 'minute is outside 0–59' }, + { message: 'Face set but failed to save to SD', condition: 'SD card write failed' }, + ], + example: '{ "v": 2, "cmd": "clock.set_face", "contents": { "hour": 10, "minute": 10 } }', + warning: null, + }, + { + cmd: 'clock.set_timezone', + handler: 'ClockHandler', + transports: ['All'], + description: 'Set timezone independently of RTC time. Does not alter the stored time value — only changes how it is displayed/offset.', + contents: [ + { field: 'gmt_offset_sec', type: 'int', required: true, notes: 'GMT offset in seconds (e.g. 7200 for UTC+2)' }, + { field: 'dst_offset_sec', type: 'int', required: false, notes: 'DST offset in seconds (e.g. 3600)' }, + ], + response: '{ "status": "SUCCESS", "type": "clock.set_timezone", "message": "Timezone updated and saved" }', + errors: [ + { message: 'Missing required field: gmt_offset_sec', condition: 'contents.gmt_offset_sec is absent' }, + { message: 'Timezone updated but failed to save to SD', condition: 'SD card write failed' }, + ], + example: '{ "v": 2, "cmd": "clock.set_timezone", "contents": { "gmt_offset_sec": 7200, "dst_offset_sec": 3600 } }', + warning: null, + }, + { + cmd: 'clock.get_timezone', + handler: 'ClockHandler', + transports: ['All'], + description: 'Read current timezone settings.', + contents: null, + response: '{\n "status": "SUCCESS",\n "type": "clock.get_timezone",\n "data": {\n "gmt_offset_sec": 7200,\n "dst_offset_sec": 3600,\n "ntp_server": "pool.ntp.org"\n }\n}', + errors: [], + example: '{ "v": 2, "cmd": "clock.get_timezone" }', + warning: null, + }, + { + cmd: 'clock.sync_ntp', + handler: 'ClockHandler', + transports: ['All'], + description: 'Force an immediate NTP time sync.', + contents: null, + response: '{ "status": "SUCCESS", "type": "clock.sync_ntp", "message": "NTP sync initiated" }', + errors: [], + example: '{ "v": 2, "cmd": "clock.sync_ntp" }', + warning: null, + }, + { + cmd: 'clock.enable', + handler: 'ClockHandler', + transports: ['All'], + description: 'Enable clock strike output. Covers both physical C1/C2 outputs and alert chimes.', + contents: null, + response: '{ "status": "SUCCESS", "type": "clock.enable", "message": "Clock enabled and saved" }', + errors: [ + { message: 'Enabled but failed to save to SD', condition: 'SD card write failed — state applied in RAM only' }, + ], + example: '{ "v": 2, "cmd": "clock.enable" }', + warning: null, + }, + { + cmd: 'clock.disable', + handler: 'ClockHandler', + transports: ['All'], + description: 'Disable clock strike output. Covers both physical C1/C2 outputs and alert chimes.', + contents: null, + response: '{ "status": "SUCCESS", "type": "clock.disable", "message": "Clock disabled and saved" }', + errors: [ + { message: 'Disabled but failed to save to SD', condition: 'SD card write failed — state applied in RAM only' }, + ], + example: '{ "v": 2, "cmd": "clock.disable" }', + warning: null, + }, + { + cmd: 'clock.pause', + handler: 'ClockHandler', + transports: ['All'], + description: 'Pause automatic clock updates (C1/C2 advance stops). Does not disable alerts.', + contents: null, + response: '{ "status": "SUCCESS", "type": "clock.pause", "message": "Clock updates paused" }', + errors: [], + example: '{ "v": 2, "cmd": "clock.pause" }', + warning: null, + }, + { + cmd: 'clock.resume', + handler: 'ClockHandler', + transports: ['All'], + description: 'Resume automatic clock updates.', + contents: null, + response: '{ "status": "SUCCESS", "type": "clock.resume", "message": "Clock updates resumed" }', + errors: [], + example: '{ "v": 2, "cmd": "clock.resume" }', + warning: null, + }, + { + cmd: 'clock.get_face', + handler: 'ClockHandler', + transports: ['All'], + description: 'Read current analog clock face position and sync state.', + contents: null, + response: '{\n "status": "SUCCESS",\n "type": "clock.get_face",\n "data": {\n "clock_hour": 10,\n "clock_minute": 30,\n "last_sync_time": 1740000000,\n "next_output_is_c1": true\n }\n}', + errors: [], + example: '{ "v": 2, "cmd": "clock.get_face" }', + warning: null, + }, + { + cmd: 'clock.get_config', + handler: 'ClockHandler', + transports: ['All'], + description: 'Read full clock configuration — outputs, timings, alerts, backlight, silence, enabled state.', + contents: null, + response: '{\n "status": "SUCCESS",\n "type": "clock.get_config",\n "data": {\n "enabled": true,\n "c1output": 4, "c2output": 5,\n "pulse_duration": 100, "pause_duration": 500,\n "physical_hour": 10, "physical_minute": 30,\n "next_is_c1": true, "last_sync_time": 1740000000,\n "alert_type": "HOURS", "alert_interval": 1,\n "hour_bell": 5, "half_bell": 255, "quarter_bell": 255,\n "backlight": true, "backlight_output": 6,\n "backlight_on": "07:00", "backlight_off": "22:00",\n "daytime_silence": false, "daytime_on": "00:00", "daytime_off": "00:00",\n "night_silence": true, "night_on": "22:00", "night_off": "07:00"\n }\n}', + errors: [], + example: '{ "v": 2, "cmd": "clock.get_config" }', + warning: null, + }, { cmd: 'clock.test_c1', handler: 'ClockHandler', transports: ['All'], - description: 'Fire the C1 (ODD) clock output immediately using the configured pulse duration. Non-blocking. Useful for verifying wiring and output assignment.', + description: 'Fire the C1 (ODD) clock output immediately using the configured pulse_duration. Non-blocking.', contents: null, - returns: '{ "output": "C1", "duration_ms": 100 }', + response: '{\n "status": "SUCCESS",\n "type": "clock.test_c1",\n "data": { "output": "C1", "duration_ms": 100 }\n}', + errors: [ + { message: 'OutputManager not available', condition: 'OutputManager reference was not injected (should not happen in normal operation)' }, + ], example: '{ "v": 2, "cmd": "clock.test_c1" }', warning: null, }, @@ -162,9 +547,12 @@ const NAMESPACES = [ cmd: 'clock.test_c2', handler: 'ClockHandler', transports: ['All'], - description: 'Fire the C2 (EVEN) clock output immediately using the configured pulse duration. Non-blocking. Useful for verifying wiring and output assignment.', + description: 'Fire the C2 (EVEN) clock output immediately using the configured pulse_duration. Non-blocking.', contents: null, - returns: '{ "output": "C2", "duration_ms": 100 }', + response: '{\n "status": "SUCCESS",\n "type": "clock.test_c2",\n "data": { "output": "C2", "duration_ms": 100 }\n}', + errors: [ + { message: 'OutputManager not available', condition: 'OutputManager reference was not injected' }, + ], example: '{ "v": 2, "cmd": "clock.test_c2" }', warning: null, }, @@ -175,15 +563,38 @@ const NAMESPACES = [ label: 'system', description: 'Device status, settings, health, and control. Handler: SystemHandler.', commands: [ - { cmd: 'system.status', handler: 'SystemHandler', transports: ['All'], description: 'Get current device status: player state, strike counters, projected run time.', contents: null, returns: '{\n "player_status": "STOPPED|PLAYING|PAUSED|STOPPING",\n "time_elapsed_ms": 0,\n "projected_run_time": 0,\n "strike_counters": { "0": 1234, "1": 567, ... }\n}', example: '{ "v": 2, "cmd": "system.status" }', warning: null }, - { cmd: 'system.get_time', handler: 'SystemHandler', transports: ['All'], description: 'Get current RTC time.', contents: null, returns: '{ "local_timestamp": 1740000000, "utc_timestamp": 1739993000, "year": 2025, "month": 2, "day": 20, "hour": 10, "minute": 30, "second": 0, "rtc_available": true }', example: '{ "v": 2, "cmd": "system.get_time" }', warning: null }, + { + cmd: 'system.status', + handler: 'SystemHandler', + transports: ['All'], + description: 'Get current player status, projected run time, and per-bell strike counters.', + contents: null, + response: '{\n "status": "SUCCESS",\n "type": "system.status",\n "data": {\n "player_status": "STOPPED",\n "time_elapsed_ms": 0,\n "projected_run_time": 0,\n "strike_counters": { "0": 1234, "1": 567, ... }\n }\n}', + errors: [], + example: '{ "v": 2, "cmd": "system.status" }', + warning: null, + }, + { + cmd: 'system.get_time', + handler: 'SystemHandler', + transports: ['All'], + description: 'Get current RTC time in local and UTC, plus individual date/time fields.', + contents: null, + response: '{\n "status": "SUCCESS",\n "type": "system.get_time",\n "data": {\n "local_timestamp": 1740000000,\n "utc_timestamp": 1739993000,\n "rtc_available": true,\n "year": 2025, "month": 2, "day": 20,\n "hour": 10, "minute": 30, "second": 0\n }\n}', + errors: [], + example: '{ "v": 2, "cmd": "system.get_time" }', + warning: null, + }, { cmd: 'system.get_config', handler: 'SystemHandler', transports: ['All'], description: 'Full ConfigManager dump — all saved settings grouped by subsystem. Alias: system.get_settings (backward compatible).', contents: null, - returns: '{\n "general": { "bells_enabled": true, "mqtt_enabled": true, "ota_channel": "stable", "serial_log": 3, "sd_log": 2, "mqtt_log": 1 },\n "bell": { "durations": {...}, "outputs": {...} },\n "clock": { ... },\n "time": { ... },\n "network": { ... },\n "mqtt": { ... }\n}', + response: '{\n "status": "SUCCESS",\n "type": "system.get_config",\n "data": {\n "general": {\n "bells_enabled": true, "mqtt_enabled": true,\n "ota_channel": "stable",\n "serial_log": 3, "sd_log": 2, "mqtt_log": 1\n },\n "bell": { "durations": {...}, "outputs": {...} },\n "clock": { ... },\n "time": { "gmtOffsetSec": 7200, "daylightOffsetSec": 3600, ... },\n "network": { "hostname": "...", "useStaticIP": false, ... },\n "mqtt": { "host": "...", "port": 1883, ... }\n }\n}', + errors: [ + { message: 'Failed to serialize settings', condition: 'Internal JSON serialization error — rare' }, + ], example: '{ "v": 2, "cmd": "system.get_config" }', warning: null, }, @@ -191,9 +602,10 @@ const NAMESPACES = [ cmd: 'system.get_all', handler: 'SystemHandler', transports: ['All'], - description: 'Single-shot fetch of everything: device identity + full config + live telemetry snapshot + player status. Use when loading the console for the first time — avoids multiple round trips.', + description: 'Single-shot fetch of everything: device identity + full config + live telemetry + player status. Use on initial console load to avoid multiple round trips.', contents: null, - returns: '{\n "device": { "serial": "...", "hw_family": "VS", "hw_revision": "01", "fw_version": "143", "uptime_ms": 123456, "free_heap": 180000 },\n "config": { ... },\n "telemetry": { "strikes": {...}, "loads": {...}, "max_loads": {...}, "cooling_active": false, "guard_enabled": true, "lifetime_runtime_seconds": 1234567, "session_runtime_seconds": 3600, "total_playbacks": 4200 },\n "player": { "status": "STOPPED" }\n}', + response: '{\n "status": "SUCCESS",\n "type": "system.get_all",\n "data": {\n "device": {\n "serial": "BSVSPR-26E11J-STD10R-M4SC2Z",\n "hw_family": "VS", "hw_revision": "01",\n "fw_version": "143",\n "uptime_ms": 123456, "free_heap": 180000\n },\n "config": { ... },\n "telemetry": {\n "strikes": {...}, "loads": {...}, "max_loads": {...},\n "cooling_active": false, "guard_enabled": true,\n "lifetime_runtime_seconds": 1234567,\n "session_runtime_seconds": 3600,\n "total_playbacks": 4200\n },\n "player": { "status": "STOPPED" }\n }\n}', + errors: [], example: '{ "v": 2, "cmd": "system.get_all" }', warning: null, }, @@ -201,16 +613,59 @@ const NAMESPACES = [ cmd: 'system.get_telemetry', handler: 'SystemHandler', transports: ['All'], - description: 'Live telemetry snapshot: strike counters, bell loads, guard state, runtime metrics, device uptime, and free heap. Read-only data — does not include config.', + description: 'Live telemetry snapshot: strike counters, bell loads, guard state, runtime metrics, uptime, free heap. Read-only — no config included.', contents: null, - returns: '{\n "strikes": { "0": 1234, ... },\n "loads": { "0": 42, ... },\n "max_loads": { "0": 500, ... },\n "cooling_active": false,\n "guard_enabled": true,\n "lifetime_runtime_seconds": 1234567,\n "session_runtime_seconds": 3600,\n "total_playbacks": 4200,\n "uptime_ms": 12345678,\n "free_heap": 178000\n}', + response: '{\n "status": "SUCCESS",\n "type": "system.get_telemetry",\n "data": {\n "strikes": { "0": 1234, ... },\n "loads": { "0": 42, ... },\n "max_loads":{ "0": 500, ... },\n "cooling_active": false,\n "guard_enabled": true,\n "lifetime_runtime_seconds": 1234567,\n "session_runtime_seconds": 3600,\n "total_playbacks": 4200,\n "uptime_ms": 12345678,\n "free_heap": 178000\n }\n}', + errors: [], example: '{ "v": 2, "cmd": "system.get_telemetry" }', warning: null, }, - { cmd: 'system.get_device_info', handler: 'SystemHandler', transports: ['All'], description: 'Get device identity and runtime stats.', contents: null, returns: '{ "uid": "PV000000000000", "hw_type": "VS", "hw_version": "01", "fw_version": "143", "uptime_ms": 12345678, "free_heap": 178000, "min_free_heap": 150000 }', example: '{ "v": 2, "cmd": "system.get_device_info" }', warning: null }, - { cmd: 'system.health', handler: 'SystemHandler', transports: ['All'], description: 'Full HealthMonitor report — all subsystem states, warnings, and critical failures.', contents: null, returns: '{ "critical_count": 0, "warning_count": 1, "firmware_stable": true, "subsystems": [{ "name": "BellEngine", "status": "HEALTHY|WARNING|CRITICAL|FAILED", "error": "..." }, ...] }', example: '{ "v": 2, "cmd": "system.health" }', warning: null }, - { cmd: 'system.factory_reset', handler: 'SystemHandler', transports: ['All'], description: 'Reset all settings to factory defaults. Device must be restarted to apply.', contents: null, returns: null, example: '{ "v": 2, "cmd": "system.factory_reset" }', warning: 'Non-reversible. All saved configuration is permanently wiped.' }, - { cmd: 'system.restart', handler: 'SystemHandler', transports: ['All'], description: 'Reboot the device. Response is sent before reboot (2 s delay).', contents: null, returns: null, example: '{ "v": 2, "cmd": "system.restart" }', warning: null }, + { + cmd: 'system.get_device_info', + handler: 'SystemHandler', + transports: ['All'], + description: 'Get device identity and runtime stats.', + contents: null, + response: '{\n "status": "SUCCESS",\n "type": "system.get_device_info",\n "data": {\n "uid": "BSVSPR-26E11J-STD10R-M4SC2Z",\n "hw_type": "VS", "hw_version": "01",\n "fw_version": "143",\n "uptime_ms": 12345678,\n "free_heap": 178000,\n "min_free_heap": 150000\n }\n}', + errors: [], + example: '{ "v": 2, "cmd": "system.get_device_info" }', + warning: null, + }, + { + cmd: 'system.health', + handler: 'SystemHandler', + transports: ['All'], + description: 'Full HealthMonitor report — all subsystem states, warnings, and critical failures.', + contents: null, + response: '{\n "status": "SUCCESS",\n "type": "system.health",\n "data": {\n "critical_count": 0,\n "warning_count": 1,\n "firmware_stable": true,\n "subsystems": [\n { "name": "BellEngine", "status": "HEALTHY" },\n { "name": "OutputManager", "status": "WARNING", "error": "..." },\n ...\n ]\n }\n}', + errors: [], + example: '{ "v": 2, "cmd": "system.health" }', + warning: null, + }, + { + cmd: 'system.factory_reset', + handler: 'SystemHandler', + transports: ['All'], + description: 'Reset all settings to factory defaults. Device must be restarted to apply.', + contents: null, + response: '{ "status": "SUCCESS", "type": "system.factory_reset", "message": "Factory reset complete. Restart device to apply." }', + errors: [ + { message: 'Factory reset partially applied — some settings may not have saved', condition: 'One or more ConfigManager save calls failed during reset' }, + ], + example: '{ "v": 2, "cmd": "system.factory_reset" }', + warning: 'Non-reversible. All saved configuration is permanently wiped.', + }, + { + cmd: 'system.restart', + handler: 'SystemHandler', + transports: ['All'], + description: 'Reboot the device. Response is sent before reboot (2 s delay).', + contents: null, + response: '{ "status": "SUCCESS", "type": "system.restart", "message": "Device will restart in 2 seconds" }', + errors: [], + example: '{ "v": 2, "cmd": "system.restart" }', + warning: null, + }, ], }, { @@ -218,9 +673,44 @@ const NAMESPACES = [ label: 'firmware', description: 'OTA slot management — validate, commit, and roll back firmware. Handler: FirmwareHandler.', commands: [ - { cmd: 'firmware.status', handler: 'FirmwareHandler', transports: ['All'], description: 'Get firmware validation state.', contents: null, returns: '{ "validation_state", "version", "boot_count", "can_commit", "can_rollback" }', example: '{ "v": 2, "cmd": "firmware.status" }', warning: null }, - { cmd: 'firmware.commit', handler: 'FirmwareHandler', transports: ['All'], description: 'Commit the current firmware as permanent (marks OTA slot as valid).', contents: null, returns: null, example: '{ "v": 2, "cmd": "firmware.commit" }', warning: null }, - { cmd: 'firmware.rollback', handler: 'FirmwareHandler', transports: ['All'], description: 'Roll back to the previous firmware. Device reboots.', contents: null, returns: null, example: '{ "v": 2, "cmd": "firmware.rollback" }', warning: 'Device will reboot immediately after this command.' }, + { + cmd: 'firmware.status', + handler: 'FirmwareHandler', + transports: ['All'], + description: 'Get firmware validation state, version, boot count, and commit/rollback eligibility.', + contents: null, + response: '{\n "status": "SUCCESS",\n "type": "firmware.status",\n "data": {\n "validation_state": "VALIDATED",\n "current_version": "143",\n "is_testing": false,\n "is_valid": true,\n "boot_count": 12,\n "build_date": "2026-07-10",\n "ota_channel": "stable",\n "can_commit": false,\n "can_rollback": false\n }\n}', + errors: [], + example: '{ "v": 2, "cmd": "firmware.status" }', + warning: null, + }, + { + cmd: 'firmware.commit', + handler: 'FirmwareHandler', + transports: ['All'], + description: 'Commit the current firmware as permanent (marks OTA slot as valid). Only valid during a testing window.', + contents: null, + response: '{ "status": "SUCCESS", "type": "firmware.commit", "message": "Firmware committed successfully" }', + errors: [ + { message: 'No firmware validation in progress', condition: 'Device is not in testing mode — firmware already committed or no OTA was performed' }, + { message: 'Failed to commit firmware', condition: 'esp_ota_mark_app_valid_cancel_rollback() threw an exception' }, + ], + example: '{ "v": 2, "cmd": "firmware.commit" }', + warning: null, + }, + { + cmd: 'firmware.rollback', + handler: 'FirmwareHandler', + transports: ['All'], + description: 'Roll back to the previous OTA slot. Device reboots immediately.', + contents: null, + response: '{ "status": "SUCCESS", "type": "firmware.rollback", "message": "Firmware rollback initiated — device will reboot" }', + errors: [ + { message: 'Failed to initiate firmware rollback', condition: 'esp_ota_mark_app_invalid_rollback_and_reboot() threw an exception' }, + ], + example: '{ "v": 2, "cmd": "firmware.rollback" }', + warning: 'Device will reboot immediately after this command.', + }, ], }, { @@ -234,9 +724,12 @@ const NAMESPACES = [ transports: ['All'], description: 'Trigger OTA update from the VPS update server using the configured channel. Device reboots after flashing.', contents: [ - { field: 'channel', type: 'string', required: false, notes: 'Update channel: "stable" or "beta". If omitted, uses the channel saved in config.' }, + { field: 'channel', type: 'string', required: false, notes: '"stable", "beta", or "development". If omitted, uses the channel saved in config.' }, + ], + response: '{ "status": "SUCCESS", "type": "ota.update", "message": "OTA update started from channel: stable" }', + errors: [ + { message: 'Cannot update while playback is active', condition: 'Player is currently PLAYING' }, ], - returns: null, example: '{ "v": 2, "cmd": "ota.update" }\n{ "v": 2, "cmd": "ota.update", "contents": { "channel": "stable" } }', warning: 'Device will reboot after flashing. Ensure playback is stopped first.', }, @@ -246,12 +739,16 @@ const NAMESPACES = [ transports: ['All'], description: 'Flash firmware from a custom HTTP URL. Use for staging builds or manual pushes.', contents: [ - { field: 'firmware_url', type: 'string', required: true, notes: 'Direct HTTP download URL for the .bin firmware file. HTTPS not supported.' }, + { field: 'firmware_url', type: 'string', required: true, notes: 'Direct HTTP download URL for the .bin file. HTTPS not supported.' }, { field: 'checksum', type: 'string', required: false, notes: 'Optional integrity checksum.' }, { field: 'file_size', type: 'int', required: false, notes: 'Optional file size in bytes.' }, { field: 'version', type: 'string', required: false, notes: 'Version label for display/logging.' }, ], - returns: null, + response: '{ "status": "SUCCESS", "type": "ota.custom", "message": "Custom OTA update started" }', + errors: [ + { message: 'Missing required field: firmware_url', condition: 'contents.firmware_url is absent' }, + { message: 'Cannot update while playback is active', condition: 'Player is currently PLAYING' }, + ], example: '{\n "v": 2,\n "cmd": "ota.custom",\n "contents": {\n "firmware_url": "http://example.com/firmware.bin",\n "version": "143"\n }\n}', warning: 'Device will reboot after flashing.', }, @@ -261,9 +758,13 @@ const NAMESPACES = [ transports: ['All'], description: 'Persist the OTA update channel. Subsequent ota.update calls without a channel argument will use this value.', contents: [ - { field: 'channel', type: 'string', required: true, notes: '"stable" or "beta"' }, + { field: 'channel', type: 'string', required: true, notes: '"stable", "beta", or "development"' }, + ], + response: '{\n "status": "SUCCESS",\n "type": "ota.set_channel",\n "data": { "channel": "beta" }\n}', + errors: [ + { message: 'Missing required field: channel', condition: 'contents.channel is absent' }, + { message: 'Invalid channel. Valid values: stable, beta, development', condition: 'channel is not one of the three allowed values' }, ], - returns: null, example: '{ "v": 2, "cmd": "ota.set_channel", "contents": { "channel": "beta" } }', warning: null, }, @@ -274,9 +775,55 @@ const NAMESPACES = [ label: 'network', description: 'Network configuration and connection status. Handler: NetworkHandler.', commands: [ - { cmd: 'network.info', handler: 'NetworkHandler', transports: ['All'], description: 'Get IP address, gateway, and DNS.', contents: null, returns: '{ "ip", "gateway", "dns" }', example: '{ "v": 2, "cmd": "network.info" }', warning: null }, - { cmd: 'network.status', handler: 'NetworkHandler', transports: ['All'], description: 'Get full connection state including SSID, MAC, and hostname.', contents: null, returns: '{ "connected": true, "state": "CONNECTED", "type": "WiFi", "ip": "10.0.0.3", "rssi": -62, "ap_mode": false, "ssid": "MyNetwork", "mac": "AA:BB:CC:DD:EE:FF", "hostname": "vesper-lab" }', example: '{ "v": 2, "cmd": "network.status" }', warning: null }, - { cmd: 'network.set_config', handler: 'NetworkHandler', transports: ['All'], description: 'Update network configuration. Partial updates accepted. Returns restart_required flag.', contents: [{ field: 'hostname', type: 'string', required: false, notes: 'Device hostname on the network' }, { field: 'useStaticIP', type: 'bool', required: false, notes: 'Enable static IP mode' }, { field: 'ip', type: 'string', required: false, notes: 'Static IP address. Required together with gateway + subnet when using static IP.' }, { field: 'gateway', type: 'string', required: false, notes: 'Gateway address' }, { field: 'subnet', type: 'string', required: false, notes: 'Subnet mask' }, { field: 'dns1', type: 'string', required: false, notes: 'Primary DNS' }, { field: 'dns2', type: 'string', required: false, notes: 'Secondary DNS' }], returns: '{ "restart_required": true/false }', example: '{\n "v": 2,\n "cmd": "network.set_config",\n "contents": { "hostname": "vesper-lab", "useStaticIP": false }\n}', warning: null }, + { + cmd: 'network.info', + handler: 'NetworkHandler', + transports: ['All'], + description: 'Get IP address, gateway, and DNS.', + contents: null, + response: '{\n "status": "SUCCESS",\n "type": "network.info",\n "data": {\n "ip": "10.0.0.3",\n "gateway": "10.0.0.1",\n "dns": "8.8.8.8"\n }\n}', + errors: [], + example: '{ "v": 2, "cmd": "network.info" }', + warning: null, + }, + { + cmd: 'network.status', + handler: 'NetworkHandler', + transports: ['All'], + description: 'Get full connection state including SSID, MAC, hostname, RSSI, and AP mode flag.', + contents: null, + response: '{\n "status": "SUCCESS",\n "type": "network.status",\n "data": {\n "connected": true,\n "state": "CONNECTED_WIFI",\n "type": "WIFI",\n "ip": "10.0.0.3",\n "rssi": -62,\n "ap_mode": false,\n "ssid": "BellSystemsInfra",\n "mac": "AA:BB:CC:DD:EE:FF",\n "hostname": "vesper-lab"\n }\n}\n\n// Possible state values: DISCONNECTED | CONNECTING | CONNECTED_WIFI | CONNECTED_ETHERNET | RECONNECTING | AP_MODE\n// Possible type values: WIFI | ETHERNET | AP | NONE', + errors: [], + example: '{ "v": 2, "cmd": "network.status" }', + warning: null, + }, + { + cmd: 'network.set_config', + handler: 'NetworkHandler', + transports: ['All'], + description: 'Update network configuration. Partial updates accepted. Restart required to apply changes.', + contents: [ + { field: 'hostname', type: 'string', required: false, notes: 'Device hostname (1–32 chars)' }, + { field: 'useStaticIP', type: 'bool', required: false, notes: 'Enable static IP mode' }, + { field: 'ip', type: 'string', required: false, notes: 'Static IP. Required with gateway + subnet when useStaticIP is true.' }, + { field: 'gateway', type: 'string', required: false, notes: 'Gateway address' }, + { field: 'subnet', type: 'string', required: false, notes: 'Subnet mask' }, + { field: 'dns1', type: 'string', required: false, notes: 'Primary DNS' }, + { field: 'dns2', type: 'string', required: false, notes: 'Secondary DNS' }, + { field: 'permanent_ap_mode', type: 'bool', required: false, notes: 'Enable/disable permanent AP mode. If present, all other fields are ignored.' }, + ], + response: '{ "status": "SUCCESS", "type": "network.set_config", "message": "Network configuration saved. Restart device to apply changes." }', + errors: [ + { message: 'No network parameters provided', condition: 'Contents contains none of: hostname, useStaticIP, permanent_ap_mode' }, + { message: 'Hostname must be 1-32 characters', condition: 'hostname is empty or longer than 32 chars' }, + { message: 'Static IP requires: ip, gateway, subnet', condition: 'useStaticIP is true but ip, gateway, or subnet is missing' }, + { message: 'Invalid IP address format', condition: 'ip string cannot be parsed as an IPv4 address' }, + { message: 'Invalid gateway format', condition: 'gateway string cannot be parsed as an IPv4 address' }, + { message: 'Invalid subnet format', condition: 'subnet string cannot be parsed as an IPv4 address' }, + ], + example: '{\n "v": 2,\n "cmd": "network.set_config",\n "contents": { "hostname": "vesper-lab", "useStaticIP": false }\n}', + warning: null, + }, ], }, { @@ -284,10 +831,63 @@ const NAMESPACES = [ label: 'files', description: 'Melody file management on the SD card and built-in firmware storage. Handler: FileHandler.', commands: [ - { cmd: 'files.list', handler: 'FileHandler', transports: ['All'], description: 'List melody files on the SD card.', contents: null, returns: 'Array of SD card melody file objects', example: '{ "v": 2, "cmd": "files.list" }', warning: null }, - { cmd: 'files.list_builtin', handler: 'FileHandler', transports: ['All'], description: 'List all built-in melodies compiled into the firmware.', contents: null, returns: '[{ "name": "...", "uid": "..." }, ...]', example: '{ "v": 2, "cmd": "files.list_builtin" }', warning: null }, - { cmd: 'files.download', handler: 'FileHandler', transports: ['All'], description: 'Download a melody from the server to the SD card.', contents: [{ field: 'melodys_uid', type: 'string', required: true, notes: 'UID of the melody to download' }], returns: null, example: '{ "v": 2, "cmd": "files.download", "contents": { "melodys_uid": "ABC123" } }', warning: null }, - { cmd: 'files.delete', handler: 'FileHandler', transports: ['All'], description: 'Delete a melody file from the SD card.', contents: [{ field: 'name', type: 'string', required: true, notes: 'Filename including extension, e.g. "westminster.mel"' }], returns: null, example: '{ "v": 2, "cmd": "files.delete", "contents": { "name": "westminster.mel" } }', warning: null }, + { + cmd: 'files.list', + handler: 'FileHandler', + transports: ['All'], + description: 'List melody files on the SD card.', + contents: null, + response: '{\n "status": "SUCCESS",\n "type": "files.list",\n "data": {\n "files": [\n { "name": "westminster.bsm", "size": 1024 },\n ...\n ]\n }\n}', + errors: [ + { message: 'Failed to read melody list', condition: 'SD card unavailable or /melodies directory cannot be read' }, + ], + example: '{ "v": 2, "cmd": "files.list" }', + warning: null, + }, + { + cmd: 'files.list_builtin', + handler: 'FileHandler', + transports: ['All'], + description: 'List all built-in melodies compiled into the firmware.', + contents: null, + response: '{\n "status": "SUCCESS",\n "type": "files.list_builtin",\n "data": [\n { "name": "Westminster", "uid": "westminster" },\n ...\n ]\n}', + errors: [ + { message: 'Failed to read built-in melody list', condition: 'Internal JSON parse error (should never happen)' }, + ], + example: '{ "v": 2, "cmd": "files.list_builtin" }', + warning: null, + }, + { + cmd: 'files.download', + handler: 'FileHandler', + transports: ['All'], + description: 'Download a melody from the server to the SD card.', + contents: [ + { field: 'melodys_uid', type: 'string', required: true, notes: 'UID of the melody to download' }, + ], + response: '{ "status": "SUCCESS", "type": "files.download", "message": "Melody downloaded: ABC123" }', + errors: [ + { message: 'Failed to download melody: ', condition: 'HTTP request failed, SD write failed, or melody not found on server' }, + ], + example: '{ "v": 2, "cmd": "files.download", "contents": { "melodys_uid": "ABC123" } }', + warning: null, + }, + { + cmd: 'files.delete', + handler: 'FileHandler', + transports: ['All'], + description: 'Delete a melody file from the SD card.', + contents: [ + { field: 'name', type: 'string', required: true, notes: 'Filename including extension, e.g. "westminster.bsm"' }, + ], + response: '{ "status": "SUCCESS", "type": "files.delete", "message": "Melody deleted: westminster.bsm" }', + errors: [ + { message: 'Missing required field: name', condition: 'contents.name is absent' }, + { message: 'Failed to delete melody: ', condition: 'File does not exist on SD or SD remove call failed' }, + ], + example: '{ "v": 2, "cmd": "files.delete", "contents": { "name": "westminster.bsm" } }', + warning: null, + }, ], }, { @@ -295,10 +895,62 @@ const NAMESPACES = [ label: 'log', description: 'Log verbosity control per output channel. Level 0 = off, 5 = verbose. Handler: LoggingHandler.', commands: [ - { cmd: 'log.set_serial', handler: 'LoggingHandler', transports: ['All'], description: 'Set log verbosity for the UART serial output.', contents: [{ field: 'level', type: 'int', required: true, notes: '0 = None, 1 = Error, 2 = Warning, 3 = Info, 4 = Debug, 5 = Verbose' }], returns: null, example: '{ "v": 2, "cmd": "log.set_serial", "contents": { "level": 3 } }', warning: null }, - { cmd: 'log.set_sd', handler: 'LoggingHandler', transports: ['All'], description: 'Set log verbosity for SD card logging.', contents: [{ field: 'level', type: 'int', required: true, notes: '0 = None, 1 = Error, 2 = Warning, 3 = Info, 4 = Debug, 5 = Verbose' }], returns: null, example: '{ "v": 2, "cmd": "log.set_sd", "contents": { "level": 2 } }', warning: null }, - { cmd: 'log.set_mqtt', handler: 'LoggingHandler', transports: ['All'], description: 'Set log verbosity for MQTT log publishing.', contents: [{ field: 'level', type: 'int', required: true, notes: '0 = None, 1 = Error, 2 = Warning, 3 = Info, 4 = Debug, 5 = Verbose' }], returns: null, example: '{ "v": 2, "cmd": "log.set_mqtt", "contents": { "level": 1 } }', warning: null }, - { cmd: 'log.get_config', handler: 'LoggingHandler', transports: ['All'], description: 'Read current log levels for all three output channels.', contents: null, returns: '{ "serial_level": 3, "sd_level": 2, "mqtt_level": 1 }', example: '{ "v": 2, "cmd": "log.get_config" }', warning: null }, + { + cmd: 'log.set_serial', + handler: 'LoggingHandler', + transports: ['All'], + description: 'Set log verbosity for the UART serial output.', + contents: [{ field: 'level', type: 'int', required: true, notes: '0 = None, 1 = Error, 2 = Warning, 3 = Info, 4 = Debug, 5 = Verbose' }], + response: '{ "status": "SUCCESS", "type": "log.set_serial", "message": "serial log level set to 3 and saved" }', + errors: [ + { message: 'Missing or invalid \'level\' (0-5 required)', condition: 'contents.level is absent or not a uint8' }, + { message: 'Invalid log level (must be 0-5)', condition: 'level is greater than 5' }, + { message: 'Level applied but failed to save to SD', condition: 'Level takes effect immediately but SD config write failed' }, + ], + example: '{ "v": 2, "cmd": "log.set_serial", "contents": { "level": 3 } }', + warning: null, + }, + { + cmd: 'log.set_sd', + handler: 'LoggingHandler', + transports: ['All'], + description: 'Set log verbosity for SD card logging.', + contents: [{ field: 'level', type: 'int', required: true, notes: '0 = None, 1 = Error, 2 = Warning, 3 = Info, 4 = Debug, 5 = Verbose' }], + response: '{ "status": "SUCCESS", "type": "log.set_sd", "message": "sd log level set to 2 and saved" }', + errors: [ + { message: 'Missing or invalid \'level\' (0-5 required)', condition: 'contents.level is absent or not a uint8' }, + { message: 'Invalid log level (must be 0-5)', condition: 'level is greater than 5' }, + { message: 'Level applied but failed to save to SD', condition: 'Level takes effect immediately but SD config write failed' }, + ], + example: '{ "v": 2, "cmd": "log.set_sd", "contents": { "level": 2 } }', + warning: null, + }, + { + cmd: 'log.set_mqtt', + handler: 'LoggingHandler', + transports: ['All'], + description: 'Set log verbosity for MQTT log publishing.', + contents: [{ field: 'level', type: 'int', required: true, notes: '0 = None, 1 = Error, 2 = Warning, 3 = Info, 4 = Debug, 5 = Verbose' }], + response: '{ "status": "SUCCESS", "type": "log.set_mqtt", "message": "mqtt log level set to 1 and saved" }', + errors: [ + { message: 'Missing or invalid \'level\' (0-5 required)', condition: 'contents.level is absent or not a uint8' }, + { message: 'Invalid log level (must be 0-5)', condition: 'level is greater than 5' }, + { message: 'Level applied but failed to save to SD', condition: 'Level takes effect immediately but SD config write failed' }, + ], + example: '{ "v": 2, "cmd": "log.set_mqtt", "contents": { "level": 1 } }', + warning: null, + }, + { + cmd: 'log.get_config', + handler: 'LoggingHandler', + transports: ['All'], + description: 'Read current log levels for all three output channels.', + contents: null, + response: '{\n "status": "SUCCESS",\n "type": "log.get_config",\n "data": { "serial_level": 3, "sd_level": 2, "mqtt_level": 1 }\n}', + errors: [], + example: '{ "v": 2, "cmd": "log.get_config" }', + warning: null, + }, ], }, { @@ -306,9 +958,43 @@ const NAMESPACES = [ label: 'mqtt', description: 'MQTT connectivity control. Handler: MQTTHandler.', commands: [ - { cmd: 'mqtt.enable', handler: 'MQTTHandler', transports: ['All'], description: 'Enable MQTT connectivity.', contents: null, returns: null, example: '{ "v": 2, "cmd": "mqtt.enable" }', warning: null }, - { cmd: 'mqtt.disable', handler: 'MQTTHandler', transports: ['All'], description: 'Disable MQTT connectivity.', contents: null, returns: null, example: '{ "v": 2, "cmd": "mqtt.disable" }', warning: null }, - { cmd: 'mqtt.get_config', handler: 'MQTTHandler', transports: ['All'], description: 'Read MQTT connection configuration from firmware (does not expose password).', contents: null, returns: '{ "enabled": true, "host": "72.61.191.197", "port": 1883, "user": "PV000000000000", "use_ssl": false }', example: '{ "v": 2, "cmd": "mqtt.get_config" }', warning: null }, + { + cmd: 'mqtt.enable', + handler: 'MQTTHandler', + transports: ['All'], + description: 'Enable MQTT connectivity. Persists to SD and triggers an immediate connect attempt.', + contents: null, + response: '{ "status": "SUCCESS", "type": "mqtt.enable", "message": "MQTT enabled and saved" }', + errors: [ + { message: 'MQTT state changed but failed to save to SD', condition: 'SD config write failed — state applied in RAM only' }, + ], + example: '{ "v": 2, "cmd": "mqtt.enable" }', + warning: null, + }, + { + cmd: 'mqtt.disable', + handler: 'MQTTHandler', + transports: ['All'], + description: 'Disable MQTT connectivity. Persists to SD and disconnects immediately.', + contents: null, + response: '{ "status": "SUCCESS", "type": "mqtt.disable", "message": "MQTT disabled and saved" }', + errors: [ + { message: 'MQTT state changed but failed to save to SD', condition: 'SD config write failed — state applied in RAM only' }, + ], + example: '{ "v": 2, "cmd": "mqtt.disable" }', + warning: null, + }, + { + cmd: 'mqtt.get_config', + handler: 'MQTTHandler', + transports: ['All'], + description: 'Read MQTT connection configuration. Password is never included in the response.', + contents: null, + response: '{\n "status": "SUCCESS",\n "type": "mqtt.get_config",\n "data": {\n "enabled": true,\n "host": "72.61.191.197",\n "port": 1883,\n "user": "BSVSPR-26E11J-STD10R-M4SC2Z",\n "use_ssl": false\n }\n}', + errors: [], + example: '{ "v": 2, "cmd": "mqtt.get_config" }', + warning: null, + }, ], }, { @@ -322,7 +1008,8 @@ const NAMESPACES = [ transports: ['All'], description: 'Get lifetime strike counts per bell channel (persisted to SD).', contents: null, - returns: '{ "strikes": { "0": 1234, "1": 567, "2": 890, ... } }', + response: '{\n "status": "SUCCESS",\n "type": "telemetry.get_strikes",\n "data": {\n "strikes": { "0": 1234, "1": 567, "2": 890, ... }\n }\n}', + errors: [], example: '{ "v": 2, "cmd": "telemetry.get_strikes" }', warning: null, }, @@ -330,19 +1017,23 @@ const NAMESPACES = [ cmd: 'telemetry.reset_strikes', handler: 'TelemetryHandler', transports: ['All'], - description: 'Reset strike counters. Omit bell to reset all channels; include it to reset a single channel. Changes are saved to SD immediately.', - contents: [{ field: 'bell', type: 'int', required: false, notes: 'Channel index (0–15). If omitted, all channels are reset.' }], - returns: null, - example: '// Reset all\n{ "v": 2, "cmd": "telemetry.reset_strikes" }\n\n// Reset one channel\n{ "v": 2, "cmd": "telemetry.reset_strikes", "contents": { "bell": 2 } }', + description: 'Reset all strike counters to zero. Saved to SD immediately. (Per-bell reset is not yet implemented — all channels are always reset.)', + contents: [ + { field: 'bell', type: 'int', required: false, notes: 'Currently ignored — all channels are reset regardless. Reserved for a future per-bell path.' }, + ], + response: '{ "status": "SUCCESS", "type": "telemetry.reset_strikes", "message": "All strike counts reset" }', + errors: [], + example: '{ "v": 2, "cmd": "telemetry.reset_strikes" }', warning: null, }, { cmd: 'telemetry.get_loads', handler: 'TelemetryHandler', transports: ['All'], - description: 'Get current heat load per bell channel, configured max loads, cooling/guard status, and overload flags.', + description: 'Get current heat load per bell channel, configured max loads, cooling status, guard state, and overloaded channel list.', contents: null, - returns: '{\n "loads": { "0": 42, "1": 0, ... },\n "max_loads": { "0": 500, "1": 500, ... },\n "cooling_active": false,\n "guard_enabled": true,\n "overloaded": []\n}', + response: '{\n "status": "SUCCESS",\n "type": "telemetry.get_loads",\n "data": {\n "loads": { "0": 42, "1": 0, ... },\n "max_loads":{ "0": 500, "1": 500, ... },\n "cooling_active": false,\n "guard_enabled": true,\n "overloaded": []\n }\n}', + errors: [], example: '{ "v": 2, "cmd": "telemetry.get_loads" }', warning: null, }, @@ -350,12 +1041,16 @@ const NAMESPACES = [ cmd: 'telemetry.set_max_loads', handler: 'TelemetryHandler', transports: ['All'], - description: 'Set the overload threshold for one or more bell channels. Use "all" to apply a single value across all channels, or "loads" to set per-channel values.', + description: 'Set overload threshold. Use "all" to set all channels at once, or "loads" for per-channel values. At least one is required.', contents: [ - { field: 'all', type: 'int', required: false, notes: 'Set all channels to this max load. e.g. 500' }, - { field: 'loads', type: 'object', required: false, notes: 'Per-channel map: { "0": 300, "3": 700, ... }. At least one of all/loads required.' }, + { field: 'all', type: 'int', required: false, notes: 'Set all 16 channels to this value. Must be > 0.' }, + { field: 'loads', type: 'object', required: false, notes: 'Per-channel map: { "0": 300, "3": 700, ... }. Values must be > 0.' }, + ], + response: '{ "status": "SUCCESS", "type": "telemetry.set_max_loads", "message": "All bell max loads updated" }', + errors: [ + { message: 'max_load must be > 0', condition: 'contents.all is 0' }, + { message: 'Provide \'all\' (uint16) or \'loads\' (object)', condition: 'Neither all nor loads is present in contents' }, ], - returns: null, example: '// Set all channels to 500\n{ "v": 2, "cmd": "telemetry.set_max_loads", "contents": { "all": 500 } }\n\n// Set specific channels\n{ "v": 2, "cmd": "telemetry.set_max_loads", "contents": { "loads": { "0": 300, "3": 700 } } }', warning: null, }, @@ -363,11 +1058,14 @@ const NAMESPACES = [ cmd: 'telemetry.set_guard_enabled', handler: 'TelemetryHandler', transports: ['All'], - description: 'Enable or disable the bell load guard. When disabled, the guard never triggers an emergency stop — load still accumulates and decays but no action is taken.', + description: 'Enable or disable the bell load guard. When disabled, the guard never triggers an emergency stop — load still decays but no action is taken.', contents: [ { field: 'enabled', type: 'bool', required: true, notes: 'true = guard active, false = guard bypassed' }, ], - returns: null, + response: '{ "status": "SUCCESS", "type": "telemetry.set_guard_enabled", "message": "Bell guard enabled" }', + errors: [ + { message: 'Missing required field: enabled (bool)', condition: 'contents.enabled is absent or not a boolean' }, + ], example: '{ "v": 2, "cmd": "telemetry.set_guard_enabled", "contents": { "enabled": false } }', warning: null, }, @@ -375,9 +1073,10 @@ const NAMESPACES = [ cmd: 'telemetry.get_metrics', handler: 'TelemetryHandler', transports: ['All'], - description: 'Read runtime metrics: total lifetime seconds, current session seconds, total melody playback count, and boot log (up to 200 entries).', + description: 'Read runtime metrics: lifetime seconds, current session seconds, total playback count, and guard state.', contents: null, - returns: '{\n "lifetime_runtime_seconds": 1234567,\n "session_runtime_seconds": 3600,\n "total_playbacks": 4200,\n "boot_log": [\n { "timestamp": 1740000000 },\n ...\n ]\n}', + response: '{\n "status": "SUCCESS",\n "type": "telemetry.get_metrics",\n "data": {\n "lifetime_runtime_seconds": 1234567,\n "session_runtime_seconds": 3600,\n "total_playbacks": 4200,\n "guard_enabled": true\n }\n}', + errors: [], example: '{ "v": 2, "cmd": "telemetry.get_metrics" }', warning: null, }, @@ -385,9 +1084,10 @@ const NAMESPACES = [ cmd: 'telemetry.reset_metrics', handler: 'TelemetryHandler', transports: ['All'], - description: 'Reset all lifetime runtime metrics (lifetime seconds, total playbacks, boot log). Strike counters are NOT reset — use telemetry.reset_strikes for that.', + description: 'Reset lifetime runtime and playback count to zero. Strike counters are NOT affected — use telemetry.reset_strikes for that.', contents: null, - returns: null, + response: '{ "status": "SUCCESS", "type": "telemetry.reset_metrics", "message": "Lifetime runtime and playback count reset to zero" }', + errors: [], example: '{ "v": 2, "cmd": "telemetry.reset_metrics" }', warning: 'Clears all historical runtime data. Cannot be undone.', }, @@ -404,7 +1104,10 @@ const NAMESPACES = [ transports: ['All'], description: 'Enable the bell mechanism. Allows BellEngine playback and clock alerts to fire. Manual relay tests (relay.test_*) bypass this flag regardless.', contents: null, - returns: null, + response: '{ "status": "SUCCESS", "type": "bells.enable", "message": "Bell mechanism enabled and saved" }', + errors: [ + { message: 'Bell mechanism enabled but failed to save to SD', condition: 'SD config write failed — state applied in RAM only' }, + ], example: '{ "v": 2, "cmd": "bells.enable" }', warning: null, }, @@ -412,9 +1115,12 @@ const NAMESPACES = [ cmd: 'bells.disable', handler: 'BellsHandler', transports: ['All'], - description: 'Disable the bell mechanism. Blocks BellEngine playback and clock alerts. Manual relay tests still work. Setting is persisted to SD.', + description: 'Disable the bell mechanism. Blocks BellEngine playback and clock alerts. Manual relay tests still work. Persisted to SD.', contents: null, - returns: null, + response: '{ "status": "SUCCESS", "type": "bells.disable", "message": "Bell mechanism disabled and saved" }', + errors: [ + { message: 'Bell mechanism disabled but failed to save to SD', condition: 'SD config write failed — state applied in RAM only' }, + ], example: '{ "v": 2, "cmd": "bells.disable" }', warning: null, }, @@ -424,7 +1130,8 @@ const NAMESPACES = [ transports: ['All'], description: 'Read bell mechanism enabled state plus all 16 channel durations and output assignments.', contents: null, - returns: '{\n "bells_enabled": true,\n "durations": { "0": 95, "1": 100, ... },\n "outputs": { "0": 2, "1": 3, ... }\n}', + response: '{\n "status": "SUCCESS",\n "type": "bells.get_config",\n "data": {\n "bells_enabled": true,\n "durations": { "0": 95, "1": 100, ... },\n "outputs": { "0": 1, "1": 2, ... } // 1-based: 1=first relay, 0=disabled\n }\n}', + errors: [], example: '{ "v": 2, "cmd": "bells.get_config" }', warning: null, }, @@ -483,7 +1190,10 @@ const LEGACY_MAP = [ { v1_cmd: 'bells', v1_action: 'set_enabled (enabled=false)', v2: 'bells.disable' }, ] -// Transport badge styles +// --------------------------------------------------------------------------- +// Styles +// --------------------------------------------------------------------------- + const TRANSPORT_META = { MQTT: { color: 'var(--color-info)', bg: 'var(--color-info-bg)' }, WebSocket: { color: 'var(--color-primary)', bg: 'var(--color-primary-subtle)' }, @@ -493,6 +1203,15 @@ const TRANSPORT_META = { All: { color: 'var(--color-text-muted)', bg: 'var(--color-bg-island)' }, } +const LABEL_STYLE = { + fontSize: 'var(--font-size-xs)', + fontWeight: 'var(--font-weight-semibold)', + color: 'var(--color-text-muted)', + textTransform: 'uppercase', + letterSpacing: 'var(--tracking-wide)', + marginBottom: 'var(--space-2)', +} + // --------------------------------------------------------------------------- // Sub-components // --------------------------------------------------------------------------- @@ -507,15 +1226,31 @@ function TransportPill({ name }) { fontSize: 'var(--font-size-xs)', fontFamily: 'var(--font-family-mono)', fontWeight: 'var(--font-weight-medium)', - color: meta.color, - backgroundColor: meta.bg, - whiteSpace: 'nowrap', + color: meta.color, backgroundColor: meta.bg, whiteSpace: 'nowrap', }}> {name} ) } +function MonoBlock({ code, color = 'var(--color-info)' }) { + return ( +
+      {code}
+    
+ ) +} + function CodeBlock({ code }) { const [copied, setCopied] = useState(false) const handleCopy = () => { @@ -526,36 +1261,17 @@ function CodeBlock({ code }) { } return (
-
-        {code}
-      
+
- {/* Command list */}
{visibleNamespaces.map(ns => )} {noResults && ( @@ -967,12 +1679,8 @@ export default function ApiReferencePage() { `}
- + - {/* Stats strip */}
{[ { label: 'Namespaces', value: NAMESPACES.length }, @@ -980,20 +1688,9 @@ export default function ApiReferencePage() { { label: 'Transports', value: 5 }, { label: 'Protocol', value: 'v2' }, ].map(stat => ( -
- - {stat.value} - - - {stat.label} - +
+ {stat.value} + {stat.label}
))}