The node
What it takes to be a Ranvier node: the manifest you ship, the ports you declare, the settings you accept, and what your program does between being spawned and exiting.
Read wire.md and control.md alongside this.
control.md specifies the protocol on the connection; this document specifies
what the node owes that protocol and what it declares before anything runs.
The key words MUST, MUST NOT, REQUIRED, SHALL, SHALL NOT, SHOULD, SHOULD NOT, RECOMMENDED, MAY and OPTIONAL are to be interpreted as described in RFC 2119.
How to read the citations
This document is authoritative. Where it and any other document in this
repository disagree about what a node must do, this one governs and the other is
the one to correct. docs/decisions/ records why a rule was chosen; it does not
state what the rule is.
Every requirement below carries a path:line or a check block. Re-derive it
rather than trusting the sentence — line numbers move, and a specification that
cannot be checked goes stale invisibly. docs/docs.tools/check-spec.py resolves
every citation and runs every check block in continuous integration.
This repository ships no device integration. It is transport and runtime; a
device integration and a modality's vocabulary belong to the modality, and are
packaged separately. The worked examples this document cites — pulse-source,
pulse-relay and pulse-sink, under
crates/runtime/session/nodes/ — are what the manifest format and the control
plane are exercised by here.
A consequence worth stating rather than hiding: where a rule is held down only by a test inside a device package, no build in this repository fails when that test goes. Such a rule is marked test-asserted below, which means exactly that and nothing stronger.
Three kinds of rule appear here, and they are not interchangeable. Each requirement says which it is:
| Marked | Means |
|---|---|
| parser-enforced | Registry refuses the manifest at load, on a machine with none of the hardware attached. You cannot ship past it. |
| runtime-enforced | Checked when a session starts or when your node registers. You can ship past it; the session refuses. |
| test-asserted | Held down by a test inside one package. Nothing in the core checks it, and a package without that test is not covered. |
| convention | Neither. Followed by every shipped package and enforced by nothing. |
A rule that is convention is still a rule — a SHOULD, or a MUST whose
failure shows up somewhere other than a refusal. Where enforcement is absent,
this document says so explicitly rather than letting a MUST imply a check
that does not exist.
1. What a node is
A node is two things shipped together:
- A manifest — a YAML file named
ranvier-node.yamldeclaring one node type: what it is called, what to run, which ports it offers, and what it can be told. - A program — the binary the manifest names, which registers with a runtime, publishes and subscribes on the ports it advertises, and stops when it is asked.
The split is load-bearing. A graph is checked before anything is spawned, on a machine that may have none of the instruments attached, so everything a graph needs to know MUST be readable without running the program [rvr_1pdfxj81uzzs min 2]. The cost of the split is that a manifest can drift from its binary, and §9.3 specifies where that drift is caught.
A node type is not a node. The manifest declares a type; a graph document instantiates it any number of times under different identifiers [rvr_tf8htkxrfoi3]. Two Muse headbands are two nodes of one type.
2. The manifest file
2.1 Name and location
The file MUST be named ranvier-node.yaml [rvr_c27dm6uxgp96]. No other name
is read.
A registry is built by scanning one or more search roots. For each root, the
scan considers exactly two places: <root>/ranvier-node.yaml, and
<root>/<subdirectory>/ranvier-node.yaml for every immediate subdirectory
[rvr_lst9vltvvh64]. It does not recurse further — a manifest two levels below a
root is not found [rvr_0ky04af5pkte].
So a package MUST place each manifest either at a search root or exactly one directory below it. The shipped convention is one directory per type, named for the type:
find crates -name ranvier-node.yaml \
-not -path '*/target/*' -not -path '*/dist/*'
# crates/runtime/session/nodes/pulse-relay/ranvier-node.yaml
# crates/runtime/session/nodes/pulse-sink/ranvier-node.yaml
# crates/runtime/session/nodes/pulse-source/ranvier-node.yaml
Three manifests ship here, the runtime's own pulse-* worked examples. That
directory-per-type naming is convention: the registry reads type: out of
the file and never looks at the directory name.
Search roots are given to ranvier-run with --nodes <dir>, which is
repeatable and defaults to ./nodes when that directory exists
[rvr_vxlorbf3sxu0 min 2].
Anything in a search root that is not named ranvier-node.yaml is ignored.
crates/runtime/session/nodes/two-processes.yaml is a graph document rather than
a manifest. It sits beside manifests and is never read as one, because the scan
matches on the filename alone. You MAY ship other YAML beside your manifests
for the same reason.
2.2 One type per file, one type per registry
A file declares exactly one type. The format has no list form.
Two manifests declaring the same type: are refused — the registry does not
pick one [rvr_nhmhs5jnnz3r min 2]. Resolution by scan order would make the same
graph start differently on two machines.
2.3 Unknown fields are refused
Manifest, Probing, Where, PortSpec and ParamSpec all carry
#[serde(deny_unknown_fields)] [rvr_v8h106ni6zfv min 5].
A manifest MUST NOT contain a field this specification does not list. A
misspelled field is a Problem::Malformed naming the file and the parser's
complaint [rvr_54of1vrl64gp], not a field silently ignored.
The consequence for you: there is no forward-compatible extension point. A manifest written for a newer runtime does not load on an older one. The format carries no version field, and what happens when it needs one is unspecified.
2.4 YAML that parses and YAML that does not
Two failure modes are worth naming because both were hit by package authors writing manifests, and neither is obvious:
- Prose containing
:breaks a plain scalar. Ahelpreadingdoes not travel: a graph…is natural to write and refuses to parse. Everysummary,defaultandhelpSHOULD use a folded block scalar (>-). Recorded indocs/decisions/0035-a-manifest-says-where-and-the-setting-says-what-it-is-called.md:157-164; every shipped manifest follows it. - A numeric
examplemust be quoted.Where::exampleis aString[rvr_2wev9fknsr11], soexample: 12351is refused andexample: "12351"is not. A UDP port number is the case that hits this.
3. Manifest fields
Authoritative source: the Manifest type and Registry::add
[rvr_8ywfw91d2yxp min 2]. Read those, not an example manifest — an example shows
one type's choices, not the rules.
| Field | YAML key | Type | Required | Absent means |
|---|---|---|---|---|
| type | type | string | yes | the manifest is Malformed |
| command | command | string | yes | the manifest is Malformed |
| ports | ports | list | yes | the manifest is Malformed |
| label | label | string | no | the type identifier is the label |
| summary | summary | string | no | nothing is shown when choosing between types |
| vendor | vendor | string | no | there is no maker to name |
| args | args | list of strings | no | no arguments before the runtime's |
| probe | probe | string | no | there is nothing to look for |
| probe-args | probe-args | list of strings | no | the probe's default question |
| probing | probing | block | no | a running node is not re-probed |
| location | location | block | no | there is nothing to locate |
| params | params | list | no | the type takes no settings |
3.1 type — required
The identifier a graph document writes in type: [rvr_sznqxv41mfcz]. See §4.
Absent: Problem::Malformed, missing field \type``.
3.2 command — required
The program to run [rvr_d71vcsfqxygi].
Resolved beside the manifest first, then the search path [rvr_wvxg9iqw03hd min 2]. The resolution tries the name as written, then the name plus the platform's executable extension, then falls back to the bare name for the operating system to resolve.
So a manifest MUST name the command without a platform extension: write
command: input, not command: input.exe. One manifest describes the package
on all three platforms. Omitting the extension handling is what made
beside-the-manifest resolution silently never match on Windows
[rvr_wsfsaw8x0ywe].
A package SHOULD ship its binary beside its manifest, which is what makes it runnable with nothing installed.
Absent: Problem::Malformed. A command naming a program that does not exist
is not caught at load — the spawn fails when the session starts
[rvr_oi5ohwj9w2tq].
3.3 ports — required
The list of ports this type offers [rvr_azjpmc0kij1c]. No #[serde(default)],
so the key MUST be present. An empty list (ports: []) parses and declares
a type nothing can wire.
See §5.
3.4 label — optional
What this type is called for a person [rvr_8f6344heu944].
Empty means the type identifier is the label. That is a real answer for a
type whose identifier reads well on its own, and wrong for one that does not:
pupillabs-neon does not say Neon (Pupil Labs), and an interface guessing at
that is how a stream name drifted once already.
A type whose identifier is not readable as a name SHOULD set label.
Enforced nowhere: a manifest without one loads, and the type identifier stands in.
All three manifests here set it:
grep -l '^label:' $(find crates -name ranvier-node.yaml \
-not -path '*/target/*' -not -path '*/dist/*') | wc -l # 3 of 3
3.5 summary — optional
One sentence on what this type does, for somebody choosing between two
[rvr_gy2os3dzyv5u]. Same enforcement status as label: refused by nothing, and
held down where it is held down at all by a test inside the package that ships
the manifest. All three manifests here set it.
3.6 vendor — optional
Who makes the instrument, where there is a maker [rvr_67ky8ah5i839].
Empty is a statement, not an omission. A processor has no vendor, and
neither does a device every computer has. A package whose types have no maker
SHOULD assert the absence in its own tests, because an unchecked empty
field is indistinguishable from work left undone — the next person filling in
presentation fields would reasonably invent one. None of the three pulse-*
types here declares a vendor, and none has one.
You MUST NOT invent a vendor for hardware that has none. Enforced by nothing: this is convention, held by whoever reviews the manifest.
3.7 args — optional
Arguments passed before any the runtime adds [rvr_co5qwlsz1ecp], used verbatim at spawn [rvr_32yorab19lex].
This is how one binary serves several types. ranvier-pulse ships one program
and three manifests, distinguished only by args:
# crates/runtime/session/nodes/pulse-relay/ranvier-node.yaml
command: ranvier-pulse
args: ["--relay"]
A package shipping several types off one binary SHOULD assert in its own tests that each manifest starts that binary on its own role. A copied-and-edited manifest that forgot to change its arguments would run the same role twice, and nothing in the core would notice.
Absent: an empty list.
3.8 probe, probe-args, probing — optional
See §8. Three refusals live here, all parser-enforced:
probe-argswith noprobe→Problem::ArgumentsForNoProbe[rvr_tnhqqxp3jljn].probingwith noprobe→Problem::ProbingForNoProbe[rvr_2ok0m3kurwnu].probing.while-running: falsewith an empty or whitespacebecause→Problem::NoReasonNotToProbe[rvr_8qa0rbic249l].
3.9 location — optional
See §7. One refusal, parser-enforced: a location.param naming a setting
the type does not declare → Problem::NoSuchSetting, with the misspelling and
the real setting names in the message [rvr_hfd4vrt69igp min 2].
3.10 params — optional
See §6. Absent and empty mean the same thing, and it is a strong claim: a type declaring no settings refuses every setting a document writes on it [rvr_y5if6q8mhdxd min 2].
That is different from a type nobody declared at all, which is reported as an unknown type instead [rvr_pc5h50ws1pqv].
4. The type identifier
4.1 What the parser requires
Nothing beyond being a string. Registry::add never inspects it except to key
the map and detect duplicates [rvr_2oserzufd1at]. An empty type: loads.
The identifier is compared as an exact string when a graph resolves a node [rvr_kcxaeavund4n]. Case, punctuation and length are unspecified.
4.2 What the convention requires
The rule is vendor-and-model where there is a vendor:
<vendor>-<model>for a particular product from a particular maker.devices-for what every computer already has:devices-mouse,devices-keyboard,devices-microphone,devices-screen.
The reasoning is that a type name is a lookup key rather than a description — its only requirements are uniqueness and permanence — and that vendor, model and serial are metadata carried in their own fields, which is where both LSL and BIDS put them. A model name alone is the shape that breaks: a rig with a second suit from a different maker has nowhere to put the distinction.
This is convention, not parser-enforced. A package holds it down for its own types or not at all:
assert_eq!(manifest().node_type, "sony-mocopi");
You SHOULD follow it. Nothing stops you doing otherwise, and nothing will tell you that you did.
4.3 The type your binary registers under
Your program sends a node_type in its Register [rvr_trrl9j7139id min 2]. It
MUST be the same string its manifest declares.
The runtime does not check this. plane::accept compares protocol, token,
node identity and ports, and never compares node_type [rvr_53kcf0lcl9qp]; the
only use of the name is in the refusal message, where it is taken from the
manifest side [rvr_wyo0j24flv1e]. A node that registers under a name its
manifest does not declare is therefore accepted by a real plane::Listener
expecting the declared name, and no test in this repository goes red.
So this is test-asserted only, and each package asserts it for itself. A node whose package carries no such test has nothing checking it at all. The failure it produces is a session that works and a refusal message naming a type nobody can find.
A binary that can publish more than one type's streams and is not spawnable as any of them SHOULD register under a name no manifest declares, and a test SHOULD assert that no manifest declares it.
5. Ports
A port is what your node's own code calls one of its connections. It is not a stream name: the port is fixed by the node type, and what it is bound to varies per session [rvr_6yb17u92ts8f].
5.1 Declaration
ports:
- name: reading
direction: out
schema: ranvier.example.v1.Reading
optional: true
| Key | Type | Required | Meaning |
|---|---|---|---|
name | string | yes | the port's local name [rvr_8jm367d31fmo] |
direction | in | out | yes | in consumes, out produces [rvr_ahhs7ikdxs2z min 2] |
schema | string | yes | the fully-qualified protobuf message name [rvr_srzj2610zbe5] |
optional | boolean | no, default false | whether the node runs without it bound [rvr_aojja28t4b3e] |
direction is spelled in and out in the file, not with Rust's names
[rvr_ldaxu0ihrezs]. Any other word is Malformed.
schema is required as a key and MAY be the empty string. It is not
validated: no check that it is a well-formed protobuf name, and no check that
any such message exists. The requirement that it be a fully-qualified protobuf
name is convention [rvr_bmq8ukablnfx].
A manifest names a schema and MUST NOT carry its descriptor. There is no field for one [rvr_5l257v39ptd5]. The descriptor travels from the node itself when a stream is announced; what the registry needs identity for is comparing two ends of an edge, which is a string comparison.
5.2 Required and optional
optional: true in the manifest becomes required: false on the port the
runtime builds [rvr_921fwj4ctozx].
A graph MUST bind every port that is not optional. A required port nothing
binds is refused before anything is spawned, as WhyPort::Unbound
[rvr_zxowhorlb2f0]. This is runtime-enforced at session start.
Declare a port optional when the node genuinely runs without it. Two cases that recur: a sensor that only some revisions of a device carry — a graph binding it on a device without one should record nothing there rather than refuse to start — and a stream whose capture is a decision a study makes rather than a default it inherits, such as video of a participant's face.
5.3 What the graph checks
Graph::check_ports runs before anything is spawned [rvr_lstuyc0wp9i5], given
the ports the registry read. It refuses:
| Problem | When |
|---|---|
UnknownType | no manifest declares the node's type [rvr_1cja4khvrcw7] |
NoSuchPort | publish: names a port that is not an output, or subscribe: names one that is not an input [rvr_iaw4k3n1pwfz] |
Unbound | a required port is bound by nothing [rvr_y1wutpu3m0hc] |
SchemaMismatch | a subscriber's declared schema differs from the publisher's for the same stream name [rvr_q6v6ma3zzjpw] |
Schema comparison uses the first publisher of a stream name [rvr_04hpldptszdo]. Several nodes publishing one name is valid and is not a mistake [rvr_ar2vmoqjb1ig].
An input naming no schema accepts any stream. That exemption is narrow and deliberate: naming a schema is how a port says what it will interpret, and a port naming none is claiming it interprets nothing [rvr_72huxhsdhv6i]. The recorder is the only case so far — it writes bytes it never decodes, and writes the schema the publisher announced beside them:
- name: input
direction: in
schema: ""
optional: true
An output port MUST name a schema. No exemption exists for one, and none is
checked at load. The consequence appears one step later and only conditionally:
an empty output schema is what gets recorded as the stream's identity
[rvr_fum0htrinhdk], so any subscriber that does name a schema is refused with
SchemaMismatch reporting an empty found. A graph whose subscribers also name
nothing is accepted.
5.4 What your binary must advertise
Your program sends its ports in Register [rvr_g7f7h03x3ujh min 2], carrying
name, direction, schema_identity and required.
The ports your binary advertises MUST match the ports its manifest declares.
This is runtime-enforced: plane::accept compares the two and refuses the
registration when they disagree, with one line per disagreement
[rvr_hst922rautwf min 2].
What is compared, in both directions:
- a port the manifest declares and the binary does not advertise [rvr_b7pwjlarknkc]
- a port the binary advertises and the manifest does not declare [rvr_zhcg4jn1g1t9]
- a direction that differs [rvr_2xdy17sylmmp]
- a schema identity that differs, unless either side is empty [rvr_q7xrft093is9]
What is not compared:
required. A binary whose ports are required while its manifest calls them optional is two documents disagreeing about whether a device missing one sensor can run this graph, and nothing in the core reports it. A package SHOULD assert the agreement in its own tests.node_type. See §4.3.- the descriptor. The runtime compares identities and never opens a descriptor [rvr_240ceesno4kk].
Advertise every port the type has, every time, not the subset this run was asked to serve. A binary told at the command line to serve two of its five sensors still advertises all five, because the manifest declares five and the comparison is by set.
5.5 What nothing checks
Nothing verifies that a declared port is ever published on. The manifest, the graph and the registration can all agree, and the binary can still never write a sample. That produces a stream the graph binds, the recorder declares, and nothing ever fills — a session that checks clean and then does nothing.
You MUST publish on every output port you declare and required, and you MUST read every required input. Enforcement is absent from the core entirely. A package SHOULD carry a test comparing its manifest against the constants its binary publishes under, because there is nothing else.
A port a binary cannot yet fill MUST NOT be declared, and its absence
SHOULD be asserted, because a manifest is exactly where that lie would be
told first: ranvier-run check would pass a graph binding a video port on a
machine where no picture will ever arrive. By the same rule, a binary that
refuses to start MUST NOT ship a manifest at all.
5.6 Port names
Any string. Shipped names include audio, motion, gaze.events, video.scene
and input-16, so dots and hyphens are in use. There is no syntax rule and no
length limit — unspecified.
A port name MUST NOT be reused within one node in the same direction. A
graph binding one name in both publish: and subscribe: is refused as
Why::DuplicatePort [rvr_vavo83m1dmxx].
6. Parameters
A setting is what a graph document writes under params: on a node. Declaring
them is what turns a misspelling into a refusal instead of a node running on its
defaults while the document — which is the record of the method — says otherwise
[rvr_c6ahgklhsmjo].
6.1 Declaration
params:
- name: scope
kind: text
required: false
one-of: [own-window, window, everything]
label: Whose typing is recorded
default: >-
only while this node's own console window is in front
help: >-
Set it to `window` and name the stimulus program in `window`, and
everything typed into any other application is discarded before it
becomes a sample.
| Key | Type | Required | Meaning |
|---|---|---|---|
name | string | yes | what the document writes left of the colon [rvr_prm6px47ot7f] |
kind | text | whole | real | switch | yes | which scalar it takes [rvr_afyane96e52r min 2] |
required | boolean | no, default false | whether the node refuses to run without it [rvr_pbm3n304uyfw] |
one-of | list of strings | no, default empty | the permitted values [rvr_p3idujrwr040] |
label | string | no | what to call it for a person [rvr_nlc33yzifcv4] |
default | string | no | prose about what happens when it is left alone [rvr_pdy12ghlvb7c] |
help | string | no | why somebody would change it, and what goes wrong [rvr_98lrtfux5vrq] |
ParamSpec also carries deny_unknown_fields [rvr_13fhe2lhd5uz].
default is prose, not a value. "every sensor the device offers", "the port
the operating system chooses". A value here would be a second copy of the node's
own fallback, and nothing keeps the two in step [rvr_12bl4vzbxx1u]. A package
SHOULD assert this shape for its own settings.
6.2 What check_params refuses
Graph::check_params runs before anything is spawned, after check_ports so an
unknown type is reported once [rvr_flw76hvlboor]. It is runtime-enforced and
reports every problem, in document order [rvr_o2yfwi5taqtj]:
https://github.com/extendedresearch/ranvier/blob/main/spec/| Problem | When | Line |
|---|---|---|
| Unknown | the document sets a name the type does not declare; the message lists what it does declare | [rvr_nh6xjibh4vwi] |
| WrongKind | the value is the wrong scalar | [rvr_pwnir45pl7q9] |
| NotOneOf | one-of is non-empty and the value is not in it | [rvr_8eyafs09nn4s] |
| Missing | a required: true setting was not given | [rvr_hitkbriw7gbt] |
Kind matching is not quite equality: a whole number is accepted where real
is declared, because YAML writes 1 for a duration a reader means as 1.0.
The reverse is refused — a real number where a count belongs is a rounding
decision nobody wrote down [rvr_ktgyfdzo72gp min 2].
one-of comparison is against the value's Display form [rvr_sogotb1oz1g5], so
one-of on a switch is matched against true/false and on a number against
its decimal rendering.
6.3 How settings reach your node
Settings arrive in the environment variable RANVIER_PARAMS, as a JSON object
mapping name to scalar ([rvr_v46ecgu227zm min 2], set at [rvr_t2scjcbcxmhi]).
Only settings the document actually wrote are present. Nothing fills in defaults — there are no default values to fill in, only prose. So your node MUST carry its own fallback for every optional setting it declares. The Rust helper reflects this: every accessor takes a fallback and none of them can fail [rvr_km074f5tlo8t min 2].
By the time RANVIER_PARAMS reaches you, the document has already been checked
against your manifest, so a name you did not declare cannot have arrived and a
value of the wrong kind cannot have arrived [rvr_k6lswwwecw8t].
A node run by hand has no RANVIER_PARAMS and runs on its own defaults; that is
not an error [rvr_vpnz13akz7i2].
6.4 Four settings the core reads
Settings::attributed reads four optional settings and attaches them to a
stream's announcement [rvr_cr21x14ryl9p]:
https://github.com/extendedresearch/ranvier/blob/main/spec/| Name | Kind | Means |
|---|---|---|
| operator | text | who the graph says is capturing |
| authorisation | text | the protocol or approval this is captured under |
| authorisation_from | whole | when that authority began, unix nanoseconds |
| authorisation_until | whole | when it lapses |
A node that carries identity onto its streams SHOULD declare these four with
exactly these names and kinds, and call the equivalent of attributed. The
operator is
recorded as configured — the weak claim, and the honest one, because a name
written into a graph document was not asked for at the moment the session ran.
An unset window is zero, meaning unstated rather than "began at the epoch"
[rvr_un3xddfoj2oe min 2].
This is convention plus a per-package test. Nothing in the registry requires
these names, and a node declaring operator as a whole would load.
6.5 What nothing checks
Nothing verifies that a declared setting is read by the binary, or that a
setting the binary reads is declared. The second direction is at least loud —
an undeclared setting is one no document can use, because check_params refuses
it. The first is silent: a setting an interface renders, a researcher fills in,
and the node never reads.
List both halves by hand in your package's own tests. There is nothing else.
7. Location
location says which setting carries the instrument's whereabouts, and what
kind of where it is [rvr_zqwbf6hz911d]. What to call that setting and what
to explain about it stay on the setting itself, so each fact has one home.
location:
param: device # MUST name a setting this type declares
kind: address # address | enumerated | port
example: 192.168.1.49
travels: false
| Key | Type | Required | Meaning |
|---|---|---|---|
param | string | yes | the setting that carries it [rvr_kkdl9j97f7i7] |
kind | address | enumerated | port | yes | what kind of place [rvr_lohb92uixre5 min 2] |
example | string | no | a value to show greyed out in an empty field [rvr_kujkmcwguv2t] |
travels | boolean | no, default false | whether a value here still works on another machine [rvr_r2auvbxhts6d] |
7.1 When to declare one
Declare a location when there is something to locate. Omit it when there is
not — a processor has no where, and a keyboard is wherever the computer is
[rvr_32fferp96hvk].
The absence is a decision worth asserting in your own tests, because nothing distinguishes a deliberate omission from an oversight. Assert it in capitals if that is what it takes to stop the next person filling one in.
None of the three manifests here declares a location: the pulse-* examples have
nothing to locate.
grep -l '^location:' $(find crates -name ranvier-node.yaml \
-not -path '*/target/*' -not -path '*/dist/*') | wc -l # 0
7.2 param — parser-enforced
param MUST name a setting the same manifest declares in params. A
location naming a setting that does not exist is Problem::NoSuchSetting, and
the message carries the misspelling and the real setting names
[rvr_k2ff3og04gfi min 2].
The failure this closes is silent: an interface renders a field for a setting nothing reads, a researcher types an address into it, and the instrument is plugged in, configured, and mute [rvr_obvklmsk863c].
param naming a setting the binary never reads is not checked, and that is
the same failure with the refusal removed. Assert in your own tests that the
setting is not merely declared but is the one your binary binds.
7.3 kind
The list is short on purpose: an entry earns its place by making an interface behave differently, not by describing a transport [rvr_vsfr6zcfe1l1].
kind | The interface draws | Shipped by |
|---|---|---|
address | a text field | pupillabs-neon |
enumerated | a list, filled from the probe's Found::id | devices-microphone, devices-screen, interaxon-muse |
port | a number | sony-mocopi |
kind: enumerated SHOULD be paired with a probe, since the list an
interface draws comes from the probe's answers [rvr_m3u6wp2a4kmh]. Nothing
enforces the pairing.
7.4 example — not a default
example is what to show greyed out, not a value to fill in. An address has
no sensible default: the empty value means look, and a filled-in value would
be somebody else's phone [rvr_2yvqfmuriq91].
example MAY be omitted where the probe supplies the values, so there is no
empty field to show one in. A type whose location is typed rather than chosen
SHOULD carry one, and SHOULD assert that it does.
A numeric example MUST be quoted. See §2.4.
7.5 travels
travels: false says a value pinned here does not work on another machine. A
WASAPI endpoint string or a Bluetooth address names a device to one computer,
so a graph carrying one is not the portable artifact a graph beside a manuscript
is supposed to be [rvr_0znfzgnxg6vx].
travels: true says the opposite, and there is exactly one:
sony-mocopi binds a UDP port, and 12351 binds on the next laptop exactly as it
binds here. What does not travel in that case is the phone's own configuration,
and that is not in the document to travel
(docs/decisions/0035-a-manifest-says-where-and-the-setting-says-what-it-is-called.md:145-155).
The default is false [rvr_qig0zedj7vof]. Nothing checks that the value is
truthful.
8. The probe
A probe answers one question — is one of this type's instruments here — in one
shape (docs/decisions/0033-a-probe-answers-one-question-in-one-shape.md).
8.1 When to declare one
A manifest MAY name a probe. Absent is ordinary — a processor has no
hardware to look for, and a type declaring no probe is answered with
NotAsked::NoProbe rather than treated as a fault [rvr_uee01cuexfjp min 2].
Declare a probe only if the program answers and exits. A package may ship a
program it calls a probe that is really a live monitor — it listens and prints
every frame until Ctrl-C. Decision 0033's contract is a program that answers and
exits, and Registry::probe waits for that exit, so naming such a program would
hang the runtime rather than tell it anything. Declare no probe in that case, and
assert the absence: an absence nobody asserted is an absence somebody fills in.
Better still, run the program you ship through the runtime's reader, so the
reason is demonstrated rather than merely stated.
None of the three manifests here names a probe: the pulse-* examples have no
instrument to look for.
grep -l '^probe:' $(find crates -name ranvier-node.yaml \
-not -path '*/target/*' -not -path '*/dist/*') | wc -l # 0
8.2 How the runtime runs it
Resolved beside the manifest first, with the platform's executable extension tried, then as a bare name [rvr_aes6kngw9fsw min 2].
Invoked as <probe> <probe-args…> --json [rvr_8l28qjrcfbre]. probe-args
come before --json, because they select the question. One binary can answer
more than one. An audio probe that enumerates capture devices by default and
playback devices given --desktop-audio serves a microphone type and a screen
type from one binary, each naming it with its own probe-args
[rvr_m9b57n94fx5s].
Standard output is captured; standard error is discarded [rvr_w1zv4icoui2g]. Every probe that scans writes progress to standard error, so mixing them would make the object unparseable exactly on the devices that take longest to find.
Settings MAY be passed: Registry::probe_told sets RANVIER_PARAMS to the
same JSON the node would receive, so a probe and its node read one set of
settings with one piece of code [rvr_h64gzojx19bh min 2]. Registry::probe
passes nothing [rvr_hfm9h0v19q1g]. A probe that reads nothing is unaffected.
8.3 The output contract
A probe MUST print exactly one JSON object on standard output, and it
MUST exit (docs/decisions/0033-a-probe-answers-one-question-in-one-shape.md:88-96).
{
"found": [
{
"id": "NEON-0042",
"label": "Neon (Companion 3a4b)",
"serial": "NEON-0042",
"firmware": "2.8.3",
"address": "192.168.1.24",
"detail": {}
}
]
}
| Field | Means | Line |
|---|---|---|
id | what a reconnect matches on — a serial where the instrument has one | [rvr_gvkll5off7zv] |
label | what a person reads in a list | [rvr_okgjfgkw81l1] |
serial | as the device reports it | [rvr_i352t1xho4r2] |
firmware | as the device reports it | [rvr_4pby8al3v89c] |
address | where to reach it, when that is how it is reached | [rvr_160z03a5yzs0] |
detail | the vendor's own findings, carried and never interpreted | [rvr_e05wymcl6ixh] |
Every field MAY be empty, and empty means the device did not say. A Bluetooth headband advertises a name and no serial; refusing a device for not reporting a firmware would refuse most devices [rvr_lgvbtbhr8vlh]. Every field defaults, so a probe MAY omit any of them.
detail is carried to whatever displays a device and is never read by the core
([rvr_5ph4ug90ctys], asserted at [rvr_2o5sn5pkdwy1]). Put in it what only your
package understands.
When you could not look, print {"error": "…"} instead. An empty found
means the probe looked and saw nothing, which is a real and different answer:
no adapter and no headband in range are different problems with different
fixes [rvr_3qo565lx4w8k].
An object carrying both is read as the error ([rvr_rbjv97d3p7yn], asserted at [rvr_dh5clxwn81yr]). Reading the list from a probe that could not look would report "saw nothing" for a missing adapter.
A probe that was told nothing it needed SHOULD refuse rather than report an
empty network: told no address, return error naming the missing setting rather
than found: [].
8.4 Timing, and what the runtime does when a probe misbehaves
The runtime gives a probe 30 seconds (probe::PATIENCE, [rvr_b77ytl6mpb99])
— five times the longest scan any shipped probe performs, a six-second Bluetooth
sweep.
Reading happens on another thread so the wait can be bounded, and so a probe
printing more than the pipe holds does not block in write and never reach its
own exit [rvr_3clnve6lqomz].
| The probe | The runtime answers |
|---|---|
| will not start | NotAsked::Unrunnable [rvr_tvvjy9trvsug] |
| has not answered at 30 s | killed, then NotAsked::NeverAnswered [rvr_x2chsl5sutne min 2] |
| printed something that is not this envelope | NotAsked::Unreadable, quoting the first 400 characters [rvr_wkdr9nn42v50 min 3] |
printed neither found nor error | NotAsked::Unreadable, no \found` and no `error`` [rvr_ivg3u0xg2ohs] |
printed error | Answer::Refused — not an error here; that is the probe answering [rvr_32d88nu4lq2j min 2] |
A probe that overruns is killed rather than left, because one holding a Bluetooth adapter or a bound port would stop the node that needs it from starting [rvr_b0uvz70chyfj].
probe::ask_within and ask_told_within are public so a package can test that
its own probe answers and exits under a short patience, which 0033 requires and
nothing else checks [rvr_a34l6tqnqbmw]. Use them, and state in the test what your
probe tolerates and why.
8.5 probing — when a probe may be run again
A manifest that declares a probe MAY say whether running it while its node is running is safe and useful [rvr_4dp4lhu8n1ol].
probing:
while-running: false
because: >-
Finding a headband means a Bluetooth scan. A connected peripheral stops
advertising, so a scan during a session reports the headband as gone, and
scanning costs airtime on the radio already carrying the EEG.
| Key | Type | Required | Meaning |
|---|---|---|---|
while-running | boolean | yes | whether asking again during a session is safe and useful [rvr_3kibal78g97s] |
because | string | when while-running is false | why not, in a sentence somebody reads before flipping the flag [rvr_5qt3acuvio0i] |
Three rules, all parser-enforced:
probingwithoutprobeis refused [rvr_qkh25u44weri].while-running: falsewith an empty or whitespacebecauseis refused [rvr_lg85406i1h8t]. A flag switched off with no reason beside it is a flag the next author flips while chasing a stale reading.becausebesidetrueis optional and permitted.
Absent means a running node is not re-probed. That is the safe direction rather than the obvious one: a stale battery percentage is recovered by filling in this field, and a Bluetooth scan across a live electroencephalography link is not recovered at all [rvr_7amoxbgmnhmz].
No shipped manifest declares probing:
grep -l '^probing:' $(find crates -name ranvier-node.yaml -not -path '*/target/*') | wc -l # 0
A package MAY lie here and nothing catches it: while-running: true on a
Bluetooth type is obeyed
(docs/decisions/0039-probing-is-not-free-and-the-manifest-says-when-it-is-not.md:212-215).
There is no per-type probe interval. while-running is a flag, not a
cadence, and how often a supervisor asks a type that permits asking is not
something a manifest can state — unspecified, and named as the field a second
slow probe would want (docs/decisions/0039-probing-is-not-free-and-the-manifest-says-when-it-is-not.md:186-194).
9. Lifecycle
Unproven. Every requirement in this section is exercised by Rust nodes only. A second implementation has not run against it.
The protocol itself is control.md. This section is what your
program owes it.
9.1 Spawn
The runtime resolves your command beside the manifest, spawns it with your
args, and sets four environment variables [rvr_j432aqn81ahm]:
https://github.com/extendedresearch/ranvier/blob/main/spec/| Variable | Carries | Line |
|---|---|---|
| RANVIER_CONTROL | host:port of the runtime's loopback control listener | [rvr_bcfhlbrqvl96] |
| RANVIER_TOKEN | the token you present when you register | [rvr_cpe3gpn10p14] |
| RANVIER_NODE_ID | this node's identity in the graph | [rvr_u50lsph8et4z] |
| RANVIER_PARAMS | the document's settings, as JSON | [rvr_snwsnmy0ym5a] |
The token is a separate variable so a diagnostic can print where a node is dialling without printing the secret [rvr_xle7ide55cx0].
Standard input is a pipe; standard output and standard error stay inherited, because a node prints where it is listening and an operator watching a session start is reading exactly those lines [rvr_70iq2sgc7ea9 min 2].
Bindings arrive on the connection, and nothing is passed in the environment, which is what lets a subscriber be told an address nobody knew when the process started [rvr_teg2q0en3k1u].
9.2 Join
RANVIER_CONTROL decides which channel you are on, and the two are mutually
exclusive by construction [rvr_x11hgiqdez05 min 2]:
https://github.com/extendedresearch/ranvier/blob/main/spec/| | Channel |
|---|---|
| RANVIER_CONTROL set | the connection. Bind, Transport and Stop arrive on it |
| RANVIER_CONTROL unset | standard input. The node is being run by hand |
A node with no RANVIER_CONTROL MUST still run. Being started by hand is a
supported way to run one [rvr_pz9ep8h20eu8 min 2]. It runs on its own direct
declarations and its own defaults.
On the standard-input channel your node MUST read lines and treat stop as
the ask to stop. End of input is not a stop — a node started with no
terminal would otherwise exit the instant it began [rvr_8iu2x8cixzya min 2]. An
empty line is nothing; an unrecognised word is reported on standard error rather
than acted on.
9.3 Register
You dial, and you speak first: one listener serves the whole graph, so an accepted socket is anonymous until you name yourself [rvr_hagziasidnkp].
Register carries protocol, token, node identity, node type, package version,
process identifier, whether you take transport actions, the stop deadline you
ask for, and your ports [rvr_sspp38tnizrs].
Register before you touch your instrument. Nothing in a registration needs hardware — the ports come from your own code and the type from your manifest — which is what makes the runtime's join timeout a bound on process start rather than on device discovery. This is a rule rather than an observation [rvr_nsniq0kq4afi].
The runtime refuses a registration for five reasons, in order [rvr_kzmi15axnjx1]:
https://github.com/extendedresearch/ranvier/blob/main/spec/| Reason | When |
|---|---|
| Protocol | your protocol is not the runtime's [rvr_qdv3peiw29sw min 2] |
| Token | your token is not this session's [rvr_sgiblvw3juea] |
| UnknownNode | no node of that identity is part of this session [rvr_bnwoqzp4b0hu] |
| AlreadyRegistered | that identity is live on another connection [rvr_rjq88g04gqzy] |
| Ports | your ports and your manifest disagree (§5.4) [rvr_ht4frxnxadhk] |
A node that is refused SHOULD report the refusal and exit non-zero. There is nothing for it to do in a session it is not part of [rvr_hk5y3rqiiide].
A node that cannot reach the runtime is a different case and MUST NOT
treat it as fatal: it retries in the background, holds no bindings, publishes
nothing, and opens no instrument [rvr_kjoepvhd8gt9]. The Rust helper waits up to
JOIN_PATIENCE — five seconds — for a first answer before returning
[rvr_061f1au2mbk3 min 2].
package_version is sent and the runtime reads it nowhere:
grep -n 'package_version' crates/runtime/session/src/plane.rs # no matches
9.4 Run
Bindings arrive in Bind, and Bind replaces rather than merges
[rvr_s2ibek8yikbk min 2]. A stream the runtime stops naming is one you MUST
disconnect; holding a connection the runtime has forgotten is the stale binding
the replace rule exists to make impossible.
A subscribe binding whose from is empty is not an error. It is a port
bound to a stream nobody has published yet: the port comes up subscribed and not
connected, and a later Bind carries the address [rvr_w3pppb4iwrnw]. You
MUST be able to take an address that arrives after you started, which is
what watching the binding generation is for [rvr_9z4ghdn4vzgx].
Publish under the name the Bind gave you, not under your own constant.
That is what lets a document rename a stream, and it is how two microphones in
one room become two streams a person can tell apart. Where a graph named no
stream for one of your ports, fall back to your own default.
Once your publishers are up, say so: Listening { address, published } is what
tells the runtime where you landed, and it is what a subscriber elsewhere in the
graph is waiting for [rvr_s882zlm5vemq]. Resend it on every redial, so a runtime
that came back learns where you are without asking [rvr_z01fsmwdzb7b].
Keep beating. Alive goes on the period the runtime asked for in Bind,
defaulting to two seconds, and three consecutive misses mean gone
[rvr_ulnvjvdsyckb min 2]. The heartbeat MUST NOT queue behind your data
path: a beat that waited on a 1 kHz publisher's delivery loop would report the
node unhealthy at the moment it was working hardest [rvr_tmv54lf0x8u3].
If the connection drops, keep publishing and redial. A node whose runtime went away keeps every binding, keeps publishing, and reconnects on the heartbeat period without backoff [rvr_m54wohkccnmh min 2].
Transport { PAUSE | RESUME } is delivered only if you declared
accepts_transport. A node that did not declare it says so on standard error
and acts on nothing [rvr_1ssjyijaoo7m]. The runtime does not gate on the flag —
grep -n accepts_transport crates/runtime/session/src/plane.rs returns nothing —
so refusing it is your side's job.
What a capture node should do about a pause is not decided. A recorder pauses by not writing, which is well defined; whether a capture node drops the samples, buffers them, or leaves a gap the recording can show is unspecified, and inventing an answer would put it in five packages at once [rvr_716qdvbf2hia].
9.5 Stop
Stop { deadline_ms } arrives on the connection; on standard input the ask is
the line stop, which carries no deadline [rvr_9m50vvmcqqmb min 2].
You MUST exit within the deadline. The default is five seconds
[rvr_3fx9trhv68yi], and a request for longer is capped at 30 seconds —
probe::PATIENCE reused, so there is one answer to how long will the runtime
wait for a child rather than two that can drift [rvr_tj3beixwka18].
Say Stopping before the process exits. That is the whole distinction a
session report exists to draw — a node that answered was stopped, and a node
that did not was killed [rvr_jf7te3qe9mnq].
Everything is asked before anything is waited for, so the session's patience is the longest node's rather than the sum of every node's, and a node that ignores the ask does not delay one that took it [rvr_xsf9immiyc95].
What the runtime reports [rvr_7ht0do22q794]:
https://github.com/extendedresearch/ranvier/blob/main/spec/| Outcome | Means |
|---|---|
| Stopped | asked, and gone inside the deadline. A recorder that ended this way sealed its file |
| Ignored | asked, still running at the deadline, killed |
| Unreachable | killed without ever having been asked, because no channel reached it |
Unreachable claims nothing about your shutdown behaviour: you had no Stop to
answer [rvr_epo3ew6jjm56].
Being stopped is not the only ending. A node MAY end for its own reasons — a capture thread that died, an input whose publisher went away — and SHOULD route that through the same path as an ask so one code path leaves the program rather than two that drift [rvr_9ajh6e866uw8].
What happens next is the graph's decision, not yours: on-exit is per node and
defaults to restart [rvr_eu207ygmrrb9 min 2]. A restart re-runs the original
instruction rather than reconstructing one [rvr_kuu5klbpglbl].
9.6 Placement
A manifest says nothing about where a node runs. Placement is the graph's, in
on: [rvr_qjsvqvb6ylwn], and decision
0017 makes it
part of the document.
A node placed anywhere other than the runtime reading the document is refused
today, as WhyNot::Elsewhere [rvr_2ov11fma2bhp min 2]. Reaching another
runtime needs authentication and cross-host clock synchronisation, and neither
is built. You have nothing to declare about this.
10. Checklist
Work through this to be usable in a graph.
The manifest
- Named
ranvier-node.yaml, at a search root or one directory below it (§2.1) -
type,commandandportspresent (§3) -
typefollows §3.1 naming — vendor-and-model, ordevices-(§4.2) -
commandnames the program without a platform extension (§3.2) - No field this document does not list (§2.3)
-
summary,defaultandhelpwritten as folded block scalars>-(§2.4) -
labelandsummaryset, unless the identifier reads as a name (§3.4, §3.5) -
vendorset if and only if there is a maker (§3.6)
Ports
- Every port your binary publishes on or reads from is declared (§5.5)
- No port declared that your binary cannot fill (§5.5)
- Every output names a schema; an input names one unless it interprets nothing (§5.3)
-
optional: trueon every port the node genuinely runs without (§5.2) - Your
Registeradvertises exactly this set, every run (§5.4)
Parameters
- Every setting your binary reads is declared (§6.5)
- No setting declared that your binary never reads (§6.5)
- Each has
label,defaultandhelp(§6.1) -
defaultis prose about what happens, not a copy of the value (§6.1) -
one-ofon every setting that selects behaviour (§6.2) -
operatorand the three authorisation settings declared, if your streams carry identity (§6.4) - Your node supplies its own fallback for every optional setting (§6.3)
Location
-
locationpresent if there is something to locate, absent if not (§7.1) -
paramnames a setting you declare and your binary reads (§7.2) -
kindmatches how a person picks the value (§7.3) -
examplequoted if numeric; omitted where a probe supplies values (§7.4) -
travelshonest (§7.5)
Probe
- Declared only if the program answers and exits (§8.1)
- Prints one object:
found, orerrorwhen it could not look (§8.3) -
probe-argsselect the question, and are declared where one binary answers several (§8.2) -
probing.becausefilled in whereverwhile-runningisfalse(§8.5)
Lifecycle
- Runs with
RANVIER_CONTROLunset (§9.2) - Registers before touching the instrument (§9.3)
- Registers under the type its manifest declares (§4.3)
- Publishes under the name
Bindgave, not its own constant (§9.4) - Reports
Listeningonce its publishers are up, and again on every redial (§9.4) - Beats on the period
Bindasked for, off the data path (§9.4) - Takes a
Bindthat arrives later and fills in an address (§9.4) - Keeps publishing and redials when the connection drops (§9.4)
- Says
Stopping, then exits inside the deadline (§9.5)
Tests, because the core checks none of the above's silent half
- Load your own manifests through
Registry::scan, not a YAML parser of your own - Compare declared ports against what your binary registers, in both directions
- List the settings your binary reads, by hand, and assert each is declared
- Run your own probe through
probe::ask_withinandprobe::read - Assert your deliberate absences — no vendor, no location, no probe
11. Not yet specified
These are holes, not permissions. Do not depend on what the Rust does here. Closing one of these is a change to this document.
| Hole | Why it is open |
|---|---|
| Type identifier syntax | Any string loads, including empty (§4.1). §3.1's convention is a naming rule, not a grammar. |
| Port name syntax | Any string. Dots, hyphens and digits are all in shipped names (§5.6). |
| Schema identifier validation | Nothing checks it is a well-formed protobuf name, or that the message exists (§5.1). |
| Manifest versioning | No version field, and deny_unknown_fields means a newer manifest does not load on an older runtime (§2.3). |
| Probe cadence | while-running is a flag, not an interval. How often a permitted probe is asked is the supervisor's business, and no manifest can state it (§8.5). |
| What a capture node does on pause | Undecided (§9.4). |
Whether command may be an absolute path or carry arguments | Resolution joins the manifest's directory with the string and otherwise passes it to the operating system (§3.2). No shipped manifest tries either. |
| Two manifests in one directory | Impossible by the filename rule, and nothing says what a package with several types in one directory should do instead (§2.1, §2.2). |
| Exit codes | A refused node SHOULD exit non-zero (§9.3). Nothing else specifies a code, and nothing reads one except to render it in a report. |
How a probe synthesises an id for an instrument with no serial | Each package invents its own today. |
No shipped manifest carries a probing block, so §8.5's rules are enforced by
the parser and exercised by nothing. The check below runs in continuous
integration and covers this repository's three manifests:
grep -l '^probing:' $(find crates -name ranvier-node.yaml -not -path '*/target/*') | wc -l
A manifest shipped elsewhere is covered by nothing here.
12. Worked examples
12.1 The smallest manifest that loads
type: example-nothing
command: example-nothing
ports: []
Three keys, because three are required (§3). This declares a type that takes no settings, offers no ports, and therefore cannot be wired into anything. It is here to show the floor, not as a thing to ship.
12.2 The smallest useful manifest
type: pulse-source
label: Pulse (source)
summary: Publishes a counter on a port the operating system chooses.
command: ranvier-pulse
args: ["--source"]
ports:
- name: beat
direction: out
schema: ranvier.example.v1.PointerSample
params:
- name: device
kind: text
- name: rest_ms
kind: whole
Shipped verbatim as crates/runtime/session/nodes/pulse-source/ranvier-node.yaml.
One output, two settings, no probe, no location — a node with nothing to find and
nowhere to be. Note that its settings carry no label, default or help: this
is a worked example inside the runtime rather than something a researcher
configures, and the presentation fields §3.4 and §6.1 ask for are exactly what it
omits.
12.3 A realistic device manifest, annotated
A worn EEG headband, with the help and default prose abridged where marked.
No such manifest ships here — this repository ships no device integration — so
read this for the shape rather than as a file to open.
The schema prefix canonical.v1. is a stand-in. A device integration names
the vocabulary its modality publishes; the only vocabulary this repository ships
is ranvier.example.v1, and it exists to exercise the wire rather than to
describe an instrument.
type: interaxon-muse # vendor-and-model, §3.1. Nothing enforces it;
# a test in the package asserts it.
label: Muse # the type identifier is not a name a person reads
vendor: Interaxon # there is a maker, so name it
summary: Scalp potentials, movement and pulse from a worn EEG headband.
location: # a headband has to be found before it is used
param: device # MUST be one of `params` below — parser-enforced
kind: enumerated # picked out of a scan, so an interface draws a list
example: AA:BB:CC:DD:EE:01 # shown greyed out; not a default
travels: false # the identifier is the host Bluetooth stack's,
# so a graph pinning one does not run elsewhere
command: muse # resolved beside this file first, then the path
probe: muse-probe # answers and exits, so it may be named
ports: # every port the binary registers, always all five
- name: eeg
direction: out
schema: canonical.v1.EegChunk
optional: true # a Muse 1 has no optical sensor; a graph binding
- name: acc # `ppg` on one should record nothing rather than
direction: out # refuse to start
schema: canonical.v1.ImuSample
optional: true
- name: gyro
direction: out
schema: canonical.v1.ImuSample
optional: true
- name: ppg
direction: out
schema: canonical.v1.PpgChunk
optional: true
- name: device
direction: out
schema: canonical.v1.DeviceState
optional: true
params:
- name: device # the setting `location.param` names
kind: text
label: Headband
default: >-
the first headband the scan turns up, which is right for a rig with one
and wrong for a rig with two
help: >-
Pick the headband out of a scan. … The identifier is the host Bluetooth
stack's — a MAC address on Windows and Linux, an opaque per-machine
identifier on macOS — so it is not a serial number and it does not travel.
- name: sensors
kind: text
label: Sensors to publish
default: >-
eeg, acc, gyro and device — everything except the optical sensor
help: >-
Comma-separated, in the words the ports are called … A name this does not
know is refused at startup rather than skipped.
- name: port
kind: whole
label: Publisher port
default: >-
the port the operating system chooses, which the runtime reads off this
node's first line of output
help: >-
Fix it when something outside this graph has to dial in …
- name: operator # §6.4 — read by `Settings::attributed`
kind: text
label: Operator
default: >-
nobody recorded, rather than an identity nothing backs
help: >-
Who is capturing … recorded as `configured`, which is the weak claim.
- name: authorisation
kind: text
label: Authorisation
default: >-
no authority recorded on the streams
help: >-
The protocol number or approval this capture is made under …
- name: authorisation_from
kind: whole
label: Authorisation valid from
default: >-
unstated, which is a complete answer …
help: >-
When the authority above began, in unix nanoseconds …
- name: authorisation_until
kind: whole
label: Authorisation valid until
default: >-
unstated, the same as its start …
help: >-
When the authority above lapses, in unix nanoseconds …
Three things this manifest does not do, each deliberate:
- No
probingblock. 0039 §5 says this type should carrywhile-running: falsewith the Bluetooth reason. It does not yet, and until it does the supervisor does not re-probe it — which is the safe direction (§8.5). - No
one-ofonsensors. The value is a comma-separated list rather than a choice, andone-ofmatches whole values (§6.2). The binary refuses an unknown name at startup instead. - No required setting. Every one is optional, and the binary carries the fallback (§6.3).