Skip to content

Commit ce92203

Browse files
Copilotdorkmo
andcommitted
Add hall effect sensor type selection and time-based detection
Co-authored-by: dorkmo <[email protected]>
1 parent 61e889e commit ce92203

File tree

1 file changed

+227
-47
lines changed

1 file changed

+227
-47
lines changed

TankAlarm-112025-Client-BluesOpta/TankAlarm-112025-Client-BluesOpta.ino

Lines changed: 227 additions & 47 deletions
Original file line numberDiff line numberDiff line change
@@ -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
279293
enum 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;
416432
static unsigned long gRpmLastSampleMillis[MAX_TANKS] = {0};
417433
static float gRpmLastReading[MAX_TANKS] = {0.0f};
418434
static 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

Comments
 (0)