diff --git a/MAINTAINER_NOTES.md b/MAINTAINER_NOTES.md new file mode 100644 index 0000000..127634e --- /dev/null +++ b/MAINTAINER_NOTES.md @@ -0,0 +1,22 @@ +# Maintainer Notes + +## 2026-06-09 maintenance branch + +This branch is intentionally kept on the `2.5.1` release line so behavior can be tested against the published release without introducing a new public version number. + +The maintenance goals are: + +- align the Arduino Library Manager metadata and in-code version macros with release `v2.5.1` +- make sensor index checks reject `sensorIndex == SensorCount` +- make `sawNewSample()` match its documented behavior when hardware timers are used +- align timer setup, pause, and resume platform macros for nRF52 and Due +- fix small timer setup issues found by inspection before adding broader compile coverage +- add an end-user ZIP install smoke test so the branch can be tested like an Arduino IDE library install +- add a manual hardware test plan and draft `2.5.2` release notes for the next maintainer pass + +Open follow-up areas: + +- Add GitHub Actions smoke tests for supported Arduino cores that can be compiled automatically. A workflow file was prepared during this branch, but pushing `.github/workflows/arduino-compile.yml` requires a GitHub token with `workflow` scope. +- Expand CI to optional examples that require display, websocket, Bluetooth, and web server dependencies. +- Decide whether Servo timer selection should depend on `Servo.h` being included by the sketch instead of merely available in the installed core. +- Add board-specific manual test notes for Pulse Transit Time and speaker/tone timer interactions. diff --git a/library.properties b/library.properties index 04fb809..990738c 100644 --- a/library.properties +++ b/library.properties @@ -1,5 +1,5 @@ name=PulseSensor Playground -version=2.5.0 +version=2.5.1 author=Joel Murphy, Yury Gitman, Brad Needham maintainer=Joel Murphy, Yury Gitman sentence=Support at PulseSensor.com diff --git a/resources/EndUserInstallTesting.md b/resources/EndUserInstallTesting.md new file mode 100644 index 0000000..2541f9e --- /dev/null +++ b/resources/EndUserInstallTesting.md @@ -0,0 +1,41 @@ +# End User Install Testing + +Use this test before merging changes that might affect Arduino IDE users. + +The goal is to verify the library the way an end user gets it: as an installed Arduino library, not as a checked-out source tree. + +## ZIP install smoke test + +Run this from the repository root: + +```sh +scripts/end-user-install-test.sh +``` + +The script: + +1. creates a ZIP from the current Git commit +2. installs that ZIP into a temporary Arduino sketchbook +3. compiles examples from the installed library folder + +By default it compiles `PulseSensor_XIAO_ESP32S3_BPM` for `esp32:esp32:XIAO_ESP32S3`, because that catches the current ESP32-S3 support path without requiring optional display or web server libraries. + +## Broader board matrix + +Set `PULSE_SENSOR_END_USER_MATRIX` to run more boards. Each line uses: + +```text +name|core|fqbn|sketch|additional_board_manager_urls +``` + +Example: + +```sh +PULSE_SENSOR_INSTALL_CORES=1 PULSE_SENSOR_END_USER_MATRIX='Uno BPM|arduino:avr|arduino:avr:uno|examples/PulseSensor_BPM| +Mega two-sensor|arduino:avr|arduino:avr:mega|examples/TwoPulseSensors_On_OneArduino| +UNO R4 WiFi plotter|arduino:renesas_uno|arduino:renesas_uno:unor4wifi|examples/PulseSensor_UNO_R4_WiFi_LEDmatrix_Plotter| +XIAO ESP32-S3 BPM|esp32:esp32|esp32:esp32:XIAO_ESP32S3|examples/PulseSensor_XIAO_ESP32S3_BPM|https://espressif.github.io/arduino-esp32/package_esp32_index.json' \ +scripts/end-user-install-test.sh +``` + +Some optional examples require extra libraries such as display, websocket, Bluetooth, or async web server packages. Keep those in a separate optional matrix so the core library install test remains focused and dependable. diff --git a/resources/MaintenanceBranchHandoff.md b/resources/MaintenanceBranchHandoff.md new file mode 100644 index 0000000..1320084 --- /dev/null +++ b/resources/MaintenanceBranchHandoff.md @@ -0,0 +1,126 @@ +# Maintenance Branch Handoff + +Branch: + +```text +codex/maintenance-traceability-ci +``` + +Draft PR: + +```text +https://github.com/WorldFamousElectronics/PulseSensorPlayground/pull/207 +``` + +## Current State + +The remote PR branch includes: + +- timer and bounds maintenance fixes +- version metadata alignment to `2.5.1` +- maintainer notes +- end-user ZIP install smoke test +- manual hardware test plan +- draft `2.5.2` release notes + +The local branch has one extra commit that is not pushed: + +```text +7e624af Add Arduino compile smoke workflow +``` + +That commit adds: + +```text +.github/workflows/arduino-compile.yml +``` + +It is local-only because GitHub rejects workflow-file pushes unless the token has `workflow` scope. + +## Workflow Auth Step + +When a maintainer is available, run: + +```sh +gh auth refresh -h github.com -s workflow +``` + +Complete the GitHub browser/device-code approval. Then push: + +```sh +cd /Users/narwhal2/Documents/PulseSensorPlayground +git push +``` + +If the local branch is not checked out: + +```sh +cd /Users/narwhal2/Documents/PulseSensorPlayground +git switch codex/maintenance-traceability-ci +git push +``` + +## Local Validation Already Run + +Basic checks: + +```sh +git diff --check +ruby -e 'require "yaml"; YAML.load_file(".github/workflows/arduino-compile.yml"); puts "YAML OK"' +``` + +End-user install check: + +```sh +scripts/end-user-install-test.sh +``` + +Expanded installed-library compile matrix on `esp32:esp32:XIAO_ESP32S3`: + +- `GettingStartedProject` +- `Getting_BPM_to_Monitor` +- `PulseSensor_BPM` +- `PulseSensor_Pulse_Transit_Time` +- `TwoPulseSensors_On_OneArduino` +- `PulseSensor_XIAO_ESP32S3_BPM` + +The test installs a ZIP of the current commit into a temporary Arduino sketchbook and compiles from the installed library folder. + +## Known Local Limitation + +AVR and UNO R4 compiles were attempted locally, but this Mac's installed Arduino toolchains fail before compilation with: + +```text +bad CPU type in executable +``` + +Use the GitHub Actions workflow or another machine to validate: + +- `arduino:avr:uno` +- `arduino:avr:mega` +- `arduino:renesas_uno:unor4wifi` + +## Hardware Still Needed + +Run the checklist in: + +```text +resources/ManualHardwareTestPlan.md +``` + +Highest-value first pass: + +1. Arduino Uno R3 or Nano with `PulseSensor_BPM` +2. XIAO ESP32-S3 with `PulseSensor_XIAO_ESP32S3_BPM` +3. UNO R4 WiFi with LED matrix plotter +4. Two-sensor PTT setup + +## Related Docs PR + +Separate draft PR: + +```text +https://github.com/WorldFamousElectronics/PulseSensorPlayground/pull/208 +``` + +That PR links WebSerial and CYD resources without mixing them into the maintenance-fix PR. diff --git a/resources/ManualHardwareTestPlan.md b/resources/ManualHardwareTestPlan.md new file mode 100644 index 0000000..f5b8029 --- /dev/null +++ b/resources/ManualHardwareTestPlan.md @@ -0,0 +1,105 @@ +# Manual Hardware Test Plan + +Use this checklist before merging or releasing maintenance changes to PulseSensor Playground. + +The goal is to confirm that the library works for end users after installation through Arduino IDE or a downloaded ZIP, not only from a developer checkout. + +## Install Path + +1. Download the branch ZIP: + + ```text + https://github.com/WorldFamousElectronics/PulseSensorPlayground/archive/refs/heads/codex/maintenance-traceability-ci.zip + ``` + +2. In Arduino IDE, use `Sketch > Include Library > Add .ZIP Library...`. +3. Confirm examples appear under `File > Examples > PulseSensor Playground`. +4. Open each target example from the Examples menu, not from the Git checkout. + +## Boards To Smoke Test + +### Arduino Uno R3 or Nano + +Examples: + +- `PulseSensor_BPM` +- `PulseSensor_Speaker` +- `TwoPulseSensors_On_OneArduino` + +Checks: + +- Upload succeeds. +- Serial Plotter shows live samples. +- BPM becomes non-zero after several clean beats. +- LED blink follows `sawStartOfBeat()`. +- Speaker example uses PWM-style beep behavior without `tone()` duration. + +### Arduino Mega 2560 + +Example: + +- `TwoPulseSensors_On_OneArduino` + +Checks: + +- Upload succeeds. +- Both channels produce independent sample traces. +- Calling two-sensor methods does not hang or reset the board. +- Out-of-range sensor indexes are ignored or return error values rather than reading past the sensor array. + +### Arduino UNO R4 WiFi + +Examples: + +- `PulseSensor_UNO_R4_WiFi_LEDmatrix_Plotter` +- `PulseSensor_UNO_R4_WiFi_LEDmatrix_Heartbeat` + +Checks: + +- Upload succeeds. +- LED matrix updates. +- Serial Plotter output remains readable. +- `pause()` / `resume()` can be tried manually if a small sketch is available. + +### Seeed XIAO ESP32-S3 + +Example: + +- `PulseSensor_XIAO_ESP32S3_BPM` + +Checks: + +- Upload succeeds. +- Serial Monitor reports BPM after clean beats. +- `analogReadResolution(10)` behavior remains compatible with Playground's 0..1023 detector assumptions where examples use ESP32 ADC. + +### Pulse Transit Time Setup + +Example: + +- `PulseSensor_Pulse_Transit_Time` + +Checks: + +- Both sensors share the same voltage and ground. +- Visualizer or Serial Plotter shows two clean waveforms. +- PTT values change only when both channels detect beats. +- Fingertip/earlobe placement gives cleaner signals than wrist placement. + +## Regression Areas + +- `sawNewSample()` should return `true` after hardware-timer samples. +- `sensorIndex == SensorCount` should not access outside the sensor array. +- `pause()` and `resume()` should return `true` on supported hardware timer platforms. +- Unsupported boards should fall back to software timing with a compile warning. +- Library version output should report `v2.5.1` while this branch remains a test branch. + +## Pass Criteria + +This maintenance branch is ready to merge when: + +- At least one AVR board passes. +- XIAO ESP32-S3 passes. +- UNO R4 WiFi passes, or any failure is documented as board-core/toolchain specific. +- The branch ZIP install path works in Arduino IDE. +- No example requires editing the installed library files to compile. diff --git a/resources/ReleaseNotes-2.5.2-draft.md b/resources/ReleaseNotes-2.5.2-draft.md new file mode 100644 index 0000000..602498b --- /dev/null +++ b/resources/ReleaseNotes-2.5.2-draft.md @@ -0,0 +1,55 @@ +# PulseSensor Playground 2.5.2 Draft Release Notes + +This draft assumes the maintenance branch is tested and merged after `v2.5.1`. + +## Summary + +PulseSensor Playground 2.5.2 is a maintenance release focused on version traceability, timer behavior, bounds checks, and install-path testing. + +## Changes + +- Align `library.properties` and in-code version macros with the published `v2.5.1` baseline before the next version bump. +- Fix sensor-index validation so `sensorIndex == SensorCount` is rejected instead of treated as valid. +- Make `sawNewSample()` reflect hardware-timer samples as documented. +- Align hardware timer pause/resume platform checks for nRF52 and Arduino Due. +- Fix ESP8266 timer setup so it uses the shared timer object and returns success. +- Fix ESP32 timer disable so `pause()` can report success. +- Fix Arduino Mega timer disable mismatch when Timer2 is selected. +- Guard Renesas timer setup if no timer is available after forcing PWM-reserved timer use. +- Fix an ATtiny85 1 MHz timer setup typo. +- Add an end-user ZIP install smoke test for Arduino IDE-style library installation. +- Add manual hardware validation notes for release testing. + +## Testing Notes + +Automated/local: + +- `git diff --check` +- XIAO ESP32-S3 BPM example compile with `arduino-cli` +- ZIP install smoke test with `scripts/end-user-install-test.sh` +- Remote GitHub branch ZIP install and compile smoke test +- Expanded installed-library compile pass on `esp32:esp32:XIAO_ESP32S3`: + - `GettingStartedProject` + - `Getting_BPM_to_Monitor` + - `PulseSensor_BPM` + - `PulseSensor_Pulse_Transit_Time` + - `TwoPulseSensors_On_OneArduino` + - `PulseSensor_XIAO_ESP32S3_BPM` + +Manual hardware testing recommended before release: + +- Arduino Uno R3 or Nano +- Arduino Mega 2560 +- Arduino UNO R4 WiFi +- Seeed XIAO ESP32-S3 +- Two-sensor Pulse Transit Time setup + +## Release Process Notes + +After merge and hardware validation: + +1. Bump `library.properties` from `2.5.1` to `2.5.2`. +2. Bump `PULSESENSOR_PLAYGROUND_VERSION_STRING` to `v2.5.2`. +3. Bump `PULSESENSOR_PLAYGROUND_VERSION_NUMBER` to `252`. +4. Tag the release as `v2.5.2`. +5. Publish release notes using this draft as the starting point. diff --git a/scripts/end-user-install-test.sh b/scripts/end-user-install-test.sh new file mode 100755 index 0000000..4a26e3c --- /dev/null +++ b/scripts/end-user-install-test.sh @@ -0,0 +1,64 @@ +#!/usr/bin/env bash +set -euo pipefail + +repo_root="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" +tmp_dir="$(mktemp -d)" +cleanup() { + rm -rf "$tmp_dir" +} +trap cleanup EXIT + +zip_path="$tmp_dir/PulseSensorPlayground.zip" +sketchbook="$tmp_dir/sketchbook" +config_file="$tmp_dir/arduino-cli.yaml" + +mkdir -p "$sketchbook" + +cat > "$config_file" <&2 + exit 1 +fi + +default_matrix="XIAO ESP32-S3 BPM|esp32:esp32|esp32:esp32:XIAO_ESP32S3|examples/PulseSensor_XIAO_ESP32S3_BPM|https://espressif.github.io/arduino-esp32/package_esp32_index.json" +matrix="${PULSE_SENSOR_END_USER_MATRIX:-$default_matrix}" + +while IFS='|' read -r name core fqbn sketch additional_urls; do + [[ -z "${name:-}" ]] && continue + + if [[ "${PULSE_SENSOR_INSTALL_CORES:-0}" == "1" ]]; then + if [[ -n "${additional_urls:-}" ]]; then + arduino-cli --config-file "$config_file" core update-index --additional-urls "$additional_urls" + arduino-cli --config-file "$config_file" core install "$core" --additional-urls "$additional_urls" + else + arduino-cli --config-file "$config_file" core update-index + arduino-cli --config-file "$config_file" core install "$core" + fi + fi + + echo "Compiling installed-library example: $name" + arduino-cli --config-file "$config_file" compile \ + --fqbn "$fqbn" \ + "$installed_library/$sketch" \ + --warnings default +done <<< "$matrix" + +echo "End-user ZIP install test passed for installed library: $installed_library" diff --git a/src/PulseSensorPlayground.cpp b/src/PulseSensorPlayground.cpp index ce971c6..94252d4 100644 --- a/src/PulseSensorPlayground.cpp +++ b/src/PulseSensorPlayground.cpp @@ -34,7 +34,7 @@ PulseSensorPlayground::PulseSensorPlayground(int numberOfSensors) { #endif // Dynamically create the array to minimize ram usage. - SensorCount = (byte) numberOfSensors; + SensorCount = (byte) constrain(numberOfSensors, 1, 255); Sensors = new PulseSensor[SensorCount]; // set our internal variable to reflect hardware timer use @@ -83,21 +83,21 @@ bool PulseSensorPlayground::PulseSensorPlayground::begin() { } void PulseSensorPlayground::analogInput(int inputPin, int sensorIndex) { - if (sensorIndex != constrain(sensorIndex, 0, SensorCount)) { + if (!isValidSensorIndex(sensorIndex)) { return; // out of range. } Sensors[sensorIndex].analogInput(inputPin); } void PulseSensorPlayground::blinkOnPulse(int blinkPin, int sensorIndex) { - if (sensorIndex != constrain(sensorIndex, 0, SensorCount)) { + if (!isValidSensorIndex(sensorIndex)) { return; // out of range. } Sensors[sensorIndex].blinkOnPulse(blinkPin); } void PulseSensorPlayground::fadeOnPulse(int fadePin, int sensorIndex) { - if (sensorIndex != constrain(sensorIndex, 0, SensorCount)) { + if (!isValidSensorIndex(sensorIndex)) { return; // out of range. } Sensors[sensorIndex].fadeOnPulse(fadePin); @@ -117,10 +117,10 @@ bool PulseSensorPlayground::sawNewSample() { if(!Paused){ if (UsingHardwareTimer) { // Disable interrupts to avoid a race with the ISR. - // DISABLE_PULSE_SENSOR_INTERRUPTS; + DISABLE_PULSE_SENSOR_INTERRUPTS; bool sawOne = SawNewSample; SawNewSample = false; - // ENABLE_PULSE_SENSOR_INTERRUPTS; + ENABLE_PULSE_SENSOR_INTERRUPTS; result = sawOne; } else { @@ -166,46 +166,51 @@ void PulseSensorPlayground::onSampleTime() { } // Set the flag that says we've read a sample since the Sketch checked. + SawNewSample = true; // digitalWrite(timingPin,LOW); // optionally connect timingPin to oscilloscope to time algorithm run time } +bool PulseSensorPlayground::isValidSensorIndex(int sensorIndex) { + return sensorIndex >= 0 && sensorIndex < SensorCount; +} + int PulseSensorPlayground::getLatestSample(int sensorIndex) { - if (sensorIndex != constrain(sensorIndex, 0, SensorCount)) { + if (!isValidSensorIndex(sensorIndex)) { return -1; // out of range. } return Sensors[sensorIndex].getLatestSample(); } int PulseSensorPlayground::getBeatsPerMinute(int sensorIndex) { - if (sensorIndex != constrain(sensorIndex, 0, SensorCount)) { + if (!isValidSensorIndex(sensorIndex)) { return -1; // out of range. } return Sensors[sensorIndex].getBeatsPerMinute(); } int PulseSensorPlayground::getInterBeatIntervalMs(int sensorIndex) { - if (sensorIndex != constrain(sensorIndex, 0, SensorCount)) { + if (!isValidSensorIndex(sensorIndex)) { return -1; // out of range. } return Sensors[sensorIndex].getInterBeatIntervalMs(); } bool PulseSensorPlayground::sawStartOfBeat(int sensorIndex) { - if (sensorIndex != constrain(sensorIndex, 0, SensorCount)) { + if (!isValidSensorIndex(sensorIndex)) { return false; // out of range. } return Sensors[sensorIndex].sawStartOfBeat(); } bool PulseSensorPlayground::isInsideBeat(int sensorIndex) { - if (sensorIndex != constrain(sensorIndex, 0, SensorCount)) { + if (!isValidSensorIndex(sensorIndex)) { return false; // out of range. } return Sensors[sensorIndex].isInsideBeat(); } void PulseSensorPlayground::setThreshold(int threshold, int sensorIndex) { - if (sensorIndex != constrain(sensorIndex, 0, SensorCount)) { + if (!isValidSensorIndex(sensorIndex)) { return; // out of range. } Sensors[sensorIndex].setThreshold(threshold); @@ -236,14 +241,14 @@ void PulseSensorPlayground::setThreshold(int threshold, int sensorIndex) { #endif int PulseSensorPlayground::getPulseAmplitude(int sensorIndex) { - if (sensorIndex != constrain(sensorIndex, 0, SensorCount)) { + if (!isValidSensorIndex(sensorIndex)) { return -1; // out of range. } return Sensors[sensorIndex].getPulseAmplitude(); } unsigned long PulseSensorPlayground::getLastBeatTime(int sensorIndex) { - if (sensorIndex != constrain(sensorIndex, 0, SensorCount)) { + if (!isValidSensorIndex(sensorIndex)) { return -1; // out of range. } return Sensors[sensorIndex].getLastBeatTime(); @@ -440,7 +445,7 @@ bool PulseSensorPlayground::setupInterrupt(){ #elif F_CPU == 8000000L TCCR1 = 0x88; // Clear Timer on Compare, Set Prescaler to 128 #elif F_CPU == 1000000L - TCCR1 = 0x85 // Clear Timer on Compare, Set Prescaler to 16 + TCCR1 = 0x85; // Clear Timer on Compare, Set Prescaler to 16 #endif bitSet(TIMSK,6); // Enable interrupt on match between TCNT1 and OCR1A ENABLE_PULSE_SENSOR_INTERRUPTS; @@ -450,15 +455,17 @@ bool PulseSensorPlayground::setupInterrupt(){ #if defined(ARDUINO_ARCH_RENESAS) uint8_t timer_type = GPT_TIMER; int8_t tindex = FspTimer::get_available_timer(timer_type); - if(tindex == 0){ + if(tindex < 0){ FspTimer::force_use_of_pwm_reserved_timer(); tindex = FspTimer::get_available_timer(timer_type); } - sampleTimer.begin(TIMER_MODE_PERIODIC, timer_type, tindex, SAMPLE_RATE_500HZ, 0.0f, sampleTimerISR); - sampleTimer.setup_overflow_irq(); - sampleTimer.open(); - sampleTimer.start(); - result = true; + if(tindex >= 0){ + sampleTimer.begin(TIMER_MODE_PERIODIC, timer_type, tindex, SAMPLE_RATE_500HZ, 0.0f, sampleTimerISR); + sampleTimer.setup_overflow_irq(); + sampleTimer.open(); + sampleTimer.start(); + result = true; + } #endif #if defined(ARDUINO_SAM_DUE) @@ -497,9 +504,9 @@ bool PulseSensorPlayground::setupInterrupt(){ #endif #if defined(ARDUINO_ARCH_ESP8266) - ESP8266Timer sampleTimer; sampleTimer.setFrequency(500,onInterrupt); sampleTimer.restartTimer(); + result = true; #endif #if defined(ARDUINO_SAMD_ZERO) || defined(ARDUINO_ARCH_SAMD) @@ -566,7 +573,7 @@ bool PulseSensorPlayground::enableInterrupt(){ result = true; #endif - #if defined(ARDUINO_ARCH_NRF52840) + #if defined(ARDUINO_NRF52_ADAFRUIT) sampleTimer.restartTimer(); result = true; #endif @@ -586,7 +593,7 @@ bool PulseSensorPlayground::enableInterrupt(){ result = true; #endif - #if defined(ARDUINO_ARCH_SAM) + #if defined(ARDUINO_SAM_DUE) sampleTimer.start(2000); result = true; #endif @@ -631,7 +638,7 @@ bool PulseSensorPlayground::disableInterrupt(){ result = true; #else DISABLE_PULSE_SENSOR_INTERRUPTS; - TIMSK3 = 0x00; // Disable OCR2A match interrupt + TIMSK2 = 0x00; // Disable OCR2A match interrupt ENABLE_PULSE_SENSOR_INTERRUPTS; result = true; #endif @@ -646,9 +653,10 @@ bool PulseSensorPlayground::disableInterrupt(){ #if defined(ARDUINO_ARCH_ESP32) timerStop(sampleTimer); + result = true; #endif - #if defined(ARDUINO_ARCH_NRF52840) + #if defined(ARDUINO_NRF52_ADAFRUIT) sampleTimer.stopTimer(); result = true; #endif @@ -668,7 +676,7 @@ bool PulseSensorPlayground::disableInterrupt(){ result = true; #endif - #if defined(ARDUINO_ARCH_SAM) + #if defined(ARDUINO_SAM_DUE) sampleTimer.stop(); result = true; #endif diff --git a/src/PulseSensorPlayground.h b/src/PulseSensorPlayground.h index 8a8821a..bb91fb3 100644 --- a/src/PulseSensorPlayground.h +++ b/src/PulseSensorPlayground.h @@ -33,8 +33,8 @@ /* Library version number */ -#define PULSESENSOR_PLAYGROUND_VERSION_STRING "v2.1.0" -#define PULSESENSOR_PLAYGROUND_VERSION_NUMBER 210 +#define PULSESENSOR_PLAYGROUND_VERSION_STRING "v2.5.1" +#define PULSESENSOR_PLAYGROUND_VERSION_NUMBER 251 /* If you wish to perform timing statistics on your non-interrupt Sketch: @@ -443,6 +443,7 @@ vvvvvvvv THIS NEEDS MODIFICATION FOR V2 vvvvvvvv bool setupInterrupt(); bool disableInterrupt(); bool enableInterrupt(); +bool isValidSensorIndex(int sensorIndex); /* Varialbles