WebBLE Dual Board Control - v16

Nano 33 BLE Sense (peripheral, inference-only) + XIAO ESP32S3 (dual role: central to Nano33, peripheral to this page)

Uses Web Bluetooth directly for the boards - Chrome or Edge on Windows/Mac/Linux/ChromeOS/Android only. Phone motion training below uses a separate, plain-web-standards API (DeviceMotionEvent) that also works in mobile Safari.

This page can train a model right here in the browser, using the identical Conv1D+Dense+Dense+Output architecture both boards run, then push the trained weights to either board. The Nano33 never trains - it only ever receives a finished model (from the ESP32, from this page, or baked in at compile time) and runs inference locally, reporting results back over BLE. Quick and full inference both work everywhere.

What's new in v16 - browser-only fix, no firmware changes needed (nano33-v14.ino / esp32-v14.ino still current):

What's new in v14.2:

What's new in v14.1:

What's new in v14 - requires nano33-v14.ino AND esp32-v14.ino:

What was new in v13:

What was new in v12: model pushes to the Nano33 switched from write-without-response to write-with-response (the browser now waits for each chunk's acknowledgement before sending the next, instead of flooding faster than the board could keep up), plus other commands to a board are held off while a push to it is in flight.

Nano 33 BLE Sense (peripheral, inference-only)

Name prefix:
Status: not connected



Custom command:

On-device result: no model loaded yet

Heartbeat: ...
Static context: ...

XIAO ESP32S3 (dual role: central + peripheral)

Name prefix:
Status: not connected





Paste Model From Serial (fallback if the Fetch button above keeps stalling)
On the ESP32's own USB Serial Monitor, type e and press enter. Select everything between (not including) the ===MODEL_EXPORT_BEGIN=== / ===MODEL_EXPORT_END=== lines, copy it, and paste it below - goes over USB instead of BLE, so it isn't affected by the stall above.


Custom command:

On-device result: no model loaded yet

Heartbeat: ...
Static context: ...

This Phone/Tablet (motion only, no BLE)

Uses the browser's built-in accelerometer + gyroscope to add a third data source to the dataset below. No magnetometer, mic, or static sensors on a phone, so those channels are sent as 0. Best for quick gesture-shape demos - see the code comments for details/caveats.



Status: not enabled yet

On-device result: n/a - a phone has no on-device model, this runs in the browser
- auto capture + predict, roughly once a second, until stopped

Static context: all 0 - phone has no barometer/temp/humidity/proximity/color sensor

Train In Browser (same architecture as both boards)

Choose a source and a class below, then hit "Capture Labeled Sample" - it now triggers a FRESH capture on that board (or the phone) itself and adds whatever comes back, so there's no need to separately press Capture/Quick on the panels above first. (Those panels' own buttons are still there if you just want to preview a reading, or to grab a quiet/neutral one for Step 1's calibration baseline below, without adding it to the dataset.) Once you've got a few samples per class, set a calibration baseline, train right here in the page, and push the result to either board.

Step 1 - Calibration Baseline (fixes the v02-v05 bug where browser-trained models didn't work on-device):
Capture one quiet/neutral reading above (board at rest / normal background), select that board below, then: not set - training will fall back to a less accurate method and warn in the log
Capture fresh sample from: as class: idle

Dataset: 0unknown: 0 1normal: 0 2issue: 0
View captured samples individually
#class sourcetime
No samples captured yet.

Step 2 - Train
Epochs: Learning rate:
Status: not trained yet
Step 3 - Model Info & Transfer
No model loaded in the browser yet.


Load Model From Hard Drive:

- bake the current model into either sketch at compile time (see USE_BAKED_WEIGHTS)

Log:


Protocol notes (must match the firmware exactly):
Service UUID: 7e400001-b2c3-5d4e-af60-9b3c7d8eaf20
Control char (write): 7e400002-... - browser sends "CAPTURE", "QUICK", "TRAIN", "INFER", "QUICKINFER", "PUSHTONANO", or "PUSHMODEL" as plain ASCII text (Nano33 only understands CAPTURE/QUICK/INFER/QUICKINFER - it never trains)
Heartbeat char (notify, ~2Hz): 7e400003-... - CSV text "ax,ay,az,mic,temp,hum,prox"
Binary char (notify, chunked) [CHANGED v14.2 - was indicate on the ESP32 side; the Nano33's binary char is still indicate, since its transfers are small (up to ~1.6KB) and haven't shown this issue]: 7e400004-... - 4-byte little-endian uint32 length header, then raw float32LE payload in ~180-byte chunks. 407 floats = full 1-second window, 17 floats = QUICK reading, 5872 floats = a full MODEL PACKAGE (only sent by the ESP32 in response to "PUSHMODEL").
Model char (write, chunked) [NEW v02]: 7e400005-... - same 4-byte-header + chunk pattern, browser -> board, carries a model package (numClasses + weights + calibration) into either board's live weights.
Result char (notify) [NEW v02]: 7e400006-... - CSV text "predIdx,conf,p0,p1,...,pN-1" sent after any INFER/QUICKINFER, by whichever board just ran the forward pass.
myFusionWeights.h [NEW v09]: not BLE - a plain-text C++ header download (Save myWeights.h button) with the same numbers as the .bin, for baking a model into either sketch at compile time (USE_BAKED_WEIGHTS).
Phone motion training [NEW v09]: not BLE either - uses the browser's own DeviceMotionEvent API on the phone/tablet the page is open on, feeding straight into the same labeled dataset as the two boards.
On-device result text format [CHANGED v14]: both boards' Serial Monitor AND this page's "On-device result" line now use the exact same format, produced by one shared function on each board (myPrintResultLine in both .ino files): "On-device result: 1normal (0unknown=3%, 1normal=90%, 2issue=7%,)" - the trailing comma before the closing paren is intentional (falls out of the shared per-class loop), kept so the two are byte-for-byte identical.

Serial Monitor commands (type these into each board's own USB Serial Monitor at 115200 baud - these are NOT sent over BLE, they only work with a USB cable plugged into that specific board):

Nano33 (nano33-v14.ino) - no training on this board by design:
  i = Infer once/continuous (real 1s window, 'x' to stop)
  q = Quick continuous infer (fast replicated reading, 'x' to stop)
  s = status (BLE connection + model-loaded state)
  d = toggle debug output [NEW v14] (verbose per-timestep sampling lines)
  z = reset (restart the board)
  ? = show this menu

ESP32S3 (esp32-v14.ino):
  1-3 = Collect a sample for that class (then c to capture, x to exit)
  t = Train (on-device, from SD-collected samples)
  i = Infer once/continuous (real 1s window, 'x' to stop)
  q = Quick continuous infer (fast replicated reading, 'x' to stop)
  r = (re)connect to the Nano33
  k = recalibrate
  s = status
  p = push the current model to the Nano33 for on-device inference
  e = export the current model as pasteable hex text [NEW v14.2] - BLE-free fallback, see "Paste Model From Serial" on this page
  d = toggle debug output (verbose BLE/transfer diagnostics)
  b = cycle BLE mode (Dual / Peripheral only / Central only / Off)
  z = restart now, to apply a newly-selected BLE mode
  ? = show this menu

The Control char commands above ("CAPTURE", "QUICK", etc) are the separate BLE-over-the-air command set this webpage actually sends - use the "Custom command" box on either board's panel to send any of these, or the single letters 'r'/'k'/'s' (the ESP32 also accepts those same letters over BLE, not just USB Serial).


By Jeremy Ellis @rocksetta
Use at your own risk!