You may need to update the bootloader on your nRF52840 board to install the latest CircuitPython! Use the link below for more info:

Text Editor

Adafruit recommends using the Mu editor for editing your CircuitPython code. You can get more info in this guide.

Alternatively, you can use any text editor that saves simple text files.

Download the Project Bundle

Your project will use a specific set of CircuitPython libraries, .wav files, and the code.py file. To get everything you need, click on the Download Project Bundle link below, and uncompress the .zip file.

Drag the contents of the uncompressed bundle directory onto your board's CIRCUITPY drive, replacing any existing files or directories with the same names, and adding any new ones that are necessary.

# SPDX-FileCopyrightText: 2022 John Park for Adafruit Industries
# SPDX-License-Identifier: MIT

# BLE Ouija Board

import time
import random
import board
import digitalio
import neopixel
from adafruit_motorkit import MotorKit
from adafruit_ble import BLERadio
from adafruit_ble.advertising.standard import ProvideServicesAdvertisement
from adafruit_ble.services.nordic import UARTService
from adafruit_bluefruit_connect.packet import Packet
from adafruit_bluefruit_connect.button_packet import ButtonPacket
import audiocore
import audiomixer
import audiobusio

# Prep the status LEDs on the Feather
blue_led = digitalio.DigitalInOut(board.BLUE_LED)
red_led = digitalio.DigitalInOut(board.RED_LED)
blue_led.direction = digitalio.Direction.OUTPUT
red_led.direction = digitalio.Direction.OUTPUT

ble = BLERadio()
uart_service = UARTService()
advertisement = ProvideServicesAdvertisement(uart_service)

num_motors = 1  # up to 4 motors depending on the prop you are driving

i2c = board.I2C()  # uses board.SCL and board.SDA
# i2c = board.STEMMA_I2C()  # For using the built-in STEMMA QT connector on a microcontroller
motorwing = MotorKit(i2c=i2c)
motorwing.frequency = 122  # tune this 50 - 200 range
max_throttle = 0.65  # tune this 0.2 - 1 range

# # make arrays for all the things we care about
motors = [None] * num_motors

motors[0] = motorwing.motor1

# # set motors to "off"
for i in range(num_motors):
    motors[i].throttle = None

# - Audio setup
audio = audiobusio.I2SOut(bit_clock=board.TX, word_select=board.MISO, data=board.RX)
mixer = audiomixer.Mixer(voice_count=2, sample_rate=11025, channel_count=1,
                         bits_per_sample=16, samples_signed=True)
audio.play(mixer)  # attach mixer to audio playback
wav_files = (('spooky_ouija.wav', 0.07, True), ('lars_ouija.wav', 0.09, False))
# open samples
for i in range(len(wav_files)):
    wave = audiocore.WaveFile(open(wav_files[i][0], "rb"))
    mixer.voice[i].level = 0  # start up with level down
    mixer.voice[i].play(wave, loop=wav_files[i][2])

# - NeoPixels
fire_color = 0xcc6600
fade_by = -1
num_leds = 7
max_bright = 0.5
led_pin = board.D6
leds = neopixel.NeoPixel(led_pin, num_leds, brightness=0.0, auto_write=False)
leds.fill(fire_color)
leds.show()

last_time = 0
next_duration = 0.2

print("BLE Ouija board")
print("Use Adafruit Bluefruit app to connect")


while True:
    blue_led.value = False
    ble.name = 'Ouija'
    ble.start_advertising(advertisement)

    while not ble.connected:
        # Wait for a connection.
        pass
    blue_led.value = True  # turn on blue LED when connected
    while ble.connected:
        if uart_service.in_waiting:
            # Packet is arriving.
            red_led.value = False  # turn off red LED
            packet = Packet.from_stream(uart_service)
            if isinstance(packet, ButtonPacket) and packet.pressed:
                red_led.value = True  # blink to show a packet has been received
                if packet.button == ButtonPacket.RIGHT:  # > button pressed
                    print("forward")
                    motors[0].throttle = max_throttle
                    time.sleep(0.1)  # wait a moment

                elif packet.button == ButtonPacket.LEFT:  # < button
                    print("reverse")
                    motors[0].throttle = max_throttle * -1
                    time.sleep(0.1)

                elif packet.button == ButtonPacket.DOWN:  # v button
                    print("stop")
                    motors[0].throttle = None
                    time.sleep(0.1)

                elif packet.button == ButtonPacket.BUTTON_1:  # 1 button
                    print("on BG music")
                    mixer.voice[0].level = wav_files[0][1]
                    time.sleep(0.1)

                elif packet.button == ButtonPacket.BUTTON_3:  # 3 button
                    print("off BG music")
                    mixer.voice[0].level = 0.0
                    time.sleep(0.1)

                elif packet.button == ButtonPacket.BUTTON_2:  # 2 button
                    print("on led & Lars")
                    leds.brightness = max_bright
                    mixer.voice[1].play(wave, loop=False)
                    mixer.voice[1].level = wav_files[1][1]
                    time.sleep(0.1)

                elif packet.button == ButtonPacket.BUTTON_4:  # 4 button
                    print("off led & Lars")
                    leds.brightness = 0
                    mixer.voice[1].level = 0.0
                    time.sleep(0.1)

        # fade down all LEDs
        leds[:] = [[min(max(i+fade_by, 0), 255) for i in l] for l in leds]
        leds.show()

        # add new fire to leds
        if time.monotonic() - last_time > next_duration:
            last_time = time.monotonic()
            next_duration = random.uniform(0.95, 1.95)  # tune these nums
            # for i in range( 1):
            c = fire_color  # get our color
            c = (c >> 16 & 0xff, c >> 8 & 0xff, c & 0xff)  # make it a tuple
            leds[random.randint(0, num_leds-1)] = c

How It Works

The code imports a number of libraries needed to use the board pins, NeoPixels, BLE, Motor FeatherWing, and audio.

import time
import random
import board
import digitalio
import neopixel
from adafruit_motorkit import MotorKit
from adafruit_ble import BLERadio
from adafruit_ble.advertising.standard import ProvideServicesAdvertisement
from adafruit_ble.services.nordic import UARTService
from adafruit_bluefruit_connect.packet import Packet
from adafruit_bluefruit_connect.button_packet import ButtonPacket
import audiocore
import audiomixer
import audiobusio

Setup

Next, the code does the setup for LED indicators, BLE, motor driving, audio, and NeoPixel animation.

blue_led = digitalio.DigitalInOut(board.BLUE_LED)
red_led = digitalio.DigitalInOut(board.RED_LED)
blue_led.direction = digitalio.Direction.OUTPUT
red_led.direction = digitalio.Direction.OUTPUT

ble = BLERadio()
uart_service = UARTService()
advertisement = ProvideServicesAdvertisement(uart_service)

num_motors = 1  # up to 4 motors depending on the prop you are driving

motorwing = MotorKit(i2c=board.I2C())
motorwing.frequency = 122  # tune this 50 - 200 range
max_throttle = 0.65  # tune this 0.2 - 1 range

# # make arrays for all the things we care about
motors = [None] * num_motors

motors[0] = motorwing.motor1

# # set motors to "off"
for i in range(num_motors):
    motors[i].throttle = None

# - Audio setup
audio = audiobusio.I2SOut(bit_clock=board.TX, word_select=board.MISO, data=board.RX)
mixer = audiomixer.Mixer(voice_count=2, sample_rate=11025, channel_count=1,
                         bits_per_sample=16, samples_signed=True)
audio.play(mixer)  # attach mixer to audio playback
wav_files = (('spooky_ouija.wav', 0.07, True), ('lars_ouija.wav', 0.09, False))
# open samples
for i in range(len(wav_files)):
    wave = audiocore.WaveFile(open(wav_files[i][0], "rb"))
    mixer.voice[i].level = 0  # start up with level down
    mixer.voice[i].play(wave, loop=wav_files[i][2])

# - NeoPixels
fire_color = 0xcc6600
fade_by = -1
num_leds = 7
max_bright = 0.5
led_pin = board.D6
leds = neopixel.NeoPixel(led_pin, num_leds, brightness=0.0, auto_write=False)
leds.fill(fire_color)
leds.show()

Main Loop

The main loop of the program establishes the BLE advertisement an waits for a connection with the Bluefruit program.

Once connected, the UART service waits to receive packets of button presses. These are translated into motor movement in both directions, motor stop, and enabling or disabling the audio tracks and NeoPixel animations.

while True:
    blue_led.value = False
    ble.name = 'Ouija'
    ble.start_advertising(advertisement)

    while not ble.connected:
        # Wait for a connection.
        pass
    blue_led.value = True  # turn on blue LED when connected
    while ble.connected:
        if uart_service.in_waiting:
            # Packet is arriving.
            red_led.value = False  # turn off red LED
            packet = Packet.from_stream(uart_service)
            if isinstance(packet, ButtonPacket) and packet.pressed:
                red_led.value = True  # blink to show a packet has been received
                if packet.button == ButtonPacket.RIGHT:  # > button pressed
                    print("forward")
                    motors[0].throttle = max_throttle
                    time.sleep(0.1)  # wait a moment

                elif packet.button == ButtonPacket.LEFT:  # < button
                    print("reverse")
                    motors[0].throttle = max_throttle * -1
                    time.sleep(0.1)

                elif packet.button == ButtonPacket.DOWN:  # v button
                    print("stop")
                    motors[0].throttle = None
                    time.sleep(0.1)

                elif packet.button == ButtonPacket.BUTTON_1:  # 1 button
                    print("on BG music")
                    mixer.voice[0].level = wav_files[0][1]
                    time.sleep(0.1)

                elif packet.button == ButtonPacket.BUTTON_3:  # 3 button
                    print("off BG music")
                    mixer.voice[0].level = 0.0
                    time.sleep(0.1)

                elif packet.button == ButtonPacket.BUTTON_2:  # 2 button
                    print("on led & Lars")
                    leds.brightness = max_bright
                    mixer.voice[1].play(wave, loop=False)
                    mixer.voice[1].level = wav_files[1][1]
                    time.sleep(0.1)

                elif packet.button == ButtonPacket.BUTTON_4:  # 4 button
                    print("off led & Lars")
                    leds.brightness = 0
                    mixer.voice[1].level = 0.0
                    time.sleep(0.1)

Animation

The fade animation on the NeoPixels is run at all times, with the BLE packets only controlling the brightness.

# fade down all LEDs
        leds[:] = [[min(max(i+fade_by, 0), 255) for i in l] for l in leds]
        leds.show()

        # add new fire to leds
        if time.monotonic() - last_time > next_duration:
            last_time = time.monotonic()
            next_duration = random.uniform(0.95, 1.95)  # tune these nums
            # for i in range( 1):
            c = fire_color  # get our color
            c = (c >> 16 & 0xff, c >> 8 & 0xff, c & 0xff)  # make it a tuple
            leds[random.randint(0, num_leds-1)] = c

This guide was first published on Nov 09, 2022. It was last updated on Apr 19, 2024.

This page (Code the Ouija Board Controller) was last updated on Apr 18, 2024.

Text editor powered by tinymce.