puda-drivers 0.0.4__py3-none-any.whl → 0.0.5__py3-none-any.whl
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/core/serialcontroller.py +4 -4
- puda_drivers/move/gcode.py +119 -124
- {puda_drivers-0.0.4.dist-info → puda_drivers-0.0.5.dist-info}/METADATA +44 -3
- {puda_drivers-0.0.4.dist-info → puda_drivers-0.0.5.dist-info}/RECORD +6 -6
- {puda_drivers-0.0.4.dist-info → puda_drivers-0.0.5.dist-info}/WHEEL +0 -0
- {puda_drivers-0.0.4.dist-info → puda_drivers-0.0.5.dist-info}/licenses/LICENSE +0 -0
|
@@ -2,12 +2,12 @@
|
|
|
2
2
|
Generic Serial Controller for communicating with devices over serial ports.
|
|
3
3
|
"""
|
|
4
4
|
|
|
5
|
-
import serial
|
|
6
5
|
import time
|
|
7
|
-
import serial.tools.list_ports
|
|
8
6
|
import logging
|
|
9
7
|
from typing import Optional, List, Tuple
|
|
10
8
|
from abc import ABC
|
|
9
|
+
import serial
|
|
10
|
+
import serial.tools.list_ports
|
|
11
11
|
|
|
12
12
|
logger = logging.getLogger(__name__)
|
|
13
13
|
|
|
@@ -45,7 +45,7 @@ def list_serial_ports(filter_desc: Optional[str] = None) -> List[Tuple[str, str,
|
|
|
45
45
|
|
|
46
46
|
class SerialController(ABC):
|
|
47
47
|
DEFAULT_BAUDRATE = 9600
|
|
48
|
-
DEFAULT_TIMEOUT =
|
|
48
|
+
DEFAULT_TIMEOUT = 30 # seconds
|
|
49
49
|
POLL_INTERVAL = 0.1 # seconds
|
|
50
50
|
|
|
51
51
|
def __init__(self, port_name, baudrate=DEFAULT_BAUDRATE, timeout=DEFAULT_TIMEOUT):
|
|
@@ -127,8 +127,8 @@ class SerialController(ABC):
|
|
|
127
127
|
try:
|
|
128
128
|
self._serial.reset_input_buffer() # clear input buffer
|
|
129
129
|
self._serial.reset_output_buffer() # clear output buffer
|
|
130
|
-
self._serial.write(bytes(command, "utf-8"))
|
|
131
130
|
self._serial.flush()
|
|
131
|
+
self._serial.write(bytes(command, "utf-8"))
|
|
132
132
|
|
|
133
133
|
except serial.SerialTimeoutException as e:
|
|
134
134
|
# Log the timeout error and return None as requested (no re-raise)
|
puda_drivers/move/gcode.py
CHANGED
|
@@ -55,7 +55,9 @@ class GCodeController(SerialController):
|
|
|
55
55
|
|
|
56
56
|
DEFAULT_FEEDRATE = 3000 # mm/min
|
|
57
57
|
MAX_FEEDRATE = 3000 # mm/min
|
|
58
|
+
MAX_Z_FEED_RATE = 1000 # mm/min
|
|
58
59
|
TOLERANCE = 0.01 # tolerance for position sync in mm
|
|
60
|
+
SAFE_MOVE_HEIGHT = -5 # safe height for Z and A axes in mm
|
|
59
61
|
|
|
60
62
|
PROTOCOL_TERMINATOR = "\r"
|
|
61
63
|
VALID_AXES = "XYZA"
|
|
@@ -66,6 +68,7 @@ class GCodeController(SerialController):
|
|
|
66
68
|
baudrate: int = SerialController.DEFAULT_BAUDRATE,
|
|
67
69
|
timeout: int = SerialController.DEFAULT_TIMEOUT,
|
|
68
70
|
feed: int = DEFAULT_FEEDRATE,
|
|
71
|
+
z_feed: int = MAX_Z_FEED_RATE,
|
|
69
72
|
):
|
|
70
73
|
"""
|
|
71
74
|
Initialize the G-code controller.
|
|
@@ -87,13 +90,14 @@ class GCodeController(SerialController):
|
|
|
87
90
|
)
|
|
88
91
|
|
|
89
92
|
# Tracks internal position state
|
|
90
|
-
self.
|
|
93
|
+
self._current_position: Dict[str, float] = {
|
|
91
94
|
"X": 0.0,
|
|
92
95
|
"Y": 0.0,
|
|
93
96
|
"Z": 0.0,
|
|
94
97
|
"A": 0.0,
|
|
95
98
|
}
|
|
96
99
|
self._feed: int = feed
|
|
100
|
+
self._z_feed: int = z_feed
|
|
97
101
|
|
|
98
102
|
# Initialize axis limits with default values
|
|
99
103
|
self._axis_limits: Dict[str, AxisLimits] = {
|
|
@@ -151,6 +155,15 @@ class GCodeController(SerialController):
|
|
|
151
155
|
"""
|
|
152
156
|
return f"{command}{self.PROTOCOL_TERMINATOR}"
|
|
153
157
|
|
|
158
|
+
def wait_for_move(self) -> None:
|
|
159
|
+
"""
|
|
160
|
+
Wait for the current move to complete (M400 command).
|
|
161
|
+
|
|
162
|
+
This sends the M400 command which waits for all moves in the queue to complete
|
|
163
|
+
before continuing. This ensures that position updates are accurate.
|
|
164
|
+
"""
|
|
165
|
+
self.execute(self._build_command("M400"))
|
|
166
|
+
|
|
154
167
|
def _validate_axis(self, axis: str) -> str:
|
|
155
168
|
"""
|
|
156
169
|
Validate and normalize an axis name.
|
|
@@ -292,19 +305,19 @@ class GCodeController(SerialController):
|
|
|
292
305
|
home_target = "All"
|
|
293
306
|
|
|
294
307
|
self._logger.info("[%s] homing axis/axes: %s **", cmd, home_target)
|
|
295
|
-
self.
|
|
308
|
+
self.execute(self._build_command(cmd))
|
|
296
309
|
self._logger.info("Homing of %s completed.", home_target)
|
|
297
310
|
|
|
298
311
|
# Update internal position (optimistic zeroing)
|
|
299
312
|
if axis:
|
|
300
|
-
self.
|
|
313
|
+
self._current_position[axis] = 0.0
|
|
301
314
|
else:
|
|
302
|
-
for key in self.
|
|
303
|
-
self.
|
|
315
|
+
for key in self._current_position:
|
|
316
|
+
self._current_position[key] = 0.0
|
|
304
317
|
|
|
305
318
|
self._logger.debug(
|
|
306
319
|
"Internal position updated (optimistically zeroed) to %s",
|
|
307
|
-
self.
|
|
320
|
+
self._current_position,
|
|
308
321
|
)
|
|
309
322
|
|
|
310
323
|
def move_absolute(
|
|
@@ -314,7 +327,7 @@ class GCodeController(SerialController):
|
|
|
314
327
|
z: Optional[float] = None,
|
|
315
328
|
a: Optional[float] = None,
|
|
316
329
|
feed: Optional[int] = None,
|
|
317
|
-
) ->
|
|
330
|
+
) -> Dict[str, float]:
|
|
318
331
|
"""
|
|
319
332
|
Move to an absolute position (G90 + G1 command).
|
|
320
333
|
|
|
@@ -331,17 +344,26 @@ class GCodeController(SerialController):
|
|
|
331
344
|
# Validate positions before executing move
|
|
332
345
|
self._validate_move_positions(x=x, y=y, z=z, a=a)
|
|
333
346
|
|
|
347
|
+
# Fill in missing axes with current positions
|
|
348
|
+
target_x = x if x is not None else self._current_position["X"]
|
|
349
|
+
target_y = y if y is not None else self._current_position["Y"]
|
|
350
|
+
target_z = z if z is not None else self._current_position["Z"]
|
|
351
|
+
target_a = a if a is not None else self._current_position["A"]
|
|
352
|
+
|
|
334
353
|
feed_rate = feed if feed is not None else self._feed
|
|
335
354
|
self._logger.info(
|
|
336
355
|
"Preparing absolute move to X:%s, Y:%s, Z:%s, A:%s at F:%s",
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
356
|
+
target_x,
|
|
357
|
+
target_y,
|
|
358
|
+
target_z,
|
|
359
|
+
target_a,
|
|
341
360
|
feed_rate,
|
|
342
361
|
)
|
|
343
362
|
|
|
344
|
-
self._execute_move(
|
|
363
|
+
return self._execute_move(
|
|
364
|
+
position={"X": target_x, "Y": target_y, "Z": target_z, "A": target_a},
|
|
365
|
+
feed=feed_rate
|
|
366
|
+
)
|
|
345
367
|
|
|
346
368
|
def move_relative(
|
|
347
369
|
self,
|
|
@@ -350,7 +372,7 @@ class GCodeController(SerialController):
|
|
|
350
372
|
z: Optional[float] = None,
|
|
351
373
|
a: Optional[float] = None,
|
|
352
374
|
feed: Optional[int] = None,
|
|
353
|
-
) ->
|
|
375
|
+
) -> Dict[str, float]:
|
|
354
376
|
"""
|
|
355
377
|
Move relative to the current position (converted to absolute move internally).
|
|
356
378
|
|
|
@@ -374,133 +396,111 @@ class GCodeController(SerialController):
|
|
|
374
396
|
feed_rate,
|
|
375
397
|
)
|
|
376
398
|
|
|
377
|
-
# Convert relative movements to absolute positions
|
|
378
|
-
abs_x = (self.
|
|
379
|
-
abs_y = (self.
|
|
380
|
-
abs_z = (self.
|
|
381
|
-
abs_a = (self.
|
|
399
|
+
# Convert relative movements to absolute positions, filling in missing axes with current position
|
|
400
|
+
abs_x = (self._current_position["X"] + x) if x is not None else self._current_position["X"]
|
|
401
|
+
abs_y = (self._current_position["Y"] + y) if y is not None else self._current_position["Y"]
|
|
402
|
+
abs_z = (self._current_position["Z"] + z) if z is not None else self._current_position["Z"]
|
|
403
|
+
abs_a = (self._current_position["A"] + a) if a is not None else self._current_position["A"]
|
|
382
404
|
|
|
383
405
|
# Validate absolute positions before executing move
|
|
384
406
|
self._validate_move_positions(x=abs_x, y=abs_y, z=abs_z, a=abs_a)
|
|
385
407
|
|
|
386
|
-
self._execute_move(
|
|
408
|
+
return self._execute_move(
|
|
409
|
+
position={"X": abs_x, "Y": abs_y, "Z": abs_z, "A": abs_a},
|
|
410
|
+
feed=feed_rate
|
|
411
|
+
)
|
|
387
412
|
|
|
388
413
|
def _execute_move(
|
|
389
414
|
self,
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
a: Optional[float] = None,
|
|
394
|
-
feed: Optional[int] = None,
|
|
395
|
-
) -> None:
|
|
415
|
+
position: Dict[str, float],
|
|
416
|
+
feed: int,
|
|
417
|
+
) -> Dict[str, float]:
|
|
396
418
|
"""
|
|
397
419
|
Internal helper for executing G1 move commands with safe movement pattern.
|
|
398
420
|
All coordinates are treated as absolute positions.
|
|
399
421
|
|
|
400
422
|
Safe move pattern:
|
|
401
423
|
1. If X or Y movement is needed, first move Z to 0 (safe height)
|
|
402
|
-
2. Then move X, Y
|
|
403
|
-
3. Finally move Z to
|
|
424
|
+
2. Then move X, Y to target
|
|
425
|
+
3. Finally move Z and A back to original position (or target if specified)
|
|
404
426
|
|
|
405
427
|
Args:
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
"""
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
has_z = z is not None
|
|
417
|
-
has_a = a is not None
|
|
418
|
-
|
|
419
|
-
if not (has_x or has_y or has_z or has_a):
|
|
428
|
+
position: Dictionary with absolute positions for X, Y, Z, A axes
|
|
429
|
+
feed: Feed rate for the move
|
|
430
|
+
"""
|
|
431
|
+
# Check if any movement is needed
|
|
432
|
+
needs_x_move = abs(position["X"] - self._current_position["X"]) > self.TOLERANCE
|
|
433
|
+
needs_y_move = abs(position["Y"] - self._current_position["Y"]) > self.TOLERANCE
|
|
434
|
+
needs_z_move = abs(position["Z"] - self._current_position["Z"]) > self.TOLERANCE
|
|
435
|
+
needs_a_move = abs(position["A"] - self._current_position["A"]) > self.TOLERANCE
|
|
436
|
+
|
|
437
|
+
if not (needs_x_move or needs_y_move or needs_z_move or needs_a_move):
|
|
420
438
|
self._logger.warning(
|
|
421
439
|
"Move command issued without any axis movement. Skipping transmission."
|
|
422
440
|
)
|
|
423
441
|
return
|
|
442
|
+
|
|
443
|
+
if needs_z_move and needs_a_move:
|
|
444
|
+
self._logger.warning(
|
|
445
|
+
"Move command issued with both Z and A movement. This is not supported. Skipping transmission."
|
|
446
|
+
)
|
|
447
|
+
raise ValueError("Move command issued with both Z and A movement. This is not supported.")
|
|
424
448
|
|
|
425
|
-
#
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
if has_y:
|
|
429
|
-
target_pos["Y"] = y
|
|
430
|
-
if has_z:
|
|
431
|
-
target_pos["Z"] = z
|
|
432
|
-
if has_a:
|
|
433
|
-
target_pos["A"] = a
|
|
434
|
-
|
|
435
|
-
feed_rate = feed if feed is not None else self._feed
|
|
436
|
-
if feed_rate > self.MAX_FEEDRATE:
|
|
437
|
-
feed_rate = self.MAX_FEEDRATE
|
|
438
|
-
|
|
439
|
-
# Ensure absolute mode is active
|
|
440
|
-
self._send_command(self._build_command("G90"))
|
|
441
|
-
|
|
442
|
-
# Safe move pattern: Z to 0, then XY, then Z to target
|
|
443
|
-
needs_xy_move = has_x or has_y
|
|
444
|
-
current_z = self.current_position["Z"]
|
|
445
|
-
target_z = target_pos["Z"] if has_z else current_z
|
|
449
|
+
# Step 0: Ensure absolute mode is active
|
|
450
|
+
self.execute(self._build_command("G90"))
|
|
451
|
+
needs_xy_move = needs_x_move or needs_y_move
|
|
446
452
|
|
|
447
|
-
# Step 1: Move Z
|
|
448
|
-
if needs_xy_move
|
|
449
|
-
# Validate safe height (Z=0) is within limits
|
|
450
|
-
self._validate_move_positions(z=0.0)
|
|
453
|
+
# Step 1: Move Z and A to SAFE_MOVE_HEIGHT if XY movement is needed
|
|
454
|
+
if needs_xy_move:
|
|
451
455
|
self._logger.info(
|
|
452
|
-
"Safe move: Raising Z to safe height (
|
|
456
|
+
"Safe move: Raising Z and A to safe height (%s) before XY movement", self.SAFE_MOVE_HEIGHT
|
|
453
457
|
)
|
|
454
|
-
move_cmd = f"G1
|
|
455
|
-
self.
|
|
456
|
-
self.
|
|
457
|
-
self.
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
458
|
+
move_cmd = f"G1 Z-5 A-5 F{self._z_feed}"
|
|
459
|
+
self.execute(self._build_command(move_cmd))
|
|
460
|
+
self.wait_for_move()
|
|
461
|
+
self._current_position["Z"] = self.SAFE_MOVE_HEIGHT
|
|
462
|
+
self._current_position["A"] = self.SAFE_MOVE_HEIGHT
|
|
463
|
+
self._logger.debug("Z and A moved to safe height (%s)", self.SAFE_MOVE_HEIGHT)
|
|
464
|
+
|
|
465
|
+
# Step 2: Move X, Y to target
|
|
466
|
+
if needs_xy_move:
|
|
461
467
|
move_cmd = "G1"
|
|
462
|
-
if
|
|
463
|
-
move_cmd += f" X{
|
|
464
|
-
if
|
|
465
|
-
move_cmd += f" Y{
|
|
466
|
-
|
|
467
|
-
move_cmd += f" A{target_pos['A']}"
|
|
468
|
-
move_cmd += f" F{feed_rate}"
|
|
468
|
+
if needs_x_move:
|
|
469
|
+
move_cmd += f" X{position['X']}"
|
|
470
|
+
if needs_y_move:
|
|
471
|
+
move_cmd += f" Y{position['Y']}"
|
|
472
|
+
move_cmd += f" F{feed}"
|
|
469
473
|
|
|
470
474
|
self._logger.info("Executing XY move command: %s", move_cmd)
|
|
471
|
-
self.
|
|
475
|
+
self.execute(self._build_command(move_cmd))
|
|
476
|
+
self.wait_for_move()
|
|
472
477
|
|
|
473
478
|
# Update position for moved axes
|
|
474
|
-
if
|
|
475
|
-
self.
|
|
476
|
-
if
|
|
477
|
-
self.
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
"Safe move: Lowering Z to target position: %s", target_z
|
|
490
|
-
)
|
|
491
|
-
else:
|
|
492
|
-
self._logger.info("Executing Z move command: %s", move_cmd)
|
|
493
|
-
|
|
494
|
-
self._send_command(self._build_command(move_cmd))
|
|
495
|
-
self.current_position["Z"] = target_z
|
|
479
|
+
if needs_x_move:
|
|
480
|
+
self._current_position["X"] = position['X']
|
|
481
|
+
if needs_y_move:
|
|
482
|
+
self._current_position["Y"] = position['Y']
|
|
483
|
+
|
|
484
|
+
# Step 3: Move Z and A back to original position (or target if specified)
|
|
485
|
+
if needs_z_move:
|
|
486
|
+
move_cmd = f"G1 Z{position['Z']} F{self._z_feed}"
|
|
487
|
+
self.execute(self._build_command(move_cmd))
|
|
488
|
+
self._current_position["Z"] = position['Z']
|
|
489
|
+
elif needs_a_move:
|
|
490
|
+
move_cmd = f"G1 A{position['A']} F{self._z_feed}"
|
|
491
|
+
self.execute(self._build_command(move_cmd))
|
|
492
|
+
self._current_position["A"] = position['A']
|
|
493
|
+
self.wait_for_move()
|
|
496
494
|
|
|
497
495
|
self._logger.info(
|
|
498
|
-
"Move complete. Final position: %s", self.
|
|
496
|
+
"Move complete. Final position: %s", self._current_position
|
|
499
497
|
)
|
|
500
|
-
self._logger.debug("New internal position: %s", self.
|
|
498
|
+
self._logger.debug("New internal position: %s", self._current_position)
|
|
501
499
|
|
|
502
|
-
# Post-move position synchronization check
|
|
500
|
+
# Step 4: Post-move position synchronization check
|
|
503
501
|
self.sync_position()
|
|
502
|
+
|
|
503
|
+
return self._current_position
|
|
504
504
|
|
|
505
505
|
def query_position(self) -> Dict[str, float]:
|
|
506
506
|
"""
|
|
@@ -555,8 +555,8 @@ class GCodeController(SerialController):
|
|
|
555
555
|
queried_position = self.query_position()
|
|
556
556
|
|
|
557
557
|
if not queried_position:
|
|
558
|
-
self._logger.
|
|
559
|
-
|
|
558
|
+
self._logger.error("Query position failed. Cannot synchronize.")
|
|
559
|
+
raise ValueError("Query position failed. Cannot synchronize.")
|
|
560
560
|
|
|
561
561
|
# Compare internal vs. queried position
|
|
562
562
|
axis_keys = ["X", "Y", "Z", "A"]
|
|
@@ -564,36 +564,31 @@ class GCodeController(SerialController):
|
|
|
564
564
|
|
|
565
565
|
for axis in axis_keys:
|
|
566
566
|
if (
|
|
567
|
-
axis in self.
|
|
567
|
+
axis in self._current_position
|
|
568
568
|
and axis in queried_position
|
|
569
|
-
and abs(self.
|
|
569
|
+
and abs(self._current_position[axis] - queried_position[axis])
|
|
570
570
|
> self.TOLERANCE
|
|
571
571
|
):
|
|
572
572
|
self._logger.warning(
|
|
573
573
|
"Position mismatch found on %s axis: Internal=%.3f, Queried=%.3f",
|
|
574
574
|
axis,
|
|
575
|
-
self.
|
|
575
|
+
self._current_position[axis],
|
|
576
576
|
queried_position[axis],
|
|
577
577
|
)
|
|
578
578
|
adjustment_needed = True
|
|
579
579
|
elif axis in queried_position:
|
|
580
580
|
# Update internal position with queried position if it differs slightly
|
|
581
|
-
self.
|
|
581
|
+
self._current_position[axis] = queried_position[axis]
|
|
582
582
|
|
|
583
583
|
# Perform re-synchronization move if needed
|
|
584
584
|
if adjustment_needed:
|
|
585
585
|
self._logger.info(
|
|
586
|
-
"** DISCREPANCY DETECTED. Moving robot
|
|
587
|
-
self.
|
|
586
|
+
"** DISCREPANCY DETECTED. Moving robot to internal position: %s **",
|
|
587
|
+
self._current_position,
|
|
588
588
|
)
|
|
589
589
|
|
|
590
590
|
try:
|
|
591
|
-
|
|
592
|
-
target_y = self.current_position.get("Y")
|
|
593
|
-
target_z = self.current_position.get("Z")
|
|
594
|
-
target_a = self.current_position.get("A")
|
|
595
|
-
|
|
596
|
-
self.move_absolute(x=target_x, y=target_y, z=target_z, a=target_a)
|
|
591
|
+
self.move_absolute(x=self._current_position["X"], y=self._current_position["Y"], z=self._current_position["Z"], a=self._current_position["A"])
|
|
597
592
|
self._logger.info("Synchronization move successfully completed.")
|
|
598
593
|
|
|
599
594
|
# Recursive call to verify position after move
|
|
@@ -609,7 +604,7 @@ class GCodeController(SerialController):
|
|
|
609
604
|
else:
|
|
610
605
|
self._logger.info("No adjustment was made.")
|
|
611
606
|
|
|
612
|
-
return adjustment_needed, self.
|
|
607
|
+
return adjustment_needed, self._current_position.copy()
|
|
613
608
|
|
|
614
609
|
def get_info(self) -> str:
|
|
615
610
|
"""
|
|
@@ -628,5 +623,5 @@ class GCodeController(SerialController):
|
|
|
628
623
|
Returns:
|
|
629
624
|
Dictionary containing the current internal position for all axes
|
|
630
625
|
"""
|
|
631
|
-
self._logger.debug("Returning internal position: %s", self.
|
|
632
|
-
return self.
|
|
626
|
+
self._logger.debug("Returning internal position: %s", self._current_position)
|
|
627
|
+
return self._current_position.copy()
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: puda-drivers
|
|
3
|
-
Version: 0.0.
|
|
3
|
+
Version: 0.0.5
|
|
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
|
|
@@ -56,13 +56,19 @@ from puda_drivers.move import GCodeController
|
|
|
56
56
|
gantry = GCodeController(port_name="/dev/ttyACM0", feed=3000)
|
|
57
57
|
gantry.connect()
|
|
58
58
|
|
|
59
|
+
# Configure axis limits for safety (recommended)
|
|
60
|
+
gantry.set_axis_limits("X", 0, 200)
|
|
61
|
+
gantry.set_axis_limits("Y", -200, 0)
|
|
62
|
+
gantry.set_axis_limits("Z", -100, 0)
|
|
63
|
+
gantry.set_axis_limits("A", -180, 180)
|
|
64
|
+
|
|
59
65
|
# Home the gantry
|
|
60
66
|
gantry.home()
|
|
61
67
|
|
|
62
|
-
# Move to absolute position
|
|
68
|
+
# Move to absolute position (validated against limits)
|
|
63
69
|
gantry.move_absolute(x=50.0, y=-100.0, z=-10.0)
|
|
64
70
|
|
|
65
|
-
# Move relative to current position
|
|
71
|
+
# Move relative to current position (validated after conversion to absolute)
|
|
66
72
|
gantry.move_relative(x=20.0, y=-10.0)
|
|
67
73
|
|
|
68
74
|
# Query current position
|
|
@@ -73,6 +79,8 @@ print(f"Current position: {position}")
|
|
|
73
79
|
gantry.disconnect()
|
|
74
80
|
```
|
|
75
81
|
|
|
82
|
+
**Axis Limits and Validation**: The `move_absolute()` and `move_relative()` methods automatically validate that target positions are within configured axis limits. If a position is outside the limits, a `ValueError` is raised before any movement is executed. Use `set_axis_limits()` to configure limits for each axis.
|
|
83
|
+
|
|
76
84
|
### Liquid Handling (Sartorius)
|
|
77
85
|
|
|
78
86
|
```python
|
|
@@ -134,6 +142,7 @@ pipette.disconnect()
|
|
|
134
142
|
- Supports X, Y, Z, and A axes
|
|
135
143
|
- Configurable feed rates
|
|
136
144
|
- Position synchronization and homing
|
|
145
|
+
- Automatic axis limit validation for safe operation
|
|
137
146
|
|
|
138
147
|
### Liquid Handling
|
|
139
148
|
|
|
@@ -142,6 +151,38 @@ pipette.disconnect()
|
|
|
142
151
|
- Tip attachment and ejection
|
|
143
152
|
- Configurable speeds and volumes
|
|
144
153
|
|
|
154
|
+
## Error Handling
|
|
155
|
+
|
|
156
|
+
### Axis Limit Validation
|
|
157
|
+
|
|
158
|
+
Both `move_absolute()` and `move_relative()` validate positions against configured axis limits before executing any movement. If a position is outside the limits, a `ValueError` is raised:
|
|
159
|
+
|
|
160
|
+
```python
|
|
161
|
+
from puda_drivers.move import GCodeController
|
|
162
|
+
|
|
163
|
+
gantry = GCodeController(port_name="/dev/ttyACM0")
|
|
164
|
+
gantry.connect()
|
|
165
|
+
|
|
166
|
+
# Set axis limits
|
|
167
|
+
gantry.set_axis_limits("X", 0, 200)
|
|
168
|
+
gantry.set_axis_limits("Y", -200, 0)
|
|
169
|
+
|
|
170
|
+
try:
|
|
171
|
+
# This will raise ValueError: Value 250 outside axis limits [0, 200]
|
|
172
|
+
gantry.move_absolute(x=250.0, y=-50.0)
|
|
173
|
+
except ValueError as e:
|
|
174
|
+
print(f"Move rejected: {e}")
|
|
175
|
+
|
|
176
|
+
# Relative moves are also validated after conversion to absolute positions
|
|
177
|
+
try:
|
|
178
|
+
# If current X is 150, moving 100 more would exceed the limit
|
|
179
|
+
gantry.move_relative(x=100.0)
|
|
180
|
+
except ValueError as e:
|
|
181
|
+
print(f"Move rejected: {e}")
|
|
182
|
+
```
|
|
183
|
+
|
|
184
|
+
Validation errors are automatically logged at the ERROR level before the exception is raised.
|
|
185
|
+
|
|
145
186
|
## Finding Serial Ports
|
|
146
187
|
|
|
147
188
|
To discover available serial ports on your system:
|
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
puda_drivers/__init__.py,sha256=rcF5xCkMgyLlJLN3gWwJnUoW0ShPyISeyENvaqwg4Ik,503
|
|
2
2
|
puda_drivers/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
3
3
|
puda_drivers/core/__init__.py,sha256=JM6eWTelwcmjTGM3gprQlJWzPGEpIdRrDmbCHtGoKyM,119
|
|
4
|
-
puda_drivers/core/serialcontroller.py,sha256=
|
|
4
|
+
puda_drivers/core/serialcontroller.py,sha256=I7TsLNl45HPrO29LkhMRIQQ8fWdjdJUvIwQQ5swbMHM,7660
|
|
5
5
|
puda_drivers/move/__init__.py,sha256=i7G5VKD5FgnmC21TLxoASVtC88IrPUTLDJrTnp99u-0,35
|
|
6
|
-
puda_drivers/move/gcode.py,sha256=
|
|
6
|
+
puda_drivers/move/gcode.py,sha256=egZw3D5m9d1R8P32L1wd3lDwiWcFMDGPHsFMFIYXkRA,22069
|
|
7
7
|
puda_drivers/move/grbl/__init__.py,sha256=vBeeti8DVN2dACi1rLmHN_UGIOdo0s-HZX6mIepLV5I,98
|
|
8
8
|
puda_drivers/move/grbl/api.py,sha256=loj8_Vap7S9qaD0ReHhgxr9Vkl6Wp7DGzyLkZyZ6v_k,16995
|
|
9
9
|
puda_drivers/move/grbl/constants.py,sha256=4736CRDzLGWVqGscLajMlrIQMyubsHfthXi4RF1CHNg,9585
|
|
@@ -11,7 +11,7 @@ puda_drivers/transfer/liquid/sartorius/__init__.py,sha256=QGpKz5YUwa8xCdSMXeZ0iR
|
|
|
11
11
|
puda_drivers/transfer/liquid/sartorius/api.py,sha256=jxwIJmY2k1K2ts6NC2ZgFTe4MOiH8TGnJeqYOqNa3rE,28250
|
|
12
12
|
puda_drivers/transfer/liquid/sartorius/constants.py,sha256=mcsjLrVBH-RSodH-pszstwcEL9wwbV0vOgHbGNxZz9w,2770
|
|
13
13
|
puda_drivers/transfer/liquid/sartorius/sartorius.py,sha256=iW3v-YHjj4ZAfGv0x0J-XV-Y0fAAhS6xmSg2ozQm4UI,13803
|
|
14
|
-
puda_drivers-0.0.
|
|
15
|
-
puda_drivers-0.0.
|
|
16
|
-
puda_drivers-0.0.
|
|
17
|
-
puda_drivers-0.0.
|
|
14
|
+
puda_drivers-0.0.5.dist-info/METADATA,sha256=5VB_QiC_hcuV4-FEoXi-qEp637jAjQLiv5fipHX9QPg,6559
|
|
15
|
+
puda_drivers-0.0.5.dist-info/WHEEL,sha256=WLgqFyCfm_KASv4WHyYy0P3pM_m7J5L9k2skdKLirC8,87
|
|
16
|
+
puda_drivers-0.0.5.dist-info/licenses/LICENSE,sha256=7EI8xVBu6h_7_JlVw-yPhhOZlpY9hP8wal7kHtqKT_E,1074
|
|
17
|
+
puda_drivers-0.0.5.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|