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 the build 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 carries one help entry for the button itself and one per gesture. Gesture help is keyed on the gesture string you already passed to .Action(). A key that matches no declared gesture fails the descriptor build, so a renamed gesture cannot silently orphan its documentation.
VirtualButton page = VirtualButton("b1", "Page / Lock")
.Role(VirtualButton::Role::Modal)
.Action("tap", "Next Page")
.Action("hold+knob", "Record Param Lock")
.Help("Navigation and recording. On its own it walks the pages; held, "
"it turns the pots into recorders.")
.GestureHelp("tap", "Pot-catch re-arms on arrival, so nothing jumps.")
.GestureHelp("hold+knob", "Repeat on a locked pot to clear it.");
Jacks
Jack is pure descriptor metadata. It has no runtime behavior and no state, and it does not touch the schema hash. Declaring your jacks tells the host what each one is, what it is called on the panel, and what it connects to, which is what lets the programmer draw a labelled faceplate.
constexpr Jack in_l = Jack("IN_L", "In L", JackSig::AudioIn).Short("IN L");
constexpr Jack cv1 = Jack("J3", "CV 1", JackSig::CvBi)
.Short("CV1")
.Help("Assignable CV input. Destination and amount are set on the CV pages.")
.SeeAlso(cv_dest_1, cv_atten_1);
/* in main(), before the link starts */
host.Jacks(in_l, in_r, cv1, cv2, out_l, out_r);
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 what has no control to hang from: a one line tagline for the header, an opening preamble that appears in the header, and up to eight long-form sections for the things that span the whole module, like signal flow or the character of an engine, or vibes, or credits.
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.
What fails the build
Manual content is validated when the descriptor is assembled.
| Mistake | Result |
|---|---|
| SeeAlso pointing at a control with no Ident() | descriptor build fails |
| SeeAlso naming an id nothing emits | descriptor build fails |
| GestureHelp keyed to a gesture that is not declared | descriptor build fails |
| More than four SeeAlso refs, or more than eight sections | descriptor build fails |
| Prose that overflows the descriptor buffer | descriptor build fails |
A failed build does not ship a broken manual and does not fail silently either. The module reports the reason over the wire, and the programmer shows it instead of the editor.
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.