193 lines
5.9 KiB
Python
Executable file
193 lines
5.9 KiB
Python
Executable file
# SPDX-FileCopyrightText: 2023 Phillip Burgess for Adafruit Industries
|
|
#
|
|
# SPDX-License-Identifier: MIT
|
|
|
|
"""
|
|
Talking D20 for Adafruit RP2040 Prop-Maker Feather.
|
|
Required additions:
|
|
- 8 Ohm 1 watt speaker (Adafruit #4227)
|
|
- 400 mAh LiPoly battery (Adafruit #3898)
|
|
- 3D printed enclosure
|
|
Optional additions:
|
|
- Battery monitoring can be added with two 10K resistors in series.
|
|
One end to BAT, one to GND, and center point to an analog pin (e.g. A3).
|
|
Then set BATT_SENSE in configurables section, e.g. BATT_SENSE = board.A3
|
|
"""
|
|
|
|
|
|
# pylint: disable=import-error
|
|
from random import randint
|
|
import time
|
|
import adafruit_lis3dh
|
|
import analogio
|
|
import audiocore
|
|
import audiobusio
|
|
import board
|
|
from digitalio import DigitalInOut, Direction
|
|
|
|
# CONFIGURABLES ------------------------------------------------------------
|
|
|
|
WAV_PATH = "WAVs"
|
|
WAV_FILES = (
|
|
"01", # Index 0 (WAV for face 1)
|
|
"02", # Index 1 (WAV for face 2)
|
|
"03", # etc...
|
|
"04",
|
|
"05",
|
|
"06",
|
|
"07",
|
|
"08",
|
|
"09",
|
|
"10",
|
|
"11",
|
|
"12",
|
|
"13",
|
|
"14",
|
|
"15",
|
|
"16",
|
|
"17",
|
|
"18",
|
|
"19",
|
|
"20",
|
|
"annc1", # Index 20
|
|
"annc2",
|
|
"annc3",
|
|
"bad1", # Index 23
|
|
"bad2",
|
|
"bad3",
|
|
"good1", # Index 26
|
|
"good2",
|
|
"good3",
|
|
"startup", # Index 29
|
|
"03alt",
|
|
"batt1",
|
|
"batt2",
|
|
)
|
|
|
|
BATT_SENSE = None # Assign analog pin if voltage divider present
|
|
BATT_LOW = 3.4 # Voltage for battery warning (if BATT_SENSE)
|
|
FREEFALL_THRESHOLD = 8.65 # Near-freefall = 0.3G ^ 2
|
|
FREEFALL_MIN_DURATION = 1 / 25 # Time (seconds) roll is in near-freefall
|
|
SETTLE_TIME = 0.5 # Time (seconds) to settle on a face
|
|
SETTLE_TIMEOUT = 3.0 # If unsettled by this, resume freefall check
|
|
|
|
FACE_VECTORS = ( # Accelerometer vectors, shouldn't need to edit
|
|
(-3.50, 0.00, 9.16), # Face 1 (index 0)
|
|
(5.66, -5.66, -5.66), # Face 2 (index 1)
|
|
(-9.16, 3.50, 0.00), # 3 etc...
|
|
(9.16, 3.50, 0.00),
|
|
(5.66, -5.66, 5.66), # 5
|
|
(0.00, 9.16, -3.50),
|
|
(-5.66, -5.66, 5.66),
|
|
(-3.50, 0.00, -9.16),
|
|
(0.00, 9.16, 3.50),
|
|
(-5.66, -5.66, -5.66), # 10
|
|
(5.66, 5.66, 5.66),
|
|
(0.00, -9.16, -3.50),
|
|
(3.50, 0.00, 9.16),
|
|
(5.66, 5.66, -5.66),
|
|
(0.00, -9.16, 3.50), # 15
|
|
(-5.66, 5.66, -5.66),
|
|
(-9.16, -3.50, 0.00),
|
|
(9.16, -3.50, 0.00),
|
|
(-5.66, 5.66, 5.66),
|
|
(3.50, 0.00, -9.16), # 20
|
|
)
|
|
|
|
# HARDWARE SETUP -----------------------------------------------------------
|
|
|
|
# Enable power to audio amp, etc.
|
|
external_power = DigitalInOut(board.EXTERNAL_POWER)
|
|
external_power.direction = Direction.OUTPUT
|
|
external_power.value = True
|
|
|
|
# I2S audio out
|
|
audio = audiobusio.I2SOut(board.I2S_BIT_CLOCK, board.I2S_WORD_SELECT, board.I2S_DATA)
|
|
|
|
# LIS3DH accelerometer
|
|
lis3dh = adafruit_lis3dh.LIS3DH_I2C(board.I2C())
|
|
lis3dh.range = adafruit_lis3dh.RANGE_4_G
|
|
|
|
# Battery monitor, if present (requires two 10K resistors)
|
|
if BATT_SENSE:
|
|
adc = analogio.AnalogIn(BATT_SENSE)
|
|
|
|
# FUNCTIONS ----------------------------------------------------------------
|
|
|
|
|
|
def play(index, block=True):
|
|
"""Play one WAV file from the WAV_FILES table. Pass in table index (0-n)
|
|
and optional 'block' flag (if True, function blocks until audio is
|
|
finished playing)."""
|
|
wave_file = open(f"{WAV_PATH}/{WAV_FILES[index]}.wav", "rb")
|
|
wave = audiocore.WaveFile(wave_file)
|
|
audio.play(wave)
|
|
while block and audio.playing:
|
|
pass
|
|
|
|
|
|
def freefall_wait():
|
|
"""Watch for freefall condition (low G for FREEFALL_MIN_DURATION)."""
|
|
start_time = time.monotonic()
|
|
while time.monotonic() - start_time < FREEFALL_MIN_DURATION:
|
|
accel = lis3dh.acceleration
|
|
if accel[0] ** 2 + accel[1] ** 2 + accel[2] ** 2 > FREEFALL_THRESHOLD:
|
|
start_time = time.monotonic()
|
|
|
|
|
|
# pylint: disable=redefined-outer-name
|
|
def settle_wait():
|
|
"""Wait for die to stabilize (steady ~1G) on one number. Returns
|
|
index of corresponding audio file (0-19 for faces 1-20), or -1
|
|
if acceleration did not stabilize within SETTLE_TIMEOUT."""
|
|
start_time = time.monotonic()
|
|
prev_face = -1
|
|
while time.monotonic() - start_time < SETTLE_TIMEOUT:
|
|
accel = lis3dh.acceleration
|
|
mag = accel[0] ** 2 + accel[1] ** 2 + accel[2] ** 2
|
|
if 77.89 < mag < 116.35: # ~1G
|
|
face = -1
|
|
min_dist = 1000000
|
|
for index, vec in enumerate(FACE_VECTORS):
|
|
dist_sq = (
|
|
(accel[0] - vec[0]) ** 2
|
|
+ (accel[1] - vec[1]) ** 2
|
|
+ (accel[2] - vec[2]) ** 2
|
|
)
|
|
if dist_sq < min_dist: # New closest match?
|
|
min_dist = dist_sq # Save closest distance^2
|
|
face = index # Save index of closest match
|
|
if face != prev_face:
|
|
prev_face = face
|
|
settle_start = time.monotonic()
|
|
elif time.monotonic() - settle_start > SETTLE_TIME:
|
|
return face
|
|
else:
|
|
prev_face = -1
|
|
return -1
|
|
|
|
|
|
# STARTUP & MAIN LOOP ------------------------------------------------------
|
|
|
|
play(29, False) # Play greeting (non-blocking)
|
|
|
|
# pylint: disable=invalid-name, used-before-assignment
|
|
while True:
|
|
freefall_wait() # Wait for roll
|
|
face = settle_wait() # Wait for landing (or timeout)
|
|
if face >= 0: # Not timeout...
|
|
if face == 2: # If '3' face
|
|
if randint(0, 9) == 0: # 1-in-10 chance of...
|
|
face = 30 # Alternate 'face 3' track
|
|
play(randint(20, 22)) # One of 3 random announcements
|
|
play(face) # Face number
|
|
if face != 30: # If not the alt face...
|
|
if face <= 3: # Index 0-3 (face 1-4) = bad
|
|
play(randint(23, 25)) # Random jab
|
|
elif face >= 16: # index 16-19 (face 17-20) = good
|
|
play(randint(26, 28)) # Random praise
|
|
if BATT_SENSE:
|
|
volts = adc.value / 65535 * 3.3 * 2
|
|
if volts < BATT_LOW:
|
|
time.sleep(0.5)
|
|
play(randint(31, 32), False)
|