Skip to content

Latest commit

 

History

2 Commits

Folders and files

NameName
Last commit message
Last commit date
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

cantest — minimal CAN messenger for two Raspberry Pis

A tiny end-to-end demo: two Raspberry Pi 4Bs exchanging short text messages over a Classic CAN bus at 1 Mbps, with a UAVCAN-style priority convention (arbitration ID = priority digit, lower = higher priority).

Role Hardware Sends priorities
EULER Waveshare RS485 CAN HAT (MCP2515) P0, P3
NEWTON UCAN USB-C adapter (gs_usb) P1, P2

Hardware Monitor

Files

node.py               # plain implementation: raw python-can, 8-byte frames
node_pycyphal.py      # pycyphal implementation: full UAVCAN/Cyphal v1 stack
priority_test.py      # arbitration demo: EULER P3 vs NEWTON P2 + a P0 burst
ansible/provision.yml # one playbook: apt + overlay + can0 up + code sync + tmux helpers
ansible/inventory.yml # reads IPs/user from environment (populated from .env)
cantest               # the only Mac-side script — your daily entry point
.env.example          # template for local config (copy to .env and fill in)

Two implementations

NODE_IMPL in .env selects which messenger runs on the Pis:

NODE_IMPL Runs Wire format Extra deps installed
plain node.py raw CAN frames, arbitration ID = priority none beyond python3-can
pycyphal node_pycyphal.py UAVCAN/Cyphal v1 over Classic CAN, uavcan.primitive.String.1.0 pycyphal venv + compiled DSDL

Both Pis must run the same implementation — a Cyphal publisher and a raw-frame listener cannot interoperate. To switch:

$EDITOR .env          # set NODE_IMPL=pycyphal (or plain)
./cantest up          # provisions deps for the new mode (idempotent, no-op when unchanged)
./cantest stop && ./cantest start

Configuration

All host-specific values live in a local-only .env (gitignored).

cp .env.example .env
$EDITOR .env       # set REMOTE_USER, EULER_IP, NEWTON_IP

Target OS

This project is tested and maintained for Ubuntu 24.04 LTS on Raspberry Pi 4B. The Ansible playbook is specifically tailored to handle the apt package dependencies and system configuration for this OS version.

First-time setup (Mac)

brew install ansible tmux
ansible-galaxy collection install ansible.posix community.general

# Passwordless SSH to both Pis:
ssh-copy-id "$REMOTE_USER@$EULER_IP"
ssh-copy-id "$REMOTE_USER@$NEWTON_IP"

./cantest sudoless    # one-time: enable passwordless sudo on both Pis
./cantest up          # full provision (apt, overlays, can0 up, deploy code)

EULER will reboot during the first up to activate the MCP2515 overlay. Re-run ./cantest up afterwards if the run was interrupted.

Daily use

./cantest start      # launches node.py inside a tmux session on both Pis
./cantest monitor    # split-pane local tmux — live output + type into either side
./cantest stop       # kills the program on both Pis

In monitor:

  • Left pane = EULER (allowed P0, P3)
  • Right pane = NEWTON (allowed P1, P2)
  • Ctrl-b o switches between panes.
  • Ctrl-b d detaches the local view (programs keep running on the Pis).

Message format

Type into stdin:

##P TEXT
  • P — priority digit, must be in that node's allowed set (0, 3 on EULER; 1, 2 on NEWTON)
  • TEXT — alphanumeric, ≤ 6 characters (limit comes from a single Classic-CAN data field: see Frame size below)

Examples:

##0 HI       # EULER, priority 0 (highest)
##3 BYE      # EULER, priority 3
##1 HELLO    # NEWTON
##2 OK       # NEWTON

Output

  • TX lines (sender) show no timestamp: TX P0 HI
  • RX lines (receiver) show a timestamp and the payload in cyan: [12:34:56] RX P0 HI
  • Priority tag color: P0 red, P1 orange, P2 yellow, P3 lime.

Other commands

./cantest sync                # only push node.py changes (no apt, no reboot)
./cantest canup               # only re-bring up can0 on both Pis
./cantest status              # is each remote tmux session alive?
./cantest logs                # split-pane tail -F of both Pis (read-only)
./cantest logs euler          # just one Pi's log
./cantest send euler "##0 HI" # inject one line remotely without attaching
./cantest monitor --rc        # restart can0 on both Pis before attaching
./cantest deploy-test         # push priority_test.py to both Pis + reset can0
./cantest priority-test       # run the arbitration test (no deploy)

Arbitration demo (priority-test)

priority_test.py is a small standalone script that demonstrates how lower arbitration IDs win the bus.

Sequence:

  1. EULER starts sending P3 frames every 50 ms.
  2. After 5 s, NEWTON starts sending P2 frames every 50 ms (P2 wins arbitration over P3, so EULER's TX rate visibly drops).
  3. After EULER receives 20 P2 frames, it sends a single P0 frame.
  4. NEWTON ends as soon as it sees that P0 (highest priority).

Run it:

./cantest deploy-test     # one-time per script change: scp + restart can0
./cantest priority-test   # run the test; output from both Pis is interleaved

Output is interleaved live from both nodes. Sent: lines are local TX, Recv: lines are RX. Note that the NEWTON payload prints as NEWTON_P because Classical CAN caps the data field at 8 bytes (see Frame size).

Wiring

Three wires between the EULER HAT screw terminals and the NEWTON UCAN screw terminals:

EULER (RS485 CAN HAT) NEWTON (UCAN USB-C)
CAN_H CAN_H
CAN_L CAN_L
GND GND

Termination: 120 Ω at each end (jumper on the HAT, switch/jumper on the UCAN). Bus speed: 1 Mbps.

Frame size

The TEXT field is capped at 6 alphanumeric characters in node.py, but the hard physical limit is the CAN data field:

Protocol Max payload per frame
Classic CAN (this project) 8 bytes
CAN FD up to 64 bytes

The MCP2515 chip on the Waveshare HAT only speaks Classic CAN, so 8 bytes is the ceiling here regardless of what the UCAN adapter would otherwise support. To send anything longer than 8 bytes you'd have to fragment in software (this is what UAVCAN/Cyphal's transfer protocol does on top of Classic CAN).

Smoke-test the bus (without node.py)

ssh "$REMOTE_USER@$EULER_IP"  "candump can0"             # listener
ssh "$REMOTE_USER@$NEWTON_IP" "cansend can0 123#DEADBEEF" # sender

The listener should print can0 123 [4] DE AD BE EF.

Troubleshooting

  • mcp251x ... didn't enter in conf mode after reset / err=-110 on EULER — the MCP2515 SPI clock is too fast for the HAT's traces. The playbook already sets spimaxfrequency=2000000. If you still see this, do a cold power cycle of EULER (full power off ≥ 10 s, then back on); a warm reboot is not enough.
  • can0 missing on NEWTON — the UCAN adapter isn't enumerating. Try a different USB port or a known-good USB-C data cable, then check lsusb | grep -i candle and dmesg | grep gs_usb.
  • Frames don't cross the bus — termination jumper missing on one end, or CAN_H/CAN_L swapped.
  • can0 is BUS-OFF / ERROR-PASSIVE (check with ip -details -statistics link show can0) — almost always a physical- layer problem: missing/incorrect 120 Ω termination, swapped H/L, or no shared GND. After fixing the wiring, restart the interface on both Pis with ./cantest monitor --rc (or manually sudo ip link set can0 down && sudo ip link set can0 up type can bitrate 1000000).
  • OSError: [Errno 105] No buffer space available when sending on Cyphal — the controller is in BUS-OFF/ERROR-PASSIVE, see above.
  • OSError: [Errno 22] Invalid argument when sending — payload is longer than 8 bytes. Classical CAN's data field is capped at 8 bytes; truncate or pad before calling bus.send().
  • ./cantest up fails on Ubuntu 24.04 with "held broken packages" — re-run with ./cantest up --tags bootstrap. The playbook installs build-essential and its known-conflicting dependencies (bzip2, zlib1g-dev) in a separate transaction first to avoid this.
  • ./cantest up keeps prompting for sudo — ./cantest sudoless was skipped or didn't take. Re-run it.
  • Wi-Fi stuck DORMANT after a router/SSID change — the saved netplan credentials no longer match. SSH in over Ethernet (use macOS Internet Sharing if the Pi has no network), edit /etc/netplan/50-cloud-init.yaml, then sudo netplan apply.

About

Open-source reference setup for two-node CAN on Raspberry Pi 4B (Waveshare RS485 CAN HAT + UCAN USB-C, 1 Mbps Classic CAN). Idempotent Ansible provisioning for Ubuntu 24.04 LTS, switchable raw python-can / PyCyphal (UAVCAN v1) implementations, live tmux monitor, and an arbitration-priority test — designed to be cloned and working in minutes.

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages