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 secrets.py 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
  • 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.

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

secrets.py

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 secrets.py

Copy and paste the following exactly as it is, as a starting point:

secrets = {
    'ssid' : 'wifi_network_name',
    'password' : 'wifi_password',
    'aio_username' : 'adafruit_io_username',
    'aio_key' : 'adafruit_io_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_io_username and adafruit_io_key with your name and unique key as explained on the “Adafruit IO Setup” page.

This guide was first published on Oct 05, 2022. It was last updated on Jun 21, 2024.

This page (CircuitPython Code) was last updated on Jun 21, 2024.

Text editor powered by tinymce.