Hermetic Modular

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

Connect any module to the web programmer and its manual appears inline with the controls. Hermetic Modular firmwares additionally publish a standalone page built from the same data, like Echoa's manual. If you're interested in this for your firmware, get in touch!

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.

a documented knob
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 connectionDescriptor ID
Left audio input, J1IN_L
Right audio input, J2IN_R
Switchable jacks J3–J8J3, J4, J5, J6, J7, J8
Left audio output, J9OUT_L
Right audio output, J10OUT_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.

JackSigRendered as
AudioInAudio In
AudioOutAudio Out
CvBi±5 V
CvUni0–5 V
Voct1 V/oct
GateGate
TrigTrig

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 and settings text
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.

the module block
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)

One line of identity, shown in the module header.

Manual& Preamble(const char* markdown)

The introduction at the top of the manual.

Manual& Section(const char* id, const char* title, const char* body)

Adds a long-form section with a stable ID, title, and Markdown body. A manual supports up to sixteen sections. The section ID is used for its deep link.

Manual& PresetsHelp(const char* markdown)

Replaces the stock preset text with your own.

Host& Attach(const Manual& manual)

Attaches the block. Call it anywhere in main() before the loop runs.

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 declarationResult
A cross-reference to a control without an emitted IDDescriptor assembly fails
A cross-reference naming an entity that is not emittedDescriptor assembly fails
Gesture help naming an undeclared gestureDescriptor assembly fails
More than four cross-references on an entityDescriptor assembly fails
More than sixteen manual sectionsDescriptor assembly fails
Content exceeding the configured descriptor bufferDescriptor 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.