mirror of
https://github.com/apirrone/Open_Duck_Mini_Runtime.git
synced 2025-09-03 11:43:58 +00:00
Use pigpio for remote projector, eyes and antennas on Pi Zero
This commit is contained in:
parent
32037347dc
commit
85d7efde7c
3 changed files with 186 additions and 57 deletions
|
@ -1,49 +1,96 @@
|
|||
import board
|
||||
import pwmio
|
||||
"""
|
||||
Servo control for the duck's antennas using a remote Raspberry Pi Zero.
|
||||
|
||||
In the previous implementation this module used CircuitPython's ``pwmio``
|
||||
module to produce PWM signals on the local GPIO pins (``board.D13`` and
|
||||
``board.D12``). In the new setup the antennas are physically connected to
|
||||
a Pi Zero that is exposed via USB gadget mode. We therefore use the
|
||||
pigpio library to send servo pulse widths to the remote pins over the
|
||||
network. This allows the controlling code to run on the Pi 4 while
|
||||
driving servos attached to the Pi Zero.
|
||||
|
||||
The servo angle is specified in the range [-1, 1], where -1 corresponds
|
||||
to the minimum position (1 ms pulse), 0 corresponds to the neutral
|
||||
position (1.5 ms pulse) and 1 corresponds to the maximum position
|
||||
(2 ms pulse). These values are converted into microsecond pulse widths
|
||||
for ``pi.set_servo_pulsewidth``.
|
||||
"""
|
||||
|
||||
import pigpio
|
||||
import math
|
||||
import time
|
||||
|
||||
LEFT_ANTENNA_PIN = board.D13
|
||||
RIGHT_ANTENNA_PIN = board.D12
|
||||
# BCM pin numbers for the left and right antenna servos on the Pi Zero.
|
||||
# ``board.D13`` → BCM 13, ``board.D12`` → BCM 12.
|
||||
LEFT_ANTENNA_PIN = 13
|
||||
RIGHT_ANTENNA_PIN = 12
|
||||
LEFT_SIGN = 1
|
||||
RIGHT_SIGN = -1
|
||||
MIN_UPDATE_INTERVAL = 1 / 50 # 20ms
|
||||
# Minimum update interval when sweeping the antennas (20 ms).
|
||||
MIN_UPDATE_INTERVAL = 1 / 50
|
||||
|
||||
|
||||
def value_to_duty_cycle(v):
|
||||
pulse_width_ms = 1.5 + (v * 0.5) # 1ms to 2ms
|
||||
duty_cycle = int((pulse_width_ms / 20) * 65535)
|
||||
return min(max(duty_cycle, 3277), 6553)
|
||||
def value_to_pulse_width(value: float) -> int:
|
||||
"""Map a value in [-1, 1] to a servo pulse width in microseconds.
|
||||
|
||||
A value of 0 returns the neutral pulse width of 1500 µs. Values of -1
|
||||
and 1 return 1000 µs and 2000 µs respectively. The result is
|
||||
clamped to [500, 2500] µs which is within the pigpio servo limits.
|
||||
"""
|
||||
# Clamp input range
|
||||
v = max(-1.0, min(1.0, value))
|
||||
return int(1500 + (v * 500))
|
||||
|
||||
|
||||
class Antennas:
|
||||
def __init__(self):
|
||||
neutral_duty = value_to_duty_cycle(0)
|
||||
self.pwm_left = pwmio.PWMOut(LEFT_ANTENNA_PIN, frequency=50, duty_cycle=neutral_duty)
|
||||
self.pwm_right = pwmio.PWMOut(RIGHT_ANTENNA_PIN, frequency=50, duty_cycle=neutral_duty)
|
||||
"""Control left and right servo antennas via a remote pigpio daemon.
|
||||
|
||||
def set_position_left(self, position):
|
||||
self.set_position(self.pwm_left, position, LEFT_SIGN)
|
||||
Parameters
|
||||
----------
|
||||
host : str, optional
|
||||
IP address or hostname of the pigpio daemon running on the Pi Zero.
|
||||
Defaults to ``"192.168.7.2"``.
|
||||
"""
|
||||
|
||||
def set_position_right(self, position):
|
||||
self.set_position(self.pwm_right, position, RIGHT_SIGN)
|
||||
def __init__(self, host: str = "192.168.7.2") -> None:
|
||||
# Connect to remote pigpio
|
||||
self.pi = pigpio.pi(host)
|
||||
if not self.pi.connected:
|
||||
raise RuntimeError(f"Failed to connect to pigpio daemon on {host}")
|
||||
|
||||
def set_position(self, pwm, value, sign=1):
|
||||
# if value == 0:
|
||||
# return
|
||||
if -1 <= value <= 1:
|
||||
duty_cycle = value_to_duty_cycle(value * sign) # Convert value to duty cycle (1ms-2ms)
|
||||
pwm.duty_cycle = duty_cycle
|
||||
# Store pins and set to servo mode (pigpio automatically sets mode)
|
||||
self.left_pin = LEFT_ANTENNA_PIN
|
||||
self.right_pin = RIGHT_ANTENNA_PIN
|
||||
# Initialise servos to neutral position
|
||||
neutral = value_to_pulse_width(0)
|
||||
self.pi.set_servo_pulsewidth(self.left_pin, neutral)
|
||||
self.pi.set_servo_pulsewidth(self.right_pin, neutral)
|
||||
|
||||
def set_position_left(self, position: float) -> None:
|
||||
self.set_position(self.left_pin, position, LEFT_SIGN)
|
||||
|
||||
def set_position_right(self, position: float) -> None:
|
||||
self.set_position(self.right_pin, position, RIGHT_SIGN)
|
||||
|
||||
def set_position(self, pin: int, value: float, sign: int = 1) -> None:
|
||||
"""Set the servo on ``pin`` to ``value`` (clamped to [-1, 1])."""
|
||||
if -1.0 <= value <= 1.0:
|
||||
pulse_width = value_to_pulse_width(value * sign)
|
||||
self.pi.set_servo_pulsewidth(pin, pulse_width)
|
||||
else:
|
||||
print("Invalid input! Enter a value between -1 and 1.")
|
||||
|
||||
def stop(self):
|
||||
def stop(self) -> None:
|
||||
"""Centre the servos and close the pigpio connection."""
|
||||
time.sleep(MIN_UPDATE_INTERVAL)
|
||||
# Centre both servos
|
||||
self.set_position_left(0)
|
||||
self.set_position_right(0)
|
||||
time.sleep(MIN_UPDATE_INTERVAL)
|
||||
self.pwm_left.deinit()
|
||||
self.pwm_right.deinit()
|
||||
# Disable servo pulses and close connection
|
||||
self.pi.set_servo_pulsewidth(self.left_pin, 0)
|
||||
self.pi.set_servo_pulsewidth(self.right_pin, 0)
|
||||
self.pi.stop()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
|
|
|
@ -1,32 +1,80 @@
|
|||
import board
|
||||
import digitalio
|
||||
"""
|
||||
Update: This module has been modified to use the pigpio library for driving
|
||||
the eye LEDs on a remote Raspberry Pi Zero. The Pi Zero is connected to
|
||||
the main Raspberry Pi 4 via USB (gadget mode) and exposes its GPIO pins over
|
||||
the network using the pigpio daemon. The IP address of the Pi Zero must
|
||||
be supplied when creating the `Eyes` instance (defaults to ``192.168.7.2``).
|
||||
|
||||
Instead of using CircuitPython's ``digitalio`` and ``board`` modules, we
|
||||
instantiate a ``pigpio.pi`` object and configure the relevant pins as
|
||||
outputs. The pins are referenced using their Broadcom (BCM) numbers, which
|
||||
match the ``board.Dxx`` naming convention used previously (e.g. ``board.D24``
|
||||
corresponds to BCM 24).
|
||||
|
||||
To blink the LEDs, this class writes a high (1) or low (0) value to both
|
||||
pins using ``pi.write``. When stopping, the pins are explicitly set low and
|
||||
the pigpio connection is terminated.
|
||||
"""
|
||||
|
||||
import pigpio
|
||||
import random
|
||||
import time
|
||||
from threading import Thread, Event
|
||||
|
||||
LEFT_EYE_PIN = board.D24
|
||||
RIGHT_EYE_PIN = board.D23
|
||||
# BCM pin numbers corresponding to the eye LEDs on the Pi Zero. These match
|
||||
# the ``board.Dxx`` names previously used (D24 → BCM 24, D23 → BCM 23).
|
||||
LEFT_EYE_PIN = 24
|
||||
RIGHT_EYE_PIN = 23
|
||||
|
||||
|
||||
class Eyes:
|
||||
def __init__(self, blink_duration=0.1, min_interval=1.0, max_interval=4.0):
|
||||
self.left_eye = digitalio.DigitalInOut(LEFT_EYE_PIN)
|
||||
self.left_eye.direction = digitalio.Direction.OUTPUT
|
||||
"""Controls blinking of two eye LEDs via a remote pigpio daemon.
|
||||
|
||||
self.right_eye = digitalio.DigitalInOut(RIGHT_EYE_PIN)
|
||||
self.right_eye.direction = digitalio.Direction.OUTPUT
|
||||
Parameters
|
||||
----------
|
||||
host : str, optional
|
||||
IP address or hostname of the pigpio daemon running on the Pi Zero.
|
||||
Defaults to ``"192.168.7.2"``. Change this if your Pi Zero uses a
|
||||
different IP address.
|
||||
blink_duration : float, optional
|
||||
Duration (in seconds) that the eyes remain closed for each blink.
|
||||
min_interval : float, optional
|
||||
Minimum time (in seconds) between blinks.
|
||||
max_interval : float, optional
|
||||
Maximum time (in seconds) between blinks.
|
||||
"""
|
||||
|
||||
self.blink_duration = blink_duration
|
||||
self.min_interval = min_interval
|
||||
self.max_interval = max_interval
|
||||
def __init__(self, host: str = "192.168.7.2", blink_duration: float = 0.1,
|
||||
min_interval: float = 1.0, max_interval: float = 4.0) -> None:
|
||||
# Establish a remote connection to the pigpio daemon running on the
|
||||
# Raspberry Pi Zero. If the connection fails, ``pigpio.pi`` will
|
||||
# return an object with ``connected`` set to 0.
|
||||
self.pi = pigpio.pi(host)
|
||||
if not self.pi.connected:
|
||||
raise RuntimeError(f"Failed to connect to pigpio daemon on {host}")
|
||||
|
||||
self._stop_event = Event()
|
||||
self._thread = Thread(target=self.run, daemon=True)
|
||||
# Configure the eye pins as outputs and start with the LEDs off.
|
||||
self.left_pin = LEFT_EYE_PIN
|
||||
self.right_pin = RIGHT_EYE_PIN
|
||||
self.pi.set_mode(self.left_pin, pigpio.OUTPUT)
|
||||
self.pi.set_mode(self.right_pin, pigpio.OUTPUT)
|
||||
self.pi.write(self.left_pin, 0)
|
||||
self.pi.write(self.right_pin, 0)
|
||||
|
||||
self.blink_duration: float = blink_duration
|
||||
self.min_interval: float = min_interval
|
||||
self.max_interval: float = max_interval
|
||||
|
||||
# Threading setup for blinking logic.
|
||||
self._stop_event: Event = Event()
|
||||
self._thread: Thread = Thread(target=self.run, daemon=True)
|
||||
self._thread.start()
|
||||
|
||||
def _set_eyes(self, state):
|
||||
self.left_eye.value = state
|
||||
self.right_eye.value = state
|
||||
def _set_eyes(self, state: bool) -> None:
|
||||
"""Set both eye LEDs on (True) or off (False)."""
|
||||
value = 1 if state else 0
|
||||
self.pi.write(self.left_pin, value)
|
||||
self.pi.write(self.right_pin, value)
|
||||
|
||||
def run(self):
|
||||
try:
|
||||
|
@ -41,11 +89,13 @@ class Eyes:
|
|||
self._stop_event.set()
|
||||
|
||||
def stop(self):
|
||||
"""Stop the blinking thread and release resources."""
|
||||
self._stop_event.set()
|
||||
self._thread.join()
|
||||
# Ensure the LEDs are turned off before closing the connection.
|
||||
self._set_eyes(False)
|
||||
self.left_eye.deinit()
|
||||
self.right_eye.deinit()
|
||||
# Terminate connection to the pigpio daemon.
|
||||
self.pi.stop()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
|
|
|
@ -1,24 +1,56 @@
|
|||
import board
|
||||
import digitalio
|
||||
"""
|
||||
This module controls a projector relay connected to a Raspberry Pi Zero.
|
||||
The Pi Zero is exposed to the main Raspberry Pi 4 via USB in gadget mode,
|
||||
and GPIO control is achieved through the pigpio library. The default
|
||||
IP address of the Pi Zero is ``192.168.7.2``; update this if your setup
|
||||
differs.
|
||||
|
||||
Previously this module relied on CircuitPython's ``digitalio`` and ``board``
|
||||
modules. Those do not work when the code is executed on the Pi 4 and the
|
||||
device is physically attached to a remote Pi Zero. The updated version
|
||||
instead uses pigpio to toggle a BCM pin on the remote device. The pin
|
||||
number corresponds to ``board.D25`` (BCM 25).
|
||||
"""
|
||||
|
||||
import pigpio
|
||||
import time
|
||||
|
||||
PROJECTOR_GPIO = board.D25
|
||||
# BCM pin used to toggle the projector. ``board.D25`` translates to BCM 25.
|
||||
PROJECTOR_GPIO = 25
|
||||
|
||||
|
||||
class Projector:
|
||||
def __init__(self):
|
||||
self.project = digitalio.DigitalInOut(PROJECTOR_GPIO)
|
||||
self.project.direction = digitalio.Direction.OUTPUT
|
||||
"""Toggle a projector connected to a remote Raspberry Pi Zero via pigpio.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
host : str, optional
|
||||
IP address or hostname of the pigpio daemon on the Pi Zero.
|
||||
Defaults to ``"192.168.7.2"``.
|
||||
"""
|
||||
|
||||
def __init__(self, host: str = "192.168.7.2") -> None:
|
||||
# Connect to the remote pigpio daemon. If the connection fails,
|
||||
# ``pigpio.pi`` returns an object with ``connected`` set to 0.
|
||||
self.pi = pigpio.pi(host)
|
||||
if not self.pi.connected:
|
||||
raise RuntimeError(f"Failed to connect to pigpio daemon on {host}")
|
||||
|
||||
# Configure the projector GPIO as an output and start with it off.
|
||||
self.pin = PROJECTOR_GPIO
|
||||
self.pi.set_mode(self.pin, pigpio.OUTPUT)
|
||||
self.on = False
|
||||
self.pi.write(self.pin, 0)
|
||||
|
||||
def switch(self):
|
||||
def switch(self) -> None:
|
||||
"""Toggle the projector on/off."""
|
||||
self.on = not self.on
|
||||
self.pi.write(self.pin, 1 if self.on else 0)
|
||||
|
||||
self.project.value = self.on
|
||||
|
||||
def stop(self):
|
||||
self.project.value = False
|
||||
self.project.deinit()
|
||||
def stop(self) -> None:
|
||||
"""Ensure the projector is off and clean up the pigpio connection."""
|
||||
self.pi.write(self.pin, 0)
|
||||
self.pi.stop()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
|
|
Loading…
Reference in a new issue