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.
Files changed (47) hide show
  1. {uc2_rest-0.2.0.39 → uc2_rest-0.2.0.40}/PKG-INFO +1 -1
  2. {uc2_rest-0.2.0.39 → uc2_rest-0.2.0.40}/UC2_REST.egg-info/PKG-INFO +1 -1
  3. {uc2_rest-0.2.0.39 → uc2_rest-0.2.0.40}/uc2rest/__version__.py +1 -1
  4. {uc2_rest-0.2.0.39 → uc2_rest-0.2.0.40}/uc2rest/galvo.py +67 -15
  5. {uc2_rest-0.2.0.39 → uc2_rest-0.2.0.40}/uc2rest/motor.py +188 -2
  6. {uc2_rest-0.2.0.39 → uc2_rest-0.2.0.40}/uc2rest/mserial.py +4 -4
  7. {uc2_rest-0.2.0.39 → uc2_rest-0.2.0.40}/LICENSE +0 -0
  8. {uc2_rest-0.2.0.39 → uc2_rest-0.2.0.40}/README.md +0 -0
  9. {uc2_rest-0.2.0.39 → uc2_rest-0.2.0.40}/UC2_REST.egg-info/SOURCES.txt +0 -0
  10. {uc2_rest-0.2.0.39 → uc2_rest-0.2.0.40}/UC2_REST.egg-info/dependency_links.txt +0 -0
  11. {uc2_rest-0.2.0.39 → uc2_rest-0.2.0.40}/UC2_REST.egg-info/not-zip-safe +0 -0
  12. {uc2_rest-0.2.0.39 → uc2_rest-0.2.0.40}/UC2_REST.egg-info/requires.txt +0 -0
  13. {uc2_rest-0.2.0.39 → uc2_rest-0.2.0.40}/UC2_REST.egg-info/top_level.txt +0 -0
  14. {uc2_rest-0.2.0.39 → uc2_rest-0.2.0.40}/setup.cfg +0 -0
  15. {uc2_rest-0.2.0.39 → uc2_rest-0.2.0.40}/setup.py +0 -0
  16. {uc2_rest-0.2.0.39 → uc2_rest-0.2.0.40}/uc2rest/MockSerial.py +0 -0
  17. {uc2_rest-0.2.0.39 → uc2_rest-0.2.0.40}/uc2rest/UC2Client.py +0 -0
  18. {uc2_rest-0.2.0.39 → uc2_rest-0.2.0.40}/uc2rest/__init__.py +0 -0
  19. {uc2_rest-0.2.0.39 → uc2_rest-0.2.0.40}/uc2rest/analog.py +0 -0
  20. {uc2_rest-0.2.0.39 → uc2_rest-0.2.0.40}/uc2rest/camera.py +0 -0
  21. {uc2_rest-0.2.0.39 → uc2_rest-0.2.0.40}/uc2rest/camera_trigger.py +0 -0
  22. {uc2_rest-0.2.0.39 → uc2_rest-0.2.0.40}/uc2rest/can.py +0 -0
  23. {uc2_rest-0.2.0.39 → uc2_rest-0.2.0.40}/uc2rest/canota.py +0 -0
  24. {uc2_rest-0.2.0.39 → uc2_rest-0.2.0.40}/uc2rest/cmdrecorder.py +0 -0
  25. {uc2_rest-0.2.0.39 → uc2_rest-0.2.0.40}/uc2rest/digitalin.py +0 -0
  26. {uc2_rest-0.2.0.39 → uc2_rest-0.2.0.40}/uc2rest/digitalout.py +0 -0
  27. {uc2_rest-0.2.0.39 → uc2_rest-0.2.0.40}/uc2rest/fan.py +0 -0
  28. {uc2_rest-0.2.0.39 → uc2_rest-0.2.0.40}/uc2rest/gpio.py +0 -0
  29. {uc2_rest-0.2.0.39 → uc2_rest-0.2.0.40}/uc2rest/gripper.py +0 -0
  30. {uc2_rest-0.2.0.39 → uc2_rest-0.2.0.40}/uc2rest/home.py +0 -0
  31. {uc2_rest-0.2.0.39 → uc2_rest-0.2.0.40}/uc2rest/i2c.py +0 -0
  32. {uc2_rest-0.2.0.39 → uc2_rest-0.2.0.40}/uc2rest/laser.py +0 -0
  33. {uc2_rest-0.2.0.39 → uc2_rest-0.2.0.40}/uc2rest/lcddisplay.py +0 -0
  34. {uc2_rest-0.2.0.39 → uc2_rest-0.2.0.40}/uc2rest/ledmatrix.py +0 -0
  35. {uc2_rest-0.2.0.39 → uc2_rest-0.2.0.40}/uc2rest/logger.py +0 -0
  36. {uc2_rest-0.2.0.39 → uc2_rest-0.2.0.40}/uc2rest/message.py +0 -0
  37. {uc2_rest-0.2.0.39 → uc2_rest-0.2.0.40}/uc2rest/modules.py +0 -0
  38. {uc2_rest-0.2.0.39 → uc2_rest-0.2.0.40}/uc2rest/motor_config.py +0 -0
  39. {uc2_rest-0.2.0.39 → uc2_rest-0.2.0.40}/uc2rest/objective.py +0 -0
  40. {uc2_rest-0.2.0.39 → uc2_rest-0.2.0.40}/uc2rest/pid.py +0 -0
  41. {uc2_rest-0.2.0.39 → uc2_rest-0.2.0.40}/uc2rest/ptz.py +0 -0
  42. {uc2_rest-0.2.0.39 → uc2_rest-0.2.0.40}/uc2rest/rotator.py +0 -0
  43. {uc2_rest-0.2.0.39 → uc2_rest-0.2.0.40}/uc2rest/slm.py +0 -0
  44. {uc2_rest-0.2.0.39 → uc2_rest-0.2.0.40}/uc2rest/state.py +0 -0
  45. {uc2_rest-0.2.0.39 → uc2_rest-0.2.0.40}/uc2rest/temperature.py +0 -0
  46. {uc2_rest-0.2.0.39 → uc2_rest-0.2.0.40}/uc2rest/utils.py +0 -0
  47. {uc2_rest-0.2.0.39 → uc2_rest-0.2.0.40}/uc2rest/wifi.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: UC2-REST
3
- Version: 0.2.0.39
3
+ Version: 0.2.0.40
4
4
  Summary: This pacage will help you to drive the ESP32-driven microscopy control modules from UC2
5
5
  Home-page: https://github.com/openUC2/UC2-REST
6
6
  Author: Benedict Diederich
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: UC2-REST
3
- Version: 0.2.0.39
3
+ Version: 0.2.0.40
4
4
  Summary: This pacage will help you to drive the ESP32-driven microscopy control modules from UC2
5
5
  Home-page: https://github.com/openUC2/UC2-REST
6
6
  Author: Benedict Diederich
@@ -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.39"
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, bidirectional=True)
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
- #"laser_trigger": laser_trigger,
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 += 1
379
- return self.identifier_counter % 255 # Wrap around after 1024 to avoid large numbers
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