Skip to content

Add 4mA-20mA sensor type options (pressure vs ultrasonic)#114

Merged
dorkmo merged 6 commits intomasterfrom
copilot/add-4ma-20ma-sensor-options
Dec 4, 2025
Merged

Add 4mA-20mA sensor type options (pressure vs ultrasonic)#114
dorkmo merged 6 commits intomasterfrom
copilot/add-4ma-20ma-sensor-options

Conversation

Copy link
Contributor

Copilot AI commented Dec 3, 2025

  • Add CurrentLoopSensorType enum to distinguish between ultrasonic (top-mounted) and pressure (bottom-mounted) 4-20mA sensors
  • Add currentLoopType and sensorMountHeight fields to TankConfig struct
  • Update readTankSensor() to handle different sensor types with inverted/non-inverted mappings
  • Update config loading/saving to persist new fields
  • Update applyConfigUpdate() to handle new fields in remote config updates
  • Update printHardwareRequirements() to show sensor types being used
  • Update Config Generator UI in server to allow selecting sensor subtype and entering calibration data
  • Update documentation to explain the calibration requirements for each sensor type
  • Add upper bound clamping for pressure sensor readings
  • Add validation for non-negative sensorMountHeight in config loading
  • Add validation for non-negative sensorMountHeight in applyConfigUpdate
  • Use generic tooltip for current loop sensors before subtype is selected
  • Fix documentation example to correctly show maxValue calculation for pressure sensors
  • Add sensor native range fields (sensorRangeMin, sensorRangeMax, sensorRangeUnit) for 4-20mA sensors
  • Update UI with sensor range inputs and unit selector (PSI, bar, m, ft, in, cm)
  • Update documentation with sensor range examples for both pressure and ultrasonic sensors
Original prompt

This section details on the original issue you should resolve

<issue_title>4mA-20mA options</issue_title>
<issue_description>in 112025, for 4ma-20ma sensors, lets allow the selection between an ultrasonic level sensor (example: siemens sitrans lu240) that is mounted on the top of the tank, and a 0-5psi pressure sensor mounted near the bottom of the tank (example: Dwyer 626-06-CB-P1-E5-S1). How should these be calibrated? Should approximate mounting height be required when setting up client config? What other data might be needed?</issue_description>

Comments on the Issue (you are @copilot in this section)


💬 We'd love your input! Share your thoughts on Copilot coding agent in our 2 minute survey.

Copilot AI and others added 3 commits December 3, 2025 21:08
Co-authored-by: dorkmo <1923070+dorkmo@users.noreply.github.com>
Co-authored-by: dorkmo <1923070+dorkmo@users.noreply.github.com>
Co-authored-by: dorkmo <1923070+dorkmo@users.noreply.github.com>
Copilot AI changed the title [WIP] Add 4mA-20mA options for sensor selection Add 4mA-20mA sensor type options (pressure vs ultrasonic) Dec 3, 2025
Copilot AI requested a review from dorkmo December 3, 2025 21:14
@dorkmo dorkmo marked this pull request as ready for review December 4, 2025 14:26
Copilot AI review requested due to automatic review settings December 4, 2025 14:26
Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR adds support for distinguishing between two types of 4-20mA current loop sensors: bottom-mounted pressure sensors and top-mounted ultrasonic sensors. These sensor types have inverted signal mappings - pressure sensors read low (4mA) when empty and high (20mA) when full, while ultrasonic sensors read high (4mA) when full and low (20mA) when empty. The implementation includes new configuration fields, type-specific level calculation logic, UI enhancements for sensor selection, and comprehensive documentation.

Key Changes:

  • Added CurrentLoopSensorType enum and currentLoopType/sensorMountHeight fields to client configuration
  • Implemented inverted signal processing logic for ultrasonic vs pressure sensors in readTankSensor()
  • Enhanced server config generator with sensor type dropdown, mount height input, and contextual help boxes
  • Added calibration examples and detailed explanations to README

Reviewed changes

Copilot reviewed 3 out of 3 changed files in this pull request and generated 5 comments.

File Description
TankAlarm-112025-Client-BluesOpta/TankAlarm-112025-Client-BluesOpta.ino Adds CurrentLoopSensorType enum, config fields (currentLoopType, sensorMountHeight), type-specific level calculations with clamping, config loading/saving/applying logic, and hardware requirements reporting
TankAlarm-112025-Server-BluesOpta/TankAlarm-112025-Server-BluesOpta.ino Adds HTML UI for sensor type selection with dropdown and mount height input, JavaScript functions for dynamic field updates, contextual info boxes explaining each sensor type, and config export support
TankAlarm-112025-Client-BluesOpta/README.md Adds comprehensive documentation section for 4-20mA sensor configuration with example configurations for both pressure and ultrasonic sensors, calibration tips, and signal mapping explanations

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

// Add mount height offset (sensor is mounted above tank bottom)
levelInches = rawLevel + cfg.sensorMountHeight;
// Clamp to valid range (sensorMountHeight to maxValue + sensorMountHeight)
if (levelInches < cfg.sensorMountHeight) levelInches = cfg.sensorMountHeight;
Copy link

Copilot AI Dec 4, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The upper bound clamping for pressure sensors is missing. While the lower bound is clamped to sensorMountHeight, there's no upper bound check like if (levelInches > cfg.maxValue + cfg.sensorMountHeight) levelInches = cfg.maxValue + cfg.sensorMountHeight; to match the ultrasonic sensor's complete range validation.

Suggested change
if (levelInches < cfg.sensorMountHeight) levelInches = cfg.sensorMountHeight;
if (levelInches < cfg.sensorMountHeight) levelInches = cfg.sensorMountHeight;
if (levelInches > cfg.maxValue + cfg.sensorMountHeight) levelInches = cfg.maxValue + cfg.sensorMountHeight;

Copilot uses AI. Check for mistakes.
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@copilot would it help to have the min to max range of the sensor defined in the config settings? would this correspond to the 4mA - 20mA range?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, having an explicit min/max mA range in the config (like minMilliamps and maxMilliamps) could provide more flexibility for sensors that don't use the standard 4-20mA range (e.g., 0-20mA or 4-12mA custom ranges). This would also help with dead-band handling where the sensor's minimum/maximum outputs might not exactly hit 4mA or 20mA. For now, the implementation uses the standard 4-20mA range, but this could be added as a future enhancement if needed.

Added the upper bound clamping as suggested in commit 27ffcdf.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@copilot if the pressure sensor has a range from 0-5psi, or 0-2bar can we input that into the configuration. likewise, if the max range of the ultrasonic sensor is 10meters can we input that aswell.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Added sensor native range configuration fields in commit 08789b9. You can now specify:

  • sensorRangeMin / sensorRangeMax: The min/max values of the sensor's native range (e.g., 0-5 for 0-5 PSI, 0-10 for 0-10m)
  • sensorRangeUnit: The unit of measurement (PSI, bar, m, ft, in, cm)

The Config Generator UI now includes a "Sensor Range" field that shows min/max inputs plus a unit dropdown. When switching between pressure and ultrasonic sensors, it auto-populates sensible defaults (0-5 PSI for pressure, 0-10 m for ultrasonic).

- Configuration:
- `currentLoopType`: "pressure"
- `sensorMountHeight`: 2.0
- `maxValue`: 138.0 (or actual tank height if tank is smaller)
Copy link

Copilot AI Dec 4, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The documentation suggests setting maxValue to 138.0 inches for a 5 PSI sensor on a 120-inch tank, but the implementation adds sensorMountHeight to this value (line 1563 in the .ino file: levelInches = rawLevel + cfg.sensorMountHeight). This means the total reported level could reach 140 inches (138 + 2), which exceeds the actual tank height of 120 inches. The documentation should clarify that maxValue should be set to tankHeight - sensorMountHeight (118 inches in this example) to ensure the maximum reported level doesn't exceed the actual tank capacity.

Suggested change
- `maxValue`: 138.0 (or actual tank height if tank is smaller)
- `maxValue`: 118.0 # Set to tankHeight - sensorMountHeight (120 - 2 = 118 inches)
> **Note:** Set `maxValue` to the tank's usable height minus the sensor mount height. The implementation adds `sensorMountHeight` to the measured value, so setting `maxValue` to the full sensor range (e.g., 138.0) would cause the reported level to exceed the actual tank height.

Copilot uses AI. Check for mistakes.
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed the documentation in commit 27ffcdf. The example now correctly shows maxValue: 118.0 (tank height minus mount height) with a note explaining that maxValue represents the height of liquid above the sensor, not the total tank height.

} else if (type === 2) { // Current Loop (4-20mA)
heightField.style.display = 'flex';
heightLabel.textContent = 'Tank Height (in)';
heightTooltip.setAttribute('data-tooltip', 'Maximum height of liquid in the tank in inches. For ultrasonic sensors, this is the distance the sensor measures at full tank.');
Copy link

Copilot AI Dec 4, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The initial tooltip text 'Maximum height of liquid in the tank in inches. For ultrasonic sensors, this is the distance the sensor measures at full tank.' is set when current loop sensor type is selected, but it assumes an ultrasonic sensor. This tooltip gets updated correctly when updateCurrentLoopFields() is called at line 1071, but the initial text could be misleading if viewed before that function executes. Consider using a more generic initial tooltip like 'Maximum height of liquid in the tank in inches. Meaning varies based on sensor subtype.' or ensure updateCurrentLoopFields() is always called before the UI is displayed.

Suggested change
heightTooltip.setAttribute('data-tooltip', 'Maximum height of liquid in the tank in inches. For ultrasonic sensors, this is the distance the sensor measures at full tank.');
heightTooltip.setAttribute('data-tooltip', 'Maximum height of liquid in the tank in inches. Meaning varies based on sensor subtype.');

Copilot uses AI. Check for mistakes.
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in commit 27ffcdf. The initial tooltip now shows a generic message that the meaning varies based on sensor subtype, and updateCurrentLoopFields() updates it with the correct sensor-specific text.

cfg.tanks[i].currentLoopType = CURRENT_LOOP_PRESSURE; // Default: pressure sensor
}
// Load sensor mount height (for calibration)
cfg.tanks[i].sensorMountHeight = t["sensorMountHeight"].is<float>() ? t["sensorMountHeight"].as<float>() : 0.0f;
Copy link

Copilot AI Dec 4, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The sensorMountHeight value is loaded without validation. While the HTML form enforces min="0", there's no firmware-side validation to ensure the value is non-negative. Consider adding validation like cfg.tanks[i].sensorMountHeight = max(0.0f, t["sensorMountHeight"].as<float>()) or similar bounds checking to prevent potential issues if the configuration is modified directly or corrupted.

Suggested change
cfg.tanks[i].sensorMountHeight = t["sensorMountHeight"].is<float>() ? t["sensorMountHeight"].as<float>() : 0.0f;
cfg.tanks[i].sensorMountHeight = t["sensorMountHeight"].is<float>() ? fmaxf(0.0f, t["sensorMountHeight"].as<float>()) : 0.0f;

Copilot uses AI. Check for mistakes.
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Added fmaxf(0.0f, ...) validation in commit 27ffcdf to ensure sensorMountHeight is always non-negative.

}
// Handle sensor mount height (for calibration)
if (t.containsKey("sensorMountHeight")) {
gConfig.tanks[i].sensorMountHeight = t["sensorMountHeight"].as<float>();
Copy link

Copilot AI Dec 4, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Similar to the config loading in loadConfigFromFlash(), the sensorMountHeight value is applied without validation here. Consider adding bounds checking to ensure the value is non-negative, e.g., gConfig.tanks[i].sensorMountHeight = max(0.0f, t["sensorMountHeight"].as<float>()) to prevent issues from malformed configuration updates.

Suggested change
gConfig.tanks[i].sensorMountHeight = t["sensorMountHeight"].as<float>();
gConfig.tanks[i].sensorMountHeight = max(0.0f, t["sensorMountHeight"].as<float>());

Copilot uses AI. Check for mistakes.
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Added fmaxf(0.0f, ...) validation in commit 27ffcdf to ensure sensorMountHeight is always non-negative when applied from remote config updates.

…tion

Co-authored-by: dorkmo <1923070+dorkmo@users.noreply.github.com>
Co-authored-by: dorkmo <1923070+dorkmo@users.noreply.github.com>
@dorkmo
Copy link
Collaborator

dorkmo commented Dec 4, 2025

@copilot does the pressure sensor need to approximate inch of tank height per psi of sensor reading to calculate height or does it do it by other means? can this be updated or improved from the server? is there any way to track the accuracy of these values over time and improve the reading?

Copy link
Contributor Author

Copilot AI commented Dec 4, 2025

@copilot does the pressure sensor need to approximate inch of tank height per psi of sensor reading to calculate height or does it do it by other means? can this be updated or improved from the server...

The current implementation uses maxValue to define the inches-per-PSI relationship implicitly. You set maxValue to the liquid height (in inches) that corresponds to the sensor's maximum PSI reading. The formula is:

levelInches = linearMap(milliamps, 4mA, 20mA, 0, maxValue) + sensorMountHeight

For example, with a 0-5 PSI sensor and water (1 PSI ≈ 27.7 inches of water column):

  • maxValue = 118 means 5 PSI = 118 inches of liquid height above the sensor

Improving from Server:
Yes, all calibration parameters (maxValue, sensorMountHeight, sensorRangeMin, sensorRangeMax, sensorRangeUnit) can be updated remotely via config push from the server. This allows fine-tuning without reflashing the client.

Tracking Accuracy:
This would require additional features like:

  1. Reference level inputs (e.g., manual sight glass readings) to compare against sensor values
  2. Logging historical readings with timestamps for drift analysis
  3. Statistical tracking of calibration factor changes

This is a great enhancement idea for a future iteration - building a calibration learning system that could automatically suggest corrections based on operator-verified readings.

@dorkmo
Copy link
Collaborator

dorkmo commented Dec 4, 2025

@copilot does the pressure sensor need to approximate inch of tank height per psi of sensor reading to calculate height or does it do it by other means? can this be updated or improved from the server...

The current implementation uses maxValue to define the inches-per-PSI relationship implicitly. You set maxValue to the liquid height (in inches) that corresponds to the sensor's maximum PSI reading. The formula is:

levelInches = linearMap(milliamps, 4mA, 20mA, 0, maxValue) + sensorMountHeight

For example, with a 0-5 PSI sensor and water (1 PSI ≈ 27.7 inches of water column):

* `maxValue = 118` means 5 PSI = 118 inches of liquid height above the sensor

Improving from Server: Yes, all calibration parameters (maxValue, sensorMountHeight, sensorRangeMin, sensorRangeMax, sensorRangeUnit) can be updated remotely via config push from the server. This allows fine-tuning without reflashing the client.

Tracking Accuracy: This would require additional features like:

1. Reference level inputs (e.g., manual sight glass readings) to compare against sensor values

2. Logging historical readings with timestamps for drift analysis

3. Statistical tracking of calibration factor changes

This is a great enhancement idea for a future iteration - building a calibration learning system that could automatically suggest corrections based on operator-verified readings.

can we implement this? can manual readings be entered into a log with the height in feet and inches along with the time in minutes and hours. Is it possible to retire the maxvalue style of formula calculation?

@dorkmo
Copy link
Collaborator

dorkmo commented Dec 4, 2025

@copilot does the pressure sensor need to approximate inch of tank height per psi of sensor reading to calculate height or does it do it by other means? can this be updated or improved from the server...

The current implementation uses maxValue to define the inches-per-PSI relationship implicitly. You set maxValue to the liquid height (in inches) that corresponds to the sensor's maximum PSI reading. The formula is:

levelInches = linearMap(milliamps, 4mA, 20mA, 0, maxValue) + sensorMountHeight

For example, with a 0-5 PSI sensor and water (1 PSI ≈ 27.7 inches of water column):

  • maxValue = 118 means 5 PSI = 118 inches of liquid height above the sensor

Improving from Server:
Yes, all calibration parameters (maxValue, sensorMountHeight, sensorRangeMin, sensorRangeMax, sensorRangeUnit) can be updated remotely via config push from the server. This allows fine-tuning without reflashing the client.

Tracking Accuracy:
This would require additional features like:

  1. Reference level inputs (e.g., manual sight glass readings) to compare against sensor values
  2. Logging historical readings with timestamps for drift analysis
  3. Statistical tracking of calibration factor changes

This is a great enhancement idea for a future iteration - building a calibration learning system that could automatically suggest corrections based on operator-verified readings.

@copilot can we implement this? can manual readings be entered into a log with the height in feet and inches along with the time in minutes and hours. Is it possible to retire the maxvalue style of formula calculation?

Copy link
Contributor Author

Copilot AI commented Dec 4, 2025

@copilot does the pressure sensor need to approximate inch of tank height per psi of sensor reading to calculate height or does it do it by other means? can this be updated or improved from the...

Implementing a calibration learning system is a larger feature that warrants its own issue/PR. It would involve:

  1. Server UI: Form to enter manual readings (feet/inches + timestamp)
  2. Database: Store calibration log entries with sensor readings + verified levels
  3. Analysis: Calculate actual PSI-to-inches ratio from logged data points
  4. Auto-calibration: Generate suggested maxValue corrections from collected data

This is valuable but separate from the current 4-20mA sensor type selection feature. Should I create a new issue to track this calibration learning enhancement?

Regarding retiring maxValue: The linear mapping approach is actually the standard method for 4-20mA sensors - it just needs accurate calibration. The proposed logging system would help determine the correct maxValue empirically rather than replacing it with a different calculation method.

@dorkmo
Copy link
Collaborator

dorkmo commented Dec 4, 2025

@copilot does the pressure sensor need to approximate inch of tank height per psi of sensor reading to calculate height or does it do it by other means? can this be updated or improved from the...

Implementing a calibration learning system is a larger feature that warrants its own issue/PR. It would involve:

  1. Server UI: Form to enter manual readings (feet/inches + timestamp)
  2. Database: Store calibration log entries with sensor readings + verified levels
  3. Analysis: Calculate actual PSI-to-inches ratio from logged data points
  4. Auto-calibration: Generate suggested maxValue corrections from collected data

This is valuable but separate from the current 4-20mA sensor type selection feature. Should I create a new issue to track this calibration learning enhancement?

Regarding retiring maxValue: The linear mapping approach is actually the standard method for 4-20mA sensors - it just needs accurate calibration. The proposed logging system would help determine the correct maxValue empirically rather than replacing it with a different calculation method.

@copilot yes please create a new issue for this work

Copy link
Contributor Author

Copilot AI commented Dec 4, 2025

@copilot does the pressure sensor need to approximate inch of tank height per psi of sensor reading to calculate height or does it do it by other means? can this be updated or improv...

I'm unable to create GitHub issues directly due to environment limitations. You can create the issue with the following suggested content:

Title: Add calibration learning system for 4-20mA sensors

Description:
Implement a calibration learning system that allows operators to enter manual tank level readings and automatically improve sensor accuracy over time.

Features:

  1. Server UI form to enter manual readings (feet/inches + timestamp)
  2. Store calibration log entries with sensor readings + verified levels
  3. Calculate actual PSI-to-inches or distance-to-inches ratio from logged data points
  4. Generate suggested maxValue corrections from collected calibration data
  5. Display calibration drift analysis over time

Related: Follows work from #113 (4mA-20mA sensor type options)

@dorkmo dorkmo merged commit 983b382 into master Dec 4, 2025
7 checks passed
@dorkmo dorkmo deleted the copilot/add-4ma-20ma-sensor-options branch December 4, 2025 17:34
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4mA-20mA options

2 participants