Biosignal collection firmware for the Bangle.js 2 smartwatch. Captures PPG (heart rate), accelerometer, and temperature data in time-synchronized chunks, stores them on-device, and transmits to the phone via BLE.
bangle/
├── README.md # This file
├── biosignalBangle.js # Production bundle (upload this to watch)
└── segmentedBangle/ # Modular source code
├── package.json
├── src/
│ ├── 00_prelude.js # Globals, CONFIG, constants
│ ├── 01_logging.js # Log helpers, debug persistence
│ ├── 02_state.js # MODE enum, state object, queue
│ ├── 03_buffers.js # Binary buffer allocation/writers
│ ├── 04_sensors.js # HRM, accel, temp handlers
│ ├── 05_window.js # Recording lifecycle
│ ├── 06_ui.js # Display, menus, buttons
│ ├── 07_ble.js # BLE protocol, commands, transfers
│ └── 08_init.js # Initialization, event binding
├── tools/
│ └── bundle.js # Simple concatenation bundler
└── dist/
└── biosignal.bundle.js # Local bundle output
- Node.js (v14 or later) - Download
- Chrome or Edge browser - Required for Web Bluetooth support
- Bangle.js 2 watch - Buy here
cd segmentedBangle
npm installnpm run bundleThis creates biosignalBangle.js in the parent folder.
- Open the Espruino Web IDE in Chrome/Edge (requires Web Bluetooth)
- Click the Connect button (yellow plug icon, top-left)
- Select Web Bluetooth from the popup
- Choose your Bangle.js 2 from the device list and click Pair
- Once connected, click the Open File button (folder icon, middle of toolbar)
- Navigate to and select
biosignalBangle.js - Click the Send to Espruino button (chip/upload icon, top-middle) to flash
The watch will buzz and display "BIOSIG" when the firmware is running.
In the Espruino IDE console (left panel), type:
X({type: "get_status"})You should see a JSON response with the current watch status.
- Device: Bangle.js 2 (BANGLEJS2)
- Firmware: Espruino 2v28+
- SoC: Nordic nRF52840 (64MHz ARM Cortex-M4F)
- RAM: 256KB (JS heap ~30-50KB)
- Flash: 8MB (user storage ~4MB)
- BLE: Bluetooth 5.0, Nordic UART Service (NUS)
The firmware uses continuous recording mode which captures biosignals in 5-minute chunks. When recording starts, the watch continuously collects data and saves each chunk to storage. Chunks are automatically queued for upload to the phone.
| Parameter | Value |
|---|---|
| Chunk duration | 5 minutes |
| Trigger | BLE command or button long-press |
| Sensor | Rate | Sync Interval | Binary Format |
|---|---|---|---|
| PPG (HRM) | 25 Hz | 1 second | uint32 timestamp + 25×uint16 |
| Accelerometer | 12 Hz | 1 second | uint32 timestamp + 12×(int16 x,y,z) |
| Temperature | 0.1 Hz | 10 seconds | uint32 timestamp + float32 |
Each recording chunk creates 4 files:
biosig_<windowId>_m.json # Manifest (metadata)
biosig_<windowId>_ppg.bin # PPG binary data
biosig_<windowId>_acc.bin # Accelerometer binary data
biosig_<windowId>_tmp.bin # Temperature binary data
Note: Filenames use short suffixes to stay within Bangle.js Storage's 28-character limit.
- Service: Nordic UART Service (NUS)
- Format: JSON commands over BLE UART, newline-delimited
- Binary: Chunked transfers with JSON header/end markers
| Command | Description |
|---|---|
start |
Start recording |
stop |
Stop recording |
get_queue |
Get list of pending uploads |
get_manifest |
Get chunk metadata {windowId} |
get_ppg |
Request PPG binary data {windowId} |
get_accel |
Request accelerometer data {windowId} |
get_temp |
Request temperature data {windowId} |
next_chunk |
Request next binary chunk {dataType, windowId} |
binary_ack |
Acknowledge binary receipt {dataType, windowId} |
confirm_upload |
Delete chunk after successful upload {windowId} |
cancel_transfer |
Cancel active binary transfer |
get_status |
Get current watch status |
get_health |
Get sensor health during recording |
delete_all_windows |
Clear all stored data |
dev_console |
Enable/disable BLE console {enabled} |
1. Phone sends: {"type":"get_ppg","windowId":"123"}
2. Watch sends: {"type":"ppg","windowId":"123","length":1024,"chunked":true}
3. Watch sends: [binary chunks, auto-pumped]
4. Phone sends: {"type":"binary_ack","dataType":"ppg","windowId":"123"}
5. Watch sends: {"type":"end","dataType":"ppg","windowId":"123"}
BLE_CHUNK_SIZE: 244 // MTU 247 - 3 byte header
BLE_MTU: 247 // Negotiated MTU
BLE_CONN_INTERVAL: 7.5-15ms
BLE_TX_POWER: 4 // dBmStoragerequireAPP_IDfor sensor power managementCONFIGobject with all tunables- Global BLE transfer state variables
FILE_SUFFIXESfor storage filenames- Button detection (
HAS_BTN2,MENU_BTN) DEV_MODEdetection (hold BTN1 at startup)
LOGenum (ERROR, WARN, INFO, DEBUG)logE(),logW(),logI(),logD()helpersdbg()- Persistent debug log for BLE transfer diagnosisflushDebugLog(),clearDebugLog(),readDebugLog()- BLE console management (
enableBleConsole,disableBleConsole) applyBleTuning()for MTU/interval negotiation
MODEenum (IDLE, CONTINUOUS)stateobject with all runtime tracking- Upload queue functions:
getUploadQueue(),addToUploadQueue(),removeFromUploadQueue()
allocatePpgBuffer(),allocateAccelBuffer(),allocateTempBuffer()- Binary writers:
writeUint32(),writeInt16(),writeUint16(),writeFloat32() flushPpgBuffer(),flushAccelBuffer(),flushTempBuffer()safeStorageWrite(),safeStorageWriteJSON()
onHRM(hrm)- PPG event handleronAccel(acc)- Accelerometer event handlersampleTemperature()- Temperature sampling
startWindow(mode, duration, label)- Begin recordingstopWindow()- End recording, save datasaveWindow(endTime)- Persist to storagestartContinuous(),stopContinuous(),startContinuousChunk()
updateDisplay()- Main status screengetConnectionStatus()- BLE connection check- Button handlers:
onButton1Down(),onButton1Up(),onButton2() - Menus:
showMenu(),showRecordingMenu(),showQueueInfo(),showStorageInfo() - Temperature timer:
startTempSampling(),stopTempSampling()
sendReady(),sendStatus()- Handshake and statushandleBleCommand(cmd)- Command dispatchersendBleData(type, windowId, data)- Binary transfersendNextChunk()- Chunked transfer pumphandleBinaryAck()- ACK handlingcancelBleTransfer()- Transfer cleanupdeleteWindow()- File cleanupbleHandler- UART event handler object
init()- Main initialization- Event handler registration (HRM, accel, buttons)
- BLE UART setup with JSON command parsing
- NRF connect/disconnect handlers
- Calls
init()at end of file
Hold BTN1 during startup to enable developer mode:
- Keeps BLE console/REPL available for IDE uploads
- Enables verbose logging
- Prevents console disable on BLE connect
Re-enable console via BLE: {"type":"dev_console","enabled":true}
The debug build includes persistent logging for diagnosing BLE transfer issues:
// In Espruino IDE console:
readDebugLog() // Print saved debug log
clearDebugLog() // Clear log buffer and file
flushDebugLog() // Force write buffer to StorageDebug log tags:
[XFER]- Transfer start events[SNC]- sendNextChunk() execution flow[CANCEL]- Transfer cancellation[CMD]- BLE command handling[ACK]- Binary acknowledgment flow
From Espruino IDE console:
// Inject BLE commands locally
X({type: "get_status"})
X({type: "start"})
X({type: "stop"})
X({type: "get_queue"})
// Check state
state
getUploadQueue()
// Manual sensor test
Bangle.setHRMPower(1, "test")Key CONFIG values in 00_prelude.js:
CONFIG = {
// Sampling
PPG_HZ: 25,
ACCEL_HZ: 12,
TEMP_HZ: 0.1,
// Recording
CONTINUOUS_CHUNK: 300000, // 5 minute chunks
// BLE
BLE_CHUNKED_MODE: true,
BLE_CHUNK_SIZE: 244,
BLE_MTU: 247,
// Behavior
COMMAND_ONLY_STARTS: true, // Require BLE commands to start recording
// Logging
DEBUG_LOG: true,
DEBUG_LEVEL: 2, // 0=error, 1=warn, 2=info, 3=debug
}- Check HRM is enabled:
Bangle.setHRMPower(1, "biosig") - Verify watch is on wrist with good skin contact
- Look for "No PPG samples after 5s" warning in logs
- Check
readDebugLog()for[SNC]entries - Look for
timer_fired not appearing after scheduling_next - Phone may need to send
cancel_transferand retry
- Use
get_queue_detailsto see pending uploads - Use
delete_all_windowsto clear all data - Check
Storage.getStats()for space info
- BTN1 long press: Toggle recording (if COMMAND_ONLY_STARTS=false)
- BTN2: Menu button
- If BTN2 unavailable, BTN1 handles menu
- v1.0 - Initial release with modular source structure
- PPG, accelerometer, temperature collection
- Chunked BLE binary transfers
- Continuous recording mode
- Persistent debug logging