Skip to content

Comms sniffers

The comms sniffers are host-facing plugins that passively wiretap a bus in a running emulator and republish every frame over ZeroMQ as a msgpack message. Point any external tool — a logger, a dashboard, a protocol analyser, or the bundled Python client — at the socket to watch the flight software's comms traffic live, without touching the guest.

There is one sniffer per protocol: CAN, SPI, MIL-STD-1553, SpaceWire, serial (UART). They observe passively (an emulated controller cannot tell a sniffer is attached) and never block the simulation (a slow or absent subscriber loses frames rather than back-pressuring the guest).

For the design (how a plugin resolves its target, the observation seams, the wire format), see the plugin system architecture page.

Building

The sniffers and their ZeroMQ/msgpack dependencies are opt-in — a normal build fetches nothing new:

cmake -S . -B build -DLINCE_BUILD_SNIFFERS=ON
cmake --build build

Adding a sniffer to a configuration

You name the peripheral to watch; the sniffer reaches that peripheral's bus or stream through it. Add a declarative spec to EmulatorConfig::plugins:

#include "lince/plugins/can_sniffer.hpp"
#include "lince/plugins/serial_sniffer.hpp"

auto cfg = lince::runtime::gr712rc_config();

// Watch the CAN traffic the OCCAN controller "occan0" is connected to:
cfg.plugins.push_back(lince::plugins::make_can_sniffer_spec(
    "can0-sniffer", {.peripheral = "occan0", .endpoint = "tcp://127.0.0.1:5556"}));

// Wiretap the console UART:
cfg.plugins.push_back(lince::plugins::make_serial_sniffer_spec(
    "uart0-tap", {.peripheral = "apbuart0", .endpoint = "tcp://127.0.0.1:5560"}));

auto emu = lince::runtime::Emulator::create(std::move(cfg));

Each protocol has its own make_*_sniffer_spec and config struct (CanSnifferConfig, SpiSnifferConfig, MilStd1553SnifferConfig, SpaceWireSnifferConfig, SerialSnifferConfig). The config names the owning peripheral (a CAN/SPI/1553 controller, a SpaceWire peripheral + port, or a UART), the ZeroMQ endpoint, and the send high-water mark. The default endpoints are 5556 (CAN), 5557 (SPI), 5558 (1553), 5559 (SpaceWire), 5560 (serial).

Filtering: on the subscriber

A sniffer publishes the whole wire; you filter on the subscriber using ZeroMQ's native SUBSCRIBE prefix match — dynamically, with no change to the running simulation. Every message is two frames, [topic, payload], where the topic encodes the identifier:

Protocol Topic Example
CAN can/<hexid>/ can/100/
SPI spi/cs<n>/ spi/cs1/
MIL-1553 1553/rt<n>/ 1553/rt7/
SpaceWire spw/<hexaddr>/, spw/tc/ spw/20/
serial serial/tx/, serial/rx/ serial/tx/

The trailing slash means an exact-id subscription (can/100/) is not a prefix of a longer id; a shorter prefix (can/1) selects a range, can/ a whole protocol, and "" everything.

The Python client

The lince-sniffer Python package (in python/) decodes the envelopes into typed objects and ships a CLI:

cd python && pip install -e .          # pulls pyzmq + msgpack

python -m lince_sniffer tcp://127.0.0.1:5556                 # all CAN-bus traffic
python -m lince_sniffer tcp://127.0.0.1:5556 --topic can/100/
python -m lince_sniffer tcp://127.0.0.1:5560 --serial        # reassemble UART lines

As a library:

from lince_sniffer import SnifferClient, CanFrame

with SnifferClient("tcp://127.0.0.1:5556", topics=["can/"]) as client:
    for msg in client:
        print(msg.topic, msg.frame)         # msg.frame is a typed dataclass

End-to-end demo

The sniffer-demo tool runs an emulator with a serial sniffer on the console UART and a guest that prints to it. In two terminals:

# terminal 1 — the emulator + sniffer (waits for you to connect):
./build/plugins/demo/sniffer-demo tests/guest-programs/rtems/hello-world/hello-world.elf

# terminal 2 — the Python subscriber:
python -m lince_sniffer tcp://127.0.0.1:5560 --serial

Press Enter in terminal 1 once the subscriber is connected; the guest's Hello World arrives decoded in terminal 2, confirming the whole sniffer → ZeroMQ → msgpack → Python pipeline.