Connect
Turn the sensor on (press its button), then tap Connect sensor and choose it from the list. The default name is WMETRIX. Or plug it into this computer with a USB data cable and tap Connect by USB cable (pick CP2102N / usbserial).
The simulator is a pretend sensor that follows the firmware's rules. Use it to practise. It never touches a real board. A second button, Simulator: locked board, pretends the board is not enrolled.
What the sensor says
Not connected yet.
Activity log
Nothing yet.
Device name
Now: - locked until enrolled
Up to 9 plain characters. Saved in the board's settings memory. It probably shows in Bluetooth searches after the board restarts (my reading of the code).
Measuring ranges
Now: accelerometer -, gyro - locked until enrolled
LED
always open Find which board is which.
Orientation zero (tare)
locked until enrolled Makes the way the sensor is lying right now the zero point. The test code does this before every recording.
Sleep
always open Switches the motion chips off and puts the board to sleep. It disconnects. Press the sensor's button to wake it.
Enrollment
Status: -
An enrolled board accepts the locked commands and firmware updates. Enrolling saves a hash in the board's settings memory. The token is worked out from the board's own address (see the Security tab).
What can and cannot be changed
| Item | Change it from this page? |
|---|---|
| Name, accelerometer range, gyro range, orientation zero, calibration | yes, if enrolled |
| LED, sleep | yes |
| Firmware itself | yes, replace the whole file (Firmware tab) |
| Bluetooth address, hardware version, USB IDs | no set in the board / firmware |
| Firmware version number | no hard-coded in main.c; changes only with a new build |
| Data rate (104 Hz accel/gyro, 20 Hz magnetometer) | no there is no command for it |
| Battery limits, button pin, services and UUIDs, flash layout, enrollment key | no only by editing the source and building new firmware |
| Reading the firmware back off the board | no not possible over Bluetooth (see Backup) |
1. Pick a firmware file
A .bin built from this project, such as the delivered 0441225001NDSA V1.0 20261021.bin. It is read in this browser only.
2. Send it to the sensor
Checks that must pass first:
Idle.
Going back (reload an older version)
Pick the older .bin in step 1 and send it the same way. The board does not keep the old one for you in a form you can fetch, so keep every .bin you flash. The delivered build is your baseline.
Modify the firmware
- Open the source folder 0441225001NSDA V1.0 in a terminal.
- Edit the file (table below).
- Build it. You need ESP-IDF 5.4.1:
idf.py build
- The new file is build/0441225001NDSA.bin.
- Pick it in step 1 above, check the results, and send it.
| What | Where | Risk |
|---|---|---|
| Firmware version number | main/main.c line 183 (FIRMWARE_VERSION) | low Change it so you can tell builds apart. |
| Default name | main/main.c line 179 (IMU_DEFAULT_NAME, "WMETRIX") | low Only used when no name is saved. |
| Name length limit | main/main.c line 175 (IMU_MAX_NAME_LENGTH, 9) | medium The info reply layout depends on it; the web code expects 9 or 15. |
| Button pin | main/main.c line 197 (BUTTON_WAKEUP, 35) | high Must match the board's wiring. Hardware files were not delivered. |
| Magnetometer finish level | main/main.c line 71 (COVERAGE_THRESHOLD, 0.80) | medium Changes calibration quality. |
| Battery cut-off | main/main.c line 212 (BATTERY_LOW_THRESHOLD, 3200) | medium Boards switch off below it. |
| Enrollment key | main/main.c line 161 (MAC_SHA256_KEY) | high Probably locks out boards that are already enrolled. |
| Update packet size | main/gatts_sens.h line 17 (OTA_RAW_DATA_SIZE, 248) | medium The spec says send under 240 bytes. |
| Flash layout | partitions_two_ota.csv | high An over-the-air update does not rewrite the partition table. Do not change it. |
Manual checklist (if you use another tool)
The vendor's Android demo app (NDSA APP V1.0 20261021.apk) also does this on its OTA page.
Back up the sensor's settings
Reads the name, address, versions, ranges and all three calibrations, and saves them as a small file. You need an enrolled board for the calibration part.
Restore settings from a backup
No backup file chosen.
Back up the firmware itself
What you can do
- Keep the delivered file. Check it on the Firmware tab: size 684,464, MD5 0a7d7a7585055331c3480fb4c98408a7. It is the vendor's known-good build.
- Copy the whole chip over a USB cable. This also saves the calibration, name and enrollment, which a firmware file does not contain. It needs the board's programming pins. The schematic was not delivered, so ask the vendor whether a header exists.
Commands (ESP-IDF 5.4.1 ships esptool.py; newer versions call it esptool with read-flash / write-flash). Replace PORT:
esptool.py --chip esp32 -p PORT -b 460800 read_flash 0x0 0x800000 ndsa_full_backup.bin
Put a full backup back:
esptool.py --chip esp32 -p PORT -b 460800 write_flash 0x0 ndsa_full_backup.bin
Smaller pieces (settings memory only, or the two app slots):
esptool.py --chip esp32 -p PORT read_flash 0x9000 0x5000 nvs.bin esptool.py --chip esp32 -p PORT read_flash 0x10000 0x330000 app0.bin esptool.py --chip esp32 -p PORT read_flash 0x340000 0x330000 app1.bin esptool.py --chip esp32 -p PORT read_flash 0xE000 0x2000 otadata.bin
Hardware check
Checks every part of the board we know about. Chip IDs and the RST button use the USB cable. The WKUP button, LED and drift test use Bluetooth. When you're done, save the results file and drag it into the Claude chat.
| Part | Asked | Expected | Got | Result |
|---|
All parts on the board (vendor TOP/BOTTOM drawings 0441225001NDSA_251000 + V1.0 source)
| Label | Part | How sure |
|---|---|---|
| M1 | ESP32 module (ESP32-D0WD-V3, 8 MB flash) | Confirmed |
| D4 | LSM6DSOX accelerometer + gyroscope, I2C 0x6A (SDA 19, SCL 18, data-ready 26) | Confirmed in code |
| D5 | LIS2MDL magnetometer, 0x1E, read through the LSM6DSOX sensor hub | Confirmed in code |
| D3 | CP2102N USB-to-serial (28-pin QFN, USB ID 10C4/EA60) | Likely (footprint) |
| HL1 | RGB LED: green 27, red 22, blue 25 (on when low) | Confirmed in code |
| S2 WKUP | Wake button, GPIO 35 (pressed = low) | Confirmed in code |
| S1 RST | Reset button (ESP32 EN) | Label |
| BAT / X1 | Battery: voltage on GPIO 36 (×2 divider), measure enable GPIO 21; 4.2 V = 100%, below 3.2 V = 0% | Confirmed in code |
| USBX2 | USB connector | Label |
| D2 + L1 | Battery charger or regulator | Guess |
| D1 | USB protection or second power chip | Guess |
| VT1–VT6 / VD1–VD6 | Transistors / diodes (auto-reset, battery switch, power path) | Guess |
| G1 | Unknown 3-pad part | Unknown |
| X3 | 10-pin test header: GND, 3V3, TXD, RXD, CLK, MOSI, MISO, CS_N | Silkscreen |
What this firmware is
The program that runs inside the ExerMetrix sensor board. It reads a motion chip, works out the orientation, and sends it to the phone or browser over Bluetooth. It can also receive a new copy of itself over Bluetooth (an "OTA" update).
- Chip
- ESP32, 8 MB flash, 40 MHz
- Built with
- ESP-IDF 5.4.1 (CMake project 0441225001NDSA)
- Bluetooth
- BLE only (NimBLE), one connection at a time, MTU up to 512
- Default name
- WMETRIX (max 9 characters; can be changed)
- Version numbers
- Firmware 1.0, hardware 1.0 (hard-coded in main.c)
- Motion chips
- LSM6DSOX (accelerometer + gyro, 104 Hz) and LIS2MDL (magnetometer, 20 Hz)
- Orientation
- Madgwick filter, sent as a quaternion
- Button
- GPIO 35, active low. Wakes the board from deep sleep and sends press/release to the app
- Delivered build
- 1.0, built Oct 21 2026 09:46:42 with IDF v5.4.1-dirty
Important
Most commands are locked until the board is enrolled (Security tab). The spec PDF does not describe this, and the web reference code never sends the enroll command.
What I could not see
The Hardware folder is empty (no schematic, so no USB pin info). The README.md in the source is Espressif's stock example text, not notes about this project.
What is in the source folder
Tap a file to see what it does.
The 8 MB flash, divided up
From partitions_two_ota.csv, the layout the build actually uses. Tap a block.
Why two app slots?
The board runs from one slot (app0 or app1). An update is written into the other slot. Only after the file's checksum matches does the board switch over to it. If the transfer fails halfway, the slot it was running from is untouched.
What lives in the settings block (NVS)
From the code, the board saves these in a block called storage: the magnetometer, accelerometer and gyro calibration, the orientation zero point (tare), the device name, and the enrollment hash.
Two Bluetooth services
| Service | Purpose | UUIDs |
|---|---|---|
| NDOF 0xFEE7 | Commands, motion data, replies | service 0000fee7-8e22-4541-9d4c-21edae82ed19 FEC7 command (write) FEC8 data (notify) FEC9 replies, progress, button (notify) |
| OTA 0xFE20 | Send a new firmware | service 0000fe20-cc7a-482a-984a-7f2ed5b3e58f FE22 command (write) FE23 replies (notify) FE24 firmware data (write) (characteristics use base -8e22-4541-9d4c-21edae82ed19) |
NDOF commands
Send to FEC7. First byte is the code.
Data packets (56 bytes each)
Two streams use the same 56-byte record in a different field order. Values are little-endian 32-bit floats; the last 4 bytes are the board's time stamp in milliseconds.
| Offset | 0x01 Measurement | 0x04 Real-time report (used for tests) |
|---|---|---|
| 0 | magnetometer x,y,z (about 1.0 when calibrated) | quaternion w,x,y,z |
| 12 / 16 | 12: accelerometer x,y,z (g) | 16: magnetometer x,y,z (milligauss) |
| 24 / 28 | 24: gyro x,y,z (°/s) | 28: accelerometer x,y,z (g) |
| 36 / 40 | 36: quaternion w,x,y,z | 40: gyro x,y,z (°/s) |
| 52 | time stamp (u32, ms) | |
Enrollment: why a board can ignore your commands
- At power-up the board checks whether it is enrolled (a hash is saved in its settings).
- If it is not, it still advertises, but the locked commands are ignored and writes to the OTA service are refused.
- To enroll, send command 0xF1 followed by an 8-byte token. If the token is right, the board saves the hash and unlocks.
The token is the first 8 bytes of SHA-256( "ndof_key" + the board's 6-byte Bluetooth address ). You can work it out for any address below, or use the Enroll button on the Settings tab for a connected board.
Not tested on a real board.
What the code itself says
Serial (cable) text commands
115200 baud. Every line starts with AT and replies end with OK or ERROR.
Calibration, as the firmware does it
Magnetometer
Figure-8 movement. 10 samples per direction bin. May finish at 80%. Result is a hard-iron offset plus a 3×3 soft-iron matrix.
Accelerometer
Six faces. Finishes at 100%. Result is an offset plus a 3×3 matrix.
Gyroscope
Lay it still. A drift calibrator measures the bias. Result is three offsets.