Adafruit_Learning_System_Gu.../Talking_D20/CircuitPython/code.py
2023-08-29 11:25:16 -07:00

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)