CMSDK 2.0.1
Cross-platform C++ base library and SDK for the Psyclone AIOS platform
Loading...
Searching...
No Matches
Core Concepts

This page explains the ideas that make a Psyclone system tick: publish– subscribe, triggers, contexts, signals, and the split between code and PsySpec. If you only read one page before writing code, read this one.

Publish–subscribe

Psyclone is based on the publish–subscribe principle. Modules never call each other; they post messages and subscribe to message types. Every message carries a dot-delimited type (Input.Audio.Raw, robot.sensor.bumper), and each module declares — in the PsySpec, not in code — which types it wants via <trigger> entries. When a message of a matching type is posted anywhere in the system, every subscribed module is woken up with a copy.

This gives you the classic pub/sub payoffs:

  • Decoupling. A speech-recognition module that consumes audio and posts Speech.Transcript.Ready neither knows nor cares who produces the audio or who consumes its findings — even if they run in another process, on another machine, or are written in another language.
  • Re-wiring without recompiling. Because the wiring lives in the PsySpec, you can swap a live camera for a recorded stream, insert a filter module, or fan one output out to five consumers by editing XML.

Message types should follow the convention of a dot-delimited string where the first segment is a root namespace and later segments increase in specificity left to right (MyProject.Input.Audio.Raw). Wildcards such as Psyclone.*.MySpace.Connect are supported in trigger types.

Modules and cranks

A module is a named unit declared in the PsySpec. What it does is its crank: a C/C++ function with the signature

int8 MyCrank(cmlabs::PsyAPI* api);
The API handle a component (crank) uses to talk to the Psyclone system.
Definition PsyAPI.h:82

exported from a library that the PsySpec loads. The crank receives a cmlabs::PsyAPI handle — its one window onto the system — through which it waits for triggers, posts output, emits signals, reads parameters, and queries whiteboards and catalogs. Every module has two distinct concerns and only one of them is yours: message handling (receiving, queuing, routing, posting) is done by the system; message processing is the crank. Details in Writing a Module (Crank).

Triggers

A <trigger> subscribes the module to a message type:

<trigger name="Ball" type="ball.1" maxage="2000" />

The name is local to the module: when the crank wakes up, cmlabs::PsyAPI::waitForNewMessage() reports which named trigger fired, so one crank can react differently to different inputs without parsing types. Useful attributes seen throughout the examples:

  • maxage — ignore messages older than this many milliseconds.
  • after — delay the wake-up after the triggering post.
  • interval — a timed trigger: fire automatically every N milliseconds, no message required (see Examples/contextprint.xml).
  • context — trigger on a context change instead of a message type (<trigger name="Start" context="Test.PingPong" />).

Contexts

A context is a named, hierarchical, system-wide state, written as a dot-delimited tree path: SoB.Alive.Awake, Battery.Low, Test.PingPong. Any module can post a context change, and modules can declare that their triggers are only live while a given context is active:

<module name="Print1">
<context name="My.Context.1">
<trigger name="Ball1" type="ball.1" interval="100" />
<crank name="print" function="Examples::Print" />
<post name="done" context="My.Context.2" />
</context>
</module>

The rules:

  • Context trees are designed by you; the only requirement is dot-delimited names.
  • For each root (the first segment), at most one branch is active at a time: posting My.Context.2 deactivates My.Context.1. Contexts with different roots co-exist freely — Battery.Low and People.Present can both be active, Battery.Low and Battery.Full cannot.
  • A module that is out of context is simply not woken by its triggers.
  • If a module declares no context, it lives in the always-on default and is active whenever the system runs.
  • Context changes are delivered as signals system-wide, so modules can also trigger directly on them (<trigger context="..."/>).

Contexts are the tool for managing complexity at scale: a large system rarely needs all of its modules at once. Instead of littering module code with mode flags, you switch whole groups of modules on and off — and change which crank a trigger fires — by posting one context:

Contexts, triggers and cranks combining

While context A.B is active, types t1/t2 fire crank 1; after a switch to A.C, crank 1 is instead fired by t3. The same trigger type can fire different cranks in different contexts, and if two contexts with different roots are active simultaneously, one message can fire two cranks. See Examples/contexts.xml and Examples/contextprint.xml for runnable demonstrations, including the case of a module whose context never activates.

Signals

Besides typed messages, modules can communicate through signals — named, system-wide broadcasts that need no subscription routing:

<signal name="Output" type="My.Signal.1" />
<signal name="Input" type="My.Signal.2" />
api->emitSignal("Output"); // broadcast
DataMessage* msg = api->waitForSignal("Input", 100, lastReceivedTime);

A signal is broadcast to every space in the whole system and all waiting modules are released at the same time, optionally with a payload message. Signals are the lowest-latency path in the system and are also how context changes propagate. See Examples/signals.xml and Examples/src/Examples.cpp (SignalPing/SignalPong).

Messages and their lifecycle

Messages are cmlabs::DataMessage objects: flat, binary, self-describing containers. A crank fills one with typed user entries and posts it:

DataMessage* msg = new DataMessage();
msg->setString("RobotStatus", "Almost ready");
msg->setInt("BatteryLevel", 78);
api->postOutputMessage("Status", msg);

Entries can be strings, integers, floats, times, binary blobs and nested messages — plus arrays (integer-indexed) and maps (string-indexed) of each. Because the whole message is one flat allocation, posting it means copying it verbatim into shared memory; delivery to another process on the same node never serialises or parses anything, and delivery to another machine sends the same bytes over a socket (see cmlabs::DataMessage and Messaging).

The lifecycle of a posted message:

  1. The crank posts through a named <post> entry; the system fills in type, origin and timestamps (microsecond resolution, see Functions dealing with time).
  2. The node routes copies to every subscribed trigger — and to any whiteboard or catalog that subscribed or was addressed with to=.
  3. Receiving cranks get the message from cmlabs::PsyAPI::waitForNewMessage(). Trigger messages are owned by the system: do not delete them.
  4. Messages stored on a whiteboard live until their TTL expires or the whiteboard hits its size limit; until then they can be retrieved by any module (see Whiteboards & Catalogs).

The two ID spaces

CMSDK uses numeric identifiers in two entirely separate ways, and mixing them up is a classic source of connection failures:

  • Psyclone node system id = the node's port. When a process joins a Psyclone system with cmlabs::PsySpace::connect() it passes a uint16 system id, and that id is the port the node listens on (10000 by default). connect(10000) means "attach to the node on port 10000".
  • Request-fabric gateway/executor ids are rendezvous keys, not ports. The uint32 id in the cmlabs::RequestGateway constructor and in addGateway(id, addr, port) on cmlabs::RequestExecutor / cmlabs::RequestClient is an arbitrary label (1, 42, ...) whose only job is to match: the id a client or executor passes to addGateway() must equal the id the gateway was constructed with. The actual network address is the separate addr/port arguments.

So a gateway with id 1 can happily listen on port 10000 — the two numbers live in different spaces and never interact.

PsySpec vs. code — why the split matters

The crank gets input and returns output but does not know where either comes from or goes: sources and destinations — subscriptions, signals, retrieves, queries, posts — are all PsySpec. This is what makes modules genuinely modular: the same compiled crank can be instantiated several times under different names with different wiring and different parameters (ping-pong runs the same function as both players in Examples/pingpong.xml). The PsySpec is the single place where a system's architecture can be read, reviewed and changed. Continue with Writing a PsySpec.