puda-drivers 0.0.3__py3-none-any.whl → 0.0.4__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.
@@ -3,12 +3,11 @@ Generic Serial Controller for communicating with devices over serial ports.
3
3
  """
4
4
 
5
5
  import serial
6
- import sys
7
6
  import time
8
7
  import serial.tools.list_ports
9
8
  import logging
10
9
  from typing import Optional, List, Tuple
11
- from abc import ABC, abstractmethod
10
+ from abc import ABC
12
11
 
13
12
  logger = logging.getLogger(__name__)
14
13
 
@@ -58,23 +57,6 @@ class SerialController(ABC):
58
57
 
59
58
  self.connect()
60
59
 
61
- @staticmethod
62
- def list_ports(filter_desc: Optional[str] = None) -> List[Tuple[str, str, str]]:
63
- """
64
- Lists available serial ports on the system.
65
-
66
- .. deprecated:: 0.0.1
67
- Use the module-level function :func:`list_serial_ports` instead.
68
- This method is kept for backward compatibility.
69
-
70
- Args:
71
- filter_desc: Optional string to filter ports by description (case-insensitive).
72
-
73
- Returns:
74
- List of tuples, where each tuple contains (port_name, description, hwid).
75
- """
76
- return list_serial_ports(filter_desc)
77
-
78
60
  def connect(self) -> None:
79
61
  """
80
62
  Establishes the serial connection to the port.
@@ -89,7 +71,9 @@ class SerialController(ABC):
89
71
 
90
72
  try:
91
73
  self._logger.info(
92
- f"Attempting connection to {self.port_name} at {self.baudrate} baud."
74
+ "Attempting connection to %s at %s baud.",
75
+ self.port_name,
76
+ self.baudrate,
93
77
  )
94
78
  self._serial = serial.Serial(
95
79
  port=self.port_name,
@@ -97,10 +81,10 @@ class SerialController(ABC):
97
81
  timeout=self.timeout,
98
82
  )
99
83
  self._serial.flush()
100
- self._logger.info(f"Successfully connected to {self.port_name}.")
84
+ self._logger.info("Successfully connected to %s.", self.port_name)
101
85
  except serial.SerialException as e:
102
86
  self._serial = None
103
- self._logger.error(f"Error connecting to port {self.port_name}: {e}")
87
+ self._logger.error("Error connecting to port %s: %s", self.port_name, e)
104
88
  raise serial.SerialException(
105
89
  f"Error connecting to port {self.port_name}: {e}"
106
90
  )
@@ -113,7 +97,7 @@ class SerialController(ABC):
113
97
  port_name = self._serial.port
114
98
  self._serial.close()
115
99
  self._serial = None
116
- self._logger.info(f"Serial connection to {port_name} closed.")
100
+ self._logger.info("Serial connection to %s closed.", port_name)
117
101
  else:
118
102
  self._logger.warning(
119
103
  "Serial port already disconnected or was never connected."
@@ -131,29 +115,31 @@ class SerialController(ABC):
131
115
  """
132
116
  if not self.is_connected or not self._serial:
133
117
  self._logger.error(
134
- f"Attempt to send command '{command}' failed: Device not connected."
118
+ "Attempt to send command '%s' failed: Device not connected.",
119
+ command,
135
120
  )
136
121
  # Retain raising an error for being disconnected, as that's a connection state issue
137
122
  raise serial.SerialException("Device not connected. Call connect() first.")
138
123
 
139
- command_bytes = bytes(command, "utf-8")
140
- self._logger.info(f"-> Sending: {repr(command)}")
124
+ self._logger.info("-> Sending: %r", command)
141
125
 
142
126
  # Send the command
143
127
  try:
144
128
  self._serial.reset_input_buffer() # clear input buffer
145
129
  self._serial.reset_output_buffer() # clear output buffer
146
- self._serial.write(command_bytes)
130
+ self._serial.write(bytes(command, "utf-8"))
147
131
  self._serial.flush()
148
132
 
149
133
  except serial.SerialTimeoutException as e:
150
134
  # Log the timeout error and return None as requested (no re-raise)
151
- self._logger.error(f"Timeout on command '{command}'. Error: {e}")
135
+ self._logger.error("Timeout on command '%s'. Error: %s", command, e)
152
136
  return None
153
137
 
154
138
  except serial.SerialException as e:
155
139
  self._logger.error(
156
- f"Serial error writing or reading command '{command}'. Error: {e}"
140
+ "Serial error writing or reading command '%s'. Error: %s",
141
+ command,
142
+ e,
157
143
  )
158
144
  return None
159
145
 
@@ -173,22 +159,51 @@ class SerialController(ABC):
173
159
  # Read all available bytes
174
160
  response += self._serial.read(self._serial.in_waiting)
175
161
 
176
- # 5. Check if the 'ok' response is in the accumulated data
177
- if b"ok" in response:
178
- self._logger.debug(f"<- Received response: {response!r}")
179
- return response.decode("utf-8", errors="ignore").strip()
180
- if b"err" in response:
181
- self._logger.error(f"<- Received error: {response!r}")
182
- return response.decode("utf-8", errors="ignore").strip()
162
+ # Check for expected response markers for early return
163
+ if b"ok" in response or b"err" in response:
164
+ break
183
165
  else:
184
166
  time.sleep(0.1)
185
167
 
168
+ # Timeout reached - check what we got
186
169
  if not response:
187
- self._logger.warning("<- Received no data (empty response).")
188
- self._logger.error(f"No response within {self.timeout} seconds.")
189
- sys.exit(124) # Exit code for timeout
190
-
191
- self._logger.info(
192
- f"<- Received: {response.decode('utf-8', errors='ignore').strip()!r}"
193
- )
194
- return response.decode("utf-8", errors="ignore").strip()
170
+ self._logger.error("No response within %s seconds.", self.timeout)
171
+ raise serial.SerialTimeoutException(
172
+ f"No response received within {self.timeout} seconds."
173
+ )
174
+
175
+ # Decode once and check the decoded string
176
+ decoded_response = response.decode("utf-8", errors="ignore").strip()
177
+
178
+ if "ok" in decoded_response.lower():
179
+ self._logger.debug("<- Received response: %r", decoded_response)
180
+ elif "err" in decoded_response.lower():
181
+ self._logger.error("<- Received error: %r", decoded_response)
182
+ else:
183
+ self._logger.warning(
184
+ "<- Received unexpected response (no 'ok' or 'err'): %r", decoded_response
185
+ )
186
+
187
+ return decoded_response
188
+
189
+ def execute(self, command: str) -> str:
190
+ """
191
+ Send a command and read the response atomically.
192
+
193
+ This method combines sending a command and reading its response into a
194
+ single atomic operation, ensuring the response corresponds to the command
195
+ that was just sent. This is the preferred method for commands that
196
+ require a response.
197
+
198
+ Args:
199
+ command: Command string to send (should include protocol terminator if needed)
200
+
201
+ Returns:
202
+ Response string from the device
203
+
204
+ Raises:
205
+ serial.SerialException: If device is not connected or communication fails
206
+ serial.SerialTimeoutException: If no response is received within timeout
207
+ """
208
+ self._send_command(command)
209
+ return self._read_response()
@@ -2,24 +2,50 @@
2
2
  G-code controller for motion systems.
3
3
 
4
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.
5
+ systems (e.g., QuBot) via serial communication. All movements are executed in
6
+ absolute coordinates, with relative moves converted to absolute internally.
7
+ Supports homing and position synchronization.
7
8
  """
8
9
 
9
10
  import re
10
11
  import logging
11
- from typing import Optional, Dict, Tuple
12
+ from dataclasses import dataclass
13
+ from typing import Optional, Dict, Tuple, Union
12
14
 
13
15
  from puda_drivers.core.serialcontroller import SerialController
14
16
 
15
17
 
18
+ @dataclass
19
+ class AxisLimits:
20
+ """Holds min/max limits for an axis."""
21
+
22
+ min: float
23
+ max: float
24
+
25
+ def validate(self, value: float) -> None:
26
+ """
27
+ Validate that a value is within the axis limits.
28
+
29
+ Args:
30
+ value: Value to validate
31
+
32
+ Raises:
33
+ ValueError: If value is outside the limits
34
+ """
35
+ if not (self.min <= value <= self.max):
36
+ raise ValueError(
37
+ f"Value {value} outside axis limits [{self.min}, {self.max}]"
38
+ )
39
+
40
+
16
41
  class GCodeController(SerialController):
17
42
  """
18
43
  Controller for G-code compatible motion systems.
19
44
 
20
45
  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.
46
+ understand G-code commands. All movements are executed in absolute coordinates,
47
+ with relative moves converted to absolute internally. Supports homing and
48
+ position synchronization.
23
49
 
24
50
  Attributes:
25
51
  DEFAULT_FEEDRATE: Default feed rate in mm/min (3000)
@@ -68,7 +94,14 @@ class GCodeController(SerialController):
68
94
  "A": 0.0,
69
95
  }
70
96
  self._feed: int = feed
71
- self._is_absolute_mode: bool = True # absolute mode by default
97
+
98
+ # Initialize axis limits with default values
99
+ self._axis_limits: Dict[str, AxisLimits] = {
100
+ "X": AxisLimits(0, 0),
101
+ "Y": AxisLimits(0, 0),
102
+ "Z": AxisLimits(0, 0),
103
+ "A": AxisLimits(0, 0),
104
+ }
72
105
 
73
106
  @property
74
107
  def feed(self) -> int:
@@ -143,6 +176,102 @@ class GCodeController(SerialController):
143
176
  )
144
177
  return axis_upper
145
178
 
179
+ def _validate_move_positions(
180
+ self,
181
+ x: Optional[float] = None,
182
+ y: Optional[float] = None,
183
+ z: Optional[float] = None,
184
+ a: Optional[float] = None,
185
+ ) -> None:
186
+ """
187
+ Validate that move positions are within axis limits.
188
+
189
+ Only validates axes that are being moved (not None). Raises ValueError
190
+ if any position is outside the configured limits.
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
+
198
+ Raises:
199
+ ValueError: If any position is outside the axis limits
200
+ """
201
+ if x is not None:
202
+ if "X" in self._axis_limits:
203
+ try:
204
+ self._axis_limits["X"].validate(x)
205
+ except ValueError as e:
206
+ self._logger.error("Move validation failed for X axis: %s", e)
207
+ raise
208
+ if y is not None:
209
+ if "Y" in self._axis_limits:
210
+ try:
211
+ self._axis_limits["Y"].validate(y)
212
+ except ValueError as e:
213
+ self._logger.error("Move validation failed for Y axis: %s", e)
214
+ raise
215
+ if z is not None:
216
+ if "Z" in self._axis_limits:
217
+ try:
218
+ self._axis_limits["Z"].validate(z)
219
+ except ValueError as e:
220
+ self._logger.error("Move validation failed for Z axis: %s", e)
221
+ raise
222
+ if a is not None:
223
+ if "A" in self._axis_limits:
224
+ try:
225
+ self._axis_limits["A"].validate(a)
226
+ except ValueError as e:
227
+ self._logger.error("Move validation failed for A axis: %s", e)
228
+ raise
229
+
230
+ def set_axis_limits(
231
+ self, axis: str, min_val: float, max_val: float
232
+ ) -> None:
233
+ """
234
+ Set the min/max limits for an axis.
235
+
236
+ Args:
237
+ axis: Axis name ('X', 'Y', 'Z', 'A')
238
+ min_val: Minimum allowed value
239
+ max_val: Maximum allowed value
240
+
241
+ Raises:
242
+ ValueError: If axis is unknown or min >= max
243
+ """
244
+ axis = self._validate_axis(axis)
245
+
246
+ if min_val >= max_val:
247
+ raise ValueError("min must be < max")
248
+
249
+ self._axis_limits[axis] = AxisLimits(min_val, max_val)
250
+ self._logger.info(
251
+ "Set limits for axis %s: [%s, %s]", axis, min_val, max_val
252
+ )
253
+
254
+ def get_axis_limits(
255
+ self, axis: Optional[str] = None
256
+ ) -> Union[AxisLimits, Dict[str, AxisLimits]]:
257
+ """
258
+ Get the current limits for an axis or all axes.
259
+
260
+ Args:
261
+ axis: Optional axis name ('X', 'Y', 'Z', 'A'). If None, returns all limits.
262
+
263
+ Returns:
264
+ If axis is specified: AxisLimits object with min and max values.
265
+ If axis is None: Dictionary of all axis limits.
266
+
267
+ Raises:
268
+ ValueError: If axis is unknown (only when axis is provided)
269
+ """
270
+ if axis is None:
271
+ return self._axis_limits.copy()
272
+ axis = self._validate_axis(axis)
273
+ return self._axis_limits[axis]
274
+
146
275
  def home(self, axis: Optional[str] = None) -> None:
147
276
  """
148
277
  Home one or all axes (G28 command).
@@ -195,7 +324,13 @@ class GCodeController(SerialController):
195
324
  z: Target Z position (optional)
196
325
  a: Target A position (optional)
197
326
  feed: Feed rate for this move (optional, uses current feed if not specified)
327
+
328
+ Raises:
329
+ ValueError: If any position is outside the axis limits
198
330
  """
331
+ # Validate positions before executing move
332
+ self._validate_move_positions(x=x, y=y, z=z, a=a)
333
+
199
334
  feed_rate = feed if feed is not None else self._feed
200
335
  self._logger.info(
201
336
  "Preparing absolute move to X:%s, Y:%s, Z:%s, A:%s at F:%s",
@@ -206,12 +341,6 @@ class GCodeController(SerialController):
206
341
  feed_rate,
207
342
  )
208
343
 
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
344
  self._execute_move(x=x, y=y, z=z, a=a, feed=feed)
216
345
 
217
346
  def move_relative(
@@ -223,7 +352,7 @@ class GCodeController(SerialController):
223
352
  feed: Optional[int] = None,
224
353
  ) -> None:
225
354
  """
226
- Move relative to the current position (G91 + G1 command).
355
+ Move relative to the current position (converted to absolute move internally).
227
356
 
228
357
  Args:
229
358
  x: Relative X movement (optional)
@@ -231,6 +360,9 @@ class GCodeController(SerialController):
231
360
  z: Relative Z movement (optional)
232
361
  a: Relative A movement (optional)
233
362
  feed: Feed rate for this move (optional, uses current feed if not specified)
363
+
364
+ Raises:
365
+ ValueError: If any resulting absolute position is outside the axis limits
234
366
  """
235
367
  feed_rate = feed if feed is not None else self._feed
236
368
  self._logger.info(
@@ -242,13 +374,16 @@ class GCodeController(SerialController):
242
374
  feed_rate,
243
375
  )
244
376
 
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
377
+ # Convert relative movements to absolute positions
378
+ abs_x = (self.current_position["X"] + x) if x is not None else None
379
+ abs_y = (self.current_position["Y"] + y) if y is not None else None
380
+ abs_z = (self.current_position["Z"] + z) if z is not None else None
381
+ abs_a = (self.current_position["A"] + a) if a is not None else None
250
382
 
251
- self._execute_move(x=x, y=y, z=z, a=a, feed=feed)
383
+ # Validate absolute positions before executing move
384
+ self._validate_move_positions(x=abs_x, y=abs_y, z=abs_z, a=abs_a)
385
+
386
+ self._execute_move(x=abs_x, y=abs_y, z=abs_z, a=abs_a, feed=feed)
252
387
 
253
388
  def _execute_move(
254
389
  self,
@@ -260,6 +395,7 @@ class GCodeController(SerialController):
260
395
  ) -> None:
261
396
  """
262
397
  Internal helper for executing G1 move commands with safe movement pattern.
398
+ All coordinates are treated as absolute positions.
263
399
 
264
400
  Safe move pattern:
265
401
  1. If X or Y movement is needed, first move Z to 0 (safe height)
@@ -267,13 +403,13 @@ class GCodeController(SerialController):
267
403
  3. Finally move Z to target position (if specified)
268
404
 
269
405
  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)
406
+ x: Absolute X position (optional)
407
+ y: Absolute Y position (optional)
408
+ z: Absolute Z position (optional)
409
+ a: Absolute A position (optional)
274
410
  feed: Feed rate (optional)
275
411
  """
276
- # Calculate target positions
412
+ # Calculate target positions (all absolute)
277
413
  target_pos = self.current_position.copy()
278
414
  has_x = x is not None
279
415
  has_y = y is not None
@@ -286,30 +422,23 @@ class GCodeController(SerialController):
286
422
  )
287
423
  return
288
424
 
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
425
+ # Set target positions (all absolute)
426
+ if has_x:
427
+ target_pos["X"] = x
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
308
434
 
309
435
  feed_rate = feed if feed is not None else self._feed
310
436
  if feed_rate > self.MAX_FEEDRATE:
311
437
  feed_rate = self.MAX_FEEDRATE
312
438
 
439
+ # Ensure absolute mode is active
440
+ self._send_command(self._build_command("G90"))
441
+
313
442
  # Safe move pattern: Z to 0, then XY, then Z to target
314
443
  needs_xy_move = has_x or has_y
315
444
  current_z = self.current_position["Z"]
@@ -317,15 +446,12 @@ class GCodeController(SerialController):
317
446
 
318
447
  # Step 1: Move Z to safe height (0) if XY movement is needed and Z is not already at 0
319
448
  if needs_xy_move and abs(current_z) > 0.001: # Small tolerance for floating point
449
+ # Validate safe height (Z=0) is within limits
450
+ self._validate_move_positions(z=0.0)
320
451
  self._logger.info(
321
452
  "Safe move: Raising Z to safe height (0) before XY movement"
322
453
  )
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}"
454
+ move_cmd = f"G1 Z0 F{feed_rate}"
329
455
  self._send_command(self._build_command(move_cmd))
330
456
  self.current_position["Z"] = 0.0
331
457
  self._logger.debug("Z moved to safe height (0)")
@@ -334,21 +460,11 @@ class GCodeController(SerialController):
334
460
  if needs_xy_move or has_a:
335
461
  move_cmd = "G1"
336
462
  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}"
463
+ move_cmd += f" X{target_pos['X']}"
342
464
  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}"
465
+ move_cmd += f" Y{target_pos['Y']}"
347
466
  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}"
467
+ move_cmd += f" A{target_pos['A']}"
352
468
  move_cmd += f" F{feed_rate}"
353
469
 
354
470
  self._logger.info("Executing XY move command: %s", move_cmd)
@@ -366,11 +482,7 @@ class GCodeController(SerialController):
366
482
  if has_z:
367
483
  z_needs_move = abs(target_z - (0.0 if needs_xy_move else current_z)) > 0.001
368
484
  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}"
485
+ move_cmd = f"G1 Z{target_z} F{feed_rate}"
374
486
 
375
487
  if needs_xy_move:
376
488
  self._logger.info(
@@ -401,8 +513,7 @@ class GCodeController(SerialController):
401
513
  Returns an empty dictionary if the query fails or no positions are found.
402
514
  """
403
515
  self._logger.info("Querying current machine position (M114).")
404
- self._send_command(self._build_command("M114"))
405
- res: str = self._read_response()
516
+ res: str = self.execute(self._build_command("M114"))
406
517
 
407
518
  # Extract position values using regex
408
519
  pattern = re.compile(r"([XYZA]):(-?\d+\.\d+)")
@@ -491,8 +602,6 @@ class GCodeController(SerialController):
491
602
  self._logger.error("Synchronization move failed: %s", e)
492
603
  adjustment_needed = False
493
604
 
494
- final_position = self.current_position.copy()
495
-
496
605
  if adjustment_needed:
497
606
  self._logger.info(
498
607
  "Position check complete. Internal position is synchronized with machine."
@@ -500,7 +609,7 @@ class GCodeController(SerialController):
500
609
  else:
501
610
  self._logger.info("No adjustment was made.")
502
611
 
503
- return adjustment_needed, final_position
612
+ return adjustment_needed, self.current_position.copy()
504
613
 
505
614
  def get_info(self) -> str:
506
615
  """
@@ -510,9 +619,7 @@ class GCodeController(SerialController):
510
619
  Machine information string from the device
511
620
  """
512
621
  self._logger.info("Querying machine information (M115).")
513
- self._send_command(self._build_command("M115"))
514
- res: str = self._read_response()
515
- return res
622
+ return self.execute(self._build_command("M115"))
516
623
 
517
624
  def get_internal_position(self) -> Dict[str, float]:
518
625
  """
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: puda-drivers
3
- Version: 0.0.3
3
+ Version: 0.0.4
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
@@ -158,8 +158,6 @@ for port, desc, hwid in ports:
158
158
  sartorius_ports = list_serial_ports(filter_desc="Sartorius")
159
159
  ```
160
160
 
161
- **Note:** The `list_ports()` method is also available as a static method on `SerialController` for backward compatibility, but the module-level `list_serial_ports()` function is the recommended approach.
162
-
163
161
  ## Requirements
164
162
 
165
163
  - Python >= 3.14
@@ -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=-wdQqq30yb6dSsNwA2FQec5HUh0U3j71OOMt6pbQIpc,7230
4
+ puda_drivers/core/serialcontroller.py,sha256=c-_kMLKxz9hLhpG-54wENkLeN0bNzY1rP9zML1RV9uc,7660
5
5
  puda_drivers/move/__init__.py,sha256=i7G5VKD5FgnmC21TLxoASVtC88IrPUTLDJrTnp99u-0,35
6
- puda_drivers/move/gcode.py,sha256=1KkpgwXBiNrcV4Ilc-6n-ADQwOVwVgK_9x08LKhbE5g,18106
6
+ puda_drivers/move/gcode.py,sha256=hgI5YzvSABJBlz5QlaNoUysB_7m9dhqXXkNm-zguYqQ,21504
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.3.dist-info/METADATA,sha256=and-Tnldz1Er_yVN76nJdmDDCsqoiEEJmyIovzaXNMI,5153
15
- puda_drivers-0.0.3.dist-info/WHEEL,sha256=WLgqFyCfm_KASv4WHyYy0P3pM_m7J5L9k2skdKLirC8,87
16
- puda_drivers-0.0.3.dist-info/licenses/LICENSE,sha256=7EI8xVBu6h_7_JlVw-yPhhOZlpY9hP8wal7kHtqKT_E,1074
17
- puda_drivers-0.0.3.dist-info/RECORD,,
14
+ puda_drivers-0.0.4.dist-info/METADATA,sha256=Kx3TqzraU6R_AxNW8kPRK5F9GntTiBDqk8Z7HEFDhDs,4948
15
+ puda_drivers-0.0.4.dist-info/WHEEL,sha256=WLgqFyCfm_KASv4WHyYy0P3pM_m7J5L9k2skdKLirC8,87
16
+ puda_drivers-0.0.4.dist-info/licenses/LICENSE,sha256=7EI8xVBu6h_7_JlVw-yPhhOZlpY9hP8wal7kHtqKT_E,1074
17
+ puda_drivers-0.0.4.dist-info/RECORD,,