Plan to bring the protocentral-pulse-express library in line with current
ProtoCentral Arduino library conventions, and to expand the examples so they
exercise the full capability set of the MAX32664D biometric sensor hub.
Reference library (newest, the canonical template): protocentral_max30001_arduino_library
(Nov 2025). Secondary reference for the dotted example style: protocentral_st1vafe3bx_arduino.
Locked decisions
- Public class name:
PulseExpress(board-centric, breaking rename fromMax32664). - Example naming:
01.Name/01.Name.ino(dotted, matches ST1VAFE3BX and the current sketches — lowest churn). - Deliverable: this written plan only — no code changes until approved.
From UG6921 (bundled in docs/) and the existing src/max32664.h. The hub owns
the MAX30101 optical sensor over a private I2C bus; the host only ever talks to the
hub at 0x55. Everything below is already supported by the driver internals — the
gap is that the examples only surface 3 of these flows.
| Capability | Hub output / mechanism | Surfaced today? |
|---|---|---|
| Raw PPG (IR + Red, 24-bit ADC counts) | Raw Data Collection mode | ✅ (IR only) |
| Heart rate (10× bpm) | Algorithm mode sample | |
| SpO2 (10× %) + confidence + report flag | Algorithm mode sample | |
| R-value (1000×) — used to calibrate SpO2 | Algorithm mode sample | ❌ |
| SpO2 coefficient calibration (a, b, c per AN6845) | startEstimation(coeffs) |
❌ (defaults only) |
| Systolic / diastolic BP (mmHg) | Algorithm mode sample | ✅ |
| BPT calibration → user calibration vector | startCalibration → readCalibrationVector |
✅ (not persisted) |
| Vector persistence + reload | loadCalibrationVector |
|
| Multi-subject calibration (calIndex 0..4, FW ≥40.5.0) | startCalibration(CalibrationRef) |
❌ |
| Legacy single-shot calibration (FW <40.5.0) | startCalibration(LegacyCalibrationRefs) |
❌ |
| Inter-beat interval (IBI, ms, FW ≥40.5.0) → HRV | Algorithm mode sample | ❌ |
| BP status codes (24 states, Table 4) | Max32664BpStatus |
|
| Firmware/algo version + derived capability flags | version(), algoVersion(), caps() |
|
| Hub status / FIFO overflow diagnostics | internal HubStatus |
❌ not exposed publicly |
| MFIO interrupt-driven reads vs polling | mfioPin() exposed |
❌ polling only |
Implication: the driver is already capable; modernisation is mostly packaging + examples, not a rewrite.
| Area | Current | Modern convention (target) |
|---|---|---|
| Class name | Max32664 |
PulseExpress |
| Main header | src/max32664.h |
src/protocentral_pulse_express.h (+ keep thin max32664.h shim?) |
| License header | banner ////// block |
SPDX two-line header + MIT block (MAX30001 style) |
library.properties |
present, OK | add includes=, align name=, bump to keep 2.0.0 / 2.1.0 |
library.json (PlatformIO) |
none | none (siblings omit it) — leave out |
keywords.txt |
present, stale | regenerate for new class + all public symbols |
| README | board-focused, license-heavy | badges → buy → overview/key-caps → install → hardware → quick start → API → examples → license |
| Badges | Compile Examples only | + License: MIT, + Arduino Library |
| CI workflows | compile-examples.yml, main.yml (lint) |
keep both; confirm main.yml = arduino-lint |
assets/ |
has board photo + gif | keep; ensure README references resolve |
docs/ |
5 datasheets/PDFs | keep (sibling ADS1293 also ships a docs/) |
| Examples | 3 sketches | renumber + expand to ~9 (section 4) |
build-uno-r4.sh |
present, good | rename mentally as the "compile_all" equivalent; siblings call it compile_all_examples.sh — optionally add that as the canonical name |
Breaking change; bump version to 2.1.0 (still a 2.x clean-break line, README
already warns API is not 1.x-compatible). To keep churn low and avoid breaking the
existing 2.0.0 sketches in one shot:
- Rename the class and all
Max32664*free types? No — keep theMax32664Status,Max32664Sample, etc. type names OR rename them too. Recommend renaming the class only and type-aliasing the struct/enum names toPulseExpress…(e.g.using PulseExpressStatus = Max32664Status;) so both spellings compile. Decide at implementation time; default: rename class, alias the rest, document the canonical names. - Provide
src/protocentral_pulse_express.has the new primary include. Keepsrc/max32664.has a one-line#include "protocentral_pulse_express.h"shim +typedef PulseExpress Max32664;for backward compatibility, marked deprecated. - Update
library.propertiesincludes=protocentral_pulse_express.h.
Replace the banner block at the top of src/*.{h,cpp} and every examples/**/*.ino
with the MAX30001-style header:
// SPDX-License-Identifier: MIT
// SPDX-FileCopyrightText: Copyright (c) 2025 Ashwin Whitchurch, Protocentral Electronics
// SPDX-FileCopyrightText: Copyright (c) 2020 Maxim Integrated / Analog Devices (protocol)
/*
* ProtoCentral Pulse Express — MAX30101 + MAX32664D Biometric Sensor Hub
*
* Copyright (c) 2025 Ashwin Whitchurch, Protocentral Electronics
* ... full MIT text ...
*/Preserve the original-author attribution ("Original 2020 driver: Joice Tm").
# Protocentral Pulse Express ...title- Badges: Compile Examples, License: MIT, Arduino Library
## Don't have one? [Buy it here](...)+ board photo## Overview+ Key Capabilities bullet list (mirror section 1 table)## Important medical disclaimer(R&D only — keep prominent)## Installation(Library Manager + manual ZIP)## Hardware Setup— pin table (already in README; extend with ESP32 column)## Quick Start— minimal HR/SpO2 sketch## Firmware versions & capabilities— explain the 40.x breakpoints +caps(); state the board ships pre-flashed, list the minimum/known-good version, and howbegin()/version()report it. No.msbllink. One line: re-flashing is a factory/recovery operation (seeextras/), image not redistributed.## Examples— table mapping each sketch to the capability it demos## API Reference— constructor,begin(), the three modes, status enum## Licenseblock (current detailed block, trimmed)
Small, additive API surface (no behaviour change to existing methods):
- Public hub status accessor — promote the private
HubStatusread to a publicMax32664Status readStatus(...)so a diagnostics example can show FIFO overflow / data-ready / busy. (Needed by example 08.) - HRV helper is example-side — compute SDNN/RMSSD from the existing
ibiMsfield in the sketch; no driver change. (Example 06.) - Confirm
rValue()and confidence fields are populated in estimation samples (already inMax32664Sample). No change; just used by examples 03/05. - Optional: a convenience
begin(TwoWire&, sda, scl)overload for ESP32 boards with remappable I2C, to make the multi-board story clean. Low priority.
If keeping the change set minimal, only item (1) touches the driver; the rest is examples + docs.
Goal: one focused sketch per capability, ordered simplest → most advanced. Existing sketches are renumbered/retitled; ✚ = new.
| # | Folder | Demonstrates | Status |
|---|---|---|---|
| 01 | 01.RawPPGStreamPlotter |
Raw IR PPG → Arduino Serial Plotter | keep |
| 02 | 02.RawPPGStreamOpenView |
Raw IR/Red → ProtoCentral OpenView GUI | keep (was 03) |
| 03 | 03.HeartRateSpO2 |
✚ Algorithm mode: live HR + SpO2 + confidence, no BP — the simplest "vitals" demo | new |
| 04 | 04.BPTCalibration |
✚ Run calibration only; dump the user calibration vector (hex) to Serial | new |
| 05 | 05.BPTEstimation |
Load a saved vector + stream BP/HR/SpO2 (the current "estimation" half) | refactor from current 02 |
| 06 | 06.BPTCalibrateAndEstimate |
Full end-to-end cal→estimate in one sketch (current 02 behaviour) | keep/rename |
| 07 | 07.SaveLoadCalibrationEEPROM |
✚ Persist the vector to EEPROM/Preferences (size from caps().calibVectorBytes), reload on boot, then estimate |
new |
| 08 | 08.MultiSubjectCalibration |
✚ FW ≥40.5.0 multi-point calibration, calIndex 0..4, one vector per subject; branch on caps().multiPointCalib |
new |
| 09 | 09.HeartRateVariability |
✚ Derive SDNN/RMSSD from the IBI field (FW ≥40.5.0) | new |
| 10 | 10.DeviceInfoAndDiagnostics |
✚ Print hub + algo firmware version, decoded caps(), and live hub status (FIFO overflow / busy) |
new |
Factory / advanced (bootloader — see §5):
11.FirmwareFlash— ✚ bootloader-mode flasher; streams a user-supplied.msblover Serial (PC script) or reads it from SD. Documented as factory/recovery; image never shipped. Serial-stream variant uses no extra library so it stays in the CI matrix.
Optional / stretch (only if worthwhile):
12.SpO2Calibration— upload custom SpO2 (a,b,c) coefficients and compare against defaults using the R-value output (AN6845 workflow).13.InterruptDriven— use the MFIO pin as a data-ready interrupt instead of polling.
Recommend shipping 01–11; treat 12–13 as fast-follows. Every new .ino gets the
SPDX header, the standard pin table comment block, and a top-of-file WARNING about
placeholder BP/SpO2 reference values (matching the current 02 example's tone).
CI impact: each new sketch is auto-picked up by compile-examples.yml
(sketch-paths: - examples) and build-uno-r4.sh. Examples that need EEPROM
(07) must guard for cores without <EEPROM.h> (ESP32 uses Preferences/emulated
EEPROM) — use #if defined(...) so the CI matrix keeps compiling across all ~30 boards.
The hub ships pre-flashed at the ProtoCentral factory; end users never need the
.msbl. The flashing code nonetheless lives in this library as a factory/recovery
tool. The firmware image is treated as non-redistributable IP and is never
committed — the flasher consumes a user-supplied file.
Everything the driver does today is application mode. Flashing is bootloader
mode — a distinct command set (reference: docs/user-guide-6806-max32664.pdf):
- Entry: hold MFIO low while releasing RSTN (application entry holds MFIO
high). After settle, read operating mode (
0x02 0x00) → expect0x08(bootloader). - Flash sequence (Family
0x80): set page count (0x80 0x02), upload init-vector- auth bytes parsed from the
.msblheader (0x80 0x00/0x80 0x01), erase app (0x80 0x03), then stream each 8192-byte page + 16 check bytes (0x80 0x04) with a long per-pageCMD_DELAY(~340 ms). Bootloader status codes0x80–0x83(checksum/auth/invalid-app) surface here — distinct from the app-mode codes already inMax32664Status.
- auth bytes parsed from the
- Exit: reset with MFIO high → re-enters application mode; then the normal
begin()path reads back the new version.
A typical .msbl is >100 KB — it cannot be a const array on an Uno. Two
supported transports, both keeping the file off-device-flash:
- Serial-stream (primary): a host PC script (Python) parses the
.msbland streams pages over USB-Serial to the flasher sketch, which writes them to the hub. No extra Arduino library → compiles on the full CI matrix. Recommended default. - SD card (optional): sketch reads
firmware.msblfrom SD. Needs<SD.h>; guard with#if __has_include(<SD.h>)so CI keeps building on cores without it.
New, clearly separated from the app-mode API (own header section or a small
PulseExpressBootloader helper class to avoid bloating the hot-path driver):
Max32664Status enterBootloader()/exitBootloaderToApp()Max32664Status readBootloaderInfo(...)— page size, mode confirm, BL versionMax32664Status beginFlash(uint16_t numPages, const uint8_t iv[11], const uint8_t auth[16])Max32664Status flashPage(const uint8_t *page8208)— 8192 + 16 check bytesMax32664Status finishFlash()- Reuse the existing
writeImpl()retry-on-0xFElayer; add bootloader status decoding.
The .msbl header parsing (page count, IV, auth, page size) lives in the host
Python script and/or the example sketch — not baked into the driver.
Independent of flashing: begin() already reads the version. Add a documented
minimum/known-good firmware constant and have examples warn (not hard-fail) when
the hub reports older firmware — this is the user-facing substitute for shipping a
.msbl.
.gitignore: add*.msbland*.bin(neither is currently excluded — the image could be committed today by accident).- No
.msblin the repo, README, or releases. The flasher docs say "supply your own image (factory-internal)"; the consumer README says only "ships pre-flashed." - Add a
extras/firmware/README.mdplaceholder explaining where the operator drops their local (gitignored).msbl, without including one.
src/protocentral_pulse_express.h NEW primary header; class PulseExpress + types
src/protocentral_pulse_express.cpp NEW (renamed from max32664.cpp)
src/pulse_express_bootloader.h/.cpp NEW bootloader-mode API (§5.3), kept separate
src/max32664.h EDIT shrink to compat shim (#include new + typedef)
src/max32664.cpp DEL (content moved)
README.md REWRITE per §2.3 (incl. "ships pre-flashed" note)
keywords.txt REGEN for PulseExpress + bootloader + all symbols
library.properties EDIT name/includes/version=2.1.0
CHANGELOG.md NEW document 2.1.0 rename + new examples
examples/01..11 ADD/RENAME per §4, SPDX headers on all
extras/firmware/README.md NEW placeholder telling operator where the (gitignored) .msbl goes
extras/flash_tool/flash_msbl.py NEW host PC script: parse .msbl header + stream pages
.gitignore EDIT add *.msbl, *.bin
.github/workflows/main.yml VERIFY arduino-lint passes with new layout
build-uno-r4.sh KEEP (optionally add compile_all_examples.sh alias)
CLAUDE.md UPDATE class name + example list + bootloader after rename
assets/ KEEP; verify README image paths
Backward-compat note: the src/max32664.h shim means existing user sketches that
#include "max32664.h" and use Max32664 keep compiling — important since 2.0.0
already shipped.
- Driver: add public
readStatus()(§3.1); rename class + add header/shim + type aliases. Compile existing examples unchanged via the shim to prove back-compat. (./build-uno-r4.sh) - Examples: renumber existing, then add new sketches one capability at a time,
compiling each. (
arduino-cli compile --fqbn arduino:renesas_uno:minima ...) - Metadata:
library.properties,keywords.txt, SPDX headers across all files. - Docs: README rewrite, CHANGELOG, CLAUDE.md update.
- CI: push branch, confirm
compile-examplesmatrix +arduino-lintare green.
Each step is independently compilable; nothing is merged until the full board matrix passes.
-
Hard version gate — FIXED (soft warning).
begin()used toreturn UnsupportedFirmwarewhen_hubVer.major != 40, blocking earlier/other firmware outright. Now a soft warning:begin()proceeds with legacy capability defaults and exposesfirmwareSupported()for callers to warn on. (src/max32664.cppbegin(),src/max32664.haccessor +_fwSupported.) -
Dead capability flags — OPEN.
Max32664Caps::sendBpMedication/sendRestModeare set bycapsFor()but never consumed anywhere. For firmware <40.2.2 these should betrueand trigger extra calibration-setup commands that the rewrite never sends → legacy calibration is incomplete on the oldest hubs. Fix needs the older UG opcodes confirmed (docs/user-guide-6806,docs/an6921) before implementing — do not guess the bytes. Decide: implement the steps, or drop the flags and document <40.2.2 as unsupported. -
Legacy path likely never hardware-validated. The 824-byte vector / 23-byte sample / single-point calibration branch was reimplemented from scratch; the only shipped example (
02) exercises whichever path the test board reported. The04/05examples (calibration-only, estimation-only) should be run against a real pre-40.5.0 hub as part of this work.
Diagnostic needed: what does begin()'s trace print for the affected board
(hub + algo version), and at which call does it fail? setDebug(&Serial) enables it.
That single line tells us whether the symptom was the gate (#1, now fixed) or the
legacy-path gaps (#2/#3).
- Type renames: rename the
Max32664*struct/enum names too, or alias them? Default plan: rename class only, alias the rest, document canonicalPulseExpress*spellings. (Lower risk, slightly less "clean".) - EEPROM portability (example 07): AVR/SAMD use
<EEPROM.h>; ESP32 usesPreferencesorEEPROM.begin(size). Need#ifguards so the 30-board CI matrix keeps compiling. Vector is up to 824 bytes (legacy) — exceeds Uno's 1 KB EEPROM headroom only slightly; fine on Uno (1 KB), tight on some AVRs — document it. - Multi-subject example (08): requires FW ≥40.5.0 hardware to actually run;
it must compile and degrade gracefully (print "requires 40.5.0+") on older hubs via
caps().multiPointCalib. - Repo/folder name: current
protocentral-pulse-express(kebab) differs from the newest snake_case siblings; not worth renaming the repo. Leave as-is. .msblleakage: the image must never reach git, the README, or a GitHub release. Mitigations:.gitignore*.msbl/*.bin, no download link,extras/firmware/ships only a placeholder README. Worth a one-timegit log -p/ history scan to confirm no image was committed before the ignore rule existed.- Bricking risk (flasher): a failed/interrupted flash can leave the hub in bootloader mode. The flasher example must detect bootloader-on-boot and offer re-flash rather than assuming app mode. Document recovery (it's non-destructive — re-enter bootloader and retry).
- Flasher CI: the Serial-stream variant compiles everywhere (no deps); the SD
variant must be
#if __has_include(<SD.h>)-guarded so the 30-board matrix stays green. - Bootloader code placement: keep it in a separate
PulseExpressBootloaderclass / files so the common app-mode driver isn't enlarged on memory-tight AVRs that never flash.