pi-Stomp Manual Developers

Configuration Guide

pi-Stomp uses YAML configuration files to define hardware layout, MIDI bindings, and pedalboard-specific overrides. There are two classes of config:

At pedalboard load time, the per-pedalboard config merges into the global config field-by-field. Unspecified fields keep their defaults.

Loading runs in three stages, in pistomp/config/:

Stage Module What it holds
Parse schema_v1.py The file format, one Struct per YAML shape. Also the merge
Adapt adapt_v1.py Turns a merged document into the application model, applying built-in defaults
Model model.py PedalboardConfig — what the rest of the app reads

Three states are distinguished per field: absent (use the layer below), explicit null (clear it), and a value. msgspec's UNSET carries "absent" through parse and merge; it never crosses into model.py.

Hardware.reinit() applies every behavioural field unconditionally on each pedalboard load, rather than only the fields a pedalboard mentions. A field that sticks from the previous pedalboard is therefore not representable. Structural fields (e.g. adc_input, gpio_input, ledstrip_position...) are read once at object creation and ignored by reinit.

The JSON Schema is generated from the same Structs via msgspec.json.schema().

Validation and failure

Both files are validated on read. Unknown keys, wrong types and out-of-range numbers are rejected, and errors name a path such as $.hardware.footswitches[0].midi_CC.

A broken pedalboard config.yml is caught in read_bundle_config(), logged, and skipped — the pedalboard loads on the global defaults. A broken default_config.yml propagates out of load_default_cfg() and ends startup, since there is nothing safe to fall back to. Users can see the traceback in the recovery screen.

Config templates

The OS ships with templates for each hardware variant:

File Hardware
default_config_pistomptre.yml v3 Tre — copied for version >= 3.0
default_config_3fs_2knob_exp.yml v2 with 3 footswitches + 2 knobs + expression — copied for version >= 2.0
default_config_pistompcore.yml v2 Core — shipped, but not what modify_version.sh copies
default_config_3fs_2knob.yml v2 with 3 footswitches + 2 knobs
default_config.yml Base template

On first boot, firstboot.sh calls util/modify_version.sh <version>, which copies the matching template to ~/data/config/default_config.yml and checks out the corresponding branch of ~/.pedalboards.

Two traps here. The v2 path copies default_config_3fs_2knob_exp.yml, not the similarly-named default_config_pistompcore.yml. And modify_version.sh still has a v1 branch referencing default_config_pistomp.yml, a file that no longer exists in setup/config_templates/ — that branch is dead, since firstboot never passes a version below 2.0.

Hardware version

hardware:
  version: 3.0

Determines which hardware and handler classes are instantiated. firstboot.sh greps /proc/device-tree/model and passes 3.0 for a Pi 5, 2.0 for everything else — so Pi 3 and Pi 4 both land on 2.0.

Hardwarefactory.create() maps the version to a class: [2.0, 3.0) → Pistompcore, [3.0, 4.0) → Pistomptre. There is no v1 class; anything below 2.0 returns None and the app exits. firstboot says as much in a comment: v1 is no longer supported.

Footswitches

hardware:
  footswitches:
    - id: 0
      adc_input: 0
      midi_CC: 60
      midi_port: ~              # optional, external MIDI device name
      midi_channel: ~           # optional, per-switch channel override
      longpress: next_snapshot  # optional, handler callback name
      tap_tempo: ~              # optional, for tap tempo footswitches
      ledstrip_position: 0      # optional, v3 LED strip index

Each footswitch has an id (physical position), adc_input (analog pin), and midi_CC (MIDI CC to send on press). id is required and is the merge key.

longpress accepts a handler name, a list of handler names (chords), or a single-key mapping ({midi_CC: n}, {preset: UP|DOWN|<index>}, {pedalboard: UP|DOWN}). The accepted handler names are the LongpressName literal in schema_v1.py; a test asserts that set matches the handler's callback map, so adding a callback without adding the name fails the suite.

Encoders

hardware:
  encoders:
    - id: 1
      midi_CC: 70
      midi_port: ~
      midi_channel: ~
    - id: 2
      midi_CC: 71
    - id: 3
      type: VOLUME

type is KNOB (the default) or VOLUME. The navigation encoder is id 0; it drives the LCD menu, is wired in hardware, and is not configured here. Knob encoders send MIDI CC on rotation. The volume encoder adjusts the audio card output level directly and cannot also carry a midi_CC.

Analog controls

hardware:
  analog_controllers:
    - id: 0                      # required, and the merge key
      adc_input: 5
      type: EXPRESSION           # KNOB or EXPRESSION
      midi_CC: 75
      midi_port: ~
      threshold: 16              # movement needed before a new value is sent
      autosync: true             # send position on pedalboard load

Analog controls read the 10-bit ADC (MCP3008) and convert to MIDI CC (0–127). type: EXPRESSION renders as an expression pedal graphic on the LCD. autosync: true sends the current position on pedalboard load so the value doesn't jump.

The expression pedal input is commented out in the default config. Enable it with:

~/extras/expression-pedal.sh on

External MIDI routing

Controls can be routed to an external hardware MIDI device instead of the internal virtual port:

hardware:
  external_midi:
    enabled: true
    send_delay_ms: 10
    messages:
      "Source Audio C4":       # ALSA client name from `aconnect -l`
        - [0xB0, 0x66, 0x00]  # hex MIDI bytes

Individual controls can also specify midi_port: to route to a specific device. midi_port requires midi_channel alongside it — an external device rarely shares the hardware's default channel, and the parser rejects the pair when the channel is missing.

_merged_external_midi in pistomp/config/schema_v1.py overlays external_midi.messages by device name, thus a global entry for a device that the pedalboard does not provide an entry for stays active. A pedalboard section of external_midi: null clears the full section.

Blend mode

blend_snapshots:
  - name: "Clean to Fuzz"
    input_id: 0                # expression pedal (0) or encoder (1, 2)
    interpolation: smooth      # linear, smooth, build, drop, snap, bloom
    stops:
      "0.0": "Clean"           # snapshot name at heel position
      "0.5": "Crunch"
      "1.0": "Fuzz"

See Configuration for details on blend mode.

Configuring JACK

JACK settings live in /etc/default/jack, which firstboot.sh writes from the JACK_* keys in /boot/firmware/pistomp.conf. The startup script that consumes them is /usr/lib/pistomp/jackdrc, shipped by the jack2-pistomp package.

Edit /etc/default/jack directly to change settings on a running device. Don't edit jackdrc — package upgrades overwrite it, and it deliberately lives under /usr/lib rather than /etc so dpkg never treats it as a conffile. To change the systemd unit, drop a file in /etc/systemd/system/jack.service.d/.

Unset keys are written empty by firstboot; jackdrc supplies the default at every boot, which lets OTA updates change defaults without touching anyone's pistomp.conf.

Key Default Notes
JACK_SAMPLE_RATE 48000 (from pistomp.conf) No fallback — jackdrc exits 2 if it's unset
JACK_PERIOD 128 on Pi 5, 256 elsewhere Frames per period. The latency knob
JACK_NPERIODS 2 Fewer periods means lower latency and more XRUNs
JACK_PORT_MAX 512 on Pi 5, 256 elsewhere See below
JACK_RTPRIO 75 Realtime priority
JACK_DEVICE hw:0 Change for an external USB interface
JACK_EXTRA_ARGS empty Passed before -d
JACK_DRIVER_ARGS empty Passed after the ALSA driver args

jackd splits its arguments at the first -d, which is why there are two separate argument keys rather than one.

Ports and memory

The port cap matters more than it looks. JACK preallocates shared memory per port whether or not anything ever connects to it, and the allocation is independent of JACK_PERIOD. JACK's own default of 2048 ports reserves about 102 MB — 22% of RAM on a 512 MB Pi 3A+ — against the roughly 60 ports a large pedalboard actually uses, so pi-Stomp caps it per model:

Board JACK_PORT_MAX JACK_PERIOD
Pi 5 512 128
Everything else 256 256

A Pi 5 has both the CPU headroom for a shorter period and the RAM for more ports; everything else is a Pi 3 or 4, where a 512 MB Pi 3A+ sets the floor.

jackdrc derives both values from /proc/device-tree/model at every boot rather than baking them into the SD card, so a card stays swappable between machines. Each plugin is a JACK client using roughly three ports, so both caps leave several times the headroom a normal pedalboard needs.

If /etc/default/jack is missing, jackdrc exits 1 and systemd's Restart=always retries until firstboot has run.

Settings store

Runtime settings like input gain and headphone volume are stored in /home/pistomp/data/config/settings.yml. These persist across reboots without needing to save a pedalboard.

First boot

firstboot.sh runs once via firstboot.service and:

  1. Expands the root partition to fill the SD card
  2. Checks whether Raspberry Pi Imager already configured the OS (see below)
  3. Applies /boot/firmware/pistomp.conf: Wi-Fi, hostname, user password, timezone, SSH key
  4. Runs the SSH lockout guard
  5. Writes /etc/default/jack from the JACK_* keys
  6. Detects the Pi model and runs modify_version.sh to write default_config.yml
  7. Disables Bluetooth and hciuart
  8. Renames itself to firstboot.done, disables firstboot.service, and reboots

Preseed takes precedence over pistomp.conf

pistomp.conf is the fallback path, not the main one. If the card was flashed with the Imager customization wizard, Imager writes /boot/firmware/rpi-preseed.toml and the rpi-preseed package applies it before firstboot runs. firstboot detects this and skips every OS-level setting (Wi-Fi, hostname, password, timezone, SSH key) so it doesn't overwrite the user's Imager choices. pi-Stomp-specific settings — JACK, hardware version — are applied either way.

The detection keys on the /var/lib/rpi-preseed/applied stamp, not on whether the TOML exists: rpi-preseed redacts the TOML's secrets in place and leaves the file on the boot partition, so its presence proves nothing. A TOML with no stamp means the apply failed, which would leave the device with no Wi-Fi and no credentials — firstboot reports that on the LCD and falls back to pistomp.conf rather than booting a silently unreachable system.

SSH lockout guard

This is a headless appliance with no console, so an sshd that accepts neither passwords nor keys means re-flashing the card. Before finishing, firstboot runs sshd -T and checks whether UID 1000 has a non-empty file matching any AuthorizedKeysFile pattern. If password auth is off and no keys are installed, it writes PasswordAuthentication yes to /etc/ssh/sshd_config.d/00-pistomp-lockout-guard.conf — sorted first, so it wins over the main config and any other drop-in — restarts ssh, and says so on the LCD.

Re-running firstboot

Restoring the script is not enough; firstboot disables its own unit on the way out, so you have to re-enable it:

sudo mv /boot/firmware/firstboot.done /boot/firmware/firstboot.sh
sudo systemctl enable firstboot.service
sudo reboot