08/Manual Hooks
Documentation In The Firmware
A module can declare helpful text that renders in the web programmer. This can be statically displayed as a manual, or it can be rendered as hints alongside the user via hostlink. In this way, a manual can travel with a firmware itself.
Why it lives in the firmware
Everything is optional and additive. A firmware with no help text produces the same descriptor it always did, and the programmer renders it exactly as before. Add a .Help() or .SeeAlso() and a disclosure appears on that row.
Where the text ends up
Help on a control
Every declaration site takes .Help(). It accepts a markdown subset: paragraphs, bold, italics, lists, and inline code. Write what the control does and why you would reach for it. Do not restate the range, the default, or the zone labels. The host already renders those as structured facts beside your prose.
VirtualKnob cutoff = VirtualKnob(0, "Cutoff")
.Exp(20.f, 12000.f)
.Unit("Hz")
.Ident("flt.cutoff")
.Help("Corner frequency of the ladder filter. The taper spends most "
"of its travel below 2 kHz intentionally.")
.SeeAlso(resonance, "presets");
.Ident() (see hostlink) is what makes a control referenceable. A knob or a settings handle without one can still carry help, but nothing can link to it, and pointing a .SeeAlso() at it fails descriptor assembly rather than emitting a dead link.
Cross-references
.SeeAlso() takes the objects themselves: a knob, a button, a jack, a settings handle. The descriptor build resolves each one to its wire id and validates it, so a rename cannot leave a broken reference behind. The string form exists for entities with no object, currently "presets". Up to four per entity.
The programmer renders them as chips under the help text. Clicking one switches to whatever tab holds the target, expands its row, and scrolls it into view, which is what makes a module's documentation navigable, particularly for linking settings and controls.
Buttons and gestures
A button can describe its overall purpose with .Help() and attach separate explanations to its gestures with .GestureHelp().
Structured .Tap() and .Hold() declarations are included in the button’s published metadata. Add their help after declaring the gesture, using the keys "tap" and "hold":
static VirtualButton mode =
VirtualButton(kButtonB2, "Mode")
.Selector(3)
.Tap(VirtualButton::Action::Cycle, "Next mode")
.GestureHelp("tap", "Advances to the next mode and wraps at the end.");
A metadata-only gesture declared with .Action(key, label) uses that same key for .GestureHelp(key, text).
The key identifies the gesture; the label is the text shown to the reader. Do not duplicate a structured tap or hold with an extra .Action() merely to make its help appear.
An unmatched gesture-help key fails descriptor validation. Runtime handling still belongs to the button bank or the application’s own button code; publishing a description alone does not install a handler.
Jacks
Jack describes a physical panel connection. It carries a stable ID, display name, short label, signal class, help text, and optional cross-references. It does not configure the electrical direction or add preset state.
On the Alchemy Lab panel, use these physical IDs:
| Physical connection | Descriptor ID |
|---|---|
| Left audio input, J1 | IN_L |
| Right audio input, J2 | IN_R |
| Switchable jacks J3–J8 | J3, J4, J5, J6, J7, J8 |
| Left audio output, J9 | OUT_L |
| Right audio output, J10 | OUT_R |
The ID determines where the jack appears on the panel. Array order does not determine its position. Put the firmware-specific purpose in the name and short label:
static Jack clock_in =
Jack("J3", "Clock input", JackSig::Trig)
.Short("CLOCK")
.Help("Receives a rising pulse for each clock tick.");
Keep the declaration alive for as long as the Host uses it. If its published metadata changes while connected, request a descriptor refresh.
Signal classes describe the firmware’s intended use. Labels such as “±5 V” or “0–5 V” are not electrical maximum ratings or a statement of the ADC’s normalized input span. The hardware documentation defines those separately.
The signal class comes from a closed vocabulary so hosts can label it consistently. An input that is normalled from another jack declares it with .Normalled(other), and the host shows the connection on both rows.
| JackSig | Rendered as |
|---|---|
AudioIn | Audio In |
AudioOut | Audio Out |
CvBi | ±5 V |
CvUni | 0–5 V |
Voct | 1 V/oct |
Gate | Gate |
Trig | Trig |
Pages and settings
A page takes .Help() for the block above its rows, which is the place to explain what the page is for rather than what each knob does. Settings slots take the same treatment, plus the identity that makes them referenceable and readable.
Page shape = Page(1).Name("Shape").Color("#f59e0b")
.Help("How the voice is contoured after the oscillator.");
handles.mode = settings.Page(0).Pot(0)
.Selector(4).Labels(kModeNames)
.Ident("routing.mode").Name("Delay Mode")
.Help("How the two lines are connected.");
settings.Page(0).Help("Routing between the lines, plus preset save and load.");
.Labels() is worth setting even without help text: it replaces zone indices with names everywhere the host shows that setting.
Module level
Manual carries content that belongs to the whole module: a one-line tagline, an opening preamble, and up to sixteen long-form sections. Use sections for signal flow, operating modes, engine behavior, or other explanations that span several controls.
const Manual kManual = Manual()
.Tagline("Stereo dual delay, four engines")
.Preamble("Two independent delay lines, each with its own character.")
.Section("signal-flow", "Signal Flow",
"Input is routed to two stereo lines. Inside a line, per sample: ...")
.PresetsHelp("A preset captures every pot on every page.");
host.Attach(kManual);
The SDK ships stock text for the features it owns, so presets, parameter locks, SD storage, and brightness are documented even if you write nothing. Override any of them from alchemy::stock_help when your module does something unusual.
Manual& Tagline(const char* one_line)
Manual& Preamble(const char* markdown)
Manual& Section(const char* id, const char* title, const char* body)
Manual& PresetsHelp(const char* markdown)
Host& Attach(const Manual& manual)
Keeping it in one file
Prose grows, and it reads badly interleaved with DSP wiring. The convention in the template and in Hermetic Modular's own firmwares is a single file for the prose: the jack table, the button table, the module block, and a table of help text keyed to the controls declared elsewhere. Your control declarations stay where they are and stay readable, and the writing lives somewhere you can edit like a document.
Descriptor validation
Manual content is validated when the descriptor is assembled. This is distinct from compiling the firmware.
| Invalid declaration | Result |
|---|---|
| A cross-reference to a control without an emitted ID | Descriptor assembly fails |
| A cross-reference naming an entity that is not emitted | Descriptor assembly fails |
| Gesture help naming an undeclared gesture | Descriptor assembly fails |
| More than four cross-references on an entity | Descriptor assembly fails |
| More than sixteen manual sections | Descriptor assembly fails |
| Content exceeding the configured descriptor buffer | Descriptor assembly fails |
The section limit and descriptor byte capacity are separate limits. The default Host descriptor buffer is 24 KiB; a larger manual may require a larger buffer even when it uses fewer than sixteen sections.
On initial auto-derived construction, the Host publishes an error descriptor so the programmer can show the failure reason. A failed runtime refresh keeps the previously published descriptor instead.
Applications using a custom descriptor builder must check its result before publishing the buffer.
Seeing your own manual
Build your firmware, flash it, and connect on the web programmer. A static generation tool may come later if there is demand. If you want it, let me know on discord or github issues!
Manual hooks ride on HostLink, so everything here works alongside preset transfer, live editing, and the updater. The headers under alchemy/surface are the full reference.