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 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()