Skip to main content

The message-passing interface

Basilisk modules communicate exclusively through the message-passing interface (MPI), a singleton pattern that controls all data flow in the simulation. No module can reach directly into another module’s state — every piece of data that crosses a module boundary travels through a typed message. This design gives you:
  • Data traceability — the messaging system instruments every exchange, so you can see exactly what data moved between which modules in a simulation run
  • Replaceability — swap one module for another without touching anything else, as long as the message types match
  • Testability — unit-test any module in isolation by feeding it stand-alone messages instead of wiring it to other modules

Output and input messages

Every module declares named output messages (written during UpdateState()) and input messages (read at the start of UpdateState()). By convention:
  • Output message variables end with OutMsg
  • Input message variables end with InMsg
The content of a message is called its payload. Payloads are typed C structures defined in architecture/msgPayloadDefC/. For example, CameraImageMsg carries image data, and AttRefMsgPayload carries a reference attitude.

Connecting messages

Use .subscribeTo() to connect an output message to an input message:
from Basilisk.moduleTemplates import cModuleTemplate
from Basilisk.moduleTemplates import cppModuleTemplate
from Basilisk.utilities import SimulationBaseClass, macros
from Basilisk.architecture import messaging

scSim = SimulationBaseClass.SimBaseClass()
dynProcess = scSim.CreateNewProcess("dynamicsProcess")
dynProcess.addTask(scSim.CreateNewTask("dynamicsTask", macros.sec2nano(5.)))

mod1 = cModuleTemplate.cModuleTemplate()
mod1.ModelTag = "cModule1"

mod2 = cppModuleTemplate.CppModuleTemplate()
mod2.ModelTag = "cppModule2"

scSim.AddModelToTask("dynamicsTask", mod1)
scSim.AddModelToTask("dynamicsTask", mod2)

# Connect mod1's output to mod2's input, and mod2's output back to mod1's input
mod2.dataInMsg.subscribeTo(mod1.dataOutMsg)
mod1.dataInMsg.subscribeTo(mod2.dataOutMsg)
The .subscribeTo() method handles all combinations — C-to-C, C-to-C++, C++-to-C, and C++-to-C++ — transparently. The messages being connected must be the same payload type.
You can only subscribe an input message to an output message that already exists. Always create all module instances before calling .subscribeTo().
To remove a connection:
mod2.dataInMsg.unsubscribe()

Stand-alone messages

Sometimes a module needs an input that comes from configuration data rather than another running module — for example, spacecraft mass properties, reaction wheel geometry, or thruster configuration. Create a stand-alone message for this:
from Basilisk.architecture import messaging

# 1. Create the payload container (zero-initialized)
msgData = messaging.VehicleConfigMsgPayload()
msgData.massSC = 750.0  # [kg]

# 2. Create the message object and write the payload
msg = messaging.VehicleConfigMsg()
msg.write(msgData)

# Or combine into one line:
msg = messaging.VehicleConfigMsg().write(msgData)

# 3. Connect the stand-alone message to the module input
attController.vehConfigInMsg.subscribeTo(msg)
You can also initialize fields on payload construction:
msgData = messaging.VehicleConfigMsgPayload(massSC=750.0)
Stand-alone message objects must remain in memory for the lifetime of the simulation. If a message object is created inside a function and not stored in a persistent variable, Python’s garbage collector may destroy it after the function returns, causing silent failures or segmentation faults in the C++ layer.Store stand-alone messages as instance attributes or in a class-level registry:
class MySimulation:
    def setup(self):
        msgData = messaging.VehicleConfigMsgPayload(massSC=750.0)
        self.vehicleConfigMsg = messaging.VehicleConfigMsg().write(msgData)
        controller.vehConfigInMsg.subscribeTo(self.vehicleConfigMsg)

Reading message payloads

To inspect the current payload of any message:
payload = mod1.dataOutMsg.read()
print(payload)
For large payloads, use pprint for readable output:
from pprint import pprint
pprint(mod1.dataOutMsg.read())

Recording messages

To capture a time history of a message, create a recorder module and add it to a task:
# Record at the task update rate
msgRec = mod1.dataOutMsg.recorder()
scSim.AddModelToTask("dynamicsTask", msgRec)

# Record at most once every 20 seconds
msgRec20 = mod1.dataOutMsg.recorder(macros.sec2nano(20.))
scSim.AddModelToTask("dynamicsTask", msgRec20)
After the simulation runs, access the recorded data:
scSim.ExecuteSimulation()

# Access a message field by name
print(msgRec.dataVector)         # array of recorded values
print(msgRec.times())            # nanoseconds when each sample was recorded
print(msgRec.timesWritten())     # nanoseconds when each message was written
The two time arrays differ when a message is written less frequently than it is read. times() gives the recording timestamps; timesWritten() gives the write timestamps and will repeat when a stale message is read. To clear accumulated data between simulation runs:
msgRec.clear()

Data flow visualization

The MPI instruments every message exchange. After a simulation run you can inspect which modules wrote to which messages and which modules subscribed to them. This makes it possible to draw the complete data-flow graph of your simulation and verify that no unexpected connections exist.
Use message recorders on intermediate messages — not just final outputs — to debug unexpected module behavior. Recording an output message has no effect on module execution.

Message types reference

All message payload definitions are in src/architecture/msgPayloadDefC/ (C structs) and src/architecture/msgPayloadDefCpp/ (C++ structs). Import the messaging module to instantiate any payload or message type:
from Basilisk.architecture import messaging

# List available message types
dir(messaging)

Build docs developers (and LLMs) love