Skip to main content

uc2canopen — Python CANopen master for openUC2

Control UC2 microscope motors, lasers, LEDs, and galvos directly over CAN bus from Python — no ESP32 master required, no JSON-over-serial, just CANopen SDO/PDO.

Two transports are supported:

  • MCP2515 SPI HAT (SocketCAN) — recommended on a Raspberry Pi. The HAT enumerates as a native can0 interface. This is the default.
  • Waveshare USB-CAN-A — a USB dongle, handy on a laptop.

Same API shape as UC2-REST, different transport: CANopen instead of JSON-over-serial.


1. Raspberry Pi + MCP2515 HAT setup (one-time)

Skip this section if you use the Waveshare USB adapter — see Waveshare USB-CAN-A at the bottom.

The HAT is an MCP2515 CAN controller on SPI0 (CS = SPI0_CE0/GPIO8, INT = GPIO12) with an SN65HVD230 3.3 V transceiver and a 12 MHz crystal.

1a. Enable SPI and load the MCP2515 driver

Edit /boot/firmware/config.txt and add:

dtparam=spi=on
dtoverlay=mcp2515-can0,oscillator=12000000,interrupt=12,spimaxfrequency=10000000
  • oscillator=12000000 must match the crystal on the board (12 MHz here). A wrong value = can0 comes up but never sees a frame.
  • interrupt=12 is the INT line wiring (GPIO12). If SPI is flaky, drop spimaxfrequency to 2000000.

Reboot, then confirm the driver bound:

sudo reboot
# after it's back:
dmesg | grep -iE "mcp251|can0"
#   → mcp251x spi0.0 can0: MCP2515 successfully initialized.

1b. Bring the interface up @ 500 kbit/s

sudo ip link set can0 up type can bitrate 500000 restart-ms 100
ip -details link show can0          # state should be ERROR-ACTIVE

500 kbit/s is the UC2 firmware bitrate — it must match your nodes. restart-ms 100 auto-recovers from bus-off while debugging.

sudo apt install -y can-utils
candump can0

You should see live traffic — CANopen heartbeats at 0x700 + node-ID (e.g. 0x70B = node 11) and motor TPDOs at 0x180 + node-ID. If frames scroll, the whole chain works. Send an NMT "start all":

cansend can0 000#0100

1d. Make can0 come up at boot (optional)

Create /etc/systemd/system/can0.service:

[Unit]
Description=Bring up can0 (MCP2515)
After=sys-subsystem-net-devices-can0.device
BindsTo=sys-subsystem-net-devices-can0.device

[Service]
Type=oneshot
RemainAfterExit=yes
ExecStart=/sbin/ip link set can0 up type can bitrate 500000 restart-ms 100
ExecStop=/sbin/ip link set can0 down

[Install]
WantedBy=multi-user.target
sudo systemctl enable --now can0.service

2. Install the Python package

# with uv (recommended)
uv pip install -e .

# or with pip
pip install -e .

python-can's SocketCAN backend is built in on Linux — nothing extra to install for the HAT. (pyserial is pulled in too, only needed for the Waveshare adapter.)

pip install uc2canopen

3. Run the demo

With can0 up (step 1b), from the repo root:

python src/motor_demo.py --motor-node 11

This scans for nodes, jogs motor node 11 back and forth, blinks the laser/LED board, and prints node status. Useful flags: --skip-laser --skip-led, --steps, --speed, --channel can1.


4. Python API

from uc2canopen import UC2Client, NODE

# Defaults to SocketCAN can0 (the MCP2515 HAT) — bring it up first with
#   sudo ip link set can0 up type can bitrate 500000
uc2 = UC2Client()
# Other channel:        UC2Client(channel="can1")
# Waveshare USB adapter: UC2Client(port="/dev/ttyUSB0")

# Move motor X axis 1000 steps
uc2.motor.move(axis=0, position=1000, speed=20000, node_id=NODE.MOT_X)
uc2.motor.wait_for_idle(axis=0, node_id=NODE.MOT_X)
print(f"Motor at {uc2.motor.get_position(axis=0, node_id=NODE.MOT_X)} steps")

uc2.laser.set_value(channel=0, pwm=512, node_id=NODE.LASER_0)   # laser 50%
uc2.led.fill(r=255, g=0, b=0, node_id=NODE.LED_0)               # LEDs red
print(f"Uptime: {uc2.state.get_uptime(NODE.MOT_X)}s")

# Galvo scanner: centre, raster scan, stop
uc2.galvo.set_position(2048, 2048, node_id=NODE.GALVO)
uc2.galvo.raster_scan(nx=128, ny=128, sample_period_us=2, node_id=NODE.GALVO)
uc2.galvo.stop(node_id=NODE.GALVO)

uc2.close()

Device modules

UC2Client exposes one controller per firmware device class (all mirror the CANopen Object Dictionary in od.py):

Module Highlights
uc2.motor move, home/wait_for_homed, get_homing_status, set_soft_limits, configure_tmc, set_hardlimit
uc2.laser set_value, set_pwm_frequency/_resolution, set_despeckle, get_safety_state
uc2.led fill, set_mode, set_pattern (rainbow/fire/…), set_brightness
uc2.galvo set_position/goto, line_scan, raster_scan, stop/estop, get_status
uc2.pid set_setpoint, set_gains, set_enabled, get_actual
uc2.encoder get_position, get_velocity, set_zero
uc2.digital read_input, write_output
uc2.analog read_input, read_filtered, write_dac
uc2.collision configure, calibrate, get_mean, get_sigma
uc2.state get_uptime, get_cpu_temperature, get_enabled_modules, scan_nodes, set_node_id

See CHANGELOG.md for the full 0.2.0 feature list and tests/test_new_features.py for runnable examples of every new command.

5. CLI

The uc2can command also defaults to SocketCAN can0:

uc2can scan                          # find nodes on the bus
uc2can move --node 11 --pos 1000 --speed 20000 --wait
uc2can laser --node 20 --ch 0 --pwm 512
uc2can led --node 20 --r 255 --g 0 --b_val 0
uc2can status --node 11
uc2can sniff                         # dump raw CAN frames
uc2can reboot --node 11

# pick a transport/channel (global flags, before the subcommand):
uc2can --channel can1 scan
uc2can --interface waveshare --port /dev/ttyUSB0 scan

Node-ID assignments

Role Node-ID Python constant
Master (this script) 1 NODE.MASTER
Motor X 11 NODE.MOT_X
Motor Y 12 NODE.MOT_Y
Motor Z 13 NODE.MOT_Z
Motor A 14 NODE.MOT_A
LED / combined illum. board 20 NODE.LED (alias: LED_0, LASER_0)
Laser (separate board) 21 NODE.LASER (alias: LASER_1)
Joystick 22 NODE.JOYSTICK
Galvo 30 NODE.GALVO
Galvo 2 31 NODE.GALVO_2
Encoder 40 NODE.ENCODER
PID 50 NODE.PID

Architecture

Your Python script
    │
    ▼
UC2Client
├── motor  → Motor()    SDO writes to 0x2000-0x200B
├── laser  → Laser()    SDO writes to 0x2100
├── led    → Led()      SDO writes to 0x2200
├── state  → State()    SDO reads from 0x2500+
│
├── SdoClient           raw SDO upload/download over python-can
├── TpdoListener        background thread for motor state (TPDO1)
│
└── python-can BusABC   ── SocketCAN can0 (MCP2515 HAT)   ← default
                        └─ WaveshareBus (USB-CAN-A serial)
        │
        ▼ CAN bus @ 500 kbit/s
        │
    ┌───┴───┬───────┬────────┐
    │       │       │        │
  Slave   Slave   Slave    Slave
  node11  node12  node13   node20
  (mot X) (mot Y) (mot Z)  (illum)

The whole SDO/PDO stack is transport-agnostic (it talks to a python-can BusABC), so the only difference between transports is which bus object UC2Client builds.

OD index reference

All indices are in uc2canopen.od.OD and match the firmware's UC2_OD_Indices.h (generated from uc2_canopen_registry.yaml).

Troubleshooting

Symptom Most likely cause
No can0; dmesg empty/error SPI not enabled, wrong interrupt=, or SPI wiring
can0 up but candump empty Wrong oscillator= (crystal), bitrate ≠ 500 k, or missing 120 Ω termination
RuntimeError: No Waveshare ... adapter found You asked for the Waveshare transport (--port/interface="waveshare") but none is attached; for the HAT just use the default
Failed to open CAN bus on the HAT can0 isn't up — run sudo ip link set can0 up type can bitrate 500000 restart-ms 100
Error frames / bus-off Bitrate mismatch, CANH↔CANL swapped, or no common GND

Waveshare USB-CAN-A (alternative)

No driver setup needed — plug it in and select it explicitly:

python src/motor_demo.py --interface waveshare --port /dev/ttyUSB0 --motor-node 11
uc2can --interface waveshare scan          # auto-detects the port
uc2 = UC2Client(port="/dev/ttyUSB0")       # a port implies the Waveshare transport

Requirements

  • Python ≥ 3.10
  • python-can ≥ 4.0 (SocketCAN backend built in on Linux)
  • pyserial (only for the Waveshare adapter)
  • Hardware: an MCP2515 SPI HAT or a Waveshare USB-CAN-A adapter, plus UC2 slave(s) on the bus

License

MIT — same as the UC2-ESP32 firmware.

Metadata

Release files for uc2canopen 0.2.3

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for uc2canopen 0.2.3
File Size Uploaded
uc2canopen-0.2.3.tar.gz 66.9 kB Details

Built distribution (wheel)

Table of built distributions (wheels) for uc2canopen 0.2.3
File Interpreter ABI Platform
uc2canopen-0.2.3-py3-none-any.whl Python 3 none any Details

Total release size: 104.3 kB

Release files / uc2canopen-0.2.3.tar.gz

Download URL uc2canopen-0.2.3.tar.gz
Size 66.9 kB
Tags Source
SHA-256 checksum
How to use checksums
9fe13bbc6aef0f2a157d562d4c9026e74e9086b93ae7c9267342fd423ba334e7
BLAKE2b-256 checksum
How to use checksums
6d77dc65b60f824eb76e3fd76373ec15ec7b08d93c804246f3adde9a0698bf7c
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via uv/0.12.20 {"installer":{"name":"uv","version":"0.12.20","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"Ubuntu","version":"24.04","id":"noble","libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":true}

Release files / uc2canopen-0.2.3-py3-none-any.whl

Download URL uc2canopen-0.2.3-py3-none-any.whl
Size 37.4 kB
Tags Python 3
SHA-256 checksum
How to use checksums
ce55820a4a2cacc771b93460a4196c50d9d48e304382f9011fc0391a76b8091f
BLAKE2b-256 checksum
How to use checksums
571ba26271a25cef6da55d0a6ac8e2753205ab2566967c074f701ac4458c2be5
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via uv/0.12.20 {"installer":{"name":"uv","version":"0.12.20","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"Ubuntu","version":"24.04","id":"noble","libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":true}

Release history Release notifications | RSS feed

This release

0.2.3 This release

2 release files

0.2.2

2 release files

0.2.1

2 release files

0.1.4

2 release files

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page