CMLabs · Psyclone AIOS

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.Status
msg = cmsdk.DataMessage()
msg.setString("RobotStatus", "Almost ready")
msg.setInt("BatteryLevel", 78)
api.postOutputMessage("exampleMsg", msg)   # spec maps name -> Robot.Status

User 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)
Tip. Binary entries make Psyclone a practical glue layer: attach raw camera frames, audio buffers or serialised third-party structures directly — no re-encoding, and PsyProbe can still show the surrounding header and text fields live.

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…OwnerRule
waitForNewMessage() (also waitForNewSignal())FrameworkThe 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 callsYouThe 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.

Note. 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.
Psyclone AIOS · CMLabs