Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions docs/BOARD_AUTHORING_GUIDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -151,6 +151,8 @@ Each param fills one `{placeholder}` in the template.
| `default` | Value used when left blank. |
| `required` | Marks the param as required (advisory). |
| `pad` | Zero-pad a numeric value to a fixed width (e.g. `4` → `0025`). |
| `omittable` | The firmware accepts the command with this param left off and uses its own default (e.g. a FlthyHPs colour). `match()` accepts the command without it (the param parses as `''`); `encode()` leaves it — and every param after it — off when its value is `''`. Only omittable params may follow it in the template, and it can't also be `required`. The composer adds a **Board default** choice. |
| `leadingZeros` | The firmware reads the number with `atoi`/`toInt`, so `03` means `3`. `match()` accepts leading zeros and the step holds the canonical code, so a rebuild drops them. Needs an enum whose codes are all numeric. Set it only after reading the firmware's parser. |

A param is either enum-backed **or** numeric (`type: "int"`), not both.

Expand Down
7 changes: 5 additions & 2 deletions docs/INTEGRATION_GUIDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -89,15 +89,18 @@ DroidNetCommandLibrary.match('A0055'); // { commandId: 'flthy.led.solid', param
- `;t<ms>` is a **delay** step.
- Anything `match()` doesn't recognize is preserved verbatim as a **raw** step,
so unknown tokens round-trip without loss.
- An empty fragment (a stray, doubled or trailing `^`) becomes no step. The WCB
skips empty segments too, so rebuilding drops them.

```text
A006^*** Flthy rainbow^;t500^T52
└ cmd ┘└── label ──┘└delay┘└cmd┘
```

Round-trips are byte-identical except where a board's firmware is itself lossy
(e.g. RSeriesLogic drops leading zeros — the engine normalizes to the
firmware-equivalent canonical form).
(e.g. RSeriesLogic drops leading zeros, and PSIPro reads `4T03` as `4T3` — the
engine normalizes to the firmware-equivalent canonical form) and for empty
fragments, which are dropped.

## The visual composer (UI)

Expand Down
53 changes: 34 additions & 19 deletions libraries/boards/flthy-hps.json
Original file line number Diff line number Diff line change
Expand Up @@ -250,13 +250,15 @@
{
"name": "color",
"enum": "flthy.color",
"default": "5"
"default": "5",
"omittable": true
}
],
"supportsDuration": true,
"examples": [
"F0025",
"A0023|20"
"A0023|20",
"F002"
],
"commentLabel": "Flthy color projector",
"category": "Lighting"
Expand All @@ -276,21 +278,24 @@
{
"name": "color",
"enum": "flthy.color",
"default": "5"
"default": "5",
"omittable": true
},
{
"name": "speed",
"type": "int",
"min": 0,
"max": 9,
"default": 5,
"required": true
"omittable": true
}
],
"supportsDuration": true,
"examples": [
"A00365",
"F00362|30"
"F00362|30",
"A003",
"A0031"
],
"commentLabel": "Flthy dim pulse",
"category": "Lighting"
Expand All @@ -310,13 +315,15 @@
{
"name": "color",
"enum": "flthy.color",
"default": "5"
"default": "5",
"omittable": true
}
],
"supportsDuration": true,
"examples": [
"F0043",
"A0045|20"
"A0045|20",
"F004"
],
"commentLabel": "Flthy cycle",
"category": "Lighting"
Expand All @@ -336,13 +343,15 @@
{
"name": "color",
"enum": "flthy.color",
"default": "7"
"default": "7",
"omittable": true
}
],
"supportsDuration": true,
"examples": [
"A0077",
"F0075|10"
"F0075|10",
"A007|240"
],
"commentLabel": "Flthy short circuit",
"category": "Lighting"
Expand Down Expand Up @@ -389,15 +398,17 @@
{
"name": "color",
"enum": "flthy.color",
"default": "5"
"default": "5",
"omittable": true
}
],
"supportsDuration": true,
"examples": [
"A0055",
"R0053",
"A0055|60",
"X0055"
"X0055",
"A005"
],
"commentLabel": "Flthy solid",
"category": "Lighting"
Expand Down Expand Up @@ -462,9 +473,10 @@
"required": true
}
],
"supportsDuration": false,
"supportsDuration": true,
"examples": [
"F102"
"F102",
"F102|10"
],
"commentLabel": "Flthy RC L/R",
"category": "Servo"
Expand All @@ -482,9 +494,10 @@
"required": true
}
],
"supportsDuration": false,
"supportsDuration": true,
"examples": [
"F103"
"F103",
"F103|10"
],
"commentLabel": "Flthy RC U/D",
"category": "Servo"
Expand Down Expand Up @@ -523,9 +536,10 @@
"required": true
}
],
"supportsDuration": false,
"supportsDuration": true,
"examples": [
"F105"
"F105",
"F105|3"
],
"commentLabel": "Flthy wag L/R",
"category": "Servo"
Expand All @@ -543,9 +557,10 @@
"required": true
}
],
"supportsDuration": false,
"supportsDuration": true,
"examples": [
"F106"
"F106",
"F106|3"
],
"commentLabel": "Flthy wag U/D",
"category": "Servo"
Expand Down
3 changes: 2 additions & 1 deletion libraries/boards/ia-magic-panel.json
Original file line number Diff line number Diff line change
Expand Up @@ -72,7 +72,8 @@
{
"name": "mode",
"enum": "iamp.mode",
"required": true
"required": true,
"leadingZeros": true
}
],
"examples": [
Expand Down
2 changes: 1 addition & 1 deletion libraries/boards/psi-pro.json
Original file line number Diff line number Diff line change
Expand Up @@ -78,7 +78,7 @@
"template": "{address}T{mode}",
"params": [
{ "name": "address", "enum": "psi.address", "required": true },
{ "name": "mode", "enum": "psi.mode", "required": true }
{ "name": "mode", "enum": "psi.mode", "required": true, "leadingZeros": true }
],
"supportsDuration": true,
"examples": ["0T18", "4T92", "5T3", "0T1", "0T11|47"],
Expand Down
2 changes: 1 addition & 1 deletion libraries/manifest.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"$schema": "droidnet-command-library/catalog/v1",
"libraryVersion": "4.4.0",
"libraryVersion": "4.5.0",
"schemaVersion": "v1",
"generatedFrom": "C2B5 firmware + WIPDocs (2026-06); R2UppitySpinnerV3 ALT fw 3.5.3 (2026-07); Printed Droid Magic Panel 32 Wireless KB (2026-07); WCB 6.1.5_290119RJUN2026 (2026-07); WCB 6.2.1_021242RSEP2026 + WcbCmd 0.8.0 (2026-09); DroidNet forks (2026-09)",
"boards": [
Expand Down
4 changes: 2 additions & 2 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "droidnet-command-library",
"version": "4.4.0",
"version": "4.5.0",
"description": "Schema-driven encoder/decoder and visual composer for building serial commands to control a vibrant community of Astromech droid components. Anyone can add their board by editing a JSON library — no code changes required.",
"license": "MPL-2.0",
"keywords": [
Expand Down
8 changes: 4 additions & 4 deletions releases.json
Original file line number Diff line number Diff line change
Expand Up @@ -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": "4.4.0",
"libraryVersion": "4.5.0",
"schemaVersion": "v1",
"releasedAt": "2026-09-23",
"releasedAt": "2026-09-29",
"url": "https://raw.githubusercontent.com/travisccook/droidnet-command-library/main/libraries/manifest.json",
"notes": "Minor: DroidNet fork boards. Adds two device-native boards, each listed right after its stock board. RSeriesLogic · DroidNet fork (droidnet-rseries-logic) has Brightness (rseries.cfg.brightness): #LEBRI{unit}{level}, where the unit is none (both displays), F (front) or R (rear) and the level is 1-255. FlthyHPs · DroidNet fork (droidnet-flthy-hps) has Brightness (flthy.led.brightness): H{unit}B{level}, where the unit is F, R, T, X, Y, Z or A (all) and the level is 1-255. Both need the DroidNet fork firmware named in the board's firmware field; stock RSeriesLogic and FlthyHPs firmware ignore them. The level is RAM-only: nothing is saved on the board, and a power cycle restores the power-on brightness. The stock RSeriesLogic and FlthyHPs boards are unchanged. Board catalog 22 -> 24, commands 500 -> 502. Safety: new class `storedcommand` for commands that run a command the user saved on the WCB (;C/;SEQ run-sequence, and the DFPlayer/MP3 play+ONFIN callbacks, which were cosmetic). Queries, save and clear stay config."
"notes": "Minor: parse what the firmware accepts. The composer now recognizes stored commands it showed as unrecognized raw steps. Engine: an empty fragment (a stray, doubled or trailing ^) becomes no step, since the WCB skips empty segments too. Two new param flags: omittable (the firmware accepts the command with the param left off and uses its own default; the composer offers a Board default choice) and leadingZeros (the firmware reads the number with atoi/toInt, so 03 is 3). FlthyHPs Holoprojectors (flthy-hps): the colour is omittable on Color Projector, Dim Pulse, Cycle, Solid Color and Short Circuit, and the speed on Dim Pulse, so A007 decodes as Short Circuit in the board's own colour and A0031 as Dim Pulse at the board's speed. RC Control L/R and U/D and Wag L/R and U/D accept a |n duration (FlthyHPs v1.81 stops them after n seconds). PSIPro (psi-pro) Run Effect and IA MagicPanel (ia-magic-panel) Run Mode accept leading zeros (4T03, T00). Saved values still decode losslessly; rebuilding a value writes the canonical form (4T3, no empty segments), which the firmware runs identically. No ids, templates or params change; board and command counts are unchanged."
},
"libraries": [
{
"id": "droidnet-astromech",
"name": "DroidNet Astromech Boards",
"libraryVersion": "4.4.0",
"libraryVersion": "4.5.0",
"schemaVersion": "v1",
"url": "https://raw.githubusercontent.com/travisccook/droidnet-command-library/main/libraries/manifest.json"
}
Expand Down
2 changes: 2 additions & 0 deletions schema/library.schema.json
Original file line number Diff line number Diff line change
Expand Up @@ -170,6 +170,8 @@
"default": { "type": ["string", "number"], "description": "Default code/value used when the param is left blank. Strings for enum codes; numbers are accepted for int params." },
"required": { "type": "boolean" },
"pad": { "type": "integer", "minimum": 1, "description": "Zero-pad a numeric value to this fixed width (e.g. 4 → '0025')." },
"omittable": { "type": "boolean", "description": "The firmware accepts the command with this trailing param left off and uses its own default (e.g. a FlthyHPs colour). match() accepts the command without it (the param parses as ''); encode() leaves it, and every param after it, off the wire when its value is ''. Only omittable params may follow it in the template. The composer offers a 'Board default' choice." },
"leadingZeros": { "type": "boolean", "description": "The firmware reads this number with atoi/toInt, so leading zeros do not change it ('03' is '3'). match() accepts them and the step holds the canonical code, so a rebuild drops them (firmware-equivalent, not byte-identical). Needs an enum whose codes are all numeric." },
"pattern": { "type": "string", "description": "Regex fragment this free-text param matches so its wire value round-trips (e.g. '[0-9A-Fa-f]+' for a hex bitmask, '.+' for arbitrary text). Use ONLY non-capturing groups '(?:...)' — a capturing group would break param↔capture-group indexing. Renders as a text input." }
},
"additionalProperties": true
Expand Down
21 changes: 21 additions & 0 deletions scripts/validate.js
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,27 @@ function boardSemanticErrors(lib) {
for (const name of paramNames) {
if (!placeholders.includes(name)) warnings.push(`${where}: param '${name}' is never used in the template`);
}
// omittable params are left off the END of the wire string (the firmware reads them
// by position), so from the first one on the template may hold only omittable
// placeholders — no literal text, no required param.
const byName = new Map((cmd.params || []).map(p => [p.name, p]));
const firstOmit = [...cmd.template.matchAll(/\{(\w+)\}/g)].find(m => (byName.get(m[1]) || {}).omittable);
if (firstOmit) {
const tail = cmd.template.slice(firstOmit.index);
const tailNames = [...tail.matchAll(/\{(\w+)\}/g)].map(m => m[1]);
if (tail.replace(/\{\w+\}/g, '') !== '' || tailNames.some(n => !(byName.get(n) || {}).omittable)) {
errors.push(`${where}: omittable param '${firstOmit[1]}' must be followed only by other omittable params (no literal text or required param after it)`);
}
}
}
for (const p of cmd.params || []) {
if (p.omittable && p.required) errors.push(`${where}: param '${p.name}' cannot be both omittable and required`);
if (p.leadingZeros) {
const codes = p.enum && enums[p.enum] ? (enums[p.enum].values || []).map(x => String(x.code)) : null;
if (!codes || !codes.every(c => /^\d+$/.test(c))) {
errors.push(`${where}: leadingZeros on param '${p.name}' needs an enum whose codes are all numeric`);
}
}
}
// commentLabel may interpolate {param} placeholders (→ selected value labels) and
// [ ... ] optional segments; every placeholder must reference a real param.
Expand Down
43 changes: 25 additions & 18 deletions src/droidnet-command-library-ui.js
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,29 @@
return humanize(p.name);
}

// The input for one param, holding `val`. An omittable param (the firmware uses its own
// default when it is left off) gets a 'Board default' choice whose value is '' — the
// engine then leaves it off the wire.
function paramControlHtml(p, val, getEnum) {
const name = esc(p.name);
const cap = esc(captionFor(p, getEnum));
if (p.enum) {
const en = getEnum(p.enum);
const opt = (code, label) => `<option value="${esc(code)}"${String(code) === String(val) ? ' selected' : ''}>${esc(label)}</option>`;
const opts = (p.omittable ? opt('', 'Board default') : '')
+ ((en && en.values) || []).map(v => opt(v.code, v.label)).join('');
return `<select class="form-control wcb-param" data-param="${name}" aria-label="${cap}">${opts}</select>`;
}
if (p.pattern) {
// free-text param (e.g. a display string or hex bitmask) → text input, not a number spinner
return `<input class="form-control wcb-param" data-param="${name}" aria-label="${cap}" type="text" value="${esc(val)}">`;
}
const min = p.min !== undefined ? ` min="${p.min}"` : '';
const max = p.max !== undefined ? ` max="${p.max}"` : '';
const ph = p.omittable ? ' placeholder="Board default"' : '';
return `<input class="form-control wcb-param" data-param="${name}" aria-label="${cap}" type="number"${min}${max}${ph} value="${esc(val)}">`;
}

// Standard category display order (see spec 2026-07-07-command-categories-design).
const STANDARD_CATEGORY_ORDER = ['Lighting', 'Movement', 'Sound', 'Sequences', 'Setup', 'Config', 'Power', 'System'];

Expand Down Expand Up @@ -261,23 +284,7 @@

function paramControl(p, cur) {
const val = cur[p.name] !== undefined ? cur[p.name] : (p.default !== undefined ? p.default : '');
const name = esc(p.name);
const cap = captionFor(p, E().getEnum);
let control;
if (p.enum) {
const en = E().getEnum(p.enum);
const opts = ((en && en.values) || [])
.map(v => `<option value="${esc(v.code)}"${String(v.code) === String(val) ? ' selected' : ''}>${esc(v.label)}</option>`).join('');
control = `<select class="form-control wcb-param" data-param="${name}" aria-label="${esc(cap)}">${opts}</select>`;
} else if (p.pattern) {
// free-text param (e.g. a display string or hex bitmask) → text input, not a number spinner
control = `<input class="form-control wcb-param" data-param="${name}" aria-label="${esc(cap)}" type="text" value="${esc(val)}">`;
} else {
const min = p.min !== undefined ? ` min="${p.min}"` : '';
const max = p.max !== undefined ? ` max="${p.max}"` : '';
control = `<input class="form-control wcb-param" data-param="${name}" aria-label="${esc(cap)}" type="number"${min}${max} value="${esc(val)}">`;
}
return fieldCell(cap, control);
return fieldCell(captionFor(p, E().getEnum), paramControlHtml(p, val, E().getEnum));
}

function loadStepIntoAddBar(i) {
Expand Down Expand Up @@ -378,5 +385,5 @@
renderAddBar();
}

return { renderComposer, stepLabel, humanize, captionFor, groupCommandsForDropdown };
return { renderComposer, stepLabel, humanize, captionFor, groupCommandsForDropdown, paramControlHtml };
});
Loading
Loading