Skip to content

Commit 97b0541

Browse files
authored
Merge pull request #112 from SenaxInc/copilot/make-float-switch-selectable
Add NO/NC mode selection for float switches
2 parents ba120f5 + 635114d commit 97b0541

File tree

3 files changed

+73
-8
lines changed

3 files changed

+73
-8
lines changed

TankAlarm-112025-Client-BluesOpta/README.md

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -83,12 +83,24 @@ The client creates a default configuration on first boot. You can update configu
8383
- **High Alarm**: Threshold in inches for high level alert
8484
- **Low Alarm**: Threshold in inches for low level alert
8585
- **Analog Pin**: Arduino Opta analog input (A0-A7, I1-I8)
86-
- **Sensor Type**: "voltage" (0-10V) or "current" (4-20mA)
86+
- **Sensor Type**: "voltage" (0-10V), "current" (4-20mA), or "digital" (float switch)
8787
- **Min Value**: Minimum sensor value (e.g., 0.0V or 4.0mA)
8888
- **Max Value**: Maximum sensor value (e.g., 10.0V or 20.0mA)
8989
- **Min Inches**: Tank level in inches at minimum sensor value
9090
- **Max Inches**: Tank level in inches at maximum sensor value
9191

92+
### Float Switch Configuration (Digital Sensors)
93+
Float switches can be configured as either normally-open (NO) or normally-closed (NC):
94+
95+
- **Digital Switch Mode**: "NO" (normally-open) or "NC" (normally-closed)
96+
- **NO (Normally-Open)**: Switch is open by default, closes when fluid reaches the switch position
97+
- **NC (Normally-Closed)**: Switch is closed by default, opens when fluid reaches the switch position
98+
- **Digital Trigger**: When to trigger the alarm
99+
- "activated": Alarm when switch is activated (fluid present)
100+
- "not_activated": Alarm when switch is not activated (fluid absent)
101+
102+
**Wiring Note**: For both NO and NC float switches, connect the switch between the digital input pin and GND. The Arduino uses an internal pull-up resistor, and the software interprets the signal based on your configured switch mode. The wiring is the same for both modes - only the software interpretation changes.
103+
92104
## Operation
93105

94106
### Normal Operation

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

Lines changed: 41 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -250,6 +250,7 @@ struct TankConfig {
250250
RelayMode relayMode; // How long relay stays on (momentary, until_clear, manual_reset)
251251
// Digital sensor (float switch) specific settings
252252
char digitalTrigger[16]; // 'activated' or 'not_activated' - when to trigger alarm for digital sensors
253+
char digitalSwitchMode[4]; // 'NO' for normally-open, 'NC' for normally-closed (default: NO)
253254
};
254255

255256
struct ClientConfig {
@@ -631,6 +632,7 @@ static void createDefaultConfig(ClientConfig &cfg) {
631632
cfg.tanks[0].relayTrigger = RELAY_TRIGGER_ANY; // Default: trigger on any alarm
632633
cfg.tanks[0].relayMode = RELAY_MODE_MOMENTARY; // Default: momentary 30 min activation
633634
cfg.tanks[0].digitalTrigger[0] = '\0'; // Not a digital sensor by default
635+
strlcpy(cfg.tanks[0].digitalSwitchMode, "NO", sizeof(cfg.tanks[0].digitalSwitchMode)); // Default: normally-open
634636
}
635637

636638
static bool loadConfigFromFlash(ClientConfig &cfg) {
@@ -759,6 +761,13 @@ static bool loadConfigFromFlash(ClientConfig &cfg) {
759761
// Load digital sensor trigger state (for float switches)
760762
const char *digitalTriggerStr = t["digitalTrigger"].as<const char *>();
761763
strlcpy(cfg.tanks[i].digitalTrigger, digitalTriggerStr ? digitalTriggerStr : "", sizeof(cfg.tanks[i].digitalTrigger));
764+
// Load digital switch mode (NO = normally-open, NC = normally-closed)
765+
const char *digitalSwitchModeStr = t["digitalSwitchMode"].as<const char *>();
766+
if (digitalSwitchModeStr && strcmp(digitalSwitchModeStr, "NC") == 0) {
767+
strlcpy(cfg.tanks[i].digitalSwitchMode, "NC", sizeof(cfg.tanks[i].digitalSwitchMode));
768+
} else {
769+
strlcpy(cfg.tanks[i].digitalSwitchMode, "NO", sizeof(cfg.tanks[i].digitalSwitchMode)); // Default: normally-open
770+
}
762771
}
763772

764773
return true;
@@ -827,6 +836,8 @@ static bool saveConfigToFlash(const ClientConfig &cfg) {
827836
if (cfg.tanks[i].digitalTrigger[0] != '\0') {
828837
t["digitalTrigger"] = cfg.tanks[i].digitalTrigger;
829838
}
839+
// Save digital switch mode (NO/NC)
840+
t["digitalSwitchMode"] = cfg.tanks[i].digitalSwitchMode;
830841
}
831842

832843
#if defined(ARDUINO_OPTA) || defined(ARDUINO_ARCH_MBED)
@@ -1260,6 +1271,15 @@ static void applyConfigUpdate(const JsonDocument &doc) {
12601271
digitalTriggerStr ? digitalTriggerStr : "",
12611272
sizeof(gConfig.tanks[i].digitalTrigger));
12621273
}
1274+
// Handle digital switch mode (NO/NC) for float switches
1275+
if (t.containsKey("digitalSwitchMode")) {
1276+
const char *digitalSwitchModeStr = t["digitalSwitchMode"].as<const char *>();
1277+
if (digitalSwitchModeStr && strcmp(digitalSwitchModeStr, "NC") == 0) {
1278+
strlcpy(gConfig.tanks[i].digitalSwitchMode, "NC", sizeof(gConfig.tanks[i].digitalSwitchMode));
1279+
} else {
1280+
strlcpy(gConfig.tanks[i].digitalSwitchMode, "NO", sizeof(gConfig.tanks[i].digitalSwitchMode));
1281+
}
1282+
}
12631283
}
12641284
}
12651285

@@ -1408,15 +1428,30 @@ static float readTankSensor(uint8_t idx) {
14081428
switch (cfg.sensorType) {
14091429
case SENSOR_DIGITAL: {
14101430
// 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
1431+
// The digitalSwitchMode field controls how the hardware is interpreted:
1432+
// - "NO" (normally-open): Switch is open by default, closes when fluid is present
1433+
// - With INPUT_PULLUP: HIGH = switch open (no fluid), LOW = switch closed (fluid present)
1434+
// - activated when pin is LOW
1435+
// - "NC" (normally-closed): Switch is closed by default, opens when fluid is present
1436+
// - With INPUT_PULLUP: LOW = switch closed (no fluid), HIGH = switch open (fluid present)
1437+
// - activated when pin is HIGH
14151438
int pin = (cfg.primaryPin >= 0 && cfg.primaryPin < 255) ? cfg.primaryPin : (2 + idx);
14161439
pinMode(pin, INPUT_PULLUP);
14171440
int level = digitalRead(pin);
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;
1441+
1442+
// Determine if switch is configured as normally-closed
1443+
bool isNormallyClosed = (strcmp(cfg.digitalSwitchMode, "NC") == 0);
1444+
1445+
// For NO switches: LOW = activated (switch closed, fluid present)
1446+
// For NC switches: HIGH = activated (switch opened, fluid present)
1447+
bool isActivated;
1448+
if (isNormallyClosed) {
1449+
isActivated = (level == HIGH); // NC switch opens (goes HIGH) when activated
1450+
} else {
1451+
isActivated = (level == LOW); // NO switch closes (goes LOW) when activated
1452+
}
1453+
1454+
return isActivated ? DIGITAL_SENSOR_ACTIVATED_VALUE : DIGITAL_SENSOR_NOT_ACTIVATED_VALUE;
14201455
}
14211456
case SENSOR_ANALOG: {
14221457
// Use explicit bounds check for channel (A0602 has channels 0-7)

TankAlarm-112025-Server-BluesOpta/TankAlarm-112025-Server-BluesOpta.ino

Lines changed: 19 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -730,13 +730,20 @@ static const char CONFIG_GENERATOR_HTML[] PROGMEM = R"HTML(
730730
${optaPins.map(p => `<option value="${p.value}">${p.label}</option>`).join('')}
731731
</select>
732732
</label>
733+
<label class="field switch-mode-field" style="display: none;"><span>Switch Mode<span class="tooltip-icon" tabindex="0" data-tooltip="NO (Normally-Open): Switch is open by default, closes when fluid is present. NC (Normally-Closed): Switch is closed by default, opens when fluid is present. The wiring is the same - only the software interpretation changes.">?</span></span>
734+
<select class="switch-mode">
735+
<option value="NO">Normally-Open (NO)</option>
736+
<option value="NC">Normally-Closed (NC)</option>
737+
</select>
738+
</label>
733739
<label class="field pulses-per-rev-field" style="display: none;"><span>Pulses/Rev</span><input type="number" class="pulses-per-rev" value="1" min="1" max="255"></label>
734740
<label class="field height-field"><span><span class="height-label">Height (in)</span><span class="tooltip-icon height-tooltip" tabindex="0" data-tooltip="Maximum height or capacity of the tank in inches. Used to calculate fill percentage and set alarm thresholds relative to tank size.">?</span></span><input type="number" class="tank-height" value="120"></label>
735741
</div>
736742
737743
<!-- Digital sensor info box (shown only for float switches) -->
738744
<div class="digital-sensor-info" style="display: none; background: var(--chip); border: 1px solid var(--card-border); border-radius: 8px; padding: 12px; margin-top: 8px; font-size: 0.9rem; color: var(--muted);">
739-
<strong>Float Switch Mode:</strong> This sensor only detects whether fluid has reached the switch position. It does not measure actual fluid level. The alarm will trigger when the switch is activated (fluid present) or not activated (fluid absent).
745+
<strong>Float Switch Mode:</strong> This sensor only detects whether fluid has reached the switch position. It does not measure actual fluid level. The alarm will trigger when the switch is activated (fluid present) or not activated (fluid absent).<br><br>
746+
<strong>Wiring Note:</strong> For both NO and NC switches, connect the switch between the input pin and GND. The software uses an internal pull-up resistor and interprets the signal based on your selected switch mode.
740747
</div>
741748
742749
<button type="button" class="add-section-btn add-alarm-btn" onclick="toggleAlarmSection(${id})">+ Add Alarm</button>
@@ -995,13 +1002,16 @@ static const char CONFIG_GENERATOR_HTML[] PROGMEM = R"HTML(
9951002
const digitalAlarmGrid = card.querySelector('.digital-alarm-grid');
9961003
const alarmSectionTitle = card.querySelector('.alarm-section-title');
9971004
const pulsesPerRevField = card.querySelector('.pulses-per-rev-field');
1005+
const switchModeField = card.querySelector('.switch-mode-field');
9981006
9991007
// Digital Input (Float Switch) - type === 0
10001008
if (type === 0) {
10011009
// Hide height field (not applicable for float switches)
10021010
heightField.style.display = 'none';
10031011
// Show digital sensor info box
10041012
digitalInfoBox.style.display = 'block';
1013+
// Show switch mode selector for digital sensors
1014+
switchModeField.style.display = 'flex';
10051015
// Update alarm section for digital sensors
10061016
alarmThresholdsGrid.style.display = 'none';
10071017
digitalAlarmGrid.style.display = 'grid';
@@ -1012,6 +1022,7 @@ static const char CONFIG_GENERATOR_HTML[] PROGMEM = R"HTML(
10121022
heightLabel.textContent = 'Max RPM';
10131023
heightTooltip.setAttribute('data-tooltip', 'Maximum expected RPM value. Used for alarm threshold reference.');
10141024
digitalInfoBox.style.display = 'none';
1025+
switchModeField.style.display = 'none';
10151026
alarmThresholdsGrid.style.display = 'grid';
10161027
digitalAlarmGrid.style.display = 'none';
10171028
alarmSectionTitle.textContent = 'Alarm Thresholds';
@@ -1022,6 +1033,7 @@ static const char CONFIG_GENERATOR_HTML[] PROGMEM = R"HTML(
10221033
heightLabel.textContent = 'Height (in)';
10231034
heightTooltip.setAttribute('data-tooltip', 'Maximum height or capacity of the tank in inches. Used to calculate fill percentage and set alarm thresholds relative to tank size.');
10241035
digitalInfoBox.style.display = 'none';
1036+
switchModeField.style.display = 'none';
10251037
alarmThresholdsGrid.style.display = 'grid';
10261038
digitalAlarmGrid.style.display = 'none';
10271039
alarmSectionTitle.textContent = 'Alarm Thresholds';
@@ -1087,6 +1099,7 @@ static const char CONFIG_GENERATOR_HTML[] PROGMEM = R"HTML(
10871099
10881100
const sensor = sensorKeyFromValue(type);
10891101
const pulsesPerRev = Math.max(1, Math.min(255, parseInt(card.querySelector('.pulses-per-rev').value) || 1));
1102+
const switchMode = card.querySelector('.switch-mode').value; // 'NO' or 'NC'
10901103
10911104
// Check if alarm section is enabled
10921105
const alarmSectionVisible = card.querySelector('.alarm-section').classList.contains('visible');
@@ -1110,6 +1123,11 @@ static const char CONFIG_GENERATOR_HTML[] PROGMEM = R"HTML(
11101123
upload: true
11111124
};
11121125
1126+
// Add switch mode for digital sensors (float switches)
1127+
if (sensor === 'digital') {
1128+
tank.digitalSwitchMode = switchMode; // 'NO' or 'NC'
1129+
}
1130+
11131131
// Handle alarms differently based on sensor type
11141132
if (alarmSectionVisible) {
11151133
if (sensor === 'digital') {

0 commit comments

Comments
 (0)