Installing the Project Code
Download a zip of the project by clicking Download Project Bundle below.
After unzipping the file, the contents to both of the CIRCUITPY drives (the second one appears as CIRCUITPY1 on some operating systems) which appear when the Circuit Playgrounds are connected to your computer with USB cables. After you've copied everything over, it should look something like this:

# SPDX-FileCopyrightText: Copyright (c) 2021 Eva Herrada for Adafruit Industries # SPDX-FileCopyrightText: Copyright (c) 2021 Dan Halbert for Adafruit Industries # # SPDX-License-Identifier: Unlicense """ Code for communicating between two CircuitPlayground Express boards using UART. Sends value from the onboard light sensor to the other board and the other board sets its NeoPixels accordingly. """ import time import board import busio import digitalio import neopixel import analogio pixels = neopixel.NeoPixel(board.NEOPIXEL, 10, brightness=0.1, auto_write=False) light_sensor = analogio.AnalogIn(board.LIGHT) btn_A = digitalio.DigitalInOut(board.BUTTON_A) btn_A.switch_to_input(pull=digitalio.Pull.DOWN) btn_B = digitalio.DigitalInOut(board.BUTTON_B) btn_B.switch_to_input(pull=digitalio.Pull.DOWN) # Use a timeout of zero so we don't delay while waiting for a message. uart = busio.UART(board.TX, board.RX, baudrate=9600, timeout=0) # Messages are of the form: # "<TYPE,value,value,value,...>" # We send and receive two types of messages: # # Message contains a light sensor value (float): # <L,light> # # Message contains statuses of two buttons. Increment NeoPixel brightness by 0.1 if the second # button is pressed, and reduce brightness by 0.1 if the first button is pressed. # <B,btn_A,btn_B> UPDATE_INTERVAL = 3.0 last_time_sent = 0 # Wait for the beginning of a message. message_started = False while True: # Send light sensor value only every UPDATE_INTERVAL seconds. now = time.monotonic() if now - last_time_sent >= UPDATE_INTERVAL: light = light_sensor.value uart.write(bytes(f"<L,{light}>", "ascii")) print("sending light value", light) last_time_sent = now if any((btn_A.value, btn_B.value)): # Send values of built-in buttons if any are pressed uart.write(bytes(f"<B,{btn_A.value},{btn_B.value}>", "ascii")) print(f"Sent ({btn_A.value}, {btn_B.value})") # Don't do anything else until both buttons are released time.sleep(0.1) while any((btn_A.value, btn_B.value)): pass byte_read = uart.read(1) # Read one byte over UART lines if not byte_read: # Nothing read. continue if byte_read == b"<": # Start of message. Start accumulating bytes, but don't record the "<". message = [] message_started = True continue if message_started: if byte_read == b">": # End of message. Don't record the ">". # Now we have a complete message. Convert it to a string, and split it up. print(message) message_parts = "".join(message).split(",") message_type = message_parts[0] message_started = False if message_parts[0] == "L": # Received a message telling us a light sensor value peak = int(((int(message_parts[1]) - 2000) / 62000) * 10) for i in range(0, 10): if i <= peak: pixels[i] = (0, 255, 0) else: pixels[i] = (0, 0, 0) pixels.show() print(f"Received light value of {message_parts[1]}") print(f"Lighting up {peak + 1} NeoPixels") elif message_parts[0] == "B": # Received a message asking us to change our brightness. if message_parts[1] == "True": pixels.brightness = max(0.0, pixels.brightness - 0.1) print(f"Brightness set to: {pixels.brightness}") if message_parts[2] == "True": pixels.brightness = min(1.0, pixels.brightness + 0.1) print(f"Brightness set to: {pixels.brightness}") else: # Accumulate message byte. message.append(chr(byte_read[0]))
If you open up the REPL for both boards in two separate windows, you can see messages being passed back and forth like this:
import time import board import busio import digitalio import neopixel import analogio
Next, it sets up the NeoPixel and UART, as well as the buttons for controlling the brightness.
pixels = neopixel.NeoPixel(board.NEOPIXEL, 10, brightness=0.1, auto_write=False) light_sensor = analogio.AnalogIn(board.LIGHT) btn_A = digitalio.DigitalInOut(board.BUTTON_A) btn_A.switch_to_input(pull=digitalio.Pull.DOWN) btn_B = digitalio.DigitalInOut(board.BUTTON_B) btn_B.switch_to_input(pull=digitalio.Pull.DOWN) # Use a timeout of zero so we don't delay while waiting for a message. uart = busio.UART(board.TX, board.RX, baudrate=9600, timeout=0)
Before the code starts the main loop, it needs to define a few variables it will be using. The first one is a variable for how often to send a new message over the UART bus. The second variable will store the last time that a message was sent, and the final one is used to
UPDATE_INTERVAL = 3.0 last_time_sent = 0 # Wait for the beginning of a message. message_started = False
Now, the code enters the main loop. It first checks to see how long since the last packet was sent, and if it has been long enough, it writes the packet containing the value from the light sensor to the other board over UART.
while True: # Send light sensor value only every UPDATE_INTERVAL seconds. now = time.monotonic() if now - last_time_sent >= UPDATE_INTERVAL: light = light_sensor.value uart.write(bytes(f"<L,{light}>", "ascii")) print("sending light value", light) last_time_sent = now
If any of the built-in buttons are pressed, the code will send a different packet to the other device telling it the status of both of the buttons. This packet will be used to set the brightness of the NeoPixels.
⠀⠀⠀⠀if any((btn_A.value, btn_B.value)): # Send values of built-in buttons if any are pressed uart.write(bytes(f"<B,{btn_A.value},{btn_B.value}>", "ascii")) print(f"Sent ({btn_A.value}, {btn_B.value})") # Don't do anything else until both buttons are released time.sleep(0.1) while any((btn_A.value, btn_B.value)): pass
The code now reaches the part where it tries to read a packet over the UART bus. It first tries to read one byte. If it doesn't read one, it will run the main loop over again.
Assuming it has received a byte, the code then goes on to check if the byte received is a "<", which is the defined starting byte. If it has received this byte, it then clears the message
variable, and sets the variable to indicate that it is in the process of reading a packet.
Now that the packet is being read, it checks to see if it has reached the final byte. Up until then, it will go through and keep adding to the message
variable. When it has reached the end, it prints the message, and sets a few variables to make reading the message a bit easier.
⠀⠀⠀byte_read = uart.read(1) # Read one byte over UART lines ⠀⠀⠀if not byte_read: # Nothing read. continue if byte_read == b"<": # Start of message. Start accumulating bytes, but don't record the "<". message = [] message_started = True continue if message_started: if byte_read == b">": # End of message. Don't record the ">". # Now we have a complete message. Convert it to a string, and split it up. print(message) message_parts = "".join(message).split(",") message_type = message_parts[0] message_started = False
Now that the packet is easily readable, the code checks to see what type of packet it is. If it's a light sensor value, it converts the light sensor value to an integer out of 10. It then turns on the NeoPixels based on that value.
If the packet contains instructions for brightness, the code then raises or lowers the brightness of the NeoPixels accordingly.
At the end of the file, and moving out one level, the code adds the byte it has just read to the message
variable.
⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀if message_parts[0] == "L": # Received a message telling us a light sensor value peak = int(((int(message_parts[1]) - 2000) / 62000) * 10) for i in range(0, 10): if i <= peak: pixels[i] = (0, 255, 0) else: pixels[i] = (0, 0, 0) pixels.show() print(f"Received light value of {message_parts[1]}") print(f"Lighting up {peak + 1} NeoPixels") elif message_parts[0] == "B": # Received a message asking us to change our brightness. if message_parts[1] == "True": pixels.brightness = max(0.0, pixels.brightness - 0.1) print(f"Brightness set to: {pixels.brightness}") if message_parts[2] == "True": pixels.brightness = min(1.0, pixels.brightness + 0.1) print(f"Brightness set to: {pixels.brightness}") else: # Accumulate message byte. message.append(chr(byte_read[0]))