Skip to content

Commit

Permalink
More serial servo examples
Browse files Browse the repository at this point in the history
  • Loading branch information
ZodiusInfuser committed Nov 9, 2023
1 parent cdd1857 commit 69fa5cf
Show file tree
Hide file tree
Showing 4 changed files with 178 additions and 3 deletions.
81 changes: 81 additions & 0 deletions examples/modules/serial_servo/drive_servo.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
from pimoroni_yukon import Yukon
from pimoroni_yukon import SLOT1 as SLOT
from pimoroni_yukon.modules import SerialServoModule
from pimoroni_yukon.devices.lx_servo import LXServo
from pimoroni_yukon.timing import ticks_ms, ticks_add

"""
Drive a servo continuously (aka wheel mode) at a speed with Yukon's onboard buttons.
This uses a Serial Bus Servos module connected to Slot1.
Press "A" to drive the servo at the speed.
Press "B" to stop the servo.
Press "Boot/User" to exit the program.
"""

# Constants
SERVO_ID = 3 # The ID of the servo to control
VOLTAGE_LIMIT = 8.4 # The voltage to not exceed, to protect the servos
SPEED = 0.5 # The speed (between -1.0 and 1.0) the servo will drive at when button 'A' is pressed
SLEEP = 0.1 # The time to sleep between each update

# Variables
yukon = Yukon(voltage_limit=VOLTAGE_LIMIT) # Create a new Yukon object, with a lower voltage limit set
module = SerialServoModule() # Create a SerialServoModule object
last_button_states = {'A': False, 'B': False} # The last states of the buttons


# Function to check if the button has been newly pressed
def button_newly_pressed(btn):
global last_button_states
button_state = yukon.is_pressed(btn)
button_pressed = button_state and not last_button_states[btn]
last_button_states[btn] = button_state

return button_pressed


# Wrap the code in a try block, to catch any exceptions (including KeyboardInterrupt)
try:
yukon.register_with_slot(module, SLOT) # Register the SerialServoModule object with the slot
yukon.verify_and_initialise() # Verify that a SerialServoModule is attached to Yukon, and initialise it
yukon.enable_main_output() # Turn on power to the module slots

yukon.monitored_sleep(1) # Wait for serial servos to power up

# Create an LXServo object to interact with the servo,
# giving it access to the module's UART and Duplexer
servo = LXServo(SERVO_ID, module.uart, module.duplexer)

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():

# Has the A button been pressed?
if button_newly_pressed('A'):
servo.drive_at(SPEED)
yukon.set_led('A', True) # Show that A was pressed
else:
yukon.set_led('A', False) # A is no longer newly pressed

# Has the B button been pressed?
if button_newly_pressed('B'):
servo.stop()
yukon.set_led('B', True) # Show that B was pressed
else:
yukon.set_led('B', False) # B is no longer newly pressed

# Print out sensor readings from the servo
print(f"Angle: {servo.read_angle()}, Vin: {servo.read_voltage()}, Temp: {servo.read_temperature()}")

# Advance the current time by a number of seconds
current_time = ticks_add(current_time, int(SLEEP * 1000))

# Monitor sensors until the current time is reached, recording the min, max, and average for each
# This approach accounts for the updating of the rainbows taking a non-zero amount of time to complete
yukon.monitor_until_ms(current_time)

finally:
# Put the board back into a safe state, regardless of how the program may have ended
yukon.reset()
83 changes: 83 additions & 0 deletions examples/modules/serial_servo/move_servo.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
from pimoroni_yukon import Yukon
from pimoroni_yukon import SLOT1 as SLOT
from pimoroni_yukon.modules import SerialServoModule
from pimoroni_yukon.devices.lx_servo import LXServo
from pimoroni_yukon.timing import ticks_ms, ticks_add

"""
Move a servo between two angles with Yukon's onboard buttons.
This uses a Serial Bus Servos module connected to Slot1.
Press "A" to move the servo to the first angle.
Press "B" to move the servo to the second angle.
Press "Boot/User" to exit the program.
"""

# Constants
SERVO_ID = 1 # The ID of the servo to control
VOLTAGE_LIMIT = 8.4 # The voltage to not exceed, to protect the servos
ANGLE_A = -45 # The angle (in degrees) the servo will move to when button 'A' is pressed
ANGLE_B = 45 # The angle (in degrees) the servo will move to when button 'B' is pressed
MOVEMENT_DURATION = 1.0 # The time (in seconds) the servo will perform the movements over
SLEEP = 0.1 # The time to sleep between each update

# Variables
yukon = Yukon(voltage_limit=VOLTAGE_LIMIT) # Create a new Yukon object, with a lower voltage limit set
module = SerialServoModule() # Create a SerialServoModule object
last_button_states = {'A': False, 'B': False} # The last states of the buttons


# Function to check if the button has been newly pressed
def button_newly_pressed(btn):
global last_button_states
button_state = yukon.is_pressed(btn)
button_pressed = button_state and not last_button_states[btn]
last_button_states[btn] = button_state

return button_pressed


# Wrap the code in a try block, to catch any exceptions (including KeyboardInterrupt)
try:
yukon.register_with_slot(module, SLOT) # Register the SerialServoModule object with the slot
yukon.verify_and_initialise() # Verify that a SerialServoModule is attached to Yukon, and initialise it
yukon.enable_main_output() # Turn on power to the module slots

yukon.monitored_sleep(1) # Wait for serial servos to power up

# Create an LXServo object to interact with the servo,
# giving it access to the module's UART and Duplexer
servo = LXServo(SERVO_ID, module.uart, module.duplexer)

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():

# Has the A button been pressed?
if button_newly_pressed('A'):
servo.move_to(ANGLE_A, MOVEMENT_DURATION)
yukon.set_led('A', True) # Show that A was pressed
else:
yukon.set_led('A', False) # A is no longer newly pressed

# Has the B button been pressed?
if button_newly_pressed('B'):
servo.move_to(ANGLE_B, MOVEMENT_DURATION)
yukon.set_led('B', True) # Show that B was pressed
else:
yukon.set_led('B', False) # B is no longer newly pressed

# Print out sensor readings from the servo
print(f"Angle: {servo.read_angle()}, Vin: {servo.read_voltage()}, Temp: {servo.read_temperature()}")

# Advance the current time by a number of seconds
current_time = ticks_add(current_time, int(SLEEP * 1000))

# Monitor sensors until the current time is reached, recording the min, max, and average for each
# This approach accounts for the updating of the rainbows taking a non-zero amount of time to complete
yukon.monitor_until_ms(current_time)

finally:
# Put the board back into a safe state, regardless of how the program may have ended
yukon.reset()
2 changes: 1 addition & 1 deletion examples/modules/serial_servo/servo_detect.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
from pimoroni_yukon.devices.lx_servo import LXServo

"""
Detect any servos that are attached to a Serial Bus Servos module connected to Slot1.
Detect any servos that are attached to a Serial Bus Servo module connected to Slot1.
"""

# Constants
Expand Down
15 changes: 13 additions & 2 deletions lib/pimoroni_yukon/devices/lx_servo.py
Original file line number Diff line number Diff line change
Expand Up @@ -223,7 +223,7 @@ def __init__(self, id, uart, duplexer, timeout=DEFAULT_READ_TIMEOUT, debug_pin=N
self.__debug_pin.init(Pin.OUT)

if self.__id != self.BROADCAST_ID:
logging.info(self.__message_header() + "Searching for servo ... ", end="")
logging.info(f"> Searching for Serial Servo #{self.__id} ... ", end="")

self.verify_id()

Expand All @@ -244,7 +244,7 @@ def detect(id, uart, duplexer, timeout=DEFAULT_READ_TIMEOUT):
try:
received = receive(id, uart, duplexer, timeout, "B")
if received != id:
raise RuntimeError(f"Serial servo #{id} incorrectly reported its ID as {received}")
raise RuntimeError(f"Serial Servo #{id} incorrectly reported its ID as {received}")
return True
except TimeoutError:
return False
Expand Down Expand Up @@ -311,6 +311,8 @@ def move_to(self, angle, duration):

self.__send(SERVO_MOVE_TIME_WRITE, "HH", position, ms)

logging.info(self.__message_header() + f"Moving to {(position - 500) * 90 / 360}° in {duration}s")

def queue_move(self, angle, duration):
position = int(((angle / 90) * 360) + 500)
position = min(max(position, 0), 1000)
Expand All @@ -321,6 +323,8 @@ def queue_move(self, angle, duration):

self.__send(SERVO_MOVE_TIME_WAIT_WRITE, "HH", position, ms)

logging.info(self.__message_header() + f"Queued movement to {(position - 500) * 90 / 360}° in {duration}s")

def start_queued(self):
if self.__id == self.BROADCAST_ID or self.__mode != LXServo.SERVO_MODE:
self.__switch_to_servo_mode()
Expand All @@ -335,6 +339,11 @@ def drive_at(self, speed):
if self.__id != self.BROADCAST_ID:
self.__mode = LXServo.MOTOR_MODE

if value == 0:
logging.info(self.__message_header() + f"Stop driving")
else:
logging.info(self.__message_header() + f"Driving at {value / 1000}")

def last_move(self):
if self.__id == self.BROADCAST_ID:
raise ValueError("cannot read the last move when broadcasting")
Expand All @@ -357,10 +366,12 @@ def last_speed(self):
def stop(self):
if self.__id == self.BROADCAST_ID:
self.__send(SERVO_MOVE_STOP)
logging.info(self.__message_header() + f"Stop moving")
self.drive_at(0.0)
else:
if self.__mode == LXServo.SERVO_MODE:
self.__send(SERVO_MOVE_STOP)
logging.info(self.__message_header() + f"Stop moving")
else:
self.drive_at(0.0)

Expand Down

0 comments on commit 69fa5cf

Please sign in to comment.