UC2-REST 0.2.0.30__tar.gz → 0.2.0.32__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.30 → uc2_rest-0.2.0.32}/PKG-INFO +1 -1
- {uc2_rest-0.2.0.30 → uc2_rest-0.2.0.32}/UC2_REST.egg-info/PKG-INFO +1 -1
- {uc2_rest-0.2.0.30 → uc2_rest-0.2.0.32}/UC2_REST.egg-info/SOURCES.txt +4 -0
- {uc2_rest-0.2.0.30 → uc2_rest-0.2.0.32}/uc2rest/UC2Client.py +12 -0
- {uc2_rest-0.2.0.30 → uc2_rest-0.2.0.32}/uc2rest/__version__.py +1 -1
- uc2_rest-0.2.0.32/uc2rest/camera_trigger.py +197 -0
- uc2_rest-0.2.0.32/uc2rest/can.py +143 -0
- uc2_rest-0.2.0.32/uc2rest/canota.py +256 -0
- uc2_rest-0.2.0.32/uc2rest/digitalin.py +122 -0
- uc2_rest-0.2.0.32/uc2rest/digitalout.py +187 -0
- uc2_rest-0.2.0.32/uc2rest/home.py +314 -0
- uc2_rest-0.2.0.32/uc2rest/laser.py +143 -0
- {uc2_rest-0.2.0.30 → uc2_rest-0.2.0.32}/uc2rest/motor.py +607 -84
- uc2_rest-0.2.0.32/uc2rest/motor_config.py +315 -0
- {uc2_rest-0.2.0.30 → uc2_rest-0.2.0.32}/uc2rest/mserial.py +112 -96
- {uc2_rest-0.2.0.30 → uc2_rest-0.2.0.32}/uc2rest/objective.py +35 -33
- {uc2_rest-0.2.0.30 → uc2_rest-0.2.0.32}/uc2rest/state.py +1 -1
- uc2_rest-0.2.0.30/uc2rest/can.py +0 -33
- uc2_rest-0.2.0.30/uc2rest/digitalout.py +0 -65
- uc2_rest-0.2.0.30/uc2rest/home.py +0 -125
- uc2_rest-0.2.0.30/uc2rest/laser.py +0 -77
- {uc2_rest-0.2.0.30 → uc2_rest-0.2.0.32}/LICENSE +0 -0
- {uc2_rest-0.2.0.30 → uc2_rest-0.2.0.32}/README.md +0 -0
- {uc2_rest-0.2.0.30 → uc2_rest-0.2.0.32}/UC2_REST.egg-info/dependency_links.txt +0 -0
- {uc2_rest-0.2.0.30 → uc2_rest-0.2.0.32}/UC2_REST.egg-info/not-zip-safe +0 -0
- {uc2_rest-0.2.0.30 → uc2_rest-0.2.0.32}/UC2_REST.egg-info/requires.txt +0 -0
- {uc2_rest-0.2.0.30 → uc2_rest-0.2.0.32}/UC2_REST.egg-info/top_level.txt +0 -0
- {uc2_rest-0.2.0.30 → uc2_rest-0.2.0.32}/setup.cfg +0 -0
- {uc2_rest-0.2.0.30 → uc2_rest-0.2.0.32}/setup.py +0 -0
- {uc2_rest-0.2.0.30 → uc2_rest-0.2.0.32}/uc2rest/MockSerial.py +0 -0
- {uc2_rest-0.2.0.30 → uc2_rest-0.2.0.32}/uc2rest/__init__.py +0 -0
- {uc2_rest-0.2.0.30 → uc2_rest-0.2.0.32}/uc2rest/analog.py +0 -0
- {uc2_rest-0.2.0.30 → uc2_rest-0.2.0.32}/uc2rest/camera.py +0 -0
- {uc2_rest-0.2.0.30 → uc2_rest-0.2.0.32}/uc2rest/cmdrecorder.py +0 -0
- {uc2_rest-0.2.0.30 → uc2_rest-0.2.0.32}/uc2rest/galvo.py +0 -0
- {uc2_rest-0.2.0.30 → uc2_rest-0.2.0.32}/uc2rest/gripper.py +0 -0
- {uc2_rest-0.2.0.30 → uc2_rest-0.2.0.32}/uc2rest/lcddisplay.py +0 -0
- {uc2_rest-0.2.0.30 → uc2_rest-0.2.0.32}/uc2rest/ledmatrix.py +0 -0
- {uc2_rest-0.2.0.30 → uc2_rest-0.2.0.32}/uc2rest/logger.py +0 -0
- {uc2_rest-0.2.0.30 → uc2_rest-0.2.0.32}/uc2rest/message.py +0 -0
- {uc2_rest-0.2.0.30 → uc2_rest-0.2.0.32}/uc2rest/modules.py +0 -0
- {uc2_rest-0.2.0.30 → uc2_rest-0.2.0.32}/uc2rest/pid.py +0 -0
- {uc2_rest-0.2.0.30 → uc2_rest-0.2.0.32}/uc2rest/rotator.py +0 -0
- {uc2_rest-0.2.0.30 → uc2_rest-0.2.0.32}/uc2rest/slm.py +0 -0
- {uc2_rest-0.2.0.30 → uc2_rest-0.2.0.32}/uc2rest/temperature.py +0 -0
- {uc2_rest-0.2.0.30 → uc2_rest-0.2.0.32}/uc2rest/utils.py +0 -0
- {uc2_rest-0.2.0.30 → uc2_rest-0.2.0.32}/uc2rest/wifi.py +0 -0
|
@@ -13,8 +13,11 @@ uc2rest/__init__.py
|
|
|
13
13
|
uc2rest/__version__.py
|
|
14
14
|
uc2rest/analog.py
|
|
15
15
|
uc2rest/camera.py
|
|
16
|
+
uc2rest/camera_trigger.py
|
|
16
17
|
uc2rest/can.py
|
|
18
|
+
uc2rest/canota.py
|
|
17
19
|
uc2rest/cmdrecorder.py
|
|
20
|
+
uc2rest/digitalin.py
|
|
18
21
|
uc2rest/digitalout.py
|
|
19
22
|
uc2rest/galvo.py
|
|
20
23
|
uc2rest/gripper.py
|
|
@@ -26,6 +29,7 @@ uc2rest/logger.py
|
|
|
26
29
|
uc2rest/message.py
|
|
27
30
|
uc2rest/modules.py
|
|
28
31
|
uc2rest/motor.py
|
|
32
|
+
uc2rest/motor_config.py
|
|
29
33
|
uc2rest/mserial.py
|
|
30
34
|
uc2rest/objective.py
|
|
31
35
|
uc2rest/pid.py
|
|
@@ -21,12 +21,15 @@ from .camera import Camera
|
|
|
21
21
|
from .analog import Analog
|
|
22
22
|
from .modules import Modules
|
|
23
23
|
from .digitalout import DigitalOut
|
|
24
|
+
from .digitalin import DigitalIn
|
|
24
25
|
from .rotator import Rotator
|
|
25
26
|
from .logger import Logger
|
|
26
27
|
from .cmdrecorder import cmdRecorder
|
|
27
28
|
from .temperature import Temperature
|
|
28
29
|
from .message import Message
|
|
29
30
|
from .can import CAN
|
|
31
|
+
from .canota import CANOTA
|
|
32
|
+
from .camera_trigger import CameraTrigger
|
|
30
33
|
try:
|
|
31
34
|
import requests
|
|
32
35
|
except:
|
|
@@ -109,6 +112,9 @@ class UC2Client(object):
|
|
|
109
112
|
# initialize CAN
|
|
110
113
|
self.can = CAN(self)
|
|
111
114
|
|
|
115
|
+
# initialize CAN OTA
|
|
116
|
+
self.canota = CANOTA(self)
|
|
117
|
+
|
|
112
118
|
# initialize gripper
|
|
113
119
|
self.gripper = Gripper(self)
|
|
114
120
|
|
|
@@ -145,8 +151,14 @@ class UC2Client(object):
|
|
|
145
151
|
# initialize digital out
|
|
146
152
|
self.digitalout = DigitalOut(self)
|
|
147
153
|
|
|
154
|
+
# initialize digital in
|
|
155
|
+
self.digitalin = DigitalIn(self)
|
|
156
|
+
|
|
148
157
|
# initialize messaging
|
|
149
158
|
self.message = Message(self)
|
|
159
|
+
|
|
160
|
+
# initialize camera trigger callback handler
|
|
161
|
+
self.camera_trigger = CameraTrigger(self)
|
|
150
162
|
|
|
151
163
|
# initialize module controller
|
|
152
164
|
self.modules = Modules(parent=self)
|
|
@@ -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.32"
|
|
10
10
|
__author__ = 'Benedict Diederich'
|
|
11
11
|
__author_email__ = 'benedictdied@gmail.com'
|
|
12
12
|
__license__ = 'GPL v3'
|
|
@@ -0,0 +1,197 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Camera trigger callback module for UC2-REST.
|
|
3
|
+
|
|
4
|
+
This module handles camera trigger signals from the firmware ({"cam":1})
|
|
5
|
+
to enable software triggering based on hardware events during stage scanning.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
import numpy as np
|
|
9
|
+
import time
|
|
10
|
+
import json
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
gTIMEOUT = 1 # seconds to wait for a response from the ESP32
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class CameraTrigger(object):
|
|
17
|
+
"""
|
|
18
|
+
This class parses incoming camera trigger signals from the ESP32 firmware.
|
|
19
|
+
|
|
20
|
+
When the firmware sends {"cam":1}, this module triggers registered callbacks
|
|
21
|
+
which can be used for software-triggered image acquisition during stage scanning.
|
|
22
|
+
|
|
23
|
+
Example usage:
|
|
24
|
+
import uc2rest
|
|
25
|
+
|
|
26
|
+
ESP32 = uc2rest.UC2Client(serialport=port, baudrate=500000)
|
|
27
|
+
|
|
28
|
+
# Register callback for camera trigger
|
|
29
|
+
def my_camera_callback(data):
|
|
30
|
+
print(f"Camera trigger received: {data}")
|
|
31
|
+
# Trigger image acquisition here
|
|
32
|
+
|
|
33
|
+
ESP32.camera_trigger.register_callback(0, my_camera_callback)
|
|
34
|
+
"""
|
|
35
|
+
|
|
36
|
+
def __init__(self, parent=None, nCallbacks=10):
|
|
37
|
+
"""
|
|
38
|
+
Initialize camera trigger handler.
|
|
39
|
+
|
|
40
|
+
Args:
|
|
41
|
+
parent: Parent UC2Client instance
|
|
42
|
+
nCallbacks: Maximum number of callback functions to register
|
|
43
|
+
"""
|
|
44
|
+
self._parent = parent
|
|
45
|
+
self.nCallbacks = nCallbacks
|
|
46
|
+
|
|
47
|
+
# Track trigger count for diagnostics
|
|
48
|
+
self._trigger_count = 0
|
|
49
|
+
self._last_trigger_time = None
|
|
50
|
+
|
|
51
|
+
# Initialize callback functions
|
|
52
|
+
self._callbackPerKey = {}
|
|
53
|
+
self.init_callback_functions(self.nCallbacks)
|
|
54
|
+
|
|
55
|
+
# Register callback for camera trigger on serial loop
|
|
56
|
+
if hasattr(self._parent, "serial"):
|
|
57
|
+
self._parent.serial.register_callback(self._callback_camera_trigger, pattern="cam")
|
|
58
|
+
|
|
59
|
+
def _callback_camera_trigger(self, data):
|
|
60
|
+
"""
|
|
61
|
+
Parse camera trigger message from firmware.
|
|
62
|
+
|
|
63
|
+
Expected JSON format:
|
|
64
|
+
{
|
|
65
|
+
"cam": 1 # Trigger signal
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
or with additional data:
|
|
69
|
+
{
|
|
70
|
+
"cam": {
|
|
71
|
+
"trigger": 1,
|
|
72
|
+
"frame_id": 123,
|
|
73
|
+
"illumination": 0
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
Args:
|
|
78
|
+
data: JSON data dictionary from firmware
|
|
79
|
+
"""
|
|
80
|
+
try:
|
|
81
|
+
# Update trigger statistics
|
|
82
|
+
self._trigger_count += 1
|
|
83
|
+
self._last_trigger_time = time.time()
|
|
84
|
+
|
|
85
|
+
# Extract trigger information
|
|
86
|
+
cam_data = data.get("cam", {})
|
|
87
|
+
|
|
88
|
+
# Handle simple trigger ({"cam": 1})
|
|
89
|
+
if isinstance(cam_data, (int, float)):
|
|
90
|
+
trigger_info = {
|
|
91
|
+
"trigger": int(cam_data),
|
|
92
|
+
"frame_id": self._trigger_count,
|
|
93
|
+
"timestamp": self._last_trigger_time
|
|
94
|
+
}
|
|
95
|
+
self._parent.logger.debug(f"Camera trigger received: {trigger_info}")
|
|
96
|
+
else:
|
|
97
|
+
# Handle extended trigger data
|
|
98
|
+
trigger_info = {
|
|
99
|
+
"trigger": cam_data.get("trigger", 1),
|
|
100
|
+
"frame_id": cam_data.get("frame_id", self._trigger_count),
|
|
101
|
+
"illumination": cam_data.get("illumination", -1),
|
|
102
|
+
"timestamp": self._last_trigger_time
|
|
103
|
+
}
|
|
104
|
+
self._parent.logger.debug(f"Camera trigger with data received: {trigger_info}")
|
|
105
|
+
|
|
106
|
+
# Call all registered callbacks
|
|
107
|
+
for key, callback in self._callbackPerKey.items():
|
|
108
|
+
if callback is not None and callable(callback):
|
|
109
|
+
try:
|
|
110
|
+
callback(trigger_info)
|
|
111
|
+
except Exception as callback_error:
|
|
112
|
+
print(f"Error in camera trigger callback {key}: {callback_error}")
|
|
113
|
+
|
|
114
|
+
except Exception as e:
|
|
115
|
+
print(f"Error in _callback_camera_trigger: {e}")
|
|
116
|
+
|
|
117
|
+
def init_callback_functions(self, nCallbacks=10):
|
|
118
|
+
"""
|
|
119
|
+
Initialize callback function dictionary.
|
|
120
|
+
|
|
121
|
+
Args:
|
|
122
|
+
nCallbacks: Number of callback slots to create
|
|
123
|
+
"""
|
|
124
|
+
self._callbackPerKey = {}
|
|
125
|
+
self.nCallbacks = nCallbacks
|
|
126
|
+
for i in range(nCallbacks):
|
|
127
|
+
self._callbackPerKey[i] = None
|
|
128
|
+
|
|
129
|
+
def register_callback(self, key, callback):
|
|
130
|
+
"""
|
|
131
|
+
Register a callback function for camera trigger events.
|
|
132
|
+
|
|
133
|
+
Args:
|
|
134
|
+
key: Integer key (0 to nCallbacks-1) for this callback
|
|
135
|
+
callback: Function to call when trigger is received.
|
|
136
|
+
Function signature: callback(trigger_info: dict)
|
|
137
|
+
|
|
138
|
+
Example:
|
|
139
|
+
def on_camera_trigger(info):
|
|
140
|
+
print(f"Frame {info['frame_id']} triggered at {info['timestamp']}")
|
|
141
|
+
camera.snap_image()
|
|
142
|
+
|
|
143
|
+
ESP32.camera_trigger.register_callback(0, on_camera_trigger)
|
|
144
|
+
"""
|
|
145
|
+
if key < 0 or key >= self.nCallbacks:
|
|
146
|
+
raise ValueError(f"Callback key must be between 0 and {self.nCallbacks-1}")
|
|
147
|
+
self._callbackPerKey[key] = callback
|
|
148
|
+
|
|
149
|
+
def unregister_callback(self, key):
|
|
150
|
+
"""
|
|
151
|
+
Remove a registered callback.
|
|
152
|
+
|
|
153
|
+
Args:
|
|
154
|
+
key: Integer key of callback to remove
|
|
155
|
+
"""
|
|
156
|
+
if key in self._callbackPerKey:
|
|
157
|
+
self._callbackPerKey[key] = None
|
|
158
|
+
|
|
159
|
+
def clear_all_callbacks(self):
|
|
160
|
+
"""Remove all registered callbacks."""
|
|
161
|
+
self.init_callback_functions(self.nCallbacks)
|
|
162
|
+
|
|
163
|
+
def get_trigger_count(self):
|
|
164
|
+
"""
|
|
165
|
+
Get the total number of triggers received since initialization.
|
|
166
|
+
|
|
167
|
+
Returns:
|
|
168
|
+
int: Number of camera triggers received
|
|
169
|
+
"""
|
|
170
|
+
return self._trigger_count
|
|
171
|
+
|
|
172
|
+
def get_last_trigger_time(self):
|
|
173
|
+
"""
|
|
174
|
+
Get the timestamp of the last trigger received.
|
|
175
|
+
|
|
176
|
+
Returns:
|
|
177
|
+
float or None: Unix timestamp of last trigger, or None if no triggers received
|
|
178
|
+
"""
|
|
179
|
+
return self._last_trigger_time
|
|
180
|
+
|
|
181
|
+
def reset_trigger_count(self):
|
|
182
|
+
"""Reset the trigger counter to zero."""
|
|
183
|
+
self._trigger_count = 0
|
|
184
|
+
self._last_trigger_time = None
|
|
185
|
+
|
|
186
|
+
def get_trigger_stats(self):
|
|
187
|
+
"""
|
|
188
|
+
Get statistics about trigger events.
|
|
189
|
+
|
|
190
|
+
Returns:
|
|
191
|
+
dict: Dictionary with trigger statistics
|
|
192
|
+
"""
|
|
193
|
+
return {
|
|
194
|
+
"total_triggers": self._trigger_count,
|
|
195
|
+
"last_trigger_time": self._last_trigger_time,
|
|
196
|
+
"callbacks_registered": sum(1 for cb in self._callbackPerKey.values() if cb is not None)
|
|
197
|
+
}
|
|
@@ -0,0 +1,143 @@
|
|
|
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 reboot_remote(self, qid=1, can_address=0, isBlocking=False, timeout=2):
|
|
61
|
+
"""
|
|
62
|
+
Send a reboot signal to the remote CAN device.
|
|
63
|
+
|
|
64
|
+
:param qid: Query ID for the CAN command (default: 1)
|
|
65
|
+
:param isBlocking: If True, wait for response
|
|
66
|
+
:param timeout: Timeout for the command in seconds
|
|
67
|
+
:param can_address: Address of the CAN device to reboot (0 is master)
|
|
68
|
+
:return: Response from the device
|
|
69
|
+
"""
|
|
70
|
+
path = "/can_act"
|
|
71
|
+
payload = {
|
|
72
|
+
"task": path,
|
|
73
|
+
"restart": int(can_address)
|
|
74
|
+
}
|
|
75
|
+
nResponses = 1 if isBlocking else 0
|
|
76
|
+
# Send the payload to the parent, which handles the actual communication
|
|
77
|
+
return self._parent.post_json(
|
|
78
|
+
path,
|
|
79
|
+
payload,
|
|
80
|
+
getReturn=isBlocking,
|
|
81
|
+
timeout=timeout if isBlocking else 0,
|
|
82
|
+
nResponses=nResponses
|
|
83
|
+
)
|
|
84
|
+
|
|
85
|
+
def scan(self, qid=1, timeout=5):
|
|
86
|
+
"""
|
|
87
|
+
Scan the CAN bus for connected devices.
|
|
88
|
+
|
|
89
|
+
:param qid: Query ID for the CAN command (default: 1)
|
|
90
|
+
:param timeout: Timeout for the scan in seconds (default: 5)
|
|
91
|
+
:return: Response containing scan results with device information
|
|
92
|
+
Example: {
|
|
93
|
+
"scan": [
|
|
94
|
+
{"canId": 10, "deviceType": 0, "deviceTypeStr": "motor", "status": 0, "statusStr": "idle"},
|
|
95
|
+
{"canId": 20, "deviceType": 1, "deviceTypeStr": "laser", "status": 0, "statusStr": "idle"}
|
|
96
|
+
],
|
|
97
|
+
"qid": 1,
|
|
98
|
+
"count": 2
|
|
99
|
+
}
|
|
100
|
+
"""
|
|
101
|
+
path = "/can_act"
|
|
102
|
+
payload = {
|
|
103
|
+
"task": path,
|
|
104
|
+
"scan": True,
|
|
105
|
+
"qid": qid
|
|
106
|
+
}
|
|
107
|
+
return self._parent.post_json(
|
|
108
|
+
path,
|
|
109
|
+
payload,
|
|
110
|
+
getReturn=True,
|
|
111
|
+
timeout=timeout,
|
|
112
|
+
nResponses=2
|
|
113
|
+
)
|
|
114
|
+
|
|
115
|
+
def get_available_devices(self, timeout=2):
|
|
116
|
+
"""
|
|
117
|
+
Get list of available CAN devices.
|
|
118
|
+
|
|
119
|
+
:param timeout: Timeout for the command in seconds (default: 2)
|
|
120
|
+
:return: Response containing available CAN IDs and other bus information
|
|
121
|
+
Example: {
|
|
122
|
+
"input": {"task": "/can_get"},
|
|
123
|
+
"address": 1,
|
|
124
|
+
"addresspref": 1,
|
|
125
|
+
"addressgetcan": 1,
|
|
126
|
+
"nonworking": [0,0,0,...],
|
|
127
|
+
"available": [20,0,0,...],
|
|
128
|
+
"rx": 18,
|
|
129
|
+
"tx": 17
|
|
130
|
+
}
|
|
131
|
+
"""
|
|
132
|
+
path = "/can_act"
|
|
133
|
+
payload = {
|
|
134
|
+
"task": path,
|
|
135
|
+
"scan": True
|
|
136
|
+
}
|
|
137
|
+
return self._parent.post_json(
|
|
138
|
+
path,
|
|
139
|
+
payload,
|
|
140
|
+
getReturn=True,
|
|
141
|
+
timeout=timeout,
|
|
142
|
+
nResponses=2
|
|
143
|
+
)
|
|
@@ -0,0 +1,256 @@
|
|
|
1
|
+
import time
|
|
2
|
+
import json
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
gTIMEOUT = 10 # seconds to wait for a response from the ESP32
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class CANOTA(object):
|
|
9
|
+
"""
|
|
10
|
+
CAN OTA (Over-The-Air) update controller for UC2 satellite devices.
|
|
11
|
+
|
|
12
|
+
This class handles sending OTA commands to CAN slave devices and processing
|
|
13
|
+
the responses through callbacks. It manages the WiFi connection setup and
|
|
14
|
+
OTA server initialization on remote devices.
|
|
15
|
+
|
|
16
|
+
The OTA process involves:
|
|
17
|
+
1. Sending OTA command with WiFi credentials to a specific CAN device
|
|
18
|
+
2. Device connects to WiFi and starts ArduinoOTA server
|
|
19
|
+
3. Device sends back acknowledgment and IP address via serial
|
|
20
|
+
4. Device becomes available as UC2-CAN-<HEXID>.local for firmware upload
|
|
21
|
+
"""
|
|
22
|
+
|
|
23
|
+
def __init__(self, parent=None, nCallbacks=10):
|
|
24
|
+
"""
|
|
25
|
+
Initialize the CAN OTA controller.
|
|
26
|
+
|
|
27
|
+
:param parent: Parent UC2Client object with post_json and serial interfaces
|
|
28
|
+
:param nCallbacks: Number of callback slots to initialize (default: 10)
|
|
29
|
+
"""
|
|
30
|
+
self._parent = parent
|
|
31
|
+
self.nCallbacks = nCallbacks
|
|
32
|
+
|
|
33
|
+
# Initialize callback functions for different types of OTA events
|
|
34
|
+
self.init_callback_functions(self.nCallbacks)
|
|
35
|
+
|
|
36
|
+
# Register callback function for OTA status messages on the serial loop
|
|
37
|
+
if hasattr(self._parent, "serial"):
|
|
38
|
+
self._parent.serial.register_callback(self._callback_ota_status, pattern="ota")
|
|
39
|
+
|
|
40
|
+
def init_callback_functions(self, nCallbacks=10):
|
|
41
|
+
"""
|
|
42
|
+
Initialize the callback function dictionary.
|
|
43
|
+
|
|
44
|
+
:param nCallbacks: Number of callback slots to create
|
|
45
|
+
"""
|
|
46
|
+
self._callbackPerKey = {}
|
|
47
|
+
self.nCallbacks = nCallbacks
|
|
48
|
+
for i in range(nCallbacks):
|
|
49
|
+
self._callbackPerKey[i] = None
|
|
50
|
+
|
|
51
|
+
def _callback_ota_status(self, data):
|
|
52
|
+
"""
|
|
53
|
+
Process incoming OTA status messages from CAN devices.
|
|
54
|
+
|
|
55
|
+
Expected message format:
|
|
56
|
+
{
|
|
57
|
+
"ota": {
|
|
58
|
+
"canId": 20,
|
|
59
|
+
"status": 0,
|
|
60
|
+
"statusMsg": "Success",
|
|
61
|
+
"ip": "192.168.2.137",
|
|
62
|
+
"hostname": "UC2-CAN-14.local"
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
Status codes:
|
|
67
|
+
- 0: Success (WiFi connected, OTA server started)
|
|
68
|
+
- 1: WiFi connection failed
|
|
69
|
+
- 2: OTA start failed
|
|
70
|
+
|
|
71
|
+
:param data: JSON data containing OTA status information
|
|
72
|
+
"""
|
|
73
|
+
try:
|
|
74
|
+
ota_data = data["ota"]
|
|
75
|
+
can_id = ota_data.get("canId")
|
|
76
|
+
status = ota_data.get("status")
|
|
77
|
+
status_msg = ota_data.get("statusMsg", "")
|
|
78
|
+
ip_address = ota_data.get("ip", "")
|
|
79
|
+
hostname = ota_data.get("hostname", "")
|
|
80
|
+
|
|
81
|
+
# Create a structured response for callbacks
|
|
82
|
+
ota_response = {
|
|
83
|
+
"canId": can_id,
|
|
84
|
+
"status": status,
|
|
85
|
+
"statusMsg": status_msg,
|
|
86
|
+
"ip": ip_address,
|
|
87
|
+
"hostname": hostname,
|
|
88
|
+
"success": status == 0
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
# Trigger callbacks for different events
|
|
92
|
+
# Key 0: General OTA status updates
|
|
93
|
+
if callable(self._callbackPerKey[0]):
|
|
94
|
+
self._callbackPerKey[0](ota_response)
|
|
95
|
+
|
|
96
|
+
# Key for specific CAN ID (use can_id % nCallbacks to avoid overflow)
|
|
97
|
+
if can_id is not None:
|
|
98
|
+
callback_key = (can_id % (self.nCallbacks - 1)) + 1 # Reserve key 0 for general
|
|
99
|
+
if callable(self._callbackPerKey[callback_key]):
|
|
100
|
+
self._callbackPerKey[callback_key](ota_response)
|
|
101
|
+
|
|
102
|
+
except Exception as e:
|
|
103
|
+
print(f"Error in _callback_ota_status: {e}")
|
|
104
|
+
|
|
105
|
+
def register_callback(self, key, callback_function):
|
|
106
|
+
"""
|
|
107
|
+
Register a callback function for OTA events.
|
|
108
|
+
|
|
109
|
+
:param key: Callback key (0 for general events, 1-9 for specific CAN IDs)
|
|
110
|
+
:param callback_function: Function to call when OTA event occurs
|
|
111
|
+
Function should accept one parameter: ota_response dict
|
|
112
|
+
|
|
113
|
+
Example:
|
|
114
|
+
def my_ota_callback(ota_response):
|
|
115
|
+
if ota_response["success"]:
|
|
116
|
+
print(f"Device {ota_response['canId']} ready at {ota_response['ip']}")
|
|
117
|
+
else:
|
|
118
|
+
print(f"OTA failed for device {ota_response['canId']}: {ota_response['statusMsg']}")
|
|
119
|
+
|
|
120
|
+
ESP32.canota.register_callback(0, my_ota_callback)
|
|
121
|
+
"""
|
|
122
|
+
if 0 <= key < self.nCallbacks:
|
|
123
|
+
self._callbackPerKey[key] = callback_function
|
|
124
|
+
else:
|
|
125
|
+
raise ValueError(f"Callback key must be between 0 and {self.nCallbacks-1}")
|
|
126
|
+
|
|
127
|
+
def start_ota_update(self, can_id, ssid, password, timeout=300000, is_blocking=False,
|
|
128
|
+
response_timeout=gTIMEOUT):
|
|
129
|
+
"""
|
|
130
|
+
Send OTA command to a specific CAN slave device.
|
|
131
|
+
|
|
132
|
+
This tells the slave to connect to WiFi and start an OTA server.
|
|
133
|
+
The device will become available as UC2-CAN-<HEXID>.local for firmware upload.
|
|
134
|
+
|
|
135
|
+
:param can_id: CAN ID of the target device (e.g., 11 for Motor X, 20 for Laser)
|
|
136
|
+
:param ssid: WiFi network name
|
|
137
|
+
:param password: WiFi password
|
|
138
|
+
:param timeout: OTA timeout in milliseconds (default: 300000 = 5 minutes)
|
|
139
|
+
:param is_blocking: If True, wait for acknowledgment response
|
|
140
|
+
:param response_timeout: Timeout for response in seconds
|
|
141
|
+
:return: Response from the device (if blocking)
|
|
142
|
+
|
|
143
|
+
Examples:
|
|
144
|
+
# Motor X (CAN ID 11)
|
|
145
|
+
ESP32.canota.start_ota_update(11, "WiFi", "pass123")
|
|
146
|
+
|
|
147
|
+
# Laser (CAN ID 20) with custom timeout
|
|
148
|
+
ESP32.canota.start_ota_update(20, "WiFi", "pass123", timeout=600000)
|
|
149
|
+
"""
|
|
150
|
+
path = "/can_act"
|
|
151
|
+
payload = {
|
|
152
|
+
"task": path,
|
|
153
|
+
"ota": {
|
|
154
|
+
"canid": can_id,
|
|
155
|
+
"ssid": ssid,
|
|
156
|
+
"password": password,
|
|
157
|
+
"timeout": timeout
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
# Send the payload to the parent, which handles the actual communication
|
|
162
|
+
nResponses = 1 if is_blocking else 0
|
|
163
|
+
return self._parent.post_json(
|
|
164
|
+
path,
|
|
165
|
+
payload,
|
|
166
|
+
getReturn=is_blocking,
|
|
167
|
+
timeout=response_timeout if is_blocking else 0,
|
|
168
|
+
nResponses=nResponses
|
|
169
|
+
)
|
|
170
|
+
|
|
171
|
+
def start_motor_ota(self, motor_axis, ssid, password, timeout=300000, is_blocking=False):
|
|
172
|
+
"""
|
|
173
|
+
Convenience method for starting OTA on motor controllers.
|
|
174
|
+
|
|
175
|
+
:param motor_axis: Motor axis ("X", "Y", "Z") or CAN ID (11, 12, 13)
|
|
176
|
+
:param ssid: WiFi network name
|
|
177
|
+
:param password: WiFi password
|
|
178
|
+
:param timeout: OTA timeout in milliseconds
|
|
179
|
+
:param is_blocking: If True, wait for acknowledgment response
|
|
180
|
+
:return: Response from the device
|
|
181
|
+
|
|
182
|
+
Examples:
|
|
183
|
+
ESP32.canota.start_motor_ota("X", "WiFi", "pass123")
|
|
184
|
+
ESP32.canota.start_motor_ota(11, "WiFi", "pass123") # Same as above
|
|
185
|
+
"""
|
|
186
|
+
# Map motor axes to CAN IDs
|
|
187
|
+
motor_can_ids = {
|
|
188
|
+
"X": 11,
|
|
189
|
+
"Y": 12,
|
|
190
|
+
"Z": 13
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
if isinstance(motor_axis, str):
|
|
194
|
+
motor_axis = motor_axis.upper()
|
|
195
|
+
if motor_axis not in motor_can_ids:
|
|
196
|
+
raise ValueError(f"Invalid motor axis '{motor_axis}'. Use 'X', 'Y', or 'Z'")
|
|
197
|
+
can_id = motor_can_ids[motor_axis]
|
|
198
|
+
else:
|
|
199
|
+
can_id = motor_axis
|
|
200
|
+
|
|
201
|
+
return self.start_ota_update(can_id, ssid, password, timeout, is_blocking)
|
|
202
|
+
|
|
203
|
+
def start_led_ota(self, ssid, password, timeout=300000, is_blocking=False):
|
|
204
|
+
"""
|
|
205
|
+
Convenience method for starting OTA on LED controller (CAN ID 30).
|
|
206
|
+
|
|
207
|
+
:param ssid: WiFi network name
|
|
208
|
+
:param password: WiFi password
|
|
209
|
+
:param timeout: OTA timeout in milliseconds
|
|
210
|
+
:param is_blocking: If True, wait for acknowledgment response
|
|
211
|
+
:return: Response from the device
|
|
212
|
+
"""
|
|
213
|
+
return self.start_ota_update(30, ssid, password, timeout, is_blocking)
|
|
214
|
+
|
|
215
|
+
def start_laser_ota(self, laser_id=0, ssid="", password="", timeout=300000, is_blocking=False):
|
|
216
|
+
"""
|
|
217
|
+
Convenience method for starting OTA on laser controller.
|
|
218
|
+
|
|
219
|
+
:param laser_id: Laser ID (0 for main laser, maps to CAN ID 20)
|
|
220
|
+
:param ssid: WiFi network name
|
|
221
|
+
:param password: WiFi password
|
|
222
|
+
:param timeout: OTA timeout in milliseconds
|
|
223
|
+
:param is_blocking: If True, wait for acknowledgment response
|
|
224
|
+
:return: Response from the device
|
|
225
|
+
"""
|
|
226
|
+
# Map laser ID to CAN ID (for now, laser 0 = CAN ID 20)
|
|
227
|
+
can_id = 20 + laser_id
|
|
228
|
+
return self.start_ota_update(can_id, ssid, password, timeout, is_blocking)
|
|
229
|
+
|
|
230
|
+
def get_ota_hostname(self, can_id):
|
|
231
|
+
"""
|
|
232
|
+
Generate the expected hostname for a CAN device in OTA mode.
|
|
233
|
+
|
|
234
|
+
:param can_id: CAN ID of the device
|
|
235
|
+
:return: Expected hostname (e.g., "UC2-CAN-14.local" for CAN ID 20)
|
|
236
|
+
"""
|
|
237
|
+
hex_id = format(can_id, 'X') # Convert to hexadecimal
|
|
238
|
+
return f"UC2-CAN-{hex_id}.local"
|
|
239
|
+
|
|
240
|
+
def get_platformio_upload_command(self, can_id, project_path="."):
|
|
241
|
+
"""
|
|
242
|
+
Generate the PlatformIO upload command for OTA firmware update.
|
|
243
|
+
|
|
244
|
+
:param can_id: CAN ID of the target device
|
|
245
|
+
:param project_path: Path to the PlatformIO project (default: current directory)
|
|
246
|
+
:return: PlatformIO upload command string
|
|
247
|
+
|
|
248
|
+
Example:
|
|
249
|
+
cmd = ESP32.canota.get_platformio_upload_command(20)
|
|
250
|
+
# Returns: "platformio run -t upload --upload-port UC2-CAN-14.local"
|
|
251
|
+
"""
|
|
252
|
+
hostname = self.get_ota_hostname(can_id)
|
|
253
|
+
if project_path != ".":
|
|
254
|
+
return f"platformio run -t upload --upload-port {hostname} -d {project_path}"
|
|
255
|
+
else:
|
|
256
|
+
return f"platformio run -t upload --upload-port {hostname}"
|