Skip to content

Commit 33cd7af

Browse files
authored
Merge pull request #110 from SenaxInc/copilot/update-sensor-configuration-tool
Add digital input (float switch) sensor support with boolean alarm handling
2 parents eb57e86 + 1016b5c commit 33cd7af

File tree

2 files changed

+228
-25
lines changed

2 files changed

+228
-25
lines changed

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

Lines changed: 115 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -187,6 +187,19 @@ static size_t strlcpy(char *dst, const char *src, size_t size) {
187187
#define MIN_ALARM_INTERVAL_SECONDS 300 // Minimum 5 minutes between same alarm type
188188
#endif
189189

190+
// Digital sensor (float switch) constants
191+
#ifndef DIGITAL_SWITCH_THRESHOLD
192+
#define DIGITAL_SWITCH_THRESHOLD 0.5f // Threshold to determine activated vs not-activated state
193+
#endif
194+
195+
#ifndef DIGITAL_SENSOR_ACTIVATED_VALUE
196+
#define DIGITAL_SENSOR_ACTIVATED_VALUE 1.0f // Value returned when switch is activated
197+
#endif
198+
199+
#ifndef DIGITAL_SENSOR_NOT_ACTIVATED_VALUE
200+
#define DIGITAL_SENSOR_NOT_ACTIVATED_VALUE 0.0f // Value returned when switch is not activated
201+
#endif
202+
190203
static const uint8_t NOTECARD_I2C_ADDRESS = 0x17;
191204
static const uint32_t NOTECARD_I2C_FREQUENCY = 400000UL;
192205

@@ -235,6 +248,8 @@ struct TankConfig {
235248
uint8_t relayMask; // Bitmask of relays to trigger (bit 0=relay 1, etc.)
236249
RelayTrigger relayTrigger; // Which alarm type triggers the relay (any, high, low)
237250
RelayMode relayMode; // How long relay stays on (momentary, until_clear, manual_reset)
251+
// Digital sensor (float switch) specific settings
252+
char digitalTrigger[16]; // 'activated' or 'not_activated' - when to trigger alarm for digital sensors
238253
};
239254

240255
struct ClientConfig {
@@ -615,6 +630,7 @@ static void createDefaultConfig(ClientConfig &cfg) {
615630
cfg.tanks[0].relayMask = 0; // No relays triggered by default
616631
cfg.tanks[0].relayTrigger = RELAY_TRIGGER_ANY; // Default: trigger on any alarm
617632
cfg.tanks[0].relayMode = RELAY_MODE_MOMENTARY; // Default: momentary 30 min activation
633+
cfg.tanks[0].digitalTrigger[0] = '\0'; // Not a digital sensor by default
618634
}
619635

620636
static bool loadConfigFromFlash(ClientConfig &cfg) {
@@ -740,6 +756,9 @@ static bool loadConfigFromFlash(ClientConfig &cfg) {
740756
} else {
741757
cfg.tanks[i].relayMode = RELAY_MODE_MOMENTARY;
742758
}
759+
// Load digital sensor trigger state (for float switches)
760+
const char *digitalTriggerStr = t["digitalTrigger"].as<const char *>();
761+
strlcpy(cfg.tanks[i].digitalTrigger, digitalTriggerStr ? digitalTriggerStr : "", sizeof(cfg.tanks[i].digitalTrigger));
743762
}
744763

745764
return true;
@@ -804,6 +823,10 @@ static bool saveConfigToFlash(const ClientConfig &cfg) {
804823
case RELAY_MODE_MANUAL_RESET: t["relayMode"] = "manual_reset"; break;
805824
default: t["relayMode"] = "momentary"; break;
806825
}
826+
// Save digital sensor trigger state (for float switches)
827+
if (cfg.tanks[i].digitalTrigger[0] != '\0') {
828+
t["digitalTrigger"] = cfg.tanks[i].digitalTrigger;
829+
}
807830
}
808831

809832
#if defined(ARDUINO_OPTA) || defined(ARDUINO_ARCH_MBED)
@@ -1230,6 +1253,13 @@ static void applyConfigUpdate(const JsonDocument &doc) {
12301253
gConfig.tanks[i].relayMode = RELAY_MODE_MOMENTARY;
12311254
}
12321255
}
1256+
// Handle digital sensor trigger state (for float switches)
1257+
if (t.containsKey("digitalTrigger")) {
1258+
const char *digitalTriggerStr = t["digitalTrigger"].as<const char *>();
1259+
strlcpy(gConfig.tanks[i].digitalTrigger,
1260+
digitalTriggerStr ? digitalTriggerStr : "",
1261+
sizeof(gConfig.tanks[i].digitalTrigger));
1262+
}
12331263
}
12341264
}
12351265

@@ -1377,11 +1407,16 @@ static float readTankSensor(uint8_t idx) {
13771407

13781408
switch (cfg.sensorType) {
13791409
case SENSOR_DIGITAL: {
1380-
// Use explicit bounds check for pin
1410+
// Float switch sensor - returns activated/not-activated state
1411+
// This implementation assumes normally-open (NO) float switches with INPUT_PULLUP:
1412+
// - Default state is HIGH (switch open, no fluid)
1413+
// - When fluid is present, switch closes and pulls pin LOW
1414+
// For normally-closed (NC) switches, invert the trigger condition in config
13811415
int pin = (cfg.primaryPin >= 0 && cfg.primaryPin < 255) ? cfg.primaryPin : (2 + idx);
13821416
pinMode(pin, INPUT_PULLUP);
13831417
int level = digitalRead(pin);
1384-
return level == HIGH ? cfg.maxValue : 0.0f;
1418+
// Return activated value when switch is closed (LOW), not-activated when open (HIGH)
1419+
return (level == LOW) ? DIGITAL_SENSOR_ACTIVATED_VALUE : DIGITAL_SENSOR_NOT_ACTIVATED_VALUE;
13851420
}
13861421
case SENSOR_ANALOG: {
13871422
// Use explicit bounds check for channel (A0602 has channels 0-7)
@@ -1520,7 +1555,71 @@ static void evaluateAlarms(uint8_t idx) {
15201555
return;
15211556
}
15221557

1523-
// Apply hysteresis: use different thresholds for triggering vs clearing
1558+
// Handle digital sensors (float switches) differently
1559+
if (cfg.sensorType == SENSOR_DIGITAL) {
1560+
// For digital sensors, currentInches is either DIGITAL_SENSOR_ACTIVATED_VALUE (1.0) or DIGITAL_SENSOR_NOT_ACTIVATED_VALUE (0.0)
1561+
bool isActivated = (state.currentInches > DIGITAL_SWITCH_THRESHOLD);
1562+
bool shouldAlarm = false;
1563+
bool triggerOnActivated = true; // Track what condition triggers the alarm
1564+
1565+
// Determine if we should alarm based on trigger configuration
1566+
if (cfg.digitalTrigger[0] != '\0') {
1567+
if (strcmp(cfg.digitalTrigger, "activated") == 0) {
1568+
shouldAlarm = isActivated; // Alarm when switch is activated
1569+
triggerOnActivated = true;
1570+
} else if (strcmp(cfg.digitalTrigger, "not_activated") == 0) {
1571+
shouldAlarm = !isActivated; // Alarm when switch is NOT activated
1572+
triggerOnActivated = false;
1573+
}
1574+
} else {
1575+
// Legacy behavior: use highAlarm/lowAlarm thresholds
1576+
// Only one of these should be configured for a digital sensor
1577+
// highAlarm = 1 means trigger when reading is 1.0 (switch activated)
1578+
// lowAlarm = 0 means trigger when reading is 0.0 (switch not activated)
1579+
bool hasHighAlarm = (cfg.highAlarmThreshold >= DIGITAL_SENSOR_ACTIVATED_VALUE);
1580+
bool hasLowAlarm = (cfg.lowAlarmThreshold == DIGITAL_SENSOR_NOT_ACTIVATED_VALUE);
1581+
1582+
if (hasHighAlarm && !hasLowAlarm) {
1583+
shouldAlarm = isActivated;
1584+
triggerOnActivated = true;
1585+
} else if (hasLowAlarm && !hasHighAlarm) {
1586+
shouldAlarm = !isActivated;
1587+
triggerOnActivated = false;
1588+
} else if (hasHighAlarm) {
1589+
// Default to high alarm behavior if both are set
1590+
shouldAlarm = isActivated;
1591+
triggerOnActivated = true;
1592+
}
1593+
}
1594+
1595+
// Handle alarm state with debouncing
1596+
if (shouldAlarm && !state.highAlarmLatched) {
1597+
state.highAlarmDebounceCount++;
1598+
state.clearDebounceCount = 0;
1599+
if (state.highAlarmDebounceCount >= ALARM_DEBOUNCE_COUNT) {
1600+
state.highAlarmLatched = true;
1601+
state.highAlarmDebounceCount = 0;
1602+
// Send alarm with descriptive type based on configured trigger condition
1603+
const char *alarmType = triggerOnActivated ? "triggered" : "not_triggered";
1604+
sendAlarm(idx, alarmType, state.currentInches);
1605+
}
1606+
} else if (!shouldAlarm && state.highAlarmLatched) {
1607+
state.clearDebounceCount++;
1608+
state.highAlarmDebounceCount = 0;
1609+
if (state.clearDebounceCount >= ALARM_DEBOUNCE_COUNT) {
1610+
state.highAlarmLatched = false;
1611+
state.clearDebounceCount = 0;
1612+
sendAlarm(idx, "clear", state.currentInches);
1613+
}
1614+
} else if (!shouldAlarm) {
1615+
state.highAlarmDebounceCount = 0;
1616+
} else {
1617+
state.clearDebounceCount = 0;
1618+
}
1619+
return; // Skip the standard analog threshold evaluation
1620+
}
1621+
1622+
// Standard analog/current loop sensor alarm evaluation with hysteresis
15241623
float highTrigger = cfg.highAlarmThreshold;
15251624
float highClear = cfg.highAlarmThreshold - cfg.hysteresisValue;
15261625
float lowTrigger = cfg.lowAlarmThreshold;
@@ -1598,9 +1697,19 @@ static void sendTelemetry(uint8_t idx, const char *reason, bool syncNow) {
15981697
doc["label"] = cfg.name;
15991698
doc["tank"] = cfg.tankNumber;
16001699
doc["id"] = String(cfg.id);
1601-
doc["maxValue"] = cfg.maxValue;
1602-
doc["levelInches"] = state.currentInches;
1603-
doc["percent"] = (cfg.maxValue > 0.1f) ? (state.currentInches / cfg.maxValue * 100.0f) : 0.0f;
1700+
1701+
// Handle digital sensors differently in telemetry
1702+
if (cfg.sensorType == SENSOR_DIGITAL) {
1703+
doc["sensorType"] = "digital";
1704+
bool activated = (state.currentInches > DIGITAL_SWITCH_THRESHOLD);
1705+
doc["activated"] = activated; // Boolean state: true = switch activated
1706+
doc["levelInches"] = state.currentInches; // 1.0 or 0.0
1707+
doc["percent"] = activated ? 100.0f : 0.0f; // 100% when activated, 0% when not
1708+
} else {
1709+
doc["maxValue"] = cfg.maxValue;
1710+
doc["levelInches"] = state.currentInches;
1711+
doc["percent"] = (cfg.maxValue > 0.1f) ? (state.currentInches / cfg.maxValue * 100.0f) : 0.0f;
1712+
}
16041713
doc["reason"] = reason;
16051714
doc["time"] = currentEpoch();
16061715

0 commit comments

Comments
 (0)