puda-drivers 0.0.2__tar.gz → 0.0.3__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.
- {puda_drivers-0.0.2 → puda_drivers-0.0.3}/PKG-INFO +1 -1
- {puda_drivers-0.0.2 → puda_drivers-0.0.3}/pyproject.toml +1 -1
- puda_drivers-0.0.3/src/puda_drivers/move/gcode.py +525 -0
- puda_drivers-0.0.3/src/puda_drivers/transfer/liquid/sartorius/sartorius.py +398 -0
- {puda_drivers-0.0.2 → puda_drivers-0.0.3}/uv.lock +1 -1
- puda_drivers-0.0.2/src/puda_drivers/move/gcode.py +0 -367
- puda_drivers-0.0.2/src/puda_drivers/transfer/liquid/sartorius/sartorius.py +0 -258
- {puda_drivers-0.0.2 → puda_drivers-0.0.3}/.gitignore +0 -0
- {puda_drivers-0.0.2 → puda_drivers-0.0.3}/LICENSE +0 -0
- {puda_drivers-0.0.2 → puda_drivers-0.0.3}/README.md +0 -0
- {puda_drivers-0.0.2 → puda_drivers-0.0.3}/src/puda_drivers/__init__.py +0 -0
- {puda_drivers-0.0.2 → puda_drivers-0.0.3}/src/puda_drivers/core/__init__.py +0 -0
- {puda_drivers-0.0.2 → puda_drivers-0.0.3}/src/puda_drivers/core/serialcontroller.py +0 -0
- {puda_drivers-0.0.2 → puda_drivers-0.0.3}/src/puda_drivers/move/__init__.py +0 -0
- {puda_drivers-0.0.2 → puda_drivers-0.0.3}/src/puda_drivers/move/grbl/__init__.py +0 -0
- {puda_drivers-0.0.2 → puda_drivers-0.0.3}/src/puda_drivers/move/grbl/api.py +0 -0
- {puda_drivers-0.0.2 → puda_drivers-0.0.3}/src/puda_drivers/move/grbl/constants.py +0 -0
- {puda_drivers-0.0.2 → puda_drivers-0.0.3}/src/puda_drivers/py.typed +0 -0
- {puda_drivers-0.0.2 → puda_drivers-0.0.3}/src/puda_drivers/transfer/liquid/sartorius/__init__.py +0 -0
- {puda_drivers-0.0.2 → puda_drivers-0.0.3}/src/puda_drivers/transfer/liquid/sartorius/api.py +0 -0
- {puda_drivers-0.0.2 → puda_drivers-0.0.3}/src/puda_drivers/transfer/liquid/sartorius/constants.py +0 -0
- {puda_drivers-0.0.2 → puda_drivers-0.0.3}/tests/example.py +0 -0
- {puda_drivers-0.0.2 → puda_drivers-0.0.3}/tests/qubot.py +0 -0
- {puda_drivers-0.0.2 → puda_drivers-0.0.3}/tests/sartorius.py +0 -0
- {puda_drivers-0.0.2 → puda_drivers-0.0.3}/tests/together.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: puda-drivers
|
|
3
|
-
Version: 0.0.
|
|
3
|
+
Version: 0.0.3
|
|
4
4
|
Summary: Hardware drivers for the PUDA platform.
|
|
5
5
|
Project-URL: Homepage, https://github.com/zhao-bears/puda-drivers
|
|
6
6
|
Project-URL: Issues, https://github.com/zhao-bears/puda-drivers/issues
|
|
@@ -0,0 +1,525 @@
|
|
|
1
|
+
"""
|
|
2
|
+
G-code controller for motion systems.
|
|
3
|
+
|
|
4
|
+
This module provides a Python interface for controlling G-code compatible motion
|
|
5
|
+
systems (e.g., QuBot) via serial communication. Supports absolute and relative
|
|
6
|
+
positioning, homing, and position synchronization.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
import re
|
|
10
|
+
import logging
|
|
11
|
+
from typing import Optional, Dict, Tuple
|
|
12
|
+
|
|
13
|
+
from puda_drivers.core.serialcontroller import SerialController
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class GCodeController(SerialController):
|
|
17
|
+
"""
|
|
18
|
+
Controller for G-code compatible motion systems.
|
|
19
|
+
|
|
20
|
+
This class provides methods for controlling multi-axis motion systems that
|
|
21
|
+
understand G-code commands, including homing, absolute/relative movement,
|
|
22
|
+
and position synchronization.
|
|
23
|
+
|
|
24
|
+
Attributes:
|
|
25
|
+
DEFAULT_FEEDRATE: Default feed rate in mm/min (3000)
|
|
26
|
+
MAX_FEEDRATE: Maximum allowed feed rate in mm/min (3000)
|
|
27
|
+
TOLERANCE: Position synchronization tolerance in mm (0.01)
|
|
28
|
+
"""
|
|
29
|
+
|
|
30
|
+
DEFAULT_FEEDRATE = 3000 # mm/min
|
|
31
|
+
MAX_FEEDRATE = 3000 # mm/min
|
|
32
|
+
TOLERANCE = 0.01 # tolerance for position sync in mm
|
|
33
|
+
|
|
34
|
+
PROTOCOL_TERMINATOR = "\r"
|
|
35
|
+
VALID_AXES = "XYZA"
|
|
36
|
+
|
|
37
|
+
def __init__(
|
|
38
|
+
self,
|
|
39
|
+
port_name: Optional[str] = None,
|
|
40
|
+
baudrate: int = SerialController.DEFAULT_BAUDRATE,
|
|
41
|
+
timeout: int = SerialController.DEFAULT_TIMEOUT,
|
|
42
|
+
feed: int = DEFAULT_FEEDRATE,
|
|
43
|
+
):
|
|
44
|
+
"""
|
|
45
|
+
Initialize the G-code controller.
|
|
46
|
+
|
|
47
|
+
Args:
|
|
48
|
+
port_name: Serial port name (e.g., '/dev/ttyACM0' or 'COM3')
|
|
49
|
+
baudrate: Baud rate for serial communication. Defaults to 9600.
|
|
50
|
+
timeout: Timeout in seconds for operations. Defaults to 20.
|
|
51
|
+
feed: Initial feed rate in mm/min. Defaults to 3000.
|
|
52
|
+
"""
|
|
53
|
+
super().__init__(port_name, baudrate, timeout)
|
|
54
|
+
|
|
55
|
+
self._logger = logging.getLogger(__name__)
|
|
56
|
+
self._logger.info(
|
|
57
|
+
"GCodeController initialized with port='%s', baudrate=%s, timeout=%s",
|
|
58
|
+
port_name,
|
|
59
|
+
baudrate,
|
|
60
|
+
timeout,
|
|
61
|
+
)
|
|
62
|
+
|
|
63
|
+
# Tracks internal position state
|
|
64
|
+
self.current_position: Dict[str, float] = {
|
|
65
|
+
"X": 0.0,
|
|
66
|
+
"Y": 0.0,
|
|
67
|
+
"Z": 0.0,
|
|
68
|
+
"A": 0.0,
|
|
69
|
+
}
|
|
70
|
+
self._feed: int = feed
|
|
71
|
+
self._is_absolute_mode: bool = True # absolute mode by default
|
|
72
|
+
|
|
73
|
+
@property
|
|
74
|
+
def feed(self) -> int:
|
|
75
|
+
"""Get the current feed rate in mm/min."""
|
|
76
|
+
return self._feed
|
|
77
|
+
|
|
78
|
+
@feed.setter
|
|
79
|
+
def feed(self, new_feed: int) -> None:
|
|
80
|
+
"""
|
|
81
|
+
Set the movement feed rate, enforcing the maximum limit.
|
|
82
|
+
|
|
83
|
+
Args:
|
|
84
|
+
new_feed: New feed rate in mm/min (must be > 0)
|
|
85
|
+
|
|
86
|
+
Raises:
|
|
87
|
+
ValueError: If feed rate is not positive
|
|
88
|
+
"""
|
|
89
|
+
if new_feed <= 0:
|
|
90
|
+
error_msg = (
|
|
91
|
+
f"Attempted to set invalid feed rate: {new_feed}. Must be > 0."
|
|
92
|
+
)
|
|
93
|
+
self._logger.error(error_msg)
|
|
94
|
+
raise ValueError(error_msg)
|
|
95
|
+
|
|
96
|
+
if new_feed > self.MAX_FEEDRATE:
|
|
97
|
+
self._logger.warning(
|
|
98
|
+
"Requested feed rate (%s) exceeds maximum (%s). "
|
|
99
|
+
"Setting feed rate to maximum: %s.",
|
|
100
|
+
new_feed,
|
|
101
|
+
self.MAX_FEEDRATE,
|
|
102
|
+
self.MAX_FEEDRATE,
|
|
103
|
+
)
|
|
104
|
+
self._feed = self.MAX_FEEDRATE
|
|
105
|
+
else:
|
|
106
|
+
self._feed = new_feed
|
|
107
|
+
self._logger.debug("Feed rate set to: %s mm/min.", self._feed)
|
|
108
|
+
|
|
109
|
+
def _build_command(self, command: str) -> str:
|
|
110
|
+
"""
|
|
111
|
+
Build a G-code command with terminator.
|
|
112
|
+
|
|
113
|
+
Args:
|
|
114
|
+
command: G-code command string (without terminator)
|
|
115
|
+
|
|
116
|
+
Returns:
|
|
117
|
+
Complete command string with terminator
|
|
118
|
+
"""
|
|
119
|
+
return f"{command}{self.PROTOCOL_TERMINATOR}"
|
|
120
|
+
|
|
121
|
+
def _validate_axis(self, axis: str) -> str:
|
|
122
|
+
"""
|
|
123
|
+
Validate and normalize an axis name.
|
|
124
|
+
|
|
125
|
+
Args:
|
|
126
|
+
axis: Axis name to validate
|
|
127
|
+
|
|
128
|
+
Returns:
|
|
129
|
+
Uppercase axis name
|
|
130
|
+
|
|
131
|
+
Raises:
|
|
132
|
+
ValueError: If axis is not valid
|
|
133
|
+
"""
|
|
134
|
+
axis_upper = axis.upper()
|
|
135
|
+
if axis_upper not in self.VALID_AXES:
|
|
136
|
+
self._logger.error(
|
|
137
|
+
"Invalid axis '%s' provided. Must be one of: %s.",
|
|
138
|
+
axis_upper,
|
|
139
|
+
", ".join(self.VALID_AXES),
|
|
140
|
+
)
|
|
141
|
+
raise ValueError(
|
|
142
|
+
f"Invalid axis. Must be one of: {', '.join(self.VALID_AXES)}."
|
|
143
|
+
)
|
|
144
|
+
return axis_upper
|
|
145
|
+
|
|
146
|
+
def home(self, axis: Optional[str] = None) -> None:
|
|
147
|
+
"""
|
|
148
|
+
Home one or all axes (G28 command).
|
|
149
|
+
|
|
150
|
+
Args:
|
|
151
|
+
axis: Optional axis to home ('X', 'Y', 'Z', 'A').
|
|
152
|
+
If None, homes all axes.
|
|
153
|
+
|
|
154
|
+
Raises:
|
|
155
|
+
ValueError: If an invalid axis is provided
|
|
156
|
+
"""
|
|
157
|
+
if axis:
|
|
158
|
+
axis = self._validate_axis(axis)
|
|
159
|
+
cmd = f"G28 {axis}"
|
|
160
|
+
home_target = axis
|
|
161
|
+
else:
|
|
162
|
+
cmd = "G28"
|
|
163
|
+
home_target = "All"
|
|
164
|
+
|
|
165
|
+
self._logger.info("[%s] homing axis/axes: %s **", cmd, home_target)
|
|
166
|
+
self._send_command(self._build_command(cmd))
|
|
167
|
+
self._logger.info("Homing of %s completed.", home_target)
|
|
168
|
+
|
|
169
|
+
# Update internal position (optimistic zeroing)
|
|
170
|
+
if axis:
|
|
171
|
+
self.current_position[axis] = 0.0
|
|
172
|
+
else:
|
|
173
|
+
for key in self.current_position:
|
|
174
|
+
self.current_position[key] = 0.0
|
|
175
|
+
|
|
176
|
+
self._logger.debug(
|
|
177
|
+
"Internal position updated (optimistically zeroed) to %s",
|
|
178
|
+
self.current_position,
|
|
179
|
+
)
|
|
180
|
+
|
|
181
|
+
def move_absolute(
|
|
182
|
+
self,
|
|
183
|
+
x: Optional[float] = None,
|
|
184
|
+
y: Optional[float] = None,
|
|
185
|
+
z: Optional[float] = None,
|
|
186
|
+
a: Optional[float] = None,
|
|
187
|
+
feed: Optional[int] = None,
|
|
188
|
+
) -> None:
|
|
189
|
+
"""
|
|
190
|
+
Move to an absolute position (G90 + G1 command).
|
|
191
|
+
|
|
192
|
+
Args:
|
|
193
|
+
x: Target X position (optional)
|
|
194
|
+
y: Target Y position (optional)
|
|
195
|
+
z: Target Z position (optional)
|
|
196
|
+
a: Target A position (optional)
|
|
197
|
+
feed: Feed rate for this move (optional, uses current feed if not specified)
|
|
198
|
+
"""
|
|
199
|
+
feed_rate = feed if feed is not None else self._feed
|
|
200
|
+
self._logger.info(
|
|
201
|
+
"Preparing absolute move to X:%s, Y:%s, Z:%s, A:%s at F:%s",
|
|
202
|
+
x,
|
|
203
|
+
y,
|
|
204
|
+
z,
|
|
205
|
+
a,
|
|
206
|
+
feed_rate,
|
|
207
|
+
)
|
|
208
|
+
|
|
209
|
+
# Ensure absolute mode is active
|
|
210
|
+
if not self._is_absolute_mode:
|
|
211
|
+
self._logger.debug("Switching to absolute positioning mode (G90).")
|
|
212
|
+
self._send_command(self._build_command("G90"))
|
|
213
|
+
self._is_absolute_mode = True
|
|
214
|
+
|
|
215
|
+
self._execute_move(x=x, y=y, z=z, a=a, feed=feed)
|
|
216
|
+
|
|
217
|
+
def move_relative(
|
|
218
|
+
self,
|
|
219
|
+
x: Optional[float] = None,
|
|
220
|
+
y: Optional[float] = None,
|
|
221
|
+
z: Optional[float] = None,
|
|
222
|
+
a: Optional[float] = None,
|
|
223
|
+
feed: Optional[int] = None,
|
|
224
|
+
) -> None:
|
|
225
|
+
"""
|
|
226
|
+
Move relative to the current position (G91 + G1 command).
|
|
227
|
+
|
|
228
|
+
Args:
|
|
229
|
+
x: Relative X movement (optional)
|
|
230
|
+
y: Relative Y movement (optional)
|
|
231
|
+
z: Relative Z movement (optional)
|
|
232
|
+
a: Relative A movement (optional)
|
|
233
|
+
feed: Feed rate for this move (optional, uses current feed if not specified)
|
|
234
|
+
"""
|
|
235
|
+
feed_rate = feed if feed is not None else self._feed
|
|
236
|
+
self._logger.info(
|
|
237
|
+
"Preparing relative move by dX:%s, dY:%s, dZ:%s, dA:%s at F:%s",
|
|
238
|
+
x,
|
|
239
|
+
y,
|
|
240
|
+
z,
|
|
241
|
+
a,
|
|
242
|
+
feed_rate,
|
|
243
|
+
)
|
|
244
|
+
|
|
245
|
+
# Ensure relative mode is active
|
|
246
|
+
if self._is_absolute_mode:
|
|
247
|
+
self._logger.debug("Switching to relative positioning mode (G91).")
|
|
248
|
+
self._send_command(self._build_command("G91"))
|
|
249
|
+
self._is_absolute_mode = False
|
|
250
|
+
|
|
251
|
+
self._execute_move(x=x, y=y, z=z, a=a, feed=feed)
|
|
252
|
+
|
|
253
|
+
def _execute_move(
|
|
254
|
+
self,
|
|
255
|
+
x: Optional[float] = None,
|
|
256
|
+
y: Optional[float] = None,
|
|
257
|
+
z: Optional[float] = None,
|
|
258
|
+
a: Optional[float] = None,
|
|
259
|
+
feed: Optional[int] = None,
|
|
260
|
+
) -> None:
|
|
261
|
+
"""
|
|
262
|
+
Internal helper for executing G1 move commands with safe movement pattern.
|
|
263
|
+
|
|
264
|
+
Safe move pattern:
|
|
265
|
+
1. If X or Y movement is needed, first move Z to 0 (safe height)
|
|
266
|
+
2. Then move X, Y (and optionally A) to target
|
|
267
|
+
3. Finally move Z to target position (if specified)
|
|
268
|
+
|
|
269
|
+
Args:
|
|
270
|
+
x: X axis value (optional)
|
|
271
|
+
y: Y axis value (optional)
|
|
272
|
+
z: Z axis value (optional)
|
|
273
|
+
a: A axis value (optional)
|
|
274
|
+
feed: Feed rate (optional)
|
|
275
|
+
"""
|
|
276
|
+
# Calculate target positions
|
|
277
|
+
target_pos = self.current_position.copy()
|
|
278
|
+
has_x = x is not None
|
|
279
|
+
has_y = y is not None
|
|
280
|
+
has_z = z is not None
|
|
281
|
+
has_a = a is not None
|
|
282
|
+
|
|
283
|
+
if not (has_x or has_y or has_z or has_a):
|
|
284
|
+
self._logger.warning(
|
|
285
|
+
"Move command issued without any axis movement. Skipping transmission."
|
|
286
|
+
)
|
|
287
|
+
return
|
|
288
|
+
|
|
289
|
+
# Calculate target positions
|
|
290
|
+
if self._is_absolute_mode:
|
|
291
|
+
if has_x:
|
|
292
|
+
target_pos["X"] = x
|
|
293
|
+
if has_y:
|
|
294
|
+
target_pos["Y"] = y
|
|
295
|
+
if has_z:
|
|
296
|
+
target_pos["Z"] = z
|
|
297
|
+
if has_a:
|
|
298
|
+
target_pos["A"] = a
|
|
299
|
+
else:
|
|
300
|
+
if has_x:
|
|
301
|
+
target_pos["X"] = self.current_position["X"] + x
|
|
302
|
+
if has_y:
|
|
303
|
+
target_pos["Y"] = self.current_position["Y"] + y
|
|
304
|
+
if has_z:
|
|
305
|
+
target_pos["Z"] = self.current_position["Z"] + z
|
|
306
|
+
if has_a:
|
|
307
|
+
target_pos["A"] = self.current_position["A"] + a
|
|
308
|
+
|
|
309
|
+
feed_rate = feed if feed is not None else self._feed
|
|
310
|
+
if feed_rate > self.MAX_FEEDRATE:
|
|
311
|
+
feed_rate = self.MAX_FEEDRATE
|
|
312
|
+
|
|
313
|
+
# Safe move pattern: Z to 0, then XY, then Z to target
|
|
314
|
+
needs_xy_move = has_x or has_y
|
|
315
|
+
current_z = self.current_position["Z"]
|
|
316
|
+
target_z = target_pos["Z"] if has_z else current_z
|
|
317
|
+
|
|
318
|
+
# Step 1: Move Z to safe height (0) if XY movement is needed and Z is not already at 0
|
|
319
|
+
if needs_xy_move and abs(current_z) > 0.001: # Small tolerance for floating point
|
|
320
|
+
self._logger.info(
|
|
321
|
+
"Safe move: Raising Z to safe height (0) before XY movement"
|
|
322
|
+
)
|
|
323
|
+
if self._is_absolute_mode:
|
|
324
|
+
move_cmd = "G1 Z0"
|
|
325
|
+
else:
|
|
326
|
+
# In relative mode, move by negative current_z to reach 0
|
|
327
|
+
move_cmd = f"G1 Z{-current_z}"
|
|
328
|
+
move_cmd += f" F{feed_rate}"
|
|
329
|
+
self._send_command(self._build_command(move_cmd))
|
|
330
|
+
self.current_position["Z"] = 0.0
|
|
331
|
+
self._logger.debug("Z moved to safe height (0)")
|
|
332
|
+
|
|
333
|
+
# Step 2: Move X, Y (and optionally A) to target
|
|
334
|
+
if needs_xy_move or has_a:
|
|
335
|
+
move_cmd = "G1"
|
|
336
|
+
if has_x:
|
|
337
|
+
# Use target position in absolute mode, relative value in relative mode
|
|
338
|
+
if self._is_absolute_mode:
|
|
339
|
+
move_cmd += f" X{target_pos['X']}"
|
|
340
|
+
else:
|
|
341
|
+
move_cmd += f" X{x}"
|
|
342
|
+
if has_y:
|
|
343
|
+
if self._is_absolute_mode:
|
|
344
|
+
move_cmd += f" Y{target_pos['Y']}"
|
|
345
|
+
else:
|
|
346
|
+
move_cmd += f" Y{y}"
|
|
347
|
+
if has_a:
|
|
348
|
+
if self._is_absolute_mode:
|
|
349
|
+
move_cmd += f" A{target_pos['A']}"
|
|
350
|
+
else:
|
|
351
|
+
move_cmd += f" A{a}"
|
|
352
|
+
move_cmd += f" F{feed_rate}"
|
|
353
|
+
|
|
354
|
+
self._logger.info("Executing XY move command: %s", move_cmd)
|
|
355
|
+
self._send_command(self._build_command(move_cmd))
|
|
356
|
+
|
|
357
|
+
# Update position for moved axes
|
|
358
|
+
if has_x:
|
|
359
|
+
self.current_position["X"] = target_pos["X"]
|
|
360
|
+
if has_y:
|
|
361
|
+
self.current_position["Y"] = target_pos["Y"]
|
|
362
|
+
if has_a:
|
|
363
|
+
self.current_position["A"] = target_pos["A"]
|
|
364
|
+
|
|
365
|
+
# Step 3: Move Z to target position (if Z movement was requested)
|
|
366
|
+
if has_z:
|
|
367
|
+
z_needs_move = abs(target_z - (0.0 if needs_xy_move else current_z)) > 0.001
|
|
368
|
+
if z_needs_move:
|
|
369
|
+
if self._is_absolute_mode:
|
|
370
|
+
move_cmd = f"G1 Z{target_z} F{feed_rate}"
|
|
371
|
+
else:
|
|
372
|
+
# In relative mode, use the original z value
|
|
373
|
+
move_cmd = f"G1 Z{z} F{feed_rate}"
|
|
374
|
+
|
|
375
|
+
if needs_xy_move:
|
|
376
|
+
self._logger.info(
|
|
377
|
+
"Safe move: Lowering Z to target position: %s", target_z
|
|
378
|
+
)
|
|
379
|
+
else:
|
|
380
|
+
self._logger.info("Executing Z move command: %s", move_cmd)
|
|
381
|
+
|
|
382
|
+
self._send_command(self._build_command(move_cmd))
|
|
383
|
+
self.current_position["Z"] = target_z
|
|
384
|
+
|
|
385
|
+
self._logger.info(
|
|
386
|
+
"Move complete. Final position: %s", self.current_position
|
|
387
|
+
)
|
|
388
|
+
self._logger.debug("New internal position: %s", self.current_position)
|
|
389
|
+
|
|
390
|
+
# Post-move position synchronization check
|
|
391
|
+
self.sync_position()
|
|
392
|
+
|
|
393
|
+
def query_position(self) -> Dict[str, float]:
|
|
394
|
+
"""
|
|
395
|
+
Query the current machine position (M114 command).
|
|
396
|
+
|
|
397
|
+
Returns:
|
|
398
|
+
Dictionary containing X, Y, Z, and A positions
|
|
399
|
+
|
|
400
|
+
Note:
|
|
401
|
+
Returns an empty dictionary if the query fails or no positions are found.
|
|
402
|
+
"""
|
|
403
|
+
self._logger.info("Querying current machine position (M114).")
|
|
404
|
+
self._send_command(self._build_command("M114"))
|
|
405
|
+
res: str = self._read_response()
|
|
406
|
+
|
|
407
|
+
# Extract position values using regex
|
|
408
|
+
pattern = re.compile(r"([XYZA]):(-?\d+\.\d+)")
|
|
409
|
+
matches = pattern.findall(res)
|
|
410
|
+
|
|
411
|
+
position_data: Dict[str, float] = {}
|
|
412
|
+
|
|
413
|
+
for axis, value_str in matches:
|
|
414
|
+
try:
|
|
415
|
+
position_data[axis] = float(value_str)
|
|
416
|
+
except ValueError:
|
|
417
|
+
self._logger.error(
|
|
418
|
+
"Failed to convert position value '%s' for axis %s to float.",
|
|
419
|
+
value_str,
|
|
420
|
+
axis,
|
|
421
|
+
)
|
|
422
|
+
continue
|
|
423
|
+
|
|
424
|
+
return position_data
|
|
425
|
+
|
|
426
|
+
def sync_position(self) -> Tuple[bool, Dict[str, float]]:
|
|
427
|
+
"""
|
|
428
|
+
Synchronize internal position with actual machine position.
|
|
429
|
+
|
|
430
|
+
Queries the machine position and compares it with the internal position.
|
|
431
|
+
If a discrepancy greater than the tolerance is found, attempts to correct
|
|
432
|
+
it by moving to the internal position.
|
|
433
|
+
|
|
434
|
+
Returns:
|
|
435
|
+
Tuple of (adjustment_occurred: bool, final_position: Dict[str, float])
|
|
436
|
+
where adjustment_occurred is True if a correction move was made.
|
|
437
|
+
|
|
438
|
+
Note:
|
|
439
|
+
This method may recursively call itself if a correction move is made.
|
|
440
|
+
"""
|
|
441
|
+
self._logger.info("Starting position synchronization check (M114).")
|
|
442
|
+
|
|
443
|
+
# Query the actual machine position
|
|
444
|
+
queried_position = self.query_position()
|
|
445
|
+
|
|
446
|
+
if not queried_position:
|
|
447
|
+
self._logger.warning("Query position failed. Cannot synchronize.")
|
|
448
|
+
return False, self.current_position
|
|
449
|
+
|
|
450
|
+
# Compare internal vs. queried position
|
|
451
|
+
axis_keys = ["X", "Y", "Z", "A"]
|
|
452
|
+
adjustment_needed = False
|
|
453
|
+
|
|
454
|
+
for axis in axis_keys:
|
|
455
|
+
if (
|
|
456
|
+
axis in self.current_position
|
|
457
|
+
and axis in queried_position
|
|
458
|
+
and abs(self.current_position[axis] - queried_position[axis])
|
|
459
|
+
> self.TOLERANCE
|
|
460
|
+
):
|
|
461
|
+
self._logger.warning(
|
|
462
|
+
"Position mismatch found on %s axis: Internal=%.3f, Queried=%.3f",
|
|
463
|
+
axis,
|
|
464
|
+
self.current_position[axis],
|
|
465
|
+
queried_position[axis],
|
|
466
|
+
)
|
|
467
|
+
adjustment_needed = True
|
|
468
|
+
elif axis in queried_position:
|
|
469
|
+
# Update internal position with queried position if it differs slightly
|
|
470
|
+
self.current_position[axis] = queried_position[axis]
|
|
471
|
+
|
|
472
|
+
# Perform re-synchronization move if needed
|
|
473
|
+
if adjustment_needed:
|
|
474
|
+
self._logger.info(
|
|
475
|
+
"** DISCREPANCY DETECTED. Moving robot back to internal position: %s **",
|
|
476
|
+
self.current_position,
|
|
477
|
+
)
|
|
478
|
+
|
|
479
|
+
try:
|
|
480
|
+
target_x = self.current_position.get("X")
|
|
481
|
+
target_y = self.current_position.get("Y")
|
|
482
|
+
target_z = self.current_position.get("Z")
|
|
483
|
+
target_a = self.current_position.get("A")
|
|
484
|
+
|
|
485
|
+
self.move_absolute(x=target_x, y=target_y, z=target_z, a=target_a)
|
|
486
|
+
self._logger.info("Synchronization move successfully completed.")
|
|
487
|
+
|
|
488
|
+
# Recursive call to verify position after move
|
|
489
|
+
return self.sync_position()
|
|
490
|
+
except (ValueError, RuntimeError, OSError) as e:
|
|
491
|
+
self._logger.error("Synchronization move failed: %s", e)
|
|
492
|
+
adjustment_needed = False
|
|
493
|
+
|
|
494
|
+
final_position = self.current_position.copy()
|
|
495
|
+
|
|
496
|
+
if adjustment_needed:
|
|
497
|
+
self._logger.info(
|
|
498
|
+
"Position check complete. Internal position is synchronized with machine."
|
|
499
|
+
)
|
|
500
|
+
else:
|
|
501
|
+
self._logger.info("No adjustment was made.")
|
|
502
|
+
|
|
503
|
+
return adjustment_needed, final_position
|
|
504
|
+
|
|
505
|
+
def get_info(self) -> str:
|
|
506
|
+
"""
|
|
507
|
+
Query machine information (M115 command).
|
|
508
|
+
|
|
509
|
+
Returns:
|
|
510
|
+
Machine information string from the device
|
|
511
|
+
"""
|
|
512
|
+
self._logger.info("Querying machine information (M115).")
|
|
513
|
+
self._send_command(self._build_command("M115"))
|
|
514
|
+
res: str = self._read_response()
|
|
515
|
+
return res
|
|
516
|
+
|
|
517
|
+
def get_internal_position(self) -> Dict[str, float]:
|
|
518
|
+
"""
|
|
519
|
+
Get the internally tracked position.
|
|
520
|
+
|
|
521
|
+
Returns:
|
|
522
|
+
Dictionary containing the current internal position for all axes
|
|
523
|
+
"""
|
|
524
|
+
self._logger.debug("Returning internal position: %s", self.current_position)
|
|
525
|
+
return self.current_position.copy()
|