Skip to content

Architecture

CAN & CANopen Studio is one protocol engine behind two front ends, plus a set of agent-facing servers. Knowing which piece does what explains most of the behaviour you will see — including why the graphical studio and the native front end decode a frame identically, and why some features are noticeably faster than others.


The shape of the project

                  crates/canopen-core   (Rust: frames, CiA 301/402, OBD-II,
                            |            SLCAN + UDP transport, PCAP, VCD)
            +---------------+----------------+
            |               |                |
      pyo3_bindings    canopen-gui      canopen-cli
            |            (Slint)        (sniff, simulate, bench)
    src/canopen_studio                  canopen-extcap
      (Tkinter studio,                  (Wireshark plugin)
       MCP / A2A servers)
Path Contents
src/canopen_studio/ The Python package: the Tkinter studio (gui.py), the command-line sniffer, the MCP and A2A servers, OBD-II diagnostics (diag/) and the protocol decoders (stack/)
crates/canopen-core/ The Rust engine: frames, SDO, NMT, PDO, EDS, ISO-TP, OBD-II, telemetry, UDP transport, the virtual simulator, and the PyO3 bindings
crates/canopen-cli/ The Rust command-line tools: canopen-cli (sniff, simulate, echo, benchmarks) and canopen-extcap, the Wireshark capture plugin
crates/canopen-gui/ The native front end, built with Slint

The Rust engine

canopen-core holds everything that can be tested without a window. It is compiled into the Python package as the canopen_core extension module, linked into the native front end, and driven directly by the CLI — so all three agree on what a frame means by construction rather than by convention.

Module Responsibility
frame.rs CanFrame: an 11/29-bit identifier and a fixed [u8; 8], so decoding allocates nothing. Converts to the python-can MessagePack wire format and to a 24-byte compact one
canopen.rs COB-ID classification, NmtState, CanopenService
nmt.rs NmtMaster: builds commands, consumes heartbeats, tracks nodes and their timeouts
sdo.rs Expedited and segmented SDO transfers, abort code decoding
pdo.rs Signal definitions, bit extraction, PDO mappings
eds.rs EDS (CiA 306) parsing; can synthesise a PdoMapping from a mapping object
telemetry.rs DriveTelemetry and Cia402State: folds SEVCON TPDOs and CiA 402 statuswords into one snapshot
obd2.rs Mode 01 requests, responses and PID decoding
isotp.rs, isotp_manager.rs ISO 15765-2 segmentation and reassembly
latency.rs LatencyTracker and the CAN ping helpers on 0x7E0 / 0x7E1
ring_buffer.rs Bounded trace storage
udp.rs UdpCanBus, the transport shared by both front ends and the CLI
slcan.rs The LAWICEL ASCII codec and a serial transport over it — CANUSB, USBtin, CANable
pcap.rs PCAP-NG writing with LINKTYPE_CAN_SOCKETCAN, to a file or a named pipe
bitstream.rs, vcd.rs Rebuilds the bit stream a frame would have produced — stuffing, CRC-15, delimiters — and writes it as VCD for sigrok and PulseView
simulator.rs, simulator_ext.rs The virtual device loop — periodic traffic, and answers to NMT, SDO, ping and OBD-II — and its UDP wiring

See Native Engine & Front End for how it is built and what it accelerates, and Wireshark Integration and Logic Analyzer & Signals for what the two exports are for.


The Python studio

gui.py is the reference implementation and the widest surface: the eight tabs described in the gallery, matplotlib plotting, CSV capture, DBC import and every hardware adapter. interfaces.py abstracts the bus, so the same interface drives SLCAN hardware, SocketCAN, a UDP multicast group or the in-process simulator.

Device knowledge lives in stack/decoders/, registered through stack/registry.py. A decoder maps COB-IDs to named signals — see Protocol Stack & Decoders.

The two front ends must agree

crates/canopen-core/src/telemetry.rs mirrors src/canopen_studio/stack/decoders/sevcon_gen4.py and cia402_generic.py. A change to how a frame is decoded on one side is a change to both.


The native front end

crates/canopen-gui is a Slint application over the same engine. It runs its own simulator across a local UDP pair, and the two ends must use different ports — sharing one socket makes each end receive the frames it just sent:

End Port
User interface 1750
Simulated nodes 1751

The simulator starts both nodes pre-operational, so Active Nodes: 0 on the dashboard is correct until you send an NMT Start.


Agent-facing servers

mcp_server.py exposes the bus over MCP and starts with the studio. a2a_server.py is opt-in behind CANOPEN_STUDIO_A2A=1 and rejects browser-originated requests. Both are documented, with the reasoning behind their differing defaults, in AI Integration.