5. Messaging & Signals
Everything about moving data: message types and wildcards, signals, dataflow timing parameters, filters, and message tagging.
Message types and wildcards
Every message has a dot-notation type: ASCII parts of any length, grouped semantically with increasing specificity left to right — input.video.raw, input.audio.raw, input.audio.normalised. Types are applied to posts and used by triggers/subscriptions. Messages that match no subscription are discarded, unless the post sets a ttl (time-to-live) to keep them available in shared memory.
Trigger types match exactly or with wildcards:
<trigger name="AnyAudio" type="input.audio.*" />
<trigger name="AnyRaw" type="input.*.raw" />
Because crank code refers only to trigger/post names, rerouting is spec-only: change a trigger's type from input.audio.raw to input.audio.smoothed and the module's code is untouched.
Signals
Signals are the fast lane: no subscription checking, no complicated routing. Think of a conductor's stick beating time — every subscriber to signal A triggers shortly after it is sent. Signals are independent of contexts and are typical in simulations: do the work for a time step, then wait for the next beat.
<module name="Stepper">
<signal name="Input" type="My.Signal.2" />
<crank name="signal" function="Signal" />
<signal name="Output" type="My.Signal.1" />
</module>
The built-in Signal crank makes a signal-based ping-pong analogue possible with no user code. Only name and type are parsed on <signal> — it is deliberately minimal.
Dataflow parameters
| Parameter | Where | Effect |
|---|---|---|
after / delay | trigger | Delay delivery: <trigger ... after="5000" /> fires 5 s after the message arrives (after is an alias of delay). |
interval | trigger | Artificial trigger at a regular interval: <trigger name="Post" type="Regular.Post" interval="1000" />. |
to | post | Direct copy to a named component regardless of its subscriptions: <post type="Data.Web" to="WB1" />. The message is also still delivered to normal subscribers. |
maxage | trigger / retrieve | Only wake the module if the message is younger than this — skip triggers when lagging behind. (Not a <post> attribute.) |
interval timer only starts after the module has first been triggered by a real message. For a whiteboard, to="WB1" on a post and a type subscription on the whiteboard are equivalent ways to store the message.Filters
Triggers can narrow what they accept with attributes:
<trigger name="input" type="msg.1" maxage="20" /> <!-- skip messages older than 20 ms -->
<trigger name="input" type="msg.1" from="MyOtherModule" /> <!-- only from that component -->
<trigger name="input" type="msg.1" to="SomeModule" /> <!-- only messages addressed with to= -->
<trigger name="input" type="msg.1" tag="SomeTag" /> <!-- only messages carrying the tag -->
Advanced filtering: <filter> children
Every filter must pass for the trigger to fire. A worked example of each of the 8 operations (the last, maxage, is written as a trigger attribute, not a <filter> child):
<trigger name="input" type="msg.1" maxage="5000"> <!-- maxage: message younger than 5000 ms -->
<filter type="haskey" key="SomeKey" /> <!-- entry exists (any type; no value=) -->
<filter type="equals" key="camera" value="front-left" /> <!-- string ==, exact, case-sensitive -->
<filter type="notequals" key="state" value="disabled" /> <!-- string != -->
<filter type="equalsnumeric" key="mode" value="3" /> <!-- float == -->
<filter type="notequalsnumeric" key="channel" value="0" /> <!-- float != -->
<filter type="greaterthan" key="temp" value="80" /> <!-- float > -->
<filter type="lessthan" key="pressure" value="2.5" /> <!-- float < -->
</trigger>
| Filter type | Comparison |
|---|---|
haskey | The entry exists (key only, no value; any entry type — string, number or binary) |
equals | String equality — case-sensitive, exact, no wildcards |
notequals | String inequality |
equalsnumeric | Float equality, converting from string/int as needed (epsilon compare) |
notequalsnumeric | Numeric inequality |
greaterthan | Numeric greater-than (values equal within epsilon also pass, i.e. effectively ≥) |
lessthan | Numeric less-than (equal values also pass, i.e. effectively ≤) |
maxage (trigger attribute) | Message created less than maxage milliseconds ago; stored internally as a filter |
type= means the filter simply never rejects anything. Double-check spelling against the table above.The delivery pipeline
Delivery order
A crank receives its messages in the order they were queued for it, so the messages one module posts arrive at each subscriber in the order it posted them — a burst of audio frames, a sequence of steps or a stream of numbered readings can be consumed as it comes. Fixed 2.2.x (before 7 Nov 2026 a fast burst could arrive slightly shuffled: about 1.7% of messages a few places out.)
- There is no order between different posters: two modules posting at the same moment interleave in either order.
- Anything that crosses an unreliable link first (UDP, a phone connection) can still arrive reordered: number such streams and order them in the consumer.
// a crank can rely on posted order from one source
int8 Sink(PsyAPI* api) {
int64 last = -1;
while (api->shouldContinue()) {
DataMessage* msg = api->waitForNewMessage(100);
if (!msg) continue;
int64 n = -1; msg->getInt("n", n);
if (n < last) api->logPrint(0, "out of order: %lld after %lld", (long long)n, (long long)last);
last = n;
}
return 0;
}
The engine test ./Psyclone test=psy_messageorder posts 20,000 numbered messages in bursts and fails on any that arrive out of order.
Tagging
Two complementary tagging mechanisms group messages of one topic or identity across differing types:
- String tags. Posts can carry
tag="mytag"; triggers can filter withtag="mytag". All downstream messages in the forward chain carry the tag, so a whole processing chain about one entity stays identifiable. - Propagating integer tags. A component can set a 32-bit unsigned integer tag on its output messages; the tag is automatically propagated to all consequent output messages of every receiving module — if A tags 10 and posts to B and C, everything B and C (and their descendants) produce carries 10.
<post name="Face" type="vision.face.found" tag="person42" />
<trigger name="ThisPerson" type="vision.*" tag="person42" />
tag="*" means "use my own name as the tag" — handy when instancing similar modules.Guaranteed vs non-guaranteed delivery
By default every posted message is guaranteed: the messaging layer keeps it until each matching subscriber has received it, even under load. Set guaranteed="no" on a <post> to mark it best-effort (internally MESSAGE_NON_GUARANTEED):
<post name="Frame" type="sensor.video.frame" guaranteed="no" />
Best-effort messages may be dropped rather than queued when a consumer is slow or a buffer is full — the sender is never blocked. Use it for high-rate, latest-value-wins streams (camera frames, telemetry ticks) where a stale frame is worthless and you would rather lose one than build a backlog. Keep the default (guaranteed) for commands, state changes and anything a downstream module must not miss.
guaranteed="no" is about buffering/backpressure behaviour, not about ordering or acknowledgement. Combine it with maxage on the consumer's trigger to also skip frames that arrived too late to be useful.