Skip to content

keyboard.send() not working with BLE after deep sleep #10348

Closed
@bradanlane

Description

@bradanlane

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

Metadata

Metadata

Assignees

Type

No type

Projects

No projects

Relationships

None yet

Development

No branches or pull requests

Issue actions