Code for this project is available both for CircuitPython and for Arduino; you can use one or the other, whichever is more your programming style. Arduino is on the next page, CircuitPython is below.
If you’ve not used CircuitPython before, begin with the Welcome to CircuitPython guide which will walk you through downloading and installation.
Click the “Download Project Bundle” button below to get all the library files packed in along with the project’s main code.py file. You will still need to create a settings.toml file with WiFi and Adafruit IO credentials, explained later on this page.
Otherwise, if you want to assemble things manually, the project requires the following CircuitPython libraries, which can be found in the library bundle matching the version of CircuitPython you’re using:
- adafruit_drv2605.mpy
- adafruit_io
- adafruit_minimqtt
- adafruit_requests.mpy
- adafruit_pixelbuf.mpy
- adafruit_ticks.mpy
- adafruit_connection_manager.mpy
- neopixel.mpy
These go inside the lib folder on the CIRCUITPY drive. “.mpy” items are individual files, others require the full folder.

# 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. """ from os import getenv 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 # Get WiFi details and Adafruit IO keys, ensure these are setup in settings.toml # (visit io.adafruit.com if you need to create an account, or if you need your Adafruit IO key.) ssid = getenv("CIRCUITPY_WIFI_SSID") password = getenv("CIRCUITPY_WIFI_PASSWORD") aio_username = getenv("ADAFRUIT_AIO_USERNAME") aio_key = getenv("ADAFRUIT_AIO_KEY") if None in [ssid, password, aio_username, aio_key]: raise RuntimeError( "WiFi and Adafruit IO settings are kept in settings.toml, " "please add them there. The settings file must contain " "'CIRCUITPY_WIFI_SSID', 'CIRCUITPY_WIFI_PASSWORD', " "'ADAFRUIT_AIO_USERNAME' and 'ADAFRUIT_AIO_KEY' at a minimum." ) # 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(f"Connecting to {ssid}...") wifi.radio.connect(ssid, password) print("OK") print(f"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 ----------------------------------------------- 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
settings.toml
If you’ve previously worked with CircuitPython WiFi projects, you might already have this file on the drive, or another CircuitPython board. If not, it’s easy enough to create anew. Using your text editor of preference, create a new file on the CIRCUITPY drive, called settings.toml
Copy and paste the following exactly as it is, as a starting point:
CIRCUITPY_WIFI_SSID="your-wifi-ssid" CIRCUITPY_WIFI_PASSWORD="your-wifi-password" ADAFRUIT_AIO_USERNAME="your-aio-username" ADAFRUIT_AIO_KEY="your-super-long-aio-key"
This is a list of Python 'key' : 'value' pairs. Do not edit the keys (the part before the colon : on each line), just the values, being careful to keep both 'quotes' around strings and the comma at the end of each line.
Replace wifi_network_name and wifi_password with the name or “SSID” of your wireless network and the password for access. If tethering from a phone, one or both might be auto-generated…this information will be somewhere in the phone settings. Only 2.4 GHz networks are supported; 5 GHz is not compatible with ESP32.
Replace ADAFRUIT_AIO_USERNAME and ADAFRUIT_AIO_KEY with your name and unique key as explained on the “Adafruit IO Setup” page.
Page last edited March 19, 2025
Text editor powered by tinymce.