uc2canopen 0.1.4__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,103 @@
1
+ name: Publish to PyPI
2
+
3
+ # Trigger on every push to main — the workflow bumps the patch version,
4
+ # commits the change, tags it, builds the wheel/sdist, publishes to PyPI,
5
+ # and creates a GitHub Release automatically.
6
+ on:
7
+ push:
8
+ branches: [main]
9
+
10
+ jobs:
11
+ publish:
12
+ name: Bump version, build & publish
13
+ runs-on: ubuntu-latest
14
+
15
+ # Skip if this push was itself the version-bump commit to avoid
16
+ # an infinite trigger loop.
17
+ if: "!startsWith(github.event.head_commit.message, 'chore: bump version')"
18
+
19
+ permissions:
20
+ contents: write # push version-bump commit + tag
21
+ id-token: write # PyPI OIDC trusted publishing (no token needed)
22
+
23
+ steps:
24
+ # ── Checkout ────────────────────────────────────────────────────────────
25
+ - name: Checkout repository
26
+ uses: actions/checkout@v4
27
+ with:
28
+ fetch-depth: 0
29
+ # Use a PAT if you need the version-bump push to re-trigger other
30
+ # workflows; GITHUB_TOKEN is fine for just publishing to PyPI.
31
+ token: ${{ secrets.GITHUB_TOKEN }}
32
+
33
+ # ── Python + uv ─────────────────────────────────────────────────────────
34
+ - name: Set up Python
35
+ uses: actions/setup-python@v5
36
+ with:
37
+ python-version: "3.11"
38
+
39
+ - name: Install uv
40
+ uses: astral-sh/setup-uv@v5
41
+
42
+ # ── Bump patch version ──────────────────────────────────────────────────
43
+ - name: Bump patch version in pyproject.toml
44
+ id: bump
45
+ run: |
46
+ NEW_VERSION=$(python - << 'PYEOF'
47
+ import re, pathlib, sys
48
+
49
+ p = pathlib.Path("pyproject.toml")
50
+ text = p.read_text()
51
+
52
+ m = re.search(r'^(version\s*=\s*")(\d+)\.(\d+)\.(\d+)(")', text, re.MULTILINE)
53
+ if not m:
54
+ print("ERROR: version field not found in pyproject.toml", file=sys.stderr)
55
+ sys.exit(1)
56
+
57
+ major, minor, patch = int(m.group(2)), int(m.group(3)), int(m.group(4))
58
+ new_version = f"{major}.{minor}.{patch + 1}"
59
+
60
+ new_text = text[:m.start()] + m.group(1) + new_version + m.group(5) + text[m.end():]
61
+ p.write_text(new_text)
62
+
63
+ print(new_version)
64
+ PYEOF
65
+ )
66
+ echo "version=$NEW_VERSION" >> "$GITHUB_OUTPUT"
67
+ echo "Bumped to $NEW_VERSION"
68
+
69
+ # ── Commit & tag ────────────────────────────────────────────────────────
70
+ - name: Commit version bump and create tag
71
+ run: |
72
+ git config user.name "github-actions[bot]"
73
+ git config user.email "github-actions[bot]@users.noreply.github.com"
74
+ git add pyproject.toml
75
+ git commit -m "chore: bump version to ${{ steps.bump.outputs.version }}"
76
+ git tag "v${{ steps.bump.outputs.version }}"
77
+ git push origin main --follow-tags
78
+
79
+ # ── Build ───────────────────────────────────────────────────────────────
80
+ - name: Build wheel and sdist
81
+ run: uv build
82
+
83
+ # ── Publish to PyPI ─────────────────────────────────────────────────────
84
+ # Option A (recommended): PyPI OIDC Trusted Publishing — no secret needed.
85
+ # Set up once at https://pypi.org/manage/project/uc2canopen/settings/publishing/
86
+ # Add a trusted publisher: owner=openUC2, repo=UC2-REST-CANOPEN,
87
+ # workflow=publish.yml, env=(leave blank).
88
+ #
89
+ # Option B: Classic API token — add PYPI_TOKEN as a repository secret and
90
+ # uncomment the env block below, then remove the trusted-publishing flag.
91
+ - name: Publish to PyPI
92
+ run: uv publish --trusted-publishing always
93
+ # env:
94
+ # UV_PUBLISH_TOKEN: ${{ secrets.PYPI_TOKEN }}
95
+
96
+ # ── GitHub Release ──────────────────────────────────────────────────────
97
+ - name: Create GitHub Release
98
+ uses: softprops/action-gh-release@v2
99
+ with:
100
+ tag_name: "v${{ steps.bump.outputs.version }}"
101
+ name: "v${{ steps.bump.outputs.version }}"
102
+ files: dist/*
103
+ generate_release_notes: true
@@ -0,0 +1,3 @@
1
+ /src/uc2canopen/__pycache__
2
+ *.pyc
3
+ .DS_Store
@@ -0,0 +1,282 @@
1
+ Metadata-Version: 2.4
2
+ Name: uc2canopen
3
+ Version: 0.1.4
4
+ Summary: Python CANopen master for openUC2 microscope hardware — control motors, lasers, LEDs, galvos directly over CAN bus (SocketCAN MCP2515 HAT or Waveshare USB-CAN-A adapter)
5
+ Project-URL: Homepage, https://github.com/openUC2/uc2-esp32
6
+ Project-URL: Documentation, https://openuc2.github.io
7
+ Author-email: openUC2 GmbH <info@openuc2.com>
8
+ License: MIT
9
+ Requires-Python: >=3.10
10
+ Requires-Dist: pyserial>=3.5
11
+ Requires-Dist: python-can>=4.0
12
+ Provides-Extra: canopen
13
+ Requires-Dist: canopen>=2.0; extra == 'canopen'
14
+ Provides-Extra: dev
15
+ Requires-Dist: pytest; extra == 'dev'
16
+ Requires-Dist: ruff; extra == 'dev'
17
+ Description-Content-Type: text/markdown
18
+
19
+ # uc2canopen — Python CANopen master for openUC2
20
+
21
+ Control UC2 microscope motors, lasers, LEDs, and galvos directly over CAN bus
22
+ from Python — no ESP32 master required, no JSON-over-serial, just CANopen
23
+ SDO/PDO.
24
+
25
+ Two transports are supported:
26
+
27
+ - **MCP2515 SPI HAT (SocketCAN)** — recommended on a Raspberry Pi. The HAT
28
+ enumerates as a native `can0` interface. *This is the default.*
29
+ - **[Waveshare USB-CAN-A](https://www.waveshare.com/wiki/USB-CAN-A)** — a USB
30
+ dongle, handy on a laptop.
31
+
32
+ Same API shape as [UC2-REST](https://github.com/openUC2/UC2-REST), different
33
+ transport: CANopen instead of JSON-over-serial.
34
+
35
+ ---
36
+
37
+ ## 1. Raspberry Pi + MCP2515 HAT setup (one-time)
38
+
39
+ > Skip this section if you use the Waveshare USB adapter — see
40
+ > [Waveshare USB-CAN-A](#waveshare-usb-can-a-alternative) at the bottom.
41
+
42
+ The HAT is an **MCP2515** CAN controller on **SPI0** (CS = `SPI0_CE0`/GPIO8,
43
+ INT = GPIO12) with an **SN65HVD230** 3.3 V transceiver and a **12 MHz** crystal.
44
+
45
+ ### 1a. Enable SPI and load the MCP2515 driver
46
+
47
+ Edit `/boot/firmware/config.txt` and add:
48
+
49
+ ```ini
50
+ dtparam=spi=on
51
+ dtoverlay=mcp2515-can0,oscillator=12000000,interrupt=12,spimaxfrequency=10000000
52
+ ```
53
+
54
+ - `oscillator=12000000` **must match the crystal on the board** (12 MHz here).
55
+ A wrong value = `can0` comes up but never sees a frame.
56
+ - `interrupt=12` is the INT line wiring (GPIO12). If SPI is flaky, drop
57
+ `spimaxfrequency` to `2000000`.
58
+
59
+ Reboot, then confirm the driver bound:
60
+
61
+ ```bash
62
+ sudo reboot
63
+ # after it's back:
64
+ dmesg | grep -iE "mcp251|can0"
65
+ # → mcp251x spi0.0 can0: MCP2515 successfully initialized.
66
+ ```
67
+
68
+ ### 1b. Bring the interface up @ 500 kbit/s
69
+
70
+ ```bash
71
+ sudo ip link set can0 up type can bitrate 500000 restart-ms 100
72
+ ip -details link show can0 # state should be ERROR-ACTIVE
73
+ ```
74
+
75
+ 500 kbit/s is the UC2 firmware bitrate — it **must match** your nodes.
76
+ `restart-ms 100` auto-recovers from bus-off while debugging.
77
+
78
+ ### 1c. Sanity-check the bus (optional but recommended)
79
+
80
+ ```bash
81
+ sudo apt install -y can-utils
82
+ candump can0
83
+ ```
84
+
85
+ You should see live traffic — CANopen heartbeats at `0x700 + node-ID`
86
+ (e.g. `0x70B` = node 11) and motor TPDOs at `0x180 + node-ID`. If frames
87
+ scroll, the whole chain works. Send an NMT "start all":
88
+
89
+ ```bash
90
+ cansend can0 000#0100
91
+ ```
92
+
93
+ ### 1d. Make `can0` come up at boot (optional)
94
+
95
+ Create `/etc/systemd/system/can0.service`:
96
+
97
+ ```ini
98
+ [Unit]
99
+ Description=Bring up can0 (MCP2515)
100
+ After=sys-subsystem-net-devices-can0.device
101
+ BindsTo=sys-subsystem-net-devices-can0.device
102
+
103
+ [Service]
104
+ Type=oneshot
105
+ RemainAfterExit=yes
106
+ ExecStart=/sbin/ip link set can0 up type can bitrate 500000 restart-ms 100
107
+ ExecStop=/sbin/ip link set can0 down
108
+
109
+ [Install]
110
+ WantedBy=multi-user.target
111
+ ```
112
+
113
+ ```bash
114
+ sudo systemctl enable --now can0.service
115
+ ```
116
+
117
+ ---
118
+
119
+ ## 2. Install the Python package
120
+
121
+ ```bash
122
+ # with uv (recommended)
123
+ uv pip install -e .
124
+
125
+ # or with pip
126
+ pip install -e .
127
+ ```
128
+
129
+ `python-can`'s SocketCAN backend is built in on Linux — nothing extra to
130
+ install for the HAT. (`pyserial` is pulled in too, only needed for the
131
+ Waveshare adapter.)
132
+
133
+ ```bash
134
+ pip install uc2canopen
135
+ ```
136
+ ---
137
+
138
+ ## 3. Run the demo
139
+
140
+ With `can0` up (step 1b), from the repo root:
141
+
142
+ ```bash
143
+ python src/motor_demo.py --motor-node 11
144
+ ```
145
+
146
+ This scans for nodes, jogs motor node 11 back and forth, blinks the laser/LED
147
+ board, and prints node status. Useful flags: `--skip-laser --skip-led`,
148
+ `--steps`, `--speed`, `--channel can1`.
149
+
150
+ ---
151
+
152
+ ## 4. Python API
153
+
154
+ ```python
155
+ from uc2canopen import UC2Client, NODE
156
+
157
+ # Defaults to SocketCAN can0 (the MCP2515 HAT) — bring it up first with
158
+ # sudo ip link set can0 up type can bitrate 500000
159
+ uc2 = UC2Client()
160
+ # Other channel: UC2Client(channel="can1")
161
+ # Waveshare USB adapter: UC2Client(port="/dev/ttyUSB0")
162
+
163
+ # Move motor X axis 1000 steps
164
+ uc2.motor.move(axis=0, position=1000, speed=20000, node_id=NODE.MOT_X)
165
+ uc2.motor.wait_for_idle(axis=0, node_id=NODE.MOT_X)
166
+ print(f"Motor at {uc2.motor.get_position(axis=0, node_id=NODE.MOT_X)} steps")
167
+
168
+ uc2.laser.set_value(channel=0, pwm=512, node_id=NODE.LASER_0) # laser 50%
169
+ uc2.led.fill(r=255, g=0, b=0, node_id=NODE.LED_0) # LEDs red
170
+ print(f"Uptime: {uc2.state.get_uptime(NODE.MOT_X)}s")
171
+
172
+ uc2.close()
173
+ ```
174
+
175
+ ## 5. CLI
176
+
177
+ The `uc2can` command also defaults to SocketCAN `can0`:
178
+
179
+ ```bash
180
+ uc2can scan # find nodes on the bus
181
+ uc2can move --node 11 --pos 1000 --speed 20000 --wait
182
+ uc2can laser --node 20 --ch 0 --pwm 512
183
+ uc2can led --node 20 --r 255 --g 0 --b_val 0
184
+ uc2can status --node 11
185
+ uc2can sniff # dump raw CAN frames
186
+ uc2can reboot --node 11
187
+
188
+ # pick a transport/channel (global flags, before the subcommand):
189
+ uc2can --channel can1 scan
190
+ uc2can --interface waveshare --port /dev/ttyUSB0 scan
191
+ ```
192
+
193
+ ---
194
+
195
+ ## Node-ID assignments
196
+
197
+ | Role | Node-ID | Python constant |
198
+ |------|---------|-----------------|
199
+ | Master (this script) | 1 | `NODE.MASTER` |
200
+ | Motor X | 11 | `NODE.MOT_X` |
201
+ | Motor Y | 12 | `NODE.MOT_Y` |
202
+ | Motor Z | 13 | `NODE.MOT_Z` |
203
+ | Motor A | 14 | `NODE.MOT_A` |
204
+ | LED / combined illum. board | 20 | `NODE.LED` (alias: `LED_0`, `LASER_0`) |
205
+ | Laser (separate board) | 21 | `NODE.LASER` (alias: `LASER_1`) |
206
+ | Joystick | 22 | `NODE.JOYSTICK` |
207
+ | Galvo | 30 | `NODE.GALVO` |
208
+ | Galvo 2 | 31 | `NODE.GALVO_2` |
209
+ | Encoder | 40 | `NODE.ENCODER` |
210
+ | PID | 50 | `NODE.PID` |
211
+
212
+ ## Architecture
213
+
214
+ ```
215
+ Your Python script
216
+
217
+
218
+ UC2Client
219
+ ├── motor → Motor() SDO writes to 0x2000-0x200B
220
+ ├── laser → Laser() SDO writes to 0x2100
221
+ ├── led → Led() SDO writes to 0x2200
222
+ ├── state → State() SDO reads from 0x2500+
223
+
224
+ ├── SdoClient raw SDO upload/download over python-can
225
+ ├── TpdoListener background thread for motor state (TPDO1)
226
+
227
+ └── python-can BusABC ── SocketCAN can0 (MCP2515 HAT) ← default
228
+ └─ WaveshareBus (USB-CAN-A serial)
229
+
230
+ ▼ CAN bus @ 500 kbit/s
231
+
232
+ ┌───┴───┬───────┬────────┐
233
+ │ │ │ │
234
+ Slave Slave Slave Slave
235
+ node11 node12 node13 node20
236
+ (mot X) (mot Y) (mot Z) (illum)
237
+ ```
238
+
239
+ The whole SDO/PDO stack is transport-agnostic (it talks to a python-can
240
+ `BusABC`), so the only difference between transports is which bus object
241
+ `UC2Client` builds.
242
+
243
+ ## OD index reference
244
+
245
+ All indices are in `uc2canopen.od.OD` and match the firmware's
246
+ `UC2_OD_Indices.h` (generated from `uc2_canopen_registry.yaml`).
247
+
248
+ ## Troubleshooting
249
+
250
+ | Symptom | Most likely cause |
251
+ |---|---|
252
+ | No `can0`; `dmesg` empty/error | SPI not enabled, wrong `interrupt=`, or SPI wiring |
253
+ | `can0` up but `candump` empty | Wrong `oscillator=` (crystal), bitrate ≠ 500 k, or missing 120 Ω termination |
254
+ | `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 |
255
+ | `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` |
256
+ | Error frames / bus-off | Bitrate mismatch, CANH↔CANL swapped, or no common GND |
257
+
258
+ ## Waveshare USB-CAN-A (alternative)
259
+
260
+ No driver setup needed — plug it in and select it explicitly:
261
+
262
+ ```bash
263
+ python src/motor_demo.py --interface waveshare --port /dev/ttyUSB0 --motor-node 11
264
+ uc2can --interface waveshare scan # auto-detects the port
265
+ ```
266
+
267
+ ```python
268
+ uc2 = UC2Client(port="/dev/ttyUSB0") # a port implies the Waveshare transport
269
+ ```
270
+
271
+ ## Requirements
272
+
273
+ - Python ≥ 3.10
274
+ - `python-can` ≥ 4.0 (SocketCAN backend built in on Linux)
275
+ - `pyserial` (only for the Waveshare adapter)
276
+ - Hardware: an MCP2515 SPI HAT **or** a Waveshare USB-CAN-A adapter, plus UC2 slave(s) on the bus
277
+
278
+ ## License
279
+
280
+ MIT — same as the UC2-ESP32 firmware.
281
+ </content>
282
+ </invoke>
@@ -0,0 +1,264 @@
1
+ # uc2canopen — Python CANopen master for openUC2
2
+
3
+ Control UC2 microscope motors, lasers, LEDs, and galvos directly over CAN bus
4
+ from Python — no ESP32 master required, no JSON-over-serial, just CANopen
5
+ SDO/PDO.
6
+
7
+ Two transports are supported:
8
+
9
+ - **MCP2515 SPI HAT (SocketCAN)** — recommended on a Raspberry Pi. The HAT
10
+ enumerates as a native `can0` interface. *This is the default.*
11
+ - **[Waveshare USB-CAN-A](https://www.waveshare.com/wiki/USB-CAN-A)** — a USB
12
+ dongle, handy on a laptop.
13
+
14
+ Same API shape as [UC2-REST](https://github.com/openUC2/UC2-REST), different
15
+ transport: CANopen instead of JSON-over-serial.
16
+
17
+ ---
18
+
19
+ ## 1. Raspberry Pi + MCP2515 HAT setup (one-time)
20
+
21
+ > Skip this section if you use the Waveshare USB adapter — see
22
+ > [Waveshare USB-CAN-A](#waveshare-usb-can-a-alternative) at the bottom.
23
+
24
+ The HAT is an **MCP2515** CAN controller on **SPI0** (CS = `SPI0_CE0`/GPIO8,
25
+ INT = GPIO12) with an **SN65HVD230** 3.3 V transceiver and a **12 MHz** crystal.
26
+
27
+ ### 1a. Enable SPI and load the MCP2515 driver
28
+
29
+ Edit `/boot/firmware/config.txt` and add:
30
+
31
+ ```ini
32
+ dtparam=spi=on
33
+ dtoverlay=mcp2515-can0,oscillator=12000000,interrupt=12,spimaxfrequency=10000000
34
+ ```
35
+
36
+ - `oscillator=12000000` **must match the crystal on the board** (12 MHz here).
37
+ A wrong value = `can0` comes up but never sees a frame.
38
+ - `interrupt=12` is the INT line wiring (GPIO12). If SPI is flaky, drop
39
+ `spimaxfrequency` to `2000000`.
40
+
41
+ Reboot, then confirm the driver bound:
42
+
43
+ ```bash
44
+ sudo reboot
45
+ # after it's back:
46
+ dmesg | grep -iE "mcp251|can0"
47
+ # → mcp251x spi0.0 can0: MCP2515 successfully initialized.
48
+ ```
49
+
50
+ ### 1b. Bring the interface up @ 500 kbit/s
51
+
52
+ ```bash
53
+ sudo ip link set can0 up type can bitrate 500000 restart-ms 100
54
+ ip -details link show can0 # state should be ERROR-ACTIVE
55
+ ```
56
+
57
+ 500 kbit/s is the UC2 firmware bitrate — it **must match** your nodes.
58
+ `restart-ms 100` auto-recovers from bus-off while debugging.
59
+
60
+ ### 1c. Sanity-check the bus (optional but recommended)
61
+
62
+ ```bash
63
+ sudo apt install -y can-utils
64
+ candump can0
65
+ ```
66
+
67
+ You should see live traffic — CANopen heartbeats at `0x700 + node-ID`
68
+ (e.g. `0x70B` = node 11) and motor TPDOs at `0x180 + node-ID`. If frames
69
+ scroll, the whole chain works. Send an NMT "start all":
70
+
71
+ ```bash
72
+ cansend can0 000#0100
73
+ ```
74
+
75
+ ### 1d. Make `can0` come up at boot (optional)
76
+
77
+ Create `/etc/systemd/system/can0.service`:
78
+
79
+ ```ini
80
+ [Unit]
81
+ Description=Bring up can0 (MCP2515)
82
+ After=sys-subsystem-net-devices-can0.device
83
+ BindsTo=sys-subsystem-net-devices-can0.device
84
+
85
+ [Service]
86
+ Type=oneshot
87
+ RemainAfterExit=yes
88
+ ExecStart=/sbin/ip link set can0 up type can bitrate 500000 restart-ms 100
89
+ ExecStop=/sbin/ip link set can0 down
90
+
91
+ [Install]
92
+ WantedBy=multi-user.target
93
+ ```
94
+
95
+ ```bash
96
+ sudo systemctl enable --now can0.service
97
+ ```
98
+
99
+ ---
100
+
101
+ ## 2. Install the Python package
102
+
103
+ ```bash
104
+ # with uv (recommended)
105
+ uv pip install -e .
106
+
107
+ # or with pip
108
+ pip install -e .
109
+ ```
110
+
111
+ `python-can`'s SocketCAN backend is built in on Linux — nothing extra to
112
+ install for the HAT. (`pyserial` is pulled in too, only needed for the
113
+ Waveshare adapter.)
114
+
115
+ ```bash
116
+ pip install uc2canopen
117
+ ```
118
+ ---
119
+
120
+ ## 3. Run the demo
121
+
122
+ With `can0` up (step 1b), from the repo root:
123
+
124
+ ```bash
125
+ python src/motor_demo.py --motor-node 11
126
+ ```
127
+
128
+ This scans for nodes, jogs motor node 11 back and forth, blinks the laser/LED
129
+ board, and prints node status. Useful flags: `--skip-laser --skip-led`,
130
+ `--steps`, `--speed`, `--channel can1`.
131
+
132
+ ---
133
+
134
+ ## 4. Python API
135
+
136
+ ```python
137
+ from uc2canopen import UC2Client, NODE
138
+
139
+ # Defaults to SocketCAN can0 (the MCP2515 HAT) — bring it up first with
140
+ # sudo ip link set can0 up type can bitrate 500000
141
+ uc2 = UC2Client()
142
+ # Other channel: UC2Client(channel="can1")
143
+ # Waveshare USB adapter: UC2Client(port="/dev/ttyUSB0")
144
+
145
+ # Move motor X axis 1000 steps
146
+ uc2.motor.move(axis=0, position=1000, speed=20000, node_id=NODE.MOT_X)
147
+ uc2.motor.wait_for_idle(axis=0, node_id=NODE.MOT_X)
148
+ print(f"Motor at {uc2.motor.get_position(axis=0, node_id=NODE.MOT_X)} steps")
149
+
150
+ uc2.laser.set_value(channel=0, pwm=512, node_id=NODE.LASER_0) # laser 50%
151
+ uc2.led.fill(r=255, g=0, b=0, node_id=NODE.LED_0) # LEDs red
152
+ print(f"Uptime: {uc2.state.get_uptime(NODE.MOT_X)}s")
153
+
154
+ uc2.close()
155
+ ```
156
+
157
+ ## 5. CLI
158
+
159
+ The `uc2can` command also defaults to SocketCAN `can0`:
160
+
161
+ ```bash
162
+ uc2can scan # find nodes on the bus
163
+ uc2can move --node 11 --pos 1000 --speed 20000 --wait
164
+ uc2can laser --node 20 --ch 0 --pwm 512
165
+ uc2can led --node 20 --r 255 --g 0 --b_val 0
166
+ uc2can status --node 11
167
+ uc2can sniff # dump raw CAN frames
168
+ uc2can reboot --node 11
169
+
170
+ # pick a transport/channel (global flags, before the subcommand):
171
+ uc2can --channel can1 scan
172
+ uc2can --interface waveshare --port /dev/ttyUSB0 scan
173
+ ```
174
+
175
+ ---
176
+
177
+ ## Node-ID assignments
178
+
179
+ | Role | Node-ID | Python constant |
180
+ |------|---------|-----------------|
181
+ | Master (this script) | 1 | `NODE.MASTER` |
182
+ | Motor X | 11 | `NODE.MOT_X` |
183
+ | Motor Y | 12 | `NODE.MOT_Y` |
184
+ | Motor Z | 13 | `NODE.MOT_Z` |
185
+ | Motor A | 14 | `NODE.MOT_A` |
186
+ | LED / combined illum. board | 20 | `NODE.LED` (alias: `LED_0`, `LASER_0`) |
187
+ | Laser (separate board) | 21 | `NODE.LASER` (alias: `LASER_1`) |
188
+ | Joystick | 22 | `NODE.JOYSTICK` |
189
+ | Galvo | 30 | `NODE.GALVO` |
190
+ | Galvo 2 | 31 | `NODE.GALVO_2` |
191
+ | Encoder | 40 | `NODE.ENCODER` |
192
+ | PID | 50 | `NODE.PID` |
193
+
194
+ ## Architecture
195
+
196
+ ```
197
+ Your Python script
198
+
199
+
200
+ UC2Client
201
+ ├── motor → Motor() SDO writes to 0x2000-0x200B
202
+ ├── laser → Laser() SDO writes to 0x2100
203
+ ├── led → Led() SDO writes to 0x2200
204
+ ├── state → State() SDO reads from 0x2500+
205
+
206
+ ├── SdoClient raw SDO upload/download over python-can
207
+ ├── TpdoListener background thread for motor state (TPDO1)
208
+
209
+ └── python-can BusABC ── SocketCAN can0 (MCP2515 HAT) ← default
210
+ └─ WaveshareBus (USB-CAN-A serial)
211
+
212
+ ▼ CAN bus @ 500 kbit/s
213
+
214
+ ┌───┴───┬───────┬────────┐
215
+ │ │ │ │
216
+ Slave Slave Slave Slave
217
+ node11 node12 node13 node20
218
+ (mot X) (mot Y) (mot Z) (illum)
219
+ ```
220
+
221
+ The whole SDO/PDO stack is transport-agnostic (it talks to a python-can
222
+ `BusABC`), so the only difference between transports is which bus object
223
+ `UC2Client` builds.
224
+
225
+ ## OD index reference
226
+
227
+ All indices are in `uc2canopen.od.OD` and match the firmware's
228
+ `UC2_OD_Indices.h` (generated from `uc2_canopen_registry.yaml`).
229
+
230
+ ## Troubleshooting
231
+
232
+ | Symptom | Most likely cause |
233
+ |---|---|
234
+ | No `can0`; `dmesg` empty/error | SPI not enabled, wrong `interrupt=`, or SPI wiring |
235
+ | `can0` up but `candump` empty | Wrong `oscillator=` (crystal), bitrate ≠ 500 k, or missing 120 Ω termination |
236
+ | `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 |
237
+ | `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` |
238
+ | Error frames / bus-off | Bitrate mismatch, CANH↔CANL swapped, or no common GND |
239
+
240
+ ## Waveshare USB-CAN-A (alternative)
241
+
242
+ No driver setup needed — plug it in and select it explicitly:
243
+
244
+ ```bash
245
+ python src/motor_demo.py --interface waveshare --port /dev/ttyUSB0 --motor-node 11
246
+ uc2can --interface waveshare scan # auto-detects the port
247
+ ```
248
+
249
+ ```python
250
+ uc2 = UC2Client(port="/dev/ttyUSB0") # a port implies the Waveshare transport
251
+ ```
252
+
253
+ ## Requirements
254
+
255
+ - Python ≥ 3.10
256
+ - `python-can` ≥ 4.0 (SocketCAN backend built in on Linux)
257
+ - `pyserial` (only for the Waveshare adapter)
258
+ - Hardware: an MCP2515 SPI HAT **or** a Waveshare USB-CAN-A adapter, plus UC2 slave(s) on the bus
259
+
260
+ ## License
261
+
262
+ MIT — same as the UC2-ESP32 firmware.
263
+ </content>
264
+ </invoke>