Designing the interface block
2026-06-05
Designing the interface block
The interface {} block is your script’s public face.
Every field you declare there becomes a parameter the host can read and
write over the JSON parameter server,
appears in discovery, and is the variable your script reads and writes
by name. Designing it well means the host side of your experiment is
self-describing: valid ranges are enforced by the device, units are
declared, and modes are explicit.
This guide builds an interface up step by step — from a single boolean to a multi-mode parameter — with each step a complete, deployable script.
A single boolean
The smallest useful interface is one flag. Sizes are declared up
front: 1w is one 32-bit word, 1b one bit. The
whole interface must total a whole number of words, so pad the rest with
reserved:
1w interface {
1b bool enable;
31b reserved;
} ral;
script {};
That is already a working contract. The host sets it with
{"ral": {"0": {"enable": true}}}, discovery reports it, and
the script reads it as ral.enable. reserved is
padding only — it is not exposed to the host.
Sized integers
Numbers are unsigned or signed, with any
width up to 32 bits. A field may not cross a word boundary, so pack
related fields into a word and pad the remainder:
1w interface {
1b bool enable;
8b unsigned level;
8b signed offset;
15b reserved;
} ral;
script {};
If the declared widths do not add up to the interface size, the
compiler rejects the script with E103 and tells you the
required and used bit counts — make the declaration and the contents
agree.
Valid ranges
So far the host could write any value that fits the width. A
{valid = ...} set turns the field into a guarded parameter:
the device rejects out-of-range writes at the JSON
interface, before your script ever sees them. The set is a parenthesised
list of single values and [low:high] (or
[low:step:high]) ranges:
1w interface {
16b unsigned {valid = (0, [1:2000])} duration;
16b reserved;
} ral;
script {};
Here duration accepts 0 (meaning “off”, say) or anything
from 1 to 2000. Put your safety limits in valid rather than
in script logic — the host learns them from discovery, and they hold
even while the script is being reloaded.
Units
Fields are integers, so fractional quantities are expressed by
declaring what one count means. {unit = 0.001 A} says the
field counts milliamperes; a host that reads the discovery information
can render 1500 as 1.5 A:
1w interface {
16b unsigned {unit = 0.001 A, valid = ([0:5000])} amplitude;
16b reserved;
} ral;
script {};
This is the same convention the built-in modules use (and the
function library’s x1000 scaling follows the same idea — see the language reference). Combine
unit and valid in one brace, separated by a
comma.
Flags: emit, protected, persistent
Three flags shape how a field behaves beyond plain read/write:
emit— stream the field’s value continuously over LSL, so it can be recorded.protected— the host can read it, but only the script can write it. Use this for results.persistent— the value survives a script reload, so one script can hand a value to its successor.
A field can carry several flags. This script publishes a result the host can watch but not corrupt:
2w interface {
1b bool enable;
31b reserved;
1w emit protected signed heart_rate_x1000;
} ral;
script {
if (ral.enable) :
72000 -> self.heart_rate_x1000;
fi;
};
(There is also hidden, which keeps a field out of
discovery while leaving it usable by name.)
Arrays
A repeated parameter is an array: one declaration, indexed elements. The width is per element, and the element count must still fit the word rules:
1w interface {
4b unsigned {valid = ([0:10])} weight[1..8];
} ral;
script {};
The host writes elements by index, and the script reads
ral.weight[3] or slices like
ral.weight[1..4].
Enums: named states
When a field’s values are states rather than quantities, name them.
An enum maps names to integers, and the names appear in
discovery — so the host sets a state, not a magic number:
1w interface {
2b enum {off = 0, ramp_up = 1, hold = 2} phase;
30b reserved;
} ral;
script {};
In the script an enum reads as its integer value, which pairs
naturally with the switch-like if from the scripting guide.
Modes: oneof
The capstone. Sometimes a group of parameters only makes sense
together, and which group applies depends on a mode — a sine wave has a
frequency, a pulse has a width. A oneof declares exactly
that: a selector plus one payload region that is decoded differently per
mode. Each mode {} names its own fields and its selector
value:
2w interface {
1w oneof 1w {
mode {
1w reserved;
} off = 0;
mode {
16b unsigned {unit = 0.001 Hz} frequency;
16b unsigned {unit = 0.001 A} amplitude;
} sine = 1;
mode {
16b unsigned {unit = 0.001 s} width;
16b unsigned {unit = 0.001 A} amplitude;
} pulse = 2;
} waveform;
} ral;
script {};
Read it as: waveform is a 1-word selector followed by 1
word of payload (two words in total). In sine mode the
payload is a frequency and an amplitude; in pulse mode the
same bits are a width and an amplitude; off needs nothing.
The mode names and their fields all appear in discovery, so the host
knows exactly which parameters each mode takes.
In the script, the selector itself reads as an integer, and a mode’s
fields are reached through the mode name with an explicit instance index
(ral.0.waveform.sine.frequency — the ral.field
shorthand does not reach inside a oneof):
2w interface {
1w oneof 1w {
mode {
1w reserved;
} off = 0;
mode {
16b unsigned {unit = 0.001 Hz} frequency;
16b unsigned {unit = 0.001 A} amplitude;
} sine = 1;
} waveform;
} ral;
script {
if (ral.0.waveform)
is 1: ral.0.waveform.sine.frequency -> dio.0.digout_1;
fi;
};
Design checklist
- One word at a time. Plan fields so none crosses a
word boundary, and pad each word with
reserved; the compiler’sE103tells you when the arithmetic is off. - Limits belong in
valid. The device enforces them at the JSON interface; the host discovers them. - Declare units. A
{unit = ...}makes the integer self-describing. - Results are
protected. Readable, not writable, never corrupted by the host. - Watchable values are
emit. Anything worth plotting or recording should stream over LSL. - States are
enums, mode-dependent parameter sets areoneofs. Names beat magic numbers, and the structure shows up in discovery.
Next steps
- Scripting guide — the script side: control flow, signals, stimulation.
- Language reference — every type, flag, and function.
- Parameter server guide — how the host reads and writes the fields you just declared.