Skip to content

Commit 97ed01b

Browse files
Merge pull request #2291 from PaintYourDragon/main
Add Cheekmate project (CircuitPython + Arduino)
2 parents 87fee11 + 0fc8b49 commit 97ed01b

File tree

4 files changed

+390
-0
lines changed

4 files changed

+390
-0
lines changed

Cheekmate/Arduino/Cheekmate/.qtpy_esp32s2.test.only

Whitespace-only changes.
Lines changed: 109 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,109 @@
1+
// SPDX-FileCopyrightText: Adafruit Industries
2+
//
3+
// SPDX-License-Identifier: MIT
4+
5+
/*
6+
CHEEKMATE: secret message receiver using WiFi, Adafruit IO and
7+
a haptic buzzer. Monitors an Adafruit IO feed, converting new
8+
messages to Morse code.
9+
10+
WiFi & Adafruit IO credentials are in the accompanying config.h file.
11+
*/
12+
13+
#include <AdafruitIO_WiFi.h>
14+
#include <Adafruit_NeoPixel.h>
15+
#include <Adafruit_DRV2605.h>
16+
#include "config.h" // SET UP WIFI AND ADAFRUIT IO CREDENTIALS HERE
17+
18+
AdafruitIO_WiFi io(IO_USERNAME, IO_KEY, WIFI_SSID, WIFI_PASS);
19+
AdafruitIO_Feed *feed = io.feed(FEED_NAME, FEED_OWNER);
20+
Adafruit_NeoPixel led(1, PIN_NEOPIXEL);
21+
Adafruit_DRV2605 drv;
22+
char message[51];
23+
int rep = REPS; // Act as though message is already played out
24+
25+
// Runs once at startup
26+
void setup() {
27+
Serial.begin(115200);
28+
29+
led.begin();
30+
led.setBrightness(LED_BRIGHTNESS);
31+
led.show();
32+
33+
// Wire1 is the "extra" I2C interface on the QT Py ESP32-S2's
34+
// STEMMA connector. If adapting to a different board, you might
35+
// want &Wire for the sole or primary I2C interface.
36+
drv.begin(&Wire1);
37+
drv.setRealtimeValue(BUZZ);
38+
39+
feed->onMessage(handleMessage); // Set up message handler for feed
40+
41+
Serial.print("Connecting to Adafruit IO");
42+
io.connect();
43+
while(io.status() < AIO_CONNECTED) { // Wait for connection
44+
Serial.write('.');
45+
delay(500);
46+
}
47+
Serial.println(io.statusText());
48+
49+
buzz_on();
50+
delay(750); // Long buzz indicates everything is OK
51+
buzz_off();
52+
}
53+
54+
// Runs repeatedly until reset or power-off
55+
void loop() {
56+
io.run(); // Must periodically call Adafruit IO event manager
57+
// Play last message up to REPS times. If a new message has come
58+
// along in the interim, old message may repeat less than this,
59+
// and new message resets the count.
60+
if (rep < REPS) {
61+
play(message);
62+
delay(MEDIUM_GAP);
63+
rep++;
64+
}
65+
}
66+
67+
// Turn on LED and haptic motor
68+
void buzz_on() {
69+
led.setPixelColor(0, LED_COLOR);
70+
led.show();
71+
drv.setMode(DRV2605_MODE_REALTIME);
72+
}
73+
74+
// Turn off LED and haptic motor
75+
void buzz_off() {
76+
led.setPixelColor(0, 0);
77+
led.show();
78+
drv.setMode(DRV2605_MODE_INTTRIG);
79+
}
80+
81+
// Convert a string to Morse code, output to both the onboard LED
82+
// and the haptic motor.
83+
void play(char *str) {
84+
while(char c = toupper(*str++)) { // Upper-caseify each character of string...
85+
int i=0;
86+
// Scan Morse dictionary (in config.h) for a match
87+
for (; i<NUM_SYMBOLS && morse[i].symbol != c; i++);
88+
if (i < NUM_SYMBOLS) { // Found one!
89+
char mark;
90+
for (int j=0; (mark = morse[i].mark[j]); j++) {
91+
buzz_on();
92+
delay(mark == '-' ? DASH_LENGTH : DOT_LENGTH);
93+
buzz_off();
94+
delay(SYMBOL_GAP);
95+
}
96+
delay(CHARACTER_GAP - SYMBOL_GAP);
97+
} else { // Not in dictionary, prob. a space
98+
delay(MEDIUM_GAP);
99+
}
100+
}
101+
}
102+
103+
// Called when feed receives a message.
104+
void handleMessage(AdafruitIO_Data *data) {
105+
// Limit incoming message to fit char buffer + NUL
106+
strncpy(message, data->toChar(), sizeof message - 1);
107+
Serial.printf("Received '%s'\n", message);
108+
rep = 0; // Reset the message repeat counter
109+
}
Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
1+
// SPDX-FileCopyrightText: Adafruit Industries
2+
//
3+
// SPDX-License-Identifier: MIT
4+
5+
#define WIFI_SSID "your_wifi_ssid"
6+
#define WIFI_PASS "your_wifi_password"
7+
8+
// visit io.adafruit.com if you need to create an account,
9+
// or if you need your Adafruit IO key.
10+
#define IO_USERNAME "your_io_username"
11+
#define IO_KEY "your_io_key"
12+
13+
#define FEED_OWNER "feed_owner_name"
14+
#define FEED_NAME "cheekmate"
15+
16+
#define REPS 3 // Max number of times to repeat new message
17+
#define WPM 15 // Morse code words-per-minute
18+
#define BUZZ 255 // Haptic buzzer amplitude, 0-255
19+
#define LED_BRIGHTNESS 50 // NeoPixel brightness 1-255, or 0 to disable
20+
#define LED_COLOR 0xFF0000 // NeoPixel color (RGB hexadecimal)
21+
22+
// These values are derived from the 'WPM' setting above and do not require
23+
// manual editing. The dot, dash and gap times are set according to accepted
24+
// Morse code procedure.
25+
#define DOT_LENGTH 1200 / WPM // Duration of one Morse dot
26+
#define DASH_LENGTH (DOT_LENGTH * 3) // Duration of one Morse dash
27+
#define SYMBOL_GAP DOT_LENGTH // Duration of gap between dot or dash
28+
#define CHARACTER_GAP (DOT_LENGTH * 3) // Duration of gap between characters
29+
#define MEDIUM_GAP (DOT_LENGTH * 7) // Duraction of gap between words
30+
31+
// Morse code symbol-to-mark conversion dictionary. This contains the
32+
// standard A-Z and 0-9, and extra symbols "+" and "=" sometimes used
33+
// in chess. If other symbols are needed for this or other games, they
34+
// can be added to the end of the list.
35+
const struct {
36+
char symbol;
37+
const char *mark;
38+
} morse[] = {
39+
'A', ".-",
40+
'B', "-...",
41+
'C', "-.-.",
42+
'D', "-..",
43+
'E', ".",
44+
'F', "..-.",
45+
'G', "--.",
46+
'H', "....",
47+
'I', "..",
48+
'J', ".---",
49+
'K', "-.-",
50+
'L', ".-..",
51+
'M', "--",
52+
'N', "-.",
53+
'O', "---",
54+
'P', ".--.",
55+
'Q', "--.-",
56+
'R', ".-.",
57+
'S', "...",
58+
'T', "-",
59+
'U', "..-",
60+
'V', "...-",
61+
'W', ".--",
62+
'X', "-..-",
63+
'Y', "-.--",
64+
'Z', "--..",
65+
'0', "-----",
66+
'1', ".----",
67+
'2', "..---",
68+
'3', "...--",
69+
'4', "....-",
70+
'5', ".....",
71+
'6', "-....",
72+
'7', "--...",
73+
'8', "---..",
74+
'9', "----.",
75+
'+', ".-.-.",
76+
'=', "-...-",
77+
};
78+
#define NUM_SYMBOLS (sizeof morse / sizeof morse[0])

Cheekmate/CircuitPython/code.py

Lines changed: 203 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,203 @@
1+
# SPDX-FileCopyrightText: Adafruit Industries
2+
#
3+
# SPDX-License-Identifier: MIT
4+
5+
"""
6+
CHEEKMATE: secret message receiver using WiFi, Adafruit IO and a haptic
7+
buzzer. Periodically polls an Adafruit IO dashboard, converting new messages
8+
to Morse code.
9+
10+
secrets.py file must be present and contain WiFi & Adafruit IO credentials.
11+
"""
12+
13+
import gc
14+
import time
15+
import ssl
16+
import adafruit_drv2605
17+
import adafruit_requests
18+
import board
19+
import busio
20+
import neopixel
21+
import socketpool
22+
import supervisor
23+
import wifi
24+
from adafruit_io.adafruit_io import IO_HTTP
25+
26+
try:
27+
from secrets import secrets
28+
except ImportError:
29+
print("WiFi secrets are kept in secrets.py, please add them there!")
30+
raise
31+
32+
# CONFIGURABLE GLOBALS -----------------------------------------------------
33+
34+
FEED_KEY = "cheekmate" # Adafruit IO feed name
35+
POLL = 10 # Feed polling interval in seconds
36+
REPS = 3 # Max number of times to repeat new message
37+
WPM = 15 # Morse code words-per-minute
38+
BUZZ = 255 # Haptic buzzer amplitude, 0-255
39+
LED_BRIGHTNESS = 0.2 # NeoPixel brightness 0.0-1.0, or 0 to disable
40+
LED_COLOR = (255, 0, 0) # NeoPixel color (R, G, B), 0-255 ea.
41+
42+
# These values are derived from the 'WPM' setting above and do not require
43+
# manual editing. The dot, dash and gap times are set according to accepted
44+
# Morse code procedure.
45+
DOT_LENGTH = 1.2 / WPM # Duration of one Morse dot
46+
DASH_LENGTH = DOT_LENGTH * 3.0 # Duration of one Morse dash
47+
SYMBOL_GAP = DOT_LENGTH # Duration of gap between dot or dash
48+
CHARACTER_GAP = DOT_LENGTH * 3 # Duration of gap between characters
49+
MEDIUM_GAP = DOT_LENGTH * 7 # Duraction of gap between words
50+
51+
# Morse code symbol-to-mark conversion dictionary. This contains the
52+
# standard A-Z and 0-9, and extra symbols "+" and "=" sometimes used
53+
# in chess. If other symbols are needed for this or other games, they
54+
# can be added to the end of the list.
55+
MORSE = {
56+
"A": ".-",
57+
"B": "-...",
58+
"C": "-.-.",
59+
"D": "-..",
60+
"E": ".",
61+
"F": "..-.",
62+
"G": "--.",
63+
"H": "....",
64+
"I": "..",
65+
"J": ".---",
66+
"K": "-.-",
67+
"L": ".-..",
68+
"M": "--",
69+
"N": "-.",
70+
"O": "---",
71+
"P": ".--.",
72+
"Q": "--.-",
73+
"R": ".-.",
74+
"S": "...",
75+
"T": "-",
76+
"U": "..-",
77+
"V": "...-",
78+
"W": ".--",
79+
"X": "-..-",
80+
"Y": "-.--",
81+
"Z": "--..",
82+
"0": "-----",
83+
"1": ".----",
84+
"2": "..---",
85+
"3": "...--",
86+
"4": "....-",
87+
"5": ".....",
88+
"6": "-....",
89+
"7": "--...",
90+
"8": "---..",
91+
"9": "----.",
92+
"+": ".-.-.",
93+
"=": "-...-",
94+
}
95+
96+
# SOME FUNCTIONS -----------------------------------------------------------
97+
98+
99+
def buzz_on():
100+
"""Turn on LED and haptic motor."""
101+
pixels[0] = LED_COLOR
102+
drv.mode = adafruit_drv2605.MODE_REALTIME
103+
104+
105+
def buzz_off():
106+
"""Turn off LED and haptic motor."""
107+
pixels[0] = 0
108+
drv.mode = adafruit_drv2605.MODE_INTTRIG
109+
110+
111+
def play(string):
112+
"""Convert a string to Morse code, output to both the onboard LED
113+
and the haptic motor."""
114+
gc.collect()
115+
for symbol in string.upper():
116+
if code := MORSE.get(symbol): # find Morse code for character
117+
for mark in code:
118+
buzz_on()
119+
time.sleep(DASH_LENGTH if mark == "-" else DOT_LENGTH)
120+
buzz_off()
121+
time.sleep(SYMBOL_GAP)
122+
time.sleep(CHARACTER_GAP - SYMBOL_GAP)
123+
else:
124+
time.sleep(MEDIUM_GAP)
125+
126+
127+
# NEOPIXEL INITIALIZATION --------------------------------------------------
128+
129+
# This assumes there is a board.NEOPIXEL, which is true for QT Py ESP32-S2
130+
# and some other boards, but not ALL CircuitPython boards. If adapting the
131+
# code to another board, you might use digitalio with board.LED or similar.
132+
pixels = neopixel.NeoPixel(
133+
board.NEOPIXEL, 1, brightness=LED_BRIGHTNESS, auto_write=True
134+
)
135+
136+
# HAPTIC MOTOR CONTROLLER INIT ---------------------------------------------
137+
138+
# board.SCL1 and SDA1 are the "extra" I2C interface on the QT Py ESP32-S2's
139+
# STEMMA connector. If adapting to a different board, you might want
140+
# board.SCL and SDA as the sole or primary I2C interface.
141+
i2c = busio.I2C(board.SCL1, board.SDA1)
142+
drv = adafruit_drv2605.DRV2605(i2c)
143+
144+
# "Real-time playback" (RTP) is an unusual mode of the DRV2605 that's not
145+
# handled in the library by default, but is desirable here to get accurate
146+
# Morse code timing. This requires bypassing the library for a moment and
147+
# writing a couple of registers directly...
148+
while not i2c.try_lock():
149+
pass
150+
i2c.writeto(0x5A, bytes([0x1D, 0xA8])) # Amplitude will be unsigned
151+
i2c.writeto(0x5A, bytes([0x02, BUZZ])) # Buzz amplitude
152+
i2c.unlock()
153+
154+
# WIFI CONNECT -------------------------------------------------------------
155+
156+
try:
157+
print("Connecting to {}...".format(secrets["ssid"]), end="")
158+
wifi.radio.connect(secrets["ssid"], secrets["password"])
159+
print("OK")
160+
print("IP:", wifi.radio.ipv4_address)
161+
162+
pool = socketpool.SocketPool(wifi.radio)
163+
requests = adafruit_requests.Session(pool, ssl.create_default_context())
164+
# WiFi uses error messages, not specific exceptions, so this is "broad":
165+
except Exception as error: # pylint: disable=broad-except
166+
print("error:", error, "\nBoard will reload in 15 seconds.")
167+
time.sleep(15)
168+
supervisor.reload()
169+
170+
# ADAFRUIT IO INITIALIZATION -----------------------------------------------
171+
172+
aio_username = secrets["aio_username"]
173+
aio_key = secrets["aio_key"]
174+
io = IO_HTTP(aio_username, aio_key, requests)
175+
176+
# SUCCESSFUL STARTUP, PROCEED INTO MAIN LOOP -------------------------------
177+
178+
buzz_on()
179+
time.sleep(0.75) # Long buzz indicates everything is OK
180+
buzz_off()
181+
182+
current_message = "" # No message on startup
183+
rep = REPS # Act as though message is already played out
184+
last_time = -POLL # Force initial Adafruit IO polling
185+
186+
while True: # Repeat forever...
187+
188+
now = time.monotonic()
189+
if now - last_time >= POLL: # Time to poll Adafruit IO feed?
190+
last_time = now # Do it! Do it now!
191+
feed = io.get_feed(FEED_KEY)
192+
new_message = feed["last_value"]
193+
if new_message != current_message: # If message has changed,
194+
current_message = new_message # Save it,
195+
rep = 0 # and reset the repeat counter
196+
197+
# Play last message up to REPS times. If a new message has come along in
198+
# the interim, old message may repeat less than this, and new message
199+
# resets the count.
200+
if rep < REPS:
201+
play(current_message)
202+
time.sleep(MEDIUM_GAP)
203+
rep += 1

0 commit comments

Comments
 (0)