|
1 | 1 | # coding=utf-8 |
2 | | - |
3 | | -from time import sleep |
4 | | -import readchar |
5 | | -import math |
6 | | -import numpy |
7 | | -from turtle_plotter import BrachioGraphTurtle |
8 | | -from plotter import Plotter |
9 | | - |
10 | | - |
11 | | -class BrachioGraph(Plotter): |
| 2 | +class BrachioGraph(): |
12 | 3 | """A shoulder-and-elbow drawing robot class.""" |
13 | 4 |
|
14 | 5 | def __init__( |
15 | 6 | self, |
16 | | - virtual: bool = False, # a virtual plotter runs in software only |
17 | | - turtle: bool = False, # create a turtle graphics plotter |
18 | | - turtle_coarseness=None, # a factor in degrees representing servo resolution |
19 | | - # ----------------- geometry of the plotter ----------------- |
20 | | - bounds: tuple = [-8, 4, 6, 13], # the maximum rectangular drawing area |
21 | | - inner_arm: float = 8, # the lengths of the arms |
| 7 | + mode = "pigpio", # machine mode: [pigpio|PCA9685|virtual] |
| 8 | + turtle: bool = False, # create a turtle graphics plotter |
| 9 | + turtle_coarseness = None, # a factor in degrees representing servo resolution |
| 10 | + |
| 11 | + resolution: float = 1, # default resolution of the plotter in cm |
| 12 | + # ----------------- geometry of the plotter |
| 13 | + bounds: tuple = [-8, 4, 6, 13], # the maximum rectangular drawing area |
| 14 | + inner_arm: float = 8, # the lengths of the arms |
22 | 15 | outer_arm: float = 8, |
23 | | - # ----------------- naive calculation values ----------------- |
24 | | - servo_1_parked_pw: int = 1500, # pulse-widths when parked |
25 | | - servo_2_parked_pw: int = 1500, |
26 | | - servo_1_degree_ms: int = -10, # milliseconds pulse-width per degree |
27 | | - servo_2_degree_ms: int = 10, # reversed for the mounting of the shoulder servo |
28 | | - servo_1_parked_angle: int = -90, # the arm angle in the parked position |
29 | | - servo_2_parked_angle: int = 90, |
30 | | - # ----------------- hysteresis ----------------- |
| 16 | + |
| 17 | + # ----------------- servo calibration |
| 18 | + angle_offset_servo_shoulder: float = 30, # offset (in BG's reference model) |
| 19 | + # for the servo's zero degrees. |
| 20 | + angle_parked_servo_shoulder: float = -90, # the arm angle in the parked position |
| 21 | + angle_parked_servo_elbow: float = 90, |
| 22 | + angle_pen_up: float = 0, |
| 23 | + angle_pen_down: float = 30, |
| 24 | + |
| 25 | + # ----------------- hysteresis # TODO: implement for PCA9685 |
31 | 26 | hysteresis_correction_1: int = 0, # hardware error compensation |
32 | 27 | hysteresis_correction_2: int = 0, |
33 | | - # ----------------- servo angles and pulse-widths in lists ----------------- |
34 | | - servo_1_angle_pws: tuple = [], # pulse-widths for various angles |
35 | | - servo_2_angle_pws: tuple = [], |
36 | | - # ----------------- servo angles and pulse-widths in lists (bi-directional) ------ |
37 | | - servo_1_angle_pws_bidi: tuple = [], # bi-directional pulse-widths for various angles |
38 | | - servo_2_angle_pws_bidi: tuple = [], |
39 | | - # ----------------- the pen ----------------- |
40 | | - pw_up: int = 1500, # pulse-widths for pen up/down |
41 | | - pw_down: int = 1100, |
42 | | - # ----------------- physical control ----------------- |
43 | | - wait: float = None, # default wait time between operations |
44 | | - resolution: float = None, # default resolution of the plotter in cm |
45 | | - ): |
46 | 28 |
|
47 | | - # set the geometry |
48 | | - self.inner_arm = inner_arm |
49 | | - self.outer_arm = outer_arm |
50 | | - |
51 | | - # Set the x and y position state, so it knows its current x/y position. |
52 | | - self.x = -self.inner_arm |
53 | | - self.y = self.outer_arm |
| 29 | + # ----------------- movement speed |
| 30 | + wait: float = 0.05, # default wait time between operations |
| 31 | + **kwargs |
| 32 | + ): |
| 33 | + if mode == "PCA9685": |
| 34 | + from plotter_pca9685 import Plotter |
| 35 | + elif mode == "pigpio": |
| 36 | + from plotter_pigpio import Plotter |
| 37 | + elif mode == "virtual": |
| 38 | + from plotter_virtual import Plotter |
| 39 | + else: |
| 40 | + print("FATAL: mode {mode} not recognized.") |
| 41 | + exit(1) |
54 | 42 |
|
55 | | - super().__init__( |
| 43 | + self.plotter = Plotter( |
56 | 44 | bounds=bounds, |
57 | | - servo_1_parked_pw=servo_1_parked_pw, |
58 | | - servo_2_parked_pw=servo_1_parked_pw, |
59 | | - servo_1_degree_ms=servo_1_degree_ms, |
60 | | - servo_2_degree_ms=servo_2_degree_ms, |
61 | | - servo_1_parked_angle=servo_1_parked_angle, |
62 | | - servo_2_parked_angle=servo_2_parked_angle, |
| 45 | + angle_parked_servo_shoulder=angle_parked_servo_shoulder, |
| 46 | + angle_parked_servo_elbow=angle_parked_servo_elbow, |
| 47 | + angle_offset_servo_shoulder=angle_offset_servo_shoulder, |
63 | 48 | hysteresis_correction_1=hysteresis_correction_1, |
64 | 49 | hysteresis_correction_2=hysteresis_correction_2, |
65 | | - servo_1_angle_pws=servo_1_angle_pws, |
66 | | - servo_2_angle_pws=servo_2_angle_pws, |
67 | | - servo_1_angle_pws_bidi=servo_1_angle_pws_bidi, |
68 | | - servo_2_angle_pws_bidi=servo_2_angle_pws_bidi, |
69 | | - pw_up=pw_up, |
70 | | - pw_down=pw_down, |
| 50 | + angle_pen_up=angle_pen_up, |
| 51 | + angle_pen_down=angle_pen_down, |
71 | 52 | wait=wait, |
72 | 53 | resolution=resolution, |
73 | | - virtual=virtual, |
74 | 54 | turtle=turtle, |
75 | 55 | turtle_coarseness=turtle_coarseness, |
| 56 | + inner_arm=inner_arm, |
| 57 | + outer_arm=outer_arm, |
| 58 | + **kwargs |
76 | 59 | ) |
77 | 60 |
|
78 | | - def setup_turtle(self, coarseness): |
79 | | - self.turtle = BrachioGraphTurtle( |
80 | | - inner_arm=self.inner_arm, # the length of the inner arm (blue) |
81 | | - outer_arm=self.outer_arm, # the length of the outer arm (red) |
82 | | - shoulder_centre_angle=-90, # the starting angle of the inner arm, relative to straight ahead |
83 | | - shoulder_sweep=180, # the arc covered by the shoulder motor |
84 | | - elbow_centre_angle=90, # the centre of the outer arm relative to the inner arm |
85 | | - elbow_sweep=180, # the arc covered by the elbow motor |
86 | | - window_size=850, # width and height of the turtle canvas |
87 | | - speed=10, # how fast to draw |
88 | | - machine=self, |
89 | | - coarseness=coarseness, |
90 | | - ) |
91 | | - |
92 | | - self.turtle.draw_grid() |
93 | | - self.t = self.turtle |
94 | | - |
95 | | - def test_arcs(self): |
96 | | - self.park() |
97 | | - elbow_angle = 120 |
98 | | - self.move_angles(angle_2=elbow_angle) |
99 | | - |
100 | | - for angle_1 in range(-135, 15, 15): |
101 | | - self.move_angles(angle_1=angle_1, draw=True) |
102 | | - |
103 | | - for angle_2 in range(elbow_angle, elbow_angle + 16): |
104 | | - self.move_angles(angle_2=angle_2, draw=True) |
105 | | - for angle_2 in range(elbow_angle + 16, elbow_angle - 16, -1): |
106 | | - self.move_angles(angle_2=angle_2, draw=True) |
107 | | - for angle_2 in range(elbow_angle - 16, elbow_angle + 1): |
108 | | - self.move_angles(angle_2=angle_2, draw=True) |
109 | | - |
110 | | - # ----------------- trigonometric methods ----------------- |
111 | | - |
112 | | - def xy_to_angles(self, x=0, y=0): |
113 | | - """Return the servo angles required to reach any x/y position.""" |
114 | | - |
115 | | - hypotenuse = math.sqrt(x**2 + y**2) |
116 | | - |
117 | | - if hypotenuse > self.inner_arm + self.outer_arm: |
118 | | - raise Exception( |
119 | | - f"Cannot reach {hypotenuse}; total arm length is {self.inner_arm + self.outer_arm}" |
120 | | - ) |
121 | | - |
122 | | - hypotenuse_angle = math.asin(x / hypotenuse) |
123 | | - |
124 | | - inner_angle = math.acos( |
125 | | - (hypotenuse**2 + self.inner_arm**2 - self.outer_arm**2) |
126 | | - / (2 * hypotenuse * self.inner_arm) |
127 | | - ) |
128 | | - outer_angle = math.acos( |
129 | | - (self.inner_arm**2 + self.outer_arm**2 - hypotenuse**2) |
130 | | - / (2 * self.inner_arm * self.outer_arm) |
131 | | - ) |
132 | | - |
133 | | - shoulder_motor_angle = hypotenuse_angle - inner_angle |
134 | | - elbow_motor_angle = math.pi - outer_angle |
135 | | - |
136 | | - return (math.degrees(shoulder_motor_angle), math.degrees(elbow_motor_angle)) |
137 | | - |
138 | | - def angles_to_xy(self, shoulder_motor_angle, elbow_motor_angle): |
139 | | - """Return the x/y co-ordinates represented by a pair of servo angles.""" |
140 | | - |
141 | | - elbow_motor_angle = math.radians(elbow_motor_angle) |
142 | | - shoulder_motor_angle = math.radians(shoulder_motor_angle) |
143 | | - |
144 | | - hypotenuse = math.sqrt( |
145 | | - ( |
146 | | - self.inner_arm**2 |
147 | | - + self.outer_arm**2 |
148 | | - - 2 |
149 | | - * self.inner_arm |
150 | | - * self.outer_arm |
151 | | - * math.cos(math.pi - elbow_motor_angle) |
152 | | - ) |
153 | | - ) |
154 | | - base_angle = math.acos( |
155 | | - (hypotenuse**2 + self.inner_arm**2 - self.outer_arm**2) |
156 | | - / (2 * hypotenuse * self.inner_arm) |
157 | | - ) |
158 | | - inner_angle = base_angle + shoulder_motor_angle |
159 | | - |
160 | | - x = math.sin(inner_angle) * hypotenuse |
161 | | - y = math.cos(inner_angle) * hypotenuse |
162 | | - |
163 | | - return (x, y) |
164 | | - |
165 | | - # ----------------- reporting methods ----------------- |
166 | | - |
167 | | - def report(self): |
168 | | - |
169 | | - print(f" -----------------|-----------------") |
170 | | - print(f" Servo 1 | Servo 2 ") |
171 | | - print(f" -----------------|-----------------") |
172 | | - |
173 | | - h1, h2 = self.hysteresis_correction_1, self.hysteresis_correction_2 |
174 | | - print(f"hysteresis {h1:>2.1f} | {h2:>2.1f}") |
175 | | - |
176 | | - pw_1, pw_2 = self.get_pulse_widths() |
177 | | - print(f"pulse-width {pw_1:<4.0f} | {pw_2:<4.0f}") |
178 | | - |
179 | | - angle_1, angle_2 = self.angle_1, self.angle_2 |
180 | | - |
181 | | - if angle_1 and angle_2: |
182 | | - |
183 | | - print( |
184 | | - f" angle {angle_1:>4.0f} | {angle_2:>4.0f}" |
185 | | - ) |
186 | | - |
187 | | - print(f" -----------------|-----------------") |
188 | | - print(f" min max mid | min max mid") |
189 | | - print(f" -----------------|-----------------") |
190 | | - |
191 | | - if ( |
192 | | - self.angles_used_1 |
193 | | - and self.angles_used_2 |
194 | | - and self.pulse_widths_used_1 |
195 | | - and self.pulse_widths_used_2 |
196 | | - ): |
197 | | - |
198 | | - min1 = min(self.pulse_widths_used_1) |
199 | | - max1 = max(self.pulse_widths_used_1) |
200 | | - mid1 = (min1 + max1) / 2 |
201 | | - min2 = min(self.pulse_widths_used_2) |
202 | | - max2 = max(self.pulse_widths_used_2) |
203 | | - mid2 = (min2 + max2) / 2 |
204 | | - |
205 | | - print( |
206 | | - f"pulse-widths {min1:>4.0f} {max1:>4.0f} {mid1:>4.0f} | {min2:>4.0f} {max2:>4.0f} {mid2:>4.0f}" |
207 | | - ) |
208 | | - |
209 | | - min1 = min(self.angles_used_1) |
210 | | - max1 = max(self.angles_used_1) |
211 | | - mid1 = (min1 + max1) / 2 |
212 | | - min2 = min(self.angles_used_2) |
213 | | - max2 = max(self.angles_used_2) |
214 | | - mid2 = (min2 + max2) / 2 |
215 | | - |
216 | | - print( |
217 | | - f" angles {min1:>4.0f} {max1:>4.0f} {mid1:>4.0f} | {min2:>4.0f} {max2:>4.0f} {mid2:>4.0f}" |
218 | | - ) |
219 | | - |
220 | | - else: |
221 | | - |
222 | | - print( |
223 | | - "No data recorded yet. Try calling the BrachioGraph.box() method first." |
224 | | - ) |
| 61 | +if __name__=="__main__": |
| 62 | + bg = BrachioGraph(mode="virtual",turtle=True) |
0 commit comments