// frontend/src/pages/developer/ApiReferencePage.jsx // Vesper Firmware API Reference v2 — Command explorer import { useState, useMemo } from 'react' import PageHeader from '@/components/ui/PageHeader' import Card from '@/components/ui/Card' import SearchBar from '@/components/ui/SearchBar' import Tabs from '@/components/ui/Tabs' // --------------------------------------------------------------------------- // Data // --------------------------------------------------------------------------- const TRANSPORTS = [ { transport: 'MQTT', address: 'vesper/{device_id}/control', direction: 'Inbound', notes: 'Commands in. Responses go to vesper/{device_id}/data' }, { transport: 'MQTT', address: 'vesper/{device_id}/data', direction: 'Outbound', notes: 'Responses + events' }, { transport: 'MQTT', address: 'vesper/{device_id}/status/heartbeat', direction: 'Outbound, retained', notes: 'Heartbeat every 30 s' }, { transport: 'MQTT', address: 'vesper/{device_id}/status/alerts', direction: 'Outbound, QoS 1', notes: 'Subsystem state changes (WARNING / CRITICAL / FAILED / CLEARED). Published on transition only.' }, { transport: 'MQTT', address: 'vesper/{device_id}/status/info', direction: 'Outbound, QoS 0', notes: 'Significant device events (playback start/stop, etc.).' }, { transport: 'WebSocket', address: 'ws://{device_ip}/ws', direction: 'Bidirectional', notes: 'Requires identify on connect' }, { transport: 'HTTP', address: 'http://{device_ip}/...', direction: 'REST', notes: 'Web console endpoints' }, { transport: 'UART', address: 'Hardware serial', direction: 'Bidirectional', notes: 'Restricted — whitelist only' }, { transport: 'UDP', address: 'Port 32101', direction: 'Discovery', notes: 'Passive — device announces itself' }, ] const UART_WHITELIST = [ 'ping', 'identify', 'playback.play', 'playback.stop', 'relay.test_bell', 'relay.test_batch', 'relay.test_output', 'clock.pause', 'clock.resume', 'clock.test_c1', 'clock.test_c2', 'system.get_time', 'system.status', 'system.get_config', 'system.get_telemetry', '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, 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, }, ], }, { id: 'playback', label: 'playback', description: 'Melody playback control. Handler: PlaybackHandler.', commands: [ { cmd: 'playback.play', handler: 'PlaybackHandler', transports: ['All'], description: 'Load and play a melody. All attributes except pid are optional — omitted values carry over from the previous command. Rejected if playback is already active (send playback.stop first).', contents: [ { field: 'pid', type: 'string', required: true, notes: 'Melody UID — matched against built-in library first, then /melodies/ on SD card.' }, { 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.' }, { 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".' }, ], 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)' }, ], 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, }, { cmd: 'playback.stop', handler: 'PlaybackHandler', transports: ['All'], description: 'Stop playback immediately. Safe to call even when player is already stopped.', contents: 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, }, ], }, { id: 'relay', 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.', 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. Player must be stopped.', contents: [ { 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' }, ], example: '{\n "v": 2,\n "cmd": "relay.test_output",\n "contents": { "output": 2, "duration_ms": 95 }\n}', warning: null, }, { cmd: 'relay.test_bell', handler: 'RelayHandler', transports: ['All'], 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)' }, ], 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, }, { cmd: 'relay.test_batch', handler: 'RelayHandler', 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). All fire simultaneously.' }, ], 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, }, ], }, { id: 'clock', 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.', 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.', contents: null, 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, }, { cmd: 'clock.test_c2', handler: 'ClockHandler', transports: ['All'], description: 'Fire the C2 (EVEN) clock output immediately using the configured pulse_duration. Non-blocking.', contents: null, 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, }, ], }, { id: 'system', label: 'system', description: 'Device status, settings, health, and control. Handler: SystemHandler.', commands: [ { 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, 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, }, { cmd: 'system.get_all', handler: 'SystemHandler', transports: ['All'], 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, 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, }, { cmd: 'system.get_telemetry', handler: 'SystemHandler', transports: ['All'], description: 'Live telemetry snapshot: strike counters, bell loads, guard state, runtime metrics, uptime, free heap. Read-only — no config included.', contents: null, 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, 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, }, ], }, { id: 'firmware', 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, 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.', }, ], }, { id: 'ota', label: 'ota', description: 'Over-the-air update management. Handler: FirmwareHandler.', commands: [ { cmd: 'ota.update', handler: 'FirmwareHandler', 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: '"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' }, ], 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.', }, { cmd: 'ota.custom', handler: 'FirmwareHandler', 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 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.' }, ], 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.', }, { cmd: 'ota.set_channel', handler: 'FirmwareHandler', 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", "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' }, ], example: '{ "v": 2, "cmd": "ota.set_channel", "contents": { "channel": "beta" } }', warning: null, }, ], }, { id: 'network', 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, 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, }, ], }, { id: 'files', 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, 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, }, ], }, { id: 'log', 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' }], 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, }, ], }, { id: 'mqtt', label: 'mqtt', description: 'MQTT connectivity control. Handler: MQTTHandler.', commands: [ { 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, }, ], }, { id: 'telemetry', label: 'telemetry', description: 'Bell strike counters, thermal load monitoring, and runtime metrics. Handler: TelemetryHandler.', commands: [ { cmd: 'telemetry.get_strikes', handler: 'TelemetryHandler', transports: ['All'], description: 'Get lifetime strike counts per bell channel (persisted to SD).', contents: null, 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, }, { cmd: 'telemetry.reset_strikes', handler: 'TelemetryHandler', transports: ['All'], 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 status, guard state, and overloaded channel list.', contents: null, 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, }, { cmd: 'telemetry.set_max_loads', handler: 'TelemetryHandler', transports: ['All'], 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 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' }, ], 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, }, { 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 decays but no action is taken.', contents: [ { field: 'enabled', type: 'bool', required: true, notes: 'true = guard active, false = guard bypassed' }, ], 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, }, { cmd: 'telemetry.get_metrics', handler: 'TelemetryHandler', transports: ['All'], description: 'Read runtime metrics: lifetime seconds, current session seconds, total playback count, and guard state.', contents: null, 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, }, { cmd: 'telemetry.reset_metrics', handler: 'TelemetryHandler', transports: ['All'], description: 'Reset lifetime runtime and playback count to zero. Strike counters are NOT affected — use telemetry.reset_strikes for that.', contents: 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.', }, ], }, { id: 'bells', label: 'bells', description: 'Bell mechanism master enable/disable and config read. Handler: BellsHandler.', commands: [ { cmd: 'bells.enable', handler: 'BellsHandler', 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, 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, }, { cmd: 'bells.disable', handler: 'BellsHandler', transports: ['All'], description: 'Disable the bell mechanism. Blocks BellEngine playback and clock alerts. Manual relay tests still work. Persisted to SD.', contents: 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, }, { cmd: 'bells.get_config', handler: 'BellsHandler', transports: ['All'], description: 'Read bell mechanism enabled state plus all 16 channel durations and output assignments.', contents: null, 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, }, ], }, ] const LEGACY_MAP = [ { v1_cmd: 'ping', v1_action: '—', v2: 'ping' }, { v1_cmd: 'identify', v1_action: '—', v2: 'identify' }, { v1_cmd: 'playback', v1_action: 'play', v2: 'playback.play' }, { v1_cmd: 'playback', v1_action: 'stop', v2: 'playback.stop' }, { v1_cmd: 'file_manager', v1_action: 'list_melodies', v2: 'files.list' }, { v1_cmd: 'file_manager', v1_action: 'download_melody', v2: 'files.download' }, { v1_cmd: 'file_manager', v1_action: 'delete_melody', v2: 'files.delete' }, { v1_cmd: 'relay_setup', v1_action: 'set_timings', v2: 'relay.set_durations' }, { v1_cmd: 'relay_setup', v1_action: 'set_outputs', v2: 'relay.set_outputs' }, { v1_cmd: 'clock_setup', v1_action: 'set_outputs', v2: 'clock.set_outputs' }, { v1_cmd: 'clock_setup', v1_action: 'set_timings', v2: 'clock.set_timings' }, { v1_cmd: 'clock_setup', v1_action: 'set_alerts', v2: 'clock.set_alerts' }, { v1_cmd: 'clock_setup', v1_action: 'set_backlight', v2: 'clock.set_backlight' }, { v1_cmd: 'clock_setup', v1_action: 'set_silence', v2: 'clock.set_silence' }, { v1_cmd: 'clock_setup', v1_action: 'set_rtc_time', v2: 'clock.set_time' }, { v1_cmd: 'clock_setup', v1_action: 'set_physical_clock_time', v2: 'clock.set_face' }, { v1_cmd: 'clock_setup', v1_action: 'pause_clock_updates', v2: 'clock.pause' }, { v1_cmd: 'clock_setup', v1_action: 'resume_clock_updates', v2: 'clock.resume' }, { v1_cmd: 'clock_setup', v1_action: 'set_enabled (enabled=true)', v2: 'clock.enable' }, { v1_cmd: 'clock_setup', v1_action: 'set_enabled (enabled=false)', v2: 'clock.disable' }, { v1_cmd: 'system_info', v1_action: 'report_status', v2: 'system.status' }, { v1_cmd: 'system_info', v1_action: 'get_device_time', v2: 'system.get_time' }, { v1_cmd: 'system_info', v1_action: 'get_clock_time', v2: 'clock.get_face' }, { v1_cmd: 'system_info', v1_action: 'get_firmware_status', v2: 'firmware.status' }, { v1_cmd: 'system_info', v1_action: 'network_info', v2: 'network.info' }, { v1_cmd: 'system_info', v1_action: 'get_full_settings', v2: 'system.get_config' }, { v1_cmd: 'system', v1_action: 'status', v2: 'system.status' }, { v1_cmd: 'system', v1_action: 'reset_defaults', v2: 'system.factory_reset' }, { v1_cmd: 'system', v1_action: 'commit_firmware', v2: 'firmware.commit' }, { v1_cmd: 'system', v1_action: 'rollback_firmware', v2: 'firmware.rollback' }, { v1_cmd: 'system', v1_action: 'get_firmware_status', v2: 'firmware.status' }, { v1_cmd: 'system', v1_action: 'set_network_config', v2: 'network.set_config' }, { v1_cmd: 'system', v1_action: 'set_serial_log_level', v2: 'log.set_serial' }, { v1_cmd: 'system', v1_action: 'set_sd_log_level', v2: 'log.set_sd' }, { v1_cmd: 'system', v1_action: 'set_mqtt_log_level', v2: 'log.set_mqtt' }, { v1_cmd: 'system', v1_action: 'set_mqtt_enabled (enabled=true)', v2: 'mqtt.enable' }, { v1_cmd: 'system', v1_action: 'set_mqtt_enabled (enabled=false)', v2: 'mqtt.disable' }, { v1_cmd: 'system', v1_action: 'restart', v2: 'system.restart' }, { v1_cmd: 'system', v1_action: 'reboot', v2: 'system.restart' }, { v1_cmd: 'system', v1_action: 'force_update', v2: 'ota.update' }, { v1_cmd: 'system', v1_action: 'custom_update', v2: 'ota.custom' }, { v1_cmd: 'system', v1_action: 'get_all_settings', v2: 'system.get_all' }, { v1_cmd: 'system', v1_action: 'get_telemetry', v2: 'system.get_telemetry' }, { v1_cmd: 'system', v1_action: 'get_full_settings', v2: 'system.get_config' }, { v1_cmd: 'telemetry', v1_action: 'get_bell_loads', v2: 'telemetry.get_loads' }, { v1_cmd: 'telemetry', v1_action: 'get_strike_counters', v2: 'telemetry.get_strikes' }, { v1_cmd: 'bells', v1_action: 'set_enabled (enabled=true)', v2: 'bells.enable' }, { v1_cmd: 'bells', v1_action: 'set_enabled (enabled=false)', v2: 'bells.disable' }, ] // --------------------------------------------------------------------------- // Styles // --------------------------------------------------------------------------- const TRANSPORT_META = { MQTT: { color: 'var(--color-info)', bg: 'var(--color-info-bg)' }, WebSocket: { color: 'var(--color-primary)', bg: 'var(--color-primary-subtle)' }, HTTP: { color: 'var(--color-success)', bg: 'var(--color-success-bg)' }, UART: { color: 'var(--color-warning)', bg: 'var(--color-warning-bg)' }, UDP: { color: 'var(--color-info)', bg: 'var(--color-info-bg)' }, 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 // --------------------------------------------------------------------------- function TransportPill({ name }) { const meta = TRANSPORT_META[name] || TRANSPORT_META.All return ( {name} ) } function MonoBlock({ code, color = 'var(--color-info)' }) { return (
      {code}
    
) } function CodeBlock({ code }) { const [copied, setCopied] = useState(false) const handleCopy = () => { navigator.clipboard.writeText(code).then(() => { setCopied(true) setTimeout(() => setCopied(false), 1800) }) } return (
) } function ContentsTable({ contents }) { return (
{['Field', 'Type', 'Req', 'Notes'].map(h => ( ))} {contents.map((field, i) => ( 0 ? '1px solid var(--color-border)' : 'none', backgroundColor: i % 2 === 1 ? 'rgba(192,193,255,0.015)' : 'transparent' }}> ))}
{h}
{field.field} {field.type} {field.required ? yes : no} {field.notes}
) } function ErrorsTable({ errors }) { return (
{['Error message', 'Condition'].map(h => ( ))} {errors.map((e, i) => ( 0 ? '1px solid var(--color-border)' : 'none', backgroundColor: i % 2 === 1 ? 'rgba(255,100,100,0.03)' : 'transparent' }}> ))}
{h}
{e.message} {e.condition}
) } function CommandCard({ command }) { const [open, setOpen] = useState(false) const isUartAllowed = UART_WHITELIST.includes(command.cmd) const hasErrors = command.errors && command.errors.length > 0 return (
{open && (

{command.description}

{command.warning && (
{command.warning}
)}
{/* Left column: meta + contents + response + errors */}
{/* Meta */}
Handler
{command.handler}
{isUartAllowed && (
UART
whitelisted
)}
{/* Contents */}
Contents
{command.contents ? :

No contents required.

}
{/* Success response */}
Success response
{/* Errors */}
Error responses
{hasErrors ? :

This command does not produce errors.

} {hasErrors && (

All error responses share the envelope: {`{ "status": "ERROR", "type": "", "message": "" }`}

)}
{/* Right column: example */}
Example
)}
) } function NamespaceSection({ ns, searchQuery }) { const filteredCommands = useMemo(() => { if (!searchQuery) return ns.commands const q = searchQuery.toLowerCase() return ns.commands.filter(c => c.cmd.toLowerCase().includes(q) || c.description.toLowerCase().includes(q) || c.handler.toLowerCase().includes(q) ) }, [ns.commands, searchQuery]) if (searchQuery && filteredCommands.length === 0) return null return (

{ns.label === 'Root' ? 'root' : ns.label}

{filteredCommands.length} {ns.description}
{filteredCommands.map(cmd => )}
) } // --------------------------------------------------------------------------- // Tab: Transports // --------------------------------------------------------------------------- function TransportsTab() { return (
{['Transport', 'Address / Topic', 'Direction', 'Notes'].map(h => ( ))} {TRANSPORTS.map((row, i) => ( 0 ? '1px solid var(--color-border)' : 'none', backgroundColor: i % 2 === 1 ? 'rgba(192,193,255,0.015)' : 'transparent' }}> ))}
{h}
{row.address} {row.direction} {row.notes}
{UART_WHITELIST.map(cmd => ( {cmd} ))}
Request
.",\n "contents": { ... } // optional\n}'} color="var(--color-success)" />
Response
",\n "message": "...", // text responses\n "data": { ... } // structured responses\n}'} />
{[ { field: 'v', type: 'int', notes: 'Protocol version. Always 2 for v2 firmware.' }, { field: 'cmd', type: 'string', notes: 'Command name, e.g. "relay.set_durations"' }, { field: 'contents', type: 'object?', notes: 'Optional payload. Omit entirely if not needed.' }, ].map(row => (
{row.field} {row.type}

{row.notes}

))}
) } // --------------------------------------------------------------------------- // Tab: Legacy migration // --------------------------------------------------------------------------- function LegacyTab() { return (
{['v1 cmd', 'v1 action', 'v2 command'].map(h => ( ))} {LEGACY_MAP.map((row, i) => ( 0 ? '1px solid var(--color-border)' : 'none', backgroundColor: i % 2 === 1 ? 'rgba(192,193,255,0.015)' : 'transparent' }}> ))}
{h}
{row.v1_cmd} {row.v1_action} {row.v2}
) } // --------------------------------------------------------------------------- // Tab: Commands // --------------------------------------------------------------------------- function CommandsTab({ activeNamespace, setActiveNamespace, searchQuery, setSearchQuery }) { const totalCmds = NAMESPACES.reduce((acc, ns) => acc + ns.commands.length, 0) const nsTabs = [{ key: 'all', label: 'All', count: totalCmds }, ...NAMESPACES.map(ns => ({ key: ns.id, label: ns.label, count: ns.commands.length }))] const visibleNamespaces = useMemo(() => { if (activeNamespace === 'all') return NAMESPACES return NAMESPACES.filter(ns => ns.id === activeNamespace) }, [activeNamespace]) const noResults = searchQuery && visibleNamespaces.every(ns => { const q = searchQuery.toLowerCase() return ns.commands.filter(c => c.cmd.toLowerCase().includes(q) || c.description.toLowerCase().includes(q) || c.handler.toLowerCase().includes(q)).length === 0 }) return (
{nsTabs.map(tab => ( ))}
{visibleNamespaces.map(ns => )} {noResults && (
No commands match "{searchQuery}"
)}
) } // --------------------------------------------------------------------------- // Page // --------------------------------------------------------------------------- export default function ApiReferencePage() { const [activeTab, setActiveTab] = useState('commands') const [activeNamespace, setActiveNamespace] = useState('all') const [searchQuery, setSearchQuery] = useState('') const TABS = [ { key: 'commands', label: 'Commands' }, { key: 'transports', label: 'Transports' }, { key: 'legacy', label: 'v1 → v2 Migration' }, ] const totalCmds = NAMESPACES.reduce((a, n) => a + n.commands.length, 0) return ( <>
{[ { label: 'Namespaces', value: NAMESPACES.length }, { label: 'Commands', value: totalCmds }, { label: 'Transports', value: 5 }, { label: 'Protocol', value: 'v2' }, ].map(stat => (
{stat.value} {stat.label}
))}
{activeTab === 'commands' && ( )} {activeTab === 'transports' && } {activeTab === 'legacy' && }
) }