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

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.)

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 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

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

A message must pass the type match, the active-context check and every filter before the crank is invoked.

## 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.
PreviousModules NextContexts