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.
- {robotframework_parrot_touchcontroller-1.1.0 → robotframework_parrot_touchcontroller-1.1.2}/PKG-INFO +1 -1
- {robotframework_parrot_touchcontroller-1.1.0 → robotframework_parrot_touchcontroller-1.1.2}/parrot/TouchController.py +7 -0
- {robotframework_parrot_touchcontroller-1.1.0 → robotframework_parrot_touchcontroller-1.1.2}/parrot/touch/axitouch.py +53 -17
- {robotframework_parrot_touchcontroller-1.1.0 → robotframework_parrot_touchcontroller-1.1.2}/pyproject.toml +1 -1
- {robotframework_parrot_touchcontroller-1.1.0 → robotframework_parrot_touchcontroller-1.1.2}/robotframework_parrot_touchcontroller.egg-info/PKG-INFO +1 -1
- {robotframework_parrot_touchcontroller-1.1.0 → robotframework_parrot_touchcontroller-1.1.2}/robotframework_parrot_touchcontroller.egg-info/SOURCES.txt +0 -1
- robotframework_parrot_touchcontroller-1.1.0/parrot/touch/axitouchserial.py +0 -1147
- {robotframework_parrot_touchcontroller-1.1.0 → robotframework_parrot_touchcontroller-1.1.2}/README.md +0 -0
- {robotframework_parrot_touchcontroller-1.1.0 → robotframework_parrot_touchcontroller-1.1.2}/parrot/touch/adbtouch.py +0 -0
- {robotframework_parrot_touchcontroller-1.1.0 → robotframework_parrot_touchcontroller-1.1.2}/parrot/touch/base.py +0 -0
- {robotframework_parrot_touchcontroller-1.1.0 → robotframework_parrot_touchcontroller-1.1.2}/parrot/touch/dummy.py +0 -0
- {robotframework_parrot_touchcontroller-1.1.0 → robotframework_parrot_touchcontroller-1.1.2}/parrot/touch/usbhidtouch.py +0 -0
- {robotframework_parrot_touchcontroller-1.1.0 → robotframework_parrot_touchcontroller-1.1.2}/robotframework_parrot_touchcontroller.egg-info/dependency_links.txt +0 -0
- {robotframework_parrot_touchcontroller-1.1.0 → robotframework_parrot_touchcontroller-1.1.2}/robotframework_parrot_touchcontroller.egg-info/requires.txt +0 -0
- {robotframework_parrot_touchcontroller-1.1.0 → robotframework_parrot_touchcontroller-1.1.2}/robotframework_parrot_touchcontroller.egg-info/top_level.txt +0 -0
- {robotframework_parrot_touchcontroller-1.1.0 → robotframework_parrot_touchcontroller-1.1.2}/setup.cfg +0 -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
|
-
|
|
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
|
-
|
|
|
117
|
-
|
|
118
|
-
| |
|
|
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
|
-
|
|
132
|
-
|
|
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
|
-
|
|
135
|
-
|
|
161
|
+
robot_x = sx + y * x_scale
|
|
162
|
+
robot_y = sy + x * y_scale
|
|
136
163
|
|
|
137
|
-
|
|
138
|
-
|
|
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
|
-
|
|
141
|
-
|
|
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
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
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.
|
|
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,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
|
-
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|