Description
CircuitPython version and board name
Adafruit CircuitPython 10.0.0-alpha.4-19-gd52beb2e33-dirty on 2025-05-14; Adafruit Circuit Playground Bluefruit with nRF52840
Also tested with 9.2.7
Code/REPL
# Bluefruit Bluetooth Presentation Remote
# Bradán Lane STUDIO (c) 2025
# based on SPDX-FileCopyrightText
# 2020 John Park for Adafruit Industries
# License: MIT
"""
This example acts as a BLE HID keyboard to be used as a presentation remote
uses the Adafruit Circuit Playground Express nRF52840 with its 2 on-board buttons
each button will send a configurable keycode to mobile device or computer
the keycodes are available at: https://docs.circuitpython.org/projects/hid/en/latest/api.html
"""
import time
import board
import busio
import digitalio
import alarm
import microcontroller
# not sure why I get an error trying to use the fully qualified names vs importing the classes
from adafruit_ble import BLERadio as bleRadio
from adafruit_ble.services.standard.hid import HIDService as bleHIDService
from adafruit_hid.keyboard import Keyboard as hidKeyboard
from adafruit_hid.keycode import Keycode as hidKeycode
from adafruit_ble.advertising import Advertisement as bleAdvertisement
from adafruit_ble.advertising.standard import ProvideServicesAdvertisement as bleProvider
from adafruit_ble.services.standard.device_info import DeviceInfoService as bleDevice
class DebugMessenger:
# we setup a UART to print debug messages when not connected to USB
def __init__(self, debug = True):
self._debug_messages = True
self._uart = busio.UART(board.TX, board.RX, baudrate=115200, timeout=0)
def print(self, msg):
if not self._debug_messages:
return
print(msg)
self._uart.write(bytes(msg + "\n", "utf-8"))
class CPE:
def __init__(self, debug = True):
self._debug = DebugMessenger(debug = debug)
self.debug("CPE initing")
# KLUDGE if we detect waking, perform a full reset to work around the BLE keyboard issue
if microcontroller.cpu.reset_reason == microcontroller.ResetReason.DEEP_SLEEP_ALARM:
self.debug("woke from sleep")
time.sleep(0.5)
# uncomment the following line to use the workaround
#microcontroller.reset() # this only works with CP 10.0.0 alpha
else:
self.debug(f"reset reason: {microcontroller.cpu.reset_reason}")
self._led = digitalio.DigitalInOut(board.LED)
self._led.direction = digitalio.Direction.OUTPUT
self._led.value = 0
# the CPE buttons are active high
# we need to set them up as pull down inputs
# so we can detect when they are pressed
self._button_1 = digitalio.DigitalInOut(board.BUTTON_A)
self._button_1.switch_to_input(pull=digitalio.Pull.DOWN)
self._button_1_active = False
self._button_2 = digitalio.DigitalInOut(board.BUTTON_B)
self._button_2.switch_to_input(pull=digitalio.Pull.DOWN)
self._button_2_active = False
# we set the following but do not need to persist them
power_control = digitalio.DigitalInOut(board.D35) # D35
power_control.direction = digitalio.Direction.OUTPUT
power_control.value = 1 # disables the neopixels and sensors (ligh, thermister, microphone)
speaker_control = digitalio.DigitalInOut(board.SPEAKER_ENABLE)
speaker_control.direction = digitalio.Direction.OUTPUT
speaker_control.value = 0 # power off
# setup the HID and keyboard
self._hid = bleHIDService()
self._keyboard = hidKeyboard(self._hid.devices)
self._keycode = hidKeycode
self.debug("create BLE advertisement")
#device_info = DeviceInfoService(software_revision=adafruit_ble.__version__, manufacturer="Adafruit Industries")
self._ble_advert = bleProvider(self._hid)
# Advertise as "Keyboard" (0x03C1) or "Presentation Remote" (0x03CA) icon when pairing
# https://www.bluetooth.com/specifications/assigned-numbers/
self._ble_advert.appearance = int("0x3CA")
self._ble_advert.short_name = "CPE"
self._ble_response = bleAdvertisement()
self._ble_response.complete_name = "CircuitPython Clicker"
self._ble = bleRadio()
self._ble.name = "CircuitPython Clicker"
self._ble_connected = False
self.debug("CPE inited")
def __del__(self):
pass
def connect(self):
# check if we are connected to a BLE device
if not self._ble.connected and self._ble_connected:
self.debug("BLE connection lost")
self._ble_connected = False # we lost our connection
if self._ble.connected:
if self._ble_connected: # if we already know, then just return
return True
self.debug("BLE already connected")
self._ble_connected = True
self.LED = True
return True
self.debug("BLE ready but waiting")
self._ble.start_advertising(self._ble_advert, self._ble_response)
while not self._ble.connected:
self.LED = not self.LED
time.sleep(0.10)
pass
self.debug("BLE connected")
self._ble_connected = True
self.LED = True
def disconnect(self):
self._ble.stop_advertising()
if self._ble_connected:
for connection in self._ble.connections:
connection.disconnect()
self.debug("BLE disconnected")
self._ble_connected = False
self.LED = False
def conditional_sleep(self):
# if both buttons are pressed then go to sleep
if not self._button_1_active or not self._button_2_active:
return
self.debug("Request to sleep")
# blink LED while we wait for teh buttons to be released
while self._button_1.value or self._button_2.value:
self._led.value = not self._led.value
time.sleep(0.25)
self.debug("Prepare to Sleep")
self._ble.stop_advertising()
self._button_1.deinit()
self._button_2.deinit()
# IMPORTANT: can not use BUTTON_A
wake = alarm.pin.PinAlarm(pin=board.BUTTON_B, value=True, pull=True)
self.debug("Sleep now")
time.sleep(0.5)
alarm.exit_and_deep_sleep_until_alarms(wake)
# we need to release the buttons before we can set one as for wake
# end conditional_sleep
def debug(self, msg):
self._debug.print(msg)
@property
def LED(self):
return self._led.value
@LED.setter
def LED(self, value):
self._led.value = value
@property
def A(self):
if self._button_1.value:
if not self._button_1_active:
self._button_1_active = True
self.debug("A")
return True
else:
return False
else:
if self._button_1_active:
self._button_1_active = False
return False
@property
def B(self):
if self._button_2.value:
if not self._button_2_active:
self._button_2_active = True
self.debug("B")
return True
else:
return False
else:
if self._button_2_active:
self._button_2_active = False
return False
@property
def connected(self):
return self._ble.connected
def send(self, keycode):
if keycode is not None:
self.debug(f"send: {keycode}")
self.LED = False
self._keyboard.send(keycode)
time.sleep(0.1)
self.LED = True
# end of class CPE
# -------------------------------------------------------
# main program
# -------------------------------------------------------
cpe = CPE(debug = True)
try: # lets us capture CTRL-C, crashes, etc
while True:
cpe.connect() # this will wait if we are not currently connected, else it just returns
if cpe.A:
cpe.send(cpe._keycode.UP_ARROW)
if cpe.B:
cpe.send(cpe._keycode.DOWN_ARROW)
cpe.conditional_sleep()
# end while
except Exception as e:
cpe.debug(f"exception: {e}")
cpe.disconnect()
pass
finally:
cpe.debug("finally")
cpe.disconnect()
time.sleep(0.5)
pass
# end of file
Behavior
---- Opened the serial port /dev/tty.usbserial-14310 ----
CPE initing
reset reason: microcontroller.ResetReason.RESET_PIN
create BLE advertisement
CPE inited
BLE already connected
B
send: 81
B
send: 81
B
send: 81
A
send: 82
A
send: 82
A
send: 82
B
send: 81
Request to sleep
Prepare to Sleep
Sleep now
finally
BLE disconnected
CPE initing
woke from sleep
create BLE advertisement
CPE inited
BLE ready but waiting
BLE connected
B
send: 81
B
send: 81
A
send: 82
A
send: 82
Description
The hardware used in this example is the Adafruit Circuit Playground Express Bluefruit (CPEB). A LiPo is used to power the CPEB without the use of a USB connection to insure 'real' sleep/wake function. (the problem was discussed with @dhalbert and the temporary workaround descried below is from his recommendations)
In the example output, the code is performing all initializations.
Then, the buttons are used to send keycodes to the target computer.
(in this test the target is a MacBook Pro M2)
All output shown is from a UART serial connection to allow for debugging messages without USB connected.
After reset, the BLE connection is established and keycodes sent using keyboard.send()
are received
by the target computer and interpreted correct.
Next, the CPEB is placed into deep sleep. Then it wakes from a button press alarm.
Following wake from deep sleep, the code again performs all initialization.
Then, the buttons are used to send keycodes to the target computer.
(in this test the target is a MacBook Pro M2)
After wake, the BLE connection is established and keycodes sent using keyboard.send()
do nothing / have no effect by the target computer.
All debug messages are the same in both executions of other code (after reset and after wake).
As a temporary workaround, there is a commented out line to detect the reset_reason
. By performing a reset()
after wake from deep sleep, the code - including sending keycodes - works as expected.
Additional information
No response