@@ -275,6 +275,20 @@ enum CurrentLoopSensorType : uint8_t {
275275 // 4mA = full (sensor close to liquid), 20mA = empty (sensor far from liquid)
276276};
277277
278+ // Hall effect sensor types for RPM measurement
279+ enum HallEffectSensorType : uint8_t {
280+ HALL_EFFECT_UNIPOLAR = 0 , // Triggered by single pole (usually South), reset when field removed
281+ HALL_EFFECT_BIPOLAR = 1 , // Latching: South pole turns ON, North pole turns OFF
282+ HALL_EFFECT_OMNIPOLAR = 2 , // Responds to either North or South pole
283+ HALL_EFFECT_ANALOG = 3 // Linear/analog: outputs voltage proportional to magnetic field strength
284+ };
285+
286+ // Hall effect detection method
287+ enum HallEffectDetectionMethod : uint8_t {
288+ HALL_DETECT_PULSE = 0 , // Count pulses (transitions) - traditional method
289+ HALL_DETECT_TIME_BASED = 1 // Measure time between pulses - more flexible for different magnet types
290+ };
291+
278292// Relay trigger conditions - which alarm type triggers the relay
279293enum RelayTrigger : uint8_t {
280294 RELAY_TRIGGER_ANY = 0 , // Trigger on any alarm (high or low)
@@ -302,6 +316,8 @@ struct TankConfig {
302316 int16_t currentLoopChannel; // 4-20mA channel index (-1 if unused)
303317 int16_t rpmPin; // Hall effect RPM sensor pin (-1 if unused)
304318 uint8_t pulsesPerRevolution; // For RPM sensors: pulses per revolution (default 1)
319+ HallEffectSensorType hallEffectType; // Type of hall effect sensor (unipolar, bipolar, omnipolar, analog)
320+ HallEffectDetectionMethod hallEffectDetection; // Detection method (pulse counting or time-based)
305321 float highAlarmThreshold; // High threshold for triggering alarm (inches or RPM)
306322 float lowAlarmThreshold; // Low threshold for triggering alarm (inches or RPM)
307323 float hysteresisValue; // Hysteresis band (default 2.0)
@@ -416,6 +432,9 @@ static bool gClearButtonInitialized = false;
416432static unsigned long gRpmLastSampleMillis [MAX_TANKS] = {0 };
417433static float gRpmLastReading [MAX_TANKS] = {0 .0f };
418434static int gRpmLastPinState [MAX_TANKS]; // Initialized dynamically in setup()
435+ // For time-based detection: track time between pulses
436+ static unsigned long gRpmLastPulseTime [MAX_TANKS] = {0 };
437+ static unsigned long gRpmPulsePeriodMs [MAX_TANKS] = {0 };
419438
420439// RPM sampling duration in milliseconds (sample for a few seconds each period)
421440#ifndef RPM_SAMPLE_DURATION_MS
@@ -539,6 +558,8 @@ void setup() {
539558 gRpmLastPinState [i] = HIGH;
540559 gRpmLastSampleMillis [i] = 0 ;
541560 gRpmLastReading [i] = 0 .0f ;
561+ gRpmLastPulseTime [i] = 0 ;
562+ gRpmPulsePeriodMs [i] = 0 ;
542563 }
543564
544565 // Explicitly initialize relay state tracking arrays for clarity and consistency
@@ -717,6 +738,8 @@ static void createDefaultConfig(ClientConfig &cfg) {
717738 cfg.tanks [0 ].currentLoopChannel = -1 ;
718739 cfg.tanks [0 ].rpmPin = -1 ; // No RPM sensor by default
719740 cfg.tanks [0 ].pulsesPerRevolution = 1 ; // Default: 1 pulse per revolution
741+ cfg.tanks [0 ].hallEffectType = HALL_EFFECT_UNIPOLAR; // Default: unipolar sensor
742+ cfg.tanks [0 ].hallEffectDetection = HALL_DETECT_PULSE; // Default: pulse counting method
720743 cfg.tanks [0 ].highAlarmThreshold = 100 .0f ;
721744 cfg.tanks [0 ].lowAlarmThreshold = 20 .0f ;
722745 cfg.tanks [0 ].hysteresisValue = 2 .0f ; // 2 unit hysteresis band
@@ -842,6 +865,24 @@ static bool loadConfigFromFlash(ClientConfig &cfg) {
842865 cfg.tanks [i].currentLoopChannel = t[" loopChannel" ].is <int >() ? t[" loopChannel" ].as <int >() : -1 ;
843866 cfg.tanks [i].rpmPin = t[" rpmPin" ].is <int >() ? t[" rpmPin" ].as <int >() : -1 ;
844867 cfg.tanks [i].pulsesPerRevolution = t[" pulsesPerRev" ].is <uint8_t >() ? max ((uint8_t )1 , t[" pulsesPerRev" ].as <uint8_t >()) : 1 ;
868+ // Load hall effect sensor type
869+ const char *hallType = t[" hallEffectType" ].as <const char *>();
870+ if (hallType && strcmp (hallType, " bipolar" ) == 0 ) {
871+ cfg.tanks [i].hallEffectType = HALL_EFFECT_BIPOLAR;
872+ } else if (hallType && strcmp (hallType, " omnipolar" ) == 0 ) {
873+ cfg.tanks [i].hallEffectType = HALL_EFFECT_OMNIPOLAR;
874+ } else if (hallType && strcmp (hallType, " analog" ) == 0 ) {
875+ cfg.tanks [i].hallEffectType = HALL_EFFECT_ANALOG;
876+ } else {
877+ cfg.tanks [i].hallEffectType = HALL_EFFECT_UNIPOLAR; // Default
878+ }
879+ // Load hall effect detection method
880+ const char *hallDetect = t[" hallEffectDetection" ].as <const char *>();
881+ if (hallDetect && strcmp (hallDetect, " time" ) == 0 ) {
882+ cfg.tanks [i].hallEffectDetection = HALL_DETECT_TIME_BASED;
883+ } else {
884+ cfg.tanks [i].hallEffectDetection = HALL_DETECT_PULSE; // Default
885+ }
845886 cfg.tanks [i].highAlarmThreshold = t[" highAlarm" ].is <float >() ? t[" highAlarm" ].as <float >() : 100 .0f ;
846887 cfg.tanks [i].lowAlarmThreshold = t[" lowAlarm" ].is <float >() ? t[" lowAlarm" ].as <float >() : 20 .0f ;
847888 cfg.tanks [i].hysteresisValue = t[" hysteresis" ].is <float >() ? t[" hysteresis" ].as <float >() : 2 .0f ;
@@ -952,6 +993,20 @@ static bool saveConfigToFlash(const ClientConfig &cfg) {
952993 t[" loopChannel" ] = cfg.tanks [i].currentLoopChannel ;
953994 t[" rpmPin" ] = cfg.tanks [i].rpmPin ;
954995 t[" pulsesPerRev" ] = cfg.tanks [i].pulsesPerRevolution ;
996+ // Save hall effect sensor type
997+ switch (cfg.tanks [i].hallEffectType ) {
998+ case HALL_EFFECT_BIPOLAR: t[" hallEffectType" ] = " bipolar" ; break ;
999+ case HALL_EFFECT_OMNIPOLAR: t[" hallEffectType" ] = " omnipolar" ; break ;
1000+ case HALL_EFFECT_ANALOG: t[" hallEffectType" ] = " analog" ; break ;
1001+ case HALL_EFFECT_UNIPOLAR:
1002+ default : t[" hallEffectType" ] = " unipolar" ; break ;
1003+ }
1004+ // Save hall effect detection method
1005+ switch (cfg.tanks [i].hallEffectDetection ) {
1006+ case HALL_DETECT_TIME_BASED: t[" hallEffectDetection" ] = " time" ; break ;
1007+ case HALL_DETECT_PULSE:
1008+ default : t[" hallEffectDetection" ] = " pulse" ; break ;
1009+ }
9551010 t[" highAlarm" ] = cfg.tanks [i].highAlarmThreshold ;
9561011 t[" lowAlarm" ] = cfg.tanks [i].lowAlarmThreshold ;
9571012 t[" hysteresis" ] = cfg.tanks [i].hysteresisValue ;
@@ -1403,6 +1458,28 @@ static void applyConfigUpdate(const JsonDocument &doc) {
14031458 if (t.containsKey (" pulsesPerRev" )) {
14041459 gConfig .tanks [i].pulsesPerRevolution = max ((uint8_t )1 , t[" pulsesPerRev" ].as <uint8_t >());
14051460 }
1461+ // Update hall effect sensor type if provided
1462+ if (t.containsKey (" hallEffectType" )) {
1463+ const char *hallType = t[" hallEffectType" ].as <const char *>();
1464+ if (hallType && strcmp (hallType, " bipolar" ) == 0 ) {
1465+ gConfig .tanks [i].hallEffectType = HALL_EFFECT_BIPOLAR;
1466+ } else if (hallType && strcmp (hallType, " omnipolar" ) == 0 ) {
1467+ gConfig .tanks [i].hallEffectType = HALL_EFFECT_OMNIPOLAR;
1468+ } else if (hallType && strcmp (hallType, " analog" ) == 0 ) {
1469+ gConfig .tanks [i].hallEffectType = HALL_EFFECT_ANALOG;
1470+ } else if (hallType && strcmp (hallType, " unipolar" ) == 0 ) {
1471+ gConfig .tanks [i].hallEffectType = HALL_EFFECT_UNIPOLAR;
1472+ }
1473+ }
1474+ // Update hall effect detection method if provided
1475+ if (t.containsKey (" hallEffectDetection" )) {
1476+ const char *hallDetect = t[" hallEffectDetection" ].as <const char *>();
1477+ if (hallDetect && strcmp (hallDetect, " time" ) == 0 ) {
1478+ gConfig .tanks [i].hallEffectDetection = HALL_DETECT_TIME_BASED;
1479+ } else if (hallDetect && strcmp (hallDetect, " pulse" ) == 0 ) {
1480+ gConfig .tanks [i].hallEffectDetection = HALL_DETECT_PULSE;
1481+ }
1482+ }
14061483 gConfig .tanks [i].highAlarmThreshold = t[" highAlarm" ].is <float >() ? t[" highAlarm" ].as <float >() : gConfig .tanks [i].highAlarmThreshold ;
14071484 gConfig .tanks [i].lowAlarmThreshold = t[" lowAlarm" ].is <float >() ? t[" lowAlarm" ].as <float >() : gConfig .tanks [i].lowAlarmThreshold ;
14081485 gConfig .tanks [i].hysteresisValue = t[" hysteresis" ].is <float >() ? t[" hysteresis" ].as <float >() : gConfig .tanks [i].hysteresisValue ;
@@ -1804,67 +1881,170 @@ static float readTankSensor(uint8_t idx) {
18041881 return levelInches;
18051882 }
18061883 case SENSOR_HALL_EFFECT_RPM: {
1807- // Hall effect RPM sensor - sample pulses for a few seconds each measurement period
1884+ // Hall effect RPM sensor - supports multiple sensor types and detection methods
18081885 // Use rpmPin if available, otherwise use primaryPin
18091886 int pin = (cfg.rpmPin >= 0 && cfg.rpmPin < 255 ) ? cfg.rpmPin :
18101887 ((cfg.primaryPin >= 0 && cfg.primaryPin < 255 ) ? cfg.primaryPin : (2 + idx));
18111888
1812- // Configure pin as input with pullup for Hall effect sensor
1889+ // Configure pin as input with pullup for digital Hall effect sensors
1890+ // Analog sensors would typically use analog input, but we support digital threshold mode here
18131891 pinMode (pin, INPUT_PULLUP);
18141892
1815- // Sample pulses for RPM_SAMPLE_DURATION_MS (default 3 seconds)
1816- // This provides accurate RPM measurement by counting multiple pulses
1817- unsigned long sampleStart = millis ();
1818- uint32_t pulseCount = 0 ;
1893+ float rpm = 0 .0f ;
18191894
1820- // Always read current pin state first to establish baseline
1821- // This prevents false pulse counts on first reading
1822- int lastState = digitalRead (pin);
1823- gRpmLastPinState [idx] = lastState;
1824-
1825- // Count pulses over the sampling duration, with debounce
1826- // Use unsigned subtraction to handle millis() overflow correctly
1827- const unsigned long DEBOUNCE_MS = 2 ; // Minimum time between pulses to filter bounce
1828- const uint32_t MAX_ITERATIONS = RPM_SAMPLE_DURATION_MS * 2 ; // Safety limit: 2x expected iterations
1829- unsigned long lastPulseTime = 0 ;
1830- uint32_t iterationCount = 0 ;
1831- while ((millis () - sampleStart) < (unsigned long )RPM_SAMPLE_DURATION_MS && iterationCount < MAX_ITERATIONS) {
1832- int currentState = digitalRead (pin);
1833- // Count falling edges (HIGH to LOW transitions) with debounce
1834- if (lastState == HIGH && currentState == LOW) {
1835- unsigned long now = millis ();
1836- if (now - lastPulseTime >= DEBOUNCE_MS) {
1837- pulseCount++;
1838- lastPulseTime = now;
1895+ // Choose detection method: pulse counting or time-based
1896+ if (cfg.hallEffectDetection == HALL_DETECT_TIME_BASED) {
1897+ // Time-based detection: measure period between pulses
1898+ // More flexible for different magnet types and orientations
1899+ // Requires fewer pulses to get a reading
1900+
1901+ unsigned long sampleStart = millis ();
1902+ int lastState = digitalRead (pin);
1903+ gRpmLastPinState [idx] = lastState;
1904+
1905+ const unsigned long DEBOUNCE_MS = 2 ;
1906+ const uint32_t MAX_ITERATIONS = RPM_SAMPLE_DURATION_MS * 2 ;
1907+ unsigned long firstPulseTime = 0 ;
1908+ unsigned long secondPulseTime = 0 ;
1909+ uint32_t iterationCount = 0 ;
1910+ bool firstPulseDetected = false ;
1911+
1912+ // Detect edge transitions based on sensor type
1913+ while ((millis () - sampleStart) < (unsigned long )RPM_SAMPLE_DURATION_MS && iterationCount < MAX_ITERATIONS) {
1914+ int currentState = digitalRead (pin);
1915+ bool edgeDetected = false ;
1916+
1917+ // Determine edge detection based on hall effect sensor type
1918+ switch (cfg.hallEffectType ) {
1919+ case HALL_EFFECT_UNIPOLAR:
1920+ // Unipolar: triggers on one pole (active low), detect falling edge
1921+ edgeDetected = (lastState == HIGH && currentState == LOW);
1922+ break ;
1923+ case HALL_EFFECT_BIPOLAR:
1924+ // Bipolar/Latching: alternates between states, detect both edges
1925+ edgeDetected = (lastState != currentState);
1926+ break ;
1927+ case HALL_EFFECT_OMNIPOLAR:
1928+ // Omnipolar: responds to either pole, detect both edges
1929+ edgeDetected = (lastState != currentState);
1930+ break ;
1931+ case HALL_EFFECT_ANALOG:
1932+ // For analog sensors in digital mode, detect falling edge (threshold crossing)
1933+ edgeDetected = (lastState == HIGH && currentState == LOW);
1934+ break ;
18391935 }
1936+
1937+ if (edgeDetected) {
1938+ unsigned long now = millis ();
1939+ if (now - gRpmLastPulseTime [idx] >= DEBOUNCE_MS) {
1940+ if (!firstPulseDetected) {
1941+ firstPulseTime = now;
1942+ firstPulseDetected = true ;
1943+ } else {
1944+ secondPulseTime = now;
1945+ gRpmPulsePeriodMs [idx] = secondPulseTime - firstPulseTime;
1946+ gRpmLastPulseTime [idx] = secondPulseTime;
1947+ break ; // Got our measurement, exit early
1948+ }
1949+ }
1950+ }
1951+ lastState = currentState;
1952+ delay (1 );
1953+ iterationCount++;
1954+
1955+ #ifdef WATCHDOG_AVAILABLE
1956+ #if defined(ARDUINO_OPTA) || defined(ARDUINO_ARCH_MBED)
1957+ mbedWatchdog.kick ();
1958+ #else
1959+ IWatchdog.reload ();
1960+ #endif
1961+ #endif
1962+ }
1963+
1964+ gRpmLastPinState [idx] = lastState;
1965+ gRpmLastSampleMillis [idx] = millis ();
1966+
1967+ // Calculate RPM from pulse period
1968+ if (gRpmPulsePeriodMs [idx] > 0 ) {
1969+ const float MS_PER_MINUTE = 60000 .0f ;
1970+ uint8_t pulsesPerRev = (cfg.pulsesPerRevolution > 0 ) ? cfg.pulsesPerRevolution : 1 ;
1971+ // RPM = (60000 ms/min) / (period_ms * pulses_per_rev)
1972+ rpm = MS_PER_MINUTE / ((float )gRpmPulsePeriodMs [idx] * (float )pulsesPerRev);
1973+ } else {
1974+ // No valid period, keep last reading
1975+ rpm = gRpmLastReading [idx];
18401976 }
1841- lastState = currentState;
1842- // Small delay to allow other processing and avoid excessive polling
1843- delay (1 );
1844- iterationCount++;
18451977
1978+ } else {
1979+ // Pulse counting method (traditional approach)
1980+ // Sample pulses for RPM_SAMPLE_DURATION_MS (default 3 seconds)
1981+ // This provides accurate RPM measurement by counting multiple pulses
1982+
1983+ unsigned long sampleStart = millis ();
1984+ uint32_t pulseCount = 0 ;
1985+
1986+ // Always read current pin state first to establish baseline
1987+ int lastState = digitalRead (pin);
1988+ gRpmLastPinState [idx] = lastState;
1989+
1990+ const unsigned long DEBOUNCE_MS = 2 ;
1991+ const uint32_t MAX_ITERATIONS = RPM_SAMPLE_DURATION_MS * 2 ;
1992+ unsigned long lastPulseTime = 0 ;
1993+ uint32_t iterationCount = 0 ;
1994+
1995+ while ((millis () - sampleStart) < (unsigned long )RPM_SAMPLE_DURATION_MS && iterationCount < MAX_ITERATIONS) {
1996+ int currentState = digitalRead (pin);
1997+ bool edgeDetected = false ;
1998+
1999+ // Determine edge detection based on hall effect sensor type
2000+ switch (cfg.hallEffectType ) {
2001+ case HALL_EFFECT_UNIPOLAR:
2002+ // Unipolar: triggers on one pole, detect falling edge (active low)
2003+ edgeDetected = (lastState == HIGH && currentState == LOW);
2004+ break ;
2005+ case HALL_EFFECT_BIPOLAR:
2006+ // Bipolar/Latching: alternates between states, count both edges
2007+ edgeDetected = (lastState != currentState);
2008+ break ;
2009+ case HALL_EFFECT_OMNIPOLAR:
2010+ // Omnipolar: responds to either pole, count both edges
2011+ edgeDetected = (lastState != currentState);
2012+ break ;
2013+ case HALL_EFFECT_ANALOG:
2014+ // For analog sensors in digital mode, detect falling edge
2015+ edgeDetected = (lastState == HIGH && currentState == LOW);
2016+ break ;
2017+ }
2018+
2019+ if (edgeDetected) {
2020+ unsigned long now = millis ();
2021+ if (now - lastPulseTime >= DEBOUNCE_MS) {
2022+ pulseCount++;
2023+ lastPulseTime = now;
2024+ }
2025+ }
2026+ lastState = currentState;
2027+ delay (1 );
2028+ iterationCount++;
2029+
18462030#ifdef WATCHDOG_AVAILABLE
1847- // Reset watchdog during long sampling to prevent timeout
1848- #if defined(ARDUINO_OPTA) || defined(ARDUINO_ARCH_MBED)
1849- mbedWatchdog.kick ();
1850- #else
1851- IWatchdog.reload ();
1852- #endif
2031+ #if defined(ARDUINO_OPTA) || defined(ARDUINO_ARCH_MBED)
2032+ mbedWatchdog.kick ();
2033+ #else
2034+ IWatchdog.reload ();
2035+ #endif
18532036#endif
2037+ }
2038+
2039+ gRpmLastPinState [idx] = lastState;
2040+ gRpmLastSampleMillis [idx] = millis ();
2041+
2042+ // Calculate RPM from pulse count
2043+ const float MS_PER_MINUTE = 60000 .0f ;
2044+ uint8_t pulsesPerRev = (cfg.pulsesPerRevolution > 0 ) ? cfg.pulsesPerRevolution : 1 ;
2045+ rpm = ((float )pulseCount * MS_PER_MINUTE) / ((float )RPM_SAMPLE_DURATION_MS * (float )pulsesPerRev);
18542046 }
18552047
1856- // Save last pin state for next sample
1857- gRpmLastPinState [idx] = lastState;
1858- gRpmLastSampleMillis [idx] = millis ();
1859-
1860- // Calculate RPM from pulse count
1861- // Formula: RPM = (pulses / sample_duration_seconds) * 60 seconds/minute / pulses_per_revolution
1862- // Simplified: RPM = (pulses * 60000) / (sample_duration_ms * pulses_per_rev)
1863- // Note: Max RPM is limited by the polling loop speed (approx 30,000 RPM at 1 pulse/rev), which is sufficient.
1864- const float MS_PER_MINUTE = 60000 .0f ;
1865- uint8_t pulsesPerRev = (cfg.pulsesPerRevolution > 0 ) ? cfg.pulsesPerRevolution : 1 ;
1866- float rpm = ((float )pulseCount * MS_PER_MINUTE) / ((float )RPM_SAMPLE_DURATION_MS * (float )pulsesPerRev);
1867-
18682048 gRpmLastReading [idx] = rpm;
18692049
18702050 // Return RPM value (use highAlarmThreshold for max expected RPM)
0 commit comments