To "clean up" the Learn System repo, we need to move groups of guides into subdirectories. This PR duplicates the CLUE guides into the CLUE subdirectory so guides may be changed prior to deleting redundant project repos to make more space.
297 lines
10 KiB
Python
297 lines
10 KiB
Python
# SPDX-FileCopyrightText: 2022 Melissa LeBlanc-Williams for Adafruit Industries
|
|
#
|
|
# SPDX-License-Identifier: MIT
|
|
import time
|
|
import pwmio
|
|
import board
|
|
import digitalio
|
|
import displayio
|
|
import vectorio
|
|
import terminalio
|
|
|
|
import neopixel
|
|
import adafruit_motor.servo
|
|
from adafruit_ble import BLERadio
|
|
from adafruit_ble.advertising.standard import ProvideServicesAdvertisement
|
|
from adafruit_ble.services.nordic import UARTService
|
|
from adafruit_bluefruit_connect.packet import Packet
|
|
from adafruit_bluefruit_connect.color_packet import ColorPacket
|
|
from adafruit_bluefruit_connect.button_packet import ButtonPacket
|
|
from adafruit_display_text.label import Label
|
|
|
|
# Throttle Directions and Speeds
|
|
FWD = 1.0
|
|
REV = -1.0
|
|
STOP = 0
|
|
|
|
# Custom Colors
|
|
RED = (200, 0, 0)
|
|
GREEN = (0, 200, 0)
|
|
BLUE = (0, 0, 200)
|
|
PURPLE = (120, 0, 160)
|
|
YELLOW = (100, 100, 0)
|
|
AQUA = (0, 100, 100)
|
|
|
|
class Robot:
|
|
def __init__(self, left_pin, right_pin, underlight_neopixel):
|
|
self.left_motor = self._init_motor(left_pin)
|
|
self.right_motor = self._init_motor(right_pin)
|
|
self._init_display()
|
|
self._init_ble()
|
|
self.under_pixels = underlight_neopixel
|
|
self.neopixel = neopixel.NeoPixel(board.NEOPIXEL, 1)
|
|
self.direction = STOP
|
|
self.release_color = None
|
|
self.headlights = digitalio.DigitalInOut(board.WHITE_LEDS)
|
|
self.headlights.switch_to_output()
|
|
self.set_underglow(PURPLE)
|
|
self.set_speed(STOP)
|
|
|
|
@classmethod
|
|
def _make_palette(cls, color):
|
|
palette = displayio.Palette(1)
|
|
palette[0] = color
|
|
return palette
|
|
|
|
@classmethod
|
|
def _init_motor(cls, pin):
|
|
pwm = pwmio.PWMOut(pin, frequency=50)
|
|
return adafruit_motor.servo.ContinuousServo(pwm, min_pulse=600, max_pulse=2400)
|
|
|
|
def _init_ble(self):
|
|
self.ble = BLERadio()
|
|
self.uart_service = UARTService()
|
|
self.advertisement = ProvideServicesAdvertisement(self.uart_service)
|
|
|
|
def _init_display(self):
|
|
self.display = board.DISPLAY
|
|
self.display_group = displayio.Group()
|
|
self.display.root_group = self.display_group
|
|
self.shape_color = 0
|
|
self.bg_color = 0xFFFF00
|
|
rect = vectorio.Rectangle(
|
|
pixel_shader=self._make_palette(0xFFFF00),
|
|
x=0, y=0,
|
|
width=self.display.width,
|
|
height=self.display.height)
|
|
self.display_group.append(rect)
|
|
|
|
def wait_for_connection(self):
|
|
self.set_status_led(BLUE)
|
|
self.ble.start_advertising(self.advertisement)
|
|
self._set_status_waiting()
|
|
while not self.ble.connected:
|
|
# Wait for a connection.
|
|
pass
|
|
self.ble.stop_advertising()
|
|
self.set_status_led(GREEN)
|
|
self.set_throttle(STOP)
|
|
|
|
def is_connected(self):
|
|
return self.ble.connected
|
|
|
|
def check_for_packets(self):
|
|
if self.uart_service.in_waiting:
|
|
self._process_packet(Packet.from_stream(self.uart_service))
|
|
|
|
def set_underglow(self, color, save_release_color = False):
|
|
if save_release_color:
|
|
self.release_color = self.get_underglow()
|
|
for index, _ in enumerate(self.under_pixels):
|
|
self.under_pixels[index] = color
|
|
|
|
def get_underglow(self):
|
|
# Set the 2 Neopixels on the underside fo the robot
|
|
return self.under_pixels[0]
|
|
|
|
def set_status_led(self, color):
|
|
# Set the status NeoPixel on the CLUE
|
|
self.neopixel[0] = color
|
|
|
|
def toggle_headlights(self):
|
|
self.headlights.value = not self.headlights.value
|
|
|
|
def _set_left_throttle(self, speed):
|
|
self.left_motor.throttle = speed
|
|
|
|
def _set_right_throttle(self, speed):
|
|
# Motor is rotated 180 degrees of the left, so we invert the throttle
|
|
self.right_motor.throttle = -1 * speed
|
|
|
|
def rotate_right(self):
|
|
self.release_color = self.get_underglow()
|
|
self.set_underglow(YELLOW, True)
|
|
if self.direction == STOP:
|
|
self._set_status_rotate_cw()
|
|
else:
|
|
self._set_status_right()
|
|
speed = FWD if self.direction == STOP else self.direction
|
|
self._set_left_throttle(speed)
|
|
self._set_right_throttle(STOP if self.direction != STOP else -1 * speed)
|
|
|
|
def rotate_left(self):
|
|
self.release_color = self.get_underglow()
|
|
self.set_underglow(YELLOW, True)
|
|
if self.direction == STOP:
|
|
self._set_status_rotate_ccw()
|
|
else:
|
|
self._set_status_left()
|
|
speed = FWD if self.direction == STOP else self.direction
|
|
self._set_left_throttle(STOP if self.direction != STOP else -1 * speed)
|
|
self._set_right_throttle(speed)
|
|
|
|
def set_throttle(self, speed):
|
|
if speed == STOP:
|
|
self._set_status_stop()
|
|
elif speed > STOP:
|
|
self._set_status_forward()
|
|
elif speed < STOP:
|
|
self._set_status_reverse()
|
|
self.set_speed(speed)
|
|
|
|
def set_speed(self, speed):
|
|
self._set_left_throttle(speed)
|
|
self._set_right_throttle(speed)
|
|
self.direction = speed
|
|
|
|
def stop(self):
|
|
# Temporarily grab the current color
|
|
color = self.get_underglow()
|
|
self.set_underglow(RED)
|
|
self.set_throttle(STOP)
|
|
time.sleep(0.5)
|
|
self.set_underglow(color)
|
|
|
|
def _remove_shapes(self):
|
|
while len(self.display_group) > 1:
|
|
self.display_group.pop()
|
|
|
|
def _add_centered_rect(self, width, height, x_offset=0, y_offset=0, color=None):
|
|
if color is None:
|
|
color = self.shape_color
|
|
rectangle = vectorio.Rectangle(
|
|
pixel_shader=self._make_palette(color),
|
|
width=width,
|
|
height=height,
|
|
x=(self.display.width//2 - width//2) + x_offset - 1,
|
|
y=(self.display.height//2 - height//2) + y_offset - 1
|
|
)
|
|
self.display_group.append(rectangle)
|
|
|
|
def _add_centered_polygon(self, points, x_offset=0, y_offset=0, color=None):
|
|
if color is None:
|
|
color = self.shape_color
|
|
# Figure out the shape dimensions by using min and max
|
|
width = max(points, key=lambda item:item[0])[0] - min(points, key=lambda item:item[0])[0]
|
|
height = max(points, key=lambda item:item[1])[1] - min(points, key=lambda item:item[1])[1]
|
|
polygon = vectorio.Polygon(
|
|
pixel_shader=self._make_palette(color),
|
|
points=points,
|
|
x=(self.display.width // 2 - width // 2) + x_offset - 1,
|
|
y=(self.display.height // 2 - height // 2) + y_offset - 1
|
|
)
|
|
self.display_group.append(polygon)
|
|
|
|
def _add_centered_circle(self, radius, x_offset=0, y_offset=0, color=None):
|
|
if color is None:
|
|
color = self.shape_color
|
|
circle = vectorio.Circle(
|
|
pixel_shader=self._make_palette(color),
|
|
radius=radius,
|
|
x=(self.display.width // 2) + x_offset - 1,
|
|
y=(self.display.height // 2) + y_offset - 1
|
|
)
|
|
self.display_group.append(circle)
|
|
|
|
def _set_status_waiting(self):
|
|
self._remove_shapes()
|
|
text_area = Label(
|
|
terminalio.FONT,
|
|
text="Waiting for\nconnection",
|
|
color=self.shape_color,
|
|
scale=3,
|
|
anchor_point=(0.5, 0.5),
|
|
anchored_position=(self.display.width // 2, self.display.height // 2)
|
|
)
|
|
self.display_group.append(text_area)
|
|
|
|
def _set_status_reverse(self):
|
|
self._remove_shapes()
|
|
self._add_centered_polygon([(40, 0), (60, 0), (100, 100), (0, 100)], 0, 0)
|
|
self._add_centered_polygon([(0, 40), (100, 40), (50, 0)], 0, -40)
|
|
|
|
def _set_status_forward(self):
|
|
self._remove_shapes()
|
|
self._add_centered_polygon([(20, 0), (60, 0), (80, 100), (0, 100)])
|
|
self._add_centered_polygon([(0, 0), (150, 0), (75, 50)], 0, 50)
|
|
|
|
def _set_status_right(self):
|
|
self._remove_shapes()
|
|
self._add_centered_rect(100, 40)
|
|
self._add_centered_polygon([(50, 0), (50, 100), (0, 50)], -50, 0)
|
|
|
|
def _set_status_rotate_ccw(self):
|
|
self._remove_shapes()
|
|
self._add_centered_circle(80)
|
|
self._add_centered_circle(50, 0, 0, self.bg_color)
|
|
self._add_centered_rect(160, 60, 0, 0, self.bg_color)
|
|
self._add_centered_polygon([(40, 0), (80, 40), (0, 40)], 60, 10)
|
|
self._add_centered_polygon([(40, 40), (80, 0), (0, 0)], -60, -10)
|
|
|
|
def _set_status_left(self):
|
|
self._remove_shapes()
|
|
self._add_centered_rect(100, 40)
|
|
self._add_centered_polygon([(0, 0), (0, 100), (50, 50)], 50)
|
|
|
|
def _set_status_rotate_cw(self):
|
|
self._remove_shapes()
|
|
self._add_centered_circle(80)
|
|
self._add_centered_circle(50, 0, 0, self.bg_color)
|
|
self._add_centered_rect(160, 60, 0, 0, self.bg_color)
|
|
self._add_centered_polygon([(40, 0), (80, 40), (0, 40)], -60, 10)
|
|
self._add_centered_polygon([(40, 40), (80, 0), (0, 0)], 60, -10)
|
|
|
|
def _set_status_stop(self):
|
|
self._remove_shapes()
|
|
self._add_centered_rect(100, 100)
|
|
|
|
def _process_packet(self, packet):
|
|
if isinstance(packet, ColorPacket):
|
|
self._handle_color_packet(packet)
|
|
elif isinstance(packet, ButtonPacket) and packet.pressed:
|
|
# do this when buttons are pressed
|
|
self._handle_button_press_packet(packet)
|
|
elif isinstance(packet, ButtonPacket) and not packet.pressed:
|
|
# do this when some buttons are released
|
|
self._handle_button_release_packet(packet)
|
|
|
|
def _handle_color_packet(self, packet):
|
|
# Change the color
|
|
self.set_underglow(packet.color)
|
|
|
|
def _handle_button_press_packet(self, packet):
|
|
if packet.button == ButtonPacket.UP: # UP button pressed
|
|
self.set_throttle(FWD)
|
|
elif packet.button == ButtonPacket.DOWN: # DOWN button
|
|
self.set_throttle(REV)
|
|
elif packet.button == ButtonPacket.RIGHT:
|
|
self.rotate_right()
|
|
elif packet.button == ButtonPacket.LEFT:
|
|
self.rotate_left()
|
|
elif packet.button == ButtonPacket.BUTTON_1:
|
|
self.stop()
|
|
elif packet.button == ButtonPacket.BUTTON_2:
|
|
self.set_underglow(GREEN)
|
|
elif packet.button == ButtonPacket.BUTTON_3:
|
|
self.set_underglow(BLUE)
|
|
elif packet.button == ButtonPacket.BUTTON_4:
|
|
self.toggle_headlights()
|
|
|
|
def _handle_button_release_packet(self, packet):
|
|
if self.release_color is not None:
|
|
self.set_underglow(self.release_color)
|
|
self.release_color = None
|
|
if packet.button == ButtonPacket.RIGHT:
|
|
self.set_throttle(self.direction)
|
|
if packet.button == ButtonPacket.LEFT:
|
|
self.set_throttle(self.direction)
|