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 0Step 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.
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" />
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 0Callers 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 invisiblejson = '{ "frames": 1024, "fps": 29.7 }'
api.setPrivateData("Stats", json, "application/json")
# binary works too, e.g. "image/bmp"; omit the mimetype to keep it invisibleFetch 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.