UC2-REST 0.2.0.39__tar.gz → 0.2.0.40__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.39 → uc2_rest-0.2.0.40}/PKG-INFO +1 -1
- {uc2_rest-0.2.0.39 → uc2_rest-0.2.0.40}/UC2_REST.egg-info/PKG-INFO +1 -1
- {uc2_rest-0.2.0.39 → uc2_rest-0.2.0.40}/uc2rest/__version__.py +1 -1
- {uc2_rest-0.2.0.39 → uc2_rest-0.2.0.40}/uc2rest/galvo.py +67 -15
- {uc2_rest-0.2.0.39 → uc2_rest-0.2.0.40}/uc2rest/motor.py +188 -2
- {uc2_rest-0.2.0.39 → uc2_rest-0.2.0.40}/uc2rest/mserial.py +4 -4
- {uc2_rest-0.2.0.39 → uc2_rest-0.2.0.40}/LICENSE +0 -0
- {uc2_rest-0.2.0.39 → uc2_rest-0.2.0.40}/README.md +0 -0
- {uc2_rest-0.2.0.39 → uc2_rest-0.2.0.40}/UC2_REST.egg-info/SOURCES.txt +0 -0
- {uc2_rest-0.2.0.39 → uc2_rest-0.2.0.40}/UC2_REST.egg-info/dependency_links.txt +0 -0
- {uc2_rest-0.2.0.39 → uc2_rest-0.2.0.40}/UC2_REST.egg-info/not-zip-safe +0 -0
- {uc2_rest-0.2.0.39 → uc2_rest-0.2.0.40}/UC2_REST.egg-info/requires.txt +0 -0
- {uc2_rest-0.2.0.39 → uc2_rest-0.2.0.40}/UC2_REST.egg-info/top_level.txt +0 -0
- {uc2_rest-0.2.0.39 → uc2_rest-0.2.0.40}/setup.cfg +0 -0
- {uc2_rest-0.2.0.39 → uc2_rest-0.2.0.40}/setup.py +0 -0
- {uc2_rest-0.2.0.39 → uc2_rest-0.2.0.40}/uc2rest/MockSerial.py +0 -0
- {uc2_rest-0.2.0.39 → uc2_rest-0.2.0.40}/uc2rest/UC2Client.py +0 -0
- {uc2_rest-0.2.0.39 → uc2_rest-0.2.0.40}/uc2rest/__init__.py +0 -0
- {uc2_rest-0.2.0.39 → uc2_rest-0.2.0.40}/uc2rest/analog.py +0 -0
- {uc2_rest-0.2.0.39 → uc2_rest-0.2.0.40}/uc2rest/camera.py +0 -0
- {uc2_rest-0.2.0.39 → uc2_rest-0.2.0.40}/uc2rest/camera_trigger.py +0 -0
- {uc2_rest-0.2.0.39 → uc2_rest-0.2.0.40}/uc2rest/can.py +0 -0
- {uc2_rest-0.2.0.39 → uc2_rest-0.2.0.40}/uc2rest/canota.py +0 -0
- {uc2_rest-0.2.0.39 → uc2_rest-0.2.0.40}/uc2rest/cmdrecorder.py +0 -0
- {uc2_rest-0.2.0.39 → uc2_rest-0.2.0.40}/uc2rest/digitalin.py +0 -0
- {uc2_rest-0.2.0.39 → uc2_rest-0.2.0.40}/uc2rest/digitalout.py +0 -0
- {uc2_rest-0.2.0.39 → uc2_rest-0.2.0.40}/uc2rest/fan.py +0 -0
- {uc2_rest-0.2.0.39 → uc2_rest-0.2.0.40}/uc2rest/gpio.py +0 -0
- {uc2_rest-0.2.0.39 → uc2_rest-0.2.0.40}/uc2rest/gripper.py +0 -0
- {uc2_rest-0.2.0.39 → uc2_rest-0.2.0.40}/uc2rest/home.py +0 -0
- {uc2_rest-0.2.0.39 → uc2_rest-0.2.0.40}/uc2rest/i2c.py +0 -0
- {uc2_rest-0.2.0.39 → uc2_rest-0.2.0.40}/uc2rest/laser.py +0 -0
- {uc2_rest-0.2.0.39 → uc2_rest-0.2.0.40}/uc2rest/lcddisplay.py +0 -0
- {uc2_rest-0.2.0.39 → uc2_rest-0.2.0.40}/uc2rest/ledmatrix.py +0 -0
- {uc2_rest-0.2.0.39 → uc2_rest-0.2.0.40}/uc2rest/logger.py +0 -0
- {uc2_rest-0.2.0.39 → uc2_rest-0.2.0.40}/uc2rest/message.py +0 -0
- {uc2_rest-0.2.0.39 → uc2_rest-0.2.0.40}/uc2rest/modules.py +0 -0
- {uc2_rest-0.2.0.39 → uc2_rest-0.2.0.40}/uc2rest/motor_config.py +0 -0
- {uc2_rest-0.2.0.39 → uc2_rest-0.2.0.40}/uc2rest/objective.py +0 -0
- {uc2_rest-0.2.0.39 → uc2_rest-0.2.0.40}/uc2rest/pid.py +0 -0
- {uc2_rest-0.2.0.39 → uc2_rest-0.2.0.40}/uc2rest/ptz.py +0 -0
- {uc2_rest-0.2.0.39 → uc2_rest-0.2.0.40}/uc2rest/rotator.py +0 -0
- {uc2_rest-0.2.0.39 → uc2_rest-0.2.0.40}/uc2rest/slm.py +0 -0
- {uc2_rest-0.2.0.39 → uc2_rest-0.2.0.40}/uc2rest/state.py +0 -0
- {uc2_rest-0.2.0.39 → uc2_rest-0.2.0.40}/uc2rest/temperature.py +0 -0
- {uc2_rest-0.2.0.39 → uc2_rest-0.2.0.40}/uc2rest/utils.py +0 -0
- {uc2_rest-0.2.0.39 → uc2_rest-0.2.0.40}/uc2rest/wifi.py +0 -0
|
@@ -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.40"
|
|
10
10
|
__author__ = 'Benedict Diederich'
|
|
11
11
|
__author_email__ = 'benedictdied@gmail.com'
|
|
12
12
|
__license__ = 'GPL v3'
|
|
@@ -37,16 +37,17 @@ class Galvo(object):
|
|
|
37
37
|
SCANNER
|
|
38
38
|
##############################################################################################################################
|
|
39
39
|
'''
|
|
40
|
-
def set_galvo_scan(self, nx=256, ny=256, x_min=500, x_max=3500,
|
|
41
|
-
y_min=500, y_max=3500, sample_period_us=1,
|
|
42
|
-
frame_count=0, bidirectional=False,
|
|
43
|
-
pre_samples=0, fly_samples=0, trig_delay_us=0,
|
|
44
|
-
trig_width_us=0, line_settle_samples=0,
|
|
40
|
+
def set_galvo_scan(self, nx=256, ny=256, x_min=500, x_max=3500,
|
|
41
|
+
y_min=500, y_max=3500, sample_period_us=1,
|
|
42
|
+
frame_count=0, bidirectional=False,
|
|
43
|
+
pre_samples=0, fly_samples=0, trig_delay_us=0,
|
|
44
|
+
trig_width_us=0, line_settle_samples=0,
|
|
45
45
|
enable_trigger=1, apply_x_lut=0,
|
|
46
|
+
overscan_samples=0, laser_blanking=0, hw_pixel_clock=0,
|
|
46
47
|
timeout=1):
|
|
47
48
|
"""
|
|
48
49
|
Start galvo scanner with new API (HighSpeedScannerCore)
|
|
49
|
-
|
|
50
|
+
|
|
50
51
|
Args:
|
|
51
52
|
nx: Number of X samples per line (default: 256)
|
|
52
53
|
ny: Number of Y lines (default: 256)
|
|
@@ -56,14 +57,36 @@ class Galvo(object):
|
|
|
56
57
|
y_max: Max Y position 0-4095 (default: 3500)
|
|
57
58
|
sample_period_us: Microseconds per sample, 0=max speed (default: 1)
|
|
58
59
|
frame_count: Number of frames, 0=infinite (default: 0)
|
|
59
|
-
bidirectional: Enable bidirectional scanning (default: False)
|
|
60
|
+
bidirectional: Enable bidirectional scanning (default: False).
|
|
61
|
+
Odd lines are scanned with a mirrored profile; the trigger
|
|
62
|
+
window stays identical, so pixel counts remain monotonic and
|
|
63
|
+
equidistant. The host must flip odd lines when reassembling.
|
|
64
|
+
trig_delay_us: Gap between the frame marker and the line marker
|
|
65
|
+
at frame start (both fire during pre-blanking, ahead of the
|
|
66
|
+
first pixel).
|
|
67
|
+
trig_width_us: Marker/pixel trigger pulse width in microseconds
|
|
68
|
+
(pixel width is capped at half the dwell time; 0 = fastest
|
|
69
|
+
possible pulse).
|
|
70
|
+
overscan_samples: Linear ramp extension (same per-pixel slope) on
|
|
71
|
+
both sides of the imaging window. Compensates galvo lag: the
|
|
72
|
+
mirror is already moving at constant velocity when triggers
|
|
73
|
+
and laser start (default: 0)
|
|
74
|
+
laser_blanking: 1 = gate the galvo laser pin HIGH only during the
|
|
75
|
+
imaging window (off during pre/overscan/flyback/settle)
|
|
76
|
+
(default: 0)
|
|
77
|
+
hw_pixel_clock: 1 = generate the pixel clock with the RMT
|
|
78
|
+
peripheral (hardware-equidistant, decoupled from the DAC/SPI
|
|
79
|
+
loop; ESP32-S3 only, silently falls back to software pulses
|
|
80
|
+
elsewhere) (default: 0)
|
|
60
81
|
timeout: Request timeout in seconds (default: 1)
|
|
61
|
-
|
|
82
|
+
|
|
62
83
|
sends:
|
|
63
|
-
{"task": "/galvo_act", "config": {"nx":512,"ny":512,"x_min":500,"x_max":3500,"y_min":500,"y_max":3500,"pre_samples":0,"fly_samples":0,"sample_period_us":0,"trig_delay_us":0,"trig_width_us":0,"line_settle_samples":0,"enable_trigger":1,"apply_x_lut":0,"frame_count":0,"bidirectional":true}}
|
|
64
|
-
|
|
84
|
+
{"task": "/galvo_act", "config": {"nx":512,"ny":512,"x_min":500,"x_max":3500,"y_min":500,"y_max":3500,"pre_samples":0,"fly_samples":0,"sample_period_us":0,"trig_delay_us":0,"trig_width_us":0,"line_settle_samples":0,"enable_trigger":1,"apply_x_lut":0,"frame_count":0,"bidirectional":true,"overscan_samples":0,"laser_blanking":0,"hw_pixel_clock":0}}
|
|
85
|
+
|
|
65
86
|
Example:
|
|
66
|
-
>>> galvo.set_galvo_scan(nx=64, ny=64, frame_count=10,
|
|
87
|
+
>>> galvo.set_galvo_scan(nx=64, ny=64, frame_count=10,
|
|
88
|
+
... overscan_samples=8, laser_blanking=1,
|
|
89
|
+
... hw_pixel_clock=1)
|
|
67
90
|
"""
|
|
68
91
|
path = '/galvo_act'
|
|
69
92
|
payload = {
|
|
@@ -84,10 +107,13 @@ class Galvo(object):
|
|
|
84
107
|
"trig_width_us": trig_width_us,
|
|
85
108
|
"line_settle_samples": line_settle_samples,
|
|
86
109
|
"enable_trigger": enable_trigger,
|
|
87
|
-
"apply_x_lut": apply_x_lut
|
|
110
|
+
"apply_x_lut": apply_x_lut,
|
|
111
|
+
"overscan_samples": overscan_samples,
|
|
112
|
+
"laser_blanking": 1 if laser_blanking else 0,
|
|
113
|
+
"hw_pixel_clock": 1 if hw_pixel_clock else 0
|
|
88
114
|
}
|
|
89
115
|
}
|
|
90
|
-
|
|
116
|
+
|
|
91
117
|
return self._parent.post_json(path, payload, timeout=timeout)
|
|
92
118
|
|
|
93
119
|
def stop_galvo_scan(self, timeout=1):
|
|
@@ -108,6 +134,32 @@ class Galvo(object):
|
|
|
108
134
|
|
|
109
135
|
return self._parent.post_json(path, payload, timeout=timeout)
|
|
110
136
|
|
|
137
|
+
def set_position(self, x, y, timeout=1):
|
|
138
|
+
"""
|
|
139
|
+
Move the galvo to a static XY position (DAC counts 0-4095) without scanning.
|
|
140
|
+
|
|
141
|
+
Sends the direct-positioning form of /galvo_act, which the firmware maps
|
|
142
|
+
to a "goto" command (single-point dwell). Useful e.g. for parking the
|
|
143
|
+
beam at a defined position when a scan is stopped.
|
|
144
|
+
|
|
145
|
+
Args:
|
|
146
|
+
x: X DAC coordinate (0-4095)
|
|
147
|
+
y: Y DAC coordinate (0-4095)
|
|
148
|
+
timeout: Request timeout in seconds (default: 1)
|
|
149
|
+
|
|
150
|
+
Example:
|
|
151
|
+
>>> galvo.set_position(2048, 2048) # park at center
|
|
152
|
+
"""
|
|
153
|
+
path = '/galvo_act'
|
|
154
|
+
payload = {
|
|
155
|
+
"task": path,
|
|
156
|
+
"galvo": {
|
|
157
|
+
"x": int(x),
|
|
158
|
+
"y": int(y)
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
return self._parent.post_json(path, payload, timeout=timeout)
|
|
162
|
+
|
|
111
163
|
def get_galvo_status(self, timeout=1):
|
|
112
164
|
"""
|
|
113
165
|
Get galvo scanner status
|
|
@@ -178,10 +230,10 @@ class Galvo(object):
|
|
|
178
230
|
path = '/galvo_act'
|
|
179
231
|
payload = {
|
|
180
232
|
"task": path,
|
|
181
|
-
|
|
233
|
+
"laser_trigger": laser_trigger,
|
|
182
234
|
"points": points
|
|
183
235
|
}
|
|
184
|
-
|
|
236
|
+
|
|
185
237
|
return self._parent.post_json(path, payload, timeout=timeout)
|
|
186
238
|
|
|
187
239
|
def stop_arbitrary_points(self, timeout=1):
|
|
@@ -55,6 +55,9 @@ class Motor(object):
|
|
|
55
55
|
self._parent.serial.register_callback(self._callback_motor_status, pattern="steppers")
|
|
56
56
|
# Register callback for stagescan completion signal: {"stagescan":{},"qid":0,"success":1}
|
|
57
57
|
self._parent.serial.register_callback(self._callback_stagescan_complete, pattern="stagescan")
|
|
58
|
+
# Register callback for async closed-loop axis fault events (design v2):
|
|
59
|
+
# {"axisEvent":{"axis":n,"fault":"STALL","posErrSteps":-142,...}}
|
|
60
|
+
self._parent.serial.register_callback(self._callback_axis_event, pattern="axisEvent")
|
|
58
61
|
# announce a function that is called when we receive a position update through the callback
|
|
59
62
|
self._callbackPerKey = {}
|
|
60
63
|
self.nCallbacks = 10
|
|
@@ -64,6 +67,8 @@ class Motor(object):
|
|
|
64
67
|
# Stage scan completion state
|
|
65
68
|
self._stagescan_complete = False
|
|
66
69
|
self._stagescan_callbacks = [] # List of callbacks to call when stagescan completes
|
|
70
|
+
# Closed-loop axis fault event listeners (design v2, WP9)
|
|
71
|
+
self._axis_event_callbacks = [] # called with the fault dict on STALL/LOST_STEPS/…
|
|
67
72
|
# move motor to wake them up #FIXME: Should not be necessary!
|
|
68
73
|
#self.move_stepper(steps=(1,1,1,1), speed=(1000,1000,1000,1000), is_absolute=(False,False,False,False))
|
|
69
74
|
#self.move_stepper(steps=(-1,-1,-1,-1), speed=(1000,1000,1000,1000), is_absolute=(False,False,False,False))
|
|
@@ -192,8 +197,189 @@ class Motor(object):
|
|
|
192
197
|
def register_callback(self, key, callbackfct):
|
|
193
198
|
''' register a callback function for a specific key '''
|
|
194
199
|
self._callbackPerKey[key] = callbackfct
|
|
195
|
-
|
|
196
|
-
|
|
200
|
+
|
|
201
|
+
# ========================================================================
|
|
202
|
+
# Closed-loop / encoder feedback (design v2, WP9)
|
|
203
|
+
#
|
|
204
|
+
# All values are in STEPS — do NOT convert to µm here; µm conversion stays
|
|
205
|
+
# in the higher-level ImSwitch code. Every method is safe when the firmware
|
|
206
|
+
# lacks encoder support: it returns sensible defaults and never raises.
|
|
207
|
+
#
|
|
208
|
+
# The firmware exposes per-axis feedback in the CANopen AXIS OD block
|
|
209
|
+
# (0x2040-0x204B) on each motor slave, addressed as (node, sub-index). The
|
|
210
|
+
# sub-index is the slave's motor axis id (usually 1). Reach it through the
|
|
211
|
+
# master's generic SDO bridge (/can_act {"sdo":{...}}).
|
|
212
|
+
# ========================================================================
|
|
213
|
+
|
|
214
|
+
# AXIS OD indices (mirror lib/uc2_od + tools/canopen registry, base 0x2040)
|
|
215
|
+
_AXIS_MEASURED_STEPS = 0x2040
|
|
216
|
+
_AXIS_POSITION_ERROR_STEPS = 0x2041
|
|
217
|
+
_AXIS_MODE = 0x2042
|
|
218
|
+
_AXIS_HEALTH = 0x2043
|
|
219
|
+
_AXIS_FAULT = 0x2044
|
|
220
|
+
_AXIS_RESET = 0x2045
|
|
221
|
+
_AXIS_CALIBRATED = 0x2046
|
|
222
|
+
_AXIS_REFERENCED = 0x2047
|
|
223
|
+
_AXIS_CALIBRATE = 0x2048
|
|
224
|
+
_AXIS_COUNTS_PER_STEP_Q16 = 0x2049
|
|
225
|
+
_AXIS_BACKLASH_STEPS = 0x204A
|
|
226
|
+
_AXIS_RAW_COUNTS = 0x204B
|
|
227
|
+
|
|
228
|
+
_AXIS_MODE_NAMES = {0: "OPEN_LOOP", 1: "MONITOR", 2: "CORRECT", 3: "SERVO"}
|
|
229
|
+
_AXIS_HEALTH_NAMES = {0: "OK", 1: "DEGRADED", 2: "FAULT"}
|
|
230
|
+
_AXIS_FAULT_NAMES = {0: "NONE", 1: "STALL", 2: "LOST_STEPS", 3: "DIVERGENCE",
|
|
231
|
+
4: "TIMEOUT", 5: "CAL_INVALID", 6: "CAL_FAILED", 7: "ENC_NOISE"}
|
|
232
|
+
_AXIS_RESET_POLICIES = {"TRUST_ENCODER": 1, "TRUST_STEPS": 2, "FORCE_REHOME": 3}
|
|
233
|
+
|
|
234
|
+
def _axis_sdo(self, node, index, sub, op, ctype="i32", value=None, timeout=2):
|
|
235
|
+
"""Low-level SDO read/write to a slave AXIS OD entry via the master.
|
|
236
|
+
|
|
237
|
+
Returns the integer value on a successful read, True on a successful
|
|
238
|
+
write, and None on any failure (never raises), so callers can degrade
|
|
239
|
+
gracefully when the firmware has no encoder/axis support.
|
|
240
|
+
"""
|
|
241
|
+
sdo = {"node": int(node), "index": int(index), "sub": int(sub),
|
|
242
|
+
"op": op, "type": ctype}
|
|
243
|
+
if op == "w":
|
|
244
|
+
sdo["value"] = int(value)
|
|
245
|
+
payload = {"task": "/can_act", "sdo": sdo}
|
|
246
|
+
try:
|
|
247
|
+
resp = self._parent.post_json("/can_act", payload, getReturn=True,
|
|
248
|
+
timeout=timeout, nResponses=1)
|
|
249
|
+
except Exception as e:
|
|
250
|
+
self._parent.logger.debug(f"axis SDO transport error: {e}")
|
|
251
|
+
return None
|
|
252
|
+
if not isinstance(resp, dict) or resp.get("status") != "ok":
|
|
253
|
+
return None
|
|
254
|
+
return True if op == "w" else resp.get("value", None)
|
|
255
|
+
|
|
256
|
+
def _callback_axis_event(self, data):
|
|
257
|
+
"""Serial callback for async axis fault events from firmware:
|
|
258
|
+
{"axisEvent":{"axis":n,"fault":"STALL","posErrSteps":-142,
|
|
259
|
+
"commandedSteps":..,"measuredSteps":..,"node":..}}
|
|
260
|
+
Forwards the inner fault dict to every registered listener so ImSwitch
|
|
261
|
+
can react to STALL / LOST_STEPS / DIVERGENCE during long acquisitions.
|
|
262
|
+
"""
|
|
263
|
+
try:
|
|
264
|
+
event = data.get("axisEvent", data) if isinstance(data, dict) else data
|
|
265
|
+
for callback in list(self._axis_event_callbacks):
|
|
266
|
+
try:
|
|
267
|
+
callback(event)
|
|
268
|
+
except Exception as e:
|
|
269
|
+
self._parent.logger.error(f"Error in axis event callback: {e}")
|
|
270
|
+
except Exception as e:
|
|
271
|
+
print(f"Error in _callback_axis_event: {e}")
|
|
272
|
+
|
|
273
|
+
def register_axis_event_callback(self, callback):
|
|
274
|
+
"""Register a callback invoked with the fault dict on every axis event."""
|
|
275
|
+
if callback not in self._axis_event_callbacks:
|
|
276
|
+
self._axis_event_callbacks.append(callback)
|
|
277
|
+
|
|
278
|
+
def unregister_axis_event_callback(self, callback):
|
|
279
|
+
"""Unregister a previously registered axis-event callback."""
|
|
280
|
+
if callback in self._axis_event_callbacks:
|
|
281
|
+
self._axis_event_callbacks.remove(callback)
|
|
282
|
+
|
|
283
|
+
def setAxisMode(self, node, mode, axis=1):
|
|
284
|
+
"""Set the closed-loop mode of a motor-slave axis.
|
|
285
|
+
|
|
286
|
+
:param node: CAN node id of the motor slave
|
|
287
|
+
:param mode: 0/1/2/3 or "OPEN_LOOP"/"MONITOR"/"CORRECT"/"SERVO"
|
|
288
|
+
:param axis: OD sub-index (the slave's motor axis id, usually 1)
|
|
289
|
+
:return: True on success, False otherwise (never raises)
|
|
290
|
+
"""
|
|
291
|
+
if isinstance(mode, str):
|
|
292
|
+
inv = {v: k for k, v in self._AXIS_MODE_NAMES.items()}
|
|
293
|
+
mode = inv.get(mode.upper(), 0)
|
|
294
|
+
ok = self._axis_sdo(node, self._AXIS_MODE, axis, "w", "u8", int(mode))
|
|
295
|
+
return ok is True
|
|
296
|
+
|
|
297
|
+
def getAxisFeedback(self, node, axis=1):
|
|
298
|
+
"""Read a motor-slave axis's live feedback (all values in STEPS).
|
|
299
|
+
|
|
300
|
+
:return: dict with commandedSteps, measuredSteps, positionErrorSteps,
|
|
301
|
+
mode, health, fault (ints + *_name), calibrated, referenced,
|
|
302
|
+
rawCounts. When the firmware has no encoder support the read fails
|
|
303
|
+
and OPEN_LOOP / zero-error defaults are returned; never raises.
|
|
304
|
+
"""
|
|
305
|
+
measured = self._axis_sdo(node, self._AXIS_MEASURED_STEPS, axis, "r", "i32")
|
|
306
|
+
if measured is None:
|
|
307
|
+
# No encoder / no axis support — uniform, non-raising default.
|
|
308
|
+
return {"commandedSteps": 0, "measuredSteps": 0, "positionErrorSteps": 0,
|
|
309
|
+
"mode": 0, "mode_name": "OPEN_LOOP", "health": 0, "health_name": "OK",
|
|
310
|
+
"fault": 0, "fault_name": "NONE", "calibrated": False,
|
|
311
|
+
"referenced": False, "rawCounts": 0}
|
|
312
|
+
posErr = self._axis_sdo(node, self._AXIS_POSITION_ERROR_STEPS, axis, "r", "i32") or 0
|
|
313
|
+
mode = self._axis_sdo(node, self._AXIS_MODE, axis, "r", "u8") or 0
|
|
314
|
+
health = self._axis_sdo(node, self._AXIS_HEALTH, axis, "r", "u8") or 0
|
|
315
|
+
fault = self._axis_sdo(node, self._AXIS_FAULT, axis, "r", "u8") or 0
|
|
316
|
+
calibrated = self._axis_sdo(node, self._AXIS_CALIBRATED, axis, "r", "u8") or 0
|
|
317
|
+
referenced = self._axis_sdo(node, self._AXIS_REFERENCED, axis, "r", "u8") or 0
|
|
318
|
+
rawCounts = self._axis_sdo(node, self._AXIS_RAW_COUNTS, axis, "r", "i32") or 0
|
|
319
|
+
return {
|
|
320
|
+
"commandedSteps": int(measured) - int(posErr),
|
|
321
|
+
"measuredSteps": int(measured),
|
|
322
|
+
"positionErrorSteps": int(posErr),
|
|
323
|
+
"mode": int(mode), "mode_name": self._AXIS_MODE_NAMES.get(int(mode), "?"),
|
|
324
|
+
"health": int(health), "health_name": self._AXIS_HEALTH_NAMES.get(int(health), "?"),
|
|
325
|
+
"fault": int(fault), "fault_name": self._AXIS_FAULT_NAMES.get(int(fault), "?"),
|
|
326
|
+
"calibrated": bool(calibrated), "referenced": bool(referenced),
|
|
327
|
+
"rawCounts": int(rawCounts),
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
def resetAxis(self, node, policy="TRUST_ENCODER", axis=1):
|
|
331
|
+
"""Clear a latched fault and re-establish the axis origin.
|
|
332
|
+
|
|
333
|
+
:param policy: "TRUST_ENCODER"|"TRUST_STEPS"|"FORCE_REHOME" or 1/2/3
|
|
334
|
+
:return: True on success (never raises)
|
|
335
|
+
"""
|
|
336
|
+
if isinstance(policy, str):
|
|
337
|
+
policy = self._AXIS_RESET_POLICIES.get(policy.upper(), 1)
|
|
338
|
+
ok = self._axis_sdo(node, self._AXIS_RESET, axis, "w", "u8", int(policy))
|
|
339
|
+
return ok is True
|
|
340
|
+
|
|
341
|
+
def getAxisCalibration(self, node, axis=1):
|
|
342
|
+
"""Read the persisted calibration of a motor-slave axis.
|
|
343
|
+
|
|
344
|
+
:return: dict with countsPerStepQ16 (signed Q16.16), backlashSteps,
|
|
345
|
+
calibrated. None fields default to 0/False; never raises.
|
|
346
|
+
"""
|
|
347
|
+
cps = self._axis_sdo(node, self._AXIS_COUNTS_PER_STEP_Q16, axis, "r", "i32")
|
|
348
|
+
backlash = self._axis_sdo(node, self._AXIS_BACKLASH_STEPS, axis, "r", "i32")
|
|
349
|
+
calibrated = self._axis_sdo(node, self._AXIS_CALIBRATED, axis, "r", "u8")
|
|
350
|
+
return {
|
|
351
|
+
"countsPerStepQ16": int(cps) if cps is not None else 0,
|
|
352
|
+
"countsPerStep": (int(cps) / 65536.0) if cps is not None else 0.0,
|
|
353
|
+
"backlashSteps": int(backlash) if backlash is not None else 0,
|
|
354
|
+
"calibrated": bool(calibrated) if calibrated is not None else False,
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
def calibrateAxis(self, node, axis=1, wait=True, timeout=30, **params):
|
|
358
|
+
"""Trigger the firmware sign/scale/backlash calibration routine.
|
|
359
|
+
|
|
360
|
+
The routine runs ON the slave (it moves the motor through probe moves);
|
|
361
|
+
`params` are accepted for API forward-compatibility but the firmware
|
|
362
|
+
currently uses its built-in defaults. With ``wait=True`` this polls until
|
|
363
|
+
the axis reports calibrated (or ``timeout`` s) and returns the resulting
|
|
364
|
+
calibration dict; otherwise returns {"status": "triggered"}.
|
|
365
|
+
"""
|
|
366
|
+
ok = self._axis_sdo(node, self._AXIS_CALIBRATE, axis, "w", "u8", 1)
|
|
367
|
+
if ok is not True:
|
|
368
|
+
return {"status": "error", "error": "calibration trigger failed (no encoder?)"}
|
|
369
|
+
if not wait:
|
|
370
|
+
return {"status": "triggered"}
|
|
371
|
+
start = time.time()
|
|
372
|
+
while time.time() - start < timeout:
|
|
373
|
+
cal = self.getAxisCalibration(node, axis)
|
|
374
|
+
fb = self.getAxisFeedback(node, axis)
|
|
375
|
+
if cal["calibrated"] or fb["fault_name"] in ("CAL_FAILED",):
|
|
376
|
+
cal["status"] = "ok" if cal["calibrated"] else "failed"
|
|
377
|
+
cal["fault"] = fb["fault_name"]
|
|
378
|
+
return cal
|
|
379
|
+
time.sleep(0.5)
|
|
380
|
+
return {"status": "timeout"}
|
|
381
|
+
|
|
382
|
+
|
|
197
383
|
def setTrigger(self, axis="X", pin=1, offset=0, period=1):
|
|
198
384
|
# {"task": "/motor_act", "setTrig": {"steppers": [{"stepperid": 1, "trigPin": 1, "trigOff":0, "trigPer":1}]}}
|
|
199
385
|
if type(axis) is not int:
|
|
@@ -373,10 +373,10 @@ class Serial:
|
|
|
373
373
|
except Exception as e:
|
|
374
374
|
self._logger.error("Error in checkFirmware: "+str(e))
|
|
375
375
|
return False
|
|
376
|
-
|
|
376
|
+
|
|
377
377
|
def _generate_identifier(self):
|
|
378
|
-
self.identifier_counter
|
|
379
|
-
return self.identifier_counter
|
|
378
|
+
self.identifier_counter = (self.identifier_counter % 254) + 1
|
|
379
|
+
return self.identifier_counter # 1..254, never 0
|
|
380
380
|
|
|
381
381
|
def _process_commands(self):
|
|
382
382
|
buffer = ""
|
|
@@ -836,4 +836,4 @@ if __name__ == "__main__":
|
|
|
836
836
|
#response = monitor.waitForResponse(command_id)
|
|
837
837
|
|
|
838
838
|
if response:
|
|
839
|
-
print("Response:", response)
|
|
839
|
+
print("Response:", response)
|
|
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
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|