Hermetic Modular

06/HostLink

HostLink

HostLink carries browser edits, preset transfers and diagnostics over the front USB-C port.

HostLink: editing a running module from the browser

Host configuration

Construct hostlink::Host with the preset store and module identity:

static alchemy::hostlink::Host host(
    presets, "my_module", "My Module", "1.0.0", GIT_HASH);

Attach it to the control loop during setup:

loop.Use(host);

The default Host starts on its first Poll(), after setup has attached pages, buttons, jacks, and manual content. Factory defaults are captured separately, before the boot preset replaces the live state.

Without ControlLoop, call host.Poll(now_ms) regularly from the same main thread that manages the module’s state.

Without presets

A firmware can provide identification, reboot, and extensions without implementing preset storage:

static alchemy::hostlink::Host host(
    "my_module", "My Module", "1.0.0", GIT_HASH);

No dummy preset store is needed. The programmer exposes the capabilities the firmware supports and omits unavailable preset and live-state operations.

Describing the module

The editor derives its controls from the firmware’s declarations. Stable field IDs identify controls; names, units, labels, help text, and page metadata explain them.

  • .Ident() supplies a stable field ID.
  • .Unit() describes an engineering unit.
  • .Labels() names discrete choices.
  • Button declarations describe gestures and state.
  • Page::HidePot() controls editor and manual visibility without removing stored data.
  • Manual hooks attach prose and cross-references to the controls and jacks they describe.

Auto-derived equal-width selectors include disp.bins. The programmer uses that bin count to interpret the normalized value and writes the selected bin’s center. Display hints without bins keep their previous interpretation.

OnDescribe() overrides a component’s description. Descriptor() supplies a complete hand-written descriptor.

Changing metadata while connected

After changing long-lived page, button, jack, or manual declarations, request a refresh:

const bool queued = host.RequestDescriptorRefresh();

The Host rebuilds the auto-derived descriptor during a later poll. DescriptorRefreshStatus() reports Idle, Pending, Succeeded, or Failed.

Refresh is for metadata. Field identities and the serialized state layout must stay stable. Factory defaults remain the values captured during startup; refreshing does not turn the current knob positions into new defaults.

If a candidate fails validation, the previous descriptor remains published. Correct the declarations and request another refresh to retry. Repeated requests while one is pending coalesce.

The default refresh workspace is 24 KiB. If a larger custom descriptor buffer is configured, provide a matching, separate workspace with DescriptorRefreshBuffer() before the Host starts.

This refresh path is unavailable for a hand-written Descriptor() and for a Host without presets. The request returns false in those cases.

The programmer detects a changed descriptor through its reported length and CRC, fetches a complete replacement, and preserves compatible edits. It does not combine chunks from different descriptor versions.

Fast values

host.Fast(pager, settings) exposes stored control values for frequent browser polling without serializing the full preset or its recordings.

Opt in during setup
presets.Manage(pager);
presets.Manage(settings);

host.Fast(pager, settings); // before the first Host Poll() or Start()
loop.Use(host);

Register each object with Presets::Manage and Host::Fast before the first Host Poll() or Start(). Attach the Host to ControlLoop so targeted edits use current physical pot readings to re-arm pickup. Keep the selected serialized layout fixed after startup. Check host.ConfigurationOk() after the first poll.

The programmer requests fast values about every 100 ms while the page is visible, the live device is selected and mirroring is enabled. Full snapshots refresh about every 1.6 seconds. Firmware updates and other busy operations pause polling.

GET_FAST reads selected byte ranges from the live preset representation. SET_FAST edits a field within a writable range. Full snapshots and preset files contain the complete serialized state. Devices without fast values use the full-snapshot path.

Custom fast projections

Use host.Fast(owner, projection) to select a byte range within a custom serializable object. Include alchemy/host_link/fast_values.h and initialize these FastProjection fields:

  • offset and size: the region within the owner's serialized preset data, in bytes.
  • context: application state that remains alive for the Host's lifetime.
  • capture(context, destination, size): fill exactly the selected region in bounded control-loop time.
  • edit(context, relative_offset, data, size): validate an edit before mutating state and return whether it succeeded. Omit it for a read-only region.

The capture callback must write exactly the selected region's bytes to its destination, using the owner's preset format. Regions must be nonempty, disjoint and within their managed owners. Up to 16 regions, including their response headers, must fit one HostLink frame. Invalid configuration disables fast values and makes Host::ConfigurationOk() false. Capture callbacks run in the main loop; avoid allocation or traversal of recording buffers.

When using HostLink with MIDI, configure a custom USB identity before either service starts. See USB configuration.

USB messages and live values

Add diagnostics to an existing Host:

#include "alchemy/host_link/diagnostics.h"

static alchemy::hostlink::Diagnostics debug;
static alchemy::hostlink::Gauge<float> peak(
    "audio.peak", "Output peak");

During setup, after hardware initialization and before the first Host poll:

peak.Unit("FS");
const bool watched = debug.Watch(peak);

host.Extend(debug);

if (!watched || !debug.ConfigurationOk() || !host.ConfigurationOk())
    debug.Error("Diagnostics configuration failed");

debug.Info("Ready");

Emit formatted messages from the main/control thread:

debug.Debug("Entered settings");
debug.Info("Preset loaded");
debug.Warn("Using factory defaults");
debug.Error("SD write failed");

Publish the latest measured value with peak.Set(value). Gauge updates may run in the audio callback. Supported gauge types are float, int32_t, uint32_t, and bool.

A gauge contains the latest value, not a sample history. Maintain counters in the application and publish their current totals.

Formatted logging uses fixed storage and does not wait for a connected computer. The default capacity is 32 messages with 160 text bytes each, and up to 64 gauges. Long messages are truncated; older messages can be overwritten when the ring fills. Logs are volatile and do not survive a device reset.

PrintLine() is an Info-level alias for migrating existing logging calls. Do not also use seed.StartLog(), seed.PrintLine(), or direct USB writes on the HostLink port.

Floating-point printf formatting requires the corresponding toolchain support. A Gauge<float> provides a numeric readout without adding that formatting dependency.

Device console

Connect the running module in the programmer, then open Device console.

Use the level filters to select messages. Pause stops console polling without stopping the firmware. Follow controls automatic scrolling. Wrap toggles line wrapping. Export downloads the captured log, and the font controls change its displayed size.

Clear clears the browser’s captured display; it does not erase the device’s retained history. Message times are device uptime.

Console polling stops while the console is closed or a firmware update is in progress. Opening or resuming it retrieves the available retained messages. If the device produced more messages than it could retain, the console reports the gap.

Live gauges are polled separately at approximately one-second intervals. Up to 64 can be displayed when the firmware registers them.

Diagnostics must be enabled by the firmware. An unsupported console does not prevent other supported editor features from working.

Descriptor errors and preset files

When descriptor validation fails, the programmer shows the received JSON in the error details.

Preset export and exact-schema restore work without a valid descriptor. To restore, select a file and destination slot, then click Write preset; for a bank file, click Restore bank. Migration between different schemas requires a valid descriptor.

Commands and audio

Call host.Poll and run command handlers in the main loop, where panel and browser edits update the same state.

Transfer command-driven changes to audio through a queue or SnapshotExchange. Keep formatted logging, USB transfers and storage operations outside the audio callback.

Terminal access

The SDK includes a HostLink CLI for macOS and Linux:

node tools/hostlink-cli/hostlink.mjs -p /dev/cu.usbmodem1234 logs --follow
node tools/hostlink-cli/hostlink.mjs -p /dev/cu.usbmodem1234 logs --level warn
node tools/hostlink-cli/hostlink.mjs -p /dev/cu.usbmodem1234 logs --json
node tools/hostlink-cli/hostlink.mjs -p /dev/cu.usbmodem1234 watch

Replace the port with the module’s actual device path. Close the browser’s device connection before opening it from the CLI.

The CLI speaks HostLink, not raw terminal text. It also supports identity, descriptor, preset, and reboot operations.

The complete wire contract is documented in hostlink-protocol.md.