-
Notifications
You must be signed in to change notification settings - Fork 5
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
1 parent
5891632
commit e7138da
Showing
3 changed files
with
324 additions
and
1 deletion.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,154 @@ | ||
# SPDX-FileCopyrightText: 2023 Christopher Parrott for Pimoroni Ltd | ||
# | ||
# SPDX-License-Identifier: MIT | ||
|
||
from pimoroni_yukon.timing import ticks_diff, ticks_ms | ||
|
||
class JoyBTCommander(): | ||
STX = 0x02 | ||
ETX = 0x03 | ||
BUFFER_SIZE = 6 | ||
BUTTON_COUNT = 6 | ||
DEFAULT_COMMS_TIMEOUT = 1.0 | ||
|
||
def __init__(self, uart, timeout=DEFAULT_COMMS_TIMEOUT): | ||
self.__uart = uart | ||
self.__no_comms_timeout = int(timeout * 1000) | ||
self.__no_comms_timeout_callback = None | ||
self.__last_received_millis = 0 | ||
self.__timeout_reached = True | ||
|
||
self.__receiving = False | ||
self.__data_index = 0 | ||
self.__in_buffer = bytearray(self.BUFFER_SIZE) | ||
|
||
self.__button_state = [False, False, False, False, False, False] | ||
|
||
self.__joystick_x = 0.0 | ||
self.__joystick_y = 0.0 | ||
self.__joystick_callback = None | ||
|
||
@property | ||
def joystick(self): | ||
return self.__joystick_x, self.__joystick_y | ||
|
||
def begin(self): | ||
# Clear the buffer | ||
while self.__uart.any() > 0: | ||
self.__uart.read() | ||
|
||
def is_connected(self): | ||
return not self.__timeout_reached | ||
|
||
def check_receive(self): | ||
while self.__uart.any() > 0: | ||
rx_byte = self.__uart.read(1)[0] | ||
if not self.__receiving: | ||
if rx_byte == self.STX: | ||
self.__receiving = True | ||
self.__data_index = 0 | ||
else: | ||
if rx_byte > 127 or self.__data_index > self.BUFFER_SIZE: | ||
self.__receiving = False | ||
elif rx_byte == self.ETX: | ||
if self.__data_index == 1: | ||
self.decode_button_state(self.__in_buffer[0]) # 3 Bytes ex: < STX "C" ETX > | ||
elif self.__data_index == 6: | ||
self.decode_joystick_state(self.__in_buffer) # 6 Bytes ex: < STX "200" "180" ETX > | ||
self.__last_received_millis = ticks_ms() | ||
self.__timeout_reached = False | ||
|
||
self.__receiving = False | ||
else: | ||
self.__in_buffer[self.__data_index] = rx_byte | ||
self.__data_index += 1 | ||
|
||
current_millis = ticks_ms() | ||
if (ticks_diff(current_millis, self.__last_received_millis) > self.__no_comms_timeout) and not self.__timeout_reached: | ||
print("here") | ||
if self.__no_comms_timeout_callback != None: | ||
self.__no_comms_timeout_callback() | ||
|
||
self.__timeout_reached = True | ||
|
||
def button_state(self, button): | ||
return self.__button_state[button] | ||
|
||
def set_button_state(self, button, pressed): | ||
if pressed: | ||
self.handle_button_press(button) | ||
else: | ||
self.handle_button_release(button) | ||
|
||
def set_timeout_callback(self, timeout_callback): | ||
self.__no_comms_timeout_callback = timeout_callback | ||
|
||
def set_joystick_callback(self, joystick_callback): | ||
self.__joystick_callback = joystick_callback | ||
|
||
def decode_button_state(self, data): | ||
# ----------------- BUTTON #1 ----------------------- | ||
if data == ord('A'): | ||
self.handle_button_press(0) | ||
elif data == ord('B'): | ||
self.handle_button_release(0) | ||
|
||
# ----------------- BUTTON #2 ----------------------- | ||
elif data == ord('C'): | ||
self.handle_button_press(1) | ||
elif data == ord('D'): | ||
self.handle_button_release(1) | ||
|
||
# ----------------- BUTTON #3 ----------------------- | ||
elif data == ord('E'): | ||
self.handle_button_press(2) | ||
elif data == ord('F'): | ||
self.handle_button_release(2) | ||
|
||
# ----------------- BUTTON #4 ----------------------- | ||
elif data == ord('G'): | ||
self.handle_button_press(3) | ||
elif data == ord('H'): | ||
self.handle_button_release(3) | ||
|
||
# ----------------- BUTTON #5 ----------------------- | ||
elif data == ord('I'): | ||
self.handle_button_press(4) | ||
elif data == ord('J'): | ||
self.handle_button_release(4) | ||
|
||
# ----------------- BUTTON #6 ----------------------- | ||
elif data == ord('K'): | ||
self.handle_button_press(5) | ||
elif data == ord('L'): | ||
self.handle_button_release(5) | ||
|
||
def decode_joystick_state(self, rx_byte): | ||
joy_x = (rx_byte[0] - 48) * 100 + (rx_byte[1] - 48) * 10 + (rx_byte[2] - 48) # obtain the Int from the ASCII representation | ||
joy_y = (rx_byte[3] - 48) * 100 + (rx_byte[4] - 48) * 10 + (rx_byte[5] - 48) | ||
joy_x = joy_x - 200; # Offset to avoid | ||
joy_y = joy_y - 200; # transmitting negative numbers | ||
|
||
if joy_x < -100 or joy_x > 100 or joy_y < -100 or joy_y > 100: | ||
return # commmunication error | ||
|
||
self.__joystick_x = float(joy_x) / 100.0 | ||
self.__joystick_y = float(joy_y) / 100.0 | ||
|
||
if self.__joystick_callback != None: | ||
self.__joystick_callback(self.__joystick_x, self.__joystick_y) | ||
|
||
def handle_button_press(self, button): | ||
self.__button_state[button] = True | ||
|
||
def handle_button_release(self, button): | ||
self.__button_state[button] = False | ||
|
||
def button_states_to_string(self): | ||
state = "" | ||
for i in range(0, len(self.__button_state)): | ||
if self.__button_state[i]: | ||
state += "1" | ||
else: | ||
state += "0" | ||
return state |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,169 @@ | ||
import time | ||
from machine import Pin, UART | ||
from pimoroni_yukon import Yukon | ||
from pimoroni_yukon import SLOT6 as LEFT_SLOT | ||
from pimoroni_yukon import SLOT1 as RIGHT_SLOT | ||
from pimoroni_yukon import SLOT5 as LED_SLOT | ||
from pimoroni_yukon import SLOT2 as BT_SLOT | ||
from pimoroni_yukon import SLOT3 as BUZZER_SLOT | ||
from pimoroni_yukon.modules import BigMotorModule, LEDStripModule | ||
from pimoroni_yukon import ticks_ms, ticks_add | ||
from pimoroni_yukon.logging import LOG_WARN | ||
from commander import JoyBTCommander | ||
|
||
# Constants | ||
UPDATES = 50 # How many times to update LEDs and Servos per second | ||
TIMESTEP = 1 / UPDATES | ||
TIMESTEP_MS = int(TIMESTEP * 1000) | ||
MOTOR_EXTENT = 0.4 # How far from zero to drive the motors | ||
|
||
STRIP_TYPE = LEDStripModule.NEOPIXEL # The type of LED strip being driven | ||
STRIP_PIO = 0 # The PIO system to use (0 or 1) to drive the strip | ||
STRIP_SM = 0 # The State Machines (SM) to use to drive the strip | ||
LEDS_PER_STRIP = 120 # How many LEDs are on the strip | ||
PULSE_TIME = 2.0 # The time to perform a complete pulse of the LEDs when motors_active | ||
|
||
BT_UART_ID = 1 # The ID of the hardware UART to use for bluetooth comms via a serial tranceiver | ||
BT_BAUDRATE = 9600 # The baudrate of the bluetooth serial tranceiver's serial | ||
BT_NO_COMMS_TIMEOUT = 1.0 # How long to wait after receiving data, to assume the transmitting device has disconnected | ||
|
||
LOW_VOLTAGE_LEVEL = 10.0 # The voltage below which the program will terminate and start the buzzer | ||
BUZZER_PERIOD = 0.5 # The time between each buzz of the low voltage alarm | ||
BUZZER_DUTY = 0.5 # The percentage of the time that the buzz will be on for | ||
|
||
# Variables | ||
yukon = Yukon(logging_level=LOG_WARN) # Create a Yukon object with the logging level set to warnings to reduce print outputs | ||
left_driver = BigMotorModule(init_encoder=False) # Create the left side BigMotorDriver object, without the encoder | ||
right_driver = BigMotorModule(init_encoder=False) # Create the left side BigMotorDriver object, without the encoder | ||
led_module = LEDStripModule(STRIP_TYPE, # Create a LEDStripModule object, with the details of the attached strip | ||
STRIP_PIO, | ||
STRIP_SM, | ||
LEDS_PER_STRIP) | ||
|
||
controller = JoyBTCommander(UART(BT_UART_ID, # Create a JoyBTCommander object, providing it with | ||
tx=BT_SLOT.FAST1, # a UART object for the serial bluetooth tranceiver | ||
rx=BT_SLOT.FAST2, | ||
baudrate=BT_BAUDRATE), | ||
BT_NO_COMMS_TIMEOUT) | ||
buzzer = BUZZER_SLOT.FAST3 # The pin the low voltage buzzer is attached to | ||
exited_due_to_low_voltage = True # Record if the program exited due to low voltage (assume true to start) | ||
|
||
|
||
def no_comms_callback(): | ||
# Disable both motors, causing them to coast to a stop | ||
left_driver.motor.disable() | ||
right_driver.motor.disable() | ||
|
||
|
||
def joystick_callback(x, y): | ||
x *= y # Prevent turning on the spot (which the chassis cannot achieve) by scaling the side input by the forward input | ||
|
||
# Update the left and right motor speeds based on the forward and side inputs | ||
left_speed = -y-x | ||
right_speed = y-x | ||
left_driver.motor.speed(left_speed * MOTOR_EXTENT) | ||
right_driver.motor.speed(right_speed * MOTOR_EXTENT) | ||
|
||
MID_LED = led_module.strip.num_leds() // 2 | ||
|
||
# Update the left side LEDs to a colour based on the left speed | ||
lefthue = map_float(left_speed, 1.5, -1.5, 0.999, 0.333) | ||
for led in range(0, MID_LED): | ||
led_module.strip.set_hsv(led, lefthue, 1.0, 1.0) | ||
|
||
# Update the right side LEDs to a colour based on the right speed | ||
righthue = map_float(right_speed, -1.5, 1.5, 0.999, 0.333) | ||
for led in range(MID_LED, led_module.strip.num_leds()): | ||
led_module.strip.set_hsv(led, righthue, 1.0, 1.0) | ||
|
||
led_module.strip.update() # Send the new colours to the LEDs | ||
|
||
|
||
# Function for mapping a value from one range to another | ||
def map_float(input, in_min, in_max, out_min, out_max): | ||
return (((input - in_min) * (out_max - out_min)) / (in_max - in_min)) + out_min | ||
|
||
|
||
# Ensure the input voltage is above the low level | ||
if yukon.read_input_voltage() > LOW_VOLTAGE_LEVEL: | ||
exited_due_to_low_voltage = False | ||
|
||
# Wrap the code in a try block, to catch any exceptions (including KeyboardInterrupt) | ||
try: | ||
# Register the SerialServoModule objects with their respective slots | ||
yukon.register_with_slot(left_driver, LEFT_SLOT) | ||
yukon.register_with_slot(right_driver, RIGHT_SLOT) | ||
yukon.register_with_slot(led_module, LED_SLOT) | ||
|
||
# Assign timeout and joystick callbacks to the controller, and start it | ||
controller.set_timeout_callback(no_comms_callback) | ||
controller.set_joystick_callback(joystick_callback) | ||
controller.begin() | ||
|
||
yukon.verify_and_initialise() # Verify that modules are attached to Yukon, and initialise them | ||
yukon.enable_main_output() # Turn on power to the module slots | ||
|
||
# Enable the drivers and regulators on all modules | ||
left_driver.enable() | ||
right_driver.enable() | ||
led_module.enable() | ||
|
||
current_time = ticks_ms() # Record the start time of the program loop | ||
|
||
# Loop until the BOOT/USER button is pressed | ||
while not yukon.is_boot_pressed(): | ||
|
||
controller.check_receive() | ||
print(controller.button_states_to_string(), controller.joystick[0], controller.joystick[1], sep=", ") | ||
|
||
# Perform a pulsing animation on the LEDs if there is no controller connection | ||
if not controller.is_connected(): | ||
# Update all the LEDs to show the same colour | ||
for led in range(led_module.strip.num_leds()): | ||
led_module.strip.set_rgb(led, 128, 128, 128) | ||
led_module.strip.update() | ||
|
||
try: | ||
# Advance the current time by a number of milliseconds | ||
current_time = ticks_add(current_time, TIMESTEP_MS) | ||
|
||
# Monitor sensors until the current time is reached, recording the min, max, and average for each | ||
# This approach accounts for the updates takinga non-zero amount of time to complete | ||
yukon.monitor_until_ms(current_time) | ||
except RuntimeError as e: | ||
left_driver.disable() | ||
right_driver.disable() | ||
import time | ||
print(str(e)) | ||
time.sleep(1.0) | ||
yukon.enable_main_output() | ||
left_driver.enable() | ||
right_driver.enable() | ||
|
||
# Get the average voltage recorded from monitoring, and print it out | ||
avg_voltage = yukon.get_readings()["Vi_avg"] | ||
print(f"V = {avg_voltage}") | ||
|
||
# Check if the average input voltage was below the low voltage level | ||
if avg_voltage < LOW_VOLTAGE_LEVEL: | ||
exited_due_to_low_voltage = True | ||
break # Break out of the loop | ||
|
||
finally: | ||
# Put the board back into a safe state, regardless of how the program may have ended | ||
yukon.reset() | ||
else: | ||
print(f"> Input voltage below {LOW_VOLTAGE_LEVEL}V!") | ||
|
||
# Was the exit caused by the input voltage dropping too low | ||
if exited_due_to_low_voltage: | ||
buzzer.init(Pin.OUT) # Set up the buzzer pin as an output | ||
|
||
yukon.set_led('A', True) | ||
while True: | ||
# Toggle the buzzer on and off repeatedly | ||
buzzer.on() | ||
time.sleep(BUZZER_PERIOD * BUZZER_DUTY) | ||
buzzer.off() | ||
time.sleep(BUZZER_PERIOD * (1.0 - BUZZER_DUTY)) | ||
|
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters