Bluetooth, simulated

Pair a phone with your ESP32 or HC-05 and talk to it. The phone is the Bluetooth pane in Mokxi, not a real radio.

Simulated Bluetooth: a phone in Mokxi, not a real radio. Nothing in Mokxi transmits anything, and no real phone can see a board here. What is faithful is the Arduino code you write: the ESP32 core's own Bluetooth examples run unchanged, and the pane does what the phone apps those tutorials use do.

Where the phone is

Press Run, then open the Bluetooth tab beside Serial (on a phone it is under More). The pane lists every device the phone can see:

  • an ESP32 running BluetoothSerial (Bluetooth Classic, the classic ESP32 only),
  • an ESP32 advertising over Bluetooth LE (the ESP32, ESP32-S3, C3 and C6 with an Arduino sketch, and the ESP32-C3 and C6 with MicroPython),
  • every HC-05 and HC-06 module in the circuit, on any board.

Pick one, pair, connect, and the right side of the pane becomes the app.

Bluetooth serial: the ESP32's SerialBT

The tutorial sketch runs as written:

#include "BluetoothSerial.h"
BluetoothSerial SerialBT;
void setup() { Serial.begin(115200); SerialBT.begin("ESP32test"); }
void loop() {
  if (Serial.available()) SerialBT.write(Serial.read());
  if (SerialBT.available()) Serial.write(SerialBT.read());
}

ESP32test shows up in the pane. Press Pair, then Connect, and the pane is a terminal like the "Serial Bluetooth Terminal" app: type a line and send it (with CR+LF, LF, CR or nothing on the end), and whatever the sketch writes to SerialBT appears under it.

What the sketch sees is what it sees on a real board:

  • register_callback() gets ESP_SPP_INIT_EVT and ESP_SPP_START_EVT from begin(), ESP_SPP_SRV_OPEN_EVT when the phone connects (with the phone's address), ESP_SPP_DATA_IND_EVT for each line it sends, and ESP_SPP_CLOSE_EVT when it disconnects;
  • hasClient() and connected() follow the link, and write() returns 0 with nobody connected;
  • available(), read(), peek(), readString() and the rest of Stream read what the phone sent, and onData() takes it instead when it is set.

Pairing the way your sketch asks

  • Just works is the default: Pair succeeds at once.
  • A PIN: after SerialBT.disableSSP() and SerialBT.setPin("1234"), the pane asks for the PIN and a wrong one fails, calling onAuthComplete(false).
  • Numeric comparison: with SerialBT.enableSSP() and onConfirmRequest(), the pane shows a six-digit code and the sketch's callback gets the same number. Both sides have to say yes: the pane's Pair button, and the sketch's confirmReply(true). Then onAuthComplete(true) runs.
  • With enableSSP(true, false) the board types the code the pane shows (onKeyRequest(), then respondPasskey()); with enableSSP(false, true) the board shows it and you type it into the pane.

Forget unpairs, as a phone's settings do.

Bluetooth LE: the ESP32's BLE server

The ESP32 core's BLE_server, BLE_notify and BLE_uart examples run unchanged. BLEDevice::startAdvertising() makes the board visible; the pane is then a scanner like nRF Connect:

  • Connect, and the services and characteristics the sketch made are listed with their UUIDs, their names where a phone knows them (the Nordic UART Service, Battery Level and so on) and their properties;
  • Read runs the characteristic's onRead() first and shows the value as text and as hex;
  • Write sends text or hex, and the sketch's onWrite() runs with it;
  • Notify subscribes, and every notify() from then on shows up with its value. Before you subscribe, notify() reaches nobody, as on a real phone.

onConnect() and onDisconnect() run when the pane connects and disconnects. The board stops advertising once connected, so it disappears from the list after a disconnect until the sketch calls startAdvertising() again. That is why the examples do it in loop().

Bluetooth LE in MicroPython: the ESP32-C3 and C6

On an ESP32-C3 or ESP32-C6 running MicroPython, the bluetooth module works as on the board, because the board's own Bluetooth software runs: only the radio chip's part is stood in for. A GATT server like this one shows up in the pane:

import bluetooth
b = bluetooth.BLE()
b.active(True)
F = bluetooth.FLAG_READ | bluetooth.FLAG_WRITE | bluetooth.FLAG_NOTIFY
((h,),) = b.gatts_register_services(((bluetooth.UUID(0x180F), ((bluetooth.UUID(0x2A19), F),)),))
b.gatts_write(h, b'42')
b.gap_advertise(100000, bytes([2, 1, 6, 8, 9]) + b'mpy-ble')
  • The pane lists the board by the name in its advertising data.
  • Connect, and your irq handler gets _IRQ_CENTRAL_CONNECT (1). The pane then finds the services the way a phone app does, so it lists the board's Generic Access and Generic Attribute services before yours.
  • Read shows what gatts_write() stored, Write fires _IRQ_GATTS_WRITE (3), and after Notify, every gatts_notify() shows up.
  • aioble works too: add micropython-lib's aioble folder to your project, as you would copy it onto a real board.
  • gap_scan() ends without finding anything, because the phone does not advertise, and the board cannot connect to the phone as a central.

HC-05 and HC-06 on any board

The modules are serial bridges: whatever the phone sends comes out of TXD, at the module's baud rate, bit by bit, and what the board sends into RXD goes to the phone. In the pane each one is listed by its name. Pair with its PIN (1234 until AT+PSWD or AT+PIN changes it), connect, and use the same terminal. Clicking the module on the canvas connects or hangs up too.

An HC-05 starts connected, so the serial monitor's box for it works at once. An HC-06 starts with nothing connected, as one out of the bag does, and while nothing is connected it answers AT commands. The wireless serial guide has both modules' AT commands.

What is real and what is not

Real, in the sense that it behaves the way your board will: the calls and their signatures, the order the callbacks come in, pairing that has to succeed before anything connects, a PIN or a code that has to match, the bytes each way, the GATT table, and a notification only after the phone subscribes.

Not real, and never pretended to be:

  • no radio. No 2.4 GHz, no RFCOMM, no advertising channels, no range, no link keys, no encryption. A PIN is compared as a string.
  • no delay of its own. A line arrives whole, at once; a real link takes tens of milliseconds and jitters. The callbacks run every 10 ms of simulated time while the sketch is in delay(), which is about where a real board's Bluetooth task gets in.
  • no other devices. The phone is the only one. A board that scans (BLEScan) or connects as a client finds nothing, and SerialBT.connect() finds nothing to connect to.
  • no MTU exchange for an Arduino sketch: a value up to 512 bytes arrives whole. With MicroPython the phone exchanges the MTU with the board's own Bluetooth software, as a real phone does.
  • no pairing over LE with MicroPython: the phone says pairing is not supported.
  • NimBLE-Arduino and the Pico W's Bluetooth are not simulated.