# Pedalnetics MIDI Controller — AI Config Generation Specification

> **Audience:** AI coding agents and LLMs that generate Pedalnetics MIDI controller
> configuration files.
> **Goal:** Produce a single JSON object that the controller firmware accepts
> verbatim (uploadable via the WebUI "Controller Connection" tab or pasted after the serial
> `upload` command), and that the WebUI can import without data loss.
>
> **Targets:** Firmware `031002` (3.10.02) / WebUI `3.x`. Parser: ArduinoJson 6, running on
> Pedalnetics MIDI Controllers. Songs/setlists require **3.10.00**; named SysEx messages require **3.04.02 or newer**;
> do not assume a connected controller has this version installed.

---

## WebUI navigation

Use **File → Import Config / Backup/Export Config / Preview / Base/Sample Configs** for configuration files. Preview requires Advanced UI. **Help → AI Help** opens an example prompt with a **Copy example to clipboard** action. Its **View AI spec/instructions** link opens this specification. The prompt is not a configuration file and is not automatically sent to an AI service. When Advanced UI is active, choose File → Switch to Simple UI. When Simple UI is active, choose File → Switch to Advanced UI. Simple UI hides Switches, Sequences, MIDI Input, Presets, and Preview without deleting their configuration. Songs & Setlists appears directly after Pages. Choosing Advanced UI does not enable Builder mode. In Advanced UI, the section bar shows Settings, Switches, Pages, a **Sequences** menu (Command Sequences, PC Sequencer, Songs & Setlists), MIDI Input, a **Presets** menu (LED Color Presets, SysEx), and Controller Connection. In Simple UI, it shows Settings, Pages, Songs & Setlists, and Controller Connection. Controller Connection sits immediately after Presets in Advanced UI, or after Songs & Setlists in Simple UI, with modest spacing and natural wrapping on narrow screens. These navigation changes do not alter JSON schemas or controller layouts.

## Device setup and firmware-update mode

Firmware **3.10.02 (`031002`) or newer** reports configuration provenance in device
status, separately from the configuration JSON. Only an explicitly unconfigured
connected device skips comparison against its minimal boot configuration. Configured,
recovery, and older/unknown devices retain applicable warnings; validation, size and
firmware checks still run. Do not add `configState` or `bootloader` to generated JSON
or infer unconfigured state from a name or a matching layout.

**Controller Connection → Boot to Firmware Mode** requests the RP2040 ROM bootloader
over USB Serial or USB MIDI when supported by device status and installed firmware.
It confirms before disconnecting; the user then copies the correct UF2 to the
**RPI-RP2** drive. This is not a switch command, config field, automatic flash, or
factory reset. On older firmware, use the physical BOOTSEL procedure.

## Hosted reference repositories and MCP

**AI agents do not need GitHub access to retrieve our reference data.** This build
publishes pinned snapshots of two repositories on the same origin as this document:

| Source repository | Hosted entry point | What to retrieve |
|---|---|---|
| `Pedalnetics/Auxidi` — `Config Examples` | [Controller base configurations](/ai/base-configs/index.json) | The exact controller model/hardware revision's exported JSON, preserving its pin, LED, screen, and layout assignments. |
| `Pedalnetics/openmidi` — OpenMIDI device data | [MIDI device catalog](/ai/midi-devices/index.json) | Documented device MIDI implementations, including the fields supplied by upstream; never guess missing commands. |

Fetch each selected catalog entry's `path` for JSON. MIDI entries also provide
`markdownPath` for a readable representation. Resolve these paths against the
origin hosting this document, not against GitHub. The [manifest](/ai/manifest.json)
identifies the source repository URLs, pinned commit revisions, licenses, and any
quarantined records. These are build-time snapshots, not live GitHub proxies.
Malformed upstream records remain available as original source through manifest
links but are not presented as validated command records.

### MCP access for connected assistants

The companion **read-only MCP reference server** exposes Streamable HTTP at
`/mcp` on the hostname where that service is deployed. It provides:

| Tool | Purpose |
|---|---|
| `list_controller_models` | List available controller base configurations. |
| `get_base_config` | Retrieve an exact base configuration by catalog ID. |
| `search_midi_devices` | Search devices by manufacturer or model name. |
| `get_midi_commands` | Retrieve a device's original parsed MIDI record and provenance. |
| `get_config_spec` | Retrieve this configuration specification. |

**The static editor and MCP service are separate deployments.** Do not assume
that `/mcp` is available merely because this document or the catalogs load. Use
the service HTTPS URL supplied by the operator and connect through your AI
client's MCP settings. If MCP is not configured, use the HTTP catalog links above;
no MCP connection is required for those files. Merely reading this specification
or `llms.txt` does not automatically connect an assistant to MCP.

The service also exposes the specification, manifest, and license/attribution as
MCP resources. It cannot edit the user's browser session, upload to a controller,
or certify a generated configuration; it is not a full firmware validator.
See [AI reference access](/ai-reference.md) for the retrieval workflow.

### OpenMIDI licensing

OpenMIDI is a community project hosted by Morningstar Engineering; this snapshot
uses the Pedalnetics fork. The distribution retains the original
[Apache-2.0 license](/ai/licenses/openmidi/LICENSE.txt),
[upstream README and contributor attribution](/ai/licenses/openmidi/README.md),
and original source files. Generated JSON/Markdown records identify their
transformations and provenance. Preserve applicable upstream notices and mark
further changes when redistributing the data. Any upstream NOTICE files are
listed in the manifest. OpenMIDI's license does not automatically license the
editor or Pedalnetics controller configurations under Apache-2.0.

## 0. How to use this document

When asked to "create a config", "add a switch", "make a page", etc., output **one
JSON object** that conforms to the schema below. Follow these rules:

1. Output **only valid JSON** (no comments, no trailing commas, no JS expressions).
   Comments in this document (`// ...`) are explanatory and **must not** appear in output.
2. Always emit the firmware **upload/export shape**, where each page switch contains a
   **`"Steps"`** object (capital **S**), keyed by **1-based string** step numbers
   (`"1"`, `"2"`, ...). This is different from the WebUI's internal in-memory shape.
   See [§5 Pages & Steps](#5-pages--the-steps-format).
3. Include all **required top-level keys** (see [§2](#2-top-level-object)). When in
   doubt, include `settings`, `switches`, `pages`, `sequences`,
   `pcSequencerSequences`, and `midiChannelDevices`.
4. Use the **exact** `commandClass` / action-class string literals (case-sensitive).
   See the gotchas in [§16](#16-casing--naming-gotchas-read-this).
5. Keep the entire JSON under **~50 KB** (hard ceiling ~64 KB; see [§14](#14-limits--validation)).
6. MIDI data bytes (`number`, `value`, velocity) must resolve to **0–127**; MIDI
   channels are **1–16**. **Exception:** the high-resolution `Control Change 16-bit`
   / `Control Change 8-bit` classes accept a wider `value` (0–16383 / 0–255) and a
   `number` (MSB CC#) of **0–31** — see [§7.1](#71-commandclass-values).
7. **Pin numbers are hardware-specific — do not guess them.** `settings.pixelPin`,
   `settings.midiInputPin`, `settings.midiOutputPin`, and each switch's `pinNumber`
   depend on the exact device/board and how it is wired. The values shown throughout
   this document are the Pedalnetics MIDI Controllers reference defaults (see §15). **If the user
   has not told you which device they have (and its pinout), ask before emitting any
   pins.**
8. **Start from the exact controller's base configuration.** Read the locally hosted
   [base configuration catalog](/ai/base-configs/index.json), then fetch the selected
   entry's `path` on this same origin. Confirm device model and hardware revision;
   preserve hardware assignments rather than inventing pins. Examples are references,
   not evidence of the connected controller's wiring or installed firmware.
9. **Look up device MIDI commands in OpenMIDI.** Read the locally hosted
   [MIDI device catalog](/ai/midi-devices/index.json), then fetch the matching entry's
   `path` (JSON) or `markdownPath`. Use the documented CC / Program Change numbers
   and values rather than guessing. Missing or ambiguous data requires clarification.
   No GitHub access is needed to consume this deployed snapshot.
10. **Check provenance and licensing.** The [dataset manifest](/ai/manifest.json)
    identifies pinned source commits. OpenMIDI-derived records carry source and change
    notices; retain these when redistributing them, along with the
    [Apache-2.0 license](/ai/licenses/openmidi/LICENSE.txt) and
    [upstream attribution](/ai/licenses/openmidi/README.md). See
    [AI reference access](/ai-reference.md) for HTTP and optional MCP usage.

### Short requests

Users usually describe what they want in musician terms, for example "Auxidi Trio Plus,
Darkglass Anagram on channel 1, short press switch 1 selects scene A, label it Scene A".
They will not list MIDI numbers, links, field names or safety rules; this document supplies
them. Fill the gaps as follows:

1. **Controller.** Find the exact controller model in the
   [base configuration catalog](/ai/base-configs/index.json) and start from that entry's
   configuration. Keep its `settings`, `switches` (pins, LEDs, screens, positions) and
   `virtualSwitches` unchanged. If several hardware revisions match and you cannot tell
   which one the user owns, ask for their exported config (**File → Backup/Export Config**)
   instead of guessing.
2. **Switch numbers.** "Switch 1" means switch ID `"1"` in that base configuration's
   `switches` object, "switch 2" means `"2"`, and so on. If a number does not exist, ask.
3. **Press types.** "Short press", "press" or "tap" → `"action": "press"`. "Hold" or "long
   press" → `"longPress"`. "Release" → `"release"`. Add no other actions.
4. **Device commands.** Find each named pedal in the
   [MIDI device catalog](/ai/midi-devices/index.json) and use its documented CC / Program
   Change numbers and values for the requested function (for example, a scene select).
   Use the user's MIDI channel. If the function is not documented, say so and ask; do not
   guess numbers.
5. **Labels.** "Label the switch …" → set `"useScreenText": true` and `"screenText"` on that
   switch's step. Do this only when the base configuration has screens (a switch
   `screenAddress` or a non-`disabled` `settings.screenMode`); otherwise skip labels and say
   so in your notes.
6. **Scope.** Implement only the requested actions on a normal (non-global) page. Do not add
   holds, double-presses, Program Changes, songs, LED changes or extra pages that were not
   asked for. Replace the base configuration's example page commands with the requested
   ones rather than mixing the two.
7. **Output.** Return one complete JSON config (strict JSON, all required top-level keys),
   then put any assumptions, unanswered questions and device setup notes (such as "set the
   Anagram to receive on channel 1") outside the JSON. The user imports and reviews it in the
   editor before uploading.

A minimal valid config:

```json
{
  "settings": {
    "midiChannel": 1,
    "ledType": "pixel",
    "pixelPin": 19,
    "midiInputPin": 1,
    "midiOutputPin": 0,
    "screenMode": "disabled"
  },
  "switches": {
    "1": { "pinNumber": 6, "ledNumber": 0, "position": { "row": 0, "col": 0 } }
  },
  "pages": [
    { "pageNumber": 0, "isGlobal": true, "switches": {} },
    {
      "pageNumber": 1,
      "isGlobal": false,
      "switches": {
        "1": {
          "Steps": {
            "1": {
              "commands": [
                { "midiChannel": 1, "commandClass": "Control Change", "number": 80, "value": 127, "action": "press" }
              ]
            }
          }
        }
      }
    }
  ],
  "sequences": {},
  "pcSequencerSequences": {},
  "midiChannelDevices": {}
}
```

---

## 1. Mental model

- **Switches** are the physical (or virtual) inputs (footswitches), defined once in the
  top-level `switches` map with their hardware wiring (GPIO pin, LED index, screen).
- **Pages** are banks/presets. **Page 0** is the **Global page** (`isGlobal: true`) and
  is always active alongside the current page. Pages 1+ are switchable.
- Within a page, each switch maps to a **`Steps`** object. Pressing the switch advances
  through steps **1 → 2 → 3 → ... → wrap to 1**.
- A **step** contains an array of **commands** (MIDI messages + controller actions) plus
  optional LED and screen-text settings for that step.
- Each command has an **`action`** (`press` / `release` / `longPress` / `longRelease`)
  that decides which physical event fires it. Commands of different actions can coexist
  in the same step.
- **Sequences** and **PC Sequencer sequences** are reusable, separately-indexed state
  machines that commands can drive.
- **MIDI Input Mappings** let incoming MIDI (from the DIN-5 input) trigger controller
  actions.
- **LED Color Presets** are named reusable colors referenced by `ledPresetId`.

---

## 2. Top-level object

| Key | Type | Required | Default | Notes |
|---|---|---|---|---|
| `settings` | object | **Yes** | — | Hardware + global MIDI settings. See [§3](#3-settings). |
| `switches` | object | **Yes** | — | Map of switch-id → physical switch. See [§4](#4-switches). |
| `pages` | array | **Yes** (non-empty) | — | Pages array. Must contain at least one page. See [§5](#5-pages--the-steps-format). |
| `sequences` | object | **Yes** | `{}` | Reusable command sequences. May be empty. See [§10](#10-sequences). |
| `pcSequencerSequences` | object | **Yes** | `{}` | PC Sequencer sequences. May be empty. See [§11](#11-pcsequencersequences-program-change-walker). |
| `midiChannelDevices` | object | **Yes** | `{}` | Per-channel device names / PC offset. See [§13](#13-midichanneldevices). |
| `sysexMessages` | object | No | `{}` | Named full-frame SysEx library, keyed by opaque ID. See [§7.2](#72-named-sysex-messages). |
| `songs` | object | No | `{}` | Named songs, optional tempo and ordered page-based sections. See §7.3; firmware 3.10.00+. |
| `setlists` | object | No | `{}` | Named ordered song-ID lists (repeats allowed). See §7.3. |
| `defaultSetlistId` | string | No | — | Default list for initial navigation; does not auto-play on boot. |
| `ledColorPresets` | array | No | `[]` | Named reusable LED colors. Parsed **first**. See [§12](#12-ledcolorpresets--ledpresetid). |
| `midiInputMappings` | array | No | `[]` | Incoming-MIDI → action mappings. See [§9](#9-midiinputmappings). |
| `virtualSwitches` | object | No | `{}` | Dual-press virtual switches. See [§6](#6-virtualswitches). |
| `name` | string | No | — | Cosmetic label. **Parsed but ignored by firmware** (used by WebUI / filename). |
| `description` | string | No | — | Cosmetic. **Ignored by firmware.** |
| `dateTimeGenerated` | string | No | — | ISO timestamp. **Ignored by firmware** (WebUI stamps it on export). |

> **Required-key nuance:** A config pushed via serial `upload` is loaded directly and is
> *not* run through the boot-time validator, so a missing key may "work" on upload but
> fail on the next reboot (the device would fall back to backup/default). **Always emit
> all required keys** so the config survives reboots.

> **Parse order matters:** `ledColorPresets` is parsed before everything else so that
> `ledPresetId` references resolve. Always define a preset before referencing its `id`.

---

## 3. `settings`

All fields are optional individually (each has a firmware default), but you should emit
the core hardware fields. Values are **not** range-checked unless noted.

| Field | Type | Default | Accepted values / notes |
|---|---|---|---|
| `midiChannel` | int | `1` | Global default channel, **1–16**. |
| `longPressTimeMS` | int | `500` | Hold time (ms) before `longPress` fires. |
| `screenMode` | string | `"i2c"` | `"i2c"` (enable OLED) or `"disabled"`. Any other value disables screens. |
| `sdaPin` | int | `4` | Global I²C SDA GPIO. |
| `sclPin` | int | `5` | Global I²C SCL GPIO. |
| `screenSizePx` | [int,int] | `[128,32]` | `[width,height]`, e.g. `[128,64]`. Only read if length ≥ 2. |
| `midiThru` | bool | `true` | Forward physical DIN input to DIN output (subject to mapping suppression). Input mappings work even when false. Firmware 3.10.01+ arbitrates concurrent output so ordinary local commands wait for an incoming SysEx boundary; bounded queues and fault recovery are not a lossless-throughput guarantee. No automatic USB Thru. |
| `statusScreenAddress` | string | `""` | I²C address of the status display. |
| `statusScreenExpanderAddress` | string | `""` | TCA9548A mux I²C address for the status screen. |
| `statusScreenExpanderIO` | int | `0` | Mux channel (0–7) for the status screen. |
| `statusLedNumber` | int or null | `-1` | Pixel index of the status LED. `null` ⇒ no status LED. |
| `ledType` | string | `"pixel"` | Only `"pixel"` (WS2812B/NeoPixel) is functional. Any other value disables LEDs. |
| `pixelPin` | int | `19` | NeoPixel data GPIO. |
| `midiInputPin` | int | `1` | DIN-5 MIDI RX GPIO (UART0 RX). |
| `midiOutputPin` | int | `0` | DIN-5 MIDI TX GPIO (UART0 TX). |
| `gridConfig` | object | `{rows:1,cols:1}` | `{ "rows": int, "cols": int }`. Each forced ≥ 1 and **capped at 20**. Visual layout only. |
| `lockCoreConfig` | bool | `false` | Legacy-compatible Builder-only hardware editing lock. Preserved on import/export; does not restrict normal editing or resistor-ladder setup/calibration. Simple/Advanced mode changes do not modify it. |
| `serialDebug` | bool | `true` | Enable serial debug logging. |
| `screenLedBehavior` | string | `"preload"` | `"preload"`, `"postload"`, or `"postloadwithpreview"`. |
| `previewLedBrightness` | int | `30` | LED preview brightness percent (0–100). |
| `statusLedColorFormat` | string | `"RGB"` | Color order: `RGB`, `RBG`, `GRB`, `GBR`, `BRG`, `BGR`. |
| `resistorLadderTolerance` | int | `100` | ADC ± match window for resistor-ladder sub-switches. |
| `ledGroupInitialState` | string | `"last"` | Which member of an LED group lights at boot: `"first"`, `"last"`, or `"none"`. Forced to one of these. |
| `ledGroupOffBrightness` | int | `0` | Brightness % of inactive group members. **Clamped 0–100.** |
| `isSimpleMode` | bool | `false` | WebUI UI mode only. **Parsed but ignored by firmware.** |

---

## 4. `switches`

Map keyed by **switch-id string** (use sequential `"1"`, `"2"`, ... for physical
switches). Each value:

| Field | Type | Default | Notes |
|---|---|---|---|
| `pinNumber` | int | `-1` | GPIO the switch is wired to (to GND, active-low, internal pull-up). |
| `ledNumber` | int | `-1` | NeoPixel index for this switch. `-1` ⇒ no LED. |
| `screenAddress` | string | `""` | Per-switch OLED I²C address. |
| `expanderAddress` | string | `""` | TCA9548A mux I²C address for this switch's screen. |
| `expanderIO` | int or null | `-1` | Mux channel 0–7. `null` ⇒ none. |
| `screenSdaPin` | int or null | `-1` | Per-switch SDA GPIO. `-1`/`null` ⇒ use global `sdaPin`. |
| `screenSclPin` | int or null | `-1` | Per-switch SCL GPIO. `-1`/`null` ⇒ use global `sclPin`. |
| `ledColorFormat` | string | `"RGB"` | Per-LED color order (`RGB`, `GRB`, etc.). |
| `position` | object | `{row:0,col:0}` | `{ "row": int, "col": int }` grid placement (UI). |
| `ledMode` | string | `"single"` | `"single"` or `"ring"` (LED ring of pixels for one switch). |
| `ringPixelCount` | int | `10` | Pixel count when `ledMode == "ring"`. |
| `ringD1Position` | int | `0` | Orientation offset (0–9) of the ring's first pixel. |
| `resistorLadder` | bool | `false` | If `true`, this is one analog pin serving multiple positions. Enables `subSwitches`. |
| `subSwitches` | array | — | Only parsed when `resistorLadder == true`. See below. |

**`subSwitches[]`** (resistor-ladder positions; one analog pin = many switches):

| Field | Type | Default | Notes |
|---|---|---|---|
| `switchNumber` | int | `-1` | Switch id assigned to this position (also exposed as a top-level switch). |
| `calibrationValue` | int | `-1` | ADC reading captured during calibration (matched within `resistorLadderTolerance`). |
| `name` | string | `""` | Optional label. |
| `ledNumber` | int or null | `-1` | LED for this sub-switch. |
| `ledColorFormat` | string | `"RGB"` | Color order. |
| `ledMode` | string | `"single"` | `"single"` or `"ring"`. |
| `ringPixelCount` | int | `10` | Ring size if `ledMode == "ring"`. |

A sub-switch is valid only when `switchNumber >= 0 && calibrationValue >= 0`. Analog pins
on Pedalnetics MIDI Controllers are **GP26–GP29** only.

---

## 5. Pages & the `Steps` format

`pages` is an **array** of page objects. Conventionally include **page 0 as the Global
page**.

**Page object:**

| Field | Type | Default | Notes |
|---|---|---|---|
| `pageNumber` | int | `0` | Unique page number. `0` = Global. |
| `isGlobal` | bool | `false` | `true` only for page 0. Global commands run alongside the active page. |
| `name` | string | — | Optional page label. |
| `switches` | object | `{}` | Map of switch-id → `{ "Steps": {...} }`. Only include switches that do something on this page. |
| `statusText` | string | `""` | Page status-screen text (only meaningful if a status screen is configured). |
| `lightRGB` | [int,int,int] | — | Page-level status LED color (only applied if length ≥ 3). |
| `lightBrightness` | int | `100` | Page-level status LED brightness 0–100 (used with `lightRGB`). |
| `ledPresetId` | string | — | If present and resolvable, overrides `lightRGB`/`lightBrightness`. |
| `pageLoadCommands` | array | — | Commands executed once when the page is loaded (array of [command objects](#7-command-object)). |

> The WebUI internally also stores `useStatusScreen`, `useStatusLed`, `statusLedRGB`,
> `statusLedPresetId`, etc. On **export to firmware**, page status-LED data is written
> using the `lightRGB`/`lightBrightness`/`ledPresetId` keys shown above. When generating
> for the firmware, use those keys.

**Per-switch `Steps` object** (THE key structure — note capital `S`):

```json
"switches": {
  "1": {
    "Steps": {
      "1": { "commands": [ /* ... */ ] },
      "2": { "commands": [ /* ... */ ] }
    }
  }
}
```

- `Steps` keys are **1-based strings** (`"1"`, `"2"`, ...) and define the press-cycle order.
- A switch with no useful content should simply be omitted from the page.

**Step object:**

| Field | Type | Default | Notes |
|---|---|---|---|
| `commands` | array | `[]` | Array of [command objects](#7-command-object). |
| `useScreenText` | bool | `false` | Enable step-level screen text. |
| `screenText` | string | `""` | Text to display (supports [variables](#8-variables--formulas)). |
| `useLightControls` | bool | `false` | Enable step-level LED color. |
| `lightRGB` | [int,int,int] | `[0,0,0]` | LED color, each 0–255. |
| `lightBrightness` | int | `0` | LED brightness 0–100. |
| `ledGroup` | string or int | `""` | Mutual-exclusion group name; lighting one member dims others. |
| `ledPresetId` | string | — | If resolvable, overrides `lightRGB`/`lightBrightness`. |
| `executeGlobalCommands` | bool | `false` | If `true`, also run the Global page's matching commands. Omit on global pages. |
| `useRingPixelControls` | bool | `false` | Enable per-pixel ring control for this step. |
| `ringPixels` | array | — | Only parsed when `useRingPixelControls == true`. See below. |

**`ringPixels[]`** (per-pixel control on a `ring` LED):

| Field | Type | Default | Notes |
|---|---|---|---|
| `pixel` | int | `1` | Pixel index (1-based, 1–`ringPixelCount`). |
| `lightRGB` | [int,int,int] | `[0,0,0]` | Color. |
| `lightBrightness` | int | `100` | 0–100. |
| `ledPresetId` | string | — | Overrides color/brightness if resolvable. |
| `flashMode` | string | `"none"` | `"none"`, `"hz"` (blink at `flashFrequency`), or `"bpm"` (blink to MIDI clock). |
| `flashFrequency` | float | `2.0` | Blink rate in Hz when `flashMode == "hz"`. |
| `ringGroup` | string | `""` | Group name for ring pixels. |

---

## 6. `virtualSwitches`

Map keyed by a virtual id starting with **`VirtSW`** (e.g. `"VirtSW1"`). A virtual switch
fires when two physical switches are pressed together within a time window. Reference it
in a page's `switches` map by its id (its `Steps` use the same shape as physical
switches).

| Field | Type | Default | Notes |
|---|---|---|---|
| `switch1Id` | string | `""` | First physical switch id. |
| `switch2Id` | string | `""` | Second physical switch id. |
| `name` | string | `""` | Optional label. |
| `doublePressVarianceMs` | int | `250` | Detection window (ms). **Clamped 50–1000.** |
| `ledNumber` | int or null | `-1` | `-1` ⇒ no LED. |
| `ledColorFormat` | string | `"RGB"` | Color order. |
| `ledMode` | string | `"single"` | `"single"` or `"ring"`. |
| `ringPixelCount` | int | `10` | Ring size. |
| `ringD1Position` | int | `0` | Ring orientation. |

---

## 7. Command object

A command is one entry in a step's `commands` array (also used in `pageLoadCommands` and,
without `action`, inside sequences).

> **Looking up a device's MIDI commands?** For the real CC / Program Change numbers and
> values a specific pedal or device responds to, consult the Pedalnetics **OpenMIDI**
> database: <https://github.com/Pedalnetics/openmidi>. Use it to set each command's
> `number` / `value` for the target device instead of guessing.

| Field | Type | Default | Used when / notes |
|---|---|---|---|
| `commandClass` | string | `""` | **Required.** One of the literals in [§7.1](#71-commandclass-values). |
| `action` | string | `""` | Which event fires it: `"press"`, `"release"`, `"longPress"`, `"longRelease"`. Default to `"press"`. (Omit inside sequences.) |
| `midiChannel` | int | `1` | MIDI channel 1–16 for channel messages; omit for Send SysEx Message. |
| `sysexMessageId` | string | — | Required for **Send SysEx Message**: exact key in `sysexMessages`, not a name or numeric index. |
| `number` | int **or** string | `""` | Meaning depends on class (see table). Accepts a number, numeric string, or a [formula](#8-variables--formulas). |
| `value` | int **or** string | `""` | Meaning depends on class. Accepts number/string/formula; ramp accepts `"start-end"`. |
| `rampRateMs` | int | `1000` | **Control Change Ramp** only: ramp duration in ms (≥ 0). |
| `walkerDirection` | string | `"Next"` | **Program Change Walker** only: `"Next"`, `"Previous"`, or `"Current"`. |
| `activatePreset` | bool | `true` | **Program Change Walker** only: send the PC (`true`) or just move the cursor (`false`). |
| `targetPageNumber` | int | `0` | **Activate Another Switch** only. |
| `targetSwitchId` | string | `""` | **Activate Another Switch** only. |
| `targetStepNumber` | int | `1` | **Activate Another Switch** only. |
| `targetAction` | string | `""` | **Activate Another Switch** only: `press`/`release`/`longPress`/`longRelease`. |
| `useRepeater` | bool | `false` | Auto-repeat the command while held. |
| `repeaterStartDelayMs` | int | `1000` | Delay before repeating starts. |
| `repeaterIntervalMs` | int | `500` | Interval between repeats. |
| `useScreenText` | bool | `false` | Command-level screen text (overrides step text). |
| `screenText` | string | `""` | Text (supports [variables](#8-variables--formulas)). |
| `useLightControls` | bool | `false` | Command-level LED (overrides step LED). |
| `lightRGB` | [int,int,int] | `[0,0,0]` | LED color. |
| `lightBrightness` | int | `0` | 0–100. |
| `ledGroup` | string or int | `""` | LED mutual-exclusion group. |
| `ledPresetId` | string | — | Overrides `lightRGB`/`lightBrightness` if resolvable. |
| `useRingPixelControls` | bool | `false` | Enable ring pixels for this command. |
| `ringPixels` | array | — | Same shape as step `ringPixels`. |
| `notes` | string | — | Free-text note. **Parsed but ignored by firmware** (documentation only). |
| `id` | string | — | UUID used by the WebUI to track identity. Optional; safe to include or omit. |

> **Only emit LED/screen/repeater fields when their `use*` flag is `true`.** If
> `useLightControls` is not `true`, omit `lightRGB`/`lightBrightness`/`ledGroup`/ring
> fields entirely. Same for `useScreenText`→`screenText` and `useRepeater`→repeater fields.
> Emitting stray LED/text data without the flag is ignored at best and confusing at worst.

### 7.1 `commandClass` values

Use these **exact, case-sensitive** strings. `number`/`value` columns describe what those
fields mean for each class.

| `commandClass` | `number` means | `value` means | Other required/used fields |
|---|---|---|---|
| `"Send SysEx Message"` | — | — | `sysexMessageId`; no MIDI channel. See §7.2. |
| `"Setlist - Select"` | — | — | `setlistId`; select/restart at the first song/section. See §7.3. |
| `"Setlist - Next Song"` | — | — | Next song occurrence, first section; no wrapping. |
| `"Setlist - Previous Song"` | — | — | Previous song occurrence, first section; no wrapping. |
| `"Song - Next Section"` | — | — | Next section within current song; no wrapping. |
| `"Song - Previous Section"` | — | — | Previous section within current song; no wrapping. |
| `"Control Change"` | CC number (0–127) | CC value (0–127) | `midiChannel` |
| `"Control Change Ramp"` | CC number | target value **or** `"start-end"` (e.g. `"0-127"`) | `midiChannel`, `rampRateMs` |
| `"Control Change 16-bit"` | MSB CC number (0–31); LSB auto-sent on CC#+32 | value 0–16383 (MIDI-standard MSB then LSB; two 7-bit data bytes = 14 usable bits, not raw 0–65535) | `midiChannel`. High-resolution CC. Outbound only. |
| `"Control Change 8-bit"` | MSB CC number (0–31); LSB auto-sent on CC#+32 | value 0–255 (same MSB/LSB pair format, restricted to 8-bit source values) | `midiChannel`. High-resolution CC. Outbound only. |
| `"Program Change"` | PC number (0–127) | — | `midiChannel` |
| `"Program Change Walker"` | PC Sequencer sequence id | — | `midiChannel`, `walkerDirection`, `activatePreset`. **Exclusive: do not add other commands/steps with it.** |
| `"Note On"` | note number (0–127) | velocity (0–127; defaults 127 if empty) | `midiChannel` |
| `"Note Off"` | note number (0–127) | velocity (defaults 0) | `midiChannel` |
| `"MIDI Clock Start"` | optional BPM | — | system-wide (no channel) |
| `"MIDI Clock Stop"` | — | — | system-wide |
| `"MIDI Clock Tap"` | — | — | tap-tempo; system-wide |
| `"Next Page"` | — | — | — |
| `"Previous Page"` | — | — | — |
| `"Go to page"` | target page number | — | **Lowercase `page`.** Ends current sequence. |
| `"GoTo Step"` | target step number | — | jump within the current switch's steps |
| `"Sequence - Next Step"` | sequence id | — | advance that sequence |
| `"Sequence - Previous Step"` | sequence id | — | |
| `"Sequence - Refire"` | sequence id | — | re-execute current step |
| `"Sequence - Step#"` | sequence id | step number | legacy alias; prefer the Goto variants |
| `"Sequence - Goto Step# and Execute"` | sequence id | step number | jump + execute |
| `"Sequence - Goto Step# without Execute"` | sequence id | step number | jump only |
| `"Sequence - Beat Step Start"` | sequence id | beat interval | clock-driven stepping |
| `"Sequence - Beat Step Stop"` | sequence id | — | |
| `"Sleep"` | delay in ms | — | pause within a step sequence |
| `"Activate Another Switch"` | — | — | `targetPageNumber`, `targetSwitchId`, `targetStepNumber`, `targetAction` |
| `"None"` | — | — | no-op placeholder |

> Any `commandClass` not in this list is logged as "Unknown command class" and does
> nothing. Spelling and case must match exactly.

> **High-resolution CC (`Control Change 16-bit` / `Control Change 8-bit`):** `number` is the
> **MSB CC#** and must be **0–31**; the firmware automatically sends the **LSB on CC# + 32**
> (32–63). The value is split as `MSB = (value >> 7) & 0x7F` and `LSB = value & 0x7F`, so the
> two 7-bit MIDI data bytes carry up to **14 usable bits**. Therefore the `16-bit` command label
> does **not** mean raw 0–65535 output; it clamps to **0–16383**. `8-bit` mode clamps to **0–255**
> and uses the same MSB/LSB pair format. These are **outbound only**; there is no inbound MSB/LSB
> reassembly. `number`/`value` still accept numbers, numeric strings, or formulas.

---

### 7.2 Named SysEx messages

Optional top-level `sysexMessages: Record<string, {name: string, data: string}>`.
Absent means `{}` (backward compatible). IDs are opaque strings: preserve them
exactly through import/export; renaming a message must not change its ID. Names
must be nonempty after trimming. The UI creates IDs, not numeric sequence indexes.

```json
"sysexMessages": {
  "device-message-a": { "name": "Device command", "data": "F0 7D 01 F7" }
}
```

`data` contains the **full frame**, including framing bytes:

- Each whitespace-separated token must match `^[0-9A-Fa-f]{2}$` exactly.
- First byte `F0`; last byte `F7`; **1–256 interior payload bytes**, each `00`–`7F`.
- Maximum full frame length **258 bytes**. Interior status/framing bytes are invalid.
- Accept lowercase/mixed case and any whitespace; canonical export is uppercase,
  single spaces. Never truncate, mask, pad, strip punctuation, add framing, or
  parse a malformed token partially. No variables/formulas, `0x` prefixes, commas,
  or empty payloads. `F0 1G F7`, `F0 80 F7`, `F0 F7` are invalid.

Command (use the exact case-sensitive class name):

```json
{ "commandClass": "Send SysEx Message", "sysexMessageId": "device-message-a", "action": "press" }
```

Omit `action` in reusable command sequences. Use in physical/global/virtual switch
steps, reusable sequence steps, or page-load commands. Do not use `midiChannel`,
`number`, or `value` for this command. Every reference must resolve to an existing
library key; do not substitute a different message for a missing/deleted ID.
The WebUI prevents deleting referenced messages and blocks export/upload for
invalid library data or missing references. Invalid imported library data is
rejected; an absent library remains backward compatible. The library tab is
Advanced-only and independent of builder mode.

The example uses non-commercial manufacturer ID `7D` for illustration only;
obtain actual device payloads from its MIDI implementation documentation. This
musical message path requires compatible firmware and sends to DIN and USB MIDI;
it is separate from Auxidi's USB-only configuration transport.

---

### 7.3 Songs and setlists (firmware 3.10.00+)

Optional top-level fields use stable opaque IDs. A complete library fragment:

```json
"songs": {
  "song-opening": {
    "name": "Opening Song",
    "tempo": 120,
    "sections": [
      { "id": "intro", "name": "Intro", "pageNumber": 1 },
      { "id": "verse", "name": "Verse", "pageNumber": 2 }
    ]
  }
},
"setlists": {
  "set-main": { "name": "Main Show", "songIds": ["song-opening", "song-opening"] }
},
"defaultSetlistId": "set-main"
```

Referenced pages must already exist and be positive, non-global pages. Each section
requires a nonempty ID (unique within its song), nonempty name and integer page number.
Names are labels, not references. Renaming must preserve IDs. Repeated song IDs in a
list are separate occurrences. Empty, unreferenced drafts may be saved, but every
song in a playable setlist must have at least one valid section.

Limits: 64 songs, 32 setlists, 32 sections per song / 256 total sections, 128 song
occurrences per list / 512 total occurrences, 64 UTF-8 bytes per ID or reference,
96 UTF-8 bytes per name, 16,384 bytes for the sum of compact JSON `songs` and
`setlists` values. Embedded NUL characters are unsupported. The whole config must
also fit device memory; USB MIDI upload is capped at 56 KiB. Serialized size is not
the same as ArduinoJson's memory usage.

Commands use the five exact strings in §7.1. Only `Setlist - Select` takes
`setlistId`; omit MIDI channel, number and value. Normal switch `action` applies;
omit action in reusable sequences. Navigation with no active list starts the
default at its **first song and first section**, regardless of direction.
No active/default list means no action. Boundaries do not wrap; section navigation
never crosses songs. Manual page changes do not update the song cursor. Reboot or
configuration reload resets the cursor. The default never starts playback on boot.

Optional tempo is a finite JSON number from **20–300 BPM**, rounded to whole BPM
by firmware. It is applied on song entry, before page-load commands, but does **not**
start MIDI clock. Section-only navigation does not reapply it; absent tempo leaves
BPM unchanged. To start at the song tempo, use `MIDI Clock Start` with `number`
`"{BPM}"` (omitting number retains the legacy 120 BPM default). Tap tempo retains
its existing 30–200 BPM range.

Page entry uses normal switch, LED, screen and page-load behavior, even when two
sections reference the same page. Entire candidate setlists are validated before
changing playback state. Missing references fail without skipping songs or applying
tempo/page changes. Source navigation-command LED/text overrides are ignored;
destination page feedback owns the display.

**Do not put navigation commands in page-load commands:** they are deliberately
no-ops during page-load execution, including indirect calls through sequences or
other switches, to prevent recursive navigation. They are retained on import/export
for compatibility, but should be assigned to footswitches or non-page-load sequences.

`{SONGNAME}`, `{SECTIONNAME}`, `{SETLISTNAME}` resolve to active names in screen text
and command-variable substitution (empty before selection/reset). These are text
values, not numeric formula operands. See [the musician workflow](/performance-workflows.md).

---

## 8. Variables & formulas

`number`, `value`, and `screenText` accept **placeholder tokens** that the firmware
substitutes at runtime. Tokens are wrapped in `{...}`.

**Numeric/value & text tokens (switch commands):**

| Token | Meaning |
|---|---|
| `{PAGENUMBER}` | Current page number |
| `{SWITCHNUMBER}` | Current switch id |
| `{STEPNUMBER}` | Current step number (1-based) |
| `{PREVIOUSLYSENTCOMMANDNUMBER}` / `{PREVIOUSLYSENTSTEPCOMMANDNUMBER}` | Last `number` sent by this switch |
| `{PREVIOUSLYSENTCOMMANDVALUE}` / `{PREVIOUSLYSENTSTEPCOMMANDVALUE}` | Last `value` sent by this switch |
| `{BPM}` | Current MIDI clock BPM (renders `---` on screen if unset) |

**Screen-text-only extra tokens:** `{STEPCOMMANDNUMBER}`, `{STEPCOMMANDVALUE}`,
`{PCWALKERTEXT}` (current PC Sequencer label).

**Math in `number`/`value`:** A formula may include `+ - * /` after substitution, e.g.
`"{PAGENUMBER}*10"`. The on-device evaluator is **very limited**:
- It applies **one** operator: the first `+`/`-` (left to right, ignoring a leading
  sign), otherwise the first `*`/`/`.
- **No parentheses, no operator precedence, no multi-term chaining.** Integer math only;
  divide-by-zero yields 0.
- Keep formulas to a single binary operation (e.g. `"{STEPNUMBER}-1"`, `"{PAGENUMBER}*8"`).

> **Different placeholder set for MIDI Input Mappings:** mapping actions use `{INCH}`
> (incoming channel, 1-based), `{INNUM}` (incoming number), `{INVAL}` (incoming value),
> with **substitution only** (no `+ - * /` math). See [§9](#9-midiinputmappings).

---

## 9. `midiInputMappings`

Array of mappings that turn **incoming** MIDI (DIN-5 on GP1, or USB MIDI from a host) into
controller actions. `settings.midiThru` is **not** required for mappings to fire — it only
controls whether incoming DIN bytes are also forwarded to the MIDI output.

**Mapping object:**

| Field | Type | Default | Notes |
|---|---|---|---|
| `id` | string | `""` | Stable id (use a UUID). |
| `name` | string | `""` | Optional label. |
| `enabled` | bool | `true` | Disable without deleting. |
| `trigger` | object | — | **Required** (mapping skipped if missing). See below. |
| `action` | object | — | **Required** (mapping skipped if missing). See below. |

**`trigger` object:**

| Field | Type | Default | Notes |
|---|---|---|---|
| `type` | string | `"cc"` | One of: `"cc"`, `"pc"`, `"noteOn"`, `"noteOff"`, `"clockStart"`, `"clockStop"`, `"clockContinue"`. Unknown ⇒ skipped. |
| `channel` | int | `0` | `0` = Omni (any channel); else 1–16. **Clamped 0–16.** |
| `number` | int | `-1` | CC#, PC#, or note#. `-1` = unused (clock types have none). |
| `valueFilter` | object | `{mode:"any"}` | Optional value gate. See below. |

**`valueFilter` object:**

| Field | Type | Default | Notes |
|---|---|---|---|
| `mode` | string | `"any"` | `"any"`, `"equals"`, `"range"`, or `"threshold"`. |
| `value` | int | `0` | Used by `"equals"`. |
| `min` | int | `0` | Used by `"range"` (inclusive). |
| `max` | int | `127` | Used by `"range"` (inclusive). |
| `threshold` | int | `64` | Used by `"threshold"` (≥ threshold passes). |

**`action` object** — `class` is one of the literals below (case-sensitive). Numeric
fields (`pageNumber`, `stepNumber`, `number`, `value`) accept numbers, strings, or the
`{INCH}`/`{INNUM}`/`{INVAL}` placeholders.

| `action.class` | Fields used |
|---|---|
| `"Go To Page"` | `pageNumber` |
| `"Next Page"` | — |
| `"Previous Page"` | — |
| `"Set Switch Screen Text"` | `switchId`, `text` (`pageNumber` optional — see note below) |
| `"Clear Switch Screen Text"` | `switchId` (`pageNumber` optional) |
| `"Set Switch LED"` | `switchId`, `rgb`, `brightness` (`ledGroup`, `ledPresetId`, `pageNumber` optional) |
| `"Clear Switch LED"` | `switchId` (`pageNumber` optional) |
| `"Set Status LED"` | `rgb`, `brightness` (`ledPresetId` optional) |
| `"Send CC"` | `midiChannel`, `number`, `value` |
| `"Send PC"` | `midiChannel`, `number` |
| `"Send Note On"` | `midiChannel`, `number`, `velocity` |
| `"Send Note Off"` | `midiChannel`, `number` |
| `"MIDI Clock Tap"` | — |
| `"Activate Switch"` | `pageNumber`, `switchId`, `stepNumber`, `targetAction` |
| `"Set Switch Step"` | `switchId`, `stepNumber` |
| `"Reset Sequence"` | `number` (sequence id), `stepNumber` |
| `"None"` | no-op |

> **Casing gotcha:** the MIDI-input action is `"Go To Page"` (Title Case), while the
> switch command is `"Go to page"` (lowercase `page`). They are *not* interchangeable.

> **THRU loop prevention (firmware ≥ 3.03):** each mapping accepts an optional
> top-level `"dontForward": true`. A DIN message matching the trigger is then
> NOT echoed out the MIDI THRU — use it to break MIDI feedback loops (device
> echoes state → pedal thru echoes it back). Pair with action `"None"` for a
> pure THRU filter. With a `threshold` value filter only the edge-crossing
> messages are blocked; use mode `"any"` to block every value of a CC.

> **Page-targeted overlays (firmware ≥ 3.03):** on the four `Set/Clear Switch
> Screen Text` / `Set/Clear Switch LED` actions, an optional `pageNumber` scopes
> the overlay to that page only. If that page is active the switch is painted
> immediately; otherwise the value is stored and painted when the user navigates
> to the page (it persists across page changes until replaced or cleared). Use
> this when an external device (e.g. an HX Stomp) echoes its state and only one
> page's switches mirror that device. Omit `pageNumber` for the classic behavior
> (current page, until the next normal repaint). Out-of-range or global-page
> values make the firmware skip the action.

**`action` field reference:** `pageNumber`, `switchId`, `stepNumber`, `targetAction`
(`press`/`release`/`longPress`/`longRelease`, default `press`), `text`,
`rgb` `[r,g,b]`, `brightness` (0–100), `ledGroup` (string/number; `"null"`→none),
`midiChannel` (default `-1`; falls back to the incoming channel if not 1–16),
`number`, `value`, `velocity` (default 127), `ledPresetId` (overrides `rgb`/`brightness`).

Example mapping (CC#1 ≥ 64 on any channel → go to page 2):

```json
{
  "id": "map-1",
  "name": "Expression to Page 2",
  "enabled": true,
  "trigger": { "type": "cc", "channel": 0, "number": 1, "valueFilter": { "mode": "threshold", "threshold": 64 } },
  "action": { "class": "Go To Page", "pageNumber": 2 }
}
```

---

## 10. `sequences`

Reusable, independently-indexed step machines. Object keyed by **sequence id string**
(`"1"`, `"2"`, ...). Drive them with the `"Sequence - *"` command classes (whose
`number` = this id).

```json
"sequences": {
  "1": {
    "name": "Amp Channels",
    "steps": [
      {
        "commands": [
          { "midiChannel": 1, "commandClass": "Control Change", "number": 71, "value": 0 }
        ],
        "screenText": "Clean",
        "useScreenText": true,
        "useLightControls": true,
        "lightRGB": [0, 200, 0],
        "lightBrightness": 60,
        "ledGroup": null
      },
      {
        "commands": [
          { "midiChannel": 1, "commandClass": "Control Change", "number": 71, "value": 64 }
        ],
        "screenText": "Crunch",
        "useScreenText": true
      }
    ]
  }
}
```

- `steps` is a JSON **array** (0-based order; runtime step numbers are 1-based).
- Each step's commands go in **`commands` (array)**. A legacy singular **`command`
  (object)** is still accepted on import but prefer `commands`.
- Sequence commands **do not** carry an `action` (they inherit it from the triggering
  step). Other per-step fields match step LED/screen fields in [§5](#5-pages--the-steps-format).

---

## 11. `pcSequencerSequences` (Program Change Walker)

Object keyed by **sequence id string**. Driven by the `"Program Change Walker"` command
(whose `number` = this id).

```json
"pcSequencerSequences": {
  "1": {
    "name": "Patch List",
    "advanced": false,
    "entries": [
      { "pcNumber": 0, "pcText": "Clean",  "midiChannel": -1 },
      { "pcNumber": 1, "pcText": "Drive",  "midiChannel": -1 },
      { "pcNumber": 2, "pcText": "Lead",   "midiChannel": 2 }
    ]
  }
}
```

**Entry fields:**

| Field | Type | Default | Notes |
|---|---|---|---|
| `pcNumber` | int | `0` | Program Change number to send (simple mode). |
| `pcText` | string | `"PC <n>"` | Label (shown via `{PCWALKERTEXT}`). |
| `midiChannel` | int | `-1` | `-1` = inherit from the triggering command's channel. |
| `messages` | array | — | **Advanced mode:** multiple PCs per step. Each `{ "pcNumber": int, "midiChannel": int }`. Presence makes the entry "advanced". |

`advanced: true` indicates entries use `messages[]`. The legacy top-level key
`pcWalkerSequences` is read as a fallback and migrated to `pcSequencerSequences`; emit the
new key.

---

## 12. `ledColorPresets` & `ledPresetId`

Named reusable colors. **Parsed first**, so any `ledPresetId` elsewhere can resolve to a
live color at load time.

```json
"ledColorPresets": [
  { "id": "preset-red",  "name": "Red",  "rgb": [255, 0, 0],   "brightness": 80 },
  { "id": "preset-blue", "name": "Blue", "rgb": [0, 80, 255],  "brightness": 60 }
]
```

| Field | Type | Notes |
|---|---|---|
| `id` | string | **Required, non-empty** (empty-id presets are skipped). The value used by `ledPresetId`. |
| `name` | string | Label. |
| `rgb` | [int,int,int] | 0–255 each. |
| `brightness` | int | 0–100. |

Any setter object (page status LED, step, command, ring pixel, MIDI-input action) that
includes `"ledPresetId": "<id>"` has its inline `rgb`/`brightness` (or
`lightRGB`/`lightBrightness`) **overwritten** by the preset when the id resolves. If the
id is empty/unknown, the inline values are kept. Always define the preset before
referencing it.

---

## 13. `midiChannelDevices`

Maps a MIDI channel to device metadata. Keyed by **1-based channel string** `"1"`..`"16"`
(other keys are skipped). Two accepted value shapes:

```json
"midiChannelDevices": {
  "1": "Quad Cortex",
  "2": { "name": "Timeline", "pcOffset": true }
}
```

- **Legacy string form:** the string is the device name.
- **Object form:** `{ "name": string, "pcOffset": bool }`.

| Field | Type | Default | Effect |
|---|---|---|---|
| `name` | string | `""` | **Ignored by firmware** (WebUI label only). |
| `pcOffset` | bool | `false` | When `true`, outbound **Program Change** on this channel is sent as `(programNumber - 1)`. Use for devices whose first preset is labelled "1" but stored at PC 0. |

---

## 14. Limits & validation

- **Size:** entire JSON must fit in the firmware's **~64 KB** ArduinoJson document.
  Keep configs **under ~50 KB**.
- **No explicit caps** on the number of switches, pages, steps, commands per step,
  sequences, or mappings — but they all share the 64 KB budget.
- `gridConfig.rows`/`cols`: forced ≥ 1, **capped at 20**.
- MIDI channel: **1–16** (validator warns but does not fail outside this).
- MIDI input mapping `channel`: clamped **0–16** (0 = omni).
- MIDI data bytes (`number`/`value`/note/velocity): clamped **0–127** at send time,
  **except** the hi-res CC classes — `Control Change 16-bit` clamps `value` to **0–16383**
  and `Control Change 8-bit` to **0–255**, with the MSB CC# (`number`) clamped to **0–31**.
- Brightness fields: clamped **0–100**.
- `doublePressVarianceMs`: clamped **50–1000**.
- GPIO pins: Pedalnetics MIDI Controllers valid range **0–29**; analog (resistor ladder) only **26–29**.
- Boot-time validator requires `settings`, a non-empty `pages` array, `switches`,
  `sequences`, `pcSequencerSequences`, and `midiChannelDevices` to be present.

---

## 15. Hardware defaults & pin reference (Pedalnetics MIDI Controllers)

> **These pins are device-specific.** The table below is the Pedalnetics MIDI Controllers
> reference wiring. A given Auxidi / Pedalnetics build may route these to different
> GPIOs, so the pin fields (`pixelPin`, `midiInputPin`, `midiOutputPin`, and each
> switch's `pinNumber`) must match the **actual hardware**. **If you don't know which
> device the user has, ask** — don't assume the defaults. Working examples for
> specific devices are at
> <https://github.com/Pedalnetics/Auxidi/tree/main/Config%20Examples>.

| GPIO | Default function | Notes |
|---|---|---|
| 0 | MIDI TX (`midiOutputPin`) | UART0 TX, DIN-5 out, 31250 baud |
| 1 | MIDI RX (`midiInputPin`) | UART0 RX, DIN-5 in / MIDI Thru |
| 4 | I²C SDA (`sdaPin`) | OLED |
| 5 | I²C SCL (`sclPin`) | OLED |
| 6+ | Switch inputs (`pinNumber`) | `INPUT_PULLUP`, active-low, to GND |
| 19 | NeoPixel data (`pixelPin`) | WS2812B |
| 26–29 | Analog (resistor ladder) | Only these support `analogRead()` |
| USB | WebUI serial | CDC, 115200 baud, config upload/debug |

- LEDs: WS2812B/NeoPixel, color order configurable (`RGB`/`GRB`/...).
- Displays: SSD1306 OLED over I²C; TCA9548A mux supported via `expanderAddress` +
  `expanderIO` (channel 0–7).
- Upload: serial `upload` command (115200 baud), JSON completion detected by balanced
  brace counting; 5 s inactivity timeout.

---

## 16. Casing & naming gotchas (READ THIS)

1. **`"Steps"`** — capital `S`, with **1-based string** keys (`"1"`, `"2"`).
2. Switch command **`"Go to page"`** (lowercase `page`) **vs** MIDI-input action
   **`"Go To Page"`** (Title Case). Not interchangeable.
3. Sequence steps use **`commands`** (array); singular `command` is legacy-only.
4. Top-level **`pcSequencerSequences`** is current; `pcWalkerSequences` is legacy.
5. `commandClass` and `action.class` strings are **exact and case-sensitive**.
6. `number`/`value` may be numbers or strings; both are accepted everywhere.
7. Only emit `lightRGB`/`screenText`/repeater fields when the matching
   `useLightControls`/`useScreenText`/`useRepeater` flag is `true`.
8. **Ignored-by-firmware metadata** (safe to include, never affects behavior):
   `name`, `description`, `dateTimeGenerated`, `isSimpleMode`, command `notes`,
   `midiChannelDevices[].name`.

---

## 17. Worked example (multi-page, LEDs, sequence, PC walker)

```json
{
  "name": "Demo 3-Switch Rig",
  "settings": {
    "midiChannel": 1,
    "longPressTimeMS": 500,
    "screenMode": "i2c",
    "sdaPin": 4,
    "sclPin": 5,
    "screenSizePx": [128, 64],
    "midiThru": true,
    "statusLedNumber": 0,
    "ledType": "pixel",
    "pixelPin": 19,
    "midiInputPin": 1,
    "midiOutputPin": 0,
    "statusLedColorFormat": "RGB",
    "serialDebug": false,
    "gridConfig": { "rows": 1, "cols": 3 }
  },
  "ledColorPresets": [
    { "id": "p-red", "name": "Red", "rgb": [255, 0, 0], "brightness": 80 }
  ],
  "switches": {
    "1": { "pinNumber": 6, "ledNumber": 1, "position": { "row": 0, "col": 0 } },
    "2": { "pinNumber": 7, "ledNumber": 2, "position": { "row": 0, "col": 1 } },
    "3": { "pinNumber": 8, "ledNumber": 3, "position": { "row": 0, "col": 2 } }
  },
  "pages": [
    { "pageNumber": 0, "isGlobal": true, "switches": {} },
    {
      "pageNumber": 1,
      "name": "Main",
      "isGlobal": false,
      "switches": {
        "1": {
          "Steps": {
            "1": {
              "commands": [
                { "midiChannel": 1, "commandClass": "Control Change", "number": 80, "value": 127, "action": "press" },
                { "midiChannel": 1, "commandClass": "Control Change", "number": 80, "value": 0, "action": "release" }
              ],
              "useLightControls": true,
              "ledPresetId": "p-red",
              "useScreenText": true,
              "screenText": "Boost",
              "executeGlobalCommands": false
            }
          }
        },
        "2": {
          "Steps": {
            "1": { "commands": [ { "commandClass": "Sequence - Next Step", "number": 1, "action": "press" } ] }
          }
        },
        "3": {
          "Steps": {
            "1": {
              "commands": [
                { "midiChannel": 1, "commandClass": "Program Change Walker", "number": 1, "walkerDirection": "Next", "activatePreset": true, "action": "press" }
              ],
              "useScreenText": true,
              "screenText": "{PCWALKERTEXT}"
            }
          }
        }
      }
    }
  ],
  "sequences": {
    "1": {
      "name": "Amp Channels",
      "steps": [
        { "commands": [ { "midiChannel": 1, "commandClass": "Control Change", "number": 71, "value": 0 } ], "screenText": "Clean", "useScreenText": true },
        { "commands": [ { "midiChannel": 1, "commandClass": "Control Change", "number": 71, "value": 64 } ], "screenText": "Crunch", "useScreenText": true }
      ]
    }
  },
  "pcSequencerSequences": {
    "1": {
      "name": "Patches",
      "advanced": false,
      "entries": [
        { "pcNumber": 0, "pcText": "Clean", "midiChannel": -1 },
        { "pcNumber": 1, "pcText": "Drive", "midiChannel": -1 }
      ]
    }
  },
  "midiChannelDevices": { "1": { "name": "Amp", "pcOffset": false } },
  "midiInputMappings": [],
  "virtualSwitches": {}
}
```

---

## 18. Generation checklist

Before returning a config, verify:

- [ ] Output is strict JSON (no comments, no trailing commas).
- [ ] All required top-level keys present: `settings`, `switches`, `pages` (non-empty),
      `sequences`, `pcSequencerSequences`, `midiChannelDevices`.
- [ ] A Global page exists (`pageNumber: 0`, `isGlobal: true`) if you use
      `executeGlobalCommands` or global behaviors.
- [ ] Every page switch uses `"Steps"` with 1-based string keys.
- [ ] Every command has a valid, exactly-spelled `commandClass` and an `action`.
- [ ] MIDI channels 1–16; `number`/`value`/velocity resolve to 0–127 (except hi-res CC:
      `value` 0–16383 for 16-bit / 0–255 for 8-bit, MSB `number` 0–31).
- [ ] `Program Change Walker` is alone in its step (no other commands/steps).
- [ ] Every `Send SysEx Message` has a string `sysexMessageId` resolving in `sysexMessages`.
- [ ] SysEx data is a full F0…F7 frame with 1–256 interior 7-bit bytes, exact two-digit hex tokens.
- [ ] Every referenced `ledPresetId` exists in `ledColorPresets`.
- [ ] `targetSwitchId` / sequence ids / page numbers referenced by commands actually exist.
- [ ] LED/screen/repeater data only present when the matching `use*` flag is `true`.
- [ ] Only the requested actions were added; base hardware (`settings`, `switches`, pins, screens) is unchanged.
- [ ] Screen labels are present only when the base configuration has screens.
- [ ] Total size well under ~50 KB.
```
