POP BOLL

DRAFT · SPECULATIVE

bollardeck API

Version 0.2-draft · Status: speculative draft · Updated 2026-11-10 · As of Bollawax 0.2.4 and Bollard SDK 0.1.0

Speculative draft. No Pop Boll hardware has shipped, and most of this API has not run against a real Boll yet. Message names, fields and limits will change without notice, and nothing here is an offer, a commitment or a promise of compatibility. We would love to hear about partnership, OEM and dev-kit ideas: use the contact form on this site.

Note: The band's brain is a removable cart, the Bolltridge. It lives in the ball, and the same cart can also go into other docks, such as the Glass Snipler. The deck is the cart's service, so where Part A says "the Boll" it means the deck on the cart. Part A was written before the cart and is still a draft with no service behind it. Part B is built. The old Part C (foot cartridges) is retired; Part C is now the Bollard SDK.

bollardeck is the software side of Bollard: everything above the physical connector, which is specified in the Bollard Physical Specification. It is also the import name of the Bollard SDK, the open library (Apache-2.0) that the Pop Boll's own image and popboll.com are built on. The overview page explains how the pieces fit together.

1. Overview#

PartForWhat it covers
A. Docks (§2–§10)anyone building a dock or an accessorywhat a dock can read from a Pop Boll (now playing, MRPH5 stems, LED frames, buttons, battery, corpus labels) and what it can send (audio routing, volume, transport, remix and stem-balance requests, light sync), over USB or BLE
B. Routines (§11–§16)anyone who wants to change how the Boll writes songscustom Python logic that runs inside the song factory on your own Boll, working only with the corpora already on the device
C. The Bollard SDK (§17–§20)anyone writing software against the platformwhat import bollardeck gives you today: routines, buttons and gestures, cues, the keymap, the light matrix engine, the corpus client and its gate, and the schemas; and what is decided about the cart and its docks
ItemVersion
This document0.2-draft
Bollard SDK0.1.0, first shipped with Bollawax 0.2.4
Wire protocol (Part A)bollardeck/1
Message envelope"v": 1
Routine API (Part B)api: 1
Corpus licence blockpopboll.licence/1
Engagement keymap"v": 2

Status.

  • Built: Part B (routines) and everything in Part C marked as shipped. The Pop Boll's image and popboll.com run on the same SDK code.
  • Not built: the deck service of Part A (dock sessions, the BLE layout, the consent prompts), sign-in, the sandbox for third-party apps, the registry, and the Glass Snipler's capability boundary. They are planned, and this page gives no date for them. The SDK's bollardeck.deck, bollardeck.snipler and bollardeck.device.dock modules are placeholders until then.
  • §22 lists everything that is still open.

Conventions:

  • The key words MUST, MUST NOT, SHOULD, RECOMMENDED and MAY are used as described in RFC 2119.
  • JSON examples are sometimes wrapped for reading. On the wire every message is a single line.
  • An ellipsis (…) in an example stands for an elided value.
  • Latencies and transfer rates marked estimate have not been measured.

2. Part A: Docks#

Read this part as a draft of the message shapes. No deck service exists yet, and the part was written when the computer was built into the ball. What the cart changes:

  • The deck runs on the cart. "The Boll" below means the deck on the cart, and the cart is the USB host and the only radio.
  • A Pop Boll or a Glass Snipler is itself a dock for the cart. How such a dock introduces itself (its descriptor and its slots) is decided in outline (§20) and not written.
  • The transports, the BLE roles, the dock.profile values and the light messages below still describe the Bollard v1 foot. They will be rewritten with Bollard v2. No message yet carries a dock's encoder or its EJECT button.
  • Buttons are numbered 1, 2 and 3 on the SDK's surface (§18). On the bollardeck/1 wire of this part they keep the labels inner, middle and outer.

A dock is anything that mates with the Boll's foot, on any Bollard profile, or an accessory with no physical connection that talks over BLE. A dock always talks to the Boll's bollardeck service. The song factory itself never talks to a dock.

DockTransport
USB-C profileUSB CDC-ACM (REQUIRED by Bollard rule C3)
Full profile, active-USB moduleUSB CDC-ACM
Full profile, passive moduleBLE only
Lite or Passive profileBLE only: there is no data path on the connector
No physical connectionBLE

A session opens after the physical layer has worked out what docked (Bollard §12):

 physical attach ──► (Full)  CHAIN and I2C header: vid, pid, class bit 11 "bollardeck"
                 └─► (USB-C) USB descriptors: a CDC interface named "bollardeck"
                 └─► (any)   BLE: the DECK_INFO characteristic
 dock  -> hello      vid and pid MUST equal the physical header's, when there is one
 boll  -> welcome    capabilities granted now, and those pending the user's consent
 boll  -> state, nowplaying, …   only the topics that are granted and subscribed

3. Transports#

3.1 USB CDC-ACM#

  • The dock is the USB device and the Boll stays the USB host (Bollard rule U1).
  • The dock exposes a CDC-ACM interface whose interface string is bollardeck. The Boll binds the interface by that string, and the baud rate is ignored.
  • One CDC interface carries one session.
  • Latency is about 1–2 ms (estimate).

3.2 BLE GATT#

  • The dock is a peripheral and the Boll is the central.
  • Pairing uses LE Secure Connections. The first pairing needs a long press of the Boll's inner (play/pause) button, button 1.
  • Latency is about 15–30 ms (estimate).
  • Airtime is shared with Bluetooth audio and the Boll's other accessories, so the Boll MAY lower led messages to 4–10 Hz on BLE.

The UUIDs below are placeholders. A random 128-bit base will be allocated, and the final UUIDs registered, with version 1.0.

ItemUUID (placeholder)PropertiesContent
bollardeck serviceb011a2d0-0000-4c6b-9e1a-6f2d0b011a2dprimaryadvertised in the scan response, with the dock's product name
DECK_TX (Boll → dock)b011a2d0-0001-4c6b-9e1a-6f2d0b011a2dnotifythe JSON-lines byte stream in ATT MTU-sized chunks (≤ 244 bytes with Data Length Extension), reassembled at each newline
DECK_RX (dock → Boll)b011a2d0-0002-4c6b-9e1a-6f2d0b011a2dwrite without responsethe same, in the other direction
DECK_INFOb011a2d0-0003-4c6b-9e1a-6f2d0b011a2dread{"v":1,"api":["bollardeck/1"],"vid":…,"pid":…}, so the Boll can filter before connecting
DECK_LED (optional)b011a2d0-0004-4c6b-9e1a-6f2d0b011a2dnotifybinary LED frames (§6.3) for docks that want lights without parsing JSON

4. Envelope and errors#

4.1 Framing and envelope#

  • JSON Lines. Each message is one JSON object on one line, UTF-8, terminated by a newline (\n), at most 64 KiB.
  • Envelope. Every message has "v": 1 (the envelope version) and "t": "<type>". All other fields are the payload, at the top level.
  • Requests and replies. A request MAY carry "id", an integer chosen by the sender. The reply carries "re" with that id.
  • Unknown fields MUST be ignored. An unknown t is answered with an unknown_type error, and the session stays open.
{"v":1,"t":"volume","id":12,"set":0.55}
{"v":1,"t":"ok","re":12}

4.2 Rates#

  • The Boll MAY drop or coalesce messages to keep the link healthy, but it never drops button, remix_status or error.
  • A dock SHOULD keep its own requests to at most 20 per second.

4.3 Errors#

{"v":1,"t":"error","re":7,"code":"not_granted","msg":"stems needs the user's consent"}
CodeMeaning
unknown_typethe message type is not known; the session stays open
bad_requestthe message is malformed or a field is out of range
not_grantedthe capability the message needs has not been granted
pending_consentthe capability is waiting for the user's consent on the Boll
rate_limitedthe dock is sending too fast
no_common_apithe dock and the Boll share no protocol major version
identity_mismatchthe hello identity does not match the physical header
not_foundthe requested song, bundle or item does not exist
corpus_rulesthe corpus does not allow this (for example, stems of a corpus song)
busythe Boll cannot take the request right now

5. Handshake and capabilities#

5.1 hello#

The dock opens every session with hello.

{"v":1,"t":"hello","id":1,"api":["bollardeck/1"],
 "dock":{"vendor":"Example Labs","product":"Bass Lean","vid":4617,"pid":49152,
         "profile":"usb-c","fw":"0.1","url":"https://example.org/bass-lean"},
 "want":["state","nowplaying","stems","beat","led","buttons","corpus"],
 "can":["remix","stem_balance","volume","transport","audio_out","audio_in","light_sync"]}
FieldMeaning
apievery protocol major the dock speaks; the Boll picks the highest one in common
dock.vendor, dock.productnames shown in the Boll's UI
dock.vid, dock.pidUSB-style vendor and product IDs. A physically attached dock MUST send the same values as its Bollard header
dock.profilethe Bollard profile: full, lite, usb-c or passive
dock.fw, dock.urlfirmware version and a product page
wantthe topics the dock wants to read (capability flags of §5.4)
canthe things the dock wants to send or control

5.2 welcome#

{"v":1,"t":"welcome","re":1,"api":"bollardeck/1",
 "boll":{"model":"Pop Boll Rev B","fw":"…","serial":null},
 "grants":["state","nowplaying","beat","led","buttons","corpus","volume","transport",
           "light_sync"],
 "pending":["stems","remix","stem_balance","audio_in"],
 "limits":{"led_hz":60,"max_line":65536}}
  • grants are the capabilities the dock may use now.
  • pending capabilities wait for the user's consent (§5.3).
  • boll.serial is null unless the user has turned on "share serial with docks".
  • limits gives the LED frame rate the Boll will send and the maximum line length.
  • Granted topics named in want are sent from the start (the example dock in §10 relies on this). subscribe and unsubscribe change the set later (§7.1).
  • A physical dock whose hello vid and pid do not match its header gets an identity_mismatch error, and the Boll then grants only state and nowplaying.

The first time a given dock asks for a consent capability, the user approves it on the Boll with a long press on the medallion, or in the Boll's local web UI. A dock is identified by its vid, pid and its USB serial number or BLE identity. When the grants change, the Boll sends:

{"v":1,"t":"grants","grants":["state","nowplaying","beat","led","buttons","corpus","volume",
 "transport","light_sync","stems","stem_balance"]}

5.4 Capability flags#

FlagDirectionDefaultMeaning
statereadgrantedbattery, charging, volume, mode, online
nowplayingreadgrantedsong metadata and position
corpusreadgrantedlabels and ids of the active corpora (never their files)
beatreadgrantedbeat and bar events
ledreadgrantedpersona colours
led_ringreadgrantedthe full 24-LED light-ring frame
matrixreadgrantedoptional: the 13 × 8 LED matrix frame and its RGB LEDs
buttonsreadgrantedmedallion button events
stemsreadconsentMRPH5 bundles of the current song (§8)
volume, transportwritegrantedvolume and play, pause, stop, next, previous
light_syncwritegrantedthe dock drives or offsets the Boll's lights (§7.7)
remix, stem_balancewriteconsentremix and re-balance requests (§7.4, §7.5)
audio_outwritegrantedthe Boll's mix to the dock over USB Audio (§7.6); not over BLE
audio_inwriteconsentthe dock's audio into the Boll over USB Audio (§7.6); not over BLE

6. Boll to dock messages#

6.1 Song and device state#

tRateGrantContent
stateon changestatebattery, charging and power source, volume (0–1), mode, whether the Boll is online, and what the factory is doing: idle, planning, jamming, overdub, exporting or paused
nowplayingon change, and every 5 snowplayingsong id, title, origin, form, corpora, tempo, key, time signature, the part list, the routines that shaped the song, and the playback position
corpuson changecorpusthe active corpora: id, label, number of songs, whether it is a remote corpus, and the edition
{"v":1,"t":"state","battery_pct":72,"charging":true,"source":"dock_vbus","volume":0.6,
 "mode":"playing","online":false,"factory":"jamming"}

{"v":1,"t":"nowplaying","song_id":"pb0042_d03_s012_…","title":"…","origin":"factory",
 "form":"single","corpus_ids":["0007-DEMO"],"corpus_labels":["PB 0007 — Demo corpus"],
 "bpm":118.0,"key":"A Major (Hexatonic Pop)","time_sig":"4/4",
 "parts":[{"index":1,"label":"Intro","category":"Intro","bar":1,"bars":8}],
 "routines":["mixolydian_bridge"],"position_ms":12345,"duration_ms":201000}

{"v":1,"t":"corpus","active":[{"id":"0007-DEMO","label":"PB 0007 — Demo corpus","songs":40,
 "remote":true}],"edition":"…"}

6.2 Stems, beats and buttons#

tRateGrantContent
stemson song changestemsthe current song's MRPH5 stems: id, MIDI channel, role, musician persona, program, gain and mute, plus the bundle's size and SHA-256. stems is an empty list when the song has no bundle
beatevery beatbeatbar, beat, whether it is a downbeat, tempo, and at_ms on the Boll's monotonic clock (use ping to learn the offset)
buttonon eventbuttonsbtn is inner, middle or outer (ids BTN_1_PLAY_PAUSE, BTN_2_ONLINE_UPLOAD, BTN_3_POWER); ev is tap, double, triple, hold, release or combo. The Boll's own button actions still happen
cueson changebuttonswhat should be lit for each button: a list of {"btn", "state", "role"} with btn inner, middle or outer, state off, on, pulse (2 Hz) or chase, and role a, b, c, live, go, warn or null; at most one cue for a button, and an empty list clears them. The three buttons have no LEDs: a cue is rendered by the Boll's lights and matrix, the Glass Snipler's keys, or a dock

inner, middle and outer are the wire's labels for buttons 1, 2 and 3. The SDK works in numbers and translates at the wire: bollardeck.device.cues.to_wire turns [{"button": 1, "state": "pulse", "role": "go"}] into [{"btn": "inner", "state": "pulse", "role": "go"}], from_wire goes back, and bollardeck.constants.BUTTONS holds the number, label and id of each button. The ev words are older than the SDK's gesture engine and do not match its gesture names (§22).

{"v":1,"t":"stems","song_id":"…","format":"mrph5/1",
 "stems":[{"id":"ch03_benthos","channel":3,"role":"bass","persona":"Benthos","program":33,
           "gain_db":0.0,"mute":false}],
 "bundle_bytes":48213,"sha256":"…"}

{"v":1,"t":"beat","bar":12,"beat":3,"downbeat":false,"bpm":118.0,"at_ms":918273}

{"v":1,"t":"button","btn":"inner","id":"BTN_1_PLAY_PAUSE","ev":"tap"}

6.3 LED frames#

tRateGrantContent
led60 Hz on USB, ≤ 10 Hz as JSON on BLEledthe frame number and 16 persona colours. With led_ring, also ring: a hex string of 24 RGB triples, the 16 outer-ring LEDs first, then the 8 inner LEDs
led_binas ledledthe binary frame below, base64-encoded, for USB docks that subscribe to the led_bin topic
matrix≤ 15 Hz on USB, ≤ 5 Hz on BLE, only while subscribed and only when it changesmatrixoptional. The frame on the Boll's 13 × 8 LED matrix: frame, t_ms, px (208 hex characters: 104 pixels, row-major, 13 wide, row 0 on top, 0–255) and rgb (three colours: LED1, LED3, LED4). A dock with a display can mirror it
{"v":1,"t":"led","frame":4711,"personas":["#ff3355","…"],"ring":"ff3355…"}
{"v":1,"t":"led_bin","b64":"…"}

The binary LED frame is 78 bytes. It is what the optional DECK_LED BLE characteristic notifies, and what led_bin carries on USB.

OffsetSizeField
02frame_no, unsigned, little-endian
21flags: bit 0 beat, bit 1 downbeat
31brightness
47224 × (R, G, B): 16 outer-ring LEDs, then 8 inner LEDs
762reserved

6.4 Replies and status#

tWhenContent
blobreply to getthe size, SHA-256 and number of chunks that follow
chunkafter blobone chunk: sequence number and up to 2,048 raw bytes as base64
remix_statuson changethe state of a stem_balance or remix request: applied, queued, rendering, done (with new_song_id) or refused
grantswhen grants changethe full list of current grants (§5.3)
pongreply to pingthe Boll's monotonic clock in ms
okreplythe request succeeded
errorreply, or unsoliciteda code from §4.3 and an optional message
{"v":1,"t":"blob","re":7,"size":48213,"sha256":"…","chunks":24}
{"v":1,"t":"chunk","re":7,"seq":0,"b64":"…"}
{"v":1,"t":"remix_status","re":4,"state":"done","song_id":"…","new_song_id":"…"}
{"v":1,"t":"pong","re":9,"boll_ms":918300}
{"v":1,"t":"ok","re":12}
{"v":1,"t":"error","re":4,"code":"pending_consent","msg":"remix is waiting for the user"}

7. Dock to Boll messages#

7.1 Session#

tGrantMeaning
hellononeopens the session (§5.1)
subscribe, unsubscribethe topic's ownadd or remove topics, for example led_ring, beat or led_bin
pingnoneask for the Boll's clock, to align at_ms timestamps
{"v":1,"t":"subscribe","topics":["led_ring","beat"]}
{"v":1,"t":"unsubscribe","topics":["beat"]}
{"v":1,"t":"ping","id":9}

7.2 get#

get fetches a file. The Boll answers with blob and then chunk messages (§6.4).

whatFieldsGrant
mrph5song_idstems
routineref, a registry reference with a digestnot yet defined (§22)
{"v":1,"t":"get","id":7,"what":"mrph5","song_id":"…"}
{"v":1,"t":"get","id":8,"what":"routine","ref":"bollawax.org/examplelabs/bass-lean@sha256:…"}

7.3 Volume and transport#

tPayloadGrant
volumeset (0–1), or step (+1 or −1, one of 16 steps)volume
transportcmd: play, pause, stop, next or prevtransport
{"v":1,"t":"volume","id":12,"set":0.55}
{"v":1,"t":"volume","id":13,"step":1}
{"v":1,"t":"transport","id":14,"cmd":"next"}

7.4 stem_balance#

The Boll plays its songs by synthesizing them from MIDI, so a dock can change how the current song sounds without touching any audio: stem_balance re-balances the stems live, and remix (§7.5) asks the factory for a new version of the song.

stem_balance re-balances the Boll's current playback, stem by stem. It lasts until the song changes, or until the dock sends "reset": true. Gains are clamped to −24…+12 dB. The Boll answers with remix_status (applied). Grant: stem_balance.

{"v":1,"t":"stem_balance","id":3,"song_id":"…",
 "stems":{"ch03_benthos":{"gain_db":4.0},"ch06_gallium":{"mute":true}}}
{"v":1,"t":"stem_balance","id":5,"song_id":"…","reset":true}

7.5 remix#

remix with "mode": "render" asks the factory for a new version of a song. The new version joins the library with the origin dock:<vendor>/<product>, and is uploaded like any song, subject to the user's settings. The factory makes it from the song's own plan and the corpora already on the Boll (never a new corpus), and installed routines apply to it as to any song. The Boll reports progress with remix_status: queued, rendering, then done with the new song id, or refused. Grant: remix.

{"v":1,"t":"remix","id":4,"song_id":"…","mode":"render",
 "stems":{"ch03_benthos":{"gain_db":4.0}},"tempo":1.0,"transpose":0,"note":"bass forward"}

A remix with "mode": "live" MUST be treated as stem_balance.

7.6 Audio routing#

Audio itself flows over USB Audio Class 2 or the analog A-BUS. Only the routing goes over the API, and it is not available over BLE.

dirMeaningGrant
from_bollthe Boll sends its mix to the dock's USB Audio sink (iface, rate, ch)audio_out
to_bollthe Boll takes the dock's USB Audio source; use is replace, mix or recordaudio_in
abusselects the Boll's analog outputs; route is foot, medallion or bothsee §22
{"v":1,"t":"audio","id":5,"dir":"from_boll","iface":"uac2","rate":48000,"ch":2}
{"v":1,"t":"audio","id":6,"dir":"to_boll","use":"mix"}
{"v":1,"t":"audio","id":7,"dir":"abus","route":"foot"}

7.7 Light sync#

light_sync either shifts the Boll's lightshow to match the dock's audio latency (offset_ms), or lets the dock's own palette of 16 colours drive the Boll's light ring until the dock hands control back. Grant: light_sync.

{"v":1,"t":"light_sync","offset_ms":-12}
{"v":1,"t":"light_sync","follow":"dock","palette":["#ff0000","…"]}
{"v":1,"t":"light_sync","follow":"boll"}

7.8 plugin#

plugin tells the Boll which published dock plugin the dock is running, so the Boll's UI can show it. No grant is needed.

{"v":1,"t":"plugin","name":"examplelabs/bass-lean","ref":"sha256:…","state":"running"}

8. Stems and rights#

8.1 MRPH5 bundles#

An MRPH5 bundle is a per-song zip (media type application/x-mrph5, zip comment MRPH5/1). It is written with fixed timestamps, so the same song always gives the same bytes.

EntryContent
mimetypethe text application/x-mrph5, stored uncompressed, first in the zip
combined.midthe full multitrack MIDI, exactly as the song plays
stems/chNN_<label>.midone Standard MIDI File per instrument stem: track 0 is a conductor track with every tempo, time-signature, key-signature, marker and text event; track 1 is that stem's channel messages at their original ticks
meta.yamleverything else, machine-friendly (below)
blueprint.md or song.jsonthe corpus track's blueprint, or the generated song's record
  • Stem names use the MIDI channel as two digits (ch01 to ch16). For a generated song, the label is the musician persona; for a corpus track, it is the track name.
  • Playing every file in stems/ together gives the same notes and tempo map as combined.mid.
  • The proposed stem roles are ch01 lead, ch02 harmony, ch03 bass and ch10 drums, with the other channels named by persona. This mapping is a proposal until MRPH5 is frozen.
  • meta.yaml includes the format and kind, title, tempo, time signature, key, the part list (bars, start and end times, active channels, chords), the channel table, the MIDI tempo, time-signature and key maps, groove descriptors (swing ratio, syncopation per stem, note density, velocity arc, kick and snare pattern) and statistics per stem. Every time is given in both ticks and seconds.

8.2 Who gets which stems#

  • The Boll hands out stems of songs it generated once the user grants stems.
  • It hands out stems of corpus songs only where the corpus allows it (a proposed per-corpus dock_stems setting).
  • A dock never receives corpus audio in any form, or the Boll's account tokens or credentials. A request for them fails with corpus_rules.

8.3 MIDI-only corpora#

Some corpora are MIDI-only: the artist licensed Pop Boll to transcribe the recordings but not to keep or play them, so no audio exists for them anywhere in Pop Boll, not on the Boll and not on the service. A dock must not assume that a corpus has recordings.

  • Each entry of the corpus message (§6.1) carries "audio": "licensed" or "audio": "not_licensed". A dock that finds no audio field must treat the corpus as "not_licensed".
  • Everything a dock can get for a corpus song, MRPH5 bundles and their stems included, is made from the corpus's MIDI transcriptions. It works the same for MIDI-only corpora.
  • A dock that wants corpus recordings gets nothing for a MIDI-only corpus and should say so ("MIDI-only corpus"). Asking for its audio fails with corpus_rules, as for any corpus.

9. Versioning#

  • The envelope's v changes only if the framing changes.
  • The protocol is bollardeck/<major>. hello lists every major the dock speaks, and the Boll picks the highest one in common, or answers no_common_api.
  • Within a major version, additions are new optional fields, new message types, new error codes and new capability flags, discovered through want, can and grants. Removing a field or changing its meaning requires a new major version.
  • The physical specification and this API move independently. A Bollard v1.0 dock speaks bollardeck/1.

10. Example dock#

Bass Lean is a minimal dock: a Linux single-board computer on the USB-C profile that runs as a USB gadget, whose CDC-ACM function (interface string bollardeck) appears to it as /dev/ttyGS0. It pushes the bass forward on every song. It needs pyserial.

#!/usr/bin/env python3
import json
import serial

port = serial.Serial("/dev/ttyGS0", timeout=1)
PLAN = {"bass": +4.0, "drums": +1.5, "lead": -2.0}   # dB per stem role


def send(t, **payload):
    port.write((json.dumps({"v": 1, "t": t, **payload}, separators=(",", ":")) + "\n").encode())


send("hello", id=1, api=["bollardeck/1"],
     dock={"vendor": "Example Labs", "product": "Bass Lean", "profile": "usb-c", "fw": "0.1"},
     want=["nowplaying", "stems"], can=["stem_balance"])

next_id = 2
for raw in port:
    if not raw.strip():
        continue
    msg = json.loads(raw)
    if msg["t"] in ("welcome", "grants"):
        print("balance allowed" if "stem_balance" in msg.get("grants", []) else "waiting for consent")
    elif msg["t"] == "stems" and msg["stems"]:
        gains = {s["id"]: {"gain_db": PLAN[s["role"]]} for s in msg["stems"] if s.get("role") in PLAN}
        if gains:
            send("stem_balance", id=next_id, song_id=msg["song_id"], stems=gains)
            next_id += 1
    elif msg["t"] == "error":
        print("boll:", msg.get("code"), msg.get("msg", ""))

11. Part B: Routines#

A routine is a small piece of Python that changes how your Pop Boll writes songs. It hooks into the song factory on the Boll, runs only on your own Boll, and works only with the corpora already on the device.

The routine API is the SDK's bollardeck.routines package, and the Pop Boll's song factory loads and runs routines through it. Where a table below restates a limit or a field, the SDK is the authority: the hard caps are bollardeck.routines.HARD_LIMITS and the manifest is the routine-manifest schema. You can try a routine without a Boll:

  • python3 -m bollardeck.routines <paths> lists what would load, in what order, and what would be skipped and why.
  • bollardeck.routines.sim is a toy song factory that calls every hook of a loaded routine on a made-up song. It checks that your hooks keep the rules. It writes no music, and its chord list is wider than the Boll's.

How a song is made, so the hooks make sense:

  1. A planning summit plans the song (a single, or a multi-movement suite): key, tempo, and a list of parts (Intro, Verse, Chorus and so on), each with a chord progression, an energy level and a length in bars.
  2. A band of virtual musicians jams each part in takes.
  3. After each take, seven judges score it. When they vote to lock a take, the part is done. A take ceiling makes sure every part finishes.
  4. When every part is locked, the band votes on overdubs.
  5. The song is exported as MIDI and a song record.
  6. A device can also render a making-of film from the session.

11.1 What a routine can never do (normative)#

  • No network. No sockets, HTTP, DNS or subprocesses.
  • No file writes outside its own folder. A folder routine may write inside its folder. A zipped routine may write inside a <zip name>.data/ folder next to the zip.
  • No corpus creation or change. No API creates a corpus, by design, and corpora are read-only to routines. A routine sees corpus labels, summaries and musical DNA through frozen views, and never gets a handle or a path to a corpus.
  • No score forging. A routine can read every judge's score, and can add directives or veto a lock. It cannot change a score, a vote or the lock rule.
  • No hidden state across songs that matters to the factory. A routine's module stays loaded for the process, but everything it returns is validated on every call.

12. Hooks#

A routine defines one or more of these as plain module-level functions in routine.py.

HookCalledMayMay not
on_plan(plan, ctx)once per song, right after the planning summitre-order the parts of a single; change each part's progression, energy, bars and labeladd or remove parts; change any other plan or part field; re-order the parts of a suite
on_part_candidate(part, notes, ctx)every take of every part, after synthesis and autotune, before the judgesreturn a transformed note list: add, remove or change notes, which are then judged as they areexceed 2 × the input notes + 512; produce a note outside the part or outside MIDI ranges
on_verdict(part, verdicts, ctx)every take, after the judgesadd up to 4 directive codes to the next take; veto a lockchange a score, a vote or a threshold
on_overdub(plan, bus, ctx)once, after the band's overdub votesadd notes to locked parts (≤ 512 per part)remove or change locked notes
on_export(meta, ctx)once, before the song record is writtenreturn a JSON mapping (≤ 16 KiB) stored under the routine's namechange any other part of the record
on_drama(shots, ctx)when a device renders the making-of filmre-order shots; add up to 32 shotsremove or alter an original shot

12.1 on_plan#

  • Receives a copy of the song plan. Return the plan, or None to keep the edits you made to the copy in place.
  • May re-order the parts of a single (not a suite), and change each part's progression (1–8 chords from I, ii, iii, IV, V, vi and bVII), energy (0–1), bars (a multiple of 4 from 4 to 32) and label (truncated to 40 characters). part_index is renumbered in the new order, and the plan's total_bars is recomputed.
  • May not add or remove parts, change any other plan field (key, tempo, teams, taste, corpora, title and so on) or any other part field (category, assigned teams, study-break channel, movement, leitmotif role).
  • A result that breaks any of these rules is rejected as a whole, and the routine is disabled for the rest of the song.

12.2 on_part_candidate#

  • Receives a copy of the part and a list of copied note events.
  • Returns a new list, or None after editing the list in place. The routine owns those notes, and the judges hear them as they are.
  • Limits: at most 2 × the input count + 512 notes, and every note valid: channel_1based 1–16, pitch 0–127, velocity 1–127, duration_ticks greater than 0, and start_tick from 0 up to (not including) bars × 1,920.

12.3 on_verdict#

  • Receives the part and the seven judges' verdicts as frozen views.
  • Returns None, or {"directives": [...], "veto": bool, "reason": str}.
  • Directives: up to 4 codes matching ^[A-Z][A-Z0-9_]{2,47}$, added to the next take of the part (§12.7).
  • Veto: stops the take from locking, so the band goes again. A veto expires at the take ceiling, so a song always finishes.
  • The views are frozen: writing to one raises an error and disables the routine. So does returning a key other than directives, veto and reason. Repeated directive codes count once.

12.4 on_overdub#

  • Receives a copy of the plan and a frozen view, {"locked_parts": {part_index: (note, …)}}.
  • Returns {part_index: [note, …]}: notes to add, at most 512 per part. They are tagged with origin_tag routine:<name> and listed in the song's overdub record.
  • Locked notes cannot be removed or changed. Notes for a part that is not locked are refused, and added notes must be valid as in §12.2.

12.5 on_export#

  • Receives the song's metadata, frozen.
  • Returns a JSON-serialisable mapping of at most 16 KiB, stored in the song record at routine_meta.<name>.

12.6 on_drama#

  • Receives a copy of the making-of film's shot list. It runs only on devices.
  • May re-order shots, and add up to 32 shots of the form {"kind": str, "duration_ms": 180–20000, "data": {}}, which are marked by_routine with the routine's name. duration_ms is an integer. Timings are recomputed.
  • May not remove or alter an original shot.

12.7 Directive codes#

Directives are advisory codes that the next take of a part reads. The factory currently acts on these:

CodeEffect on the next take
EVOLVE_COUNTERPOINT_AND_VELOCITY_ARCdevelop counterpoint and the velocity arc
EVOLVE_CALL_AND_RESPONSE_DUELdevelop call-and-response between players
DUCK_COMPING_UNDER_LEAD_MELODYkeep accompaniment under the lead melody
REINFORCE_BEAT_2_4_BACKBEATreinforce the backbeat on 2 and 4
ADD_KICK_SNARE_FOUNDATIONadd a kick and snare foundation
REDUCE_CHROMATIC_QUIRKSreduce chromatic notes
AUTOTUNE_SNAP_SOUR_NOTESsnap sour notes to the scale
TIGHTEN_RHYTHMIC_POCKET_GRIDtighten timing to the grid

Any other well-formed code is recorded with the take and carried into the next take's directive set, but it does not change the notes until the factory learns it. The MORE_HOOK code in the judge_heckler example (§15.2) is one of these.

12.8 Data types#

Parts (in plan.parts, and as the part argument):

FieldTypeEditable in on_plan
part_indexint, 1…Nrenumbered
labelstr, for example "Verse 1"yes
categoryIntro, Verse, Chorus, Bridge, Instrumental Break or Outrono
barsint, a multiple of 4yes
progressionlist of chord symbols, for example ["I", "V", "vi", "IV"]yes
energyfloat, 0–1yes
assigned_team_ids, study_break_channel, movement_index, leitmotif_roleband and suite bookkeepingno

The plan also carries read-only fields such as song_id, title, bpm, key_str, form (single or suite), corpus_ids, corpus_labels and total_bars.

Note events:

FieldMeaning
channel_1basedMIDI channel, 1–16
pitchMIDI note number, 0–127
start_tickoffset from the start of the part; 480 ticks per quarter note, 1,920 per bar
duration_tickslength in ticks
velocity1–127
origin_tagwhere the note came from, for example jam, overdub or routine:<name>
was_autotuned, quirk_waiver_grantedfactory bookkeeping

To make a new note, copy one you received with dataclasses.replace(note, pitch=…, start_tick=…).

Verdict views (frozen): judge_id (a string that starts with the judge's code, such as J2 for the Melodic Hook judge), judge_number, domain, score, threshold, lock_vote, metrics (a mapping of names to floats) and directives (a tuple of codes).

13. Context and limits#

13.1 The context#

AttributeWhatWritable
ctx.routine_name, ctx.routine_version, ctx.apiwho is running, and the API version (1)no
ctx.hook, ctx.song_id, ctx.part, ctx.takewhere in the song: part is a frozen summary, and take is the 1-based take for per-take hooksno
ctx.corpusa frozen view with ids, labels, summaries and dna (the merged musical DNA of the active corpora). There is no handle to a corpusno
ctx.roster{"musicians": {channel: …}, "judges": {judge_id: …}}, the evolving roster, frozenno
ctx.taste_dnathe song's taste profile, frozenno
ctx.device{"serial", "song_id_prefix"}; never probes hardwareno
ctx.rnga random.Random seeded from the song seed, the routine name, the hook, the part and the take: deterministic per song, and separate from the factory's own random numbers, so a routine never shifts the band's diceyes (it is the routine's own)
ctx.log(msg)one line (≤ 200 characters, ≤ 50 lines per song) into the song recordappend-only
ctx.folderthe routine's own folder, the only place it may writeyes

13.2 Limits#

A manifest's limits can only tighten these caps.

LimitHard cap
max_ms_per_call250 ms of wall time, measured after the call (Python cannot pre-empt it)
max_directives_per_verdict4
max_notes_factor2.0 (× the input notes, + 512)
max_overdub_notes_per_part512
max_export_bytes16,384
max_added_shots32
max_log_lines50

13.3 Failures and the song record#

Every hook call is wrapped. An exception, a guard refusal, a result that breaks the hook's rules, or a call slower than the time limit disables that routine for the rest of the song. Its results from that call are discarded, and the song carries on. The song record shows what happened:

"routines_applied": [
  {"name": "mixolydian_bridge", "version": "0.1.0", "author": "Pop Boll examples",
   "source": "/data/routines/mixolydian_bridge", "hooks_called": {"on_plan": 1},
   "log": ["part 5 Bridge: vi-iii-IV-V -> I-bVII-IV-I"]}
],
"routines_failed": [
  {"name": "boom", "version": "0.0.1", "hook": "on_part_candidate",
   "where": "on_part_candidate part 1 take 2", "error": "ValueError: kaboom"}
],
"routine_meta": {"calm": {"ok": 1}}
  • routines_failed also lists packages that failed to load (with "hook": "load"): a bad manifest, a banned import or a duplicate name.
  • Each part's take history also records the directives routines added, their vetoes, and whether a lock was vetoed.
  • With no routine installed, none of these keys exist, and the song is byte-identical to one made by a factory without hooks.
  • The record survives pausing and resuming a song.

14. Packaging and installation#

14.1 Package layout#

A routine is a folder, or a .zip of one (with the files at its root or inside one top-level folder):

mixolydian_bridge/
  routine.yaml     manifest (a YAML subset; one JSON object also parses)
  routine.py       the hooks, as plain module-level functions
  README.md        optional

14.2 Manifest#

name: mixolydian_bridge          # ^[a-z0-9][a-z0-9_.-]{1,63}$, unique on the device
version: 0.1.0
author: Pop Boll examples
api: 1                           # the routine API version
description: Every Bridge plays the Mixolydian jam cell.
hooks: [on_plan]                 # each MUST be defined in routine.py
order: 100                       # optional; lower runs first
limits: {max_ms_per_call: 20}    # optional; can only be tighter than the hard caps

The manifest is checked against the SDK's routine-manifest schema.

  • name, version, author and hooks are required. api may be left out and then means 1; order then means 100.
  • An unknown key under limits is refused. A limit looser than its hard cap is read as the cap. Other unknown top-level keys are ignored.
  • The file is read by the SDK's own reader, bollardeck.routines.yamlsubset, which takes a strict subset of YAML and coerces nothing. Indent with spaces. Anchors, aliases, tags, merge keys, mappings inside a list item and nested flow collections are refused. Plain values that YAML would guess at are refused too: yes, no, on, off, numbers with leading zeros or underscores, hex, exponents, and anything shaped like a date or a time. Put quotes around them. version: 0.1 is a number and is refused; version: 0.1.0 or version: "0.1" is fine.

14.3 Load order#

  1. The Boll loads routines from the routines/ folder of its data directory, in name order, skipping names that start with . or _. Further search paths can be given in the POPBOLLARD_ROUTINES environment variable; each is one routine or a folder of routines, read in the order given.
  2. The first routine with a given name wins. Later ones become duplicate name load failures.
  3. The loaded routines run in order of (order, discovery position), and every hook calls them in that order. Each routine sees the results of the routines before it.
  4. Setting POPBOLLARD_NO_ROUTINES=1 turns all routines off.

Routines are picked up when the next song starts.

14.4 Installing on a Boll#

Copy the routine's folder, or a zip of it, into the routines/ folder of the Boll's data directory. Today that means a computer connected to the Boll's medallion USB-C port, which gives the computer a network link to the Boll. A friendlier install flow, and installs from the registry (§21), are planned. Installing a routine is always the owner's act, on their own device.

15. Example routines#

15.1 mixolydian_bridge#

on_plan: every Bridge's progression becomes the Mixolydian jam cell I – bVII – IV – I (in C: C, B♭, F, C), and each change is logged. Nothing else in the plan changes.

# A bollardeck routine. Its "bollawax file" is this folder, or a zip of it.
name: mixolydian_bridge
version: 0.1.0
author: Pop Boll examples
api: 1
description: >
  Every Bridge plays the factory's Mixolydian jam cell (I - bVII - IV - I) instead of the
  progression the planning summit picked. Nothing else about the plan changes.
hooks: [on_plan]
limits:
  max_ms_per_call: 20
"""mixolydian_bridge: force every Bridge onto the Mixolydian jam cell and log it.

The cell is the "corpus quirk: Mixolydian Jam Break" progression of the factory's
Instrumental Break table, I - bVII - IV - I (in C: C, Bb, F, C).
"""

MIXOLYDIAN_JAM_CELL = ["I", "bVII", "IV", "I"]


def on_plan(plan, ctx):
    for part in plan.parts:
        if part.category == "Bridge" and list(part.progression) != MIXOLYDIAN_JAM_CELL:
            ctx.log(f"part {part.part_index} {part.label}: {'-'.join(part.progression)} -> "
                    f"{'-'.join(MIXOLYDIAN_JAM_CELL)}")
            part.progression = list(MIXOLYDIAN_JAM_CELL)
    return plan

15.2 judge_heckler#

on_verdict: when J2 (the Melodic Hook judge) scores a take below 0.6, it adds the directive MORE_HOOK. It never changes a score, and every take's scores and votes stay identical to a run without it. MORE_HOOK is not yet a code the factory acts on, so it is a heckle you can see in the song record: it does not change the music.

name: judge_heckler
version: 0.1.0
author: Pop Boll examples
api: 1
description: >
  Heckles from the back of the room: whenever J2 (Melodic Hook) scores a take below 0.6 it
  adds a MORE_HOOK directive for the next take. It never changes a score or a vote.
hooks: [on_verdict]
limits:
  max_ms_per_call: 20
  max_directives_per_verdict: 1
"""judge_heckler: shout MORE_HOOK whenever J2's score is below 0.6. Scores stay untouched.

Verdicts arrive as frozen views, so a routine cannot forge a score even by accident; the
only things on_verdict can hand back are directives, a veto and a reason.
"""

THRESHOLD = 0.6


def on_verdict(part, verdicts, ctx):
    j2 = next((v for v in verdicts if v.judge_id.startswith("J2")), None)
    if j2 is None or j2.score >= THRESHOLD:
        return None
    ctx.log(f"part {part.part_index} take {ctx.take}: J2 {j2.score:.3f} < {THRESHOLD} -> MORE_HOOK")
    return {"directives": ["MORE_HOOK"], "reason": f"J2 scored {j2.score:.3f}"}

16. Security#

A routine is ordinary Python running inside the song factory on your Boll. The factory enforces the rules of §11.1 in three layers:

  1. Before loading, statically: imports of network, process and foreign-function modules are refused (the SDK's bollardeck.routines.BANNED_IMPORTS: socket, ssl, http, urllib, requests, httpx, aiohttp, asyncio, subprocess, multiprocessing, ctypes, cffi and similar), and so are imports of the host's own closed modules, which the host adds to that list.
  2. During each hook call, at run time: a Python audit hook refuses socket, subprocess, os.system, exec, spawn, fork and ctypes events, and any open-for-write, remove, rename, mkdir or chmod outside the routine's folder, and any SQLite database opened outside it. The same guard is on while the routine's module is first loaded.
  3. On every result: the factory validates what comes back and keeps only what each hook is allowed to change.

That is a guard rail for honest code, not a sandbox against hostile code. Python offers no in-process sandbox, and a determined routine can get around audit hooks. Install only routines you have read, or that come from someone you trust.

For the same reason:

  • Routines run only on your own Boll. The website never runs user routines: its song factory loads none, whatever is installed.
  • No API creates corpora. Routines work with the corpora on the device, and cannot add, change or export them.
  • A curated path for the web (routines reviewed by a person and allowlisted, or routines compiled to WebAssembly and run in a WASI sandbox with only the hook payloads as input) is a possible future path, and is not built. Until something like it exists, the website stays routine-free.

17. Part C: The Bollard SDK#

The Bollard SDK is the open library every client of the platform builds on, ours included. Its Python package and its browser global are both named bollardeck. Bollawax 0.2.4 is the first release built on it: the Pop Boll's image and popboll.com run the SDK's code for everything in §17.1, so what a third party imports is what the device runs.

The earlier Part C described a cartridge that docked under the Boll's foot as a USB device and ran one signed image. That device class is retired: a Bolltridge is now the core cart (§20). Its messages (caps, clock, midi, voice_claim, health, the image relay and the cartridge.yaml manifest) are withdrawn and are not part of bollardeck/1. Whether add-on images and voice takeover come back as software on the cart is open.

17.1 What SDK 0.1.0 contains#

ModuleWhat it isState
bollardeck.routinesthe routine API of Part B: loader, guard, limits, a simulator and a command lineshipped
bollardeck.devicethe hal protocols with a simulated board, device identity and maker codes, gestures, cues, the keymap (§18)shipped
bollardeck.matrixthe 13 × 8 light matrix engine, every renderer included, in Python and JavaScriptshipped
bollardeck.corpusthe corpus reader, the licence gate, the hub client, attestation checks, self-hosted corpus discovery (§19)shipped
bollardeck.schemaa JSON Schema validator and the schemas of §17.2, in Python and JavaScriptshipped
bollardeck.constantsone source of the timings, button names, cue values and keymap, generated into Python and JavaScriptshipped
bollardeck.device.dockthe dock protocolplaceholder: it waits for Bollard v2 and the deck rewrite (§20)
bollardeck.deckthe deck client of Part Aplaceholder
bollardeck.sniplerGlass Snipler types and data formatsplaceholder; two draft schemas ship

The JavaScript side is plain scripts with no build step. It has the constants, the validator, gestures, cues, the keymap, the matrix engine, the corpus gate and a fetch hub client. It has no disk cache, no attestation check and no origin discovery.

bollardeck.SPEAKS names the wire and format versions the SDK speaks, and bollardeck.MIN_BOLLAWAX the oldest Bollawax it is tested against.

17.2 Schemas#

Every format has a JSON Schema in the SDK, identified as https://popboll.com/sdk/schema/<name>.schema.json. The schema is the authority for field names and types; this page does not restate them.

NameWhat it describes
corpus-manifesta corpus's corpus.json
licencethe corpus licence block, popboll.licence/1
originthe discovery file of a self-hosted corpus, popboll.origin/1
routine-manifestroutine.yaml (§14.2)
cuea button cue list (§18.3)
keymapthe engagement keymap (§18.4)
device-identity, makersa device's identity file and the maker-code registry
image-releasethe latest.json record of an image release
snipler-events, snipler-framethe Glass Snipler's events and frame (drafts)

17.3 What is planned and not available#

  • The deck service: dock sessions, hello and welcome, grants (reads free, writes by the owner's consent on the device).
  • Sign-in, planned; no version promised yet. Until it exists, a person's name sent to the hub is self-declared (§19.3).
  • A sandbox for third-party apps on the cart and in the browser, through the same client.
  • A signed registry at bollawax.org with an affirmation step, and sideloading in hacker mode (both in §21).
  • The Glass Snipler's capability boundary: third-party code asks the deck for Snipler capabilities and never imports the engine, which stays closed.
  • Signed releases and layers.
  • A C header. bollardeck.h will carry the descriptor structs and message names.

No version or date is promised yet for any of these, sign-in included.

18. Buttons, gestures, cues and the keymap#

All four are data in bollardeck.constants, read by the device and the website alike.

18.1 Buttons#

On the SDK's surface a button is its number: 1, 2 or 3 (BUTTON_NUMBERS). "Inner", "middle" and "outer" are labels for where the three sit on the Boll's medallion, and the words the bollardeck/1 wire uses for them (§6.2); BUTTONS maps one to the other.

A board reports presses as key lines, one per line of text:

1 down 91200        button 1 went down at 91,200 ms: a press beginning
1 140 91340         button 1 was held 140 ms and released at 91,340 ms: a complete press
  • <button> <hold_ms> [release_ms] is a complete press.
  • <button> down [t_ms] is a press beginning. It is a hint and never a gesture by itself: a late or lost release line can never turn a tap into a hold.
  • bollardeck.device.hal.base.parse_key_line reads a line and key_line writes one.

18.2 Gestures#

The gesture engine (bollardeck.device.gestures, and bollardeck.gestures in the browser) turns presses into named gestures.

  • A complete press is a tap or a hold by its hold_ms alone: HOLD_MS or longer is a hold. Releasing a hold means nothing more.
  • Taps on one button that follow each other within TAP_FOLLOW_MS chain into a multiple tap, up to TAP_CHAIN_MAX. Which chains a button waits for comes from the keymap, so a single tap is not delayed where no multiple tap is bound.
  • Names are tap<button>, tap<button>x<count> and hold<button>: tap1, tap3x3, hold2. GESTURES lists them.
  • A Gesture carries name, button, kind, count, t_ms and hold_ms, how long its press lasted. Its to_dict() is {"gesture": "tap1", "t_ms": 91340}: the name says the button.

The numbers behind the names are HOLD_MS, TAP_FOLLOW_MS, PRESS_MATCH_MS, PRESS_LOST_MS and TAP_CHAIN_MAX. Read them from the constants; do not copy them.

18.3 Cues#

A cue says how a button should be lit. In the SDK a cue list is an array of at most three {"button", "state", "role"} objects, with button 1, 2 or 3, checked by the cue schema.

  • state is one of CUE_STATES: off, on, pulse or chase. A pulse beats twice a second (PULSE_PERIOD_MS) and a chase turns once a second (CHASE_PERIOD_MS).
  • role is one of CUE_ROLES (a, b, c, live, go, warn) or null. An off cue has a null role.
  • One cue per button. A list with two cues for one button is invalid.
  • A button with no cue is not driven, and an empty list clears every cue.

bollardeck.device.cues checks and renders a list in Python, and bollardeck.cues does in the browser. The buttons have no LEDs of their own, so whatever renders a cue (the Boll's lights, a web page, a dock) draws it from these rules.

18.4 The keymap#

The engagement keymap says what a gesture means while the band is asking the person something, and when it is not. It is bollardeck.constants.KEYMAP, version 2, checked by the keymap schema.

{"v": 2,
 "always": {"hold3": "power"},
 "idle":   {"tap1": "skip_to_frontier"},
 "prompt": {"tap1":  {"abc": {"choice": "A"}, "spectrum": {"choice": -1}, "game": {"do": "raw"}},
            "hold1": {"abc": {"do": "replay"}, "spectrum": {"do": "replay"}, "game": null}}}
  • It has three maps keyed by gesture name. always is checked first; then prompt while a prompt is showing, and idle while none is.
  • In always and idle, a gesture's value is a string: the name of an action.
  • In prompt, a gesture's value is an object with one member for each kind of prompt (PROMPT_KINDS: abc, spectrum, game). Each member is {"choice": …} (answer the prompt with that value), {"do": …} (a named action) or null (the gesture is taken and does nothing).
  • bollardeck.device.keymap.resolve(gesture, prompt) applies it and problems(keymap) checks one; the browser twin does the same. The example above is a valid keymap, cut down from the shipped one.

A keymap whose "v" is not 2 is refused.

19. The corpus client and the gate#

A corpus is a licensed song collection, and its corpus.json carries a licence block, popboll.licence/1, that says what may be done with it. bollardeck.corpus is the one way a client reads a corpus, and it will not hand over content without first asking what is allowed.

19.1 Asking#

from bollardeck.corpus import Corpus, Identity

corpus = Corpus.load("path/to/a/corpus")
verdict = corpus.may("study", Identity(service=True))
if not verdict.allowed:
    print(verdict.reason)          # for example "licence_missing"
  • A capability is one of bollardeck.corpus.licence.CAPABILITIES (transcode, store_audio, stream_audio, store_art, store_morpus, musicians_may_study, export_morpus, device_copies) or see. study is short for musicians_may_study.
  • The answer is a Verdict: allowed or not, with one reason from bollardeck.corpus.gate.REASONS.
  • Every accessor that returns content demands a fresh verdict and raises GateRequired without one.

19.2 The rules of the gate#

The gate fails closed. REASONS is the full list; the rules that surprise people are these.

  • No licence block, no access. A corpus whose manifest has no licence block, or a null one, is refused with licence_missing for every capability of the licence. Only see, which asks about visibility and nothing else, is answered without a licence block.
  • What the code does not understand is refused: an unknown scope (unknown_scope), a newer licence version (newer_version), a manifest that fails its schema (manifest_invalid), or two differing blocks under licence and license (licence_ambiguous).
  • Private means private to people. A corpus that is not public is refused with not_visible to anyone who is neither its owner nor on its access list. The band's engines may still study it if its licence allows study.
  • An expiry the device already holds is honoured offline, by the device's own clock (expired). For a corpus cached from the hub, setting the clock back does not reopen it. A corpus read from a local folder follows the clock as it is given.
  • A revocation is news, so it takes effect when the device next reaches the network (revoked). Until then a cached corpus keeps working, and the verdict says since when it has been unverified.
  • not_granted means the licence is in order and does not include the capability.

19.3 The hub client#

bollardeck.corpus.HubClient is the client for bollawax.org: one token, conditional requests, a disk cache that revalidates, and no silent substitution of one corpus for another.

  • The read token alone is for study only. For a corpus that is not public, a caller that presents only the token gets the morpus and a manifest with the people's addresses removed. Track lists, audio, art and bundles are not found.
  • A named person gets more. acting_for= names the person a request is for. It travels in the X-Pop-For header and never in a URL, and that person's copy is cached apart from everyone else's.
  • Names are self-declared for now. The hub believes the name it is given until sign-in exists (planned, no version promised yet; §17.3). Do not build anything that depends on the name being proved.

19.4 Attestation and self-hosted corpora#

  • bollardeck.corpus.attest.verify_attestation checks a corpus edition's manifest hash and its Ed25519 signature. The ML-DSA-65 signature is checked only where the platform's OpenSSL knows the algorithm; where it does not, the result says not_checked for that signature and partial overall, never verified.
  • bollardeck.corpus.fetch_origin reads the discovery file of a self-hosted corpus. The service that pulls such a corpus into Pop Boll is not built.

20. The cart and its docks: what is decided#

The bollardeck text for the cart is not written. This section records what is decided, so that nobody builds against the retired arrangement.

  • The Bolltridge is the core. The cart holds the computer, the corpora, a secure element and the only radio, and runs Bollawax and the deck. A Pop Boll and a Glass Snipler are docks for it.
  • The cart is the USB host of whatever dock it stands in, and takes its power from the dock. The connection is the Bollport (Bollard §25).
  • A dock describes itself with the Bollard JSON descriptor and a bollardeck hello, with fields for the dock added and a slots field for its cart positions. The field list, the dock classes and the transport are for Bollard v2 to fix.
  • One image, picked by the dock. A cart runs one Bollawax image and chooses what to be from the dock's class.
  • At most two carts in one dock. The cart in the first slot is the brain. A second cart is a guest: a USB device of the first, lending its corpora and its computing, and showing nothing of its own.
  • The serial belongs to the cart. A dock has a model and a serial of its own in its descriptor and no device serial.

Bollawax 0.2.5 carries the cart's software for pocket mode (a cart running with no dock, on a battery of its own) and a revised dock descriptor. No cart exists yet, and none of it is verified on hardware. This section still describes the cart as Bollawax 0.2.4 specifies it.

Not written: who sends hello first between a cart and its dock, the link between two carts, the guest's hello and how its secure element proves its licences, and the events for a dock's encoder and EJECT button.

21. Distribution (planned)#

None of this exists yet. Today a routine reaches a Boll by being copied onto it (§14.4).

The plan is a signed registry at bollawax.org for routines, dock plugins and third-party apps, with routines first, because they are small and need no hardware.

  • Publishing. A developer records an affirmation (name, contact, licence, and the right to distribute everything inside), and publishes under their own name. Published work may bundle soundfonts, samples and audio under licences that permit redistribution. It must not bundle anything extracted from a corpus.
  • Automated checks. Routines are loaded with the static checks and run under the guard on fixture songs, and are rejected if they fail, break a limit or touch the guard.
  • Signing. What passes is signed, and a device verifies the signature before it installs anything.
  • Sideloading and hacker mode. Hacker mode is the owner's switch on one device they own, so a developer can build and test there before publishing. It is turned on at the device itself and confirmed there; it cannot be set over bollardeck, over the network or by a routine, and the device shows that it is on. While it is on, that device accepts unsigned work, labelled as unsigned. Every other rule still holds: corpus rules, consents, and no corpus files or credentials to third-party code. Unsigned work is never distributed. How the switch is thrown on a cart and its docks is not written.
  • What a developer accepts: the SDK's Apache-2.0 licence, the Bollawax Device Licence for images, and the Snipler Use Grant for the Glass Snipler's fetched files. There is no sign-up agreement.

The packaging format, the label names and the review steps will be published with the registry.

22. Open items#

Not yet validated on hardware. Nothing in Part A has run on a Boll.

  • Not built yet: everything in §17.3, the BLE UUID allocation (the UUIDs in §3.2 are placeholders), the binary LED characteristic, and remix in render mode.
  • Not written yet: the bollardeck text for the cart (§20) and Bollard v2.
  • Part A against the SDK. The matrix message of §6.3 splits a frame into px and rgb; the SDK's engine packs a frame as 113 bytes, the 104 pixels and then the three RGB LEDs. One of the two will change. The led messages describe a 24-LED ring, and the lights of the cart's docks are not fixed.
  • Estimates: every latency, rate and transfer time in this document.
  • Undefined grants: the grant that covers get with "what": "routine", and the grant that covers audio with "dir": "abus".
  • The hello profile value for an accessory with no physical connection (BLE only) is not defined, and there is none for a Bollport dock.
  • MRPH5: the stem role mapping is a proposal until the format is frozen, and so is the per-corpus dock_stems setting.
  • Routines: the plan and part fields beyond those listed in §12.8 are not frozen, and there is no stable import path yet for creating note objects from scratch (copy a received note instead). The chord symbols and the ticks per bar are the Boll's song factory's, not the SDK's.
  • Buttons and gestures on the wire. bollardeck/1 names buttons inner, middle and outer, and its ev words (§6.2) do not match the gesture names the SDK's engine reports (§18.2). A wire that carries button numbers and the SDK's gesture names is proposed for a later protocol version; within bollardeck/1 the fields stay as they are (§9).

Changes#

  • 0.2-draft (2026-11-10). The Bollard SDK 0.1.0 is described (Part C, §17–§19); the SDK's buttons, gestures, cues and keymap are described beside the wire's own labels; the cartridge messages and cartridge.yaml of the old Part C are withdrawn; §20 records what is decided about the cart; Part B points at the SDK's schema, limits, reader and simulator; the licence is no longer "proposed".
  • 0.1-draft. First draft.

23. Licence#

  • This specification text: CC BY 4.0.
  • The Bollard SDK, including the example dock and the example routines on this page: Apache-2.0.
  • Not open: Bollawax itself (the band, the judges and every image), which runs on your device under the Bollawax Device Licence; the Glass Snipler's engine and files, all rights reserved; and every corpus, which stays under its own licence.

These licences cover the documents and code, not the project's names and logos.

CONTACT THE TEAM