CMLabs · Psyclone AIOS

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

ParameterWhereEffect
after / delaytriggerDelay delivery: <trigger ... after="5000" /> fires 5 s after the message arrives (after is an alias of delay).
intervaltriggerArtificial trigger at a regular interval: <trigger name="Post" type="Regular.Post" interval="1000" />.
topostDirect copy to a named component regardless of its subscriptions: <post type="Data.Web" to="WB1" />. The message is also still delivered to normal subscribers.
maxagetrigger / retrieveOnly wake the module if the message is younger than this — skip triggers when lagging behind. (Not a <post> attribute.)
Note. An 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 typeComparison
haskeyThe entry exists (key only, no value; any entry type — string, number or binary)
equalsString equality — case-sensitive, exact, no wildcards
notequalsString inequality
equalsnumericFloat equality, converting from string/int as needed (epsilon compare)
notequalsnumericNumeric inequality
greaterthanNumeric greater-than (values equal within epsilon also pass, i.e. effectively ≥)
lessthanNumeric less-than (equal values also pass, i.e. effectively ≤)
maxage (trigger attribute)Message created less than maxage milliseconds ago; stored internally as a filter
Warning. Unrecognised filter types are silently ignored — a typo in type= means the filter simply never rejects anything. Double-check spelling against the table above.

The delivery pipeline

messagemsg.1 type matchincl. wildcards context active?see ch. 6 filtersmaxage/from/to/tag crank rejected: discarded (no wake-up)
A message must pass the type match, the active-context check and every filter before the crank is invoked.

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 with tag="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" />
Tip. On triggers, posts and components, 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.

Note. 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.
Psyclone AIOS · CMLabs