From 3a1b16a67ddc08929b09699571cbf6c995d5b062 Mon Sep 17 00:00:00 2001 From: Todd Word Date: Mon, 6 Jul 2026 14:58:58 -0500 Subject: [PATCH] feat(boards): add R2 Uppity Spinner ALT (highfalutintodd fork) Adds a device-native board for the R2UppitySpinnerV3 ALT firmware (highfalutintodd fork of Mimir Reynisson's original) at fw v3.5.3. Covers 41 commands across 7 groups: - Playback: :PS - Lifter: :PP, :PP,speed, :PPR, :PH, :PH, :PX (ESTOP) - Rotary: :PR, :PRR, :PA, :PA,speed,max, :PAR, :PD, :PDR - Random Mode: :PM, :PM, :PM,lift,rot,min,max - Lights: :PL, :PLR (light-kit modes 0-7) - Timing: :PW, :PWR - Configuration: 20 #P* commands (calibrate, ID, motor type, expert mode, WiFi, aggressiveness default, baud, limit polarities, RC mode, factory reset, reboot, etc.) Confidence: community. :PD relative-rotation range constrained to +/-180 to match the firmware 3.5.3 clamp (multi-rev fix backlogged for 3.5.4); commentLabel notes the chain-multiple workaround. Bumps libraryVersion 2.1.0 -> 2.2.0 (minor: new board) and updates releases.json and two fixture assertions in engine.test.js / load-node.test.js that hard-coded the previous version and board count. npm run validate: clean npm test: 136/136 passing --- libraries/boards/r2uppityspinner-alt.json | 594 ++++++++++++++++++++++ libraries/manifest.json | 10 +- releases.json | 8 +- test/engine.test.js | 2 +- test/load-node.test.js | 8 +- 5 files changed, 611 insertions(+), 11 deletions(-) create mode 100644 libraries/boards/r2uppityspinner-alt.json diff --git a/libraries/boards/r2uppityspinner-alt.json b/libraries/boards/r2uppityspinner-alt.json new file mode 100644 index 0000000..0890f49 --- /dev/null +++ b/libraries/boards/r2uppityspinner-alt.json @@ -0,0 +1,594 @@ +{ + "$schema": "droidnet-command-library/library/v1", + "generatedFrom": "R2UppitySpinnerV3 ALT fw 3.5.3 (highfalutintodd fork of Mimir Reynisson's original)", + "enums": { + "uppity.lightMode": { + "label": "Light Kit Mode", + "values": [ + { "code": "0", "label": "Full Cycle (random)" }, + { "code": "1", "label": "Off" }, + { "code": "2", "label": "Obi-Wan (blue)" }, + { "code": "3", "label": "Yoda (green fade)" }, + { "code": "4", "label": "Sith (red flash)" }, + { "code": "5", "label": "Search Light (white)" }, + { "code": "6", "label": "Dagobah (screen-accurate)" }, + { "code": "7", "label": "Sparkle" } + ] + }, + "uppity.aggressiveness": { + "label": "Aggressiveness", + "values": [ + { "code": "G", "label": "Gentle" }, + { "code": "M", "label": "Medium" }, + { "code": "A", "label": "Aggressive" } + ] + }, + "uppity.aggressionLevel": { + "label": "Aggressiveness (stored default)", + "values": [ + { "code": "0", "label": "Gentle" }, + { "code": "1", "label": "Medium" }, + { "code": "2", "label": "Aggressive" } + ] + }, + "uppity.motorType": { + "label": "Motor Profile", + "values": [ + { "code": "0", "label": "Greg 6.3:1" }, + { "code": "1", "label": "Pololu 19:1" }, + { "code": "2", "label": "IA-Parts" } + ] + }, + "uppity.onOff": { + "label": "State", + "values": [ + { "code": "0", "label": "Off" }, + { "code": "1", "label": "On" } + ] + }, + "uppity.limitLifter": { + "label": "Lifter Limit Polarity", + "values": [ + { "code": "NCL", "label": "Normally Closed" }, + { "code": "NOL", "label": "Normally Open" } + ] + }, + "uppity.limitRotary": { + "label": "Rotary Limit Polarity", + "values": [ + { "code": "NCR", "label": "Normally Closed" }, + { "code": "NOR", "label": "Normally Open" } + ] + } + }, + "components": [ + { + "id": "r2uppityspinner-alt", + "name": "R2 Uppity Spinner ALT (Periscope)", + "kind": "device-native", + "confidence": "community", + "firmware": "v3.5.3", + "routing": { + "class": "broadcast", + "nativeWrapper": "none", + "durationSuffix": { "supported": false } + }, + "commands": [ + + { + "id": "uppity.playseq", + "name": "Play Stored Sequence", + "group": "Playback", + "safety": "movement", + "encoder": "template", + "template": ":PS{seq}", + "params": [ + { "name": "seq", "type": "int", "min": 0, "max": 100, "required": true, "default": 0 } + ], + "examples": [":PS0", ":PS50"], + "commentLabel": "Uppity play sequence" + }, + + { + "id": "uppity.lifter.seek", + "name": "Seek Lifter to Position", + "group": "Lifter", + "safety": "movement", + "encoder": "template", + "template": ":PP{position}", + "params": [ + { "name": "position", "type": "int", "min": 0, "max": 100, "required": true, "default": 100 } + ], + "examples": [":PP0", ":PP100", ":PP50"], + "commentLabel": "Uppity seek" + }, + { + "id": "uppity.lifter.seek.speed", + "name": "Seek Lifter to Position (with speed)", + "group": "Lifter", + "safety": "movement", + "encoder": "template", + "template": ":PP{position},{speed}", + "params": [ + { "name": "position", "type": "int", "min": 0, "max": 100, "required": true, "default": 100 }, + { "name": "speed", "type": "int", "min": 0, "max": 100, "required": true, "default": 60 } + ], + "examples": [":PP100,60", ":PP0,40"], + "commentLabel": "Uppity seek + speed" + }, + { + "id": "uppity.lifter.seek.random", + "name": "Seek Lifter to Random Position", + "group": "Lifter", + "safety": "movement", + "encoder": "template", + "template": ":PPR", + "params": [], + "examples": [":PPR"], + "commentLabel": "Uppity random position" + }, + { + "id": "uppity.lifter.home", + "name": "Return Home", + "group": "Lifter", + "safety": "movement", + "encoder": "template", + "template": ":PH", + "params": [], + "examples": [":PH"], + "commentLabel": "Uppity home" + }, + { + "id": "uppity.lifter.home.speed", + "name": "Return Home (with speed)", + "group": "Lifter", + "safety": "movement", + "encoder": "template", + "template": ":PH{speed}", + "params": [ + { "name": "speed", "type": "int", "min": 0, "max": 100, "required": true, "default": 40 } + ], + "examples": [":PH40", ":PH100"], + "commentLabel": "Uppity home + speed" + }, + { + "id": "uppity.estop", + "name": "Emergency Stop", + "group": "Lifter", + "safety": "power", + "encoder": "template", + "template": ":PX", + "params": [], + "examples": [":PX"], + "commentLabel": "Uppity ESTOP" + }, + + { + "id": "uppity.rotary.spin", + "name": "Spin Rotary at Speed", + "group": "Rotary", + "safety": "movement", + "encoder": "template", + "template": ":PR{speed}", + "params": [ + { "name": "speed", "type": "int", "min": -100, "max": 100, "required": true, "default": 0 } + ], + "examples": [":PR0", ":PR50", ":PR-80"], + "commentLabel": "Uppity rotary spin (0 = home)" + }, + { + "id": "uppity.rotary.spin.random", + "name": "Spin Rotary at Random Speed", + "group": "Rotary", + "safety": "movement", + "encoder": "template", + "template": ":PRR{max}", + "params": [ + { "name": "max", "type": "int", "min": 0, "max": 100, "required": true, "default": 80 } + ], + "examples": [":PRR0", ":PRR80"], + "commentLabel": "Uppity rotary random spin" + }, + { + "id": "uppity.rotary.abs", + "name": "Rotate to Absolute Degrees", + "group": "Rotary", + "safety": "movement", + "encoder": "template", + "template": ":PA{degrees}", + "params": [ + { "name": "degrees", "type": "int", "min": 0, "max": 359, "required": true, "default": 0 } + ], + "examples": [":PA0", ":PA180", ":PA270"], + "commentLabel": "Uppity rotate absolute (0 = home)" + }, + { + "id": "uppity.rotary.abs.speed", + "name": "Rotate to Absolute Degrees (with speed)", + "group": "Rotary", + "safety": "movement", + "encoder": "template", + "template": ":PA{degrees},{speed},{maxspeed}", + "params": [ + { "name": "degrees", "type": "int", "min": 0, "max": 359, "required": true, "default": 180 }, + { "name": "speed", "type": "int", "min": 0, "max": 100, "required": true, "default": 40 }, + { "name": "maxspeed", "type": "int", "min": 0, "max": 100, "required": true, "default": 80 } + ], + "examples": [":PA180,40,80"], + "commentLabel": "Uppity rotate absolute + speed" + }, + { + "id": "uppity.rotary.abs.random", + "name": "Rotate to Random Absolute Degrees", + "group": "Rotary", + "safety": "movement", + "encoder": "template", + "template": ":PAR", + "params": [], + "examples": [":PAR"], + "commentLabel": "Uppity rotate random absolute" + }, + { + "id": "uppity.rotary.rel", + "name": "Rotate Relative Degrees", + "group": "Rotary", + "safety": "movement", + "encoder": "template", + "template": ":PD{degrees}", + "params": [ + { "name": "degrees", "type": "int", "min": -180, "max": 180, "required": true, "default": 180 } + ], + "examples": [":PD180", ":PD-90", ":PD45"], + "commentLabel": "Uppity rotate relative (chain multiple for multi-rev — single call clamps ≤180° in fw 3.5.3)" + }, + { + "id": "uppity.rotary.rel.random", + "name": "Rotate Random Relative Degrees", + "group": "Rotary", + "safety": "movement", + "encoder": "template", + "template": ":PDR", + "params": [], + "examples": [":PDR"], + "commentLabel": "Uppity rotate random relative" + }, + + { + "id": "uppity.movemode.auto", + "name": "Auto Random Mode (stored aggressiveness)", + "group": "Random Mode", + "safety": "movement", + "encoder": "template", + "template": ":PM", + "params": [], + "examples": [":PM"], + "commentLabel": "Uppity auto move mode" + }, + { + "id": "uppity.movemode.aggr", + "name": "Auto Random Mode (override aggressiveness)", + "group": "Random Mode", + "safety": "movement", + "encoder": "template", + "template": ":PM{level}", + "params": [ + { "name": "level", "enum": "uppity.aggressiveness", "required": true, "default": "M" } + ], + "examples": [":PMG", ":PMM", ":PMA"], + "commentLabel": "Uppity move mode (G/M/A)" + }, + { + "id": "uppity.movemode.params", + "name": "Continuous Move Mode (parameterized)", + "group": "Random Mode", + "safety": "movement", + "encoder": "template", + "template": ":PM,{liftSpeed},{rotSpeed},{minInterval},{maxInterval}", + "params": [ + { "name": "liftSpeed", "type": "int", "min": 0, "max": 100, "required": true, "default": 40 }, + { "name": "rotSpeed", "type": "int", "min": 0, "max": 100, "required": true, "default": 40 }, + { "name": "minInterval", "type": "int", "min": 1, "max": 300, "required": true, "default": 3 }, + { "name": "maxInterval", "type": "int", "min": 2, "max": 300, "required": true, "default": 8 } + ], + "examples": [":PM,40,40,3,8"], + "commentLabel": "Uppity continuous move mode" + }, + + { + "id": "uppity.light", + "name": "Set Light Kit Mode", + "group": "Lights", + "safety": "cosmetic", + "encoder": "template", + "template": ":PL{mode}", + "params": [ + { "name": "mode", "enum": "uppity.lightMode", "required": true, "default": "1" } + ], + "examples": [":PL0", ":PL1", ":PL7"], + "commentLabel": "Uppity light kit" + }, + { + "id": "uppity.light.random", + "name": "Random Light Kit Mode", + "group": "Lights", + "safety": "cosmetic", + "encoder": "template", + "template": ":PLR", + "params": [], + "examples": [":PLR"], + "commentLabel": "Uppity random light kit" + }, + + { + "id": "uppity.wait", + "name": "Wait Seconds", + "group": "Timing", + "safety": "cosmetic", + "encoder": "template", + "template": ":PW{seconds}", + "params": [ + { "name": "seconds", "type": "int", "min": 1, "max": 60, "required": true, "default": 3 } + ], + "examples": [":PW1", ":PW3", ":PW60"], + "commentLabel": "Uppity wait (max 60s)" + }, + { + "id": "uppity.wait.random", + "name": "Wait Random Seconds", + "group": "Timing", + "safety": "cosmetic", + "encoder": "template", + "template": ":PWR{max}", + "params": [ + { "name": "max", "type": "int", "min": 1, "max": 60, "required": true, "default": 6 } + ], + "examples": [":PWR6", ":PWR10"], + "commentLabel": "Uppity random wait" + }, + + { + "id": "uppity.cfg.calibrate", + "name": "Run Calibration", + "group": "Configuration", + "safety": "movement", + "encoder": "template", + "template": "#PSC", + "params": [], + "examples": ["#PSC"], + "commentLabel": "Uppity calibrate" + }, + { + "id": "uppity.cfg.setId", + "name": "Set Device ID", + "group": "Configuration", + "safety": "config", + "encoder": "template", + "template": "#PID{id}", + "params": [ + { "name": "id", "type": "int", "min": 0, "max": 99, "required": true, "default": 0 } + ], + "examples": ["#PID0", "#PID1"], + "commentLabel": "Uppity set ID" + }, + { + "id": "uppity.cfg.zero", + "name": "Clear All Stored Sequences", + "group": "Configuration", + "safety": "config", + "encoder": "template", + "template": "#PZERO", + "params": [], + "examples": ["#PZERO"], + "commentLabel": "Uppity clear sequences" + }, + { + "id": "uppity.cfg.factory", + "name": "Factory Reset (destructive, reboots)", + "group": "Configuration", + "safety": "config", + "encoder": "template", + "template": "#PFACTORY", + "params": [], + "examples": ["#PFACTORY"], + "commentLabel": "Uppity factory reset" + }, + { + "id": "uppity.cfg.restart", + "name": "Reboot", + "group": "Configuration", + "safety": "power", + "encoder": "template", + "template": "#PRESTART", + "params": [], + "examples": ["#PRESTART"], + "commentLabel": "Uppity reboot" + }, + { + "id": "uppity.cfg.debug", + "name": "Verbose Debug", + "group": "Configuration", + "safety": "config", + "encoder": "template", + "template": "#PDEBUG{state}", + "params": [ + { "name": "state", "enum": "uppity.onOff", "required": true, "default": "0" } + ], + "examples": ["#PDEBUG0", "#PDEBUG1"], + "commentLabel": "Uppity debug output" + }, + { + "id": "uppity.cfg.wifiReset", + "name": "Reset WiFi Credentials (reboots)", + "group": "Configuration", + "safety": "config", + "encoder": "template", + "template": "#PWIFIRESET", + "params": [], + "examples": ["#PWIFIRESET"], + "commentLabel": "Uppity WiFi reset" + }, + { + "id": "uppity.cfg.wifi", + "name": "WiFi On/Off (reboots)", + "group": "Configuration", + "safety": "config", + "encoder": "template", + "template": "#PWIFI{state}", + "params": [ + { "name": "state", "enum": "uppity.onOff", "required": true, "default": "1" } + ], + "examples": ["#PWIFI0", "#PWIFI1"], + "commentLabel": "Uppity WiFi enable" + }, + { + "id": "uppity.cfg.aggression", + "name": "Set Stored Aggressiveness Default", + "group": "Configuration", + "safety": "config", + "encoder": "template", + "template": "#PAGGRESSION{level}", + "params": [ + { "name": "level", "enum": "uppity.aggressionLevel", "required": true, "default": "1" } + ], + "examples": ["#PAGGRESSION0", "#PAGGRESSION2"], + "commentLabel": "Uppity default aggressiveness" + }, + { + "id": "uppity.cfg.motor", + "name": "Set Motor Type (invalidates calibration)", + "group": "Configuration", + "safety": "config", + "encoder": "template", + "template": "#PMOTOR{type}", + "params": [ + { "name": "type", "enum": "uppity.motorType", "required": true, "default": "0" } + ], + "examples": ["#PMOTOR0", "#PMOTOR1", "#PMOTOR2"], + "commentLabel": "Uppity motor profile" + }, + { + "id": "uppity.cfg.expert", + "name": "Expert Mode (19:1 motor only — frame damage risk)", + "group": "Configuration", + "safety": "config", + "encoder": "template", + "template": "#PEXPERT{state}", + "params": [ + { "name": "state", "enum": "uppity.onOff", "required": true, "default": "0" } + ], + "examples": ["#PEXPERT0", "#PEXPERT1"], + "commentLabel": "Uppity expert mode" + }, + { + "id": "uppity.cfg.status", + "name": "Print WiFi/Remote Status", + "group": "Configuration", + "safety": "cosmetic", + "encoder": "template", + "template": "#PSTATUS", + "params": [], + "examples": ["#PSTATUS"], + "commentLabel": "Uppity status" + }, + { + "id": "uppity.cfg.config", + "name": "Print Full Configuration", + "group": "Configuration", + "safety": "cosmetic", + "encoder": "template", + "template": "#PCONFIG", + "params": [], + "examples": ["#PCONFIG"], + "commentLabel": "Uppity config dump" + }, + { + "id": "uppity.cfg.listSeq", + "name": "List Stored Sequences", + "group": "Configuration", + "safety": "cosmetic", + "encoder": "template", + "template": "#PL", + "params": [], + "examples": ["#PL"], + "commentLabel": "Uppity list sequences" + }, + { + "id": "uppity.cfg.deleteSeq", + "name": "Delete Stored Sequence", + "group": "Configuration", + "safety": "config", + "encoder": "template", + "template": "#PD{seq}", + "params": [ + { "name": "seq", "type": "int", "min": 0, "max": 100, "required": true, "default": 0 } + ], + "examples": ["#PD0", "#PD50"], + "commentLabel": "Uppity delete sequence" + }, + { + "id": "uppity.cfg.rc", + "name": "RC (PPM) Mode", + "group": "Configuration", + "safety": "config", + "encoder": "template", + "template": "#PRC{state}", + "params": [ + { "name": "state", "enum": "uppity.onOff", "required": true, "default": "0" } + ], + "examples": ["#PRC0", "#PRC1"], + "commentLabel": "Uppity RC mode" + }, + { + "id": "uppity.cfg.rotaryToggle", + "name": "Toggle Rotary Enabled (reboots)", + "group": "Configuration", + "safety": "config", + "encoder": "template", + "template": "#PR", + "params": [], + "examples": ["#PR"], + "commentLabel": "Uppity rotary enable toggle" + }, + { + "id": "uppity.cfg.baud", + "name": "Set Baud Rate (reboots)", + "group": "Configuration", + "safety": "config", + "encoder": "template", + "template": "#PBAUD{rate}", + "params": [ + { "name": "rate", "type": "int", "min": 1200, "max": 921600, "required": true, "default": 9600 } + ], + "examples": ["#PBAUD9600", "#PBAUD115200"], + "commentLabel": "Uppity baud rate" + }, + { + "id": "uppity.cfg.lifterLimit", + "name": "Lifter Limit Polarity", + "group": "Configuration", + "safety": "config", + "encoder": "template", + "template": "#P{polarity}", + "params": [ + { "name": "polarity", "enum": "uppity.limitLifter", "required": true, "default": "NCL" } + ], + "examples": ["#PNCL", "#PNOL"], + "commentLabel": "Uppity lifter limit" + }, + { + "id": "uppity.cfg.rotaryLimit", + "name": "Rotary Limit Polarity", + "group": "Configuration", + "safety": "config", + "encoder": "template", + "template": "#P{polarity}", + "params": [ + { "name": "polarity", "enum": "uppity.limitRotary", "required": true, "default": "NCR" } + ], + "examples": ["#PNCR", "#PNOR"], + "commentLabel": "Uppity rotary limit" + } + ] + } + ] +} diff --git a/libraries/manifest.json b/libraries/manifest.json index 05068c2..2b35be1 100644 --- a/libraries/manifest.json +++ b/libraries/manifest.json @@ -1,8 +1,8 @@ { "$schema": "droidnet-command-library/catalog/v1", - "libraryVersion": "2.1.0", + "libraryVersion": "2.2.0", "schemaVersion": "v1", - "generatedFrom": "C2B5 firmware + WIPDocs (2026-06)", + "generatedFrom": "C2B5 firmware + WIPDocs (2026-06); R2UppitySpinnerV3 ALT fw 3.5.3 (2026-07)", "boards": [ { "id": "flthy-hps", @@ -51,6 +51,12 @@ "file": "boards/chirp.json", "name": "CHiRP Audio Trigger", "confidence": "community" + }, + { + "id": "r2uppityspinner-alt", + "file": "boards/r2uppityspinner-alt.json", + "name": "R2 Uppity Spinner ALT (Periscope)", + "confidence": "community" } ] } diff --git a/releases.json b/releases.json index f8569ef..15ed447 100644 --- a/releases.json +++ b/releases.json @@ -2,17 +2,17 @@ "$schema": "droidnet-command-library/releases/v1", "_comment": "Update pointer for the 'Update Library' flow. A device fetches this file, compares latest.libraryVersion to its installed catalog, and (if newer) downloads latest.url (the catalog manifest), then fetches each board listed in manifest.boards in order, validates them, and loads them via loadLibrary(boards, { libraryVersion }).", "latest": { - "libraryVersion": "2.1.0", + "libraryVersion": "2.2.0", "schemaVersion": "v1", - "releasedAt": "2026-07-05", + "releasedAt": "2026-07-06", "url": "https://raw.githubusercontent.com/travisccook/droidnet-command-library/main/libraries/manifest.json", - "notes": "Adds the CHiRP Audio Trigger (joymonkey) board: PLAY/STOP/VOL/STAT/CHRP/LIST/GMAN/GNME/BAUD/BPAGE/CCRC/MUSB/PVOICE. FlthyHPs, MagicPanel, RSeriesLogic, WCB·HCR, Maestro, PSIPro, HCR native, CHiRP, indexed by manifest.json." + "notes": "Adds the R2 Uppity Spinner ALT (highfalutintodd fork) board: 41 commands covering lifter seek/home, rotary absolute/relative/spin, random move mode, light kit, timing, ESTOP, and full #P configuration. Community confidence; :PD range constrained to ±180° per firmware 3.5.3 clamp." }, "libraries": [ { "id": "droidnet-astromech", "name": "DroidNet Astromech Boards", - "libraryVersion": "2.1.0", + "libraryVersion": "2.2.0", "schemaVersion": "v1", "url": "https://raw.githubusercontent.com/travisccook/droidnet-command-library/main/libraries/manifest.json" } diff --git a/test/engine.test.js b/test/engine.test.js index c875c34..4819efd 100644 --- a/test/engine.test.js +++ b/test/engine.test.js @@ -19,7 +19,7 @@ describe('engine lookups', () => { expect(cb.getComponents().map(c => c.id)).toEqual(expect.arrayContaining(['flthy-hps', 'magic-panel'])); }); test('getLibraryVersion reports the loaded version', () => { - expect(cb.getLibraryVersion()).toBe('2.1.0'); + expect(cb.getLibraryVersion()).toBe('2.2.0'); }); test('getCommand resolves and back-links its component', () => { const cmd = cb.getCommand('flthy.led.solid'); diff --git a/test/load-node.test.js b/test/load-node.test.js index 1a9a4c6..3e27178 100644 --- a/test/load-node.test.js +++ b/test/load-node.test.js @@ -3,20 +3,20 @@ const { readCatalog, loadCatalog } = require('../src/load-node.js'); test('readCatalog returns the manifest and every board, in order', () => { const { manifest, boards } = readCatalog(); - expect(manifest.libraryVersion).toBe('2.1.0'); + expect(manifest.libraryVersion).toBe('2.2.0'); expect(boards).toHaveLength(manifest.boards.length); expect(boards.map(b => b.components[0].id)).toEqual(manifest.boards.map(b => b.id)); }); test('loadCatalog({ load: false }) returns the merged catalog object', () => { const lib = loadCatalog({ load: false }); - expect(lib.libraryVersion).toBe('2.1.0'); - expect(lib.components.length).toBe(8); + expect(lib.libraryVersion).toBe('2.2.0'); + expect(lib.components.length).toBe(9); }); test('loadCatalog() loads the engine and resolves commands', () => { const engine = loadCatalog(); - expect(engine.getLibraryVersion()).toBe('2.1.0'); + expect(engine.getLibraryVersion()).toBe('2.2.0'); expect(engine.getCommand('flthy.led.solid')).not.toBeNull(); expect(engine.encode(engine.getCommand('flthy.led.solid'), { designator: 'A', color: '5' }, {})).toBe('A0055'); });