-
Notifications
You must be signed in to change notification settings - Fork 769
/
code.py
executable file
·203 lines (171 loc) · 6.42 KB
/
code.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
# 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 extra symbols "+" and "=" 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