robotframework-parrot-touchcontroller 1.1.0__tar.gz → 1.1.2__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 (16) hide show
  1. {robotframework_parrot_touchcontroller-1.1.0 → robotframework_parrot_touchcontroller-1.1.2}/PKG-INFO +1 -1
  2. {robotframework_parrot_touchcontroller-1.1.0 → robotframework_parrot_touchcontroller-1.1.2}/parrot/TouchController.py +7 -0
  3. {robotframework_parrot_touchcontroller-1.1.0 → robotframework_parrot_touchcontroller-1.1.2}/parrot/touch/axitouch.py +53 -17
  4. {robotframework_parrot_touchcontroller-1.1.0 → robotframework_parrot_touchcontroller-1.1.2}/pyproject.toml +1 -1
  5. {robotframework_parrot_touchcontroller-1.1.0 → robotframework_parrot_touchcontroller-1.1.2}/robotframework_parrot_touchcontroller.egg-info/PKG-INFO +1 -1
  6. {robotframework_parrot_touchcontroller-1.1.0 → robotframework_parrot_touchcontroller-1.1.2}/robotframework_parrot_touchcontroller.egg-info/SOURCES.txt +0 -1
  7. robotframework_parrot_touchcontroller-1.1.0/parrot/touch/axitouchserial.py +0 -1147
  8. {robotframework_parrot_touchcontroller-1.1.0 → robotframework_parrot_touchcontroller-1.1.2}/README.md +0 -0
  9. {robotframework_parrot_touchcontroller-1.1.0 → robotframework_parrot_touchcontroller-1.1.2}/parrot/touch/adbtouch.py +0 -0
  10. {robotframework_parrot_touchcontroller-1.1.0 → robotframework_parrot_touchcontroller-1.1.2}/parrot/touch/base.py +0 -0
  11. {robotframework_parrot_touchcontroller-1.1.0 → robotframework_parrot_touchcontroller-1.1.2}/parrot/touch/dummy.py +0 -0
  12. {robotframework_parrot_touchcontroller-1.1.0 → robotframework_parrot_touchcontroller-1.1.2}/parrot/touch/usbhidtouch.py +0 -0
  13. {robotframework_parrot_touchcontroller-1.1.0 → robotframework_parrot_touchcontroller-1.1.2}/robotframework_parrot_touchcontroller.egg-info/dependency_links.txt +0 -0
  14. {robotframework_parrot_touchcontroller-1.1.0 → robotframework_parrot_touchcontroller-1.1.2}/robotframework_parrot_touchcontroller.egg-info/requires.txt +0 -0
  15. {robotframework_parrot_touchcontroller-1.1.0 → robotframework_parrot_touchcontroller-1.1.2}/robotframework_parrot_touchcontroller.egg-info/top_level.txt +0 -0
  16. {robotframework_parrot_touchcontroller-1.1.0 → robotframework_parrot_touchcontroller-1.1.2}/setup.cfg +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: robotframework-parrot-touchcontroller
3
- Version: 1.1.0
3
+ Version: 1.1.2
4
4
  Summary: Touch control api to use within the Robot Framework ecosystem
5
5
  Author-email: Zilogic Systems <code@zilogic.com>
6
6
  License-Expression: Apache-2.0
@@ -40,6 +40,12 @@ class TouchController:
40
40
  - `width`, `height`: Touch surface resolution in pixels
41
41
  - `start_pos`: Physical start coordinates (e.g., `[0, 0]`)
42
42
  - `end_pos`: Physical end coordinates defining working area (e.g., `[8.66, 5.12]`)
43
+ - `orientation`: Defines how touchscreen pixel coordinates are mapped to
44
+ the AxiDraw's coordinate system (e.g., `landscape` or `portrait`).
45
+ - `landscape`: Screen X maps to Robot Y, Screen Y maps to Robot X.
46
+ - `portrait`: Screen X maps to Robot X, Screen Y maps to Robot Y.
47
+
48
+
43
49
 
44
50
  === ADBTouch ===
45
51
  Simulates touch events on an Android device using ADB commands.
@@ -66,6 +72,7 @@ class TouchController:
66
72
  | height: 600
67
73
  | start_pos: [0, 0]
68
74
  | end_pos: [8.66, 5.12]
75
+ | orientation: landscape
69
76
  |
70
77
  | T3:
71
78
  | type: ADBTouch
@@ -50,6 +50,7 @@ class AxiDrawTouch(BaseTouch):
50
50
  self.end_y_pos = None
51
51
  self.start_position = None
52
52
  self.end_position = None
53
+ self.orientation = config.get("orientation")
53
54
 
54
55
  # Optional: configure pen heights and speed
55
56
  pen_pos_up = 75
@@ -101,6 +102,7 @@ class AxiDrawTouch(BaseTouch):
101
102
  *Args:*
102
103
  `x` (float): X-coordinate in pixels.
103
104
  `y` (float): Y-coordinate in pixels.
105
+ `orientation` (str): The orientation of the screen ('landscape' or 'portrait').
104
106
 
105
107
  *Returns:*
106
108
  `Tuple[float, float]`: Corresponding Cartesian coordinates (x, y) in inches.
@@ -109,36 +111,65 @@ class AxiDrawTouch(BaseTouch):
109
111
  ValueError: If the screen coordinates are out of bounds or if calibration data is missing.
110
112
 
111
113
  *NOTE:*
112
- This method is valid only when the screen X-axis maps to the robot Y-axis and the screen Y-axis maps to the robot X-axis, as shown below.
114
+ Use "landscape" when the screen X-axis maps to the robot Y-axis and
115
+ the screen Y-axis maps to the robot X-axis, as shown below.
113
116
 
114
117
  Robot Y
115
118
  +-----------------------------+
116
- | Screen X |
117
- Robot X | +-------------------+ |
118
- | | | Screen Y
119
+ | |
120
+ | Screen X |
121
+ | +-------------------+ |
122
+ Robot X | | | Screen Y
123
+ | | | |
119
124
  | +-------------------+ |
120
125
  | |
126
+ | |
127
+ | |
121
128
  +-----------------------------+
122
129
 
130
+ Use "portrait" when the screen X-axis maps to the robot X-axis and
131
+ the screen Y-axis maps to the robot Y-axis, as shown below.
132
+
133
+
134
+ Robot Y
135
+ +-----------------------------+
136
+ | Screen Y |
137
+ | +-------+ |
138
+ | | | |
139
+ Robot X | | | |
140
+ | | | Screen X |
141
+ | | | |
142
+ | | | |
143
+ | | | |
144
+ | +-------+ |
145
+ +-----------------------------+
123
146
  """
124
147
 
125
148
  x = float(x)
126
149
  y = float(y)
127
150
 
151
+ if not self.orientation:
152
+ self.orientation = "landscape"
153
+
128
154
  sx, sy = self.cartesian_start
129
155
  ex, ey = self.cartesian_end
130
156
 
131
- nx = y
132
- ny = x
157
+ if self.orientation == 'landscape':
158
+ x_scale = (ex - sx) / self.screen_height_px
159
+ y_scale = (ey - sy) / self.screen_width_px
133
160
 
134
- norm_w = self.screen_height_px # 720
135
- norm_h = self.screen_width_px # 1920
161
+ robot_x = sx + y * x_scale
162
+ robot_y = sy + x * y_scale
136
163
 
137
- x_scale = (ex - sx) / norm_w
138
- y_scale = (ey - sy) / norm_h
164
+ elif self.orientation == 'portrait':
165
+ x_scale = (ex - sx) / self.screen_width_px
166
+ y_scale = (ey - sy) / self.screen_height_px
139
167
 
140
- robot_x = sx + nx * x_scale
141
- robot_y = sy + ny * y_scale
168
+ robot_x = sx + x * x_scale
169
+ robot_y = sy + y * y_scale
170
+
171
+ else:
172
+ raise ValueError("orientation must be either 'landscape' or 'portrait'")
142
173
 
143
174
  return round(robot_x, 3), round(robot_y, 3)
144
175
 
@@ -279,12 +310,17 @@ class AxiDrawTouch(BaseTouch):
279
310
  raise Error("Failed to Calibrate the device") from err
280
311
 
281
312
  def _calibrate_corner(self, message: str):
313
+ """Calibrate a corner and reconnect the AXI server afterward."""
282
314
  self.axi.disconnect()
283
- self.wait_for_ok(message)
284
- self.axi.connect()
285
- x, y = self.axi.calibrate_with_limit_switch()
286
- return round(abs(x), 2), round(abs(y), 2)
315
+ try:
316
+ self.wait_for_ok(message)
317
+ self.axi.connect()
318
+ x, y = self.axi.calibrate_with_limit_switch()
319
+ return round(abs(x), 2), round(abs(y), 2)
287
320
 
321
+ finally:
322
+ self.axi.disconnect()
323
+ self.axi.connect()
288
324
 
289
325
  def save_calibrate_position(self):
290
326
  try:
@@ -340,4 +376,4 @@ class AxiDrawTouch(BaseTouch):
340
376
  if param == 'auto_home' and param in self.PROPERTIES:
341
377
  self.auto_home = value
342
378
  else:
343
- raise Error(f"ArucoCapture Error: Unsupported parameter: {param}")
379
+ raise Error(f"ArucoCapture Error: Unsupported parameter: {param}")
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "robotframework-parrot-touchcontroller"
7
- version = "1.1.0"
7
+ version = "1.1.2"
8
8
  description = "Touch control api to use within the Robot Framework ecosystem"
9
9
  authors = [
10
10
  { name = "Zilogic Systems", email = "code@zilogic.com" },
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: robotframework-parrot-touchcontroller
3
- Version: 1.1.0
3
+ Version: 1.1.2
4
4
  Summary: Touch control api to use within the Robot Framework ecosystem
5
5
  Author-email: Zilogic Systems <code@zilogic.com>
6
6
  License-Expression: Apache-2.0
@@ -3,7 +3,6 @@ pyproject.toml
3
3
  parrot/TouchController.py
4
4
  parrot/touch/adbtouch.py
5
5
  parrot/touch/axitouch.py
6
- parrot/touch/axitouchserial.py
7
6
  parrot/touch/base.py
8
7
  parrot/touch/dummy.py
9
8
  parrot/touch/usbhidtouch.py
@@ -1,1147 +0,0 @@
1
- # pylint: disable=no-member
2
-
3
- from robot.api.deco import keyword
4
- from parrot.touch.base import BaseTouch,Error
5
- from typing import Tuple, Any
6
- import yaml
7
- import os
8
- import tkinter as tk
9
- from tkinter import messagebox
10
-
11
- from __future__ import division
12
- from __future__ import print_function
13
-
14
- from bisect import bisect
15
- from collections import namedtuple
16
- from math import sqrt, hypot, modf
17
-
18
- from serial import Serial
19
- from serial.tools.list_ports import comports
20
-
21
- TIMESLICE_MS = 10
22
-
23
- MICROSTEPPING_MODE = 1
24
- STEP_DIVIDER = 2 ** (MICROSTEPPING_MODE - 1)
25
-
26
- STEPS_PER_INCH = 2032 / STEP_DIVIDER
27
- STEPS_PER_MM = 80 / STEP_DIVIDER
28
-
29
- PEN_UP_POSITION = 60
30
- PEN_UP_SPEED = 150
31
- PEN_UP_DELAY = 0
32
-
33
- PEN_DOWN_POSITION = 40
34
- PEN_DOWN_SPEED = 150
35
- PEN_DOWN_DELAY = 0
36
-
37
- ACCELERATION = 16
38
- MAX_VELOCITY = 4
39
- CORNER_FACTOR = 0.001
40
-
41
- JOG_ACCELERATION = 16
42
- JOG_MAX_VELOCITY = 8
43
-
44
- VID_PID = '04D8:FD92'
45
-
46
- EPS = 1e-9
47
-
48
-
49
-
50
- class AxiTouchSerialError(Exception):
51
- """Represents an error in the AxiDrawTouch controller."""
52
-
53
-
54
- class AxiDrawTouchSerial(BaseTouch):
55
- """A touch controller implementation using the AxiDraw plotting device
56
- connected by serial.
57
-
58
- This class provides methods to simulate touch gestures such as tap and swipe
59
- by controlling an AxiDraw plotter based on screen coordinates.
60
- """
61
-
62
- PROPERTIES = ["auto_home"]
63
-
64
- def __init__(self, config: dict[str, Any]) -> None:
65
- """Initializes the AxiDraw touch controller.
66
-
67
- The configuration dictionary must contain:
68
- - `width`: screen width in pixels
69
- - `height`: screen height in pixels
70
- - `start_pos`: tuple (x, y) of starting cartesian coordinates (in inches)
71
- - `end_pos`: tuple (x, y) of ending cartesian coordinates (in inches)
72
-
73
- Raises:
74
- AxiTouchError: If initialization or connection to AxiDraw fails.
75
- """
76
-
77
- try:
78
- uri = f"http://{config['host']}:{config['port']}"
79
- self.screen_width_px: int = config["width"]
80
- self.screen_height_px: int = config["height"]
81
- self.auto_home = False # Flag to track auto-home behavior
82
- self.calibration_file: str = config["calibration_data"]
83
- self.device_name = config.get("device_name")
84
- self.start_x_pos = None
85
- self.start_y_pos = None
86
- self.end_x_pos = None
87
- self.end_y_pos = None
88
- self.start_position = None
89
- self.end_position = None
90
-
91
- # Optional: configure pen heights and speed
92
- pen_pos_up = 75
93
- pen_pos_down = 40
94
- speed_pendown = 100
95
- speed_penup = 100
96
- accel = 75
97
-
98
- self.axi = xmlrpc.client.ServerProxy(uri)
99
-
100
- self.axi.connect(accel,
101
- pen_pos_up, pen_pos_down,
102
- speed_penup, speed_pendown)
103
- self._load_calibration_data()
104
-
105
- except KeyError as err:
106
- raise Error(f"Missing required configuration key: {err}") from err
107
- except Exception as err:
108
- raise Error(f"Initialization failed: {err}") from err
109
-
110
- def _load_calibration_data(self):
111
- """Load start and end Cartesian positions from calibration YAML."""
112
- if not os.path.exists(self.calibration_file):
113
- raise FileNotFoundError(
114
- f"Calibration file '{self.calibration_file}' not found. "
115
- f"Please perform calibration to get the start and end positions."
116
- )
117
-
118
- try:
119
- with open(self.calibration_file, 'r') as file:
120
- config = yaml.safe_load(file) or {}
121
- except yaml.YAMLError as e:
122
- raise ValueError(
123
- f"Error reading YAML from '{self.calibration_file}': {e}"
124
- )
125
-
126
- if "start_position" not in config or "end_position" not in config:
127
- raise KeyError(
128
- "Missing 'start_position' or 'end_position' in calibration file. "
129
- "Please perform calibration again."
130
- )
131
-
132
- self.cartesian_start = tuple(config["start_position"])
133
- self.cartesian_end = tuple(config["end_position"])
134
-
135
- def convert_to_inches(self, x: float, y: float) -> Tuple[float, float]:
136
- """Convert screen coordinates (x, y) in pixels to Cartesian coordinates in inches.
137
-
138
- *Args:*
139
- `x` (float): X-coordinate in pixels.
140
- `y` (float): Y-coordinate in pixels.
141
-
142
- *Returns:*
143
- `Tuple[float, float]`: Corresponding Cartesian coordinates (x, y) in inches.
144
-
145
- *Raises:*
146
- ValueError: If the screen coordinates are out of bounds or if calibration data is missing.
147
-
148
- *NOTE:*
149
- This method is valid only when the screen X-axis maps to the robot Y-axis and the screen Y-axis maps to the robot X-axis, as shown below.
150
-
151
- Robot Y
152
- +-----------------------------+
153
- | Screen X |
154
- Robot X | +-------------------+ |
155
- | | | Screen Y
156
- | +-------------------+ |
157
- | |
158
- +-----------------------------+
159
-
160
- """
161
-
162
- x = float(x)
163
- y = float(y)
164
-
165
- sx, sy = self.cartesian_start
166
- ex, ey = self.cartesian_end
167
-
168
- nx = y
169
- ny = x
170
-
171
- norm_w = self.screen_height_px # 720
172
- norm_h = self.screen_width_px # 1920
173
-
174
- x_scale = (ex - sx) / norm_w
175
- y_scale = (ey - sy) / norm_h
176
-
177
- robot_x = sx + nx * x_scale
178
- robot_y = sy + ny * y_scale
179
-
180
- return round(robot_x, 3), round(robot_y, 3)
181
-
182
- def _validate_screen_coordinates(self, x: float, y: float) -> None:
183
- """Validate that the screen coordinates are within the configured display."""
184
-
185
- if not (0 <= x <= self.screen_width_px):
186
- raise Error(
187
- f"Touch tap failed at ({x}, {y}) due to out-of-bound coordinates"
188
- )
189
-
190
- if not (0 <= y <= self.screen_height_px):
191
- raise Error(
192
- f"Touch tap failed at ({x}, {y}) due to out-of-bound coordinates"
193
- )
194
-
195
-
196
- def touch_init(self) -> None:
197
- """Initialize the pen by lifting it up and moving to origin (0, 0).
198
-
199
- Raises:
200
- AxiTouchError: If initialization movement fails.
201
- """
202
-
203
- try:
204
- self.axi.penup()
205
- self.axi.home()
206
- print("AxiDrawTouch: Initialized and moved to origin")
207
- except Exception as err:
208
- raise Error("Initialization failed") from err
209
-
210
- def touch_tap(self, x: float, y: float, tap_time) -> None:
211
- """Simulate a tap at the given screen coordinates.
212
-
213
- Args:
214
- x (int | float): X-coordinate in pixels.
215
- y (int | float): Y-coordinate in pixels.
216
- tap_time (float): Duration to hold pen down.
217
- Raises:
218
- AxiTouchError: If the tap operation fails.
219
- """
220
-
221
- try:
222
- x = float(x)
223
- y = float(y)
224
- self._validate_screen_coordinates(x, y)
225
- px, py = self.convert_to_inches(x, y)
226
- print(px, py)
227
- self.axi.tap(px, py, tap_time)
228
- if self.auto_home:
229
- self.axi.home()
230
- print(f"AxiDrawTouch: Tap at ({x}, {y}) for {tap_time}s")
231
- except Error:
232
- raise
233
- except Exception as err:
234
- raise Error(f"Touch tap failed at ({x}, {y})") from err
235
-
236
- def touch_swipe(
237
- self,
238
- startx: float,
239
- starty: float,
240
- endx: float,
241
- endy: float
242
- ) -> None:
243
- """Simulate a swipe gesture from start to end coordinates.
244
-
245
- Args:
246
- startx (int | float): Start X-coordinate in pixels.
247
- starty (int | float): Start Y-coordinate in pixels.
248
- endx (int | float): End X-coordinate in pixels.
249
- endy (int | float): End Y-coordinate in pixels.
250
- swipe_time (float): Duration of the swipe.
251
-
252
- Raises:
253
- AxiTouchError: If the swipe operation fails.
254
- """
255
-
256
- try:
257
- startx = float(startx)
258
- starty = float(starty)
259
- endx = float(endx)
260
- endy = float(endy)
261
- self._validate_screen_coordinates(startx, starty)
262
- self._validate_screen_coordinates(endx, endy)
263
- psx, psy = self.convert_to_inches(startx, starty)
264
- pex, pey = self.convert_to_inches(endx, endy)
265
- self.axi.swipe(psx, psy, pex, pey)
266
-
267
- print(
268
- f"AxiDrawTouch: Swipe from "f"({startx}, {starty}) to ({endx}, {endy})")
269
-
270
- except Error:
271
- raise
272
-
273
- except Exception as err:
274
- raise Error(
275
- f"Swipe failed from "
276
- f"({startx}, {starty}) to ({endx}, {endy})") from err
277
-
278
-
279
- def move(self, x: float, y: float):
280
- """
281
- Moving Axi arm to the absolute position in axi usable
282
- area. where x and y are inches.
283
- """
284
- try:
285
- if x == 0 and y == 0:
286
- self.axi.home()
287
- return
288
- self.axi.move(x, y)
289
- except Exception as err:
290
- raise Error(f"Failed to Move pointing device from ({x}, {y})") from err
291
-
292
- def home(self):
293
- """Returns the touch to its home default position."""
294
- try:
295
- self.axi.home_with_limit_switch()
296
- except Exception as err:
297
- raise Error(f"Failed to Home pointing device") from err
298
-
299
- def wait_for_ok(self, message):
300
- root = tk.Tk()
301
- root.withdraw()
302
- messagebox.showinfo("Position Calculation", message)
303
- root.destroy()
304
-
305
- def calibrate(self):
306
- """Returns the calibration for start and end position"""
307
- try:
308
-
309
- self.start_x_pos, self.start_y_pos = self._calibrate_corner(f"Move the TouchBot arm to the top-left corner of {self.device_name}. Click OK to continue.")
310
-
311
- self.end_x_pos, self.end_y_pos = self._calibrate_corner(f"Move the TouchBot arm to the bottom-right corner of {self.device_name}. Click OK to continue.")
312
-
313
- self.save_calibrate_position()
314
-
315
- except Exception as err:
316
- raise Error("Failed to Calibrate the device") from err
317
-
318
- def _calibrate_corner(self, message: str):
319
- self.axi.disconnect()
320
- self.wait_for_ok(message)
321
- self.axi.connect()
322
- x, y = self.axi.calibrate_with_limit_switch()
323
- return round(abs(x), 2), round(abs(y), 2)
324
-
325
-
326
- def save_calibrate_position(self):
327
- try:
328
- calibrate_position = {}
329
-
330
- if not os.path.exists(self.calibration_file):
331
- pass
332
- else:
333
- with open(self.calibration_file, "r") as file:
334
- loaded_file = yaml.safe_load(file)
335
- if isinstance(loaded_file, dict):
336
- calibrate_position = loaded_file
337
-
338
- calibrate_position["start_position"] = [self.start_x_pos, self.start_y_pos]
339
- calibrate_position["end_position"] = [self.end_x_pos, self.end_y_pos]
340
-
341
- with open(self.calibration_file, "w") as file:
342
- yaml.safe_dump(calibrate_position, file, sort_keys=False)
343
-
344
- print("YAML file updated successfully.")
345
-
346
- except Exception as err:
347
- raise Error(f"Error saving ArUco points: {err}") from err
348
-
349
-
350
- def touch_command(self, cmd, *args):
351
- """Executes a backend-specific touch command on the given device controller."""
352
- cmd = cmd.lower()
353
- if cmd == "move":
354
- if len(args) != 2:
355
- raise Error("move command expects x and y as argument")
356
- x = float(args[0])
357
- y = float(args[1])
358
- self.move(x, y)
359
- elif cmd == "home":
360
- self.home()
361
- elif cmd == "calibrate":
362
- self.calibrate()
363
- else:
364
- raise Error(f"Invalid command {cmd}")
365
-
366
- def set_param(self, param: str, value: bool) -> None:
367
- """
368
- Set a runtime parameter for ArucoCapture.
369
-
370
- | *Arguments* | *Description* |
371
- | `param (str)` | Supported: `auto_home`. |
372
- | `value (bool)` | True or False Value Supported. |
373
-
374
- *Raises:*
375
- | `Error` | If an unsupported parameter is provided. |
376
- """
377
- if param == 'auto_home' and param in self.PROPERTIES:
378
- self.auto_home = value
379
- else:
380
- raise Error(f"ArucoCapture Error: Unsupported parameter: {param}")
381
-
382
-
383
- def path_length(points):
384
- result = 0
385
- for (x1, y1), (x2, y2) in zip(points, points[1:]):
386
- result += hypot(x2 - x1, y2 - y1)
387
- return result
388
-
389
- class Planner(object):
390
- """A planner computes a motion
391
- profile for a list of (x, y) points"""
392
- def __init__(self, acceleration, max_velocity, corner_factor):
393
- self.acceleration = acceleration
394
- self.max_velocity = max_velocity
395
- self.corner_factor = corner_factor
396
-
397
- def plan(self, points):
398
- return constant_acceleration_plan(
399
- points, self.acceleration, self.max_velocity, self.corner_factor)
400
-
401
- def plan_all(self, paths):
402
- return [self.plan(path) for path in paths]
403
-
404
- class Plan(object):
405
- """A plan is a motion profile generated
406
- by the planner"""
407
- def __init__(self, blocks):
408
- self.blocks = blocks
409
- self.ts = [] # start time of each block
410
- self.ss = [] # start distance of each block
411
- t = 0
412
- s = 0
413
- for b in blocks:
414
- self.ts.append(t)
415
- self.ss.append(s)
416
- t += b.t
417
- s += b.s
418
- self.t = t # total time
419
- self.s = s # total duration
420
-
421
- def instant(self, t):
422
- t = max(0, min(self.t, t)) # clamp t
423
- i = bisect(self.ts, t) - 1 # find block for t
424
- return self.blocks[i].instant(t - self.ts[i], self.ts[i], self.ss[i])
425
-
426
- class Block(object):
427
- """
428
- A block is a constant acceleration for a duration of time
429
- """
430
- def __init__(self, a, t, vi, p1, p2):
431
- self.a = a
432
- self.t = t
433
- self.vi = vi
434
- self.p1 = p1
435
- self.p2 = p2
436
- self.s = p1.distance(p2)
437
-
438
- def instant(self, t, dt=0, ds=0):
439
- t = max(0, min(self.t, t)) # clamp t
440
- a = self.a
441
- v = self.vi + self.a * t
442
- s = self.vi * t + self.a * t * t / 2
443
- s = max(0, min(self.s, s)) # clamp s
444
- p = self.p1.lerps(self.p2, s)
445
- return Instant(t + dt, p, s + ds, v, a)
446
-
447
- # An instant gives position, velocity, etc. at a single point in time
448
- Instant = namedtuple('Instant', ['t', 'p', 's', 'v', 'a'])
449
-
450
- # a = acceleration
451
- # v = velocity
452
- # s = distance
453
- # t = time
454
- # i = initial
455
- # f = final
456
-
457
- _Point = namedtuple('Point', ['x', 'y'])
458
-
459
- class Point(_Point):
460
- def length(self):
461
- return hypot(self.x, self.y)
462
-
463
- def normalize(self):
464
- d = self.length()
465
- if d == 0:
466
- return Point(0, 0)
467
- return Point(self.x / d, self.y / d)
468
-
469
- def distance(self, other):
470
- return hypot(self.x - other.x, self.y - other.y)
471
-
472
- def distance_squared(self, other):
473
- return (self.x - other.x) ** 2 + (self.y - other.y) ** 2
474
-
475
- def add(self, other):
476
- return Point(self.x + other.x, self.y + other.y)
477
-
478
- def sub(self, other):
479
- return Point(self.x - other.x, self.y - other.y)
480
-
481
- def mul(self, factor):
482
- return Point(self.x * factor, self.y * factor)
483
-
484
- def dot(self, other):
485
- return self.x * other.x + self.y * other.y
486
-
487
- def lerps(self, other, s):
488
- v = other.sub(self).normalize()
489
- return self.add(v.mul(s))
490
-
491
- def segment_distance(self, v, w):
492
- p = self
493
- l2 = v.distance_squared(w)
494
- if l2 == 0:
495
- return p.distance(v)
496
- t = ((p.x - v.x) * (w.x - v.x) + (p.y - v.y) * (w.y - v.y)) / l2
497
- t = max(0, min(1, t))
498
- x = v.x + t * (w.x - v.x)
499
- y = v.y + t * (w.y - v.y)
500
- q = Point(x, y)
501
- return p.distance(q)
502
-
503
- Triangle = namedtuple('Triangle',
504
- ['s1', 's2', 't1', 't2', 'vmax', 'p1', 'p2', 'p3'])
505
-
506
- def triangle(s, vi, vf, a, p1, p3):
507
- # compute a triangular profile: accelerating, decelerating
508
- s1 = (2 * a * s + vf * vf - vi * vi) / (4 * a)
509
- s2 = s - s1
510
- vmax = (vi * vi + 2 * a * s1) ** 0.5
511
- t1 = (vmax - vi) / a
512
- t2 = (vf - vmax) / -a
513
- p2 = p1.lerps(p3, s1)
514
- return Triangle(s1, s2, t1, t2, vmax, p1, p2, p3)
515
-
516
- Trapezoid = namedtuple('Trapezoid',
517
- ['s1', 's2', 's3', 't1', 't2', 't3', 'p1', 'p2', 'p3', 'p4'])
518
-
519
- def trapezoid(s, vi, vmax, vf, a, p1, p4):
520
- # compute a trapezoidal profile: accelerating, cruising, decelerating
521
- t1 = (vmax - vi) / a
522
- s1 = (vmax + vi) / 2 * t1
523
- t3 = (vf - vmax) / -a
524
- s3 = (vf + vmax) / 2 * t3
525
- s2 = s - s1 - s3
526
- t2 = s2 / vmax
527
- p2 = p1.lerps(p4, s1)
528
- p3 = p1.lerps(p4, s - s3)
529
- return Trapezoid(s1, s2, s3, t1, t2, t3, p1, p2, p3, p4)
530
-
531
- def corner_velocity(s1, s2, vmax, a, delta):
532
- """
533
- Compute a maximum velocity at the corner of two segments
534
- https://onehossshay.wordpress.com/2011/09/24/improving_grbl_cornering_algorithm/
535
- """
536
- cosine = -s1.vector.dot(s2.vector)
537
- if abs(cosine - 1) < EPS:
538
- return 0
539
- sine = sqrt((1 - cosine) / 2)
540
- if abs(sine - 1) < EPS:
541
- return vmax
542
- v = sqrt((a * delta * sine) / (1 - sine))
543
- return min(v, vmax)
544
-
545
-
546
- class Segment(object):
547
- """
548
- A segment is a line segment between two points, which will be broken
549
- up into blocks by the planner
550
- """
551
- def __init__(self, p1, p2):
552
- self.p1 = p1
553
- self.p2 = p2
554
- self.length = p1.distance(p2)
555
- self.vector = p2.sub(p1).normalize()
556
- self.max_entry_velocity = 0
557
- self.entry_velocity = 0
558
- self.blocks = []
559
-
560
- class Throttler(object):
561
- def __init__(self, points, vmax, dt, threshold):
562
- self.points = points
563
- self.vmax = vmax
564
- self.dt = dt
565
- self.threshold = threshold
566
- self.distances = []
567
- prev = points[0]
568
- d = 0
569
- for point in points:
570
- d += prev.distance(point)
571
- self.distances.append(d)
572
- prev = point
573
-
574
- def lookup(self, d):
575
- return bisect(self.distances, d) - 1
576
-
577
- def is_feasible(self, i0, v):
578
- d = v * self.dt
579
- x0 = self.distances[i0]
580
- x1 = x0 + d
581
- i1 = self.lookup(x1)
582
- if i0 == i1:
583
- return True
584
- p0 = self.points[i0]
585
- p10 = self.points[i1]
586
- try:
587
- p11 = self.points[i1+1]
588
- except IndexError:
589
- p11 = p10
590
- s = x1 - self.distances[i1]
591
- p1 = p10.lerps(p11, s)
592
- i = i0 + 1
593
- while i <= i1:
594
- p = self.points[i]
595
- if p.segment_distance(p0, p1) > self.threshold:
596
- return False
597
- i += 1
598
- return True
599
-
600
- def compute_max_velocity(self, index):
601
- if self.is_feasible(index, self.vmax):
602
- return self.vmax
603
- lo = 0
604
- hi = self.vmax
605
- for _ in range(16):
606
- v = (lo + hi) / 2
607
- if self.is_feasible(index, v):
608
- lo = v
609
- else:
610
- hi = v
611
- v = lo
612
- return v
613
-
614
- def compute_max_velocities(self):
615
- return [self.compute_max_velocity(i) for i in range(len(self.points))]
616
-
617
- def constant_acceleration_plan(points, a, vmax, cf):
618
- # make sure points are Point objects
619
- points = [Point(x, y) for x, y in points]
620
-
621
- # the throttler reduces speeds based on the discrete timeslicing nature of
622
- # the device
623
- # TODO: expose parameters
624
- throttler = Throttler(points, vmax, 0.02, 0.001)
625
- max_velocities = throttler.compute_max_velocities()
626
-
627
- # create segments for each consecutive pair of points
628
- segments = [Segment(p1, p2) for p1, p2 in zip(points, points[1:])]
629
-
630
- # compute a max_entry_velocity for each segment
631
- # based on the angle formed by the two segments at the vertex
632
- for v, s1, s2 in zip(max_velocities, segments, segments[1:]):
633
- s1.max_entry_velocity = min(s1.max_entry_velocity, v)
634
- s2.max_entry_velocity = corner_velocity(s1, s2, vmax, a, cf)
635
-
636
- # add a dummy segment at the end to force a final velocity of zero
637
- segments.append(Segment(points[-1], points[-1]))
638
-
639
- # loop over segments
640
- i = 0
641
- while i < len(segments) - 1:
642
- # pull out some variables
643
- segment = segments[i]
644
- next_segment = segments[i + 1]
645
- s = segment.length
646
- vi = segment.entry_velocity
647
- vexit = next_segment.max_entry_velocity
648
- p1 = segment.p1
649
- p2 = segment.p2
650
-
651
- # determine which profile to use for this segment
652
- m = triangle(s, vi, vexit, a, p1, p2)
653
- if m.s1 < -EPS:
654
- # too fast! update max_entry_velocity and backtrack
655
- segment.max_entry_velocity = sqrt(vexit * vexit + 2 * a * s)
656
- i -= 1
657
- elif m.s2 < 0:
658
- # accelerate
659
- vf = sqrt(vi * vi + 2 * a * s)
660
- t = (vf - vi) / a
661
- segment.blocks = [
662
- Block(a, t, vi, p1, p2),
663
- ]
664
- next_segment.entry_velocity = vf
665
- i += 1
666
- elif m.vmax > vmax:
667
- # accelerate, cruise, decelerate
668
- z = trapezoid(s, vi, vmax, vexit, a, p1, p2)
669
- segment.blocks = [
670
- Block(a, z.t1, vi, z.p1, z.p2),
671
- Block(0, z.t2, vmax, z.p2, z.p3),
672
- Block(-a, z.t3, vmax, z.p3, z.p4),
673
- ]
674
- next_segment.entry_velocity = vexit
675
- i += 1
676
- else:
677
- # accelerate, decelerate
678
- segment.blocks = [
679
- Block(a, m.t1, vi, m.p1, m.p2),
680
- Block(-a, m.t2, m.vmax, m.p2, m.p3),
681
- ]
682
- next_segment.entry_velocity = vexit
683
- i += 1
684
-
685
- # concatenate all of the blocks
686
- blocks = []
687
- for segment in segments:
688
- blocks.extend(segment.blocks)
689
-
690
- # filter out zero-duration blocks and return
691
- blocks = [b for b in blocks if b.t > EPS]
692
- return Plan(blocks)
693
-
694
-
695
- def find_port():
696
- for port in comports():
697
- if VID_PID in port[2]:
698
- return port[0]
699
- return None
700
-
701
- class Device(object):
702
- def __init__(self, **kwargs):
703
- self.steps_per_unit = STEPS_PER_INCH
704
- self.pen_up_position = PEN_UP_POSITION
705
- self.pen_up_speed = PEN_UP_SPEED
706
- self.pen_up_delay = PEN_UP_DELAY
707
- self.pen_down_position = PEN_DOWN_POSITION
708
- self.pen_down_speed = PEN_DOWN_SPEED
709
- self.pen_down_delay = PEN_DOWN_DELAY
710
- self.acceleration = ACCELERATION
711
- self.max_velocity = MAX_VELOCITY
712
- self.corner_factor = CORNER_FACTOR
713
- self.jog_acceleration = JOG_ACCELERATION
714
- self.jog_max_velocity = JOG_MAX_VELOCITY
715
-
716
- for k, v in kwargs.items():
717
- setattr(self, k, v)
718
-
719
- self.error = (0, 0) # accumulated step error
720
-
721
- port = find_port()
722
- if port is None:
723
- raise Exception('cannot find axidraw device')
724
- self.serial = Serial(port, timeout=1)
725
- self.configure()
726
-
727
- def configure(self):
728
- servo_min = 7500
729
- servo_max = 28000
730
- pen_up_position = self.pen_up_position / 100
731
- pen_up_position = int(
732
- servo_min + (servo_max - servo_min) * pen_up_position)
733
- pen_down_position = self.pen_down_position / 100
734
- pen_down_position = int(
735
- servo_min + (servo_max - servo_min) * pen_down_position)
736
- self.command('SC', 4, pen_up_position)
737
- self.command('SC', 5, pen_down_position)
738
- self.command('SC', 11, int(self.pen_up_speed * 5))
739
- self.command('SC', 12, int(self.pen_down_speed * 5))
740
-
741
- def close(self):
742
- self.serial.close()
743
-
744
- def make_planner(self, jog=False):
745
- a = self.acceleration
746
- vmax = self.max_velocity
747
- cf = self.corner_factor
748
- if jog:
749
- a = self.jog_acceleration
750
- vmax = self.jog_max_velocity
751
- return Planner(a, vmax, cf)
752
-
753
- def readline(self):
754
- return self.serial.readline().decode('utf-8').strip()
755
-
756
- def command(self, *args):
757
- line = ','.join(map(str, args))
758
- self.serial.write((line + '\r').encode('utf-8'))
759
- return self.readline()
760
-
761
- # higher level functions
762
- def move(self, dx, dy):
763
- self.run_path([(0, 0), (dx, dy)])
764
-
765
- def goto(self, x, y, jog=True):
766
- # TODO: jog if pen up
767
- px, py = self.read_position()
768
- self.run_path([(px, py), (x, y)], jog)
769
-
770
- def home(self):
771
- self.goto(0, 0, True)
772
-
773
- def enable_motors(self):
774
- m = MICROSTEPPING_MODE
775
- return self.command('EM', m, m)
776
-
777
- def disable_motors(self):
778
- return self.command('EM', 0, 0)
779
-
780
- def motor_status(self):
781
- return self.command('QM')
782
-
783
-
784
-
785
- def read_position(self):
786
- response = self.command('QS')
787
- self.readline()
788
- a, b = map(int, response.split(','))
789
- a /= self.steps_per_unit
790
- b /= self.steps_per_unit
791
- y = (a - b) / 2
792
- x = y + b
793
- return x, y
794
-
795
- def stepper_move(self, duration, a, b):
796
- return self.command('XM', duration, a, b)
797
-
798
-
799
- def run_plan(self, plan):
800
- step_ms = TIMESLICE_MS
801
- step_s = step_ms / 1000
802
- t = 0
803
- while t < plan.t:
804
- i1 = plan.instant(t)
805
- i2 = plan.instant(t + step_s)
806
- d = i2.p.sub(i1.p)
807
- ex, ey = self.error
808
- ex, sx = modf(d.x * self.steps_per_unit + ex)
809
- ey, sy = modf(d.y * self.steps_per_unit + ey)
810
- self.error = ex, ey
811
- self.stepper_move(step_ms, int(sx), int(sy))
812
- t += step_s
813
- # self.wait()
814
-
815
- def run_path(self, path, jog=False):
816
- planner = self.make_planner(jog)
817
- plan = planner.plan(path)
818
- self.run_plan(plan)
819
-
820
-
821
- # pen functions
822
- def pen_up(self):
823
- delta = abs(self.pen_up_position - self.pen_down_position)
824
- duration = int(1000 * delta / self.pen_up_speed)
825
- delay = max(0, duration + self.pen_up_delay)
826
- return self.command('SP', 1, delay)
827
-
828
- def pen_down(self):
829
- delta = abs(self.pen_up_position - self.pen_down_position)
830
- duration = int(1000 * delta / self.pen_down_speed)
831
- delay = max(0, duration + self.pen_down_delay)
832
- return self.command('SP', 0, delay)
833
-
834
-
835
- import os
836
- import sys
837
- import time
838
- import gpiozero # pylint: disable=import-error
839
- from xmlrpc.server import SimpleXMLRPCServer
840
- import yaml
841
-
842
-
843
- sys.path.append(os.path.dirname(__file__))
844
- import axi
845
-
846
- class AxiDrawError(Exception):
847
- """Custom exception for AxiDraw-related errors."""
848
- pass
849
-
850
-
851
- class AxiDrawServer:
852
-
853
- def __init__(self, x_pin, y_pin):
854
- self.axi = None
855
- self.limit_pin_x = x_pin
856
- self.limit_pin_y = y_pin
857
-
858
- self.limit_switch_x = gpiozero.Button(self.limit_pin_x, pull_up=False)
859
- self.limit_switch_y = gpiozero.Button(self.limit_pin_y, pull_up=False)
860
-
861
- def _check_connection(self):
862
- if self.axi:
863
- return True
864
- raise AxiDrawError("AxiDraw not connected")
865
-
866
- def _is_triggered(self, axis: str):
867
- """
868
- Check if limit switch is triggered for a given axis (active HIGH).
869
- axis: "x" or "y"
870
- """
871
- if axis.lower() == "x":
872
- state = self.limit_switch_x.is_pressed
873
- elif axis.lower() == "y":
874
- state = self.limit_switch_y.is_pressed
875
- else:
876
- raise ValueError("Invalid axis. Use 'x' or 'y'.")
877
-
878
- print(f"[DEBUG] {axis.upper()} limit switch state: {state}")
879
- return state
880
-
881
-
882
- def connect(self, acceleration=100,
883
- pen_up=60, pen_down=30,
884
- speed_penup=150, speed_pendown=150):
885
- try:
886
- self.axi = axi.Device(
887
- pen_up_position=pen_up,
888
- pen_down_position=pen_down,
889
- acceleration=acceleration,
890
- pen_up_speed=speed_penup,
891
- pen_down_speed=speed_pendown
892
- )
893
- return True, "Connected"
894
- except Exception as error:
895
- raise AxiDrawError(f"Unable to connect to AxiDraw: {error}")
896
-
897
- def disconnect(self):
898
- if not self.axi:
899
- return True, "Already disconnected"
900
- try:
901
- self.axi.disable_motors()
902
- self.axi.close()
903
- self.axi = None
904
- return True, "Disconnected"
905
- except Exception as e:
906
- return False, f"Error during disconnect: {str(e)}"
907
-
908
- def set_pen_height(self, pen_up: int, pen_down: int):
909
- self._check_connection()
910
- self.axi.pen_up_position = pen_up
911
- self.axi.pen_down_position = pen_down
912
- if self.axi.configure():
913
- return True, "Pen height adjusted"
914
- return False, "Failed to adjust pen height"
915
-
916
- def set_speed(self, acceleration: int, penup_speed: int, pendown_speed: int):
917
- self._check_connection()
918
- self.axi.pen_up_speed = penup_speed
919
- self.axi.pen_down_speed = pendown_speed
920
- self.axi.acceleration = acceleration
921
- if self.axi.configure():
922
- return True, "Speed adjusted"
923
- return False, "Failed to set speed"
924
-
925
- def tap(self, x, y, tap_time=0.2):
926
- self._check_connection()
927
- try:
928
- self.axi.goto(x, y)
929
- self.axi.pen_down()
930
- time.sleep(tap_time)
931
- self.axi.pen_up()
932
- return True, "Tap succeeded"
933
- except Exception as error:
934
- return False, f"Tap failed: {error}"
935
-
936
- def long_press(self, x, y, hold_time=2.0):
937
- self._check_connection()
938
- try:
939
- self.axi.goto(x, y)
940
- self.axi.pen_down()
941
- time.sleep(hold_time)
942
- self.axi.pen_up()
943
- return True, "Long press succeeded"
944
- except Exception as error:
945
- return False, f"Long press failed: {error}"
946
-
947
- def swipe(self, startx, starty, endx, endy, swipe_time=0.5):
948
- self._check_connection()
949
- try:
950
- self.axi.goto(startx, starty)
951
- self.axi.pen_down()
952
- self.axi.goto(endx, endy)
953
- # time.sleep(swipe_time)
954
- self.axi.pen_up() # Need to check the swipe flow.
955
- return True, "Swipe succeeded"
956
- except Exception as error:
957
- return False, f"Swipe failed: {error}"
958
-
959
- def home(self):
960
- self._check_connection()
961
- try:
962
- self.axi.goto(0, 0)
963
- return True, "Homed to (0,0)"
964
- except Exception as e:
965
- return False, f"Home failed: {e}"
966
-
967
- def home_with_switch(self, axis: str):
968
- """Homing routine using X or Y axis limit switch."""
969
- self._check_connection()
970
- print(f"Starting {axis.upper()}-axis homing with switch...")
971
-
972
- if axis.lower() == "x":
973
- # Step 1: Coarse move
974
- while not self._is_triggered("x"):
975
- self.axi.move(-0.4, 0)
976
- print("Moved X -0.4")
977
- time.sleep(0.1)
978
- print("X switch hit (coarse).")
979
-
980
- # Step 2: Back off
981
- self.axi.move(0.5, 0)
982
- time.sleep(0.2)
983
-
984
- # Step 3: Fine approach
985
- while not self._is_triggered("x"):
986
- self.axi.move(-0.1, 0)
987
- print("Moved X -0.1")
988
- time.sleep(0.05)
989
- print("X precise home reached.")
990
- return True, "X-axis homed"
991
-
992
- elif axis.lower() == "y":
993
- # Step 1: Coarse move
994
- while not self._is_triggered("y"):
995
- self.axi.move(0, -0.4)
996
- print("Moved Y -0.4")
997
- time.sleep(0.1)
998
- print("Y switch hit (coarse).")
999
-
1000
- # Step 2: Back off
1001
- self.axi.move(0, 0.5)
1002
- time.sleep(0.2)
1003
-
1004
- # Step 3: Fine approach
1005
- while not self._is_triggered("y"):
1006
- self.axi.move(0, -0.1)
1007
- print("Moved Y -0.1")
1008
- time.sleep(0.05)
1009
- print("Y precise home reached.")
1010
- return True, "Y-axis homed"
1011
-
1012
- else:
1013
- return False, "Invalid axis: choose 'x' or 'y'"
1014
-
1015
- def calibrate_with_switch(self, axis: str):
1016
- """calibrating using X or Y axis limit switch."""
1017
- self._check_connection()
1018
- print(f"Starting {axis.upper()}-axis calibrating with switch...")
1019
- count = 0
1020
-
1021
- if axis.lower() == "x":
1022
- # Step 1: Coarse move
1023
- while not self._is_triggered("x"):
1024
- self.axi.move(-0.1, 0)
1025
- print("Moved X -0.1")
1026
- count = count - 0.1
1027
- time.sleep(0.1)
1028
- print("X switch hit (coarse).")
1029
-
1030
- # Step 2: Back off
1031
- self.axi.move(0.2, 0)
1032
- count = count + 0.2
1033
- time.sleep(0.2)
1034
-
1035
- # Step 3: Fine approach
1036
- while not self._is_triggered("x"):
1037
- self.axi.move(-0.025, 0)
1038
- print("Moved X -0.025")
1039
- count = count - 0.025
1040
- time.sleep(0.05)
1041
- print("X precise home reached.")
1042
- return count
1043
-
1044
- elif axis.lower() == "y":
1045
- # Step 1: Coarse move
1046
- while not self._is_triggered("y"):
1047
- self.axi.move(0, -0.1)
1048
- print("Moved Y -0.1")
1049
- count = count - 0.1
1050
- time.sleep(0.1)
1051
- print("Y switch hit (coarse).")
1052
-
1053
- # Step 2: Back off
1054
- self.axi.move(0, 0.2)
1055
- count = count + 0.2
1056
- time.sleep(0.2)
1057
-
1058
- # Step 3: Fine approach
1059
- while not self._is_triggered("y"):
1060
- self.axi.move(0, -0.025)
1061
- print("Moved Y -0.025")
1062
- count = count - 0.025
1063
- time.sleep(0.05)
1064
- print("Y precise home reached.")
1065
- return count
1066
-
1067
- else:
1068
- return False, "Invalid axis: choose 'x' or 'y'"
1069
-
1070
- def home_with_limit_switch(self):
1071
- """
1072
- Homing routine using X or Y axis limit switch.
1073
- """
1074
- self._check_connection()
1075
- self.home_with_switch("x")
1076
- self.home_with_switch("y")
1077
-
1078
- def calibrate_with_limit_switch(self):
1079
- """
1080
- Calibrating using X or Y axis limit switch.
1081
- """
1082
- self._check_connection()
1083
- x_pos = self.calibrate_with_switch("x")
1084
- y_pos = self.calibrate_with_switch("y")
1085
- return x_pos, y_pos
1086
-
1087
- def penup(self):
1088
- self._check_connection()
1089
- self.axi.pen_up()
1090
- return True, "Pen up"
1091
-
1092
- def pendown(self):
1093
- self._check_connection()
1094
- self.axi.pen_down()
1095
- return True, "Pen down"
1096
-
1097
- def get_current_position(self):
1098
- self._check_connection()
1099
- return True, self.axi.read_position()
1100
-
1101
- def move(self, x, y):
1102
- self._check_connection()
1103
- self.axi.move(x, y)
1104
- return True, f"Moved by ({x}, {y})"
1105
-
1106
-
1107
- if __name__ == "__main__":
1108
-
1109
- try:
1110
- with open("/etc/rpi-axitouch/config.yaml") as config_file:
1111
- yaml_config: dict = yaml.safe_load(config_file)
1112
- config: dict = yaml_config.get("AxiDrawServer", {})
1113
- except FileNotFoundError:
1114
- print("Config file not found. Using defaults.")
1115
- config = {}
1116
-
1117
- host = config['host']
1118
- port = config['port']
1119
- limit_pin_x = config["limit_pin_x"]
1120
- limit_pin_y = config["limit_pin_y"]
1121
- auto_home = config.get("auto_home", True) # Default True to preserve existing behaviour
1122
-
1123
- server = SimpleXMLRPCServer((host, port), allow_none=True)
1124
- axidraw = AxiDrawServer(limit_pin_x, limit_pin_y)
1125
- server.register_instance(axidraw)
1126
-
1127
- print(f"AxiDraw Server started on {host}:{port}")
1128
-
1129
- try:
1130
- axidraw.connect()
1131
- print("Axi connected")
1132
-
1133
- if auto_home:
1134
- print("Auto-homing enabled — running homing sequence...")
1135
- axidraw.home_with_switch("y")
1136
- axidraw.home_with_switch("x")
1137
- print("Homing complete.")
1138
- else:
1139
- print("Auto-homing disabled — skipping homing sequence.")
1140
-
1141
- server.serve_forever()
1142
- except KeyboardInterrupt:
1143
- print("Shutting down server...")
1144
- finally:
1145
- axidraw.disconnect()
1146
- print("AxiDraw disconnected.")
1147
-