uc2canopen 0.1.4__py3-none-any.whl
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.
- uc2canopen/__init__.py +36 -0
- uc2canopen/cli.py +241 -0
- uc2canopen/client.py +492 -0
- uc2canopen/od.py +271 -0
- uc2canopen/sdo.py +295 -0
- uc2canopen/waveshare_bus.py +330 -0
- uc2canopen-0.1.4.dist-info/METADATA +282 -0
- uc2canopen-0.1.4.dist-info/RECORD +10 -0
- uc2canopen-0.1.4.dist-info/WHEEL +4 -0
- uc2canopen-0.1.4.dist-info/entry_points.txt +2 -0
uc2canopen/__init__.py
ADDED
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
"""
|
|
2
|
+
uc2canopen — Python CANopen master for openUC2 microscope hardware.
|
|
3
|
+
|
|
4
|
+
Quick start:
|
|
5
|
+
from uc2canopen import UC2Client, NODE
|
|
6
|
+
|
|
7
|
+
uc2 = UC2Client(port="/dev/ttyUSB0")
|
|
8
|
+
uc2.motor.move(axis=0, position=1000, speed=20000, node_id=NODE.MOT_X)
|
|
9
|
+
uc2.motor.wait_for_idle(axis=0, node_id=NODE.MOT_X)
|
|
10
|
+
uc2.laser.set_value(channel=0, pwm=512, node_id=NODE.LASER_0)
|
|
11
|
+
uc2.led.fill(r=255, g=0, b=0, node_id=NODE.LED_0)
|
|
12
|
+
uc2.close()
|
|
13
|
+
"""
|
|
14
|
+
|
|
15
|
+
import logging
|
|
16
|
+
|
|
17
|
+
# Library best practice: attach a NullHandler so log records are silently
|
|
18
|
+
# discarded unless the application configures a handler.
|
|
19
|
+
logging.getLogger(__name__).addHandler(logging.NullHandler())
|
|
20
|
+
|
|
21
|
+
from .client import UC2Client
|
|
22
|
+
from .od import OD, NODE
|
|
23
|
+
from .sdo import SdoClient, SdoError
|
|
24
|
+
from .waveshare_bus import WaveshareBus, find_waveshare_port
|
|
25
|
+
|
|
26
|
+
__all__ = [
|
|
27
|
+
"UC2Client",
|
|
28
|
+
"OD",
|
|
29
|
+
"NODE",
|
|
30
|
+
"SdoClient",
|
|
31
|
+
"SdoError",
|
|
32
|
+
"WaveshareBus",
|
|
33
|
+
"find_waveshare_port",
|
|
34
|
+
]
|
|
35
|
+
|
|
36
|
+
__version__ = "0.1.0"
|
uc2canopen/cli.py
ADDED
|
@@ -0,0 +1,241 @@
|
|
|
1
|
+
"""
|
|
2
|
+
uc2can CLI — command-line tool for UC2 CANopen control.
|
|
3
|
+
|
|
4
|
+
Usage:
|
|
5
|
+
uc2can scan # find nodes on the bus
|
|
6
|
+
uc2can move --node 10 --pos 1000 # move motor
|
|
7
|
+
uc2can laser --node 20 --ch 0 --pwm 512
|
|
8
|
+
uc2can led --node 20 --r 255 --g 0 --b 0
|
|
9
|
+
uc2can status --node 10 # read uptime, position
|
|
10
|
+
uc2can sniff # dump raw CAN frames
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
from __future__ import annotations
|
|
14
|
+
|
|
15
|
+
import argparse
|
|
16
|
+
import struct
|
|
17
|
+
import sys
|
|
18
|
+
import time
|
|
19
|
+
|
|
20
|
+
import can
|
|
21
|
+
|
|
22
|
+
from .client import UC2Client
|
|
23
|
+
from .od import NODE
|
|
24
|
+
from .waveshare_bus import WaveshareBus, find_waveshare_port
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def _client(args) -> UC2Client:
|
|
28
|
+
"""Build a UC2Client from the shared --interface/--channel/--port options."""
|
|
29
|
+
return UC2Client(
|
|
30
|
+
interface=args.interface,
|
|
31
|
+
channel=args.channel,
|
|
32
|
+
port=args.port,
|
|
33
|
+
bitrate=args.bitrate,
|
|
34
|
+
)
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def cmd_scan(args):
|
|
38
|
+
with _client(args) as uc2:
|
|
39
|
+
print(f"Scanning for nodes ({args.timeout}s)...")
|
|
40
|
+
nodes = uc2.state.scan_nodes(timeout=args.timeout)
|
|
41
|
+
if nodes:
|
|
42
|
+
print(f"Found {len(nodes)} node(s): {nodes}")
|
|
43
|
+
else:
|
|
44
|
+
print("No nodes found. Check CAN wiring and bitrate.")
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def cmd_move(args):
|
|
48
|
+
with _client(args) as uc2:
|
|
49
|
+
print(f"Moving motor: node={args.node} axis={args.axis} "
|
|
50
|
+
f"pos={args.pos} speed={args.speed} abs={args.abs_}")
|
|
51
|
+
uc2.motor.move(
|
|
52
|
+
axis=args.axis,
|
|
53
|
+
position=args.pos,
|
|
54
|
+
speed=args.speed,
|
|
55
|
+
is_absolute=args.abs_,
|
|
56
|
+
node_id=args.node,
|
|
57
|
+
)
|
|
58
|
+
if args.wait:
|
|
59
|
+
print("Waiting for motor to finish...")
|
|
60
|
+
ok = uc2.motor.wait_for_idle(args.axis, args.node, timeout=args.timeout)
|
|
61
|
+
pos = uc2.motor.get_position(args.axis, args.node)
|
|
62
|
+
print(f"{'Done' if ok else 'Timeout'}. Position: {pos}")
|
|
63
|
+
else:
|
|
64
|
+
print("Command sent. Use --wait to block until done.")
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
def cmd_stop(args):
|
|
68
|
+
with _client(args) as uc2:
|
|
69
|
+
uc2.motor.stop(axis=args.axis, node_id=args.node)
|
|
70
|
+
print(f"Stop sent to node {args.node} axis {args.axis}")
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
def cmd_home(args):
|
|
74
|
+
with _client(args) as uc2:
|
|
75
|
+
print(f"Homing: node={args.node} axis={args.axis}")
|
|
76
|
+
uc2.motor.home(
|
|
77
|
+
axis=args.axis,
|
|
78
|
+
speed=args.speed,
|
|
79
|
+
direction=args.direction,
|
|
80
|
+
node_id=args.node,
|
|
81
|
+
)
|
|
82
|
+
if args.wait:
|
|
83
|
+
ok = uc2.motor.wait_for_idle(args.axis, args.node, timeout=args.timeout)
|
|
84
|
+
print(f"{'Homed' if ok else 'Timeout'}")
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
def cmd_laser(args):
|
|
88
|
+
with _client(args) as uc2:
|
|
89
|
+
uc2.laser.set_value(channel=args.ch, pwm=args.pwm, node_id=args.node)
|
|
90
|
+
print(f"Laser ch={args.ch} pwm={args.pwm} on node {args.node}")
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
def cmd_led(args):
|
|
94
|
+
with _client(args) as uc2:
|
|
95
|
+
if args.off:
|
|
96
|
+
uc2.led.off(node_id=args.node)
|
|
97
|
+
print(f"LED off on node {args.node}")
|
|
98
|
+
else:
|
|
99
|
+
uc2.led.fill(r=args.r, g=args.g, b=args.b, node_id=args.node)
|
|
100
|
+
print(f"LED fill ({args.r},{args.g},{args.b}) on node {args.node}")
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
def cmd_status(args):
|
|
104
|
+
with _client(args) as uc2:
|
|
105
|
+
try:
|
|
106
|
+
uptime = uc2.state.get_uptime(args.node)
|
|
107
|
+
heap = uc2.state.get_free_heap(args.node)
|
|
108
|
+
pos = uc2.motor.get_position(axis=0, node_id=args.node)
|
|
109
|
+
running = uc2.motor.is_running(axis=0, node_id=args.node)
|
|
110
|
+
print(f"Node {args.node}:")
|
|
111
|
+
print(f" Uptime: {uptime}s")
|
|
112
|
+
print(f" Free heap: {heap} bytes")
|
|
113
|
+
print(f" Motor pos: {pos} steps")
|
|
114
|
+
print(f" Running: {running}")
|
|
115
|
+
except Exception as e:
|
|
116
|
+
print(f"Error reading node {args.node}: {e}")
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
def cmd_sniff(args):
|
|
120
|
+
iface = args.interface or ("waveshare" if args.port else "socketcan")
|
|
121
|
+
if iface == "socketcan":
|
|
122
|
+
bus = can.interface.Bus(interface="socketcan", channel=args.channel)
|
|
123
|
+
where = args.channel
|
|
124
|
+
else:
|
|
125
|
+
port = args.port or find_waveshare_port()
|
|
126
|
+
if not port:
|
|
127
|
+
print("No Waveshare adapter found.", file=sys.stderr)
|
|
128
|
+
sys.exit(1)
|
|
129
|
+
bus = WaveshareBus(channel=port, bitrate=args.bitrate)
|
|
130
|
+
where = port
|
|
131
|
+
print(f"Sniffing CAN bus on {where}. Ctrl+C to stop.\n")
|
|
132
|
+
|
|
133
|
+
FC_NAMES = {
|
|
134
|
+
0x000: "NMT", 0x080: "SYNC", 0x180: "TPDO1", 0x200: "RPDO1",
|
|
135
|
+
0x280: "TPDO2", 0x300: "RPDO2", 0x380: "TPDO3", 0x400: "RPDO3",
|
|
136
|
+
0x580: "SDO↑", 0x600: "SDO↓", 0x700: "HB",
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
try:
|
|
140
|
+
while True:
|
|
141
|
+
msg = bus.recv(timeout=1.0)
|
|
142
|
+
if msg is None:
|
|
143
|
+
continue
|
|
144
|
+
fc = msg.arbitration_id & 0x780
|
|
145
|
+
nid = msg.arbitration_id & 0x07F
|
|
146
|
+
name = FC_NAMES.get(fc, f"0x{fc:03X}")
|
|
147
|
+
data_hex = " ".join(f"{b:02X}" for b in msg.data)
|
|
148
|
+
print(f"[{msg.timestamp:.3f}] 0x{msg.arbitration_id:03X} "
|
|
149
|
+
f"{name:6s} n={nid:3d} [{msg.dlc}] {data_hex}")
|
|
150
|
+
except KeyboardInterrupt:
|
|
151
|
+
print("\nStopped.")
|
|
152
|
+
finally:
|
|
153
|
+
bus.shutdown()
|
|
154
|
+
|
|
155
|
+
|
|
156
|
+
def cmd_reboot(args):
|
|
157
|
+
with _client(args) as uc2:
|
|
158
|
+
uc2.state.reboot(args.node)
|
|
159
|
+
print(f"Reboot command sent to node {args.node}")
|
|
160
|
+
|
|
161
|
+
|
|
162
|
+
def main():
|
|
163
|
+
p = argparse.ArgumentParser(
|
|
164
|
+
prog="uc2can",
|
|
165
|
+
description="UC2 CANopen command-line tool — control motors, lasers, LEDs over CAN",
|
|
166
|
+
)
|
|
167
|
+
p.add_argument("--interface", "-i", default=None, choices=["socketcan", "waveshare"],
|
|
168
|
+
help="CAN transport. Default: socketcan (MCP2515 HAT), or waveshare if --port is given.")
|
|
169
|
+
p.add_argument("--channel", "-c", default="can0",
|
|
170
|
+
help="SocketCAN interface name (default can0; used with --interface socketcan)")
|
|
171
|
+
p.add_argument("--port", "-p", default=None,
|
|
172
|
+
help="Waveshare serial port (implies --interface waveshare; auto-detected)")
|
|
173
|
+
p.add_argument("--bitrate", "-b", type=int, default=500_000, help="CAN bitrate (default 500k)")
|
|
174
|
+
|
|
175
|
+
sub = p.add_subparsers(dest="cmd", required=True)
|
|
176
|
+
|
|
177
|
+
# scan
|
|
178
|
+
s = sub.add_parser("scan", help="Scan for nodes on the CAN bus")
|
|
179
|
+
s.add_argument("--timeout", type=float, default=3.0)
|
|
180
|
+
|
|
181
|
+
# move
|
|
182
|
+
s = sub.add_parser("move", help="Move a motor axis")
|
|
183
|
+
s.add_argument("--node", "-n", type=int, default=NODE.MOT_X, help=f"Node ID (default {NODE.MOT_X})")
|
|
184
|
+
s.add_argument("--axis", "-a", type=int, default=0, help="Axis index (default 0)")
|
|
185
|
+
s.add_argument("--pos", type=int, required=True, help="Target position (steps)")
|
|
186
|
+
s.add_argument("--speed", type=int, default=20000, help="Speed (steps/s, default 20000)")
|
|
187
|
+
s.add_argument("--abs", dest="abs_", action="store_true", help="Absolute move (default: relative)")
|
|
188
|
+
s.add_argument("--wait", "-w", action="store_true", help="Wait for motor to finish")
|
|
189
|
+
s.add_argument("--timeout", type=float, default=30.0, help="Wait timeout (s)")
|
|
190
|
+
|
|
191
|
+
# stop
|
|
192
|
+
s = sub.add_parser("stop", help="Stop a motor axis")
|
|
193
|
+
s.add_argument("--node", "-n", type=int, default=NODE.MOT_X)
|
|
194
|
+
s.add_argument("--axis", "-a", type=int, default=0)
|
|
195
|
+
|
|
196
|
+
# home
|
|
197
|
+
s = sub.add_parser("home", help="Home a motor axis")
|
|
198
|
+
s.add_argument("--node", "-n", type=int, default=NODE.MOT_X)
|
|
199
|
+
s.add_argument("--axis", "-a", type=int, default=0)
|
|
200
|
+
s.add_argument("--speed", type=int, default=15000)
|
|
201
|
+
s.add_argument("--direction", type=int, default=-1, help="-1 or +1")
|
|
202
|
+
s.add_argument("--wait", "-w", action="store_true")
|
|
203
|
+
s.add_argument("--timeout", type=float, default=30.0)
|
|
204
|
+
|
|
205
|
+
# laser
|
|
206
|
+
s = sub.add_parser("laser", help="Set laser PWM")
|
|
207
|
+
s.add_argument("--node", "-n", type=int, default=NODE.LASER_0)
|
|
208
|
+
s.add_argument("--ch", type=int, default=0, help="Laser channel (0-3)")
|
|
209
|
+
s.add_argument("--pwm", type=int, required=True, help="PWM value (0=off, up to 1023)")
|
|
210
|
+
|
|
211
|
+
# led
|
|
212
|
+
s = sub.add_parser("led", help="Set LED color")
|
|
213
|
+
s.add_argument("--node", "-n", type=int, default=NODE.LED_0)
|
|
214
|
+
s.add_argument("--r", type=int, default=0)
|
|
215
|
+
s.add_argument("--g", type=int, default=0)
|
|
216
|
+
s.add_argument("--b_val", dest="b", type=int, default=0)
|
|
217
|
+
s.add_argument("--off", action="store_true", help="Turn LEDs off")
|
|
218
|
+
|
|
219
|
+
# status
|
|
220
|
+
s = sub.add_parser("status", help="Read node status")
|
|
221
|
+
s.add_argument("--node", "-n", type=int, required=True)
|
|
222
|
+
|
|
223
|
+
# sniff
|
|
224
|
+
sub.add_parser("sniff", help="Dump raw CAN frames")
|
|
225
|
+
|
|
226
|
+
# reboot
|
|
227
|
+
s = sub.add_parser("reboot", help="Reboot a node")
|
|
228
|
+
s.add_argument("--node", "-n", type=int, required=True)
|
|
229
|
+
|
|
230
|
+
args = p.parse_args()
|
|
231
|
+
|
|
232
|
+
handlers = {
|
|
233
|
+
"scan": cmd_scan, "move": cmd_move, "stop": cmd_stop,
|
|
234
|
+
"home": cmd_home, "laser": cmd_laser, "led": cmd_led,
|
|
235
|
+
"status": cmd_status, "sniff": cmd_sniff, "reboot": cmd_reboot,
|
|
236
|
+
}
|
|
237
|
+
handlers[args.cmd](args)
|
|
238
|
+
|
|
239
|
+
|
|
240
|
+
if __name__ == "__main__":
|
|
241
|
+
main()
|