CMLabs · Psyclone AIOS

# 14. Tutorials

Step-by-step recipes: from your first running system to custom catalogs, external processes and your own PsyProbe views.

## Tutorial 1: Your first Psyclone system

The classic pingpong needs no user code — both modules use the built-in `Ping` crank. Save as `pingpong.xml`:

    <psySpec>
      <module name="Ping">
        <trigger name="Ready" type="Psyclone.Ready" />
        <trigger name="Ball" type="ball.1" />
        <crank name="Ping" function="Ping" />
        <post name="Ball" type="ball.2" />
      </module>
      <module name="Pong">
        <trigger name="Ball" type="ball.2" />
        <crank name="Pong" function="Ping" />
        <post name="Ball" type="ball.1" />
        <post name="Done" type="Psyclone.Shutdown" />
      </module>
    </psySpec>

Run it:

    Psyclone spec=pingpong.xml

At startup Psyclone posts `Psyclone.Ready`, which triggers Ping; Ping posts `ball.2`, Pong answers with `ball.1`, and the ball bounces 100,000 times — a count baked into the built-in `Ping` crank code, not set in the XML. Note the crank declarations: in `<crank name="Pong" function="Ping"/>`, `name` is your label for this crank instance while `function` selects which crank code runs (here the built-in `Ping` in both modules). Every 10,000 messages the console logs throughput (average message time/age in µs); Pong then posts `Psyclone.Shutdown`. Watch the console for “SYSTEM READY”, the stats batches, and “SYSTEM SHUTDOWN” — and open `http://localhost:10000/` while it runs to see the messages fly in PsyProbe.

## Tutorial 2: Adding your own C++ module

Step 1 — write the crank. A crank is an exported function in a library that compiles against CMSDK alone (Psyclone is not needed to build it). Header:

    #include "PsyAPI.h"
    namespace cmlabs {
      extern "C" {
        DllExport int8 MyCrankFunction(PsyAPI* api);
      }
    }

    # mycrank.py — no header or exports needed; the cmsdk import is automatic.
    # The entry point Psyclone calls is:
    def PsyCrank(apilink):
        api = cmsdk.PsyAPI.fromPython(apilink)
        ...

Step 2 — implement it. A continuous crank loops while the system runs:

    // MyCrank.cpp — complete, compilable crank library
    // build: g++ -shared -fPIC -I<CMSDK>/include MyCrank.cpp <CMSDK>/lib/<arch>/libCMSDK.a -o libExamples.so
    #include "PsyAPI.h"
    using namespace cmlabs;

    namespace cmlabs { extern "C" {
      DllExport int8 MyCrankFunction(PsyAPI* api);
    } }

    int8 MyCrankFunction(PsyAPI* api) {
      while (api->shouldContinue()) {
        DataMessage* inMsg = api->waitForNewMessage(100);
        if (!inMsg) continue;
        std::string triggerName = api->getCurrentTriggerName();
        api->logPrint(1, "Got a %s message", triggerName.c_str());
        DataMessage* outMsg = new DataMessage();
        outMsg->setInt("Count", 1);
        outMsg->setString("Status", "ok");
        api->postOutputMessage("OutputFrame", outMsg);
      }
      return 0;
    }

    def PsyCrank(apilink):
        api = cmsdk.PsyAPI.fromPython(apilink)
        while api.shouldContinue():
            inMsg = api.waitForNewMessage(100)
            if inMsg is None: continue
            triggerName = api.getCurrentTriggerName()
            api.logPrint(1, "Got a %s message" % triggerName)
            outMsg = cmsdk.DataMessage()
            outMsg.setInt("Count", 1)
            outMsg.setString("Status", "ok")
            api.postOutputMessage("OutputFrame", outMsg)
        return 0

Step 3 — build the library (e.g. `Examples.dll` / `libExamples.so` in the current directory; debug builds look for `ExamplesDebug.dll` / `libExamplesDebug.so` first). Step 4 — declare it:

    <library name="MyExamples" library="Examples" />
    <module name="MyModule">
      <trigger name="InputFrame" type="input.video.raw" />
      <crank name="MyCrank" function="MyExamples::MyCrankFunction" />
      <post name="OutputFrame" type="output.video.processed" />
    </module>

Python variant. The same module declaration works with a Python crank — just swap the crank line for `<crank name="MyCrank" language="python3" script="mycrank.py" />` (see Tutorial 3); no `<library>` element is needed.

Note. The `DllExport` classifier is required for symbol visibility, and the crank takes exactly one `PsyAPI*` parameter — your handle to messages, parameters, retrieves and queries.

## Tutorial 3: Creating Python modules

Cranks can be written in Python; the full CMSDK is exposed via SWIG, so `PsyAPI` works as in C++. Two forms:

Inline crank — code straight in the spec; the `api` object is created for you:

    <crank name="p1" language="python3"><![CDATA[
    while api.shouldContinue():
        msg = api.waitForNewMessage(20)
        if msg is None: continue
        api.logPrint(1, "trigger: " + api.getCurrentTriggerName())
        out = cmsdk.DataMessage()
        out.setInt("Count", msg.getInt("Count") + 1)
        api.postOutputMessage("Output", out)
    ]]></crank>

Script file — point the crank at a file with a `PsyCrank` entrypoint:

    <crank name="Ping" language="python3" script="path/to/ping.py" />

    def PsyCrank(apilink):
        api = cmsdk.PsyAPI.fromPython(apilink)   # cmsdk import is automatic
        while api.shouldContinue():
            ...

Libraries and paths. The interpreter needs (1) the right Python (major version and bitness); (2) the CMSDK binary lib `_cmsdk.pyd` (Windows) / `_cmsdk.so` (Linux) plus the interface file `cmsdk2.py`/`cmsdk3.py` (debug: `_cmsdkdebug.*`, `cmsdk2/3debug.py`); (3) any extra imports. Search locations: the script's directory, the Psyclone binary's directory, or an explicit module parameter:

    <parameter name="libpath" type="String" value="../CMSDK/bin/Win32" />

Warning. Python 2 crank support (`language="python2"`) exists but Python 2 is end-of-life — treat it as legacy and use Python 3.

## Tutorial 4: Creating external modules

Run a module inside your own program using an external space. In the spec, declare the space and a placeholder module whose crank has a name only:

    <space name="GUISpace" type="external" />
    <module name="MyGUI" space="GUISpace">
      <trigger name="Update" type="gui.update" />
      <crank name="MyGUI" />
      <post name="Click" type="gui.click" />
    </module>

Your program links CMSDK, includes `PsyAPI.h`, and connects:

    PsySpace* space = new PsySpace("GUISpace");
    if (!space->connect(sysid)) { /* retry later */ }
    space->start();
    PsyAPI* api = NULL;
    while (!(api = space->getCrankAPI("MyGUI")))
      utils::Sleep(500);            // retry loop
    // use api exactly like an internal crank:
    while (api->shouldContinue()) { ... }

    space = cmsdk.PsySpace("GUISpace")
    if not space.connect(sysid):
        pass  # retry later
    space.start()
    api = space.getCrankAPI("MyGUI")
    while api is None:              # retry loop
        time.sleep(0.5)
        api = space.getCrankAPI("MyGUI")
    # use api exactly like an internal crank:
    while api.shouldContinue():
        ...

Monitor the link with `space->isConnected()` and `space->hasShutdown()`. On failure delete the space and reconnect periodically — but never delete the `PsyAPI` yourself (the PsySpace owns it). Call `getCrankAPI` once per crank if the space hosts several.

## Tutorial 5: Creating your own catalog

A catalog is a crank with state plus query handling. Declare it and a query to reach it:

    <catalog name="MyStore" type="MyLib::MyStoreCatalog">
      <trigger name="Ingest" type="data.sample" />
    </catalog>

    <module name="Client">
      <query name="Store" source="MyStore" />
      ...
    </module>

Catalog crank skeleton — handle both ordinary triggers and queries:

    int8 MyStoreCatalog(PsyAPI* api) {
      std::map<std::string, std::string> store;   // your state
      while (api->shouldContinue()) {
        DataMessage* inMsg = api->waitForNewMessage(100);
        if (!inMsg) continue;
        if (inMsg->getType() == PsyAPI::CTRL_QUERY) {
          uint32 id = (uint32) inMsg->getReference();
          std::string q = inMsg->getString("Query");
          DataMessage* reply = new DataMessage();
          reply->setString("Value", store[q]);
          api->queryReply(id, PsyAPI::QUERY_SUCCESS, reply);
        } else {
          store[inMsg->getString("Key")] = inMsg->getString("Value");
        }
      }
      return 0;
    }

    def PsyCrank(apilink):
        api = cmsdk.PsyAPI.fromPython(apilink)
        store = {}                              # your state
        while api.shouldContinue():
            inMsg = api.waitForNewMessage(100)
            if inMsg is None: continue
            if inMsg.getType() == cmsdk.PsyAPI.CTRL_QUERY:
                id = inMsg.getReference()
                q = inMsg.getString("Query")
                reply = cmsdk.DataMessage()
                reply.setString("Value", store.get(q, ""))
                api.queryReply(id, cmsdk.PsyAPI.QUERY_SUCCESS, reply)
            else:
                store[inMsg.getString("Key")] = inMsg.getString("Value")
        return 0

Callers use `api->queryCatalog(&resultMsg, "Store", queryMsg, 5000)` (message form) or the string form with `result/datasize/query/operation`, and check the returned `QUERY_*` status. For inspiration at production scale — world models, roles/tasks, proxy federation — see Catalogs and the CoCoMaps case study.

## Tutorial 6: Adding custom data in PsyProbe

Any component can publish private data that PsyProbe shows in its Data tab and serves over HTTP. Store it with a mimetype from your crank:

    std::string json = "{ \"frames\": 1024, \"fps\": 29.7 }";
    api->setPrivateData("Stats", json.c_str(), json.length(), "application/json");
    // binary works too, e.g. "image/bmp"; omit the mimetype to keep it invisible

    json = '{ "frames": 1024, "fps": 29.7 }'
    api.setPrivateData("Stats", json, "application/json")
    # binary works too, e.g. "image/bmp"; omit the mimetype to keep it invisible

Fetch it from a browser or AJAX:

    GET /api/getcomponentdata?compid=15&name=Stats&format=json

(formats: text, xml, json, html, binary — match how you stored it). Read it back in code with `api->getPrivateDataCopy("Stats", size)`. For message-driven data, a RequestStore Catalog gives you `GET /api/query?from=RequestStore&query=VideoFrame` without writing any code.

## Tutorial 7: Adding a custom tab in PsyProbe

Step 1 — put a template HTML file in the PsyProbe HTML directory (e.g. `elements/mymodule.html`). The minimum contract is a script defining an ID, a name, and two callbacks:

    <script>
    var TemplateCompID;
    var TemplateCompName;
    function loadTemplate($target, compID) {
      // called once; componentMap[compID] has the component info
    }
    function updateTemplate($target, compID) {
      // called every update cycle; poll /api/getcomponentdata or /api/query here
    }
    </script>

Step 2 — register the view from the component's crank:

    api->addPsyProbeCustomView("My View", "elements/mymodule.html");

    api.addPsyProbeCustomView("My View", "elements/mymodule.html")

The tab appears on that component in PsyProbe. The bundled `elements/whiteboardmessages.html` is the reference example; CoCoMaps' Cognitive Map (jCanvas 2-D map), Time Series (live streaming chart) and Control Panel (catalog-rendered HTML) tabs are three real-world styles built on this exact mechanism.

## Tutorial 8: A larger multi-component example

Putting it together: a small pipeline with a whiteboard, a catalog, a Python module, a timer, and PsyProbe conveniences. This mirrors (at reduced scale) the structure of the CoCoMaps robot spec:

    <psySpec>
      <variable name="DataDir" value="./data" />
      <include file="common.inc" />
      <library name="MyLib" library="Examples" />

      <!-- sensor source: posts a frame every 200 ms once kicked off -->
      <module name="Sensor">
        <trigger name="Ready" type="Psyclone.Ready" />
        <trigger name="Tick" type="Regular.Post" interval="200" />
        <crank name="Sensor" function="MyLib::SensorRead" />
        <post name="Frame" type="input.video.raw" />
      </module>

      <!-- analysis in Python, in its own space -->
      <module name="Analyser">
        <trigger name="Frame" type="input.video.raw" maxage="500" />
        <crank name="Analyser" language="python3" script="analyse.py" />
        <post name="Detection" type="analysis.detection" />
        <parameter name="Threshold" type="Float" value="[0.0:1.0]=0.8" />
      </module>

      <!-- keep detections queryable -->
      <whiteboard name="Detections" key="Confidence" keytype="float">
        <trigger name="Det" type="analysis.detection" />
      </whiteboard>

      <!-- record the run for replay -->
      <catalog name="Recording" type="ReplayCatalog" root="%DataDir%/rec1" maxcount="5000">
        <trigger name="Frame" type="input.video.raw" />
        <trigger name="Det"   type="analysis.detection" />
      </catalog>

      <!-- clickable manual test post in PsyProbe -->
      <psyprobe>
        <post name="FakeDetection" type="analysis.detection" />
      </psyprobe>
    </psySpec>

Run it, open PsyProbe, watch `input.video.raw` flow into the Analyser and detections land on the whiteboard; use the Subscriptions tab's [test] buttons or the clickable post to inject synthetic detections; then replay `%DataDir%/rec1` in a second spec that swaps the Sensor for the Recording catalog with matching post names. For the full-size version of this pattern — two robots, a federated world model, dialogue and navigation — read the CoCoMaps case study.

Tip. Every element and attribute used above is specified precisely, with status badges, in the PsySpec XML Reference.
PreviousPsySpec XML Reference NextGuide Overview