Skip to content

Commit 2dcc1db

Browse files
committed
Fix legacy 40.2.2 handling in examples, drain FIFO everywhere
Audit of the examples against the dual-firmware goal: the library must keep working for users still on the original 40.2.2 MSBL, not just 40.6.0. Two examples presented 40.5.0-only data as if the legacy firmware provided it: - 03.HeartRateSpO2 printed spo2Confidence unconditionally. That field only exists in the 29-byte sample; on the 23-byte legacy sample the driver zeroes it, so 40.2.2 users saw "conf: 0 %" presented as a real reading. Now "n/a". - 09.HeartRateVariability derives HRV entirely from the hub's inter-beat interval, which does not exist before 40.5.0. It warned once at startup and then looped forever accumulating nothing, with no output and no error. It now stops with an explanation and points at 03.HeartRateSpO2. Added PulseExpressCaps::extendedSampleFields (true from 40.5.0) so the IBI, SpO2-confidence and report-flag fields are gated on a named capability rather than open-coded sampleBytes comparisons, per the convention in CLAUDE.md. parseSample() uses it too. Examples 01, 03, 05, 06, 07 and 09 now drain the hub FIFO to empty each pass instead of taking a single capped read per loop. An overflowed output FIFO rejects every subsequent read until it is emptied, which stops streaming for good -- the failure seen on the 40.6.0 board in 10.DeviceInfoAndDiagnostics. Examples 04, 05, 06, 07, 08 and 10 already branched on caps() correctly; 01, 02 and 11 are firmware-agnostic. All 11 compile for Uno R4 Minima. Note: raw PPG on 40.2.2 is still unresolved. Both 40.2.2 boards fail every command that needs the hub's secondary I2C bus to the optical AFE (0x41 03 FF and 0x44 03 01 both return 0xFF) while the 40.6.0 board reads PART_ID 0x15 and streams. The sensor-index sweep added to 10.DeviceInfoAndDiagnostics has not yet been run on a 40.2.2 board; if the AFE answers at a different index there, the fix is a caps field for it rather than a firmware reflash.
1 parent 9e462fc commit 2dcc1db

10 files changed

Lines changed: 140 additions & 62 deletions

File tree

.DS_Store

0 Bytes
Binary file not shown.

CHANGELOG.md

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,7 +48,22 @@ All notable changes to the ProtoCentral Pulse Express library.
4848
they did retrieve, and trace a warning when the overflow flag is set. The
4949
example drains the FIFO to empty each pass and polls at 100 ms.
5050

51+
### Fixed (legacy 40.2.2 firmware support)
52+
- **`03.HeartRateSpO2` printed a fabricated SpO2 confidence of 0 %** on the
53+
23-byte legacy sample, which does not carry that field. It now prints `n/a`.
54+
- **`09.HeartRateVariability` silently did nothing on legacy firmware.** HRV is
55+
derived from the hub's inter-beat interval, which only exists in the 29-byte
56+
sample (>= 40.5.0); the sketch warned once and then collected zeros forever.
57+
It now stops with an explanation and points at `03.HeartRateSpO2`.
58+
- Examples `01`, `03`, `05`, `06`, `07`, `09` drain the hub FIFO to empty each
59+
pass instead of taking one capped read per loop. An overflowed output FIFO
60+
rejects every later read until emptied, which stops streaming permanently.
61+
5162
### Added
63+
- `PulseExpressCaps::extendedSampleFields` — true when the hub reports the
64+
29-byte sample carrying IBI, SpO2 confidence and the two report flags
65+
(>= 40.5.0). Examples and `parseSample()` now branch on this named capability
66+
rather than testing `sampleBytes` directly.
5267
- **`flash-firmware.sh`** — one-command hub firmware flashing: compiles and
5368
uploads `11.FirmwareFlash` to the Arduino host, then runs
5469
`extras/flash_tool/flash_msbl.py` against it. Auto-detects the board's serial

examples/01.RawPPGStreamPlotter/01.RawPPGStreamPlotter.ino

Lines changed: 9 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -60,14 +60,16 @@ void setup()
6060

6161
void loop()
6262
{
63+
// readRaw() returns at most SAMPLE_CAP samples per call, so keep calling it
64+
// until a pass comes back short. If the host drains slower than the hub
65+
// fills, the hub's output FIFO overflows and then rejects every FIFO read
66+
// until it is emptied — streaming would stop for good.
6367
PulseExpressRawSample buf[SAMPLE_CAP];
6468
size_t n = 0;
65-
PulseExpressStatus s = hub.readRaw(buf, SAMPLE_CAP, &n, /*wantRed=*/false);
66-
if (s != PulseExpressStatus::Ok) return;
67-
68-
for (size_t i = 0; i < n; ++i)
69+
do
6970
{
70-
Serial.println(buf[i].ir);
71-
delay(3);
72-
}
71+
n = 0;
72+
if (hub.readRaw(buf, SAMPLE_CAP, &n, /*wantRed=*/false) != PulseExpressStatus::Ok) return;
73+
for (size_t i = 0; i < n; ++i) Serial.println(buf[i].ir);
74+
} while (n == SAMPLE_CAP);
7375
}

examples/03.HeartRateSpO2/03.HeartRateSpO2.ino

Lines changed: 26 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,8 @@ static void halt(const char *step, PulseExpressStatus s)
4141
}
4242
}
4343

44+
static bool hasConfidence = false;
45+
4446
void setup()
4547
{
4648
Serial.begin(57600);
@@ -59,6 +61,10 @@ void setup()
5961

6062
// Estimation mode streams HR/SpO2 (and BP, once calibrated). We use the
6163
// default SpO2 coefficients here; calibrate them per AN6845 for accuracy.
64+
// Cache once: the 29-byte sample (>= 40.5.0) carries an SpO2 confidence
65+
// figure, the 23-byte legacy sample does not.
66+
hasConfidence = hub.caps().extendedSampleFields;
67+
6268
s = hub.startEstimation();
6369
if (s != PulseExpressStatus::Ok) halt("startEstimation", s);
6470

@@ -68,17 +74,28 @@ void setup()
6874

6975
void loop()
7076
{
77+
// Drain to empty: readSamples() returns at most `cap` per call, and a hub
78+
// FIFO left to overflow rejects every later read until it is emptied.
7179
PulseExpressSample samples[8];
7280
size_t n = 0;
73-
if (hub.readSamples(samples, 8, &n) != PulseExpressStatus::Ok) return;
74-
75-
for (size_t i = 0; i < n; ++i)
81+
do
7682
{
77-
const PulseExpressSample &s = samples[i];
78-
Serial.print("HR: "); Serial.print(s.heartRate(), 1); Serial.print(" bpm");
79-
Serial.print(" SpO2: "); Serial.print(s.spo2(), 1); Serial.print(" %");
80-
Serial.print(" conf: "); Serial.print(s.spo2Confidence); Serial.print(" %");
81-
Serial.print(" status: "); Serial.println(uint8_t(s.bpStatus));
82-
}
83+
n = 0;
84+
if (hub.readSamples(samples, 8, &n) != PulseExpressStatus::Ok) return;
85+
86+
for (size_t i = 0; i < n; ++i)
87+
{
88+
const PulseExpressSample &s = samples[i];
89+
Serial.print("HR: "); Serial.print(s.heartRate(), 1); Serial.print(" bpm");
90+
Serial.print(" SpO2: "); Serial.print(s.spo2(), 1); Serial.print(" %");
91+
// SpO2 confidence only exists in the 29-byte sample (>= 40.5.0);
92+
// on legacy firmware the field is always zero, so say so rather
93+
// than printing a fake "0 %".
94+
Serial.print(" conf: ");
95+
if (hasConfidence) { Serial.print(s.spo2Confidence); Serial.print(" %"); }
96+
else { Serial.print("n/a"); }
97+
Serial.print(" status: "); Serial.println(uint8_t(s.bpStatus));
98+
}
99+
} while (n == 8);
83100
delay(100);
84101
}

examples/05.BPTEstimation/05.BPTEstimation.ino

Lines changed: 15 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -85,16 +85,21 @@ void loop()
8585
{
8686
PulseExpressSample samples[8];
8787
size_t n = 0;
88-
if (hub.readSamples(samples, 8, &n) != PulseExpressStatus::Ok) return;
89-
90-
for (size_t i = 0; i < n; ++i)
88+
// Drain to empty; a hub FIFO left to overflow rejects every later read.
89+
do
9190
{
92-
const PulseExpressSample &s = samples[i];
93-
Serial.print("sys="); Serial.print(s.systolic);
94-
Serial.print(" dia="); Serial.print(s.diastolic);
95-
Serial.print(" hr="); Serial.print(s.heartRate(), 1);
96-
Serial.print(" spo2="); Serial.print(s.spo2(), 1);
97-
Serial.print(" status="); Serial.println(uint8_t(s.bpStatus));
98-
}
91+
n = 0;
92+
if (hub.readSamples(samples, 8, &n) != PulseExpressStatus::Ok) return;
93+
94+
for (size_t i = 0; i < n; ++i)
95+
{
96+
const PulseExpressSample &s = samples[i];
97+
Serial.print("sys="); Serial.print(s.systolic);
98+
Serial.print(" dia="); Serial.print(s.diastolic);
99+
Serial.print(" hr="); Serial.print(s.heartRate(), 1);
100+
Serial.print(" spo2="); Serial.print(s.spo2(), 1);
101+
Serial.print(" status="); Serial.println(uint8_t(s.bpStatus));
102+
}
103+
} while (n == 8);
99104
delay(100);
100105
}

examples/06.BPTCalibrateAndEstimate/06.BPTCalibrateAndEstimate.ino

Lines changed: 16 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -170,17 +170,22 @@ void loop()
170170
{
171171
PulseExpressSample samples[8];
172172
size_t n = 0;
173-
PulseExpressStatus s = hub.readSamples(samples, 8, &n);
174-
if (s != PulseExpressStatus::Ok) return;
175-
176-
for (size_t i = 0; i < n; ++i)
173+
// Drain to empty; a hub FIFO left to overflow rejects every later read.
174+
do
177175
{
178-
Serial.print("sys="); Serial.print(samples[i].systolic);
179-
Serial.print(" dia="); Serial.print(samples[i].diastolic);
180-
Serial.print(" hr="); Serial.print(samples[i].heartRate(), 1);
181-
Serial.print(" spo2="); Serial.print(samples[i].spo2(), 1);
182-
Serial.print(" status=");
183-
Serial.println(uint8_t(samples[i].bpStatus));
184-
}
176+
n = 0;
177+
PulseExpressStatus s = hub.readSamples(samples, 8, &n);
178+
if (s != PulseExpressStatus::Ok) return;
179+
180+
for (size_t i = 0; i < n; ++i)
181+
{
182+
Serial.print("sys="); Serial.print(samples[i].systolic);
183+
Serial.print(" dia="); Serial.print(samples[i].diastolic);
184+
Serial.print(" hr="); Serial.print(samples[i].heartRate(), 1);
185+
Serial.print(" spo2="); Serial.print(samples[i].spo2(), 1);
186+
Serial.print(" status=");
187+
Serial.println(uint8_t(samples[i].bpStatus));
188+
}
189+
} while (n == 8);
185190
delay(100);
186191
}

examples/07.SaveLoadCalibrationEEPROM/07.SaveLoadCalibrationEEPROM.ino

Lines changed: 12 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -168,14 +168,19 @@ void loop()
168168
{
169169
PulseExpressSample samples[8];
170170
size_t n = 0;
171-
if (hub.readSamples(samples, 8, &n) != PulseExpressStatus::Ok) return;
172-
for (size_t i = 0; i < n; ++i)
171+
// Drain to empty; a hub FIFO left to overflow rejects every later read.
172+
do
173173
{
174-
Serial.print("sys="); Serial.print(samples[i].systolic);
175-
Serial.print(" dia="); Serial.print(samples[i].diastolic);
176-
Serial.print(" hr="); Serial.print(samples[i].heartRate(), 1);
177-
Serial.print(" spo2="); Serial.println(samples[i].spo2(), 1);
178-
}
174+
n = 0;
175+
if (hub.readSamples(samples, 8, &n) != PulseExpressStatus::Ok) return;
176+
for (size_t i = 0; i < n; ++i)
177+
{
178+
Serial.print("sys="); Serial.print(samples[i].systolic);
179+
Serial.print(" dia="); Serial.print(samples[i].diastolic);
180+
Serial.print(" hr="); Serial.print(samples[i].heartRate(), 1);
181+
Serial.print(" spo2="); Serial.println(samples[i].spo2(), 1);
182+
}
183+
} while (n == 8);
179184
delay(100);
180185
}
181186

examples/09.HeartRateVariability/09.HeartRateVariability.ino

Lines changed: 35 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -76,8 +76,24 @@ void setup()
7676
PulseExpressStatus s = hub.begin();
7777
if (s != PulseExpressStatus::Ok) halt("hub.begin()", s);
7878

79-
if (hub.caps().sampleBytes < 25)
80-
Serial.println("NOTE: this firmware does not report IBI (needs >= 40.5.0).");
79+
// HRV here is derived entirely from the hub's inter-beat interval, which
80+
// only exists in the 29-byte sample (>= 40.5.0). On the 23-byte legacy
81+
// sample ibiMs is always zero, so this sketch would silently collect
82+
// nothing forever. Stop with an explanation instead.
83+
if (!hub.caps().extendedSampleFields)
84+
{
85+
while (true)
86+
{
87+
Serial.print("Hub firmware ");
88+
Serial.print(hub.version().major); Serial.print('.');
89+
Serial.print(hub.version().minor); Serial.print('.');
90+
Serial.println(hub.version().patch);
91+
Serial.println("does not report inter-beat intervals (needs >= 40.5.0),");
92+
Serial.println("so HRV cannot be computed. Use 03.HeartRateSpO2 for");
93+
Serial.println("heart rate on this firmware.");
94+
delay(5000);
95+
}
96+
}
8197

8298
s = hub.startEstimation();
8399
if (s != PulseExpressStatus::Ok) halt("startEstimation", s);
@@ -88,24 +104,29 @@ void setup()
88104

89105
void loop()
90106
{
107+
// Drain to empty; a hub FIFO left to overflow rejects every later read.
91108
PulseExpressSample samples[8];
92109
size_t n = 0;
93-
if (hub.readSamples(samples, 8, &n) != PulseExpressStatus::Ok) return;
94-
95-
for (size_t i = 0; i < n; ++i)
110+
do
96111
{
97-
uint16_t ibi = samples[i].ibiMs;
98-
// Accept only fresh, plausible intervals (250..2000 ms ~ 30..240 bpm).
99-
if (ibi >= 250 && ibi <= 2000 && ibi != lastIbi)
112+
n = 0;
113+
if (hub.readSamples(samples, 8, &n) != PulseExpressStatus::Ok) return;
114+
115+
for (size_t i = 0; i < n; ++i)
100116
{
101-
lastIbi = ibi;
102-
ibis[count++] = ibi;
103-
if (count >= WINDOW)
117+
uint16_t ibi = samples[i].ibiMs;
118+
// Accept only fresh, plausible intervals (250..2000 ms ~ 30..240 bpm).
119+
if (ibi >= 250 && ibi <= 2000 && ibi != lastIbi)
104120
{
105-
computeHrv();
106-
count = 0;
121+
lastIbi = ibi;
122+
ibis[count++] = ibi;
123+
if (count >= WINDOW)
124+
{
125+
computeHrv();
126+
count = 0;
127+
}
107128
}
108129
}
109-
}
130+
} while (n == 8);
110131
delay(50);
111132
}

src/protocentral_pulse_express.cpp

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -242,6 +242,7 @@ PulseExpressCaps PulseExpress::capsFor(PulseExpressVersion v) const
242242
c.sampleBytes = 29;
243243
c.dateYYYYMMDD = true;
244244
c.multiPointCalib = true;
245+
c.extendedSampleFields = true;
245246
}
246247
return c;
247248
}
@@ -781,10 +782,11 @@ void PulseExpress::parseSample(const uint8_t *buf, PulseExpressSample &s) const
781782

782783
// 40.5.0+ extension fields. Zeroed when caps reports a smaller sample
783784
// size so user code can rely on a consistent struct layout.
784-
s.ibiMs = (_caps.sampleBytes >= 25) ? pack16BE(&buf[23]) : 0;
785-
s.spo2Confidence = (_caps.sampleBytes >= 26) ? buf[25] : 0;
786-
s.bptReportFlag = (_caps.sampleBytes >= 27) ? buf[26] : 0;
787-
s.spo2ReportFlag = (_caps.sampleBytes >= 28) ? buf[27] : 0;
785+
const bool ext = _caps.extendedSampleFields;
786+
s.ibiMs = ext ? pack16BE(&buf[23]) : 0;
787+
s.spo2Confidence = ext ? buf[25] : 0;
788+
s.bptReportFlag = ext ? buf[26] : 0;
789+
s.spo2ReportFlag = ext ? buf[27] : 0;
788790
}
789791

790792
/////////////////////////////////////////////////////////////////////////////////////////

src/protocentral_pulse_express.h

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -129,6 +129,12 @@ struct PulseExpressCaps
129129
bool multiPointCalib = false; // false: 0x50/04/01+02 | true: 0x50/04/07+08
130130
bool sendBpMedication = true; // dropped in 40.2.2+
131131
bool sendRestMode = true; // dropped in 40.2.2+
132+
/// The 29-byte sample adds IBI, SpO2 confidence and the two report flags
133+
/// (>=40.5.0). On the 23-byte legacy sample those PulseExpressSample fields
134+
/// are always zero, so features built on them (HRV from IBI, SpO2
135+
/// confidence gating) are unavailable — branch on this rather than testing
136+
/// sampleBytes, and never present a zero from them as a real reading.
137+
bool extendedSampleFields = false;
132138
};
133139

134140
/**

0 commit comments

Comments
 (0)