UC2-REST 0.2.0.35__tar.gz → 0.2.0.38__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.38}/PKG-INFO +1 -1
- {uc2_rest-0.2.0.35 → uc2_rest-0.2.0.38}/UC2_REST.egg-info/PKG-INFO +1 -1
- {uc2_rest-0.2.0.35 → uc2_rest-0.2.0.38}/UC2_REST.egg-info/SOURCES.txt +2 -0
- {uc2_rest-0.2.0.35 → uc2_rest-0.2.0.38}/uc2rest/UC2Client.py +12 -4
- {uc2_rest-0.2.0.35 → uc2_rest-0.2.0.38}/uc2rest/__version__.py +1 -1
- uc2_rest-0.2.0.38/uc2rest/can.py +282 -0
- uc2_rest-0.2.0.38/uc2rest/gpio.py +171 -0
- uc2_rest-0.2.0.38/uc2rest/i2c.py +190 -0
- {uc2_rest-0.2.0.35 → uc2_rest-0.2.0.38}/uc2rest/motor.py +87 -14
- {uc2_rest-0.2.0.35 → uc2_rest-0.2.0.38}/uc2rest/mserial.py +96 -7
- {uc2_rest-0.2.0.35 → uc2_rest-0.2.0.38}/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.38}/LICENSE +0 -0
- {uc2_rest-0.2.0.35 → uc2_rest-0.2.0.38}/README.md +0 -0
- {uc2_rest-0.2.0.35 → uc2_rest-0.2.0.38}/UC2_REST.egg-info/dependency_links.txt +0 -0
- {uc2_rest-0.2.0.35 → uc2_rest-0.2.0.38}/UC2_REST.egg-info/not-zip-safe +0 -0
- {uc2_rest-0.2.0.35 → uc2_rest-0.2.0.38}/UC2_REST.egg-info/requires.txt +0 -0
- {uc2_rest-0.2.0.35 → uc2_rest-0.2.0.38}/UC2_REST.egg-info/top_level.txt +0 -0
- {uc2_rest-0.2.0.35 → uc2_rest-0.2.0.38}/setup.cfg +0 -0
- {uc2_rest-0.2.0.35 → uc2_rest-0.2.0.38}/setup.py +0 -0
- {uc2_rest-0.2.0.35 → uc2_rest-0.2.0.38}/uc2rest/MockSerial.py +0 -0
- {uc2_rest-0.2.0.35 → uc2_rest-0.2.0.38}/uc2rest/__init__.py +0 -0
- {uc2_rest-0.2.0.35 → uc2_rest-0.2.0.38}/uc2rest/analog.py +0 -0
- {uc2_rest-0.2.0.35 → uc2_rest-0.2.0.38}/uc2rest/camera.py +0 -0
- {uc2_rest-0.2.0.35 → uc2_rest-0.2.0.38}/uc2rest/camera_trigger.py +0 -0
- {uc2_rest-0.2.0.35 → uc2_rest-0.2.0.38}/uc2rest/canota.py +0 -0
- {uc2_rest-0.2.0.35 → uc2_rest-0.2.0.38}/uc2rest/cmdrecorder.py +0 -0
- {uc2_rest-0.2.0.35 → uc2_rest-0.2.0.38}/uc2rest/digitalin.py +0 -0
- {uc2_rest-0.2.0.35 → uc2_rest-0.2.0.38}/uc2rest/digitalout.py +0 -0
- {uc2_rest-0.2.0.35 → uc2_rest-0.2.0.38}/uc2rest/fan.py +0 -0
- {uc2_rest-0.2.0.35 → uc2_rest-0.2.0.38}/uc2rest/galvo.py +0 -0
- {uc2_rest-0.2.0.35 → uc2_rest-0.2.0.38}/uc2rest/gripper.py +0 -0
- {uc2_rest-0.2.0.35 → uc2_rest-0.2.0.38}/uc2rest/home.py +0 -0
- {uc2_rest-0.2.0.35 → uc2_rest-0.2.0.38}/uc2rest/laser.py +0 -0
- {uc2_rest-0.2.0.35 → uc2_rest-0.2.0.38}/uc2rest/lcddisplay.py +0 -0
- {uc2_rest-0.2.0.35 → uc2_rest-0.2.0.38}/uc2rest/ledmatrix.py +0 -0
- {uc2_rest-0.2.0.35 → uc2_rest-0.2.0.38}/uc2rest/logger.py +0 -0
- {uc2_rest-0.2.0.35 → uc2_rest-0.2.0.38}/uc2rest/message.py +0 -0
- {uc2_rest-0.2.0.35 → uc2_rest-0.2.0.38}/uc2rest/modules.py +0 -0
- {uc2_rest-0.2.0.35 → uc2_rest-0.2.0.38}/uc2rest/motor_config.py +0 -0
- {uc2_rest-0.2.0.35 → uc2_rest-0.2.0.38}/uc2rest/objective.py +0 -0
- {uc2_rest-0.2.0.35 → uc2_rest-0.2.0.38}/uc2rest/pid.py +0 -0
- {uc2_rest-0.2.0.35 → uc2_rest-0.2.0.38}/uc2rest/rotator.py +0 -0
- {uc2_rest-0.2.0.35 → uc2_rest-0.2.0.38}/uc2rest/slm.py +0 -0
- {uc2_rest-0.2.0.35 → uc2_rest-0.2.0.38}/uc2rest/temperature.py +0 -0
- {uc2_rest-0.2.0.35 → uc2_rest-0.2.0.38}/uc2rest/utils.py +0 -0
- {uc2_rest-0.2.0.35 → uc2_rest-0.2.0.38}/uc2rest/wifi.py +0 -0
|
@@ -22,6 +22,8 @@ from .analog import Analog
|
|
|
22
22
|
from .modules import Modules
|
|
23
23
|
from .digitalout import DigitalOut
|
|
24
24
|
from .digitalin import DigitalIn
|
|
25
|
+
from .gpio import GPIO
|
|
26
|
+
from .i2c import I2C
|
|
25
27
|
from .rotator import Rotator
|
|
26
28
|
from .logger import Logger
|
|
27
29
|
from .cmdrecorder import cmdRecorder
|
|
@@ -47,9 +49,9 @@ class UC2Client(object):
|
|
|
47
49
|
is_serial = False
|
|
48
50
|
BAUDRATE = 115200
|
|
49
51
|
|
|
50
|
-
def __init__(self, host=None, port=31950, serialport=None, identity="UC2_Feather", baudrate=BAUDRATE,
|
|
52
|
+
def __init__(self, host=None, port=31950, serialport=None, identity="UC2_Feather", baudrate=BAUDRATE,
|
|
51
53
|
NLeds=64, SerialManager=None, DEBUG=False, logger=None, skipFirmwareCheck=False,
|
|
52
|
-
isPyScript=False):
|
|
54
|
+
isPyScript=False, device_id=None, requireMaster=False):
|
|
53
55
|
'''
|
|
54
56
|
This client connects to the UC2-REST microcontroller that can be found here
|
|
55
57
|
https://github.com/openUC2/UC2-REST
|
|
@@ -80,7 +82,7 @@ class UC2Client(object):
|
|
|
80
82
|
# initialize communication channel (serial only)
|
|
81
83
|
if serialport is not None:
|
|
82
84
|
# use USB connection
|
|
83
|
-
self.serial = Serial(serialport, baudrate, parent=self, identity=identity, DEBUG=DEBUG, skipFirmwareCheck=skipFirmwareCheck)
|
|
85
|
+
self.serial = Serial(serialport, baudrate, parent=self, identity=identity, DEBUG=DEBUG, skipFirmwareCheck=skipFirmwareCheck, device_id=device_id, requireMaster=requireMaster)
|
|
84
86
|
self.is_serial = True
|
|
85
87
|
self.is_connected = self.serial.is_connected
|
|
86
88
|
self.serial.DEBUG = DEBUG
|
|
@@ -155,7 +157,13 @@ class UC2Client(object):
|
|
|
155
157
|
|
|
156
158
|
# initialize digital in
|
|
157
159
|
self.digitalin = DigitalIn(self)
|
|
158
|
-
|
|
160
|
+
|
|
161
|
+
# initialize GPIO / collision-detector slave interface
|
|
162
|
+
self.gpio = GPIO(self)
|
|
163
|
+
|
|
164
|
+
# initialize generic I2C passthrough (sensors on the GPIO slave bus)
|
|
165
|
+
self.i2c = I2C(self)
|
|
166
|
+
|
|
159
167
|
# initialize messaging
|
|
160
168
|
self.message = Message(self)
|
|
161
169
|
|
|
@@ -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.38"
|
|
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)
|
|
@@ -0,0 +1,171 @@
|
|
|
1
|
+
"""GPIO / collision-detector interface for the UC2 CAN GPIO slave.
|
|
2
|
+
|
|
3
|
+
The CAN master exposes the GPIO slave (XIAO node, default CAN node-id 60)
|
|
4
|
+
through two serial endpoints:
|
|
5
|
+
|
|
6
|
+
{"task":"/gpio_get"}
|
|
7
|
+
-> {"gpio":{"node":60,"mean":585,"filtered":584,"raw":581,
|
|
8
|
+
"reference":585,"threshold":150,"sensitivity":4,
|
|
9
|
+
"trip":0,"estop":0},"ok":true,"qid":N}
|
|
10
|
+
|
|
11
|
+
{"task":"/gpio_act","threshold":150,"sensitivity":4,
|
|
12
|
+
"reference":585,"calibrate":1} (all value fields optional)
|
|
13
|
+
|
|
14
|
+
Collision detection runs ON THE SLAVE: the sensor has an idle value (the
|
|
15
|
+
"reference"); a collision = `sensitivity` consecutive samples deviating more
|
|
16
|
+
than `threshold` ADC counts from the reference (rising OR falling). Sensor
|
|
17
|
+
values are never streamed on the CAN bus — polling /gpio_get triggers SDO
|
|
18
|
+
reads on demand. Only the collision/E-stop EVENT is pushed asynchronously;
|
|
19
|
+
the master then prints:
|
|
20
|
+
|
|
21
|
+
{"gpio":{"event":1,"node":60,"trip":1,"estop":0,"flags":3,
|
|
22
|
+
"filtered":2365,"raw":2937,...}}
|
|
23
|
+
|
|
24
|
+
The "event":1 marker distinguishes pushed edges from /gpio_get responses
|
|
25
|
+
(both share the top-level "gpio" key and therefore both arrive at the same
|
|
26
|
+
serial callback).
|
|
27
|
+
"""
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
class GPIO(object):
|
|
31
|
+
def __init__(self, parent):
|
|
32
|
+
self._parent = parent
|
|
33
|
+
|
|
34
|
+
# last known state (merged from get_status responses and async events)
|
|
35
|
+
self.status = {}
|
|
36
|
+
|
|
37
|
+
# register a callback for any {"gpio":{...}} frame on the serial loop
|
|
38
|
+
if hasattr(self._parent, "serial"):
|
|
39
|
+
self._parent.serial.register_callback(self._callback_gpio, pattern="gpio")
|
|
40
|
+
|
|
41
|
+
# user-registered callbacks
|
|
42
|
+
self._collision_callbacks = [] # fired on async trip/clear events
|
|
43
|
+
self._status_callbacks = [] # fired on every gpio frame
|
|
44
|
+
|
|
45
|
+
# ─────────────────────────────────────────────────────────────────
|
|
46
|
+
# Async event plumbing
|
|
47
|
+
# ─────────────────────────────────────────────────────────────────
|
|
48
|
+
def _callback_gpio(self, data):
|
|
49
|
+
"""Handle any serial frame with a top-level "gpio" key.
|
|
50
|
+
|
|
51
|
+
Pushed events carry "event":1 and fire the collision callbacks;
|
|
52
|
+
/gpio_get responses only refresh the cached status."""
|
|
53
|
+
try:
|
|
54
|
+
gpio = data.get("gpio", None)
|
|
55
|
+
if not isinstance(gpio, dict):
|
|
56
|
+
return
|
|
57
|
+
self.status.update(gpio)
|
|
58
|
+
|
|
59
|
+
for cb in self._status_callbacks:
|
|
60
|
+
try:
|
|
61
|
+
cb(dict(self.status))
|
|
62
|
+
except Exception as e:
|
|
63
|
+
print(f"Error in gpio status callback: {e}")
|
|
64
|
+
|
|
65
|
+
if gpio.get("event", 0):
|
|
66
|
+
for cb in self._collision_callbacks:
|
|
67
|
+
try:
|
|
68
|
+
cb(dict(gpio))
|
|
69
|
+
except Exception as e:
|
|
70
|
+
print(f"Error in gpio collision callback: {e}")
|
|
71
|
+
except Exception as e:
|
|
72
|
+
print("Error in _callback_gpio: ", e)
|
|
73
|
+
|
|
74
|
+
def register_collision_callback(self, callbackfct):
|
|
75
|
+
"""Register a function called on every asynchronously pushed GPIO
|
|
76
|
+
event (collision trip/clear, E-stop press/release). The callback
|
|
77
|
+
receives the event dict, e.g. {"event":1,"trip":1,"estop":0,...}.
|
|
78
|
+
Check ["trip"] to distinguish collision-began from collision-cleared."""
|
|
79
|
+
if callbackfct not in self._collision_callbacks:
|
|
80
|
+
self._collision_callbacks.append(callbackfct)
|
|
81
|
+
|
|
82
|
+
def register_status_callback(self, callbackfct):
|
|
83
|
+
"""Register a function called on EVERY gpio frame (async events and
|
|
84
|
+
/gpio_get responses alike) with the merged status dict."""
|
|
85
|
+
if callbackfct not in self._status_callbacks:
|
|
86
|
+
self._status_callbacks.append(callbackfct)
|
|
87
|
+
|
|
88
|
+
# ─────────────────────────────────────────────────────────────────
|
|
89
|
+
# Query / configuration
|
|
90
|
+
# ─────────────────────────────────────────────────────────────────
|
|
91
|
+
@staticmethod
|
|
92
|
+
def _extract_gpio(response):
|
|
93
|
+
"""Unwrap a post_json return into the "gpio" dict.
|
|
94
|
+
|
|
95
|
+
mserial.sendMessage returns a LIST of response dicts on success
|
|
96
|
+
(self.responses[qid] is a per-frame list), a plain string on
|
|
97
|
+
timeout/interrupt, or an int qid when getReturn=False — so handle
|
|
98
|
+
all shapes defensively."""
|
|
99
|
+
candidates = response if isinstance(response, list) else [response]
|
|
100
|
+
for item in candidates:
|
|
101
|
+
if isinstance(item, dict):
|
|
102
|
+
gpio = item.get("gpio")
|
|
103
|
+
if isinstance(gpio, dict):
|
|
104
|
+
return gpio
|
|
105
|
+
return {}
|
|
106
|
+
|
|
107
|
+
def get_status(self, node=None, timeout=1):
|
|
108
|
+
"""Poll the collision detector (SDO reads on the CAN bus).
|
|
109
|
+
|
|
110
|
+
Returns the "gpio" dict: mean (rolling average), filtered, raw,
|
|
111
|
+
baseline, sigma, mode (0=auto/1=manual), reference, threshold,
|
|
112
|
+
sensitivity, trip, estop."""
|
|
113
|
+
path = "/gpio_get"
|
|
114
|
+
payload = {"task": path}
|
|
115
|
+
if node is not None:
|
|
116
|
+
payload["node"] = node
|
|
117
|
+
r = self._parent.post_json(path, payload, getReturn=True, timeout=timeout)
|
|
118
|
+
gpio = self._extract_gpio(r)
|
|
119
|
+
if gpio:
|
|
120
|
+
self.status.update(gpio)
|
|
121
|
+
return gpio
|
|
122
|
+
# Response didn't parse (timeout/odd shape) — fall back to the cache
|
|
123
|
+
# maintained by the async serial callback, which sees every frame.
|
|
124
|
+
return dict(self.status)
|
|
125
|
+
|
|
126
|
+
def _act(self, node=None, timeout=1, **fields):
|
|
127
|
+
path = "/gpio_act"
|
|
128
|
+
payload = {"task": path}
|
|
129
|
+
if node is not None:
|
|
130
|
+
payload["node"] = node
|
|
131
|
+
payload.update(fields)
|
|
132
|
+
return self._parent.post_json(path, payload, getReturn=True, timeout=timeout)
|
|
133
|
+
|
|
134
|
+
def set_threshold(self, threshold, node=None, timeout=1):
|
|
135
|
+
"""Deviation band (ADC counts) around the reference outside which a
|
|
136
|
+
sample votes for "collision". Persisted in the slave's NVS."""
|
|
137
|
+
return self._act(node=node, timeout=timeout, threshold=int(threshold))
|
|
138
|
+
|
|
139
|
+
def set_sensitivity(self, sensitivity, node=None, timeout=1):
|
|
140
|
+
"""Number of CONSECUTIVE out-of-band samples (at 50 Hz) required to
|
|
141
|
+
trip — and in-band samples to clear. 3-4 rejects single-sample spikes
|
|
142
|
+
while confirming a real collision within ~60-80 ms."""
|
|
143
|
+
return self._act(node=node, timeout=timeout, sensitivity=int(sensitivity))
|
|
144
|
+
|
|
145
|
+
def set_reference(self, reference, node=None, timeout=1):
|
|
146
|
+
"""Explicitly set the idle baseline (ADC counts). Typically you poll
|
|
147
|
+
get_status()["mean"] while the system is idle and write that value
|
|
148
|
+
here — or simply use calibrate()."""
|
|
149
|
+
return self._act(node=node, timeout=timeout, reference=int(reference))
|
|
150
|
+
|
|
151
|
+
def calibrate(self, node=None, timeout=1):
|
|
152
|
+
"""Tell the slave to take its CURRENT rolling mean as the new
|
|
153
|
+
reference (persisted in NVS). MANUAL mode only — in AUTO the baseline
|
|
154
|
+
tracks itself. Do this while the system is idle and collision-free."""
|
|
155
|
+
return self._act(node=node, timeout=timeout, calibrate=1)
|
|
156
|
+
|
|
157
|
+
def set_mode(self, mode, node=None, timeout=1):
|
|
158
|
+
"""Select the detection mode (persisted on the slave in NVS).
|
|
159
|
+
|
|
160
|
+
mode: "auto" (default, recommended) — adaptive baseline + robust
|
|
161
|
+
sigma z-score. Parameter-free: tracks a slowly drifting
|
|
162
|
+
background and trips on a fast deflection. No calibration.
|
|
163
|
+
mode: "manual" — fixed reference +/- threshold. Deterministic;
|
|
164
|
+
requires the reference to be calibrated to the idle level.
|
|
165
|
+
|
|
166
|
+
Accepts "auto"/"manual" or 0/1."""
|
|
167
|
+
if isinstance(mode, str):
|
|
168
|
+
mval = 1 if mode.lower() == "manual" else 0
|
|
169
|
+
else:
|
|
170
|
+
mval = 1 if int(mode) else 0
|
|
171
|
+
return self._act(node=node, timeout=timeout, mode=mval)
|