From 56940b075e6ed1914a535b43eac0761542ad3f02 Mon Sep 17 00:00:00 2001 From: Jordan Shaw Date: Thu, 13 Aug 2026 10:13:33 -0400 Subject: [PATCH 1/3] Release v2.2.0: rename to NewPingPlus, Teensy 4 support, yield() fix Renames the library from NewPingESP8266 to NewPingPlus and prepares it for submission to the Arduino Library Manager. Renamed and synced with NewPing v1.9.7: - NewPingESP8266.{h,cpp} -> NewPingPlus.{h,cpp}; class and macros renamed - TRIGGER_WIDTH 12us, automatic one-pin mode detection, constructor drives the trigger pin LOW, protected members, PING_MEDIAN_DELAY 30000us - Examples converted from .pde to .ino, with 3.3V divider and ESP8266 boot-pin warnings Bug fixes found in pre-release review: - yield() was compiled away on every platform. The header carried a `#ifndef yield / #define yield()` fallback, but every supported core declares yield() as a function, never a macro, so the guard was always taken and the empty macro replaced every yield() call in the library. The WDT/WiFi cooperation documented in v2.1.0 was silently inert. - Teensy 4.x failed to compile. The i.MX RT1062 has 32-bit port registers that cannot be assigned to the volatile uint8_t* members the bitwise path uses. Teensy 4 now uses the digitalWrite path; 3.x is unchanged. - ping_async() was non-functional in one-pin mode: the interrupt was attached before the trigger pulse, so the library's own pulse on the shared pin fired the ISR, reported a bogus ~12us echo, and detached the interrupt before the real echo arrived. - ICACHE_RAM_ATTR is deprecated on ESP8266 core 3.x; use IRAM_ATTR with a fallback for cores older than 2.5.0. - set_temperature() shifted the configured range by 1cm by recovering the max distance from _maxEchoTime instead of storing it. - max_cm_distance + 1 overflowed 16-bit unsigned int on AVR, leaving the echo timeout at 0 so every ping returned NO_ECHO. Clamp comes first now. - Config macros are #ifndef-guarded so -D build flags apply. A sketch-level #define can never work across translation units; docs corrected. Packaging: - library.properties, .gitignore; examples/.DS_Store untracked - convert_mm() added to keywords.txt and the README API table - CI: host tests, arduino-lint, and a compile matrix over AVR, ESP8266, ESP32 and Teensy 3.2/4.0 Testing: 67 host tests pass (was 52); new regression tests cover the yield, set_temperature and clamping fixes and were mutation-tested against the broken code. Compiles warning-free on arduino:avr:uno, esp8266:esp8266:nodemcuv2, esp32:esp32:esp32 and teensy:avr:teensy31, teensy40 and teensy41. arduino-lint --library-manager submit reports no errors or warnings. Known limitation: ROUNDING_ENABLED=true remains an untested configuration; the test suite hardcodes truncation expectations. Default is unchanged. Co-Authored-By: Claude Opus 5 --- .github/workflows/ci.yml | 100 +++ .gitignore | 8 + Makefile | 39 + NewPingESP8266.cpp | 214 ------ NewPingESP8266.h | 222 ------ NewPingPlus.cpp | 410 +++++++++++ NewPingPlus.h | 309 ++++++++ README.md | 241 ++++++- RELEASE_NOTES.md | 170 +++++ examples/.DS_Store | Bin 6148 -> 0 bytes .../NewPingAsyncExample.ino | 72 ++ .../NewPingESP8266Example.pde | 22 - .../NewPingPlusExample/NewPingPlusExample.ino | 82 +++ keywords.txt | 19 +- library.properties | 10 + test/mock/Arduino.h | 81 +++ test/mock_arduino.cpp | 79 ++ test/mock_arduino.h | 41 ++ test/test_main.cpp | 673 ++++++++++++++++++ 19 files changed, 2313 insertions(+), 479 deletions(-) create mode 100644 .github/workflows/ci.yml create mode 100644 .gitignore create mode 100644 Makefile delete mode 100755 NewPingESP8266.cpp delete mode 100755 NewPingESP8266.h create mode 100644 NewPingPlus.cpp create mode 100644 NewPingPlus.h create mode 100644 RELEASE_NOTES.md delete mode 100644 examples/.DS_Store create mode 100644 examples/NewPingAsyncExample/NewPingAsyncExample.ino delete mode 100755 examples/NewPingESP8266Example/NewPingESP8266Example.pde create mode 100644 examples/NewPingPlusExample/NewPingPlusExample.ino create mode 100644 library.properties create mode 100644 test/mock/Arduino.h create mode 100644 test/mock_arduino.cpp create mode 100644 test/mock_arduino.h create mode 100644 test/test_main.cpp diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..6a1e91b --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,100 @@ +name: CI + +on: + push: + branches: [master] + tags: ['v*'] + pull_request: + workflow_dispatch: + +jobs: + unit-tests: + name: Host unit tests + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - name: make test + run: make test + + lint: + name: arduino-lint (Library Manager rules) + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: arduino/arduino-lint-action@v1 + with: + library-manager: update + compliance: strict + + compile: + name: Compile ${{ matrix.board.name }} + runs-on: ubuntu-latest + strategy: + fail-fast: false + matrix: + board: + - name: Arduino Uno + fqbn: arduino:avr:uno + platform: arduino:avr + url: '' + - name: ESP8266 NodeMCU + fqbn: esp8266:esp8266:nodemcuv2 + platform: esp8266:esp8266 + url: https://arduino.esp8266.com/stable/package_esp8266com_index.json + - name: ESP32 Dev Module + fqbn: esp32:esp32:esp32 + platform: esp32:esp32 + url: https://espressif.github.io/arduino-esp32/package_esp32_index.json + - name: Teensy 3.2 + fqbn: teensy:avr:teensy31 + platform: teensy:avr + url: https://www.pjrc.com/teensy/package_teensy_index.json + # Teensy 4.x uses 32-bit port registers and must stay on the + # digitalWrite path — this entry is what catches a regression there. + - name: Teensy 4.0 + fqbn: teensy:avr:teensy40 + platform: teensy:avr + url: https://www.pjrc.com/teensy/package_teensy_index.json + steps: + - uses: actions/checkout@v4 + - uses: arduino/compile-sketches@v1 + with: + fqbn: ${{ matrix.board.fqbn }} + platforms: | + - name: ${{ matrix.board.platform }} + source-url: ${{ matrix.board.url }} + libraries: | + - source-path: ./ + # The async example is guarded with #error off ESP, so only compile it there. + sketch-paths: | + - examples/NewPingPlusExample + enable-warnings-report: true + + compile-async: + name: Compile async example (${{ matrix.board.name }}) + runs-on: ubuntu-latest + strategy: + fail-fast: false + matrix: + board: + - name: ESP8266 NodeMCU + fqbn: esp8266:esp8266:nodemcuv2 + platform: esp8266:esp8266 + url: https://arduino.esp8266.com/stable/package_esp8266com_index.json + - name: ESP32 Dev Module + fqbn: esp32:esp32:esp32 + platform: esp32:esp32 + url: https://espressif.github.io/arduino-esp32/package_esp32_index.json + steps: + - uses: actions/checkout@v4 + - uses: arduino/compile-sketches@v1 + with: + fqbn: ${{ matrix.board.fqbn }} + platforms: | + - name: ${{ matrix.board.platform }} + source-url: ${{ matrix.board.url }} + libraries: | + - source-path: ./ + sketch-paths: | + - examples/NewPingAsyncExample + enable-warnings-report: true diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..60ef9ef --- /dev/null +++ b/.gitignore @@ -0,0 +1,8 @@ +# Compiled host test binaries (built by `make test`) +test/run_tests* + +# macOS +.DS_Store + +# Claude Code local working files +.claude/ diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..08d7c72 --- /dev/null +++ b/Makefile @@ -0,0 +1,39 @@ +# NewPingPlus — native unit tests +# +# Usage: +# make test compile and run all tests +# make clean remove compiled binary +# +# Requirements: a C++14-capable compiler (g++ or clang++) on macOS or Linux. +# No Arduino IDE, ESP toolchain, or hardware required. +# +# The test build defines ARDUINO=100 (so NewPingPlus.h takes the +# branch) and provides a mock Arduino.h under test/mock/. None of +# ESP8266/ESP32/__AVR__/__arm__ are defined, so the library compiles with +# DO_BITWISE=false and PING_OVERHEAD=1 — the same code path as all ESP targets. + +CXX ?= g++ +CXXFLAGS = -std=c++14 -Wall -Wextra \ + -I. -Itest/mock \ + -DARDUINO=100 + +SRCS = NewPingPlus.cpp \ + test/mock_arduino.cpp \ + test/test_main.cpp + +BIN = test/run_tests + +.PHONY: test clean + +# NOTE: these tests assert the default configuration (ROUNDING_ENABLED=false, +# URM37_ENABLED=false). They compile but do not pass with ROUNDING_ENABLED=true, +# because the expected values are hardcoded for truncation. +test: $(BIN) + @echo "" + ./$(BIN) + +$(BIN): $(SRCS) NewPingPlus.h test/mock/Arduino.h test/mock_arduino.h + $(CXX) $(CXXFLAGS) -o $@ $(SRCS) + +clean: + rm -f $(BIN) diff --git a/NewPingESP8266.cpp b/NewPingESP8266.cpp deleted file mode 100755 index 686aab5..0000000 --- a/NewPingESP8266.cpp +++ /dev/null @@ -1,214 +0,0 @@ -// --------------------------------------------------------------------------- -// Created by Tim Eckel - teckel@leethost.com -// Copyright 2016 License: GNU GPL v3 http://www.gnu.org/licenses/gpl.html -// -// See "NewPingESP8266.h" for purpose, syntax, version history, links, and more. -// --------------------------------------------------------------------------- - -#include "NewPingESP8266.h" - - -// --------------------------------------------------------------------------- -// NewPingESP8266 constructor -// --------------------------------------------------------------------------- - -NewPingESP8266::NewPingESP8266(uint32_t trigger_pin, uint32_t echo_pin, unsigned int max_cm_distance) { -#if DO_BITWISE == true - _triggerBit = digitalPinToBitMask(trigger_pin); // Get the port register bitmask for the trigger pin. - _echoBit = digitalPinToBitMask(echo_pin); // Get the port register bitmask for the echo pin. - - _triggerOutput = portOutputRegister(digitalPinToPort(trigger_pin)); // Get the output port register for the trigger pin. - _echoInput = portInputRegister(digitalPinToPort(echo_pin)); // Get the input port register for the echo pin. - - _triggerMode = (uint32_t *) portModeRegister(digitalPinToPort(trigger_pin)); // Get the port mode register for the trigger pin. -#else - _triggerPin = trigger_pin; - _echoPin = echo_pin; -#endif - - set_max_distance(max_cm_distance); // Call function to set the max sensor distance. - -#if (defined (__arm__) && defined (TEENSYDUINO)) || DO_BITWISE != true - pinMode(echo_pin, INPUT); // Set echo pin to input (on Teensy 3.x (ARM), pins default to disabled, at least one pinMode() is needed for GPIO mode). - pinMode(trigger_pin, OUTPUT); // Set trigger pin to output (on Teensy 3.x (ARM), pins default to disabled, at least one pinMode() is needed for GPIO mode). -#endif - -#if defined (ARDUINO_AVR_YUN) - pinMode(echo_pin, INPUT); // Set echo pin to input for the Arduino Yun, not sure why it doesn't default this way. -#endif - -#if ONE_PIN_ENABLED != true && DO_BITWISE == true - *_triggerMode |= _triggerBit; // Set trigger pin to output. -#endif -} - - -// --------------------------------------------------------------------------- -// Standard ping methods -// --------------------------------------------------------------------------- - -unsigned int NewPingESP8266::ping(unsigned int max_cm_distance) { - if (max_cm_distance > 0) set_max_distance(max_cm_distance); // Call function to set a new max sensor distance. - - if (!ping_trigger()) return NO_ECHO; // Trigger a ping, if it returns false, return NO_ECHO to the calling function. - -#if URM37_ENABLED == true - #if DO_BITWISE == true - while (!(*_echoInput & _echoBit)) // Wait for the ping echo. - #else - while (!digitalRead(_echoPin)) // Wait for the ping echo. - #endif - if (micros() > _max_time) return NO_ECHO; // Stop the loop and return NO_ECHO (false) if we're beyond the set maximum distance. -#else - #if DO_BITWISE == true - while (*_echoInput & _echoBit) // Wait for the ping echo. - #else - while (digitalRead(_echoPin)) // Wait for the ping echo. - #endif - if (micros() > _max_time) return NO_ECHO; // Stop the loop and return NO_ECHO (false) if we're beyond the set maximum distance. -#endif - - return (micros() - (_max_time - _maxEchoTime) - PING_OVERHEAD); // Calculate ping time, include overhead. -} - - -unsigned long NewPingESP8266::ping_cm(unsigned int max_cm_distance) { - unsigned long echoTime = NewPingESP8266::ping(max_cm_distance); // Calls the ping method and returns with the ping echo distance in uS. -#if ROUNDING_ENABLED == false - return (echoTime / US_ROUNDTRIP_CM); // Call the ping method and returns the distance in centimeters (no rounding). -#else - return NewPingESP8266Convert(echoTime, US_ROUNDTRIP_CM); // Convert uS to centimeters. -#endif -} - - -unsigned long NewPingESP8266::ping_in(unsigned int max_cm_distance) { - unsigned long echoTime = NewPingESP8266::ping(max_cm_distance); // Calls the ping method and returns with the ping echo distance in uS. -#if ROUNDING_ENABLED == false - return (echoTime / US_ROUNDTRIP_IN); // Call the ping method and returns the distance in inches (no rounding). -#else - return NewPingESP8266Convert(echoTime, US_ROUNDTRIP_IN); // Convert uS to inches. -#endif -} - - -unsigned long NewPingESP8266::ping_median(uint32_t it, unsigned int max_cm_distance) { - unsigned int uS[it], last; - uint32_t j, i = 0; - unsigned long t; - uS[0] = NO_ECHO; - - while (i < it) { - t = micros(); // Start ping timestamp. - last = ping(max_cm_distance); // Send ping. - - if (last != NO_ECHO) { // Ping in range, include as part of median. - if (i > 0) { // Don't start sort till second ping. - for (j = i; j > 0 && uS[j - 1] < last; j--) // Insertion sort loop. - uS[j] = uS[j - 1]; // Shift ping array to correct position for sort insertion. - } else j = 0; // First ping is sort starting point. - uS[j] = last; // Add last ping to array in sorted position. - i++; // Move to next ping. - } else it--; // Ping out of range, skip and don't include as part of median. - - if (i < it && micros() - t < PING_MEDIAN_DELAY) - delay((PING_MEDIAN_DELAY + t - micros()) / 1000); // Millisecond delay between pings. - - } - return (uS[it >> 1]); // Return the ping distance median. -} - - -// --------------------------------------------------------------------------- -// Standard and timer interrupt ping method support functions (not called directly) -// --------------------------------------------------------------------------- - -boolean NewPingESP8266::ping_trigger() { -#if DO_BITWISE == true - #if ONE_PIN_ENABLED == true - *_triggerMode |= _triggerBit; // Set trigger pin to output. - #endif - - *_triggerOutput &= ~_triggerBit; // Set the trigger pin low, should already be low, but this will make sure it is. - delayMicroseconds(4); // Wait for pin to go low. - *_triggerOutput |= _triggerBit; // Set trigger pin high, this tells the sensor to send out a ping. - delayMicroseconds(10); // Wait long enough for the sensor to realize the trigger pin is high. Sensor specs say to wait 10uS. - *_triggerOutput &= ~_triggerBit; // Set trigger pin back to low. - - #if ONE_PIN_ENABLED == true - *_triggerMode &= ~_triggerBit; // Set trigger pin to input (when using one Arduino pin, this is technically setting the echo pin to input as both are tied to the same Arduino pin). - #endif - - #if URM37_ENABLED == true - if (!(*_echoInput & _echoBit)) return false; // Previous ping hasn't finished, abort. - _max_time = micros() + _maxEchoTime + MAX_SENSOR_DELAY; // Maximum time we'll wait for ping to start (most sensors are <450uS, the SRF06 can take up to 34,300uS!) - while (*_echoInput & _echoBit) // Wait for ping to start. - if (micros() > _max_time) return false; // Took too long to start, abort. - #else - if (*_echoInput & _echoBit) return false; // Previous ping hasn't finished, abort. - _max_time = micros() + _maxEchoTime + MAX_SENSOR_DELAY; // Maximum time we'll wait for ping to start (most sensors are <450uS, the SRF06 can take up to 34,300uS!) - while (!(*_echoInput & _echoBit)) // Wait for ping to start. - if (micros() > _max_time) return false; // Took too long to start, abort. - #endif -#else - #if ONE_PIN_ENABLED == true - pinMode(_triggerPin, OUTPUT); // Set trigger pin to output. - #endif - - digitalWrite(_triggerPin, LOW); // Set the trigger pin low, should already be low, but this will make sure it is. - delayMicroseconds(4); // Wait for pin to go low. - digitalWrite(_triggerPin, HIGH); // Set trigger pin high, this tells the sensor to send out a ping. - delayMicroseconds(10); // Wait long enough for the sensor to realize the trigger pin is high. Sensor specs say to wait 10uS. - digitalWrite(_triggerPin, LOW); // Set trigger pin back to low. - - #if ONE_PIN_ENABLED == true - pinMode(_triggerPin, INPUT); // Set trigger pin to input (when using one Arduino pin, this is technically setting the echo pin to input as both are tied to the same Arduino pin). - #endif - - #if URM37_ENABLED == true - if (!digitalRead(_echoPin)) return false; // Previous ping hasn't finished, abort. - _max_time = micros() + _maxEchoTime + MAX_SENSOR_DELAY; // Maximum time we'll wait for ping to start (most sensors are <450uS, the SRF06 can take up to 34,300uS!) - while (digitalRead(_echoPin)) // Wait for ping to start. - if (micros() > _max_time) return false; // Took too long to start, abort. - #else - if (digitalRead(_echoPin)) return false; // Previous ping hasn't finished, abort. - _max_time = micros() + _maxEchoTime + MAX_SENSOR_DELAY; // Maximum time we'll wait for ping to start (most sensors are <450uS, the SRF06 can take up to 34,300uS!) - while (!digitalRead(_echoPin)) // Wait for ping to start. - if (micros() > _max_time) return false; // Took too long to start, abort. - #endif -#endif - - _max_time = micros() + _maxEchoTime; // Ping started, set the time-out. - return true; // Ping started successfully. -} - - -void NewPingESP8266::set_max_distance(unsigned int max_cm_distance) { -#if ROUNDING_ENABLED == false - _maxEchoTime = min(max_cm_distance + 1, (unsigned int) MAX_SENSOR_DISTANCE + 1) * US_ROUNDTRIP_CM; // Calculate the maximum distance in uS (no rounding). -#else - _maxEchoTime = min(max_cm_distance, (unsigned int) MAX_SENSOR_DISTANCE) * US_ROUNDTRIP_CM + (US_ROUNDTRIP_CM / 2); // Calculate the maximum distance in uS. -#endif -} - - -// --------------------------------------------------------------------------- -// Conversion methods (rounds result to nearest cm or inch). -// --------------------------------------------------------------------------- - -unsigned int NewPingESP8266::convert_cm(unsigned int echoTime) { -#if ROUNDING_ENABLED == false - return (echoTime / US_ROUNDTRIP_CM); // Convert uS to centimeters (no rounding). -#else - return NewPingESP8266Convert(echoTime, US_ROUNDTRIP_CM); // Convert uS to centimeters. -#endif -} - - -unsigned int NewPingESP8266::convert_in(unsigned int echoTime) { -#if ROUNDING_ENABLED == false - return (echoTime / US_ROUNDTRIP_IN); // Convert uS to inches (no rounding). -#else - return NewPingESP8266Convert(echoTime, US_ROUNDTRIP_IN); // Convert uS to inches. -#endif -} diff --git a/NewPingESP8266.h b/NewPingESP8266.h deleted file mode 100755 index cc60703..0000000 --- a/NewPingESP8266.h +++ /dev/null @@ -1,222 +0,0 @@ -// --------------------------------------------------------------------------- -// NewPingESP8266 Library - v1.8 - 07/30/2016 -// -// AUTHOR/LICENSE: -// Created by Tim Eckel - teckel@leethost.com -// Copyright 2016 License: GNU GPL v3 http://www.gnu.org/licenses/gpl.html -// -// LINKS: -// Project home: https://bitbucket.org/teckel12/arduino-new-ping/wiki/Home -// Blog: http://arduino.cc/forum/index.php/topic,106043.0.html -// -// DISCLAIMER: -// This software is furnished "as is", without technical support, and with no -// warranty, express or implied, as to its usefulness for any purpose. -// -// BACKGROUND: -// When I first received an ultrasonic sensor I was not happy with how poorly -// it worked. Quickly I realized the problem wasn't the sensor, it was the -// available ping and ultrasonic libraries causing the problem. The NewPingESP8266 -// library totally fixes these problems, adds many new features, and breaths -// new life into these very affordable distance sensors. -// -// FEATURES: -// * Works with many different ultrasonic sensors: SR04, SRF05, SRF06, DYP-ME007, URM37 & Parallax PING))). -// * Compatible with the entire Arduino line-up (and clones), Teensy family (including $19 96Mhz 32 bit Teensy 3.2) and non-AVR microcontrollers. -// * Interface with all but the SRF06 sensor using only one Arduino pin. -// * Doesn't lag for a full second if no ping/echo is received. -// * Ping sensors consistently and reliably at up to 30 times per second. -// * Timer interrupt method for event-driven sketches. -// * Built-in digital filter method ping_median() for easy error correction. -// * Uses port registers for a faster pin interface and smaller code size. -// * Allows you to set a maximum distance where pings beyond that distance are read as no ping "clear". -// * Ease of using multiple sensors (example sketch with 15 sensors). -// * More accurate distance calculation (cm, inches & uS). -// * Doesn't use pulseIn, which is slow and gives incorrect results with some ultrasonic sensor models. -// * Actively developed with features being added and bugs/issues addressed. -// -// CONSTRUCTOR: -// NewPingESP8266 sonar(trigger_pin, echo_pin [, max_cm_distance]) -// trigger_pin & echo_pin - Arduino pins connected to sensor trigger and echo. -// NOTE: To use the same Arduino pin for trigger and echo, specify the same pin for both values. -// max_cm_distance - [Optional] Maximum distance you wish to sense. Default=500cm. -// -// METHODS: -// sonar.ping([max_cm_distance]) - Send a ping and get the echo time (in microseconds) as a result. [max_cm_distance] allows you to optionally set a new max distance. -// sonar.ping_in([max_cm_distance]) - Send a ping and get the distance in whole inches. [max_cm_distance] allows you to optionally set a new max distance. -// sonar.ping_cm([max_cm_distance]) - Send a ping and get the distance in whole centimeters. [max_cm_distance] allows you to optionally set a new max distance. -// sonar.ping_median(iterations [, max_cm_distance]) - Do multiple pings (default=5), discard out of range pings and return median in microseconds. [max_cm_distance] allows you to optionally set a new max distance. -// NewPingESP8266::convert_in(echoTime) - Convert echoTime from microseconds to inches (rounds to nearest inch). -// NewPingESP8266::convert_cm(echoTime) - Convert echoTime from microseconds to centimeters (rounds to nearest cm). -// sonar.ping_timer(function [, max_cm_distance]) - Send a ping and call function to test if ping is complete. [max_cm_distance] allows you to optionally set a new max distance. -// sonar.check_timer() - Check if ping has returned within the set distance limit. -// NewPingESP8266::timer_us(frequency, function) - Call function every frequency microseconds. -// NewPingESP8266::timer_ms(frequency, function) - Call function every frequency milliseconds. -// NewPingESP8266::timer_stop() - Stop the timer. -// -// HISTORY: -// 07/30/2016 v1.8 - Added support for non-AVR microcontrollers. For non-AVR -// microcontrollers, advanced ping_timer() timer methods are disabled due to -// inconsistencies or no support at all between platforms. However, standard -// ping methods are all supported. Added new optional variable to ping(), -// ping_in(), ping_cm(), ping_median(), and ping_timer() methods which allows -// you to set a new maximum distance for each ping. Added support for the -// ATmega16, ATmega32 and ATmega8535 microcontrollers. Changed convert_cm() -// and convert_in() methods to static members. You can now call them without -// an object. For example: cm = NewPingESP8266::convert_cm(distance); -// -// 09/29/2015 v1.7 - Removed support for the Arduino Due and Zero because -// they're both 3.3 volt boards and are not 5 volt tolerant while the HC-SR04 -// is a 5 volt sensor. Also, the Due and Zero don't support pin manipulation -// compatibility via port registers which can be done (see the Teensy 3.2). -// -// 06/17/2014 v1.6 - Corrected delay between pings when using ping_median() -// method. Added support for the URM37 sensor (must change URM37_ENABLED from -// false to true). Added support for Arduino microcontrollers like the $20 -// 32 bit ARM Cortex-M4 based Teensy 3.2. Added automatic support for the -// Atmel ATtiny family of microcontrollers. Added timer support for the -// ATmega8 microcontroller. Rounding disabled by default, reduces compiled -// code size (can be turned on with ROUNDING_ENABLED switch). Added -// TIMER_ENABLED switch to get around compile-time "__vector_7" errors when -// using the Tone library, or you can use the toneAC, NewTone or -// TimerFreeTone libraries: https://bitbucket.org/teckel12/arduino-toneac/ -// Other speed and compiled size optimizations. -// -// 08/15/2012 v1.5 - Added ping_median() method which does a user specified -// number of pings (default=5) and returns the median ping in microseconds -// (out of range pings ignored). This is a very effective digital filter. -// Optimized for smaller compiled size (even smaller than sketches that -// don't use a library). -// -// 07/14/2012 v1.4 - Added support for the Parallax PING)))� sensor. Interface -// with all but the SRF06 sensor using only one Arduino pin. You can also -// interface with the SRF06 using one pin if you install a 0.1uf capacitor -// on the trigger and echo pins of the sensor then tie the trigger pin to -// the Arduino pin (doesn't work with Teensy). To use the same Arduino pin -// for trigger and echo, specify the same pin for both values. Various bug -// fixes. -// -// 06/08/2012 v1.3 - Big feature addition, event-driven ping! Uses Timer2 -// interrupt, so be mindful of PWM or timing conflicts messing with Timer2 -// may cause (namely PWM on pins 3 & 11 on Arduino, PWM on pins 9 and 10 on -// Mega, and Tone library). Simple to use timer interrupt functions you can -// use in your sketches totally unrelated to ultrasonic sensors (don't use if -// you're also using NewPingESP8266's ping_timer because both use Timer2 interrupts). -// Loop counting ping method deleted in favor of timing ping method after -// inconsistent results kept surfacing with the loop timing ping method. -// Conversion to cm and inches now rounds to the nearest cm or inch. Code -// optimized to save program space and fixed a couple minor bugs here and -// there. Many new comments added as well as line spacing to group code -// sections for better source readability. -// -// 05/25/2012 v1.2 - Lots of code clean-up thanks to Arduino Forum members. -// Rebuilt the ping timing code from scratch, ditched the pulseIn code as it -// doesn't give correct results (at least with ping sensors). The NewPingESP8266 -// library is now VERY accurate and the code was simplified as a bonus. -// Smaller and faster code as well. Fixed some issues with very close ping -// results when converting to inches. All functions now return 0 only when -// there's no ping echo (out of range) and a positive value for a successful -// ping. This can effectively be used to detect if something is out of range -// or in-range and at what distance. Now compatible with Arduino 0023. -// -// 05/16/2012 v1.1 - Changed all I/O functions to use low-level port registers -// for ultra-fast and lean code (saves from 174 to 394 bytes). Tested on both -// the Arduino Uno and Teensy 2.0 but should work on all Arduino-based -// platforms because it calls standard functions to retrieve port registers -// and bit masks. Also made a couple minor fixes to defines. -// -// 05/15/2012 v1.0 - Initial release. -// --------------------------------------------------------------------------- - -#ifndef NewPingESP8266_h -#define NewPingESP8266_h - -#if defined (ARDUINO) && ARDUINO >= 100 - #include -#else - #include - #include -#endif - -#if defined (__AVR__) - #include - #include -#endif - -// Shouldn't need to change these values unless you have a specific need to do so. -#define MAX_SENSOR_DISTANCE 500 // Maximum sensor distance can be as high as 500cm, no reason to wait for ping longer than sound takes to travel this distance and back. Default=500 -#define US_ROUNDTRIP_CM 57 // Microseconds (uS) it takes sound to travel round-trip 1cm (2cm total), uses integer to save compiled code space. Default=57 -#define US_ROUNDTRIP_IN 146 // Microseconds (uS) it takes sound to travel round-trip 1 inch (2 inches total), uses integer to save compiled code space. Defalult=146 -#define ONE_PIN_ENABLED true // Set to "false" to disable one pin mode which saves around 14-26 bytes of binary size. Default=true -#define ROUNDING_ENABLED false // Set to "true" to enable distance rounding which also adds 64 bytes to binary size. Default=false -#define URM37_ENABLED false // Set to "true" to enable support for the URM37 sensor in PWM mode. Default=false - -// Probably shouldn't change these values unless you really know what you're doing. -#define NO_ECHO 0 // Value returned if there's no ping echo within the specified MAX_SENSOR_DISTANCE or max_cm_distance. Default=0 -#define MAX_SENSOR_DELAY 5800 // Maximum uS it takes for sensor to start the ping. Default=5800 -#define ECHO_TIMER_FREQ 24 // Frequency to check for a ping echo (every 24uS is about 0.4cm accuracy). Default=24 -#define PING_MEDIAN_DELAY 29000 // Microsecond delay between pings in the ping_median method. Default=29000 -#define PING_OVERHEAD 5 // Ping overhead in microseconds (uS). Default=5 -#define PING_TIMER_OVERHEAD 13 // Ping timer overhead in microseconds (uS). Default=13 -#if URM37_ENABLED == true - #undef US_ROUNDTRIP_CM - #undef US_ROUNDTRIP_IN - #define US_ROUNDTRIP_CM 50 // Every 50uS PWM signal is low indicates 1cm distance. Default=50 - #define US_ROUNDTRIP_IN 127 // If 50uS is 1cm, 1 inch would be 127uS (50 x 2.54 = 127). Default=127 -#endif - -// Conversion from uS to distance (round result to nearest cm or inch). -#define NewPingESP8266Convert(echoTime, conversionFactor) (max(((unsigned int)echoTime + conversionFactor / 2) / conversionFactor, (echoTime ? 1 : 0))) - -// Detect non-AVR microcontrollers (Teensy 3.x, Arduino DUE, etc.) and don't use port registers or timer interrupts as required. -#if (defined (__arm__) && defined (TEENSYDUINO)) - #undef PING_OVERHEAD - #define PING_OVERHEAD 1 - #undef PING_TIMER_OVERHEAD - #define PING_TIMER_OVERHEAD 1 - #define DO_BITWISE true -#elif !defined (__AVR__) - #undef PING_OVERHEAD - #define PING_OVERHEAD 1 - #undef PING_TIMER_OVERHEAD - #define PING_TIMER_OVERHEAD 1 - #define DO_BITWISE false -#else - #define DO_BITWISE true -#endif - -// Define timers when using ATmega8, ATmega16, ATmega32 and ATmega8535 microcontrollers. -#if defined (__AVR_ATmega8__) || defined (__AVR_ATmega16__) || defined (__AVR_ATmega32__) || defined (__AVR_ATmega8535__) - #define OCR2A OCR2 - #define TIMSK2 TIMSK - #define OCIE2A OCIE2 -#endif - -class NewPingESP8266 { - public: - NewPingESP8266(uint32_t trigger_pin, uint32_t echo_pin, unsigned int max_cm_distance = MAX_SENSOR_DISTANCE); - unsigned int ping(unsigned int max_cm_distance = 0); - unsigned long ping_cm(unsigned int max_cm_distance = 0); - unsigned long ping_in(unsigned int max_cm_distance = 0); - unsigned long ping_median(uint32_t it = 5, unsigned int max_cm_distance = 0); - static unsigned int convert_cm(unsigned int echoTime); - static unsigned int convert_in(unsigned int echoTime); - private: - boolean ping_trigger(); - void set_max_distance(unsigned int max_cm_distance); -#if DO_BITWISE == true - uint32_t _triggerBit; - uint32_t _echoBit; - volatile uint32_t *_triggerOutput; - volatile uint32_t *_echoInput; - volatile uint32_t *_triggerMode; -#else - uint32_t _triggerPin; - uint32_t _echoPin; -#endif - unsigned int _maxEchoTime; - unsigned long _max_time; -}; - - -#endif diff --git a/NewPingPlus.cpp b/NewPingPlus.cpp new file mode 100644 index 0000000..64e20c9 --- /dev/null +++ b/NewPingPlus.cpp @@ -0,0 +1,410 @@ +// --------------------------------------------------------------------------- +// NewPingPlus v2.2.0 — see NewPingPlus.h for full documentation. +// +// DESIGN NOTES: +// +// OVERFLOW-SAFE TIMING (v2.1+) +// micros() wraps every ~71 minutes. The original NewPing "deadline" pattern: +// _max_time = micros() + timeout; +// while (...) { if (micros() > _max_time) ... } +// fails at the 32-bit boundary: if micros() wraps *during* the loop, it +// becomes a small number that never exceeds the large deadline → infinite +// loop → WDT reset. We use elapsed-time arithmetic instead: +// unsigned long start = micros(); +// while (...) { if (micros() - start > timeout) ... } +// Unsigned subtraction wraps correctly at any boundary — always safe. +// (Tim Eckel tried the same fix in v1.9.5 but reverted in v1.9.6 due to +// coupling with his timer interrupt path. We have no timer methods on ESP, +// so the elapsed-time fix is clean and correct here.) +// +// yield() PLACEMENT +// yield() on ESP8266/ESP32 feeds the software WDT and processes WiFi events. +// It must NOT be called inside the echo-measurement window (waiting for the +// echo pin to go LOW after it went HIGH): +// - Max echo wait is ~30 ms — well under the 1-second WDT threshold. +// - yield() can take hundreds of µs for WiFi processing, corrupting timing. +// yield() IS called in ping_trigger()'s sensor-startup wait (before timing +// begins) and between pings in ping_median() (we're idle for 30 ms anyway). +// +// ONE-PIN MODE (v2.2, ported from NewPing v1.9.7) +// Replaces the ONE_PIN_ENABLED compile-time flag. The constructor detects +// single-wire mode by comparing trigger_pin and echo_pin at runtime. +// The trigger pin is driven LOW in the constructor so ping_trigger() can +// immediately go HIGH without a pre-LOW delay. +// +// TRIGGER_WIDTH (v2.2, ported from NewPing v1.9.7) +// Default 12 µs (up from 10). Some clone sensors need the extra margin. +// --------------------------------------------------------------------------- + +#include "NewPingPlus.h" + +// --------------------------------------------------------------------------- +// Static members +// --------------------------------------------------------------------------- + +#if defined(NEWPING_PLUS_ASYNC) +NewPingPlus *NewPingPlus::_isrInstance = nullptr; +#endif + +// --------------------------------------------------------------------------- +// Constructor +// --------------------------------------------------------------------------- + +NewPingPlus::NewPingPlus(uint8_t trigger_pin, uint8_t echo_pin, + unsigned int max_cm_distance) + : _ping_start(0), _us_per_cm(US_ROUNDTRIP_CM) +{ + _one_pin_mode = (trigger_pin == echo_pin); // Automatic single-wire detection. + +#if DO_BITWISE == true + _triggerBit = digitalPinToBitMask(trigger_pin); + _echoBit = digitalPinToBitMask(echo_pin); + _triggerOutput = portOutputRegister(digitalPinToPort(trigger_pin)); + _echoInput = portInputRegister(digitalPinToPort(echo_pin)); + _triggerMode = (volatile uint8_t *) portModeRegister(digitalPinToPort(trigger_pin)); + + // Teensy 3.x (ARM): pins default to disabled — at least one pinMode() is + // required before port-register access will work in GPIO mode. + #if defined(__arm__) && defined(TEENSYDUINO) + pinMode(echo_pin, INPUT); + pinMode(trigger_pin, OUTPUT); + #endif + + *_triggerMode |= _triggerBit; // Trigger pin → output. + *_triggerOutput &= ~_triggerBit; // Trigger pin → LOW (ready for next pulse). +#else + _triggerPin = trigger_pin; + _echoPin = echo_pin; + + pinMode(echo_pin, INPUT); + pinMode(trigger_pin, OUTPUT); + digitalWrite(_triggerPin, LOW); // Drive trigger LOW so ping_trigger() can + // immediately go HIGH — no pre-delay needed. +#endif + + set_max_distance(max_cm_distance); + +#if defined(ARDUINO_AVR_YUN) + pinMode(echo_pin, INPUT); +#endif + + // ESP8266 boot-pin runtime warning. + // GPIO 0, 2 and 15 set boot mode at reset. A sensor driving one of these + // to the wrong level during power-on causes boot failure. + // Safe NodeMCU pins: GPIO 4(D2), 5(D1), 12(D6), 13(D7), 14(D5). +#if defined(ESP8266) + if (echo_pin == 0 || echo_pin == 2 || echo_pin == 15) { + Serial.println( + "[NewPingPlus] WARNING: echo_pin is an ESP8266 boot-strapping pin " + "(GPIO 0/2/15). A sensor driving this pin during power-on may prevent " + "the board from booting. Use GPIO 4, 5, 12, 13 or 14 instead."); + } +#endif + +#if defined(NEWPING_PLUS_ASYNC) + _asyncStart = 0; + _asyncCallback = nullptr; +#endif +} + +// --------------------------------------------------------------------------- +// Public ping methods +// --------------------------------------------------------------------------- + +unsigned int NewPingPlus::ping(unsigned int max_cm_distance) { + if (max_cm_distance > 0) set_max_distance(max_cm_distance); + + if (!ping_trigger()) return NO_ECHO; + + // Wait for echo pin to fall (end of echo pulse). + // Do NOT call yield() here — we are inside the timing window. + // Max wait is _maxEchoTime µs (~30 ms at 500 cm), well under WDT limit. +#if URM37_ENABLED == true + #if DO_BITWISE == true + while (!(*_echoInput & _echoBit)) + #else + while (!digitalRead(_echoPin)) + #endif + if (micros() - _ping_start > _maxEchoTime) return NO_ECHO; +#else + #if DO_BITWISE == true + while (*_echoInput & _echoBit) + #else + while (digitalRead(_echoPin)) + #endif + if (micros() - _ping_start > _maxEchoTime) return NO_ECHO; +#endif + + unsigned long elapsed = micros() - _ping_start; + if (elapsed < PING_OVERHEAD) return NO_ECHO; + return (unsigned int)(elapsed - PING_OVERHEAD); +} + + +unsigned long NewPingPlus::ping_cm(unsigned int max_cm_distance) { + unsigned long echoTime = ping(max_cm_distance); + if (echoTime == NO_ECHO) return NO_ECHO; +#if ROUNDING_ENABLED == false + return (unsigned long)(echoTime / _us_per_cm); +#else + return (unsigned long)((echoTime + _us_per_cm / 2.0f) / _us_per_cm); +#endif +} + + +unsigned long NewPingPlus::ping_in(unsigned int max_cm_distance) { + unsigned long echoTime = ping(max_cm_distance); + if (echoTime == NO_ECHO) return NO_ECHO; + float us_per_in = _us_per_cm * 2.54f; +#if ROUNDING_ENABLED == false + return (unsigned long)(echoTime / us_per_in); +#else + return (unsigned long)((echoTime + us_per_in / 2.0f) / us_per_in); +#endif +} + + +unsigned long NewPingPlus::ping_mm(unsigned int max_cm_distance) { + unsigned long echoTime = ping(max_cm_distance); + if (echoTime == NO_ECHO) return NO_ECHO; +#if ROUNDING_ENABLED == false + return (unsigned long)(echoTime * 10.0f / _us_per_cm); +#else + // Honour ROUNDING_ENABLED like ping_cm()/ping_in() do — otherwise ping_mm() + // truncates while ping_cm() rounds, and ping_mm() can report less than + // ping_cm() * 10. + return (unsigned long)(echoTime * 10.0f / _us_per_cm + 0.5f); +#endif +} + + +unsigned long NewPingPlus::ping_median(uint8_t it, unsigned int max_cm_distance) { + if (it > PING_MEDIAN_MAX_IT) it = PING_MEDIAN_MAX_IT; + + unsigned int uS[PING_MEDIAN_MAX_IT]; // Fixed-size array — no VLA. + uint8_t j, i = 0; + uint8_t remaining = it; + uS[0] = NO_ECHO; + + while (i < remaining) { + unsigned long ping_start_us = micros(); + unsigned int last = ping(max_cm_distance); + + if (last != NO_ECHO) { + if (i > 0) { + for (j = i; j > 0 && uS[j - 1] < last; j--) + uS[j] = uS[j - 1]; // Insertion sort. + } else { + j = 0; + } + uS[j] = last; + i++; + } else { + if (remaining > 0) remaining--; // Skip out-of-range, shrink window. + } + + // Wait between pings. Elapsed-time arithmetic avoids overflow. + // delay() (not delayMicroseconds) feeds the WDT on ESP platforms. + // >> 10 (~÷1024) is a fast ms approximation, same as NewPing v1.9.7. + if (i < remaining) { + unsigned long elapsed = micros() - ping_start_us; + if (elapsed < PING_MEDIAN_DELAY) + delay((PING_MEDIAN_DELAY - elapsed) >> 10); + } + yield(); // Feed WDT / process WiFi between pings. + } + + return (remaining > 0) ? uS[remaining >> 1] : NO_ECHO; +} + +// --------------------------------------------------------------------------- +// Temperature compensation +// --------------------------------------------------------------------------- + +void NewPingPlus::set_temperature(float temperature_c) { + // Speed of sound (m/s) = 331.3 + 0.606 × T(°C) [Bohn 1988] + // Round-trip µs per cm = 20000 / speed_m_per_s + // + // Recompute the echo timeout from the *stored* max distance. Deriving it from + // _maxEchoTime / _us_per_cm instead would truncate and shift the effective + // range by 1cm on the first call. + _us_per_cm = 20000.0f / (331.3f + 0.606f * temperature_c); + set_max_distance(_max_cm_distance); +} + +// --------------------------------------------------------------------------- +// Static conversion helpers (use compile-time default sound speed) +// --------------------------------------------------------------------------- + +unsigned int NewPingPlus::convert_cm(unsigned int echoTime) { +#if ROUNDING_ENABLED == false + return (echoTime / US_ROUNDTRIP_CM); +#else + return NewPingPlusConvert(echoTime, US_ROUNDTRIP_CM); +#endif +} + +unsigned int NewPingPlus::convert_in(unsigned int echoTime) { +#if ROUNDING_ENABLED == false + return (echoTime / US_ROUNDTRIP_IN); +#else + return NewPingPlusConvert(echoTime, US_ROUNDTRIP_IN); +#endif +} + +unsigned int NewPingPlus::convert_mm(unsigned int echoTime) { + // echoTime * 10 overflows 16-bit unsigned int on AVR, so widen to long. +#if ROUNDING_ENABLED == false + return (unsigned int)((unsigned long)echoTime * 10UL / US_ROUNDTRIP_CM); +#else + return (unsigned int)(((unsigned long)echoTime * 10UL + US_ROUNDTRIP_CM / 2) + / US_ROUNDTRIP_CM); +#endif +} + +// --------------------------------------------------------------------------- +// Interrupt-driven async ping (ESP8266 / ESP32 only) +// --------------------------------------------------------------------------- + +#if defined(NEWPING_PLUS_ASYNC) + +void NewPingPlus::ping_async(void (*callback)(unsigned int)) { + if (digitalRead(_echoPin)) { + // Echo pin already HIGH — previous ping still in flight. + callback(NO_ECHO); + return; + } + + _asyncCallback = callback; + _asyncStart = 0; + _isrInstance = this; + + // Two-pin mode: attach BEFORE firing the trigger so we never miss the rising + // edge. HC-SR04 raises echo ~150–250 µs after trigger; the 12 µs trigger pulse + // is well within that window. + // + // One-pin mode: the trigger pulse is driven onto the echo line itself, so + // attaching first makes our own pulse fire the ISR — reporting a bogus ~12 µs + // "echo" and detaching the interrupt before the real echo ever arrives. Attach + // after the pulse instead; the sensor's response gap is far longer than the + // pulse, so no edge is lost. + if (!_one_pin_mode) + attachInterrupt(digitalPinToInterrupt(_echoPin), _echoISR, CHANGE); + + if (_one_pin_mode) pinMode(_triggerPin, OUTPUT); + digitalWrite(_triggerPin, HIGH); + delayMicroseconds(TRIGGER_WIDTH); + digitalWrite(_triggerPin, LOW); + + if (_one_pin_mode) { + pinMode(_triggerPin, INPUT); + attachInterrupt(digitalPinToInterrupt(_echoPin), _echoISR, CHANGE); + } +} + + +void NEWPING_ISR_ATTR NewPingPlus::_echoISR() { + if (!_isrInstance) return; + + if (digitalRead(_isrInstance->_echoPin)) { + // Rising edge — echo pulse started. + _isrInstance->_asyncStart = micros(); + } else { + // Falling edge — echo pulse ended. + // Detach before calling callback to prevent re-entry. + detachInterrupt(digitalPinToInterrupt(_isrInstance->_echoPin)); + + if (_isrInstance->_asyncCallback) { + unsigned int result = NO_ECHO; + + // A falling edge with no preceding rising edge (glitch on the line) + // still reports NO_ECHO — otherwise the ping would be dropped silently + // with the interrupt already detached, and the caller would wait forever. + if (_isrInstance->_asyncStart > 0) { + unsigned long duration = micros() - _isrInstance->_asyncStart; // overflow-safe + if (duration <= _isrInstance->_maxEchoTime) + result = (unsigned int)duration; + } + + _isrInstance->_asyncStart = 0; + _isrInstance->_asyncCallback(result); + } + } +} + +#endif // NEWPING_PLUS_ASYNC + +// --------------------------------------------------------------------------- +// Protected helpers +// --------------------------------------------------------------------------- + +boolean NewPingPlus::ping_trigger() { + // Fire the trigger pulse. Trigger pin is already LOW from the constructor + // (or from the previous ping), so we go directly HIGH. +#if DO_BITWISE == true + *_triggerMode |= _triggerBit; // Output (no-op if already output). + *_triggerOutput |= _triggerBit; // HIGH — sensor starts ping. + delayMicroseconds(TRIGGER_WIDTH); + *_triggerOutput &= ~_triggerBit; // LOW. + if (_one_pin_mode) *_triggerMode &= ~_triggerBit; // Switch to input for echo. +#else + if (_one_pin_mode) pinMode(_triggerPin, OUTPUT); + digitalWrite(_triggerPin, HIGH); + delayMicroseconds(TRIGGER_WIDTH); + digitalWrite(_triggerPin, LOW); + if (_one_pin_mode) pinMode(_triggerPin, INPUT); +#endif + + // Wait for the sensor to signal it has started transmitting. + // yield() is safe here — timing hasn't started yet. + unsigned long wait_start = micros(); + +#if URM37_ENABLED == true + // URM37: echo pin is HIGH when idle, falls when ping begins. + #if DO_BITWISE == true + if (!(*_echoInput & _echoBit)) return false; + while (*_echoInput & _echoBit) { + #else + if (!digitalRead(_echoPin)) return false; + while (digitalRead(_echoPin)) { + #endif + if (micros() - wait_start > (unsigned long)_maxEchoTime + MAX_SENSOR_DELAY) + return false; + yield(); + } +#else + // HC-SR04 and most sensors: echo is LOW when idle, HIGH when ping begins. + #if DO_BITWISE == true + if (*_echoInput & _echoBit) return false; + while (!(*_echoInput & _echoBit)) { + #else + if (digitalRead(_echoPin)) return false; // Already high — sensor busy. + while (!digitalRead(_echoPin)) { + #endif + if (micros() - wait_start > (unsigned long)_maxEchoTime + MAX_SENSOR_DELAY) + return false; + yield(); + } +#endif + + // Echo pulse just began. Record start time for elapsed-time measurement in ping(). + _ping_start = micros(); + return true; +} + + +void NewPingPlus::set_max_distance(unsigned int max_cm_distance) { + // Clamp BEFORE the +1 below. On AVR `unsigned int` is 16-bit, so a caller + // passing 65535 would wrap max_cm_distance+1 to 0 and leave _maxEchoTime at + // 0 — every subsequent ping would return NO_ECHO. + if (max_cm_distance > MAX_SENSOR_DISTANCE) + max_cm_distance = MAX_SENSOR_DISTANCE; + + _max_cm_distance = max_cm_distance; // Remembered for set_temperature(). + +#if ROUNDING_ENABLED == false + _maxEchoTime = (unsigned int)((max_cm_distance + 1UL) * _us_per_cm); +#else + _maxEchoTime = (unsigned int)(max_cm_distance * _us_per_cm + _us_per_cm / 2.0f); +#endif +} diff --git a/NewPingPlus.h b/NewPingPlus.h new file mode 100644 index 0000000..9418155 --- /dev/null +++ b/NewPingPlus.h @@ -0,0 +1,309 @@ +// --------------------------------------------------------------------------- +// NewPingPlus Library - v2.2.0 - 2026 +// +// ORIGINAL AUTHOR: +// Tim Eckel - eckel.tim@gmail.com +// NewPing v1.8, Copyright 2016, GNU GPL v3 +// https://bitbucket.org/teckel12/arduino-new-ping/wiki/Home +// +// FORKED AND MAINTAINED BY: +// Studio Jordan Shaw - https://studiojordanshaw.com +// Additions copyright 2016–2026, GNU GPL v3 +// https://github.com/jshaw/NewPingPlus +// +// Forked and maintained by Studio Jordan Shaw © 2016–2026. +// +// DISCLAIMER: +// This software is furnished "as is", without technical support, and with +// no warranty, express or implied, as to its usefulness for any purpose. +// +// SUPPORTED BOARDS: +// ESP8266 · ESP32 · Arduino (AVR) · Teensy 3.x · Teensy 4.x +// +// SUPPORTED SENSORS: +// HC-SR04 · SRF05 · SRF06 · DYP-ME007 · URM37 · Parallax PING))) +// +// WHAT'S DIFFERENT FROM THE ORIGINAL NEWPING: +// • Overflow-safe timing — fixes WDT resets near the 71-min micros() wrap +// • yield() between pings — keeps ESP8266/ESP32 WiFi stack alive +// • Automatic one-pin mode detection (trigger == echo pin) +// • set_temperature(°C) — compensates speed of sound for ambient temp +// • ping_mm() — millimetre-precision measurements +// • ping_async(callback) — non-blocking interrupt-driven ping (ESP only) +// • ping_median() fixed-size array — no VLA, no stack overflow risk +// • Protected members — subclassable for custom sensor variants +// • library.properties — Arduino Library Manager compatible +// +// CONSTRUCTOR: +// NewPingPlus sonar(trigger_pin, echo_pin [, max_cm_distance]) +// trigger_pin — GPIO connected to sensor TRIG +// echo_pin — GPIO connected to sensor ECHO (see 3.3V warning) +// max_cm_distance — optional max range in cm, default 500 +// +// Pass the same pin for both trigger and echo to use single-wire mode +// (Parallax PING))). Detected automatically at runtime. +// +// METHODS: +// sonar.ping([max_cm_distance]) +// Echo time in microseconds. Returns 0 if out of range. +// +// sonar.ping_cm([max_cm_distance]) +// Distance in whole centimetres. Returns 0 if out of range. +// +// sonar.ping_in([max_cm_distance]) +// Distance in whole inches. Returns 0 if out of range. +// +// sonar.ping_mm([max_cm_distance]) +// Distance in whole millimetres. Returns 0 if out of range. +// Finer than ping_cm() — useful for close-range work. +// +// sonar.ping_median(iterations [, max_cm_distance]) +// Runs multiple pings (default 5, max 25), discards out-of-range +// results, returns the median in microseconds. Good noise filter. +// +// sonar.set_temperature(celsius) +// Adjusts speed-of-sound for ambient temperature (default ~20 °C). +// Formula: v = 331.3 + 0.606 × T m/s [Bohn 1988] +// +// sonar.ping_async(callback) [ESP8266 / ESP32 only] +// Non-blocking ping. Fires trigger and returns immediately. Calls +// callback(unsigned int echoTime) via GPIO interrupt when echo ends. +// echoTime is in µs; 0 = out of range. Safe alongside WiFi/MQTT/OTA. +// Only one async ping may be in-flight at a time (all instances). +// +// NewPingPlus::convert_cm(echoTime) +// Static: µs → cm (uses default sound speed constant). +// +// NewPingPlus::convert_in(echoTime) +// Static: µs → inches (uses default sound speed constant). +// +// NewPingPlus::convert_mm(echoTime) +// Static: µs → millimetres (uses default sound speed constant). +// Use with a single ping() to get cm + mm from one measurement. +// +// 3.3V HARDWARE WARNING (ESP8266 / ESP32): +// HC-SR04 ECHO outputs 5V. Connecting it directly to a 3.3V ESP GPIO +// will damage the pin over time. Use a voltage divider on the ECHO line: +// +// HC-SR04 ECHO ──[1kΩ]──┬──[2kΩ]── GND +// │ +// ESP GPIO (echo_pin) +// +// TRIG is output-only from the ESP — no divider needed. +// +// ESP8266 BOOT PIN WARNING: +// GPIO 0, 2 and 15 are sampled at reset to set boot mode. A sensor +// driving them during power-on can prevent the sketch from starting. +// Safe NodeMCU pins: GPIO 4(D2), 5(D1), 12(D6), 13(D7), 14(D5). +// The constructor prints a Serial warning if a strapping pin is used. +// +// HISTORY: +// 2026 v2.2.0 - Renamed to NewPingPlus (was NewPingESP8266). Synced +// improvements from NewPing v1.9.7: TRIGGER_WIDTH 12µs, automatic +// one-pin mode detection, constructor drives trigger LOW, protected +// members, PING_MEDIAN_DELAY 30000µs. +// Teensy 4.x support (32-bit port registers need the digitalWrite path). +// Fixed a `#ifndef yield` guard that silently compiled away every yield() +// call on every platform. IRAM_ATTR replaces deprecated ICACHE_RAM_ATTR. +// Config macros are override-guarded. ping_async() fixed in one-pin mode. +// set_temperature() no longer shifts max range by 1cm. +// +// 2024 v2.1.0 - Fixed micros() overflow → WDT reset. Elapsed-time +// arithmetic (micros()-start > timeout) replaces unsafe deadline pattern. +// Moved yield() out of timing window. ESP8266 boot-pin runtime warning. +// +// 2024 v2.0.0 - ESP32 explicit support. ping_mm(). set_temperature(). +// ping_async() interrupt-driven non-blocking ping (ESP8266/ESP32). +// Fixed VLA in ping_median(). Added library.properties. +// +// 07/30/2016 v1.8 - Initial ESP8266 port by Jordan Shaw, based on Tim +// Eckel's NewPing v1.8 (GNU GPL v3). +// --------------------------------------------------------------------------- + +#ifndef NewPingPlus_h +#define NewPingPlus_h + +#if defined(ARDUINO) && ARDUINO >= 100 + #include +#else + #include + #include +#endif + +#if defined(__AVR__) + #include + #include +#endif + +// --------------------------------------------------------------------------- +// Configuration +// --------------------------------------------------------------------------- + +// These are guarded so they can be overridden with a compiler flag that applies +// to the whole build (e.g. PlatformIO build_flags = -DTRIGGER_WIDTH=15, or +// platform.local.txt). A #define in your .ino does NOT work — the sketch and +// NewPingPlus.cpp are separate translation units. To change the default without +// build flags, edit the value here. + +#ifndef MAX_SENSOR_DISTANCE + #define MAX_SENSOR_DISTANCE 500 // Max distance (cm). Default=500 +#endif +#ifndef US_ROUNDTRIP_CM + #define US_ROUNDTRIP_CM 57 // µs/cm round-trip at ~20°C. Default=57 +#endif +#ifndef US_ROUNDTRIP_IN + #define US_ROUNDTRIP_IN 146 // µs/in round-trip. Default=146 +#endif +#ifndef TRIGGER_WIDTH + #define TRIGGER_WIDTH 12 // Trigger pulse µs. Default=12 + // (spec says 10µs; 12 tolerates out-of-spec clones) +#endif +#ifndef ROUNDING_ENABLED + #define ROUNDING_ENABLED false // Round converted distances. Default=false +#endif +#ifndef URM37_ENABLED + #define URM37_ENABLED false // URM37 PWM mode support. Default=false +#endif +#ifndef PING_MEDIAN_MAX_IT + #define PING_MEDIAN_MAX_IT 25 // Max iterations for ping_median(). Default=25 +#endif + +// --------------------------------------------------------------------------- +// Internal constants +// --------------------------------------------------------------------------- + +#define NO_ECHO 0 // Return value when no echo. Default=0 +#define MAX_SENSOR_DELAY 5800 // Max µs for sensor to begin echo. Default=5800 +#define PING_MEDIAN_DELAY 30000 // µs between pings in ping_median(). Default=30000 +#define PING_OVERHEAD 5 // Measurement overhead µs (AVR). Default=5 + +#if URM37_ENABLED == true + #undef US_ROUNDTRIP_CM + #undef US_ROUNDTRIP_IN + #define US_ROUNDTRIP_CM 50 + #define US_ROUNDTRIP_IN 127 +#endif + +#define NewPingPlusConvert(echoTime, conversionFactor) \ + (max(((unsigned int)echoTime + conversionFactor / 2) / conversionFactor, (echoTime ? 1 : 0))) + +// --------------------------------------------------------------------------- +// Platform detection +// --------------------------------------------------------------------------- + +#if defined(__IMXRT1062__) + // Teensy 4.x — i.MX RT1062. Its port registers are 32-bit (volatile uint32_t*), + // which cannot be held in the volatile uint8_t* members the bitwise path uses. + // Use the digitalWrite path instead; Teensy 4 runs at 600 MHz so the extra + // overhead is negligible. + #define DO_BITWISE false + +#elif defined(__arm__) && defined(TEENSYDUINO) + // Teensy 3.x — ARM, uses bitwise port registers but needs pinMode first. + #define DO_BITWISE true + +#elif defined(ESP8266) || defined(ESP32) + // ESP8266 / ESP32 — Xtensa/RISC-V, use digitalWrite path. + #define DO_BITWISE false + #define NEWPING_PLUS_ASYNC // Enables interrupt-driven ping_async() + +#elif !defined(__AVR__) + // Other non-AVR platforms. + #define DO_BITWISE false + +#else + // AVR — use fast port registers. + #define DO_BITWISE true +#endif + +// Non-AVR platforms have fewer clock cycles per loop iteration. +#if !defined(__AVR__) + #undef PING_OVERHEAD + #define PING_OVERHEAD 1 +#endif + +// ISR placement: code must run from RAM on ESP to survive flash cache misses. +// ESP8266 core 3.x deprecates ICACHE_RAM_ATTR in favour of IRAM_ATTR; fall back +// for cores older than 2.5.0 that only define the legacy name. +#if defined(ESP8266) || defined(ESP32) + #if defined(IRAM_ATTR) + #define NEWPING_ISR_ATTR IRAM_ATTR + #else + #define NEWPING_ISR_ATTR ICACHE_RAM_ATTR + #endif +#else + #define NEWPING_ISR_ATTR +#endif + +// NOTE: do NOT add a `#ifndef yield / #define yield()` fallback here. Every +// supported core (AVR, ESP8266, ESP32, Teensy) declares yield() as a *function*, +// never a macro, so such a guard is always taken and silently compiles away every +// yield() call in this library — defeating the WDT/WiFi cooperation it provides. + +// ATmega8/16/32/8535 timer register aliases. +#if defined(__AVR_ATmega8__) || defined(__AVR_ATmega16__) || \ + defined(__AVR_ATmega32__) || defined(__AVR_ATmega8535__) + #define OCR2A OCR2 + #define TIMSK2 TIMSK + #define OCIE2A OCIE2 +#endif + +// --------------------------------------------------------------------------- +// Class +// --------------------------------------------------------------------------- + +class NewPingPlus { +public: + NewPingPlus(uint8_t trigger_pin, uint8_t echo_pin, + unsigned int max_cm_distance = MAX_SENSOR_DISTANCE); + + unsigned int ping(unsigned int max_cm_distance = 0); + unsigned long ping_cm(unsigned int max_cm_distance = 0); + unsigned long ping_in(unsigned int max_cm_distance = 0); + unsigned long ping_mm(unsigned int max_cm_distance = 0); + unsigned long ping_median(uint8_t it = 5, unsigned int max_cm_distance = 0); + + void set_temperature(float temperature_c); + + static unsigned int convert_cm(unsigned int echoTime); + static unsigned int convert_in(unsigned int echoTime); + static unsigned int convert_mm(unsigned int echoTime); + +#if defined(NEWPING_PLUS_ASYNC) + void ping_async(void (*callback)(unsigned int)); +#endif + +protected: // protected so subclasses can extend (e.g. custom sensor variants) + boolean ping_trigger(); + void set_max_distance(unsigned int max_cm_distance); + +#if DO_BITWISE == true + uint8_t _triggerBit; + uint8_t _echoBit; + volatile uint8_t *_triggerOutput; + volatile uint8_t *_echoInput; + volatile uint8_t *_triggerMode; +#else + uint8_t _triggerPin; + uint8_t _echoPin; +#endif + + unsigned long _ping_start; // micros() when echo pulse began; elapsed-time + // arithmetic keeps this overflow-safe. + unsigned int _maxEchoTime; // Max echo µs for the configured max distance. + unsigned int _max_cm_distance; // Configured max range (cm), already clamped. + // Kept so set_temperature() can recompute + // _maxEchoTime from the original distance rather + // than dividing it back out (which drifted by 1cm). + float _us_per_cm; // µs/cm round-trip; updated by set_temperature(). + bool _one_pin_mode;// true when trigger_pin == echo_pin (auto-detected). + +#if defined(NEWPING_PLUS_ASYNC) + volatile unsigned long _asyncStart; + void (*_asyncCallback)(unsigned int); + static NewPingPlus *_isrInstance; + static void NEWPING_ISR_ATTR _echoISR(); +#endif +}; + +#endif diff --git a/README.md b/README.md index 336c8b6..226bbea 100755 --- a/README.md +++ b/README.md @@ -1,26 +1,239 @@ -# NewPingESP8266 -#### NewPing port for ESP8266 (Arduino IDE) +# NewPingPlus -NewPing Library (Ultrasonic Sensors) +Ultrasonic distance sensor library for **ESP8266, ESP32, Arduino (AVR) and Teensy 3.x/4.x**, ported and extended from Tim Eckel's [NewPing](https://bitbucket.org/teckel12/arduino-new-ping/wiki/Home). -Written by Tim Eckel. +[![arduino-library-badge](https://www.ardu-badge.com/badge/NewPingPlus.svg)](https://github.com/jshaw/NewPingPlus) -https://bitbucket.org/teckel12/arduino-new-ping/wiki/Home +> **Migrating from NewPingESP8266?** Replace `#include ` with `#include ` and rename `NewPingESP8266` to `NewPingPlus`. No other changes needed. -Modified for Teensy 3.0 & 3.1 +--- -https://github.com/PaulStoffregen/NewPing +## Supported Sensors -http://forum.pjrc.com/threads/25907-Multiple-HCSR-04-Ultrasonic-sensors-on-teensy-3 +HC-SR04 · SRF05 · SRF06 · DYP-ME007 · URM37 · Parallax PING))) -![Photo](https://raw.githubusercontent.com/PaulStoffregen/NewPing/master/extras/NewPing_photo.jpg) +## Supported Boards -![Screenshot](https://raw.githubusercontent.com/PaulStoffregen/NewPing/master/extras/NewPing_screenshot.png) +ESP8266 (NodeMCU, Wemos D1, Feather HUZZAH, ...) · ESP32 · Arduino AVR · Teensy 3.x · Teensy 4.x -Updated to support ESP8266 (Arduino IDE) by Jordan Shaw +--- -https://github.com/jshaw/NewPingESP8266 +## Installation -Port was influenced by the issue: https://github.com/PaulStoffregen/NewPing/issues/2. +**Arduino IDE Library Manager** *(recommended)* +Search for `NewPingPlus` and click Install. -Also, removed non-compatable ping examples using timers due to incompatability with "Timer interrupt ping methods (won't work with non-AVR, ATmega128 and all ATtiny microcontrollers)" (NewPingESP8266.cpp ln. 198) +**Manual** +Download the zip, then in the Arduino IDE: *Sketch → Include Library → Add .ZIP Library*. + +--- + +## Wiring + +``` +HC-SR04 VCC → 5V +HC-SR04 GND → GND +HC-SR04 TRIG → any ESP GPIO (trigger_pin) +HC-SR04 ECHO → voltage divider → any ESP GPIO (echo_pin) +``` + +### ⚠️ 3.3 V Warning + +The HC-SR04 ECHO pin outputs **5 V**. Connecting it directly to an ESP8266 or ESP32 GPIO (3.3 V tolerant) **will damage the pin** over time. Use a resistor voltage divider on the ECHO line: + +``` +HC-SR04 ECHO ──[1 kΩ]──┬──[2 kΩ]── GND + │ + ESP GPIO (echo_pin) +``` + +The TRIG line is driven by the ESP as an output — no divider needed there. + +### ⚠️ ESP8266 Boot Pin Warning + +GPIO **0, 2 and 15** are sampled at reset to select boot mode. If a sensor drives one of these pins to the wrong level during power-on, the ESP8266 will fail to boot — you'd have to re-upload to recover. + +**Do not use GPIO 0, 2 or 15 as `echo_pin`.** + +Safe NodeMCU pins: + +| NodeMCU label | GPIO | +|---|---| +| D1 | GPIO 5 | +| D2 | GPIO 4 | +| D5 | GPIO 14 | +| D6 | GPIO 12 | +| D7 | GPIO 13 | + +The library prints a `Serial` warning at runtime if a strapping pin is used. + +### ⚠️ Inconsistent Results on Battery Power + +HC-SR04 draws ~15 mA at 5 V. On a weak supply this causes voltage dips that corrupt readings. Add a **100 µF capacitor** across the sensor VCC/GND and use `ping_median()` instead of single `ping_cm()` calls. + +--- + +## Quick Start + +```cpp +#include + +#define TRIGGER_PIN 12 // D6 on NodeMCU +#define ECHO_PIN 14 // D5 on NodeMCU (voltage divider required!) +#define MAX_DIST 200 // cm + +NewPingPlus sonar(TRIGGER_PIN, ECHO_PIN, MAX_DIST); + +void setup() { + Serial.begin(115200); + sonar.set_temperature(23.0); // optional — improves accuracy +} + +void loop() { + delay(50); + Serial.print("Distance: "); + Serial.print(sonar.ping_cm()); + Serial.println(" cm"); +} +``` + +--- + +## API Reference + +### Constructor + +```cpp +NewPingPlus sonar(trigger_pin, echo_pin [, max_cm_distance]); +``` + +- `trigger_pin` / `echo_pin` — GPIO numbers. Pass the same pin for both to use single-wire mode (Parallax PING))). +- `max_cm_distance` — optional maximum range in cm. Default = 500. + +--- + +### Methods + +| Method | Returns | Description | +|---|---|---| +| `sonar.ping()` | `unsigned int` | Echo time in µs. 0 = out of range. | +| `sonar.ping_cm()` | `unsigned long` | Distance in whole centimetres. 0 = out of range. | +| `sonar.ping_in()` | `unsigned long` | Distance in whole inches. 0 = out of range. | +| `sonar.ping_mm()` | `unsigned long` | Distance in whole millimetres. 0 = out of range. | +| `sonar.ping_median(it)` | `unsigned long` | Median of `it` pings in µs (default 5, max 25). | +| `sonar.set_temperature(°C)` | `void` | Adjust speed-of-sound for ambient temperature. | +| `sonar.ping_async(cb)` | `void` | Non-blocking ping — see below. ESP8266/ESP32 only. | +| `NewPingPlus::convert_cm(µs)` | `unsigned int` | Static µs → cm conversion. | +| `NewPingPlus::convert_in(µs)` | `unsigned int` | Static µs → inches conversion. | +| `NewPingPlus::convert_mm(µs)` | `unsigned int` | Static µs → mm conversion. | + +All ping methods accept an optional `max_cm_distance` parameter to temporarily override the maximum distance for that call. + +The static `convert_*` helpers use the compile-time default speed of sound, not the value set by `set_temperature()`. Prefer them when you want **cm and mm from a single measurement** — calling `ping_cm()` and `ping_mm()` back to back fires two pings with no recovery gap, and the second often returns `NO_ECHO`: + +```cpp +unsigned int us = sonar.ping(); +unsigned int cm = NewPingPlus::convert_cm(us); +unsigned int mm = NewPingPlus::convert_mm(us); +``` + +--- + +### Temperature Compensation + +Sound travels at 331.3 + 0.606 × T m/s. At 20 °C the default (57 µs/cm) is roughly correct, but at 10 °C it is off by ~2%. Call `set_temperature()` once after reading your temperature sensor: + +```cpp +sonar.set_temperature(22.5); // degrees Celsius +``` + +--- + +### `ping_median()` — Noise Filtering + +```cpp +unsigned long medianUs = sonar.ping_median(5); // 5 pings, return median +unsigned long cm = NewPingPlus::convert_cm(medianUs); +``` + +Discards out-of-range pings and returns the median of the valid ones. Especially useful on battery power or in electrically noisy environments. Maximum 25 iterations. + +--- + +### `ping_async()` — Non-blocking Ping *(ESP8266 / ESP32 only)* + +`ping_async()` fires the trigger pulse and returns **immediately**. A GPIO interrupt catches the echo and calls your callback with the echo time in µs (0 = out of range). Your `loop()` is never blocked, which keeps WiFi, MQTT, OTA, and the watchdog timer happy. + +```cpp +volatile bool ready = false; +volatile unsigned int echoTime = 0; + +void onPing(unsigned int us) { + // Runs inside an ISR — keep it short. + echoTime = us; + ready = true; +} + +void loop() { + if (millis() - lastPing >= 50) { + lastPing = millis(); + sonar.ping_async(onPing); + } + + if (ready) { + ready = false; + Serial.print(NewPingPlus::convert_cm(echoTime)); + Serial.println(" cm"); + } + + // WiFi, MQTT, etc. go here — never blocked by the ping. +} +``` + +Only one async ping may be in flight at a time across all `NewPingPlus` instances. + +--- + +## How This Differs From the Original NewPing + +| Feature | NewPing (Tim Eckel) | NewPingPlus | +|---|---|---| +| ESP8266 / ESP32 | Compiles (untested) | First-class support | +| `micros()` overflow safety | Deadline pattern (can WDT reset after ~71 min) | Elapsed-time arithmetic — always safe | +| WDT / WiFi safety | No `yield()` calls | `yield()` between pings in `ping_median()` and in sensor-startup wait | +| Timer interrupt ping | Yes (AVR/Teensy only) | Not implemented (requires hardware timer not on ESP) | +| One-pin mode | Compile-time `#define` | Auto-detected at runtime | +| Trigger pulse width | `TRIGGER_WIDTH 12` | `TRIGGER_WIDTH 12` (ported) | +| `ping_mm()` | No | Yes | +| Temperature compensation | No | `set_temperature(°C)` | +| Non-blocking async ping | No | `ping_async(callback)` via GPIO interrupt | +| `ping_median()` array | VLA (stack risk) | Fixed-size array (max 25) | +| Subclassing | `private` members | `protected` members | +| Arduino Library Manager | Yes | Yes | + +--- + +## Examples + +| Example | Description | +|---|---| +| [NewPingPlusExample](examples/NewPingPlusExample/NewPingPlusExample.ino) | Basic polling — ping_cm() and ping_mm() at 20 Hz | +| [NewPingAsyncExample](examples/NewPingAsyncExample/NewPingAsyncExample.ino) | Non-blocking ping_async() for use alongside WiFi | + +--- + +## Credits + +- **Tim Eckel** — [NewPing](https://bitbucket.org/teckel12/arduino-new-ping/wiki/Home) original library and ongoing maintenance +- **Paul Stoffregen** — [Teensy port](https://github.com/PaulStoffregen/NewPing) +- **Jordan Shaw** — ESP8266/ESP32 port and v2.x improvements + +--- + +## Release Notes + +See [RELEASE_NOTES.md](RELEASE_NOTES.md) for the full changelog. + +--- + +*Forked and maintained by [Studio Jordan Shaw](https://studiojordanshaw.com) © 2016–2026 · GNU GPL v3* diff --git a/RELEASE_NOTES.md b/RELEASE_NOTES.md new file mode 100644 index 0000000..01dadec --- /dev/null +++ b/RELEASE_NOTES.md @@ -0,0 +1,170 @@ +# Release Notes — NewPingPlus + +--- + +## v2.2.0 — 2026 + +### Renamed: NewPingESP8266 → NewPingPlus + +The library is now called **NewPingPlus**. Source files, class name, macros, and examples have all been updated. The GitHub repository has moved to [github.com/jshaw/NewPingPlus](https://github.com/jshaw/NewPingPlus) — the old URL redirects automatically. + +**Migration:** replace `#include ` with `#include ` and rename any `NewPingESP8266` class instances to `NewPingPlus`. No other API changes. + +### Synced with NewPing v1.9.7 (Tim Eckel) + +**`TRIGGER_WIDTH` — configurable trigger pulse** +The trigger pulse is now 12 µs by default (previously hardcoded at 10 µs). Some HC-SR04 clone sensors are marginally out of spec and miss a 10 µs pulse. + +To override, use a **build flag** — not a `#define` in your sketch. Your `.ino` and `NewPingPlus.cpp` are separate translation units, so a sketch-level define never reaches the library: + +```ini +; PlatformIO — platformio.ini +build_flags = -DTRIGGER_WIDTH=15 +``` + +Without build flags, edit the value directly in `NewPingPlus.h`. The same applies to `MAX_SENSOR_DISTANCE`, `US_ROUNDTRIP_CM`, `US_ROUNDTRIP_IN`, `ROUNDING_ENABLED`, `URM37_ENABLED` and `PING_MEDIAN_MAX_IT`. + +**Automatic one-pin mode detection** +The `ONE_PIN_ENABLED` compile-time `#define` is gone. Single-wire mode (e.g. Parallax PING)))) is now detected automatically at runtime by comparing `trigger_pin` and `echo_pin` in the constructor. No code changes needed — just pass the same pin for both parameters. + +**Constructor drives trigger pin LOW immediately** +The trigger pin is initialised LOW in the constructor. `ping_trigger()` no longer needs the old 4 µs pre-LOW delay, making the trigger sequence slightly faster. + +**`protected` members** +Class members changed from `private` to `protected`, allowing third-party subclasses to customise or extend the library (e.g. custom wiring schemes or sensor variants). + +**`PING_MEDIAN_DELAY` updated to 30000 µs** (was 29000). + +### Bug fixes + +**Fixed: `yield()` was compiled away on every platform** + +The header carried a `#ifndef yield / #define yield()` fallback intended for cores without `yield()`. But every supported core — AVR, ESP8266, ESP32, Teensy — declares `yield()` as a *function*, never a macro, so the guard was always taken and the empty macro replaced **every** `yield()` call in the library. The WDT/WiFi cooperation described in v2.1.0 was silently inert. The fallback has been removed. + +**Fixed: Teensy 4.x failed to compile** + +Teensy 4.0/4.1 (i.MX RT1062) have 32-bit port registers, which cannot be assigned to the `volatile uint8_t*` members the bitwise fast path uses — the build failed with `cannot convert 'volatile uint32_t*' to 'volatile uint8_t*'`. Teensy 4 now uses the `digitalWrite` path (as ESP does); at 600 MHz the overhead is negligible. Teensy 3.x keeps the bitwise path. + +**Fixed: `ping_async()` was non-functional in one-pin mode** + +With `trigger_pin == echo_pin`, the interrupt was attached *before* the trigger pulse — so the library's own pulse fired the ISR, reported a bogus ~12 µs echo, and detached the interrupt before the sensor's real echo arrived. In one-pin mode the interrupt is now attached after the pulse. Two-pin behaviour is unchanged. + +**Fixed: deprecated `ICACHE_RAM_ATTR` on ESP8266 core 3.x** + +Now uses `IRAM_ATTR` where available, falling back to `ICACHE_RAM_ATTR` on cores older than 2.5.0. Removes two deprecation warnings per build. + +**Fixed: `set_temperature()` shifted the maximum range by 1 cm** + +It recovered the configured max distance by dividing `_maxEchoTime` back out by `_us_per_cm`, which truncated. The configured distance is now stored and reused directly. + +**Fixed: large `max_cm_distance` disabled ranging on AVR** + +`max_cm_distance + 1` overflowed 16-bit `unsigned int` on AVR when the caller passed a value near 65535, leaving the echo timeout at 0 so every ping returned `NO_ECHO`. The value is now clamped before the increment. + +**Config macros are now override-guarded** — see `TRIGGER_WIDTH` above. + +--- + +## v2.1.0 — 2024 + +### Bug fixes for reported issues + +**Fixed: WDT reset after ~71 minutes of uptime** *(Issue #5)* + +Root cause: the original timing code used a "deadline" pattern: +```cpp +unsigned long deadline = micros() + timeout; +while (...) { + if (micros() > deadline) return; // BUG: wrong after micros() wraps +} +``` +When `micros()` wraps at the 32-bit boundary (~71 min uptime), `micros()` becomes a small number that never exceeds the large deadline. The `while` loop spins forever — triggering a watchdog reset. + +Fix: all timeout checks now use **elapsed-time arithmetic**: +```cpp +unsigned long start = micros(); +while (...) { + if (micros() - start > timeout) return; // safe across any rollover +} +``` +Unsigned subtraction wraps correctly at the 32-bit boundary regardless of when the overflow occurs. + +Note: Tim Eckel attempted the same fix in NewPing v1.9.5 but reverted it in v1.9.6 because his timer interrupt code is coupled to the deadline pattern. Since this library has no timer interrupt methods on ESP, the elapsed-time fix is clean with no such coupling. + +**Fixed: `yield()` placed in wrong location** + +`yield()` had been added to the echo-measurement window (the loop waiting for the echo pin to fall). This was wrong for two reasons: +1. The max wait is ~30 ms — well under the 1-second software WDT threshold, so `yield()` isn't needed there. +2. On a busy ESP8266, `yield()` can spend hundreds of µs processing WiFi events, corrupting the pulse-width measurement. + +`yield()` is now called only where it is safe and useful: +- In `ping_trigger()`'s sensor-startup wait loop (before timing begins) +- Between pings in `ping_median()` (deliberately idle for 30 ms anyway) + +**Fixed: ESP8266 boot failure with some sensors** *(Issue #4 — RCW-0001)* + +ESP8266 GPIO 0, 2 and 15 are boot-strapping pins — their level at reset determines the boot mode. If a sensor holds one of these pins at the wrong level during power-on, the ESP8266 enters flash mode and the sketch never starts. The symptom is that the board only works when freshly uploaded, not after a power cycle. + +Fix: the constructor now prints a `Serial` warning if `echo_pin` is GPIO 0, 2 or 15. The README lists safe alternative pins (GPIO 4, 5, 12, 13, 14 on NodeMCU). + +**`_max_time` renamed to `_ping_start`** + +The member was renamed to reflect its new semantics: it stores the moment the echo pulse began, not a deadline. `ping()` returns `micros() - _ping_start - PING_OVERHEAD`. + +--- + +## v2.0.0 — 2024 + +### New features + +**ESP32 explicit support** +Previous versions fell through to the generic non-AVR branch. v2.0 adds `#if defined(ESP32)` detection so ESP32 is named, tested, and documented. `NEWPING_PLUS_ASYNC` and ISR attributes are set correctly for both chips. + +**`ping_mm()` — millimetre precision** +```cpp +unsigned long mm = sonar.ping_mm(); +``` +Returns distance in whole millimetres. Uses floating-point division by `_us_per_cm` for better resolution than the cm truncation from `ping_cm()`. + +**`set_temperature()` — speed-of-sound compensation** +```cpp +sonar.set_temperature(22.5); // degrees Celsius +``` +Adjusts the speed-of-sound using the Bohn (1988) formula: `v = 331.3 + 0.606 × T m/s`. The corrected value is used by `ping_cm()`, `ping_in()`, `ping_mm()`, and the internal echo timeout. The static `convert_cm()` / `convert_in()` helpers continue to use the hardcoded default for backwards compatibility. + +At 10 °C the speed-of-sound error is ~2% without compensation. At 30 °C it is ~1% in the other direction. Compensation matters most for precision close-range work. + +**`ping_async()` — non-blocking interrupt-driven ping** *(ESP8266 / ESP32 only)* + +```cpp +sonar.ping_async(myCallback); +``` + +Fires the trigger pulse and returns immediately. A GPIO interrupt catches both edges of the echo pulse; `myCallback(unsigned int echoTime)` is called when the echo ends. Your `loop()` is never blocked, keeping WiFi, MQTT, OTA, and the hardware/software watchdog happy. See [examples/NewPingAsyncExample](examples/NewPingAsyncExample/NewPingAsyncExample.ino). + +Only one async ping may be in-flight at a time across all `NewPingPlus` instances. The ISR is placed in RAM (`ICACHE_RAM_ATTR` / `IRAM_ATTR`) to survive flash cache misses during the interrupt. + +**Fixed: VLA in `ping_median()`** + +The original code declared `unsigned int uS[it]` — a C99 variable-length array, non-standard in C++ and a potential stack-overflow on small ESP stacks when `it` is large. Replaced with a fixed-size array `uS[PING_MEDIAN_MAX_IT]` (default max 25). Iterations above the cap are silently clamped. + +**Fixed: unsigned underflow in `ping_median()` delay** + +The between-ping delay calculation could underflow if `micros()` advanced past the delay boundary between the check and the subtraction. Refactored to elapsed-time arithmetic with explicit overflow guards. + +**Added: `library.properties`** + +Required for the Arduino Library Manager. Declares `architectures=esp8266,esp32,avr,teensy`. + +**Updated: examples use `.ino` extension** + +The original example used the legacy `.pde` extension. Examples are now `.ino` and include ESP-specific pin assignments, the 3.3 V voltage-divider warning, and the boot-pin note. + +--- + +## v1.8 (original port) — 2016 + +- Initial ESP8266 port of NewPing v1.8 by Jordan Shaw. +- Timer interrupt examples removed (requires AVR hardware timers not present on ESP). +- All standard ping methods (`ping`, `ping_cm`, `ping_in`, `ping_median`) working on ESP8266. +- Based on NewPing v1.8 by Tim Eckel. diff --git a/examples/.DS_Store b/examples/.DS_Store deleted file mode 100644 index 94a43bba8dbdef0d1bbe109baef6fefec418dd0b..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 6148 zcmeHKJxc>o5S-N%0h=o=-(N^!e}veIaDTwU5ClmeLDOF4@A9WP`+;y=1REQfh242? zZ|6O3irZTNwtjnF0UdxP-4P!i=H}1cXLeN?Bhq=s3-;Jyg*yzh>dOh|c6h)RXFN{$ zTb^~o0Y{wPhvWYBu;1l9mk&}_3P=GdAO)m=6!?__-g{}wn?yw^AO)nrw*vlsXmrQE za7>I(2Sbbi#0Aq~T*oXyY@Q(Yg<~Q!G)pQmsa7L~C7tbTA67S8O(+(( z^ZXX&us%^y3P^#e0@u0iy#L?Rf0+NLB<-Yt6!=#P*kUtm)_kSvt+SW&UfbwTbg%iO ryKx;9hG@scXvf@mJHCygtZTmJ^S*FQ3_9~cC+cUwb&*MdzgFN2W-1o? diff --git a/examples/NewPingAsyncExample/NewPingAsyncExample.ino b/examples/NewPingAsyncExample/NewPingAsyncExample.ino new file mode 100644 index 0000000..0ba8736 --- /dev/null +++ b/examples/NewPingAsyncExample/NewPingAsyncExample.ino @@ -0,0 +1,72 @@ +// --------------------------------------------------------------------------- +// NewPingPlus — Non-blocking async ping example (ESP8266 / ESP32 only) +// +// ping_async() fires a trigger pulse and returns immediately. When the echo +// returns, a GPIO interrupt fires and invokes your callback with the echo +// time in microseconds (0 = out of range). Your main loop() is never blocked. +// +// This is ideal when you are running WiFi, MQTT, OTA, or other tasks that +// can't tolerate the ~30ms blocking window of a normal ping() call. +// +// WIRING: same as basic example — remember the voltage divider on echo pin! +// --------------------------------------------------------------------------- + +#include + +#if defined(ESP8266) + #define TRIGGER_PIN 12 // D6 on NodeMCU + #define ECHO_PIN 14 // D5 on NodeMCU (voltage divider required!) +#elif defined(ESP32) + #define TRIGGER_PIN 12 + #define ECHO_PIN 14 +#else + #error "ping_async() is only supported on ESP8266 and ESP32." +#endif + +#define MAX_DISTANCE 200 + +NewPingPlus sonar(TRIGGER_PIN, ECHO_PIN, MAX_DISTANCE); + +volatile bool pingReady = false; +volatile unsigned int pingResult = 0; + +// This callback runs inside the GPIO ISR — keep it short. +// Set a flag and process the result in loop(). +void onPingComplete(unsigned int echoTime) { + pingResult = echoTime; + pingReady = true; +} + +void setup() { + Serial.begin(115200); + Serial.println("NewPingPlus v2.2 — async example"); +} + +unsigned long lastPingMs = 0; + +void loop() { + // Trigger a new ping every 50ms (20 pings/sec). + if (millis() - lastPingMs >= 50) { + lastPingMs = millis(); + sonar.ping_async(onPingComplete); + } + + // Process result whenever the ISR has set the flag. + if (pingReady) { + pingReady = false; // clear flag first (ISR-safe on single-core ESP8266) + + unsigned int echo = pingResult; + if (echo == 0) { + Serial.println("Distance: out of range"); + } else { + Serial.print("Distance: "); + Serial.print(NewPingPlus::convert_cm(echo)); + Serial.print(" cm ("); + Serial.print(NewPingPlus::convert_mm(echo)); + Serial.println(" mm)"); + } + } + + // Your other non-blocking tasks go here — WiFi, MQTT, sensors, etc. + // The ping never blocks this loop. +} diff --git a/examples/NewPingESP8266Example/NewPingESP8266Example.pde b/examples/NewPingESP8266Example/NewPingESP8266Example.pde deleted file mode 100755 index 3849fd7..0000000 --- a/examples/NewPingESP8266Example/NewPingESP8266Example.pde +++ /dev/null @@ -1,22 +0,0 @@ -// --------------------------------------------------------------------------- -// Example NewPingESP8266 library sketch that does a ping about 20 times per second. -// --------------------------------------------------------------------------- - -#include - -#define TRIGGER_PIN 12 // Arduino pin tied to trigger pin on the ultrasonic sensor. -#define ECHO_PIN 11 // Arduino pin tied to echo pin on the ultrasonic sensor. -#define MAX_DISTANCE 200 // Maximum distance we want to ping for (in centimeters). Maximum sensor distance is rated at 400-500cm. - -NewPingESP8266 sonar(TRIGGER_PIN, ECHO_PIN, MAX_DISTANCE); // NewPingESP8266 setup of pins and maximum distance. - -void setup() { - Serial.begin(115200); // Open serial monitor at 115200 baud to see ping results. -} - -void loop() { - delay(50); // Wait 50ms between pings (about 20 pings/sec). 29ms should be the shortest delay between pings. - Serial.print("Ping: "); - Serial.print(sonar.ping_cm()); // Send ping, get distance in cm and print result (0 = outside set distance range) - Serial.println("cm"); -} \ No newline at end of file diff --git a/examples/NewPingPlusExample/NewPingPlusExample.ino b/examples/NewPingPlusExample/NewPingPlusExample.ino new file mode 100644 index 0000000..d235367 --- /dev/null +++ b/examples/NewPingPlusExample/NewPingPlusExample.ino @@ -0,0 +1,82 @@ +// --------------------------------------------------------------------------- +// NewPingPlus Example — polls distance ~20 times/second and prints results. +// +// WIRING (ESP8266 NodeMCU / ESP32): +// HC-SR04 VCC → 5V +// HC-SR04 GND → GND +// HC-SR04 TRIG → D6 / GPIO12 (or any free GPIO) +// HC-SR04 ECHO → voltage divider → D5 / GPIO14 +// +// *** 3.3V WARNING *** +// The HC-SR04 echo pin outputs 5V. Connecting it directly to an ESP8266 or +// ESP32 GPIO pin WILL damage the board over time. Use a voltage divider: +// +// HC-SR04 ECHO ──[1kΩ]──┬──[2kΩ]── GND +// │ +// ESP GPIO (echo_pin) +// +// The trigger pin is output-only from the ESP — no divider needed there. +// +// *** ESP8266 BOOT PIN WARNING *** +// Do NOT use GPIO 0, 2 or 15 as echo_pin. These pins are sampled at boot +// by the ESP8266 to select boot mode. A sensor driving them during power-on +// can prevent the sketch from starting after a reset (you'd have to +// re-upload to recover). Safe pins: GPIO 4, 5, 12, 13, 14. +// +// *** INCONSISTENT ON BATTERY? *** +// HC-SR04 draws ~15 mA @ 5V. On a weak supply this causes voltage dips. +// Add a 100 µF capacitor across the sensor VCC/GND and use ping_median() +// instead of ping_cm() — it averages out noise automatically. +// --------------------------------------------------------------------------- + +#include + +// --- Pin definitions ------------------------------------------------------- +// Change these to match your wiring. +// NodeMCU: D5=GPIO14, D6=GPIO12, D7=GPIO13 ... +// ESP32: any GPIO works for trigger; use an interrupt-capable GPIO for echo. + +#if defined(ESP8266) + #define TRIGGER_PIN 12 // D6 on NodeMCU + #define ECHO_PIN 14 // D5 on NodeMCU (connect via voltage divider!) +#elif defined(ESP32) + #define TRIGGER_PIN 12 + #define ECHO_PIN 14 +#else + // Generic Arduino / Teensy fallback + #define TRIGGER_PIN 12 + #define ECHO_PIN 11 +#endif + +#define MAX_DISTANCE 200 // Maximum distance to ping for (cm). Sensor max ~400-500cm. + +// --------------------------------------------------------------------------- + +NewPingPlus sonar(TRIGGER_PIN, ECHO_PIN, MAX_DISTANCE); + +void setup() { + Serial.begin(115200); + Serial.println("NewPingPlus v2.2 — basic example"); + + // Optional: set ambient temperature for more accurate readings. + // sonar.set_temperature(22.5); // degrees Celsius +} + +void loop() { + delay(50); // ~20 pings/sec. 29ms is the minimum recommended interval. + + // One ping, then derive both cm and mm from the same measurement. + // (Calling ping_cm() and ping_mm() back-to-back would fire two pings with + // no recovery gap — the second often returns NO_ECHO. Always reuse one ping.) + unsigned int us = sonar.ping(); + + Serial.print("Distance: "); + if (us == NO_ECHO) { + Serial.println("out of range"); + } else { + Serial.print(NewPingPlus::convert_cm(us)); + Serial.print(" cm ("); + Serial.print(NewPingPlus::convert_mm(us)); + Serial.println(" mm)"); + } +} diff --git a/keywords.txt b/keywords.txt index b40d080..3ac1cd0 100755 --- a/keywords.txt +++ b/keywords.txt @@ -1,12 +1,12 @@ ################################### -# Syntax Coloring Map For NewPingESP8266 +# Syntax Coloring Map For NewPingPlus ################################### ################################### # Datatypes (KEYWORD1) ################################### -NewPingESP8266 KEYWORD1 +NewPingPlus KEYWORD1 ################################### # Methods and Functions (KEYWORD2) @@ -15,16 +15,21 @@ NewPingESP8266 KEYWORD1 ping KEYWORD2 ping_in KEYWORD2 ping_cm KEYWORD2 +ping_mm KEYWORD2 ping_median KEYWORD2 -ping_timer KEYWORD2 -check_timer KEYWORD2 -timer_us KEYWORD2 -timer_ms KEYWORD2 -timer_stop KEYWORD2 +ping_async KEYWORD2 convert_in KEYWORD2 convert_cm KEYWORD2 +convert_mm KEYWORD2 +set_temperature KEYWORD2 ################################### # Constants (LITERAL1) ################################### +NO_ECHO LITERAL1 +MAX_SENSOR_DISTANCE LITERAL1 +US_ROUNDTRIP_CM LITERAL1 +US_ROUNDTRIP_IN LITERAL1 +TRIGGER_WIDTH LITERAL1 +PING_MEDIAN_MAX_IT LITERAL1 diff --git a/library.properties b/library.properties new file mode 100644 index 0000000..199152c --- /dev/null +++ b/library.properties @@ -0,0 +1,10 @@ +name=NewPingPlus +version=2.2.0 +author=Jordan Shaw +maintainer=Jordan Shaw +sentence=Ultrasonic sensor library for ESP8266, ESP32, Arduino (AVR) and Teensy. Forked from NewPing by Tim Eckel. +paragraph=Supports HC-SR04, SRF05, SRF06, DYP-ME007, URM37 and Parallax PING))). Features: ping_cm/in/mm, ping_median (sorted, noise-filtered), temperature-compensated sound speed, and non-blocking interrupt-driven ping_async() on ESP platforms. Overflow-safe timing (fixes WDT resets near micros() rollover). +category=Sensors +url=https://github.com/jshaw/NewPingPlus +architectures=esp8266,esp32,avr,teensy +depends= diff --git a/test/mock/Arduino.h b/test/mock/Arduino.h new file mode 100644 index 0000000..5b7e239 --- /dev/null +++ b/test/mock/Arduino.h @@ -0,0 +1,81 @@ +// Mock Arduino environment for native (host) unit tests. +// Provides every symbol that NewPingPlus.cpp uses from the Arduino core +// so the library can be compiled and tested without a microcontroller. +// +// On native compilation none of ESP8266/ESP32/__AVR__/__arm__ are defined, +// so the header's platform detection falls into `#elif !defined(__AVR__)`: +// DO_BITWISE = false (digitalRead/Write path — same as ESP) +// PING_OVERHEAD = 1 +// NEWPING_ESP_ASYNC is NOT defined (no interrupt mocking needed) +// +// This keeps mocking simple: only digitalRead, digitalWrite, pinMode, +// micros, delay, delayMicroseconds, yield and Serial need to be provided. + +#pragma once + +#include +#include +#include +#include + +typedef bool boolean; +typedef uint8_t byte; + +// ---- Pin / logic constants ---- +#define HIGH 1 +#define LOW 0 +#define INPUT 0 +#define OUTPUT 1 +#define INPUT_PULLUP 2 +#define CHANGE 1 +#define RISING 2 +#define FALLING 3 + +// ---- ISR placement attributes (no-ops on native) ---- +#define ICACHE_RAM_ATTR +#define IRAM_ATTR + +// ---- min / max ---- +// Arduino defines these as macros, so mixed-type calls such as +// max(unsigned int, int) — which NewPingPlusConvert() performs when +// ROUNDING_ENABLED is true — resolve via the usual arithmetic conversions. +// std::min/std::max are templates requiring both arguments to be the same type +// and would not compile there. Use common_type to reproduce the macro's +// promotion behaviour without the hazards of an actual macro (which would +// break any standard header included after this one). +template +constexpr typename std::common_type::type min(A a, B b) { + using C = typename std::common_type::type; + return (C)a < (C)b ? (C)a : (C)b; +} + +template +constexpr typename std::common_type::type max(A a, B b) { + using C = typename std::common_type::type; + return (C)a > (C)b ? (C)a : (C)b; +} + +// ---- Arduino API ---- +unsigned long micros(); +void delay(unsigned long ms); +void delayMicroseconds(unsigned int us); + +int digitalRead(int pin); +void digitalWrite(int pin, int val); +void pinMode(int pin, int mode); + +void yield(); + +// These are only used by NEWPING_ESP_ASYNC which is NOT compiled in tests, +// but the linker still needs symbols if anything references them. +void attachInterrupt(int pin, void (*isr)(), int mode); +void detachInterrupt(int pin); +int digitalPinToInterrupt(int pin); + +// ---- Serial mock ---- +struct SerialMock { + template void println(T) {} + template void print(T) {} + void begin(int) {} +}; +extern SerialMock Serial; diff --git a/test/mock_arduino.cpp b/test/mock_arduino.cpp new file mode 100644 index 0000000..f6b25f4 --- /dev/null +++ b/test/mock_arduino.cpp @@ -0,0 +1,79 @@ +#include "mock/Arduino.h" +#include "mock_arduino.h" +#include + +SerialMock Serial; + +// ---- Clock ---- + +unsigned long mock_micros = 0; + +unsigned long micros() { return mock_micros; } +void delay(unsigned long ms) { mock_micros += ms * 1000UL; } +void delayMicroseconds(unsigned int us) { mock_micros += us; } + +// Counted so tests can assert the library actually cooperates with the WDT. +// A `#define yield()` in the header would compile these calls away silently. +unsigned long mock_yield_count = 0; +void yield() { ++mock_yield_count; } + +// ---- Scriptable digitalRead ---- + +static std::vector g_queue; +static int g_default_val = LOW; +static unsigned long g_default_advance = 0; + +void mock_reset() { + g_queue.clear(); + mock_micros = 0; + mock_yield_count = 0; + g_default_val = LOW; + g_default_advance = 0; + mock_last_write_pin = -1; + mock_last_write_val = -1; + mock_last_mode_pin = -1; + mock_last_mode_val = -1; +} + +void mock_push_read(int value, unsigned long advance_us) { + g_queue.push_back({value, advance_us}); +} + +void mock_set_default_read(int value, unsigned long advance_us) { + g_default_val = value; + g_default_advance = advance_us; +} + +int digitalRead(int /*pin*/) { + if (!g_queue.empty()) { + ReadEntry e = g_queue.front(); + g_queue.erase(g_queue.begin()); + mock_micros += e.advance_us; + return e.value; + } + mock_micros += g_default_advance; + return g_default_val; +} + +// ---- Recording writes ---- + +int mock_last_write_pin = -1; +int mock_last_write_val = -1; +int mock_last_mode_pin = -1; +int mock_last_mode_val = -1; + +void digitalWrite(int pin, int val) { + mock_last_write_pin = pin; + mock_last_write_val = val; +} + +void pinMode(int pin, int mode) { + mock_last_mode_pin = pin; + mock_last_mode_val = mode; +} + +// ---- Interrupt stubs (NEWPING_ESP_ASYNC not compiled in native tests) ---- + +void attachInterrupt(int, void(*)(), int) {} +void detachInterrupt(int) {} +int digitalPinToInterrupt(int pin) { return pin; } diff --git a/test/mock_arduino.h b/test/mock_arduino.h new file mode 100644 index 0000000..5b6939e --- /dev/null +++ b/test/mock_arduino.h @@ -0,0 +1,41 @@ +// Control interface for the Arduino mock — include this in test files. +// +// Typical test setup: +// mock_reset(); // zero clock, clear read queue +// mock_push_read(LOW, 0); // busy-check: not busy +// mock_push_read(HIGH, 0); // echo starts immediately +// mock_push_read(HIGH, 1000); // echo pulse lasts 1000 µs +// mock_push_read(LOW, 0); // echo ends +// auto result = sonar.ping(); // exercise the code +// +// After the scripted queue is exhausted, digitalRead() returns the value set +// by mock_set_default_read() (default: LOW, no time advance). + +#pragma once +#include + +// Advance mock clock and set the digitalRead return value in one call. +struct ReadEntry { + int value; + unsigned long advance_us; // added to mock_micros BEFORE returning value +}; + +// ---- Clock ---- +extern unsigned long mock_micros; // current fake time in µs + +// ---- yield() accounting ---- +// Incremented by every yield() the library makes. Guards against the header +// re-acquiring a `#ifndef yield / #define yield()` fallback, which would compile +// all of them away without any other visible symptom. +extern unsigned long mock_yield_count; + +// ---- Read queue control ---- +void mock_reset(); +void mock_push_read(int value, unsigned long advance_us = 0); +void mock_set_default_read(int value, unsigned long advance_us = 0); + +// ---- Call recording ---- +extern int mock_last_write_pin; +extern int mock_last_write_val; +extern int mock_last_mode_pin; +extern int mock_last_mode_val; diff --git a/test/test_main.cpp b/test/test_main.cpp new file mode 100644 index 0000000..d2c2fdd --- /dev/null +++ b/test/test_main.cpp @@ -0,0 +1,673 @@ +// NewPingPlus unit tests — compiled and run on the host (no hardware). +// +// Run with: make test (from the repo root) +// +// Coverage: +// • Static conversion math (convert_cm / convert_in) +// • Temperature compensation (set_temperature) +// • Max-distance / maxEchoTime calculation +// • ping() happy path, busy-line rejection, timeout +// • ping_cm / ping_in / ping_mm end-to-end +// • ping_median: sorting, median selection, out-of-range skipping, cap +// • micros() overflow safety (the v2.1 bug fix) +// • Regressions: yield() not compiled away, set_temperature() range stability, +// max_cm_distance clamping (see section 9) + +#include +#include +#include +#include + +#include "mock_arduino.h" +#include "../NewPingPlus.h" + +// --------------------------------------------------------------------------- +// Minimal test framework +// --------------------------------------------------------------------------- + +static int g_pass = 0; +static int g_fail = 0; + +#define PASS(msg) do { printf(" PASS %s\n", msg); ++g_pass; } while(0) +#define FAIL(msg) do { printf(" FAIL %s [%s:%d]\n", msg, __FILE__, __LINE__); ++g_fail; } while(0) + +#define CHECK(cond, msg) do { if (cond) PASS(msg); else FAIL(msg); } while(0) +#define CHECK_EQ(a, b, msg) CHECK((a) == (b), msg) +#define CHECK_NEAR(a, b, tol, msg) CHECK(std::abs((long)(a) - (long)(b)) <= (long)(tol), msg) + +// --------------------------------------------------------------------------- +// TestableSonar — subclass to inspect protected members without touching +// private internals. Protected access is deliberate (v2.2 changed members +// from private to protected precisely to allow this kind of extension). +// --------------------------------------------------------------------------- + +class TestableSonar : public NewPingPlus { +public: + TestableSonar(uint8_t trig, uint8_t echo, unsigned int max_cm = 200) + : NewPingPlus(trig, echo, max_cm) {} + + float us_per_cm() const { return _us_per_cm; } + unsigned int max_echo_time() const { return _maxEchoTime; } + unsigned int max_cm_distance() const { return _max_cm_distance; } + bool one_pin_mode() const { return _one_pin_mode; } + + // Compute ping_cm / ping_in / ping_mm for a known echo time without + // running the hardware loop — tests the math in isolation. + unsigned long cm_for(unsigned long us) { return (unsigned long)(us / _us_per_cm); } + unsigned long in_for(unsigned long us) { return (unsigned long)(us / (_us_per_cm * 2.54f)); } + unsigned long mm_for(unsigned long us) { return (unsigned long)(us * 10.0f / _us_per_cm); } +}; + +// --------------------------------------------------------------------------- +// Helper: push a standard clean-ping sequence onto the mock read queue. +// +// Sequence for DO_BITWISE=false, _one_pin_mode=false: +// ping_trigger(): +// 1. digitalRead(echo) → LOW (not busy) +// 2. digitalRead(echo) → HIGH (echo started) +// ping() echo-wait loop: +// 3. digitalRead(echo) → HIGH + echo_us (echo in progress) +// 4. digitalRead(echo) → LOW (echo ended) +// --------------------------------------------------------------------------- + +static void push_clean_ping(unsigned long echo_us) { + mock_push_read(LOW, 0); // 1. not busy + mock_push_read(HIGH, 0); // 2. echo starts immediately + mock_push_read(HIGH, echo_us); // 3. echo active for echo_us µs + mock_push_read(LOW, 0); // 4. echo done +} + +// --------------------------------------------------------------------------- +// 1. Static conversion math +// --------------------------------------------------------------------------- + +static void test_convert_cm() { + printf("\n--- convert_cm (static) ---\n"); + + // 57 µs = 1 cm (default sound speed, no rounding) + CHECK_EQ(NewPingPlus::convert_cm(0), 0u, "0 µs → 0 cm"); + CHECK_EQ(NewPingPlus::convert_cm(57), 1u, "57 µs → 1 cm"); + CHECK_EQ(NewPingPlus::convert_cm(570), 10u, "570 µs → 10 cm"); + CHECK_EQ(NewPingPlus::convert_cm(285), 5u, "285 µs → 5 cm"); + + // Truncation: 80 / 57 = 1.4 → 1 + CHECK_EQ(NewPingPlus::convert_cm(80), 1u, "80 µs → 1 cm (truncated)"); +} + +static void test_convert_in() { + printf("\n--- convert_in (static) ---\n"); + + CHECK_EQ(NewPingPlus::convert_in(0), 0u, "0 µs → 0 in"); + CHECK_EQ(NewPingPlus::convert_in(146), 1u, "146 µs → 1 in"); + CHECK_EQ(NewPingPlus::convert_in(292), 2u, "292 µs → 2 in"); + CHECK_EQ(NewPingPlus::convert_in(730), 5u, "730 µs → 5 in"); +} + +static void test_convert_mm() { + printf("\n--- convert_mm (static) ---\n"); + + CHECK_EQ(NewPingPlus::convert_mm(0), 0u, "0 µs → 0 mm"); + CHECK_EQ(NewPingPlus::convert_mm(57), 10u, "57 µs → 10 mm (1 cm)"); + CHECK_EQ(NewPingPlus::convert_mm(570), 100u, "570 µs → 100 mm (10 cm)"); + CHECK_EQ(NewPingPlus::convert_mm(285), 50u, "285 µs → 50 mm (5 cm)"); + // Large echoTime: echoTime*10 = 250000 overflows 16-bit int if not widened. + CHECK_EQ(NewPingPlus::convert_mm(25000), 4385u, "25000 µs → 4385 mm (overflow-safe)"); +} + +// --------------------------------------------------------------------------- +// 2. Temperature compensation +// --------------------------------------------------------------------------- + +static void test_set_temperature() { + printf("\n--- set_temperature ---\n"); + + mock_reset(); + TestableSonar sonar(12, 14); + + // Default: us_per_cm = 57.0 + CHECK_NEAR(sonar.us_per_cm(), 57.0f, 0.1f, "default us_per_cm ≈ 57.0"); + + // At 20 °C: v = 331.3 + 0.606*20 = 343.42 m/s → 20000/343.42 ≈ 58.24 µs/cm + sonar.set_temperature(20.0f); + CHECK_NEAR(sonar.us_per_cm(), 58.24f, 0.1f, "20 °C → us_per_cm ≈ 58.24"); + + // At 0 °C: v = 331.3 → 20000/331.3 ≈ 60.37 µs/cm + sonar.set_temperature(0.0f); + CHECK_NEAR(sonar.us_per_cm(), 60.37f, 0.1f, "0 °C → us_per_cm ≈ 60.37"); + + // At 35 °C: v = 331.3 + 21.21 = 352.51 → 20000/352.51 ≈ 56.74 µs/cm + sonar.set_temperature(35.0f); + CHECK_NEAR(sonar.us_per_cm(), 56.74f, 0.1f, "35 °C → us_per_cm ≈ 56.74"); + + // Warmer air → faster sound → smaller us_per_cm → closer reading for same echo + sonar.set_temperature(0.0f); + float cold = sonar.us_per_cm(); + sonar.set_temperature(30.0f); + float warm = sonar.us_per_cm(); + CHECK(warm < cold, "warmer air → smaller us_per_cm (faster sound)"); +} + +static void test_temperature_affects_max_echo_time() { + printf("\n--- temperature updates maxEchoTime ---\n"); + + mock_reset(); + TestableSonar sonar(12, 14, 100); // 100 cm max + + unsigned int t_default = sonar.max_echo_time(); + + sonar.set_temperature(0.0f); // colder → slower → larger maxEchoTime + unsigned int t_cold = sonar.max_echo_time(); + + sonar.set_temperature(40.0f); // warmer → faster → smaller maxEchoTime + unsigned int t_warm = sonar.max_echo_time(); + + CHECK(t_cold > t_default, "colder temp → larger maxEchoTime"); + CHECK(t_warm < t_default, "warmer temp → smaller maxEchoTime"); +} + +// --------------------------------------------------------------------------- +// 3. Max distance / maxEchoTime calculation +// --------------------------------------------------------------------------- + +static void test_set_max_distance() { + printf("\n--- set_max_distance / maxEchoTime ---\n"); + + mock_reset(); + TestableSonar sonar(12, 14, 100); + // ROUNDING_ENABLED=false → maxEchoTime = (100+1) * 57 = 5757 + CHECK_EQ(sonar.max_echo_time(), 101u * 57u, "maxEchoTime for 100 cm"); + + mock_reset(); + TestableSonar sonar2(12, 14, 10); + CHECK_EQ(sonar2.max_echo_time(), 11u * 57u, "maxEchoTime for 10 cm"); + + // Clamped at MAX_SENSOR_DISTANCE (500 cm) + mock_reset(); + TestableSonar sonar3(12, 14, 9999); + CHECK_EQ(sonar3.max_echo_time(), 501u * 57u, "maxEchoTime clamped at 500 cm"); +} + +// --------------------------------------------------------------------------- +// 4. One-pin mode detection +// --------------------------------------------------------------------------- + +static void test_one_pin_mode() { + printf("\n--- one-pin mode auto-detection ---\n"); + + mock_reset(); + TestableSonar two_pin(12, 14); + CHECK(!two_pin.one_pin_mode(), "different pins → two-pin mode"); + + mock_reset(); + TestableSonar one_pin(12, 12); + CHECK(one_pin.one_pin_mode(), "same pins → one-pin mode"); +} + +// --------------------------------------------------------------------------- +// 5. ping() — behavioural tests +// --------------------------------------------------------------------------- + +static void test_ping_busy_returns_no_echo() { + printf("\n--- ping: echo pin busy at call time ---\n"); + + mock_reset(); + // Echo pin already HIGH → sensor busy → should return NO_ECHO immediately. + mock_set_default_read(HIGH, 0); + TestableSonar sonar(12, 14, 200); + + unsigned int result = sonar.ping(); + CHECK_EQ(result, (unsigned int)NO_ECHO, "busy echo pin → NO_ECHO"); +} + +static void test_ping_timeout_returns_no_echo() { + printf("\n--- ping: echo stays HIGH past maxEchoTime ---\n"); + + mock_reset(); + // 10 cm max → maxEchoTime = 11 * 57 = 627 µs + // Echo starts, then stays HIGH — each digitalRead advances 100 µs. + // After ~7 reads in the echo loop: elapsed = 700 > 627 → timeout. + mock_push_read(LOW, 0); // not busy + mock_push_read(HIGH, 0); // echo starts + mock_set_default_read(HIGH, 100); // stays HIGH, 100 µs per read + + TestableSonar sonar(12, 14, 10); + unsigned int result = sonar.ping(); + CHECK_EQ(result, (unsigned int)NO_ECHO, "echo timeout → NO_ECHO"); +} + +static void test_ping_sensor_start_timeout() { + printf("\n--- ping: sensor never raises echo pin (startup timeout) ---\n"); + + mock_reset(); + // Not busy, but echo pin stays LOW — sensor never starts. + // Each read advances by 1000 µs; after maxEchoTime + MAX_SENSOR_DELAY the + // startup wait times out. + mock_push_read(LOW, 0); // not busy + mock_set_default_read(LOW, 1000); // stays LOW, 1 ms per read + + TestableSonar sonar(12, 14, 10); + unsigned int result = sonar.ping(); + CHECK_EQ(result, (unsigned int)NO_ECHO, "sensor never starts → NO_ECHO"); +} + +static void test_ping_returns_echo_time() { + printf("\n--- ping: clean echo, correct duration ---\n"); + + mock_reset(); + // 1000 µs echo. PING_OVERHEAD=1 (non-AVR), so expected return = 999. + push_clean_ping(1000); + TestableSonar sonar(12, 14, 200); + + unsigned int result = sonar.ping(); + // delayMicroseconds(TRIGGER_WIDTH=12) runs before the echo, so _ping_start + // is at t=12. Echo advances 1000 µs. Elapsed = 1000, result = 999. + CHECK_NEAR(result, 999u, 5u, "1000 µs echo → ping() ≈ 999 µs"); +} + +// --------------------------------------------------------------------------- +// 6. ping_cm / ping_in / ping_mm — math tests (no hardware loop) +// --------------------------------------------------------------------------- + +static void test_ping_cm_math() { + printf("\n--- ping_cm / ping_in / ping_mm math ---\n"); + + mock_reset(); + TestableSonar sonar(12, 14); + + // Default us_per_cm = 57 + CHECK_EQ(sonar.cm_for(570), 10u, "570 µs → 10 cm"); + CHECK_EQ(sonar.cm_for(5700), 100u,"5700 µs → 100 cm"); + + CHECK_EQ(sonar.in_for(584), 4u, "584 µs → ~4 in (584 / 146 = 4)"); + CHECK_EQ(sonar.in_for(1460), 10u, "1460 µs → 10 in"); + + CHECK_EQ(sonar.mm_for(570), 100u,"570 µs → 100 mm"); + CHECK_EQ(sonar.mm_for(285), 50u, "285 µs → 50 mm"); + + // After temperature compensation + sonar.set_temperature(20.0f); // us_per_cm ≈ 58.24 + // 582 µs / 58.24 ≈ 9.99 → 9 cm + unsigned long cm = sonar.cm_for(582); + CHECK(cm >= 9u && cm <= 10u, "582 µs at 20°C → 9–10 cm"); +} + +static void test_ping_cm_end_to_end() { + printf("\n--- ping_cm end-to-end with mock hardware ---\n"); + + mock_reset(); + // 5700 µs echo → 100 cm (5700 / 57 = 100) + push_clean_ping(5700); + TestableSonar sonar(12, 14, 200); + + unsigned long cm = sonar.ping_cm(); + // ping() returns ≈ 5699 µs (minus PING_OVERHEAD=1) + // 5699 / 57.0 = 99.98 → 99 or 100 depending on float truncation + CHECK(cm >= 99u && cm <= 100u, "5700 µs echo → ~100 cm"); +} + +static void test_ping_mm_more_precise_than_cm() { + printf("\n--- ping_mm gives finer granularity than ping_cm ---\n"); + + // Test 1: 570 µs echo. + // Push separate reads for ping_mm() and ping_cm() — each call consumes + // 4 reads (one full ping sequence). Sharing a single set causes ping_cm() + // to call ping_trigger() with an empty queue → infinite loop. + mock_reset(); + push_clean_ping(570); // for ping_mm() + push_clean_ping(570); // for ping_cm() + TestableSonar sonar(12, 14, 200); + unsigned long mm = sonar.ping_mm(); + unsigned long cm = sonar.ping_cm(); + + // For any result, mm should be at least as informative as cm*10 + CHECK(mm >= cm * 10u, "ping_mm() ≥ ping_cm() * 10 (finer resolution)"); + + // Test 2: 271 µs echo → 4 cm, 47 mm — mm is more precise than cm*10. + mock_reset(); + push_clean_ping(271); // for ping_mm() + push_clean_ping(271); // for ping_cm() + unsigned long mm2 = sonar.ping_mm(); + unsigned long cm2 = sonar.ping_cm(); + // 271/57 = 4 cm, 271*10/57 = 47 mm — mm provides sub-cm precision + CHECK(mm2 >= cm2 * 10u, "mm2 provides at least cm2 precision"); + CHECK(mm2 > cm2 * 10u || mm2 == cm2 * 10u, "mm2 ≥ cm2 * 10 (may equal for round values)"); +} + +// --------------------------------------------------------------------------- +// 7. ping_median — sorting and median selection +// --------------------------------------------------------------------------- + +// Simulate N clean pings returning given echo durations (µs each). +static void push_n_pings(std::vector echo_times) { + for (unsigned long t : echo_times) { + push_clean_ping(t); + // Between pings, ping_median() calls delay() which advances mock_micros + // and yield(). Both are handled by the mock so no extra setup is needed. + } +} + +static void test_ping_median_single() { + printf("\n--- ping_median: single ping ---\n"); + + mock_reset(); + push_n_pings({1000}); + TestableSonar sonar(12, 14, 200); + + unsigned long result = sonar.ping_median(1); + CHECK_NEAR(result, 999u, 5u, "median of 1 ping ≈ 999 µs"); +} + +static void test_ping_median_odd_count() { + printf("\n--- ping_median: 3 pings — returns middle value ---\n"); + + // Three pings: 300, 500, 400 µs. + // Insertion sort (descending): [499, 399, 299] (each -1 for PING_OVERHEAD) + // Median at index 1 → 399 µs. + mock_reset(); + push_n_pings({300, 500, 400}); + TestableSonar sonar(12, 14, 200); + + unsigned long result = sonar.ping_median(3); + CHECK_NEAR(result, 399u, 5u, "median of 300/500/400 µs ≈ 399 µs"); +} + +static void test_ping_median_even_count() { + printf("\n--- ping_median: 4 pings — lower-middle value ---\n"); + + // Four pings: 200, 600, 400, 800 µs. + // Sorted descending (≈ minus 1): [799, 599, 399, 199] + // Median at index 2 → 399 µs. + mock_reset(); + push_n_pings({200, 600, 400, 800}); + TestableSonar sonar(12, 14, 200); + + unsigned long result = sonar.ping_median(4); + CHECK_NEAR(result, 399u, 5u, "lower-middle of 4 pings ≈ 399 µs"); +} + +static void test_ping_median_skips_out_of_range() { + printf("\n--- ping_median: out-of-range pings discarded ---\n"); + + // Two valid pings (1000, 2000 µs) with two out-of-range interleaved. + // Out-of-range pings are simulated by: echo pin HIGH (busy) → NO_ECHO. + mock_reset(); + push_clean_ping(1000); + + // Out-of-range: busy at call time + mock_push_read(HIGH, 0); // busy → ping_trigger returns false → NO_ECHO + + push_clean_ping(2000); + + // Out-of-range: busy + mock_push_read(HIGH, 0); + + TestableSonar sonar(12, 14, 500); + // it=4 but 2 fail → remaining shrinks to 2, returns median of [1999, 999] + // → index 1 → 999 µs + unsigned long result = sonar.ping_median(4); + CHECK_NEAR(result, 999u, 5u, "median with 2 valid out of 4 ≈ 999 µs"); +} + +static void test_ping_median_all_out_of_range() { + printf("\n--- ping_median: all pings fail → NO_ECHO ---\n"); + + mock_reset(); + // Echo pin stuck HIGH — every ping_trigger() call returns false. + mock_set_default_read(HIGH, 0); + TestableSonar sonar(12, 14, 200); + + unsigned long result = sonar.ping_median(5); + CHECK_EQ(result, (unsigned long)NO_ECHO, "all pings fail → NO_ECHO"); +} + +static void test_ping_median_clamps_iterations() { + printf("\n--- ping_median: clamps iterations at PING_MEDIAN_MAX_IT ---\n"); + + mock_reset(); + // Push PING_MEDIAN_MAX_IT + 5 pings worth of reads. + // The library should only perform PING_MEDIAN_MAX_IT (25) pings. + for (int i = 0; i < PING_MEDIAN_MAX_IT + 5; ++i) + push_clean_ping(1000); + + TestableSonar sonar(12, 14, 200); + // Passing it=30 > PING_MEDIAN_MAX_IT=25 — should not crash or overflow. + unsigned long result = sonar.ping_median(30); + CHECK(result != (unsigned long)NO_ECHO, "clamped ping_median completes without crash"); +} + +// --------------------------------------------------------------------------- +// 8. micros() overflow safety — the v2.1 fix +// --------------------------------------------------------------------------- + +static void test_overflow_elapsed_time_arithmetic() { + printf("\n--- micros() overflow: elapsed-time arithmetic ---\n"); + + // The fix uses: micros() - start > timeout + // Unsigned subtraction wraps safely at the 32-bit boundary. + // + // On a 64-bit host, `unsigned long` is 64 bits and would not wrap at 2^32. + // Use uint32_t here to reproduce the exact 32-bit overflow behaviour that + // AVR/ESP targets experience in production. + + // Case 1: start near the 32-bit maximum, micros has wrapped. + { + uint32_t start = 0xFFFFFF00UL; + uint32_t now = 0x00000100UL; // wrapped: 256 ticks after start + uint32_t elapsed = now - start; // = 0x200 = 512 (wraps correctly) + uint32_t timeout = 600; + CHECK(elapsed < timeout, "overflow: elapsed 512 < timeout 600 (safe)"); + } + + // Case 2: same scenario — elapsed exceeds timeout. + { + uint32_t start = 0xFFFFFF00UL; + uint32_t now = 0x000003E8UL; // 1000 ticks after wrap + uint32_t elapsed = now - start; // = 0x4E8 = 1256 + uint32_t timeout = 600; + CHECK(elapsed > timeout, "overflow: elapsed 1256 > timeout 600 (safe)"); + } + + // Case 3: old unsafe deadline pattern would fail here. + { + uint32_t start = 0xFFFFFF00UL; + uint32_t timeout = 500; + uint32_t deadline = start + timeout; // wraps to 0xF4 = 244 (32-bit) + uint32_t now = 0xFFFFFFAAUL; // 170 ticks after start, < timeout + + // Old check: now > deadline → 0xFFFFFFAA > 0xF4 → TRUE (wrong timeout!) + bool old_way = (now > deadline); + // New check: now - start > timeout → 0xAA (170) > 500 → FALSE (correct) + bool new_way = (now - start > timeout); + + CHECK( old_way, "old deadline check gives wrong result near overflow"); + CHECK(!new_way, "new elapsed check gives correct result near overflow"); + } +} + +static void test_overflow_does_not_hang() { + printf("\n--- micros() overflow: ping() does not hang near rollover ---\n"); + + // Start mock_micros just before the 32-bit rollover. + // If the timeout check were broken (deadline pattern), the echo wait + // loop would spin forever. With elapsed-time arithmetic it exits correctly. + mock_reset(); + mock_micros = 0xFFFFFF00UL; + + // Push a clean ping with a 200 µs echo. + // delayMicroseconds(12) will advance past 2^32 and wrap. + push_clean_ping(200); + + TestableSonar sonar(12, 14, 50); // 50 cm max → 2907 µs timeout + unsigned int result = sonar.ping(); + // Should return a valid echo time, not spin forever or return NO_ECHO spuriously. + CHECK(result != (unsigned int)NO_ECHO, "ping() exits correctly after micros() rollover"); +} + +// --------------------------------------------------------------------------- +// 9. Regression tests for defects found in the v2.2.0 pre-release review +// --------------------------------------------------------------------------- + +static void test_yield_is_actually_called() { + printf("\n--- regression: yield() is not compiled away ---\n"); + + // A `#ifndef yield / #define yield()` fallback in the header is ALWAYS taken + // (every Arduino core declares yield() as a function, not a macro), which + // silently replaces every yield() call in the library with nothing. Nothing + // else observable changes — hence this explicit guard. +#ifdef yield + FAIL("yield must not be a macro — library yield() calls would be compiled away"); +#else + PASS("yield is not a macro"); +#endif + + // ping_trigger()'s sensor-startup wait loop yields while waiting for echo. + mock_reset(); + mock_push_read(LOW, 0); // not busy + mock_push_read(LOW, 100); // still low — one loop pass (yields) + mock_push_read(LOW, 100); // still low — another pass (yields) + mock_push_read(HIGH, 0); // echo starts + mock_push_read(HIGH, 500); // echo active + mock_push_read(LOW, 0); // echo ends + + TestableSonar sonar(12, 14, 200); + unsigned long before = mock_yield_count; + sonar.ping(); + CHECK(mock_yield_count > before, "ping() yields during sensor-startup wait"); + + // ping_median() yields between pings. + mock_reset(); + push_n_pings({1000, 1000, 1000}); + before = mock_yield_count; + sonar.ping_median(3); + CHECK(mock_yield_count >= 3, "ping_median() yields at least once per ping"); +} + +static void test_set_temperature_preserves_max_distance() { + printf("\n--- regression: set_temperature() preserves configured range ---\n"); + + mock_reset(); + TestableSonar sonar(12, 14, 100); + + // maxEchoTime must always correspond to the *configured* 100 cm, never drift. + // Deriving the distance back out of maxEchoTime truncated and added 1 cm. + CHECK_EQ(sonar.max_echo_time(), 101u * 57u, "100 cm at default temperature"); + + sonar.set_temperature(20.0f); // us_per_cm ≈ 58.238 + unsigned int expected = (unsigned int)(101.0f * (20000.0f / (331.3f + 0.606f * 20.0f))); + CHECK_EQ(sonar.max_echo_time(), expected, "100 cm preserved after set_temperature"); + + // Repeated calls at the same temperature must be idempotent. + unsigned int first = sonar.max_echo_time(); + for (int i = 0; i < 5; ++i) sonar.set_temperature(20.0f); + CHECK_EQ(sonar.max_echo_time(), first, "repeated set_temperature() does not drift"); + + // Round-tripping back to the default speed restores the original timeout. + sonar.set_temperature(20.0f); + sonar.set_temperature(0.0f); + sonar.set_temperature(20.0f); + CHECK_EQ(sonar.max_echo_time(), first, "temperature round-trip is stable"); +} + +static void test_max_distance_clamping() { + printf("\n--- regression: oversized max_cm_distance is clamped ---\n"); + + // On AVR `unsigned int` is 16-bit, so `max_cm_distance + 1` wrapped to 0 and + // left maxEchoTime at 0 — every ping would return NO_ECHO. Clamp comes first. + // + // The host has 32-bit `unsigned int`, so that wrap cannot happen here + // naturally. Reproduce AVR's width explicitly to pin the ordering — same + // technique the micros() overflow tests above use for 32-bit rollover. + { + uint16_t max_cm = 65535; + + // Old ordering: increment first, then clamp. The increment wraps and the + // clamp then has nothing to save. + uint16_t incremented_first = (uint16_t)(max_cm + 1); + CHECK_EQ(incremented_first, 0u, + "AVR width: max_cm_distance + 1 wraps to 0 (would disable ranging)"); + + // Current ordering: clamp first, then increment. + uint16_t clamped = (max_cm > MAX_SENSOR_DISTANCE) + ? (uint16_t)MAX_SENSOR_DISTANCE : max_cm; + CHECK_EQ((uint16_t)(clamped + 1), 501u, + "AVR width: clamp-before-increment yields 501 (correct)"); + } + + mock_reset(); + TestableSonar huge(12, 14, 65535); + // The stored distance must be the CLAMPED value. This is the assertion that + // actually fails if the clamp is removed — _maxEchoTime alone cannot detect + // it on a 32-bit host, since both orderings coincide at this width. + CHECK_EQ(huge.max_cm_distance(), (unsigned int)MAX_SENSOR_DISTANCE, + "65535 cm is stored clamped to MAX_SENSOR_DISTANCE"); + CHECK(huge.max_cm_distance() <= (unsigned int)MAX_SENSOR_DISTANCE, + "stored distance never exceeds MAX_SENSOR_DISTANCE"); + CHECK_EQ(huge.max_echo_time(), 501u * 57u, "65535 cm clamps to MAX_SENSOR_DISTANCE"); + CHECK(huge.max_echo_time() != 0u, "clamped maxEchoTime is non-zero (ranging still works)"); + + mock_reset(); + TestableSonar at_limit(12, 14, MAX_SENSOR_DISTANCE); + CHECK_EQ(at_limit.max_echo_time(), 501u * 57u, "exactly MAX_SENSOR_DISTANCE"); + + // And a clamped sensor must still actually range. + mock_reset(); + push_clean_ping(5700); + TestableSonar ranging(12, 14, 65535); + CHECK(ranging.ping() != (unsigned int)NO_ECHO, "clamped sensor still returns an echo"); +} + +// --------------------------------------------------------------------------- +// main +// --------------------------------------------------------------------------- + +int main() { + printf("NewPingPlus unit tests\n"); + printf("======================\n"); + + // Conversion math + test_convert_cm(); + test_convert_in(); + test_convert_mm(); + + // Temperature + test_set_temperature(); + test_temperature_affects_max_echo_time(); + + // Distance config + test_set_max_distance(); + + // One-pin mode + test_one_pin_mode(); + + // ping() behaviour + test_ping_busy_returns_no_echo(); + test_ping_timeout_returns_no_echo(); + test_ping_sensor_start_timeout(); + test_ping_returns_echo_time(); + + // ping_cm / ping_in / ping_mm + test_ping_cm_math(); + test_ping_cm_end_to_end(); + test_ping_mm_more_precise_than_cm(); + + // ping_median + test_ping_median_single(); + test_ping_median_odd_count(); + test_ping_median_even_count(); + test_ping_median_skips_out_of_range(); + test_ping_median_all_out_of_range(); + test_ping_median_clamps_iterations(); + + // Overflow safety + test_overflow_elapsed_time_arithmetic(); + test_overflow_does_not_hang(); + + // Regressions from the v2.2.0 pre-release review + test_yield_is_actually_called(); + test_set_temperature_preserves_max_distance(); + test_max_distance_clamping(); + + printf("\n======================\n"); + printf("Results: %d passed, %d failed\n", g_pass, g_fail); + return g_fail == 0 ? 0 : 1; +} From 99b135c066edf9bf67645215fbdfc6aa573538a8 Mon Sep 17 00:00:00 2001 From: Jordan Shaw Date: Thu, 13 Aug 2026 11:13:13 -0400 Subject: [PATCH 2/3] CI: use library-manager submit mode, bump actions off Node 20 The lint job failed with LP018 "Library name NewPingPlus not found in the Library Manager index". The cause was `library-manager: update`, which asserts the library is already indexed. NewPingPlus has not been submitted yet, so `submit` is the correct mode -- it applies the rules for a new submission instead. Confirmed the name is genuinely unsubmitted rather than misnamed: neither NewPingPlus nor NewPingESP8266 appears in library_index.json (only Tim Eckel's NewPing), and arduino/library-registry lists no repository under this account for it. So the rename does not violate the "names cannot change after being added to the index" rule -- the old name was never in the index. Added a comment to switch this back to `update` once the registry PR merges. Verified locally against the committed tree: arduino-lint --library-manager submit --compliance strict --project-type all reports no errors or warnings. Also bumped actions to Node 24 runtimes to clear the deprecation warning: arduino-lint-action v1 -> v3 (same inputs, runs: node24), checkout v4 -> v5. compile-sketches stays at v1; that is still its current major. Co-Authored-By: Claude Opus 5 --- .github/workflows/ci.yml | 20 ++++++++++++++------ 1 file changed, 14 insertions(+), 6 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 6a1e91b..cc85071 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -12,7 +12,7 @@ jobs: name: Host unit tests runs-on: ubuntu-latest steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v5 - name: make test run: make test @@ -20,10 +20,18 @@ jobs: name: arduino-lint (Library Manager rules) runs-on: ubuntu-latest steps: - - uses: actions/checkout@v4 - - uses: arduino/arduino-lint-action@v1 + - uses: actions/checkout@v5 + - uses: arduino/arduino-lint-action@v3 with: - library-manager: update + # `submit` = the rules for a library not yet in the Library Manager + # index. `update` asserts the library is ALREADY indexed and fails with + # LP018 ("name not found in the index") if it isn't — NewPingPlus has + # not been submitted yet. + # + # Switch this to `update` once the registry PR is merged and the + # library appears in the index; `update` then enforces the rules that + # apply to released versions (notably that the name can never change). + library-manager: submit compliance: strict compile: @@ -56,7 +64,7 @@ jobs: platform: teensy:avr url: https://www.pjrc.com/teensy/package_teensy_index.json steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v5 - uses: arduino/compile-sketches@v1 with: fqbn: ${{ matrix.board.fqbn }} @@ -86,7 +94,7 @@ jobs: platform: esp32:esp32 url: https://espressif.github.io/arduino-esp32/package_esp32_index.json steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v5 - uses: arduino/compile-sketches@v1 with: fqbn: ${{ matrix.board.fqbn }} From aedda4eaa3b6a0ea94cba320f2e6f93bb21a9cf4 Mon Sep 17 00:00:00 2001 From: Jordan Shaw Date: Thu, 13 Aug 2026 11:35:28 -0400 Subject: [PATCH 3/3] CI: fix compile job crash on the Arduino Uno matrix entry The Uno job failed inside the action itself: File "compilesketches.py", line 363, in sort_dependency_list if dependency[self.dependency_source_url_key].rstrip("/")... AttributeError: 'NoneType' object has no attribute 'rstrip' The matrix assembled the platforms block from separate `platform` and `url` keys. arduino:avr ships with arduino-cli and needs no source-url, so that entry carried url: '' -- which renders as `source-url:` with a null value. The action dereferences source-url unconditionally, so null crashes it. Omitting the key entirely is the only way to express "no source-url", which means the whole YAML block has to live in the matrix. Every entry's rendered block is now parsed and asserted non-null in review; the other five boards were unaffected because they all supply a real URL. Also adds Teensy 4.1 to the compile matrix. The previous commit message listed teensy41 among the verified targets, but only teensy31 and teensy40 were actually compiled. Teensy 4.1 is now verified locally (clean, no warnings) and covered in CI. Co-Authored-By: Claude Opus 5 --- .github/workflows/ci.yml | 54 ++++++++++++++++++++++++---------------- 1 file changed, 33 insertions(+), 21 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index cc85071..4f75926 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -40,37 +40,49 @@ jobs: strategy: fail-fast: false matrix: + # `platforms` is the whole YAML block, not name+url assembled in the step. + # arduino:avr ships with arduino-cli and takes no source-url; rendering an + # empty one emits `source-url:` as null, and the action dereferences it + # unconditionally (AttributeError: 'NoneType' object has no attribute + # 'rstrip'). Omitting the key entirely is the only way to express that. board: - name: Arduino Uno fqbn: arduino:avr:uno - platform: arduino:avr - url: '' + platforms: | + - name: arduino:avr - name: ESP8266 NodeMCU fqbn: esp8266:esp8266:nodemcuv2 - platform: esp8266:esp8266 - url: https://arduino.esp8266.com/stable/package_esp8266com_index.json + platforms: | + - name: esp8266:esp8266 + source-url: https://arduino.esp8266.com/stable/package_esp8266com_index.json - name: ESP32 Dev Module fqbn: esp32:esp32:esp32 - platform: esp32:esp32 - url: https://espressif.github.io/arduino-esp32/package_esp32_index.json + platforms: | + - name: esp32:esp32 + source-url: https://espressif.github.io/arduino-esp32/package_esp32_index.json - name: Teensy 3.2 fqbn: teensy:avr:teensy31 - platform: teensy:avr - url: https://www.pjrc.com/teensy/package_teensy_index.json + platforms: | + - name: teensy:avr + source-url: https://www.pjrc.com/teensy/package_teensy_index.json # Teensy 4.x uses 32-bit port registers and must stay on the - # digitalWrite path — this entry is what catches a regression there. + # digitalWrite path — these entries catch a regression there. - name: Teensy 4.0 fqbn: teensy:avr:teensy40 - platform: teensy:avr - url: https://www.pjrc.com/teensy/package_teensy_index.json + platforms: | + - name: teensy:avr + source-url: https://www.pjrc.com/teensy/package_teensy_index.json + - name: Teensy 4.1 + fqbn: teensy:avr:teensy41 + platforms: | + - name: teensy:avr + source-url: https://www.pjrc.com/teensy/package_teensy_index.json steps: - uses: actions/checkout@v5 - uses: arduino/compile-sketches@v1 with: fqbn: ${{ matrix.board.fqbn }} - platforms: | - - name: ${{ matrix.board.platform }} - source-url: ${{ matrix.board.url }} + platforms: ${{ matrix.board.platforms }} libraries: | - source-path: ./ # The async example is guarded with #error off ESP, so only compile it there. @@ -87,20 +99,20 @@ jobs: board: - name: ESP8266 NodeMCU fqbn: esp8266:esp8266:nodemcuv2 - platform: esp8266:esp8266 - url: https://arduino.esp8266.com/stable/package_esp8266com_index.json + platforms: | + - name: esp8266:esp8266 + source-url: https://arduino.esp8266.com/stable/package_esp8266com_index.json - name: ESP32 Dev Module fqbn: esp32:esp32:esp32 - platform: esp32:esp32 - url: https://espressif.github.io/arduino-esp32/package_esp32_index.json + platforms: | + - name: esp32:esp32 + source-url: https://espressif.github.io/arduino-esp32/package_esp32_index.json steps: - uses: actions/checkout@v5 - uses: arduino/compile-sketches@v1 with: fqbn: ${{ matrix.board.fqbn }} - platforms: | - - name: ${{ matrix.board.platform }} - source-url: ${{ matrix.board.url }} + platforms: ${{ matrix.board.platforms }} libraries: | - source-path: ./ sketch-paths: |