CMLabs · Psyclone AIOS

# 3. Core Concepts

The mental model behind every Psyclone system: the PsySpec blueprint, the three component kinds, the publish/subscribe message bus, one-shot vs continuous execution, and what happens at start-up.

## The PsySpec: one file describes the system

A Psyclone system is declared in a single XML file, the PsySpec, given at start-up with `spec=<file>`. It states which components to create — modules, whiteboards, catalogs — and how data flows between them. After start-up, the system can additionally create components dynamically that were never in the PsySpec.

The general shape of a PsySpec:

    <psySpec>
      <variable name="MainDir" value="./data" />   <!-- %MainDir% substitution -->
      <include file="system.xml" />                 <!-- splice in another file -->
      <library name="MyExamples" library="Examples" />
      <node name="Node1" address="otherhost" port="10000" />

      <module name="..."> ... </module>
      <whiteboard name="..."> ... </whiteboard>
      <catalog name="..." type="..."> ... </catalog>
    </psySpec>

- `<variable>` entries define `%name%` text substitutions applied over the whole spec before parsing; a `name=value` command-line argument overrides the spec default.
- `<include>` splices another XML file in place, like a C include; the full XML is parsed only after all includes and variable substitution.
- `<library>` registers a DLL/SO of crank functions.
- `<node>` entries attach other computers to the system (see Distributed Systems).

The complete element-by-element reference — with honest implemented / stub / roadmap status per attribute — is chapter 13.

## Three kinds of components

Kind | Behaviour | Typical use
--- | --- | ---
Module | Triggers match incoming messages; a crank function processes them; posts publish results. May stay resident in its crank (continuous) or run once per trigger (one-shot). | All processing: sensor handling, analysis, decision logic, actuation.
Catalog | Everything a module does, plus answering synchronous queries with replies. | Data stores with a simple query language, or conduits to other systems (search services, recognition servers, other robots).
Whiteboard | A special catalog that stores messages only, with a sophisticated retrieval query language and visual viewing/filtering in PsyProbe. | Short-term shared memory: keep recent messages available for later retrieval.

Every component has at least one crank. Modules declare theirs in the spec; whiteboards have a built-in crank; for catalogs the crank is the catalog `type`.

## The publish/subscribe message bus

Components never call each other. Instead, every message carries a dot-notation type — e.g. `input.audio.raw`, `input.audio.normalised` — and components subscribe to types with `<trigger>` elements, exactly or with wildcards (`input.audio.*`, `input.*.raw`). A `<post>` publishes a message of a given type without knowing (or caring) who receives it. Messages nobody subscribes to are simply discarded (or kept in shared memory when a ttl is set).

Crucially, trigger and post names are separate from message types: crank code refers only to names (`api->waitForNewMessage(100, "Input")`, `api->postOutputMessage("Output", msg)`). Rerouting the system — say, feeding a module smoothed instead of raw frames — is a PsySpec edit only; no code changes.

The poster never names the receivers. The bus delivers `input.audio.raw` to every matching subscription — exact or wildcard — and skips the rest.

Posting from code returns the number of messages actually delivered; negative values report failures: `POST_FAILED (-1)` error, `POST_NOSPEC (-2)` posted before any trigger arrived, `POST_OUTOFCONTEXT (-3)` the context changed and this crank is no longer active (see Contexts).

Alongside messages, Psyclone offers signals: a lightweight broadcast that bypasses subscription checking, used like a conductor's stick to beat time in simulations — see Messaging & Signals.

## One-shot vs continuous components

A crank can run in two modes, chosen purely by how you write it:

- One-shot: the crank runs once per trigger and returns; the thread goes back to the pool. Cheap and scalable — the default choice.
- Continuous: the crank keeps its OS thread with a `while (api->shouldContinue())` loop, calling `waitForNewMessage(…)` with a timeout so it can also do other work every cycle. Worth it for heavy initialisation or large local persistent data — but each continuous component holds a thread, and many of them can hurt performance.

Psyclone automatically maintains a pool of spare threads either way. Details and code patterns are in Modules.

## How a system starts up

1. Psyclone reads the PsySpec, expands all `<include>` files and `%variable%` substitutions, then parses the result.
2. Crank libraries are located and loaded; nodes and process spaces are set up; every declared component is created and its subscriptions registered.
3. When everything is in place, the system posts the `Psyclone.Ready` message and prints `SYSTEM READY`. Any module with a trigger on `Psyclone.Ready` starts immediately — that is how ping-pong serves the first ball.
4. The system runs until a component posts `Psyclone.Shutdown` (or the process is stopped), then prints `SYSTEM SHUTDOWN`.

Tip. `Psyclone.Ready` is also the name of the default context, active for as long as the system runs. Everything you declare outside an explicit `<context>` block lives there — a fact that becomes important in chapter 6.
PreviousQuick Start NextModules