202 lines
6.4 KiB
Python
Executable file
202 lines
6.4 KiB
Python
Executable file
# SPDX-FileCopyrightText: Adafruit Industries
|
|
#
|
|
# SPDX-License-Identifier: MIT
|
|
|
|
"""
|
|
CHEEKMATE: secret message receiver using WiFi, Adafruit IO and a haptic
|
|
buzzer. Periodically polls an Adafruit IO dashboard, converting new messages
|
|
to Morse code.
|
|
|
|
secrets.py file must be present and contain WiFi & Adafruit IO credentials.
|
|
"""
|
|
|
|
import gc
|
|
import time
|
|
import ssl
|
|
import adafruit_drv2605
|
|
import adafruit_requests
|
|
import board
|
|
import busio
|
|
import neopixel
|
|
import socketpool
|
|
import supervisor
|
|
import wifi
|
|
from adafruit_io.adafruit_io import IO_HTTP
|
|
|
|
try:
|
|
from secrets import secrets
|
|
except ImportError:
|
|
print("WiFi secrets are kept in secrets.py, please add them there!")
|
|
raise
|
|
|
|
# CONFIGURABLE GLOBALS -----------------------------------------------------
|
|
|
|
FEED_KEY = "cheekmate" # Adafruit IO feed name
|
|
POLL = 10 # Feed polling interval in seconds
|
|
REPS = 3 # Max number of times to repeat new message
|
|
WPM = 15 # Morse code words-per-minute
|
|
BUZZ = 255 # Haptic buzzer amplitude, 0-255
|
|
LED_BRIGHTNESS = 0.2 # NeoPixel brightness 0.0-1.0, or 0 to disable
|
|
LED_COLOR = (255, 0, 0) # NeoPixel color (R, G, B), 0-255 ea.
|
|
|
|
# These values are derived from the 'WPM' setting above and do not require
|
|
# manual editing. The dot, dash and gap times are set according to accepted
|
|
# Morse code procedure.
|
|
DOT_LENGTH = 1.2 / WPM # Duration of one Morse dot
|
|
DASH_LENGTH = DOT_LENGTH * 3.0 # Duration of one Morse dash
|
|
SYMBOL_GAP = DOT_LENGTH # Duration of gap between dot or dash
|
|
CHARACTER_GAP = DOT_LENGTH * 3 # Duration of gap between characters
|
|
MEDIUM_GAP = DOT_LENGTH * 7 # Duraction of gap between words
|
|
|
|
# Morse code symbol-to-mark conversion dictionary. This contains the
|
|
# standard A-Z and 0-9, and one extra symbol "+" that's sometimes used
|
|
# in chess. If other symbols are needed for this or other games, they
|
|
# can be added to the end of the list.
|
|
MORSE = {
|
|
"A": ".-",
|
|
"B": "-...",
|
|
"C": "-.-.",
|
|
"D": "-..",
|
|
"E": ".",
|
|
"F": "..-.",
|
|
"G": "--.",
|
|
"H": "....",
|
|
"I": "..",
|
|
"J": ".---",
|
|
"K": "-.-",
|
|
"L": ".-..",
|
|
"M": "--",
|
|
"N": "-.",
|
|
"O": "---",
|
|
"P": ".--.",
|
|
"Q": "--.-",
|
|
"R": ".-.",
|
|
"S": "...",
|
|
"T": "-",
|
|
"U": "..-",
|
|
"V": "...-",
|
|
"W": ".--",
|
|
"X": "-..-",
|
|
"Y": "-.--",
|
|
"Z": "--..",
|
|
"0": "-----",
|
|
"1": ".----",
|
|
"2": "..---",
|
|
"3": "...--",
|
|
"4": "....-",
|
|
"5": ".....",
|
|
"6": "-....",
|
|
"7": "--...",
|
|
"8": "---..",
|
|
"9": "----.",
|
|
"+": ".-.-.",
|
|
}
|
|
|
|
# SOME FUNCTIONS -----------------------------------------------------------
|
|
|
|
|
|
def buzz_on():
|
|
"""Turn on LED and haptic motor."""
|
|
pixels[0] = LED_COLOR
|
|
drv.mode = adafruit_drv2605.MODE_REALTIME
|
|
|
|
|
|
def buzz_off():
|
|
"""Turn off LED and haptic motor."""
|
|
pixels[0] = 0
|
|
drv.mode = adafruit_drv2605.MODE_INTTRIG
|
|
|
|
|
|
def play(string):
|
|
"""Convert a string to Morse code, output to both the onboard LED
|
|
and the haptic motor."""
|
|
gc.collect()
|
|
for symbol in string.upper():
|
|
if code := MORSE.get(symbol): # find Morse code for character
|
|
for mark in code:
|
|
buzz_on()
|
|
time.sleep(DASH_LENGTH if mark == "-" else DOT_LENGTH)
|
|
buzz_off()
|
|
time.sleep(SYMBOL_GAP)
|
|
time.sleep(CHARACTER_GAP - SYMBOL_GAP)
|
|
else:
|
|
time.sleep(MEDIUM_GAP)
|
|
|
|
|
|
# NEOPIXEL INITIALIZATION --------------------------------------------------
|
|
|
|
# This assumes there is a board.NEOPIXEL, which is true for QT Py ESP32-S2
|
|
# and some other boards, but not ALL CircuitPython boards. If adapting the
|
|
# code to another board, you might use digitalio with board.LED or similar.
|
|
pixels = neopixel.NeoPixel(
|
|
board.NEOPIXEL, 1, brightness=LED_BRIGHTNESS, auto_write=True
|
|
)
|
|
|
|
# HAPTIC MOTOR CONTROLLER INIT ---------------------------------------------
|
|
|
|
# board.SCL1 and SDA1 are the "extra" I2C interface on the QT Py ESP32-S2's
|
|
# STEMMA connector. If adapting to a different board, you might want
|
|
# board.SCL and SDA as the sole or primary I2C interface.
|
|
i2c = busio.I2C(board.SCL1, board.SDA1)
|
|
drv = adafruit_drv2605.DRV2605(i2c)
|
|
|
|
# "Real-time playback" (RTP) is an unusual mode of the DRV2605 that's not
|
|
# handled in the library by default, but is desirable here to get accurate
|
|
# Morse code timing. This requires bypassing the library for a moment and
|
|
# writing a couple of registers directly...
|
|
while not i2c.try_lock():
|
|
pass
|
|
i2c.writeto(0x5A, bytes([0x1D, 0xA8])) # Amplitude will be unsigned
|
|
i2c.writeto(0x5A, bytes([0x02, BUZZ])) # Buzz amplitude
|
|
i2c.unlock()
|
|
|
|
# WIFI CONNECT -------------------------------------------------------------
|
|
|
|
try:
|
|
print("Connecting to {}...".format(secrets["ssid"]), end="")
|
|
wifi.radio.connect(secrets["ssid"], secrets["password"])
|
|
print("OK")
|
|
print("IP:", wifi.radio.ipv4_address)
|
|
|
|
pool = socketpool.SocketPool(wifi.radio)
|
|
requests = adafruit_requests.Session(pool, ssl.create_default_context())
|
|
# WiFi uses error messages, not specific exceptions, so this is "broad":
|
|
except Exception as error: # pylint: disable=broad-except
|
|
print("error:", error, "\nBoard will reload in 15 seconds.")
|
|
time.sleep(15)
|
|
supervisor.reload()
|
|
|
|
# ADAFRUIT IO INITIALIZATION -----------------------------------------------
|
|
|
|
aio_username = secrets["aio_username"]
|
|
aio_key = secrets["aio_key"]
|
|
io = IO_HTTP(aio_username, aio_key, requests)
|
|
|
|
# SUCCESSFUL STARTUP, PROCEED INTO MAIN LOOP -------------------------------
|
|
|
|
buzz_on()
|
|
time.sleep(0.75) # Long buzz indicates everything is OK
|
|
buzz_off()
|
|
|
|
current_message = "" # No message on startup
|
|
rep = REPS # Act as though message is already played out
|
|
last_time = -POLL # Force initial Adafruit IO polling
|
|
|
|
while True: # Repeat forever...
|
|
|
|
now = time.monotonic()
|
|
if now - last_time >= POLL: # Time to poll Adafruit IO feed?
|
|
last_time = now # Do it! Do it now!
|
|
feed = io.get_feed(FEED_KEY)
|
|
new_message = feed["last_value"]
|
|
if new_message != current_message: # If message has changed,
|
|
current_message = new_message # Save it,
|
|
rep = 0 # and reset the repeat counter
|
|
|
|
# Play last message up to REPS times. If a new message has come along in
|
|
# the interim, old message may repeat less than this, and new message
|
|
# resets the count.
|
|
if rep < REPS:
|
|
play(current_message)
|
|
time.sleep(MEDIUM_GAP)
|
|
rep += 1
|