NDSA Sensor Firmware Explorer

Connect to the sensor by Bluetooth or USB cable, read it, check the hardware, change what can be changed, send firmware. Built from the vendor delivery 0441225001NDSA 251001 (2026-10-21).

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

The web test code sets 16 g and 2000 dps each time it connects. Other ranges change the results of tests. I did not check whether the board remembers a range after it is switched off.

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).

Connect to see the token.

What can and cannot be changed

ItemChange it from this page?
Name, accelerometer range, gyro range, orientation zero, calibrationyes, if enrolled
LED, sleepyes
Firmware itselfyes, replace the whole file (Firmware tab)
Bluetooth address, hardware version, USB IDsno set in the board / firmware
Firmware version numberno 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 keyno only by editing the source and building new firmware
Reading the firmware back off the boardno 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.

No file chosen yet. The delivered build has size 684,464 bytes and MD5 0a7d7a7585055331c3480fb4c98408a7.

2. Send it to the sensor

Checks that must pass first:

    Keep the sensor close and powered. Do not close this page or the laptop lid while it sends. If the transfer fails before the end, the board keeps running its old firmware.

    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.

    If an update goes wrong after the board reboots (for example it starts but misbehaves), send the baseline file again. If the board no longer starts at all, it needs a USB cable and the commands on the Backup tab.

    Modify the firmware

    1. Open the source folder 0441225001NSDA V1.0 in a terminal.
    2. Edit the file (table below).
    3. Build it. You need ESP-IDF 5.4.1:
      idf.py build
    4. The new file is build/0441225001NDSA.bin.
    5. Pick it in step 1 above, check the results, and send it.
    WhatWhereRisk
    Firmware version numbermain/main.c line 183 (FIRMWARE_VERSION)low Change it so you can tell builds apart.
    Default namemain/main.c line 179 (IMU_DEFAULT_NAME, "WMETRIX")low Only used when no name is saved.
    Name length limitmain/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 pinmain/main.c line 197 (BUTTON_WAKEUP, 35)high Must match the board's wiring. Hardware files were not delivered.
    Magnetometer finish levelmain/main.c line 71 (COVERAGE_THRESHOLD, 0.80)medium Changes calibration quality.
    Battery cut-offmain/main.c line 212 (BATTERY_LOW_THRESHOLD, 3200)medium Boards switch off below it.
    Enrollment keymain/main.c line 161 (MAC_SHA256_KEY)high Probably locks out boards that are already enrolled.
    Update packet sizemain/gatts_sens.h line 17 (OTA_RAW_DATA_SIZE, 248)medium The spec says send under 240 bytes.
    Flash layoutpartitions_two_ota.csvhigh An over-the-air update does not rewrite the partition table. Do not change it.
    Line numbers are from the delivered source and will move if you edit above them.

    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

      Calibration belongs to one board. Restoring another board's calibration makes it measure wrongly. The page warns you if the address differs.

      No backup file chosen.

      Back up the firmware itself

      This cannot be done over Bluetooth. The update service only accepts data and sends back short status codes. I found no command in the firmware or the spec that reads the firmware back out.

      What you can do

      1. Keep the delivered file. Check it on the Firmware tab: size 684,464, MD5 0a7d7a7585055331c3480fb4c98408a7. It is the vendor's known-good build.
      2. 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
      None of these commands have been run by me. The addresses come from partitions_two_ota.csv. otadata.bin says which slot the board boots from.

      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.

      PartAskedExpectedGotResult
      All parts on the board (vendor TOP/BOTTOM drawings 0441225001NDSA_251000 + V1.0 source)
      LabelPartHow sure
      M1ESP32 module (ESP32-D0WD-V3, 8 MB flash)Confirmed
      D4LSM6DSOX accelerometer + gyroscope, I2C 0x6A (SDA 19, SCL 18, data-ready 26)Confirmed in code
      D5LIS2MDL magnetometer, 0x1E, read through the LSM6DSOX sensor hubConfirmed in code
      D3CP2102N USB-to-serial (28-pin QFN, USB ID 10C4/EA60)Likely (footprint)
      HL1RGB LED: green 27, red 22, blue 25 (on when low)Confirmed in code
      S2 WKUPWake button, GPIO 35 (pressed = low)Confirmed in code
      S1 RSTReset button (ESP32 EN)Label
      BAT / X1Battery: voltage on GPIO 36 (×2 divider), measure enable GPIO 21; 4.2 V = 100%, below 3.2 V = 0%Confirmed in code
      USBX2USB connectorLabel
      D2 + L1Battery charger or regulatorGuess
      D1USB protection or second power chipGuess
      VT1–VT6 / VD1–VD6Transistors / diodes (auto-reset, battery switch, power path)Guess
      G1Unknown 3-pad partUnknown
      X310-pin test header: GND, 3V3, TXD, RXD, CLK, MOSI, MISO, CS_NSilkscreen

      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.

      Tap a block above.

      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.

      The delivered .bin is 684,464 bytes. One slot is 3,342,336 bytes, so it uses about 20.5% of a slot.
      partitions_two_ota_org.csv is an older 1 MB-per-slot layout. It is not used.

      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.

      A full flash erase over USB would wipe all of that. This is my reading of the code; I have not tried it on a board.

      Two Bluetooth services

      ServicePurposeUUIDs
      NDOF 0xFEE7Commands, motion data, replies service 0000fee7-8e22-4541-9d4c-21edae82ed19
      FEC7 command (write)
      FEC8 data (notify)
      FEC9 replies, progress, button (notify)
      OTA 0xFE20Send 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.

      Locked means the firmware ignores it unless the board is enrolled. I checked this against each command in main.c.

      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.

      Offset0x01 Measurement0x04 Real-time report (used for tests)
      0magnetometer x,y,z (about 1.0 when calibrated)quaternion w,x,y,z
      12 / 1612: accelerometer x,y,z (g)16: magnetometer x,y,z (milligauss)
      24 / 2824: gyro x,y,z (°/s)28: accelerometer x,y,z (g)
      36 / 4036: quaternion w,x,y,z40: gyro x,y,z (°/s)
      52time stamp (u32, ms)

      Enrollment: why a board can ignore your commands

      1. At power-up the board checks whether it is enrolled (a hash is saved in its settings).
      2. If it is not, it still advertises, but the locked commands are ignored and writes to the OTA service are refused.
      3. 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.

      Enter an address and tap the button.

      Not tested on a real board.

      What the code itself says

      A comment in main.c calls this a production-line anti-swap check, not real client authentication, because the key is stored inside the firmware. It says a per-device key in the chip's eFuse would be the real fix.
      The spec PDF dated 2026-10-03 does not mention 0xF1, enrollment, or the locked commands. Worth asking the vendor how boards are enrolled in production.

      Serial (cable) text commands

      115200 baud. Every line starts with AT and replies end with OK or ERROR.

      I read the command names and what they print from main.c, but did not run them.

      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.

      The board can calibrate itself (command 0x12, progress on 0x13). The older way is to collect raw data in the app, compute, then send the result with 0x03. This page reads and restores calibration (Backup tab) but does not run a calibration.
      Vendor tip: stay away from steel, magnets, motors and speakers when calibrating the magnetometer.