Open-source, low-cost BLE adaptive switch interfaces for iPadOS/iOS Switch Control and Android Switch Access.
View the Project on GitHub mrhunsaker/Assistive_Switch_Interfaces
ESP32-S3 SuperMini · BLE HID Keyboard · Adaptive Switch Controller for Assistive Technology
A Bluetooth Low Energy (BLE) HID keyboard designed for adaptive access applications. The device presents 11 independent inputs as unique HID keyboard events, enabling accessibility frameworks such as iPadOS/iOS Switch Control and Android Switch Access to identify and map each switch independently.
The firmware is optimized for assistive technology deployments where reliability, low latency, and simple configuration are more important than high input density. GPIO 3 is intentionally excluded from the design to avoid any reset-time strapping behavior.
📄 Source:
BLE_ADAPTIVE_SWITCH_EXPANDED_SCAFFOLD/in the repository
This section describes the real, current behavior of BLE_ADAPTIVE_SWITCH_EXPANDED_SCAFFOLD.ino, so anyone extending the project knows exactly what they’re building on.
INPUT, 4 switch pins as INPUT_PULLUP, starts I²C for the proximity sensor, initializes the NeoPixel, and starts a NimBLE BLE HID keyboard server advertising as BT Switch.digitalRead). TTP223 modules drive their output HIGH on touch, so no inversion is needed.PROX_THRESHOLD to produce a simple near/far boolean.INPUT_PULLUP — a switch closure pulls the pin LOW, and the firmware treats LOW as “pressed.”updateBtn) that only reports a change once the signal has been stable for 35 ms. This is what prevents a noisy or bouncy sensor from sending dozens of keystrokes per touch.sendKey(), which writes directly to the BLE HID input report characteristic and notifies the connected host. If nothing is connected, the function logs that it skipped the send and does nothing else — no queuing, no buffering.fadeStep() call each loop nudges the LED’s current RGB value one step closer to the target, producing a smooth fade rather than a hard snap. 250 ms after the last event, the target resets to off, and the LED fades back to black.NimBLEServerCallbacks subclass restarts advertising any time the host disconnects, so the device doesn’t need a manual re-pair after every disconnect (only after the host “forgets” it).0x180F) is created and pinned at 100%. This isn’t cosmetic — some Android devices (including the one this firmware was verified against) will not finish pairing with a BLE HID keyboard that omits it.In short: 11 physical inputs → 1 shared debounce/edge-trigger pipeline → 1 of 12 possible HID keycodes (F1–F6, F8–F12) → BLE notify → LED color tells you which category of input just fired.
35 ms debounce protection
| Platform | Status |
|---|---|
| iPadOS Switch Control | Verified |
| iOS Switch Control | Verified |
| Android Switch Access | Verified |
| Windows | Verified |
| macOS | Verified |
| Linux | Verified |
| ChromeOS | Anticipated |
The current firmware has been successfully tested on:
The device paired as a standard BLE keyboard and successfully generated switch events without custom middleware or application software. The Battery Service described above was required for this pairing to succeed.
| Qty | Component |
|---|---|
| 1 | ESP32-S3 SuperMini |
| Up to 6 | TTP223 capacitive touch sensors |
| 1 | Supported I²C proximity sensor |
| Up to 4 | Adaptive mechanical switches |
| 1 | USB-C cable |
| 1 | 3.7V 1100mAh Lithium polymer Battery (optional) |
| 1 | TP4056 Type-C USB 5V 1A 18650 Lithium Battery Charging Board with Dual Protection (optional) |
| Input | GPIO | HID Key | LED Color |
|---|---|---|---|
| Touch 1 | GPIO 1 | F1 | Green |
| Touch 2 | GPIO 2 | F2 | Green |
| Touch 3 | GPIO 4 | F3 | Green |
| Touch 4 | GPIO 5 | F4 | Green |
| Touch 5 | GPIO 6 | F5 | Green |
| Touch 6 | GPIO 7 | F6 | Green |
| Interface | GPIO | HID Key | LED Color |
|---|---|---|---|
| I²C | SDA=8 SCL=9 | F8 | Red |
| Input | GPIO | HID Key | LED Color |
|---|---|---|---|
| Switch 1 | GPIO 10 | F9 | Blue |
| Switch 2 | GPIO 11 | F10 | Blue |
| Switch 3 | GPIO 12 | F11 | Blue |
| Switch 4 | GPIO 13 | F12 | Blue |
The NeoPixel uses a deliberately simple 3-color scheme based on input type, not one unique color per sensor. This is the easiest scheme for classroom staff to memorize and is the current, correct behavior of the firmware:
| LED Color | Meaning |
|---|---|
| Green | A touch sensor was activated (any of F1–F6) |
| Red | The proximity sensor was activated (F8) |
| Blue | A mechanical switch / mono-jack input was activated (any of F9–F12) |
| Off | Idle — no input in the last 250 ms, or not yet powered |
If you need to tell which specific touch sensor or switch fired, use the Serial Monitor at 115200 baud — every activation is also logged there with its GPIO number and assigned key.
| GPIO | Reason |
|---|---|
| 0 | BOOT strapping pin |
| 19 | USB D− |
| 20 | USB D+ |
| 45 | VDD_SPI strapping |
| 46 | Input-only boot strapping |
| 48 | NeoPixel LED in use |
GPIO 3 is intentionally not used.
Although functional as a digital input, it is a reset-time strapping pin. Eliminating GPIO 3 from the design avoids deployment issues and simplifies support for educational and assistive technology environments. There is no “Touch 7” / F7 input as a result — F7 is intentionally unused.
| TTP223 Pin | Connection |
|---|---|
| VCC | 3V3 |
| GND | GND |
| SIG | GPIO 1, 2, 4, 5, 6, or 7 on the ESP32-S3 SuperMini (one sensor per pin) |
| Sensor Pin (APDS-9930 or APDS-9960) | Connection |
|---|---|
| VCC | 3V3 |
| GND | GND |
| SDA | GPIO 8 on ESP32-S3 SuperMini |
| SCL | GPIO 9 on ESP32-S3 SuperMini |
Each switch connects between:
Inputs use INPUT_PULLUP internally, so no external resistor is required — a simple normally-open switch wired to ground is all that’s needed.
| Source | HID Key | LED Color |
|---|---|---|
| Touch 1 (TTP223) | F1 | Green |
| Touch 2 (TTP223) | F2 | Green |
| Touch 3 (TTP223) | F3 | Green |
| Touch 4 (TTP223) | F4 | Green |
| Touch 5 (TTP223) | F5 | Green |
| Touch 6 (TTP223) | F6 | Green |
| Proximity (APDS-9930/9960/VCNL4040/VL53L0X) | F8 | Red |
| Switch 1 (Mono Jack Input) | F9 | Blue |
| Switch 2 (Mono Jack Input) | F10 | Blue |
| Switch 3 (Mono Jack Input) | F11 | Blue |
| Switch 4 (Mono Jack Input) | F12 | Blue |
Note: F7 is intentionally unused (it was historically reserved for a 7th touch sensor on GPIO 3, which this design deliberately omits).
Default = BT Switch
To change it, edit this line in the .ino file before flashing:
NimBLEDevice::init("BT Switch");
0x180F) reporting 100% — required for reliable pairing with some Android devicesThe firmware scaffold includes support for:
#define USE_APDS9930 near the top of the .ino file)Only one sensor family can be active per build. Open the .ino file and make sure exactly one of the following lines is uncommented, with the rest commented out:
#define USE_APDS9930 // <-- Enable APDS9930
// #define USE_VCNL4040
// #define USE_VL53L0X
// #define USE_APDS9960
Sensor-specific initialization may require enabling the appropriate library and code section, and you should review PROX_I2C_ADDR / PROX_THRESHOLD for your specific sensor — the default threshold (200 out of 0–255) is tuned for the APDS-9930/9960.
Required:
NimBLE-ArduinoAdafruit NeoPixelWire (bundled with the ESP32 Arduino core)Optional (sensor dependent — install only the one matching your #define):
SparkFun APDS9960 (covers both APDS-9930 and APDS-9960)Adafruit VCNL4040Adafruit VL53L0XKnown build quirk: the sketch includes
<HIDKeyboardTypes.h>, but no named constant from that header is actually referenced anywhere in the code (all keycodes are sent as raw hex values, e.g.0x3A). If your Arduino IDE reportsHIDKeyboardTypes.h: No such file or directory, the simplest fix is to delete that one#includeline — nothing else in the sketch depends on it.
See the repository-wide pinned library versions for the exact versions this project has been verified against.
Arduino IDE (Tools → Board → Boards Manager, search “esp32”, install ESP32 by Espressif Systems, version 2.0.17 — see the board package note).Tools → Board → ESP32 Arduino → ESP32S3 Dev Module.BLE_ADAPTIVE_SWITCH_EXPANDED_SCAFFOLD.ino.types_expanded.h is present in the same folder (it should open in an adjacent tab automatically).Sketch → Include Library → Manage Libraries.Tools → Port.Tools → Serial Monitor and set the baud rate to 115200.=== SETUP START === through === SETUP COMPLETE ===).Switch Access from the Google Play Store (if not already installed/enabled).Switch Access.After flashing and pairing, confirm all three input types before deploying the device:
If a category doesn’t respond, check the Serial Monitor output for that input before assuming a hardware fault — it will print the GPIO and keycode for every successful activation, and an I²C error code if the proximity sensor isn’t responding.
=== BLE READY, ADVERTISING ===).DEBOUNCE_MS in the .ino file, default 35 ms).#define USE_... sensor line is active and it matches your physical sensor.PROX_I2C_ADDR) against your sensor’s datasheet.HIDKeyboardTypes.h Not Found#include <HIDKeyboardTypes.h> line near the top of the sketch. No keycodes in this firmware are referenced by name from that header.EXPANDED_SCAFFOLD firmware (11 inputs: 6 touch, 1 proximity, 4 switch) instead of an older 3+3 input revision.HIDKeyboardTypes.h build quirk and its fix.