puda-drivers 0.0.6__py3-none-any.whl → 0.0.8__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.
@@ -7,12 +7,14 @@ absolute coordinates, with relative moves converted to absolute internally.
7
7
  Supports homing and position synchronization.
8
8
  """
9
9
 
10
+ import time
10
11
  import re
11
12
  import logging
12
13
  from dataclasses import dataclass
13
14
  from typing import Optional, Dict, Tuple, Union
14
15
 
15
16
  from puda_drivers.core.serialcontroller import SerialController
17
+ from puda_drivers.core.position import Position
16
18
 
17
19
 
18
20
  @dataclass
@@ -90,12 +92,7 @@ class GCodeController(SerialController):
90
92
  )
91
93
 
92
94
  # Tracks internal position state
93
- self._current_position: Dict[str, float] = {
94
- "X": 0.0,
95
- "Y": 0.0,
96
- "Z": 0.0,
97
- "A": 0.0,
98
- }
95
+ self._current_position = Position(x=0.0, y=0.0, z=0.0, a=0.0)
99
96
  self._feed: int = feed
100
97
  self._z_feed: int = z_feed
101
98
 
@@ -143,7 +140,7 @@ class GCodeController(SerialController):
143
140
  self._feed = new_feed
144
141
  self._logger.debug("Feed rate set to: %s mm/min.", self._feed)
145
142
 
146
- def _build_command(self, command: str) -> str:
143
+ def _build_command(self, command: str, value: Optional[str] = None) -> str:
147
144
  """
148
145
  Build a G-code command with terminator.
149
146
 
@@ -155,14 +152,14 @@ class GCodeController(SerialController):
155
152
  """
156
153
  return f"{command}{self.PROTOCOL_TERMINATOR}"
157
154
 
158
- def wait_for_move(self) -> None:
155
+ def _wait_for_move(self) -> None:
159
156
  """
160
157
  Wait for the current move to complete (M400 command).
161
158
 
162
159
  This sends the M400 command which waits for all moves in the queue to complete
163
160
  before continuing. This ensures that position updates are accurate.
164
161
  """
165
- self.execute(self._build_command("M400"))
162
+ self.execute("M400")
166
163
 
167
164
  def _validate_axis(self, axis: str) -> str:
168
165
  """
@@ -191,51 +188,45 @@ class GCodeController(SerialController):
191
188
 
192
189
  def _validate_move_positions(
193
190
  self,
194
- x: Optional[float] = None,
195
- y: Optional[float] = None,
196
- z: Optional[float] = None,
197
- a: Optional[float] = None,
191
+ position: Position,
198
192
  ) -> None:
199
193
  """
200
194
  Validate that move positions are within axis limits.
201
195
 
202
- Only validates axes that are being moved (not None). Raises ValueError
196
+ Only validates axes that are present in the position. Raises ValueError
203
197
  if any position is outside the configured limits.
204
198
 
205
199
  Args:
206
- x: Target X position (optional)
207
- y: Target Y position (optional)
208
- z: Target Z position (optional)
209
- a: Target A position (optional)
200
+ position: Position object to validate
210
201
 
211
202
  Raises:
212
203
  ValueError: If any position is outside the axis limits
213
204
  """
214
- if x is not None:
205
+ if position.has_axis("x"):
215
206
  if "X" in self._axis_limits:
216
207
  try:
217
- self._axis_limits["X"].validate(x)
208
+ self._axis_limits["X"].validate(position.x)
218
209
  except ValueError as e:
219
210
  self._logger.error("Move validation failed for X axis: %s", e)
220
211
  raise
221
- if y is not None:
212
+ if position.has_axis("y"):
222
213
  if "Y" in self._axis_limits:
223
214
  try:
224
- self._axis_limits["Y"].validate(y)
215
+ self._axis_limits["Y"].validate(position.y)
225
216
  except ValueError as e:
226
217
  self._logger.error("Move validation failed for Y axis: %s", e)
227
218
  raise
228
- if z is not None:
219
+ if position.has_axis("z"):
229
220
  if "Z" in self._axis_limits:
230
221
  try:
231
- self._axis_limits["Z"].validate(z)
222
+ self._axis_limits["Z"].validate(position.z)
232
223
  except ValueError as e:
233
224
  self._logger.error("Move validation failed for Z axis: %s", e)
234
225
  raise
235
- if a is not None:
226
+ if position.has_axis("a"):
236
227
  if "A" in self._axis_limits:
237
228
  try:
238
- self._axis_limits["A"].validate(a)
229
+ self._axis_limits["A"].validate(position.a)
239
230
  except ValueError as e:
240
231
  self._logger.error("Move validation failed for A axis: %s", e)
241
232
  raise
@@ -305,15 +296,14 @@ class GCodeController(SerialController):
305
296
  home_target = "All"
306
297
 
307
298
  self._logger.info("[%s] homing axis/axes: %s **", cmd, home_target)
308
- self.execute(self._build_command(cmd))
309
- self._logger.info("Homing of %s completed.", home_target)
299
+ self.execute(cmd)
300
+ self._logger.info("Homing of %s completed.\n", home_target)
310
301
 
311
302
  # Update internal position (optimistic zeroing)
312
303
  if axis:
313
- self._current_position[axis] = 0.0
304
+ self._current_position[axis.lower()] = 0.0
314
305
  else:
315
- for key in self._current_position:
316
- self._current_position[key] = 0.0
306
+ self._current_position = Position(x=0.0, y=0.0, z=0.0, a=0.0)
317
307
 
318
308
  self._logger.debug(
319
309
  "Internal position updated (optimistically zeroed) to %s",
@@ -322,99 +312,118 @@ class GCodeController(SerialController):
322
312
 
323
313
  def move_absolute(
324
314
  self,
325
- x: Optional[float] = None,
326
- y: Optional[float] = None,
327
- z: Optional[float] = None,
328
- a: Optional[float] = None,
315
+ position: Position,
329
316
  feed: Optional[int] = None,
330
- ) -> Dict[str, float]:
317
+ ) -> Position:
331
318
  """
332
319
  Move to an absolute position (G90 + G1 command).
333
320
 
334
321
  Args:
335
- x: Target X position (optional)
336
- y: Target Y position (optional)
337
- z: Target Z position (optional)
338
- a: Target A position (optional)
322
+ position: Target Position object. Missing axes will use current position values.
339
323
  feed: Feed rate for this move (optional, uses current feed if not specified)
340
324
 
341
325
  Raises:
342
326
  ValueError: If any position is outside the axis limits
343
- """
344
- # Validate positions before executing move
345
- self._validate_move_positions(x=x, y=y, z=z, a=a)
346
327
 
328
+ Examples:
329
+ >>> pos = Position(x=10, y=20, z=30)
330
+ >>> controller.move_absolute(position=pos)
331
+ >>> # Partial position (only x and y specified)
332
+ >>> pos = Position(x=10, y=20)
333
+ >>> controller.move_absolute(position=pos) # z and a use current values
334
+ """
347
335
  # 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"]
336
+ final_x = position.x if position.has_axis("x") else self._current_position.x
337
+ final_y = position.y if position.has_axis("y") else self._current_position.y
338
+ final_z = position.z if position.has_axis("z") else self._current_position.z
339
+ final_a = position.a if position.has_axis("a") else self._current_position.a
340
+
341
+ # Create final position for validation and execution
342
+ final_position = Position(x=final_x, y=final_y, z=final_z, a=final_a)
343
+
344
+ # Validate positions before executing move
345
+ self._validate_move_positions(position=final_position)
352
346
 
353
347
  feed_rate = feed if feed is not None else self._feed
354
348
  self._logger.info(
355
349
  "Preparing absolute move to X:%s, Y:%s, Z:%s, A:%s at F:%s",
356
- target_x,
357
- target_y,
358
- target_z,
359
- target_a,
350
+ final_x,
351
+ final_y,
352
+ final_z,
353
+ final_a,
360
354
  feed_rate,
361
355
  )
362
356
 
363
357
  return self._execute_move(
364
- position={"X": target_x, "Y": target_y, "Z": target_z, "A": target_a},
358
+ position=final_position,
365
359
  feed=feed_rate
366
360
  )
367
361
 
368
362
  def move_relative(
369
363
  self,
370
- x: Optional[float] = None,
371
- y: Optional[float] = None,
372
- z: Optional[float] = None,
373
- a: Optional[float] = None,
364
+ position: Position,
374
365
  feed: Optional[int] = None,
375
- ) -> Dict[str, float]:
366
+ ) -> Position:
376
367
  """
377
368
  Move relative to the current position (converted to absolute move internally).
378
369
 
379
370
  Args:
380
- x: Relative X movement (optional)
381
- y: Relative Y movement (optional)
382
- z: Relative Z movement (optional)
383
- a: Relative A movement (optional)
371
+ position: Relative Position object. Only axes specified in position will move.
384
372
  feed: Feed rate for this move (optional, uses current feed if not specified)
385
373
 
386
374
  Raises:
387
375
  ValueError: If any resulting absolute position is outside the axis limits
388
- """
376
+
377
+ Examples:
378
+ >>> # Move relative in x and y only
379
+ >>> pos = Position(x=10, y=20)
380
+ >>> controller.move_relative(position=pos)
381
+ >>> # Move relative in all axes
382
+ >>> pos = Position(x=10, y=20, z=5, a=0)
383
+ >>> controller.move_relative(position=pos)
384
+ """
385
+ # Convert relative movements to absolute positions
386
+ # Only move axes that are specified in the position
387
+ abs_x = self._current_position.x
388
+ abs_y = self._current_position.y
389
+ abs_z = self._current_position.z
390
+ abs_a = self._current_position.a
391
+
392
+ if position.has_axis("x"):
393
+ abs_x = self._current_position.x + position.x
394
+ if position.has_axis("y"):
395
+ abs_y = self._current_position.y + position.y
396
+ if position.has_axis("z"):
397
+ abs_z = self._current_position.z + position.z
398
+ if position.has_axis("a"):
399
+ abs_a = self._current_position.a + position.a
400
+
401
+ # Create absolute position for validation and execution
402
+ abs_position = Position(x=abs_x, y=abs_y, z=abs_z, a=abs_a)
403
+
404
+ # Validate absolute positions before executing move
405
+ self._validate_move_positions(position=abs_position)
406
+
389
407
  feed_rate = feed if feed is not None else self._feed
390
408
  self._logger.info(
391
409
  "Preparing relative move by dX:%s, dY:%s, dZ:%s, dA:%s at F:%s",
392
- x,
393
- y,
394
- z,
395
- a,
410
+ position.x if position.has_axis("x") else 0,
411
+ position.y if position.has_axis("y") else 0,
412
+ position.z if position.has_axis("z") else 0,
413
+ position.a if position.has_axis("a") else 0,
396
414
  feed_rate,
397
415
  )
398
416
 
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"]
404
-
405
- # Validate absolute positions before executing move
406
- self._validate_move_positions(x=abs_x, y=abs_y, z=abs_z, a=abs_a)
407
-
408
417
  return self._execute_move(
409
- position={"X": abs_x, "Y": abs_y, "Z": abs_z, "A": abs_a},
418
+ position=abs_position,
410
419
  feed=feed_rate
411
420
  )
412
421
 
413
422
  def _execute_move(
414
423
  self,
415
- position: Dict[str, float],
424
+ position: Position,
416
425
  feed: int,
417
- ) -> Dict[str, float]:
426
+ ) -> Position:
418
427
  """
419
428
  Internal helper for executing G1 move commands with safe movement pattern.
420
429
  All coordinates are treated as absolute positions.
@@ -425,15 +434,15 @@ class GCodeController(SerialController):
425
434
  3. Finally move Z and A back to original position (or target if specified)
426
435
 
427
436
  Args:
428
- position: Dictionary with absolute positions for X, Y, Z, A axes
437
+ position: Position with absolute positions for X, Y, Z, A axes
429
438
  feed: Feed rate for the move
430
439
  """
431
440
  # 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
-
441
+ needs_x_move = abs(position.x - self._current_position.x) > self.TOLERANCE
442
+ needs_y_move = abs(position.y - self._current_position.y) > self.TOLERANCE
443
+ needs_z_move = abs(position.z - self._current_position.z) > self.TOLERANCE
444
+ needs_a_move = abs(position.a - self._current_position.a) > self.TOLERANCE
445
+
437
446
  if not (needs_x_move or needs_y_move or needs_z_move or needs_a_move):
438
447
  self._logger.warning(
439
448
  "Move command issued without any axis movement. Skipping transmission."
@@ -447,73 +456,70 @@ class GCodeController(SerialController):
447
456
  raise ValueError("Move command issued with both Z and A movement. This is not supported.")
448
457
 
449
458
  # 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
459
+ self.execute("G90")
452
460
 
453
461
  # Step 1: Move Z and A to SAFE_MOVE_HEIGHT if XY movement is needed
454
- if needs_xy_move:
455
- self._logger.info(
462
+ if needs_x_move or needs_y_move:
463
+ self._logger.debug(
456
464
  "Safe move: Raising Z and A to safe height (%s) before XY movement", self.SAFE_MOVE_HEIGHT
457
465
  )
458
466
  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
467
+ self.execute(move_cmd)
468
+ self._wait_for_move()
469
+ self._current_position.z = self.SAFE_MOVE_HEIGHT
470
+ self._current_position.a = self.SAFE_MOVE_HEIGHT
463
471
  self._logger.debug("Z and A moved to safe height (%s)", self.SAFE_MOVE_HEIGHT)
464
472
 
473
+ # update needs_z_move and needs_a_move based on the current position
474
+ needs_z_move = abs(position.z - self._current_position.z) > self.TOLERANCE
475
+ needs_a_move = abs(position.a - self._current_position.a) > self.TOLERANCE
476
+
465
477
  # Step 2: Move X, Y to target
466
- if needs_xy_move:
478
+ if needs_x_move or needs_y_move:
467
479
  move_cmd = "G1"
468
480
  if needs_x_move:
469
- move_cmd += f" X{position['X']}"
481
+ move_cmd += f" X{position.x}"
470
482
  if needs_y_move:
471
- move_cmd += f" Y{position['Y']}"
483
+ move_cmd += f" Y{position.y}"
472
484
  move_cmd += f" F{feed}"
473
485
 
474
- self._logger.info("Executing XY move command: %s", move_cmd)
475
- self.execute(self._build_command(move_cmd))
476
- self.wait_for_move()
486
+ self._logger.debug("Executing XY move command: %s", move_cmd)
487
+ self.execute(move_cmd)
488
+ self._wait_for_move()
477
489
 
478
490
  # Update position for moved axes
479
491
  if needs_x_move:
480
- self._current_position["X"] = position['X']
492
+ self._current_position.x = position.x
481
493
  if needs_y_move:
482
- self._current_position["Y"] = position['Y']
494
+ self._current_position.y = position.y
483
495
 
484
496
  # Step 3: Move Z and A back to original position (or target if specified)
485
497
  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']
498
+ move_cmd = f"G1 Z{position.z} F{self._z_feed}"
499
+ self.execute(move_cmd)
500
+ self._wait_for_move()
501
+ self._current_position.z = position.z
489
502
  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()
494
-
495
- self._logger.info(
496
- "Move complete. Final position: %s", self._current_position
497
- )
503
+ move_cmd = f"G1 A{position.a} F{self._z_feed}"
504
+ self.execute(move_cmd)
505
+ self._wait_for_move()
506
+ self._current_position.a = position.a
498
507
  self._logger.debug("New internal position: %s", self._current_position)
499
-
500
- # Step 4: Post-move position synchronization check
501
- self.sync_position()
502
508
 
503
509
  return self._current_position
504
510
 
505
- def query_position(self) -> Dict[str, float]:
511
+ def query_position(self) -> Position:
506
512
  """
507
513
  Query the current machine position (M114 command).
508
514
 
509
515
  Returns:
510
- Dictionary containing X, Y, Z, and A positions
516
+ Position containing X, Y, Z, and A positions
511
517
 
512
518
  Note:
513
- Returns an empty dictionary if the query fails or no positions are found.
519
+ Returns an empty Position if the query fails or no positions are found.
514
520
  """
515
521
  self._logger.info("Querying current machine position (M114).")
516
- res: str = self.execute(self._build_command("M114"))
522
+ res: str = self.execute("M114")
517
523
 
518
524
  # Extract position values using regex
519
525
  pattern = re.compile(r"([XYZA]):(-?\d+\.\d+)")
@@ -523,7 +529,7 @@ class GCodeController(SerialController):
523
529
 
524
530
  for axis, value_str in matches:
525
531
  try:
526
- position_data[axis] = float(value_str)
532
+ position_data[axis.lower()] = float(value_str)
527
533
  except ValueError:
528
534
  self._logger.error(
529
535
  "Failed to convert position value '%s' for axis %s to float.",
@@ -532,9 +538,11 @@ class GCodeController(SerialController):
532
538
  )
533
539
  continue
534
540
 
535
- return position_data
541
+ position = Position.from_dict(position_data)
542
+ self._logger.info("Query position complete. Retrieved positions: %s", position)
543
+ return position
536
544
 
537
- def sync_position(self) -> Tuple[bool, Dict[str, float]]:
545
+ def _sync_position(self) -> Tuple[bool, Position]:
538
546
  """
539
547
  Synchronize internal position with actual machine position.
540
548
 
@@ -543,7 +551,7 @@ class GCodeController(SerialController):
543
551
  it by moving to the internal position.
544
552
 
545
553
  Returns:
546
- Tuple of (adjustment_occurred: bool, final_position: Dict[str, float])
554
+ Tuple of (adjustment_occurred: bool, final_position: Position)
547
555
  where adjustment_occurred is True if a correction move was made.
548
556
 
549
557
  Note:
@@ -554,29 +562,30 @@ class GCodeController(SerialController):
554
562
  # Query the actual machine position
555
563
  queried_position = self.query_position()
556
564
 
557
- if not queried_position:
565
+ if not queried_position.get_axes():
558
566
  self._logger.error("Query position failed. Cannot synchronize.")
559
567
  raise ValueError("Query position failed. Cannot synchronize.")
560
568
 
561
569
  # Compare internal vs. queried position
562
- axis_keys = ["X", "Y", "Z", "A"]
570
+ axis_keys = ["x", "y", "z", "a"]
563
571
  adjustment_needed = False
564
572
 
565
573
  for axis in axis_keys:
574
+ axis_upper = axis.upper()
566
575
  if (
567
- axis in self._current_position
568
- and axis in queried_position
576
+ self._current_position.has_axis(axis)
577
+ and queried_position.has_axis(axis)
569
578
  and abs(self._current_position[axis] - queried_position[axis])
570
579
  > self.TOLERANCE
571
580
  ):
572
581
  self._logger.warning(
573
582
  "Position mismatch found on %s axis: Internal=%.3f, Queried=%.3f",
574
- axis,
583
+ axis_upper,
575
584
  self._current_position[axis],
576
585
  queried_position[axis],
577
586
  )
578
587
  adjustment_needed = True
579
- elif axis in queried_position:
588
+ elif queried_position.has_axis(axis):
580
589
  # Update internal position with queried position if it differs slightly
581
590
  self._current_position[axis] = queried_position[axis]
582
591
 
@@ -588,11 +597,11 @@ class GCodeController(SerialController):
588
597
  )
589
598
 
590
599
  try:
591
- self.move_absolute(x=self._current_position["X"], y=self._current_position["Y"], z=self._current_position["Z"], a=self._current_position["A"])
600
+ self.move_absolute(position=self._current_position.copy())
592
601
  self._logger.info("Synchronization move successfully completed.")
593
602
 
594
603
  # Recursive call to verify position after move
595
- return self.sync_position()
604
+ return self._sync_position()
596
605
  except (ValueError, RuntimeError, OSError) as e:
597
606
  self._logger.error("Synchronization move failed: %s", e)
598
607
  adjustment_needed = False
@@ -614,14 +623,14 @@ class GCodeController(SerialController):
614
623
  Machine information string from the device
615
624
  """
616
625
  self._logger.info("Querying machine information (M115).")
617
- return self.execute(self._build_command("M115"))
626
+ return self.execute("M115")
618
627
 
619
- def get_internal_position(self) -> Dict[str, float]:
628
+ def get_internal_position(self) -> Position:
620
629
  """
621
630
  Get the internally tracked position.
622
631
 
623
632
  Returns:
624
- Dictionary containing the current internal position for all axes
633
+ Position containing the current internal position for all axes
625
634
  """
626
635
  self._logger.debug("Returning internal position: %s", self._current_position)
627
- return self._current_position.copy()
636
+ return self._current_position.copy()