UC2-REST 0.2.0.35__tar.gz → 0.2.0.37__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.
- {uc2_rest-0.2.0.35 → uc2_rest-0.2.0.37}/PKG-INFO +1 -1
- {uc2_rest-0.2.0.35 → uc2_rest-0.2.0.37}/UC2_REST.egg-info/PKG-INFO +1 -1
- {uc2_rest-0.2.0.35 → uc2_rest-0.2.0.37}/uc2rest/UC2Client.py +3 -3
- {uc2_rest-0.2.0.35 → uc2_rest-0.2.0.37}/uc2rest/__version__.py +1 -1
- uc2_rest-0.2.0.37/uc2rest/can.py +282 -0
- {uc2_rest-0.2.0.35 → uc2_rest-0.2.0.37}/uc2rest/motor.py +86 -13
- {uc2_rest-0.2.0.35 → uc2_rest-0.2.0.37}/uc2rest/mserial.py +88 -2
- {uc2_rest-0.2.0.35 → uc2_rest-0.2.0.37}/uc2rest/state.py +35 -0
- uc2_rest-0.2.0.35/uc2rest/can.py +0 -157
- {uc2_rest-0.2.0.35 → uc2_rest-0.2.0.37}/LICENSE +0 -0
- {uc2_rest-0.2.0.35 → uc2_rest-0.2.0.37}/README.md +0 -0
- {uc2_rest-0.2.0.35 → uc2_rest-0.2.0.37}/UC2_REST.egg-info/SOURCES.txt +0 -0
- {uc2_rest-0.2.0.35 → uc2_rest-0.2.0.37}/UC2_REST.egg-info/dependency_links.txt +0 -0
- {uc2_rest-0.2.0.35 → uc2_rest-0.2.0.37}/UC2_REST.egg-info/not-zip-safe +0 -0
- {uc2_rest-0.2.0.35 → uc2_rest-0.2.0.37}/UC2_REST.egg-info/requires.txt +0 -0
- {uc2_rest-0.2.0.35 → uc2_rest-0.2.0.37}/UC2_REST.egg-info/top_level.txt +0 -0
- {uc2_rest-0.2.0.35 → uc2_rest-0.2.0.37}/setup.cfg +0 -0
- {uc2_rest-0.2.0.35 → uc2_rest-0.2.0.37}/setup.py +0 -0
- {uc2_rest-0.2.0.35 → uc2_rest-0.2.0.37}/uc2rest/MockSerial.py +0 -0
- {uc2_rest-0.2.0.35 → uc2_rest-0.2.0.37}/uc2rest/__init__.py +0 -0
- {uc2_rest-0.2.0.35 → uc2_rest-0.2.0.37}/uc2rest/analog.py +0 -0
- {uc2_rest-0.2.0.35 → uc2_rest-0.2.0.37}/uc2rest/camera.py +0 -0
- {uc2_rest-0.2.0.35 → uc2_rest-0.2.0.37}/uc2rest/camera_trigger.py +0 -0
- {uc2_rest-0.2.0.35 → uc2_rest-0.2.0.37}/uc2rest/canota.py +0 -0
- {uc2_rest-0.2.0.35 → uc2_rest-0.2.0.37}/uc2rest/cmdrecorder.py +0 -0
- {uc2_rest-0.2.0.35 → uc2_rest-0.2.0.37}/uc2rest/digitalin.py +0 -0
- {uc2_rest-0.2.0.35 → uc2_rest-0.2.0.37}/uc2rest/digitalout.py +0 -0
- {uc2_rest-0.2.0.35 → uc2_rest-0.2.0.37}/uc2rest/fan.py +0 -0
- {uc2_rest-0.2.0.35 → uc2_rest-0.2.0.37}/uc2rest/galvo.py +0 -0
- {uc2_rest-0.2.0.35 → uc2_rest-0.2.0.37}/uc2rest/gripper.py +0 -0
- {uc2_rest-0.2.0.35 → uc2_rest-0.2.0.37}/uc2rest/home.py +0 -0
- {uc2_rest-0.2.0.35 → uc2_rest-0.2.0.37}/uc2rest/laser.py +0 -0
- {uc2_rest-0.2.0.35 → uc2_rest-0.2.0.37}/uc2rest/lcddisplay.py +0 -0
- {uc2_rest-0.2.0.35 → uc2_rest-0.2.0.37}/uc2rest/ledmatrix.py +0 -0
- {uc2_rest-0.2.0.35 → uc2_rest-0.2.0.37}/uc2rest/logger.py +0 -0
- {uc2_rest-0.2.0.35 → uc2_rest-0.2.0.37}/uc2rest/message.py +0 -0
- {uc2_rest-0.2.0.35 → uc2_rest-0.2.0.37}/uc2rest/modules.py +0 -0
- {uc2_rest-0.2.0.35 → uc2_rest-0.2.0.37}/uc2rest/motor_config.py +0 -0
- {uc2_rest-0.2.0.35 → uc2_rest-0.2.0.37}/uc2rest/objective.py +0 -0
- {uc2_rest-0.2.0.35 → uc2_rest-0.2.0.37}/uc2rest/pid.py +0 -0
- {uc2_rest-0.2.0.35 → uc2_rest-0.2.0.37}/uc2rest/rotator.py +0 -0
- {uc2_rest-0.2.0.35 → uc2_rest-0.2.0.37}/uc2rest/slm.py +0 -0
- {uc2_rest-0.2.0.35 → uc2_rest-0.2.0.37}/uc2rest/temperature.py +0 -0
- {uc2_rest-0.2.0.35 → uc2_rest-0.2.0.37}/uc2rest/utils.py +0 -0
- {uc2_rest-0.2.0.35 → uc2_rest-0.2.0.37}/uc2rest/wifi.py +0 -0
|
@@ -47,9 +47,9 @@ class UC2Client(object):
|
|
|
47
47
|
is_serial = False
|
|
48
48
|
BAUDRATE = 115200
|
|
49
49
|
|
|
50
|
-
def __init__(self, host=None, port=31950, serialport=None, identity="UC2_Feather", baudrate=BAUDRATE,
|
|
50
|
+
def __init__(self, host=None, port=31950, serialport=None, identity="UC2_Feather", baudrate=BAUDRATE,
|
|
51
51
|
NLeds=64, SerialManager=None, DEBUG=False, logger=None, skipFirmwareCheck=False,
|
|
52
|
-
isPyScript=False):
|
|
52
|
+
isPyScript=False, device_id=None, requireMaster=False):
|
|
53
53
|
'''
|
|
54
54
|
This client connects to the UC2-REST microcontroller that can be found here
|
|
55
55
|
https://github.com/openUC2/UC2-REST
|
|
@@ -80,7 +80,7 @@ class UC2Client(object):
|
|
|
80
80
|
# initialize communication channel (serial only)
|
|
81
81
|
if serialport is not None:
|
|
82
82
|
# use USB connection
|
|
83
|
-
self.serial = Serial(serialport, baudrate, parent=self, identity=identity, DEBUG=DEBUG, skipFirmwareCheck=skipFirmwareCheck)
|
|
83
|
+
self.serial = Serial(serialport, baudrate, parent=self, identity=identity, DEBUG=DEBUG, skipFirmwareCheck=skipFirmwareCheck, device_id=device_id, requireMaster=requireMaster)
|
|
84
84
|
self.is_serial = True
|
|
85
85
|
self.is_connected = self.serial.is_connected
|
|
86
86
|
self.serial.DEBUG = DEBUG
|
|
@@ -6,7 +6,7 @@ __version__.py
|
|
|
6
6
|
|
|
7
7
|
__title__ = 'UC2-REST'
|
|
8
8
|
__description__ = 'This pacage will help you to drive the ESP32-driven microscopy control modules from UC2'
|
|
9
|
-
__version__ = "v0.2.0.
|
|
9
|
+
__version__ = "v0.2.0.37"
|
|
10
10
|
__author__ = 'Benedict Diederich'
|
|
11
11
|
__author_email__ = 'benedictdied@gmail.com'
|
|
12
12
|
__license__ = 'GPL v3'
|
|
@@ -0,0 +1,282 @@
|
|
|
1
|
+
class CAN(object):
|
|
2
|
+
def __init__(self, parent):
|
|
3
|
+
"""
|
|
4
|
+
CANController handles sending commands to a remote CAN device via the parent post_json interface.
|
|
5
|
+
|
|
6
|
+
:param parent: Parent object with post_json(path, payload, getReturn, timeout, nResponses)
|
|
7
|
+
"""
|
|
8
|
+
self._parent = parent
|
|
9
|
+
|
|
10
|
+
# Store latest scan results
|
|
11
|
+
self.scanResults = []
|
|
12
|
+
self.deviceCount = 0
|
|
13
|
+
|
|
14
|
+
# Register a callback function for the CAN status on the serial loop
|
|
15
|
+
if hasattr(self._parent, "serial"):
|
|
16
|
+
self._parent.serial.register_callback(self._callback_can_status, pattern="scan")
|
|
17
|
+
|
|
18
|
+
# Announce a function that is called when we receive a CAN scan update through the callback
|
|
19
|
+
self._callbackPerKey = {}
|
|
20
|
+
self.nCallbacks = 10
|
|
21
|
+
self._callbackPerKey = self.init_callback_functions(nCallbacks=self.nCallbacks)
|
|
22
|
+
print(self._callbackPerKey)
|
|
23
|
+
|
|
24
|
+
def init_callback_functions(self, nCallbacks=10):
|
|
25
|
+
"""Initialize the callback functions."""
|
|
26
|
+
_callbackPerKey = {}
|
|
27
|
+
self.nCallbacks = nCallbacks
|
|
28
|
+
for i in range(nCallbacks):
|
|
29
|
+
_callbackPerKey[i] = []
|
|
30
|
+
return _callbackPerKey
|
|
31
|
+
|
|
32
|
+
def _callback_can_status(self, data):
|
|
33
|
+
"""
|
|
34
|
+
Cast the json in the form:
|
|
35
|
+
{
|
|
36
|
+
"scan": [
|
|
37
|
+
{"canId": 20, "deviceType": 1, "status": 0, "deviceTypeStr": "laser", "statusStr": "idle"},
|
|
38
|
+
{"canId": 10, "deviceType": 0, "status": 0, "deviceTypeStr": "motor", "statusStr": "idle"}
|
|
39
|
+
],
|
|
40
|
+
"qid": 2,
|
|
41
|
+
"count": 1
|
|
42
|
+
}
|
|
43
|
+
into the scan results array.
|
|
44
|
+
"""
|
|
45
|
+
try:
|
|
46
|
+
if "scan" in data:
|
|
47
|
+
self.scanResults = data["scan"]
|
|
48
|
+
self.deviceCount = data.get("count", len(self.scanResults))
|
|
49
|
+
|
|
50
|
+
# Call registered callback function with scan results
|
|
51
|
+
if callable(self._callbackPerKey[0]):
|
|
52
|
+
self._callbackPerKey[0](self.scanResults)
|
|
53
|
+
except Exception as e:
|
|
54
|
+
print("Error in _callback_can_status: ", e)
|
|
55
|
+
|
|
56
|
+
def register_callback(self, key, callbackfct):
|
|
57
|
+
"""Register a callback function for a specific key."""
|
|
58
|
+
self._callbackPerKey[key] = callbackfct
|
|
59
|
+
|
|
60
|
+
def get_can_ids(self):
|
|
61
|
+
"""Return CAN node IDs from the latest scan (e.g. for OTA flashing)."""
|
|
62
|
+
return [entry.get("canId") for entry in self.scanResults
|
|
63
|
+
if entry.get("canId") is not None]
|
|
64
|
+
|
|
65
|
+
def reboot_remote(self, qid=1, can_address=0, isBlocking=False, timeout=2):
|
|
66
|
+
"""
|
|
67
|
+
Reboot a CAN device.
|
|
68
|
+
|
|
69
|
+
- ``can_address == 0`` reboots the master (this ESP32) itself.
|
|
70
|
+
- ``can_address in 1..127`` reboots a remote slave by SDO-writing 1
|
|
71
|
+
to OD index 0x2507 sub 0 on the target node. The slave's
|
|
72
|
+
CO_tmr_task observes the write and calls ESP.restart() ~200 ms
|
|
73
|
+
later.
|
|
74
|
+
|
|
75
|
+
:param qid: Query ID for the CAN command (unused by firmware, kept
|
|
76
|
+
for API compatibility)
|
|
77
|
+
:param can_address: 0 = master, 1..127 = remote slave nodeId
|
|
78
|
+
:param isBlocking: If True, wait for response
|
|
79
|
+
:param timeout: Timeout for the command in seconds
|
|
80
|
+
:return: Response from the device, e.g.
|
|
81
|
+
``{"status":"ok","nodeId":11}`` or
|
|
82
|
+
``{"status":"error","error":"SDO write failed","nodeId":11}``.
|
|
83
|
+
"""
|
|
84
|
+
path = "/can_act"
|
|
85
|
+
payload = {
|
|
86
|
+
"task": path,
|
|
87
|
+
"restart": int(can_address)
|
|
88
|
+
}
|
|
89
|
+
nResponses = 1 if isBlocking else 0
|
|
90
|
+
# Send the payload to the parent, which handles the actual communication
|
|
91
|
+
return self._parent.post_json(
|
|
92
|
+
path,
|
|
93
|
+
payload,
|
|
94
|
+
getReturn=isBlocking,
|
|
95
|
+
timeout=timeout if isBlocking else 0,
|
|
96
|
+
nResponses=nResponses
|
|
97
|
+
)
|
|
98
|
+
|
|
99
|
+
def scan(self, qid=1, timeout=5, probe_range=False, id_from=1, id_to=127):
|
|
100
|
+
"""
|
|
101
|
+
Scan the CAN bus for connected devices.
|
|
102
|
+
|
|
103
|
+
:param qid: Query ID for the CAN command (default: 1)
|
|
104
|
+
:param timeout: Timeout for the scan in seconds (default: 5)
|
|
105
|
+
:param probe_range: if True, additionally SDO-probe node-ids
|
|
106
|
+
``id_from..id_to`` for their MAC so brand-new / unrouted nodes are
|
|
107
|
+
discovered (reported with deviceTypeStr "unrouted"). Absent ids
|
|
108
|
+
fast-fail on the firmware side. Use this for MAC-keyed provisioning.
|
|
109
|
+
:param id_from: first node-id to probe when ``probe_range`` is set
|
|
110
|
+
:param id_to: last node-id to probe when ``probe_range`` is set
|
|
111
|
+
:return: Response containing scan results with device information.
|
|
112
|
+
Reachable nodes are SDO-probed for their firmware build
|
|
113
|
+
timestamp (OD 0x2508), version (0x2500) and MAC (0x2509);
|
|
114
|
+
the master reports its own identity under the "master" key.
|
|
115
|
+
Example: {
|
|
116
|
+
"master": {"canId": 1, "build": "Jun 22 2026 14:30:11",
|
|
117
|
+
"fwVersion": "UC2-ESP v2.0", "mac": "AA:BB:CC:DD:EE:01"},
|
|
118
|
+
"scan": [
|
|
119
|
+
{"canId": 10, "deviceType": 0, "deviceTypeStr": "motor",
|
|
120
|
+
"status": 0, "statusStr": "idle",
|
|
121
|
+
"build": "Jun 22 2026 14:31:02", "fwVersion": "UC2-ESP v2.0",
|
|
122
|
+
"mac": "AA:BB:CC:DD:EE:10"},
|
|
123
|
+
{"canId": 20, "deviceType": 1, "deviceTypeStr": "laser",
|
|
124
|
+
"status": 1, "statusStr": "unreachable"}
|
|
125
|
+
],
|
|
126
|
+
"qid": 1,
|
|
127
|
+
"count": 2
|
|
128
|
+
}
|
|
129
|
+
Note: "build"/"fwVersion"/"mac" are only present for nodes that
|
|
130
|
+
answered the SDO probe (i.e. statusStr == "idle").
|
|
131
|
+
"""
|
|
132
|
+
path = "/can_act" # {"task":"/can_act", "scan": true}
|
|
133
|
+
payload = {
|
|
134
|
+
"task": path,
|
|
135
|
+
"scan": True,
|
|
136
|
+
"qid": qid
|
|
137
|
+
}
|
|
138
|
+
if probe_range:
|
|
139
|
+
payload["probeRange"] = True
|
|
140
|
+
payload["from"] = int(id_from)
|
|
141
|
+
payload["to"] = int(id_to)
|
|
142
|
+
return self._parent.post_json(
|
|
143
|
+
path,
|
|
144
|
+
payload,
|
|
145
|
+
getReturn=True,
|
|
146
|
+
timeout=timeout,
|
|
147
|
+
nResponses=1
|
|
148
|
+
)
|
|
149
|
+
|
|
150
|
+
def discover(self, qid=1, timeout=8, id_from=1, id_to=127):
|
|
151
|
+
"""
|
|
152
|
+
Discover all nodes on the bus by SDO-probing a node-id range.
|
|
153
|
+
|
|
154
|
+
Convenience wrapper around ``scan(probe_range=True, ...)`` — finds nodes
|
|
155
|
+
that aren't in the master's routing table yet (e.g. freshly flashed
|
|
156
|
+
boards), reporting their MAC + build info so they can be provisioned by
|
|
157
|
+
MAC via :meth:`assign_node_id_by_mac`.
|
|
158
|
+
|
|
159
|
+
:return: same shape as :meth:`scan`; probed-but-unrouted nodes carry
|
|
160
|
+
deviceTypeStr "unrouted".
|
|
161
|
+
"""
|
|
162
|
+
return self.scan(qid=qid, timeout=timeout,
|
|
163
|
+
probe_range=True, id_from=id_from, id_to=id_to)
|
|
164
|
+
|
|
165
|
+
def get_available_devices(self, timeout=2):
|
|
166
|
+
"""
|
|
167
|
+
Get list of available CAN devices.
|
|
168
|
+
|
|
169
|
+
:param timeout: Timeout for the command in seconds (default: 2)
|
|
170
|
+
:return: Response containing available CAN IDs and other bus information
|
|
171
|
+
Example: {
|
|
172
|
+
"input": {"task": "/can_get"},
|
|
173
|
+
"address": 1,
|
|
174
|
+
"addresspref": 1,
|
|
175
|
+
"addressgetcan": 1,
|
|
176
|
+
"nonworking": [0,0,0,...],
|
|
177
|
+
"available": [20,0,0,...],
|
|
178
|
+
"rx": 18,
|
|
179
|
+
"tx": 17
|
|
180
|
+
}
|
|
181
|
+
"""
|
|
182
|
+
path = "/can_act"
|
|
183
|
+
payload = {
|
|
184
|
+
"task": path,
|
|
185
|
+
"scan": True
|
|
186
|
+
}
|
|
187
|
+
return self._parent.post_json(
|
|
188
|
+
path,
|
|
189
|
+
payload,
|
|
190
|
+
getReturn=True,
|
|
191
|
+
timeout=timeout,
|
|
192
|
+
nResponses=2
|
|
193
|
+
)
|
|
194
|
+
|
|
195
|
+
def get_device_build_info(self):
|
|
196
|
+
"""
|
|
197
|
+
Return per-node firmware build info from the latest scan results.
|
|
198
|
+
|
|
199
|
+
:return: dict keyed by CAN id, e.g.
|
|
200
|
+
{10: {"build": "Jun 22 2026 14:31:02",
|
|
201
|
+
"fwVersion": "UC2-ESP v2.0",
|
|
202
|
+
"mac": "AA:BB:CC:DD:EE:10"}}
|
|
203
|
+
Only nodes that answered the SDO probe during the last scan()
|
|
204
|
+
appear here. Call scan() first to refresh.
|
|
205
|
+
"""
|
|
206
|
+
info = {}
|
|
207
|
+
for entry in self.scanResults:
|
|
208
|
+
cid = entry.get("canId")
|
|
209
|
+
if cid is None:
|
|
210
|
+
continue
|
|
211
|
+
fields = {k: entry[k] for k in ("build", "fwVersion", "mac")
|
|
212
|
+
if k in entry}
|
|
213
|
+
if fields:
|
|
214
|
+
info[cid] = fields
|
|
215
|
+
return info
|
|
216
|
+
|
|
217
|
+
def set_remote_node_id(self, new_id, target=None, by_mac=None,
|
|
218
|
+
expect_mac=None, qid=1, isBlocking=True, timeout=4):
|
|
219
|
+
"""
|
|
220
|
+
Reassign a remote node's CAN id over the bus (SDO write to OD 0x250A).
|
|
221
|
+
|
|
222
|
+
Identify the node EITHER by its current id (``target``, optionally with
|
|
223
|
+
``expect_mac`` for a safety check) OR purely by its MAC (``by_mac``), in
|
|
224
|
+
which case the firmware probes the bus to find which id currently has
|
|
225
|
+
that MAC — use this when you don't know the current id. ``new_id`` is
|
|
226
|
+
always the desired new id.
|
|
227
|
+
|
|
228
|
+
The slave persists the new id to NVS and performs a CANopen
|
|
229
|
+
communication reset, so it reappears at ``new_id`` after ~0.3 s.
|
|
230
|
+
|
|
231
|
+
:param new_id: desired CAN id (1..127)
|
|
232
|
+
:param target: current CAN id (1..127) of the node to reconfigure
|
|
233
|
+
:param by_mac: target MAC "AA:BB:CC:DD:EE:FF" — firmware finds the node
|
|
234
|
+
(use instead of ``target`` when the current id is unknown)
|
|
235
|
+
:param expect_mac: when using ``target``, verify the node's MAC matches
|
|
236
|
+
before committing (id only ever binds to the intended device)
|
|
237
|
+
:param qid: Query ID (kept for API symmetry)
|
|
238
|
+
:param isBlocking: wait for the firmware acknowledgement
|
|
239
|
+
:param timeout: command timeout in seconds
|
|
240
|
+
:return: firmware response, e.g. ``{"status":"ok","target":60,"newId":70}``
|
|
241
|
+
or ``{"status":"error","error":"MAC not found on bus","mac":"..."}``
|
|
242
|
+
"""
|
|
243
|
+
if by_mac is None and target is None:
|
|
244
|
+
raise ValueError("set_remote_node_id requires either target or by_mac")
|
|
245
|
+
path = "/can_act"
|
|
246
|
+
payload = {
|
|
247
|
+
"task": path,
|
|
248
|
+
"setRemoteNodeId": int(new_id),
|
|
249
|
+
"qid": qid,
|
|
250
|
+
}
|
|
251
|
+
if by_mac is not None:
|
|
252
|
+
payload["byMac"] = str(by_mac)
|
|
253
|
+
else:
|
|
254
|
+
payload["target"] = int(target)
|
|
255
|
+
if expect_mac is not None:
|
|
256
|
+
payload["expectMac"] = str(expect_mac)
|
|
257
|
+
nResponses = 1 if isBlocking else 0
|
|
258
|
+
return self._parent.post_json(
|
|
259
|
+
path,
|
|
260
|
+
payload,
|
|
261
|
+
getReturn=isBlocking,
|
|
262
|
+
timeout=timeout if isBlocking else 0,
|
|
263
|
+
nResponses=nResponses,
|
|
264
|
+
)
|
|
265
|
+
|
|
266
|
+
def assign_node_id_by_mac(self, mac, new_id, qid=1, timeout=5):
|
|
267
|
+
"""
|
|
268
|
+
Assign a CAN id to the node with the given MAC address.
|
|
269
|
+
|
|
270
|
+
The firmware probes the bus to locate whichever node currently
|
|
271
|
+
advertises ``mac`` and reassigns it to ``new_id`` in a single round-trip
|
|
272
|
+
(no need to know the node's current id). This is the canonical
|
|
273
|
+
MAC-keyed provisioning call.
|
|
274
|
+
|
|
275
|
+
:param mac: target MAC as "AA:BB:CC:DD:EE:FF" (case-insensitive)
|
|
276
|
+
:param new_id: desired CAN id (1..127)
|
|
277
|
+
:param qid: Query ID
|
|
278
|
+
:param timeout: request timeout in seconds
|
|
279
|
+
:return: firmware response, e.g. ``{"status":"ok","target":60,"newId":70,
|
|
280
|
+
"mac":"..."}`` or ``{"status":"error","error":"MAC not found on bus"}``
|
|
281
|
+
"""
|
|
282
|
+
return self.set_remote_node_id(new_id, by_mac=mac, qid=qid, timeout=timeout)
|
|
@@ -91,12 +91,19 @@ class Motor(object):
|
|
|
91
91
|
nSteppers = len(data["steppers"])
|
|
92
92
|
stepSizes = np.array((self.stepSizeA, self.stepSizeX, self.stepSizeY, self.stepSizeZ))
|
|
93
93
|
for iMotor in range(nSteppers):
|
|
94
|
-
|
|
94
|
+
stepper = data["steppers"][iMotor]
|
|
95
|
+
# Intermediate status frames during a move carry only
|
|
96
|
+
# {stepperid, isDone} with no "position" key — skip them instead
|
|
97
|
+
# of raising KeyError 'position' (which previously aborted the
|
|
98
|
+
# whole callback and left currentPosition stale).
|
|
99
|
+
if "position" not in stepper:
|
|
100
|
+
continue
|
|
101
|
+
stepperID = stepper["stepperid"]
|
|
95
102
|
# Hardware returns raw steps in firmware frame; convert to physical units
|
|
96
103
|
# in user frame: phys = hw_steps * stepSize * direction. The direction sign
|
|
97
104
|
# hides any wiring polarity flip from the caller.
|
|
98
105
|
self.currentPosition[stepperID] = (
|
|
99
|
-
|
|
106
|
+
stepper["position"]
|
|
100
107
|
* stepSizes[stepperID]
|
|
101
108
|
* self.direction[stepperID]
|
|
102
109
|
)
|
|
@@ -333,6 +340,26 @@ class Motor(object):
|
|
|
333
340
|
self.stepSizeA = stepSize
|
|
334
341
|
self.backlash[axisIdx] = backlash
|
|
335
342
|
|
|
343
|
+
def set_backlash(self, axis, backlash):
|
|
344
|
+
"""Set the per-axis backlash (in hardware steps) used as a reversal overshoot.
|
|
345
|
+
|
|
346
|
+
``axis`` may be a name (\"X\"/\"Y\"/\"Z\"/\"A\") or a hardware index (0-3).
|
|
347
|
+
The value is stored in :attr:`backlash` and consumed by
|
|
348
|
+
:meth:`move_stepper`, which adds ``direction * backlash`` extra steps
|
|
349
|
+
whenever the axis changes direction, taking up the mechanical slack so the
|
|
350
|
+
stage still reaches the commanded target. Pass ``0`` to disable
|
|
351
|
+
compensation for that axis. Typically fed from a camera-based backlash
|
|
352
|
+
measurement (microns) after converting to steps with the axis step size.
|
|
353
|
+
"""
|
|
354
|
+
idx = self.xyztTo1230(axis) if isinstance(axis, str) else int(axis)
|
|
355
|
+
self.backlash[idx] = backlash
|
|
356
|
+
return float(self.backlash[idx])
|
|
357
|
+
|
|
358
|
+
def get_backlash(self, axis):
|
|
359
|
+
"""Return the per-axis backlash (in hardware steps)."""
|
|
360
|
+
idx = self.xyztTo1230(axis) if isinstance(axis, str) else int(axis)
|
|
361
|
+
return float(self.backlash[idx])
|
|
362
|
+
|
|
336
363
|
def xyztTo1230(self, axis):
|
|
337
364
|
axis = axis.upper()
|
|
338
365
|
if axis == "X":
|
|
@@ -613,10 +640,15 @@ class Motor(object):
|
|
|
613
640
|
if isAbsoluteArray[iMotor]:
|
|
614
641
|
# Compare current position (physical) with target (physical, already includes offset)
|
|
615
642
|
self.currentDirection[iMotor] = 1 if (self.currentPosition[iMotor] > targetPositionPhysical[iMotor]) else -1
|
|
616
|
-
#
|
|
617
|
-
#
|
|
618
|
-
|
|
619
|
-
|
|
643
|
+
# Travel distance for the time estimate = |current - target| in
|
|
644
|
+
# PHYSICAL units, converted to hardware steps. Computing it in
|
|
645
|
+
# physical units keeps it direction-agnostic: the hardware `steps`
|
|
646
|
+
# target carries the per-axis direction sign, so subtracting it
|
|
647
|
+
# from an unsigned current-in-steps produced a *sum* (not a
|
|
648
|
+
# difference) on inverted axes, hugely inflating the estimate.
|
|
649
|
+
absoluteDistances_steps[iMotor] = abs(
|
|
650
|
+
self.currentPosition[iMotor] - targetPositionPhysical[iMotor]
|
|
651
|
+
) / stepSizes[iMotor]
|
|
620
652
|
else:
|
|
621
653
|
self.currentDirection[iMotor] = np.sign(steps[iMotor])
|
|
622
654
|
# For relative motion, steps[iMotor] is already the distance in hardware steps
|
|
@@ -629,19 +661,20 @@ class Motor(object):
|
|
|
629
661
|
if not isAbsoluteArray[iMotor]:
|
|
630
662
|
absoluteDistances_steps[iMotor] = abs(steps[iMotor])
|
|
631
663
|
|
|
632
|
-
#
|
|
664
|
+
# Speed and acceleration are already in firmware step units (the same raw
|
|
665
|
+
# values sent to the device), and absoluteDistances_steps is in hardware
|
|
666
|
+
# steps too, so the time estimate is unit-consistent WITHOUT any stepSize
|
|
667
|
+
# division — dividing here would desync it from the distance and break the
|
|
668
|
+
# estimate. Just take magnitudes.
|
|
633
669
|
speed_steps = np.zeros(4)
|
|
634
670
|
acceleration_steps = np.zeros(4)
|
|
635
671
|
for iMotor in range(4):
|
|
636
672
|
if speed[iMotor] != 0:
|
|
637
|
-
|
|
638
|
-
speed_steps[iMotor] = abs(speed[iMotor]) # TODO: This is actually given in steps/s / stepSizes[iMotor]
|
|
673
|
+
speed_steps[iMotor] = abs(speed[iMotor])
|
|
639
674
|
if acceleration[iMotor] is not None and acceleration[iMotor] != 0:
|
|
640
|
-
|
|
641
|
-
acceleration_steps[iMotor] = abs(acceleration[iMotor]) # TODO: This is actually given in steps/s / stepSizes[iMotor]
|
|
675
|
+
acceleration_steps[iMotor] = abs(acceleration[iMotor])
|
|
642
676
|
else:
|
|
643
|
-
#
|
|
644
|
-
acceleration_steps[iMotor] = 20000 # This should also be converted, but we use a safe default
|
|
677
|
+
acceleration_steps[iMotor] = 20000 # safe default (firmware steps/s^2)
|
|
645
678
|
|
|
646
679
|
# Calculate travel time using HARDWARE STEPS and converted speed/acceleration
|
|
647
680
|
# Find the axis that will take the longest (limits overall movement time)
|
|
@@ -696,8 +729,10 @@ class Motor(object):
|
|
|
696
729
|
"redu": int(is_reduced)}
|
|
697
730
|
if acceleration[iMotor] is not None:
|
|
698
731
|
motorProp["accel"] = int(acceleration[iMotor])
|
|
732
|
+
motorProp["acceleration"] = int(acceleration[iMotor])
|
|
699
733
|
else:
|
|
700
734
|
motorProp["accel"] = self.DEFAULT_ACCELERATION
|
|
735
|
+
motorProp["acceleration"] = self.DEFAULT_ACCELERATION
|
|
701
736
|
motorPropList.append(motorProp)
|
|
702
737
|
if len(motorPropList)==0:
|
|
703
738
|
return "{'return':-1}"
|
|
@@ -1243,6 +1278,44 @@ class Motor(object):
|
|
|
1243
1278
|
r = self._parent.post_json(path, payload, timeout=timeout)
|
|
1244
1279
|
return r
|
|
1245
1280
|
|
|
1281
|
+
def get_tmc_parameters(self, axis=0, timeout=1):
|
|
1282
|
+
''' Read the TMC parameters for a specific axis back from the device.
|
|
1283
|
+
|
|
1284
|
+
Sends {"task":"/tmc_get", "axis":<n>} and parses the response. Returns a
|
|
1285
|
+
dict with msteps/rms_current/sgthrs/semin/semax/blank_time/toff, or None
|
|
1286
|
+
if the firmware does not implement TMC readback (older firmwares only
|
|
1287
|
+
accept /tmc_act). Callers should fall back to their last-applied values
|
|
1288
|
+
in that case.
|
|
1289
|
+
'''
|
|
1290
|
+
if type(axis) == str:
|
|
1291
|
+
axis = self.xyztTo1230(axis)
|
|
1292
|
+
path = "/tmc_get"
|
|
1293
|
+
payload = {"task": path}
|
|
1294
|
+
if axis is not None:
|
|
1295
|
+
payload["axis"] = axis
|
|
1296
|
+
try:
|
|
1297
|
+
r = self._parent.post_json(path, payload, timeout=timeout)
|
|
1298
|
+
if isinstance(r, list):
|
|
1299
|
+
r = r[0] if r else {}
|
|
1300
|
+
if not isinstance(r, dict):
|
|
1301
|
+
return None
|
|
1302
|
+
# firmware may nest the values under "tmc" or return them flat
|
|
1303
|
+
tmc = r.get("tmc", r)
|
|
1304
|
+
if not isinstance(tmc, dict):
|
|
1305
|
+
return None
|
|
1306
|
+
keys = ("msteps", "rms_current", "sgthrs", "semin", "semax", "blank_time", "toff")
|
|
1307
|
+
if not any(k in tmc for k in keys):
|
|
1308
|
+
# nothing TMC-shaped came back -> readback unsupported
|
|
1309
|
+
return None
|
|
1310
|
+
return {k: tmc[k] for k in keys if k in tmc}
|
|
1311
|
+
except Exception as e:
|
|
1312
|
+
self._parent.logger.debug(f"get_tmc_parameters failed: {e}")
|
|
1313
|
+
return None
|
|
1314
|
+
|
|
1315
|
+
# camelCase alias used by the ImSwitch ESP32StageManager
|
|
1316
|
+
def getTMCSettings(self, axis=0, timeout=1):
|
|
1317
|
+
return self.get_tmc_parameters(axis=axis, timeout=timeout)
|
|
1318
|
+
|
|
1246
1319
|
def set_hard_limits(self, axis=1, enabled=True, polarity=0, timeout=1):
|
|
1247
1320
|
'''
|
|
1248
1321
|
Configure hard limits (emergency stop) for a motor axis.
|
|
@@ -16,8 +16,8 @@ except ImportError:
|
|
|
16
16
|
T_SERIAL_WARMUP = 2.5
|
|
17
17
|
class Serial:
|
|
18
18
|
def __init__(self, port, baudrate=115200, timeout=5,
|
|
19
|
-
identity="UC2_Feather", parent=None, DEBUG=False,
|
|
20
|
-
skipFirmwareCheck=False):
|
|
19
|
+
identity="UC2_Feather", parent=None, DEBUG=False,
|
|
20
|
+
skipFirmwareCheck=False, device_id=None, requireMaster=False):
|
|
21
21
|
|
|
22
22
|
self.serialdevice = None
|
|
23
23
|
self.serialport = port
|
|
@@ -26,6 +26,15 @@ class Serial:
|
|
|
26
26
|
self._parent = parent
|
|
27
27
|
self.manufacturer = ""
|
|
28
28
|
self.skipFirmwareCheck = skipFirmwareCheck
|
|
29
|
+
# Connect only to a specific board / only to the CANopen master.
|
|
30
|
+
# device_id pins a physical board by its USB serial number (or a
|
|
31
|
+
# substring of the port path / hwid). requireMaster makes auto-discovery
|
|
32
|
+
# skip any board whose firmware does NOT report a "*_master" pindef, so a
|
|
33
|
+
# motor/slave board (ESP32-S3, native USB) is never picked accidentally.
|
|
34
|
+
self.device_id = device_id
|
|
35
|
+
self.requireMaster = requireMaster
|
|
36
|
+
# identity of the board we actually connected to (filled by checkFirmware)
|
|
37
|
+
self.firmware_info = {}
|
|
29
38
|
if self._parent is None:
|
|
30
39
|
import logging
|
|
31
40
|
self._logger = logging.getLogger(__name__)
|
|
@@ -160,6 +169,64 @@ class Serial:
|
|
|
160
169
|
|
|
161
170
|
return ser
|
|
162
171
|
|
|
172
|
+
def _portMatchesDeviceId(self, port):
|
|
173
|
+
'''True if `port` matches the configured device_id, or if no device_id
|
|
174
|
+
is set (then everything matches). The device_id is compared as a
|
|
175
|
+
case-insensitive substring against the USB serial number, the port path
|
|
176
|
+
and the hwid, so the user can pin a board by whichever is stable on
|
|
177
|
+
their OS.'''
|
|
178
|
+
if not self.device_id:
|
|
179
|
+
return True
|
|
180
|
+
did = str(self.device_id).lower()
|
|
181
|
+
candidates = [getattr(port, "serial_number", None),
|
|
182
|
+
getattr(port, "device", None),
|
|
183
|
+
getattr(port, "hwid", None)]
|
|
184
|
+
return any(c and did in str(c).lower() for c in candidates)
|
|
185
|
+
|
|
186
|
+
def _probeDeviceIdentity(self, ser, timeout=2):
|
|
187
|
+
'''Send /state_get and parse the firmware identity block into
|
|
188
|
+
self.firmware_info: {name, version, date, author, pindef, isMaster}.
|
|
189
|
+
Used by requireMaster to reject motor/slave boards. Best-effort: returns
|
|
190
|
+
an empty dict (and leaves firmware_info empty) if nothing parses.'''
|
|
191
|
+
info = {}
|
|
192
|
+
try:
|
|
193
|
+
self._write(ser, {"task": "/state_get"})
|
|
194
|
+
ser.write(b'\n')
|
|
195
|
+
buffer = ""
|
|
196
|
+
reading_json = False
|
|
197
|
+
t0 = time.time()
|
|
198
|
+
while time.time() - t0 < timeout:
|
|
199
|
+
raw = self._read(ser)
|
|
200
|
+
try:
|
|
201
|
+
line = raw.decode('utf-8').strip()
|
|
202
|
+
except Exception:
|
|
203
|
+
continue
|
|
204
|
+
if line == "":
|
|
205
|
+
continue
|
|
206
|
+
if line.find("++") >= 0:
|
|
207
|
+
reading_json = True
|
|
208
|
+
continue
|
|
209
|
+
if reading_json and line.find("--") >= 0:
|
|
210
|
+
break
|
|
211
|
+
if reading_json:
|
|
212
|
+
buffer += line
|
|
213
|
+
if buffer:
|
|
214
|
+
data = json.loads(buffer)
|
|
215
|
+
state = data.get("state", data) if isinstance(data, dict) else {}
|
|
216
|
+
pindef = state.get("pindef", "")
|
|
217
|
+
info = {
|
|
218
|
+
"name": state.get("identifier_name", ""),
|
|
219
|
+
"version": state.get("identifier_id", ""),
|
|
220
|
+
"date": state.get("identifier_date", ""),
|
|
221
|
+
"author": state.get("identifier_author", ""),
|
|
222
|
+
"pindef": pindef,
|
|
223
|
+
"isMaster": "master" in str(pindef).lower(),
|
|
224
|
+
}
|
|
225
|
+
except Exception as e:
|
|
226
|
+
self._logger.debug(f"_probeDeviceIdentity failed: {e}")
|
|
227
|
+
self.firmware_info = info
|
|
228
|
+
return info
|
|
229
|
+
|
|
163
230
|
def findCorrectSerialDevice(self):
|
|
164
231
|
'''
|
|
165
232
|
This function tries to find the correct serial device from the list of available ports
|
|
@@ -183,6 +250,9 @@ class Serial:
|
|
|
183
250
|
descriptions_to_check = ["CH340", "CP2102", "USB2.0-Serial", "USB-Serial"]
|
|
184
251
|
|
|
185
252
|
for port in _available_ports:
|
|
253
|
+
# If a specific board was requested, ignore everything else.
|
|
254
|
+
if not self._portMatchesDeviceId(port):
|
|
255
|
+
continue
|
|
186
256
|
if any(port.device.startswith(p) for p in ports_to_check) or \
|
|
187
257
|
any(port.description.startswith(d) for d in descriptions_to_check):
|
|
188
258
|
if current_os.startswith("darwin") and port.device.startswith("/dev/cu.usbserial-"):
|
|
@@ -217,6 +287,22 @@ class Serial:
|
|
|
217
287
|
#time.sleep(T_SERIAL_WARMUP)
|
|
218
288
|
self._freeSerialBuffer(self.serialdevice, timeout=2, timeMinimum=1)
|
|
219
289
|
if self.skipFirmwareCheck or self.checkFirmware(self.serialdevice):
|
|
290
|
+
# When only the master may be used, read the firmware identity and
|
|
291
|
+
# reject boards that are not a CANopen master (e.g. ESP32-S3 motor
|
|
292
|
+
# boards on native USB that also speak the UC2 protocol).
|
|
293
|
+
if self.requireMaster:
|
|
294
|
+
self._probeDeviceIdentity(self.serialdevice)
|
|
295
|
+
if not self.firmware_info.get("isMaster", False):
|
|
296
|
+
self._logger.debug(
|
|
297
|
+
f"Skipping non-master board on {getattr(port, 'device', '?')} "
|
|
298
|
+
f"(pindef={self.firmware_info.get('pindef', '?')})"
|
|
299
|
+
)
|
|
300
|
+
try:
|
|
301
|
+
self.serialdevice.close()
|
|
302
|
+
except Exception:
|
|
303
|
+
pass
|
|
304
|
+
self.is_connected = False
|
|
305
|
+
return False
|
|
220
306
|
self.is_connected = True
|
|
221
307
|
self.NumberRetryReconnect = 0
|
|
222
308
|
return True
|
|
@@ -137,6 +137,41 @@ class State(object):
|
|
|
137
137
|
r = self._parent.get_json(path, timeout=timeout)
|
|
138
138
|
return r
|
|
139
139
|
|
|
140
|
+
def get_firmware_info(self, timeout=3):
|
|
141
|
+
'''
|
|
142
|
+
Return the firmware identity of the USB-connected ESP32 as a flat dict:
|
|
143
|
+
{"name", "version", "date", "author", "pindef", "isMaster"}.
|
|
144
|
+
|
|
145
|
+
Parsed from /state_get, e.g.
|
|
146
|
+
{"state":{"identifier_name":"UC2_Feather","identifier_id":"V2.0",
|
|
147
|
+
"identifier_date":"Jun 17 2026 07:17:22","identifier_author":"BD",
|
|
148
|
+
"pindef":"UC2_canopen_master", ...},"qid":0}
|
|
149
|
+
The build date and pindef are the fields that matter for telling boards
|
|
150
|
+
apart. Returns an empty dict if nothing could be parsed.
|
|
151
|
+
'''
|
|
152
|
+
r = self.get_state(timeout=timeout)
|
|
153
|
+
try:
|
|
154
|
+
if isinstance(r, list):
|
|
155
|
+
r = r[0] if r else {}
|
|
156
|
+
state = r.get("state", r) if isinstance(r, dict) else {}
|
|
157
|
+
pindef = state.get("pindef", "")
|
|
158
|
+
return {
|
|
159
|
+
"name": state.get("identifier_name", ""),
|
|
160
|
+
"version": state.get("identifier_id", ""),
|
|
161
|
+
"date": state.get("identifier_date", ""),
|
|
162
|
+
"author": state.get("identifier_author", ""),
|
|
163
|
+
"pindef": pindef,
|
|
164
|
+
"isMaster": "master" in str(pindef).lower(),
|
|
165
|
+
}
|
|
166
|
+
except Exception as e:
|
|
167
|
+
self._parent.logger.debug(f"get_firmware_info failed: {e}")
|
|
168
|
+
return {}
|
|
169
|
+
|
|
170
|
+
def is_master(self, timeout=1):
|
|
171
|
+
'''True if the connected board reports a CANopen-master pindef.'''
|
|
172
|
+
info = self.get_firmware_info(timeout=timeout)
|
|
173
|
+
return bool(info.get("isMaster", False))
|
|
174
|
+
|
|
140
175
|
def delay(self, delay=1, getReturn=True):
|
|
141
176
|
path = "/state_act"
|
|
142
177
|
payload = {
|
uc2_rest-0.2.0.35/uc2rest/can.py
DELETED
|
@@ -1,157 +0,0 @@
|
|
|
1
|
-
class CAN(object):
|
|
2
|
-
def __init__(self, parent):
|
|
3
|
-
"""
|
|
4
|
-
CANController handles sending commands to a remote CAN device via the parent post_json interface.
|
|
5
|
-
|
|
6
|
-
:param parent: Parent object with post_json(path, payload, getReturn, timeout, nResponses)
|
|
7
|
-
"""
|
|
8
|
-
self._parent = parent
|
|
9
|
-
|
|
10
|
-
# Store latest scan results
|
|
11
|
-
self.scanResults = []
|
|
12
|
-
self.deviceCount = 0
|
|
13
|
-
|
|
14
|
-
# Register a callback function for the CAN status on the serial loop
|
|
15
|
-
if hasattr(self._parent, "serial"):
|
|
16
|
-
self._parent.serial.register_callback(self._callback_can_status, pattern="scan")
|
|
17
|
-
|
|
18
|
-
# Announce a function that is called when we receive a CAN scan update through the callback
|
|
19
|
-
self._callbackPerKey = {}
|
|
20
|
-
self.nCallbacks = 10
|
|
21
|
-
self._callbackPerKey = self.init_callback_functions(nCallbacks=self.nCallbacks)
|
|
22
|
-
print(self._callbackPerKey)
|
|
23
|
-
|
|
24
|
-
def init_callback_functions(self, nCallbacks=10):
|
|
25
|
-
"""Initialize the callback functions."""
|
|
26
|
-
_callbackPerKey = {}
|
|
27
|
-
self.nCallbacks = nCallbacks
|
|
28
|
-
for i in range(nCallbacks):
|
|
29
|
-
_callbackPerKey[i] = []
|
|
30
|
-
return _callbackPerKey
|
|
31
|
-
|
|
32
|
-
def _callback_can_status(self, data):
|
|
33
|
-
"""
|
|
34
|
-
Cast the json in the form:
|
|
35
|
-
{
|
|
36
|
-
"scan": [
|
|
37
|
-
{"canId": 20, "deviceType": 1, "status": 0, "deviceTypeStr": "laser", "statusStr": "idle"},
|
|
38
|
-
{"canId": 10, "deviceType": 0, "status": 0, "deviceTypeStr": "motor", "statusStr": "idle"}
|
|
39
|
-
],
|
|
40
|
-
"qid": 2,
|
|
41
|
-
"count": 1
|
|
42
|
-
}
|
|
43
|
-
into the scan results array.
|
|
44
|
-
"""
|
|
45
|
-
try:
|
|
46
|
-
if "scan" in data:
|
|
47
|
-
self.scanResults = data["scan"]
|
|
48
|
-
self.deviceCount = data.get("count", len(self.scanResults))
|
|
49
|
-
|
|
50
|
-
# Call registered callback function with scan results
|
|
51
|
-
if callable(self._callbackPerKey[0]):
|
|
52
|
-
self._callbackPerKey[0](self.scanResults)
|
|
53
|
-
except Exception as e:
|
|
54
|
-
print("Error in _callback_can_status: ", e)
|
|
55
|
-
|
|
56
|
-
def register_callback(self, key, callbackfct):
|
|
57
|
-
"""Register a callback function for a specific key."""
|
|
58
|
-
self._callbackPerKey[key] = callbackfct
|
|
59
|
-
|
|
60
|
-
def get_can_ids(self):
|
|
61
|
-
"""Return CAN node IDs from the latest scan (e.g. for OTA flashing)."""
|
|
62
|
-
return [entry.get("canId") for entry in self.scanResults
|
|
63
|
-
if entry.get("canId") is not None]
|
|
64
|
-
|
|
65
|
-
def reboot_remote(self, qid=1, can_address=0, isBlocking=False, timeout=2):
|
|
66
|
-
"""
|
|
67
|
-
Reboot a CAN device.
|
|
68
|
-
|
|
69
|
-
- ``can_address == 0`` reboots the master (this ESP32) itself.
|
|
70
|
-
- ``can_address in 1..127`` reboots a remote slave by SDO-writing 1
|
|
71
|
-
to OD index 0x2507 sub 0 on the target node. The slave's
|
|
72
|
-
CO_tmr_task observes the write and calls ESP.restart() ~200 ms
|
|
73
|
-
later.
|
|
74
|
-
|
|
75
|
-
:param qid: Query ID for the CAN command (unused by firmware, kept
|
|
76
|
-
for API compatibility)
|
|
77
|
-
:param can_address: 0 = master, 1..127 = remote slave nodeId
|
|
78
|
-
:param isBlocking: If True, wait for response
|
|
79
|
-
:param timeout: Timeout for the command in seconds
|
|
80
|
-
:return: Response from the device, e.g.
|
|
81
|
-
``{"status":"ok","nodeId":11}`` or
|
|
82
|
-
``{"status":"error","error":"SDO write failed","nodeId":11}``.
|
|
83
|
-
"""
|
|
84
|
-
path = "/can_act"
|
|
85
|
-
payload = {
|
|
86
|
-
"task": path,
|
|
87
|
-
"restart": int(can_address)
|
|
88
|
-
}
|
|
89
|
-
nResponses = 1 if isBlocking else 0
|
|
90
|
-
# Send the payload to the parent, which handles the actual communication
|
|
91
|
-
return self._parent.post_json(
|
|
92
|
-
path,
|
|
93
|
-
payload,
|
|
94
|
-
getReturn=isBlocking,
|
|
95
|
-
timeout=timeout if isBlocking else 0,
|
|
96
|
-
nResponses=nResponses
|
|
97
|
-
)
|
|
98
|
-
|
|
99
|
-
def scan(self, qid=1, timeout=5):
|
|
100
|
-
"""
|
|
101
|
-
Scan the CAN bus for connected devices.
|
|
102
|
-
|
|
103
|
-
:param qid: Query ID for the CAN command (default: 1)
|
|
104
|
-
:param timeout: Timeout for the scan in seconds (default: 5)
|
|
105
|
-
:return: Response containing scan results with device information
|
|
106
|
-
Example: {
|
|
107
|
-
"scan": [
|
|
108
|
-
{"canId": 10, "deviceType": 0, "deviceTypeStr": "motor", "status": 0, "statusStr": "idle"},
|
|
109
|
-
{"canId": 20, "deviceType": 1, "deviceTypeStr": "laser", "status": 0, "statusStr": "idle"}
|
|
110
|
-
],
|
|
111
|
-
"qid": 1,
|
|
112
|
-
"count": 2
|
|
113
|
-
}
|
|
114
|
-
"""
|
|
115
|
-
path = "/can_act" # {"task":"/can_act", "scan": true}
|
|
116
|
-
payload = {
|
|
117
|
-
"task": path,
|
|
118
|
-
"scan": True,
|
|
119
|
-
"qid": qid
|
|
120
|
-
}
|
|
121
|
-
return self._parent.post_json(
|
|
122
|
-
path,
|
|
123
|
-
payload,
|
|
124
|
-
getReturn=True,
|
|
125
|
-
timeout=timeout,
|
|
126
|
-
nResponses=1
|
|
127
|
-
)
|
|
128
|
-
|
|
129
|
-
def get_available_devices(self, timeout=2):
|
|
130
|
-
"""
|
|
131
|
-
Get list of available CAN devices.
|
|
132
|
-
|
|
133
|
-
:param timeout: Timeout for the command in seconds (default: 2)
|
|
134
|
-
:return: Response containing available CAN IDs and other bus information
|
|
135
|
-
Example: {
|
|
136
|
-
"input": {"task": "/can_get"},
|
|
137
|
-
"address": 1,
|
|
138
|
-
"addresspref": 1,
|
|
139
|
-
"addressgetcan": 1,
|
|
140
|
-
"nonworking": [0,0,0,...],
|
|
141
|
-
"available": [20,0,0,...],
|
|
142
|
-
"rx": 18,
|
|
143
|
-
"tx": 17
|
|
144
|
-
}
|
|
145
|
-
"""
|
|
146
|
-
path = "/can_act"
|
|
147
|
-
payload = {
|
|
148
|
-
"task": path,
|
|
149
|
-
"scan": True
|
|
150
|
-
}
|
|
151
|
-
return self._parent.post_json(
|
|
152
|
-
path,
|
|
153
|
-
payload,
|
|
154
|
-
getReturn=True,
|
|
155
|
-
timeout=timeout,
|
|
156
|
-
nResponses=2
|
|
157
|
-
)
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|