Skip to content

Commit 2d9c25f

Browse files
committed
- Split Plotter into common and hardware-specific.
- Implemented PCA9685 mode
1 parent 1e8e2e7 commit 2d9c25f

10 files changed

Lines changed: 821 additions & 647 deletions

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ var/
1818
*.egg-info/
1919
.installed.cfg
2020
*.egg
21+
.._.DS_Store
2122
.DS_Store
2223
.ipynb_checkpoints
2324

README.rst

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,3 +20,37 @@ Documentation
2020
-------------
2121

2222
`Complete documentation for the project, with detailed instructions on how to build and use it <https://www.brachiograph.art/>`_
23+
24+
Adafruit PCA9685
25+
----------------
26+
27+
Support for `Adafruit PCA9685 servo controller <https://learn.adafruit.com/16-channel-pwm-servo-driver?view=all>`_ has been implemented.
28+
29+
A PCA9685 based Brachiograph can be instantiated this way::
30+
31+
from brachiograph import BrachioGraph
32+
bg = BrachioGraph(mode="PCA9685")
33+
34+
Github: https://github.com/adafruit/Adafruit_CircuitPython_PCA9685
35+
36+
Servos
37+
------
38+
39+
The Adafruit PCA9685 mod has been done primarily as an attempt to fix jitter in what apparently are 'bad' servos.
40+
After adapting the code it turned out that such servos would jitter using the PCA9685 controller anyways.
41+
Specifically, the issue was that each movement of an arm servo would cause the arm to shake uncontrollably, until stopped by hand.
42+
43+
Using 'good' servos worked, however they haven't been tested without the controller.
44+
45+
**Bad servos**
46+
47+
- 9g Microservo SG90:
48+
49+
- Robbe RoVoR S0009 Micro-Servo
50+
- Velleman Servo VMA600
51+
52+
**Good servos**
53+
54+
- 12g ES08MA:
55+
56+
- ZHITING

brachiograph.py

Lines changed: 43 additions & 205 deletions
Original file line numberDiff line numberDiff line change
@@ -1,224 +1,62 @@
11
# 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():
123
"""A shoulder-and-elbow drawing robot class."""
134

145
def __init__(
156
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
2215
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
3126
hysteresis_correction_1: int = 0, # hardware error compensation
3227
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-
):
4628

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)
5442

55-
super().__init__(
43+
self.plotter = Plotter(
5644
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,
6348
hysteresis_correction_1=hysteresis_correction_1,
6449
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,
7152
wait=wait,
7253
resolution=resolution,
73-
virtual=virtual,
7454
turtle=turtle,
7555
turtle_coarseness=turtle_coarseness,
56+
inner_arm=inner_arm,
57+
outer_arm=outer_arm,
58+
**kwargs
7659
)
7760

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)

linedraw.py

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
import argparse
77
import json
88
import time
9+
import os
910

1011
from PIL import Image, ImageDraw, ImageOps
1112

@@ -35,6 +36,7 @@ def image_to_json(
3536
repeat_contours=1,
3637
draw_hatch=False,
3738
repeat_hatch=1,
39+
output_filename_append=""
3840
):
3941

4042
lines = vectorise(
@@ -44,9 +46,10 @@ def image_to_json(
4446
repeat_contours,
4547
draw_hatch,
4648
repeat_hatch,
49+
output_filename_append
4750
)
4851

49-
filename = json_folder + image_filename + ".json"
52+
filename = json_folder + os.path.basename(image_filename) + output_filename_append + ".json"
5053
lines_to_file(lines, filename)
5154

5255

@@ -110,6 +113,7 @@ def vectorise(
110113
repeat_contours=1,
111114
draw_hatch=False,
112115
repeat_hatch=1,
116+
output_filename_append=""
113117
):
114118

115119
image = None
@@ -173,7 +177,7 @@ def vectorise(
173177
segments = segments + len(line) - 1
174178
print(len(lines), "lines,", segments, "segments.")
175179

176-
f = open(svg_folder + image_filename + ".svg", "w")
180+
f = open(svg_folder + os.path.basename(image_filename) + output_filename_append + ".svg", "w")
177181
f.write(makesvg(lines))
178182
f.close()
179183

0 commit comments

Comments
 (0)