9. Data Messages
The DataMessage is Psyclone's universal container: typed header, microsecond timestamps, and user contents from single integers to binary blobs and nested messages.
Anatomy of a DataMessage
Every message flowing through a Psyclone system is a DataMessage. It has two parts:
- Header — like an email header: the message type, sender, addressee (
to), creation time, tags and the internal reference used for query replies. PsyProbe shows exactly these fields when you inspect a message. - User contents — any number of named entries you set yourself: strings, integers, floats, times, binary data, and even other DataMessages.
Timestamps are 64-bit with microsecond resolution and are synchronised across the computers of a distributed system — you can meaningfully compare the creation times of messages produced on different machines, which matters for sensor fusion and offline analysis.
Message types
The type is a dot-delimited ASCII string: the first part is the root namespace and each further part increases specificity left to right — input.video.raw, input.audio.normalised, Robot.Status.Navigate.Done. Types are what triggers subscribe to (including wildcards like input.audio.*); the full story is in Messaging & Signals. A message that matches no subscription is discarded — unless it was posted with a ttl, in which case it is kept available for that long.
Setting a type happens in the PsySpec, not in code: the crank posts by post name and the spec decides the type:
<post name="exampleMsg" type="Robot.Status" />
DataMessage* msg = new DataMessage();
msg->setString("RobotStatus", "Almost ready");
msg->setInt("BatteryLevel", 78);
api->postOutputMessage("exampleMsg", msg); // spec maps name -> Robot.Statusmsg = cmsdk.DataMessage()
msg.setString("RobotStatus", "Almost ready")
msg.setInt("BatteryLevel", 78)
api.postOutputMessage("exampleMsg", msg) # spec maps name -> Robot.StatusUser content entries
Entry types: String, Int, Float, Time, Binary and Message (a nested DataMessage). Each entry is addressed by name:
msg->setString("Utterance", "hello there");
msg->setInt("Count", 42);
msg->setFloat("Confidence", 0.93);
// binary blob (e.g. an image frame)
msg->setData("Frame", pixelBuffer, bufferSize);
// nested message
DataMessage* inner = new DataMessage();
inner->setInt("x", 10);
msg->setMessage("Position", inner);msg.setString("Utterance", "hello there")
msg.setInt("Count", 42)
msg.setFloat("Confidence", 0.93)
# binary blob (e.g. an image frame)
msg.setData("Frame", pixelBuffer)
# nested message
inner = cmsdk.DataMessage()
inner.setInt("x", 10)
msg.setMessage("Position", inner)Arrays (integer index)
Every entry type also comes in an array form, indexed by integer:
msg->setInt(1, "myarray", 2); // myarray[1] = 2
msg->setInt(3, "myarray", 7); // myarray[3] = 7
int64 v = msg->getInt(3, "myarray");
// whole-array access
std::map<int64,int64> arr = msg->getIntArray("myarray");
msg->setIntArray("myarray2", arr);msg.setInt(1, "myarray", 2) # myarray[1] = 2
msg.setInt(3, "myarray", 7) # myarray[3] = 7
v = msg.getInt(3, "myarray")
# whole-array access (index -> value mapping)
arr = msg.getIntArray("myarray")
msg.setIntArray("myarray2", arr)Maps (string index)
And in a map form, indexed by string:
msg->setInt("in", "mymap", 1); // mymap["in"] = 1
msg->setInt("out", "mymap", 0);
int64 n = msg->getInt("out", "mymap");
std::map<std::string,int64> m = msg->getIntMap("mymap");
msg->setIntMap("mymap2", m);msg.setInt("in", "mymap", 1) # mymap["in"] = 1
msg.setInt("out", "mymap", 0)
n = msg.getInt("out", "mymap")
m = msg.getIntMap("mymap")
msg.setIntMap("mymap2", m)Array and map forms exist for all entry types — int, float, string, time, binary data and nested messages — with the same naming pattern (getFloatArray, setStringMap, and so on).
Memory ownership (C++)
Three simple rules cover who deletes what in a C++ crank:
| You got it from… | Owner | Rule |
|---|---|---|
waitForNewMessage() (also waitForNewSignal()) | Framework | The message is owned by the system — never delete it in the crank. |
A message you new and pass to postOutputMessage() | Framework (after posting) | Posting hands ownership to the framework — do not delete or reuse it after posting (it is consumed even when the post fails or matches no post spec). |
getPrivateDataCopy() and similar copy-returning calls | You | The buffer is a fresh new char[] copy — delete[] it when done. |
Python cranks can ignore all of this — the bindings manage lifetimes for you.
A realistic example
A vision module posting a detection to the rest of the system might build:
DataMessage* out = new DataMessage();
out->setString("Identity", "Thor");
out->setFloat("PosX", 2.41);
out->setFloat("PosY", -0.87);
out->setFloat("Confidence", 0.88);
out->setData("FaceCrop", cropBuf, cropSize); // image/bmp blob
int posted = api->postOutputMessage("HumanDetected", out);
if (posted < 0) api->logPrint(1, "post failed: %d", posted);out = cmsdk.DataMessage()
out.setString("Identity", "Thor")
out.setFloat("PosX", 2.41)
out.setFloat("PosY", -0.87)
out.setFloat("Confidence", 0.88)
out.setData("FaceCrop", cropBuf) # image/bmp blob
posted = api.postOutputMessage("HumanDetected", out)
if posted < 0:
api.logPrint(1, "post failed: %d" % posted)Downstream, a CCMCollector-style component can extract PosX/PosY into world-model observations, PsyProbe operators can hover the message in the Activity tab and see every entry, and a ReplayCatalog can record it — all from the same self-describing container.
postOutputMessage returns the number of messages actually posted; negative values are errors: POST_FAILED (-1), POST_NOSPEC (-2, posted before any trigger arrived), POST_OUTOFCONTEXT (-3, the active context changed). See Messaging & Signals.