Skip to content

Commit 8488b31

Browse files
authored
Merge pull request #2283 from makermelissa/main
Added CLUE Robot Code from CircuitPython Day Build
2 parents f1c0dc2 + b57a905 commit 8488b31

File tree

2 files changed

+317
-0
lines changed

2 files changed

+317
-0
lines changed

CircuitPython_CLUEbot/code.py

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
# SPDX-FileCopyrightText: 2022 Melissa LeBlanc-Williams for Adafruit Industries
2+
#
3+
# SPDX-License-Identifier: MIT
4+
import board
5+
import neopixel
6+
from robot import Robot
7+
8+
# Ring:bit Pins
9+
UNDERLIGHT_PIXELS = board.D0
10+
LEFT_MOTOR = board.D1
11+
RIGHT_MOTOR = board.D2
12+
13+
# Set up the hardware
14+
underlight_neopixels = neopixel.NeoPixel(UNDERLIGHT_PIXELS, 2)
15+
robot = Robot(LEFT_MOTOR, RIGHT_MOTOR, underlight_neopixels)
16+
17+
while True:
18+
robot.wait_for_connection()
19+
while robot.is_connected():
20+
robot.check_for_packets()

CircuitPython_CLUEbot/robot.py

Lines changed: 297 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,297 @@
1+
# SPDX-FileCopyrightText: 2022 Melissa LeBlanc-Williams for Adafruit Industries
2+
#
3+
# SPDX-License-Identifier: MIT
4+
import time
5+
import pwmio
6+
import board
7+
import digitalio
8+
import displayio
9+
import vectorio
10+
import terminalio
11+
12+
import neopixel
13+
import adafruit_motor.servo
14+
from adafruit_ble import BLERadio
15+
from adafruit_ble.advertising.standard import ProvideServicesAdvertisement
16+
from adafruit_ble.services.nordic import UARTService
17+
from adafruit_bluefruit_connect.packet import Packet
18+
from adafruit_bluefruit_connect.color_packet import ColorPacket
19+
from adafruit_bluefruit_connect.button_packet import ButtonPacket
20+
from adafruit_display_text.label import Label
21+
22+
# Throttle Directions and Speeds
23+
FWD = 1.0
24+
REV = -1.0
25+
STOP = 0
26+
27+
# Custom Colors
28+
RED = (200, 0, 0)
29+
GREEN = (0, 200, 0)
30+
BLUE = (0, 0, 200)
31+
PURPLE = (120, 0, 160)
32+
YELLOW = (100, 100, 0)
33+
AQUA = (0, 100, 100)
34+
35+
class Robot:
36+
def __init__(self, left_pin, right_pin, underlight_neopixel):
37+
self.left_motor = self._init_motor(left_pin)
38+
self.right_motor = self._init_motor(right_pin)
39+
self._init_display()
40+
self._init_ble()
41+
self.under_pixels = underlight_neopixel
42+
self.neopixel = neopixel.NeoPixel(board.NEOPIXEL, 1)
43+
self.direction = STOP
44+
self.release_color = None
45+
self.headlights = digitalio.DigitalInOut(board.WHITE_LEDS)
46+
self.headlights.switch_to_output()
47+
self.set_underglow(PURPLE)
48+
self.set_speed(STOP)
49+
50+
@classmethod
51+
def _init_motor(cls, pin):
52+
pwm = pwmio.PWMOut(pin, frequency=50)
53+
return adafruit_motor.servo.ContinuousServo(pwm, min_pulse=600, max_pulse=2400)
54+
55+
@classmethod
56+
def _make_palette(cls, color):
57+
palette = displayio.Palette(1)
58+
palette[0] = color
59+
return palette
60+
61+
def _init_ble(self):
62+
self.ble = BLERadio()
63+
self.uart_service = UARTService()
64+
self.advertisement = ProvideServicesAdvertisement(self.uart_service)
65+
66+
def _init_display(self):
67+
self.display = board.DISPLAY
68+
self.display_group = displayio.Group()
69+
self.display.show(self.display_group)
70+
self.shape_color = 0
71+
self.bg_color = 0xFFFF00
72+
rect = vectorio.Rectangle(
73+
pixel_shader=self._make_palette(0xFFFF00),
74+
x=0, y=0,
75+
width=self.display.width,
76+
height=self.display.height)
77+
self.display_group.append(rect)
78+
79+
def wait_for_connection(self):
80+
self.set_status_led(BLUE)
81+
self.ble.start_advertising(self.advertisement)
82+
self._set_status_waiting()
83+
while not self.ble.connected:
84+
# Wait for a connection.
85+
pass
86+
self.ble.stop_advertising()
87+
self.set_status_led(GREEN)
88+
self.set_throttle(STOP)
89+
90+
def is_connected(self):
91+
return self.ble.connected
92+
93+
def set_underglow(self, color, save_release_color = False):
94+
if save_release_color:
95+
self.release_color = self.get_underglow()
96+
for index, _ in enumerate(self.under_pixels):
97+
self.under_pixels[index] = color
98+
99+
def get_underglow(self):
100+
# Set the 2 Neopixels on the underside fo the robot
101+
return self.under_pixels[0]
102+
103+
def set_status_led(self, color):
104+
# Set the status NeoPixel on the CLUE
105+
self.neopixel[0] = color
106+
107+
def toggle_headlights(self):
108+
self.headlights.value = not self.headlights.value
109+
110+
def _set_left_throttle(self, speed):
111+
self.left_motor.throttle = speed
112+
113+
def _set_right_throttle(self, speed):
114+
# Motor is rotated 180 degrees of the left, so we invert the throttle
115+
self.right_motor.throttle = -1 * speed
116+
117+
def rotate_right(self):
118+
self.release_color = self.get_underglow()
119+
self.set_underglow(YELLOW, True)
120+
if self.direction == STOP:
121+
self._set_status_rotate_cw()
122+
else:
123+
self._set_status_right()
124+
speed = FWD if self.direction == STOP else self.direction
125+
self._set_left_throttle(speed)
126+
self._set_right_throttle(STOP if self.direction != STOP else -1 * speed)
127+
128+
def rotate_left(self):
129+
self.release_color = self.get_underglow()
130+
self.set_underglow(YELLOW, True)
131+
if self.direction == STOP:
132+
self._set_status_rotate_ccw()
133+
else:
134+
self._set_status_left()
135+
speed = FWD if self.direction == STOP else self.direction
136+
self._set_left_throttle(STOP if self.direction != STOP else -1 * speed)
137+
self._set_right_throttle(speed)
138+
139+
def set_throttle(self, speed):
140+
if speed == STOP:
141+
self._set_status_stop()
142+
elif speed > STOP:
143+
self._set_status_forward()
144+
elif speed < STOP:
145+
self._set_status_reverse()
146+
self.set_speed(speed)
147+
148+
def set_speed(self, speed):
149+
self._set_left_throttle(speed)
150+
self._set_right_throttle(speed)
151+
self.direction = speed
152+
153+
def stop(self):
154+
# Temporarily grab the current color
155+
color = self.get_underglow()
156+
self.set_underglow(RED)
157+
self.set_throttle(STOP)
158+
time.sleep(0.5)
159+
self.set_underglow(color)
160+
161+
def check_for_packets(self):
162+
if self.uart_service.in_waiting:
163+
self._process_packet(Packet.from_stream(self.uart_service))
164+
165+
def _handle_color_packet(self, packet):
166+
# Change the color
167+
self.set_underglow(packet.color)
168+
169+
def _remove_shapes(self):
170+
while len(self.display_group) > 1:
171+
self.display_group.pop()
172+
173+
def _add_centered_rect(self, width, height, x_offset=0, y_offset=0, color=None):
174+
if color is None:
175+
color = self.shape_color
176+
rectangle = vectorio.Rectangle(
177+
pixel_shader=self._make_palette(color),
178+
width=width,
179+
height=height,
180+
x=(self.display.width//2 - width//2) + x_offset - 1,
181+
y=(self.display.height//2 - height//2) + y_offset - 1
182+
)
183+
self.display_group.append(rectangle)
184+
185+
def _add_centered_polygon(self, points, x_offset=0, y_offset=0, color=None):
186+
if color is None:
187+
color = self.shape_color
188+
# Figure out the shape dimensions by using min and max
189+
width = max(points, key=lambda item:item[0])[0] - min(points, key=lambda item:item[0])[0]
190+
height = max(points, key=lambda item:item[1])[1] - min(points, key=lambda item:item[1])[1]
191+
polygon = vectorio.Polygon(
192+
pixel_shader=self._make_palette(color),
193+
points=points,
194+
x=(self.display.width // 2 - width // 2) + x_offset - 1,
195+
y=(self.display.height // 2 - height // 2) + y_offset - 1
196+
)
197+
self.display_group.append(polygon)
198+
199+
def _add_centered_circle(self, radius, x_offset=0, y_offset=0, color=None):
200+
if color is None:
201+
color = self.shape_color
202+
circle = vectorio.Circle(
203+
pixel_shader=self._make_palette(color),
204+
radius=radius,
205+
x=(self.display.width // 2) + x_offset - 1,
206+
y=(self.display.height // 2) + y_offset - 1
207+
)
208+
self.display_group.append(circle)
209+
210+
def _set_status_waiting(self):
211+
self._remove_shapes()
212+
text_area = Label(
213+
terminalio.FONT,
214+
text="Waiting for\nconnection",
215+
color=self.shape_color,
216+
scale=3,
217+
anchor_point=(0.5, 0.5),
218+
anchored_position=(self.display.width // 2, self.display.height // 2)
219+
)
220+
self.display_group.append(text_area)
221+
222+
def _set_status_reverse(self):
223+
self._remove_shapes()
224+
self._add_centered_polygon([(40, 0), (60, 0), (100, 100), (0, 100)], 0, 0)
225+
self._add_centered_polygon([(0, 40), (100, 40), (50, 0)], 0, -40)
226+
227+
def _set_status_forward(self):
228+
self._remove_shapes()
229+
self._add_centered_polygon([(20, 0), (60, 0), (80, 100), (0, 100)])
230+
self._add_centered_polygon([(0, 0), (150, 0), (75, 50)], 0, 50)
231+
232+
def _set_status_right(self):
233+
self._remove_shapes()
234+
self._add_centered_rect(100, 40)
235+
self._add_centered_polygon([(50, 0), (50, 100), (0, 50)], -50, 0)
236+
237+
def _set_status_rotate_ccw(self):
238+
self._remove_shapes()
239+
self._add_centered_circle(80)
240+
self._add_centered_circle(50, 0, 0, self.bg_color)
241+
self._add_centered_rect(160, 60, 0, 0, self.bg_color)
242+
self._add_centered_polygon([(40, 0), (80, 40), (0, 40)], 60, 10)
243+
self._add_centered_polygon([(40, 40), (80, 0), (0, 0)], -60, -10)
244+
245+
def _set_status_left(self):
246+
self._remove_shapes()
247+
self._add_centered_rect(100, 40)
248+
self._add_centered_polygon([(0, 0), (0, 100), (50, 50)], 50)
249+
250+
def _set_status_rotate_cw(self):
251+
self._remove_shapes()
252+
self._add_centered_circle(80)
253+
self._add_centered_circle(50, 0, 0, self.bg_color)
254+
self._add_centered_rect(160, 60, 0, 0, self.bg_color)
255+
self._add_centered_polygon([(40, 0), (80, 40), (0, 40)], -60, 10)
256+
self._add_centered_polygon([(40, 40), (80, 0), (0, 0)], 60, -10)
257+
258+
def _set_status_stop(self):
259+
self._remove_shapes()
260+
self._add_centered_rect(100, 100)
261+
262+
def _handle_button_press_packet(self, packet):
263+
if packet.button == ButtonPacket.UP: # UP button pressed
264+
self.set_throttle(FWD)
265+
elif packet.button == ButtonPacket.DOWN: # DOWN button
266+
self.set_throttle(REV)
267+
elif packet.button == ButtonPacket.RIGHT:
268+
self.rotate_right()
269+
elif packet.button == ButtonPacket.LEFT:
270+
self.rotate_left()
271+
elif packet.button == ButtonPacket.BUTTON_1:
272+
self.stop()
273+
elif packet.button == ButtonPacket.BUTTON_2:
274+
self.set_underglow(GREEN)
275+
elif packet.button == ButtonPacket.BUTTON_3:
276+
self.set_underglow(BLUE)
277+
elif packet.button == ButtonPacket.BUTTON_4:
278+
self.toggle_headlights()
279+
280+
def _handle_button_release_packet(self, packet):
281+
if self.release_color is not None:
282+
self.set_underglow(self.release_color)
283+
self.release_color = None
284+
if packet.button == ButtonPacket.RIGHT:
285+
self.set_throttle(self.direction)
286+
if packet.button == ButtonPacket.LEFT:
287+
self.set_throttle(self.direction)
288+
289+
def _process_packet(self, packet):
290+
if isinstance(packet, ColorPacket):
291+
self._handle_color_packet(packet)
292+
elif isinstance(packet, ButtonPacket) and packet.pressed:
293+
# do this when buttons are pressed
294+
self._handle_button_press_packet(packet)
295+
elif isinstance(packet, ButtonPacket) and not packet.pressed:
296+
# do this when some buttons are released
297+
self._handle_button_release_packet(packet)

0 commit comments

Comments
 (0)