4. Modules
Modules do the work: cranks and libraries, one-shot vs continuous execution, private data, parameters, custom setup, passthrough modules, Python cranks and external modules.
Cranks and libraries
Every component has at least one crank — a function called when a trigger fires. For modules you declare cranks in the spec; whiteboards use a built-in crank; for catalogs the crank is the catalog type. A crank is a function name in a DLL/SO; built-in cranks (like Ping) need no library. Register a library and refer to its cranks by logical name:
<library name="MyExamples" library="Examples" />
<module name="MyModule">
<trigger name="Input" type="input.data" />
<crank name="Process" function="MyExamples::Ping" />
<post name="Output" type="output.data" />
</module>
File lookup: Examples.dll (Windows) / libExamples.so (UNIX) in the current directory; debug builds first try ExamplesDebug.dll / libExamplesDebug.so. Libraries compile against CMSDK alone — Psyclone itself is not needed to compile or run a library. A crank function without a lib:: prefix refers to a built-in (Internal::) crank. For Python cranks no library is needed at all: <crank name="Process" language="python3" script="process.py" /> — see Python modules below.
The C++ crank pattern
A crank is an exported C function taking a single PsyAPI* — the module's whole world: messages, parameters, retrieves and queries.
// MyCrank.cpp — compiles against CMSDK alone, e.g.:
// g++ -shared -fPIC -I<CMSDK>/include MyCrank.cpp <CMSDK>/lib/<arch>/libCMSDK.a -o libExamples.so
#include "PsyAPI.h"
using namespace cmlabs;
// export declaration
namespace cmlabs { extern "C" {
DllExport int8 MyCrankFunction(PsyAPI* api);
} }
// implementation
int8 MyCrankFunction(PsyAPI* api) {
while (api->shouldContinue()) {
DataMessage* msg = api->waitForNewMessage(100, "Input");
if (!msg) continue;
api->logPrint(1, "Got a message...");
DataMessage* outMsg = new DataMessage();
outMsg->setInt("Count", 1);
outMsg->setString("Status", "ok");
api->postOutputMessage("Output", outMsg);
}
return 0;
}# script-file crank: entry point is PsyCrank(apilink)
def PsyCrank(apilink):
api = cmsdk.PsyAPI.fromPython(apilink)
while api.shouldContinue():
msg = api.waitForNewMessage(100, "Input")
if msg is None: continue
api.logPrint(1, "Got a message...")
outMsg = cmsdk.DataMessage()
outMsg.setInt("Count", 1)
outMsg.setString("Status", "ok")
api.postOutputMessage("Output", outMsg)
return 0DllExport classifier is required for the function to be visible to Psyclone. If a module declares no crank at all, it receives a built-in default crank — see passthrough modules below.One-shot vs continuous
The same declaration supports two execution styles, decided by the crank code:
- One-shot: the crank runs once per trigger, returns, and its thread goes back to the pool. Ideal for stateless processing.
- Continuous: the crank never returns while the system runs —
while (api->shouldContinue())with a timeout onwaitForNewMessagelets it do extra work every cycle (e.g. every 100 ms). It keeps its OS thread, which pays off for heavy initialisation or large in-memory state, but many continuous components mean many threads and possible performance impact.
shouldContinue().Local persistent (private) data
Any component can save local data to persistent storage as a binary blob — especially useful for one-shot modules (state between invocations) and modules that may migrate between nodes. If you supply a mimetype, the data appears in the component's Data tab in PsyProbe.
api->setPrivateData("My Data", str.c_str(), str.length());
data = api->getPrivateDataCopy("My Data", size);
// visible in PsyProbe:
api->setPrivateData("My Report", html.c_str(), html.length(), "text/html");api.setPrivateData("My Data", s)
data = api.getPrivateDataCopy("My Data")
# visible in PsyProbe:
api.setPrivateData("My Report", html, "text/html")Component parameters
Parameters are typed values declared in the spec and read, set and tweaked from code and from PsyProbe:
<parameter name="MaxCount" type="Integer" value="[0:110:2]=55" />
<parameter name="Threshold" type="Float" value="[-2.5:2.5]" />
<parameter name="Mode" type="String" value="['fast';'accurate';'debug']" />
| Value syntax | Meaning |
|---|---|
42 | Plain value |
[0:110] | Range; initial value = lowest |
[0:110]=55 | Range with explicit initial value |
[0:110]=* | Range with random initial value |
[0:110:2] | Stepped range (step 2); step on a plain numeric via interval="2" value="10" |
[-2.5;1.5;1.8] / ['a';'b';'c'] | Value lists (numeric / string) |
From code: getParameterString/getParameterInt (and the float getter), hasParameter(name), setParameter(name, …), resetParameter(name), and getParameterDataType(name) returning PARAM_STRING 1, PARAM_INTEGER 2, PARAM_FLOAT 3, PARAM_STRING_COLL 4, PARAM_INTEGER_COLL 5, PARAM_FLOAT_COLL 6. Nudge a stepped parameter with api->tweakParameter("MaxCount", 2) (±steps). All parameters are queryable and tweakable live from PsyProbe.
Custom configuration: <setup>
For richer configuration than flat parameters, embed any valid XML in a <setup> child. The component reads it back as a string and parses it with the bundled XML parser:
<module name="Mapper">
<setup>
<mapping entry="x" key="PosX" />
<mapping entry="y" key="PosY" />
</setup>
</module>
const char* xml = api->getParameterString("componentsetup");
XMLResults xmlResults;
XMLNode node = XMLNode::parseString(xml, "setup", &xmlResults);
if (xmlResults.error == eXMLErrorNone) { /* getChildNode / getAttribute ... */ }import xml.etree.ElementTree as ET
xml = api.getParameterString("componentsetup")
node = ET.fromstring(xml) # <setup> root
for mapping in node.findall("mapping"): # child nodes / attributes ...
entry, key = mapping.get("entry"), mapping.get("key")Passthrough modules
A module with no crank gets a default crank that passes messages through: every trigger activates every post with a copy of the incoming message. This is surprisingly useful:
- Retype a message (trigger on one type, post another).
- Split one message into several posts carrying the same data.
- Delay a path:
<trigger ... after="5000" />. - Regular posting:
<trigger name="Post" type="Regular.Post" interval="1000" />plus a post.
Posts can add content on the way through, via <content> child entries:
<module name="Heartbeat">
<trigger name="Tick" type="Regular.Post" interval="1000" />
<post name="Out" type="system.heartbeat">
<content key="Source" type="String" value="Heartbeat" />
</post>
</module>
Python modules
Cranks can be written in Python; the full CMSDK is exposed via SWIG, so PsyAPI and DataMessage work as in C++. Three forms:
Script file
<crank name="Ping" language="python3" script="path/to/ping.py" />
The file's entry point is def PsyCrank(apilink):, with api = cmsdk.PsyAPI.fromPython(apilink); the cmsdk import is automatic.
Inline code
<crank name="p1" language="python3"><![CDATA[
while api.shouldContinue():
msg = api.waitForNewMessage(20)
if msg is None: continue
count = msg.getInt("Count")
outMsg = cmsdk.DataMessage()
outMsg.setInt("Count", count + 1)
api.postOutputMessage("Output", outMsg)
]]></crank>
For inline cranks the libraries are auto-loaded and an api PsyAPI object is auto-created. Python is indentation-sensitive; Psyclone attempts to adjust indentation on load, but keep inline code cleanly unindented.
Libraries and paths
Running Python cranks requires: (1) a matching Python interpreter (major version and bitness, DLLs on the search path); (2) the CMSDK binary library _cmsdk.pyd (Windows) / _cmsdk.so (Linux) plus the interface file cmsdk2.py/cmsdk3.py (debug: _cmsdkdebug.pyd/.so, cmsdk2debug.py/cmsdk3debug.py); (3) any extra imports your code needs. Psyclone searches: (a) the script's directory, (b) the Psyclone binary's directory, (c) a module parameter:
<parameter name="libpath" type="String" value="../CMSDK/bin/Win32" />
language="python2" is still accepted but Python 2 is end-of-life — treat it as legacy and use python3 for new work. Python-crank modules automatically get their own process space.External modules
A module can live inside a separate program that links CMSDK and connects to the running Psyclone. In the spec, declare an external space and a module whose crank has a name only (a placeholder the external process fills in):
<space name="GUISpace" type="external" />
<module name="CoCoMapsGUI" space="GUISpace">
<crank name="CoCoMapsGUI" />
<trigger name="Input" type="gui.command.*" />
<post name="Output" type="gui.status" />
</module>
The external program creates the space, connects, and fetches an API per crank:
PsySpace* space = new PsySpace("GUISpace");
space->connect(sysid);
space->start();
PsyAPI* api = space->getCrankAPI("CoCoMapsGUI"); // retry loop on failure
// then use api exactly like an internal modulespace = cmsdk.PsySpace("GUISpace")
space.connect(sysid)
space.start()
api = space.getCrankAPI("CoCoMapsGUI") # retry loop on failure
# then use api exactly like an internal moduleHere sysid is the system ID — a 16-bit number (type uint16) that identifies the running Psyclone instance an external process attaches to; it must match the ID of the Psyclone you are connecting to (a single local system defaults to 1, so space->connect(1) is the usual localhost value). The signature is connect(uint16 systemID, bool isMaster=false, const char* cmdline=NULL).
Monitor the link with space->isConnected() / space->hasShutdown(); on failure delete the space but not the PsyAPI (it is owned by the PsySpace), then retry periodically. External space crashes and restarts are tolerated just like internal ones.
A <module type="external"> may also carry an <executable> child naming the command line to auto-start the external process — but note this is currently parsed and stored without being launched: Roadmap (in progress). Start external processes yourself for now.
<module name="Ext1" type="external">
<executable consoleoutput="yes" autorestart="yes">./mytool --connect</executable>
</module>
Useful module attributes
| Attribute | Meaning | Status |
|---|---|---|
name | Component name (required) | Shipped |
space | Process space (default Root); see Distributed Systems | Shipped |
node | Node assignment; * = every node | Shipped |
verbose / debug / logfile | Per-module verbosity, debug level and log file | Shipped |
selftrigger | allow | warn (default) | other = never trigger on own posts | Shipped |
tag | Tag appended to the name; * = own name (see Tagging) | Shipped |
migration / priority | Migration permission / scheduling priority — parsed but not yet acted on | Stub |