Skip to content

Commit c95cdd8

Browse files
Copilotdorkmo
andcommitted
Improve calibration system validation and R-squared calculation
Co-authored-by: dorkmo <[email protected]>
1 parent 80ff31c commit c95cdd8

File tree

1 file changed

+34
-9
lines changed

1 file changed

+34
-9
lines changed

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

Lines changed: 34 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -2603,8 +2603,8 @@ static const char CALIBRATION_HTML[] PROGMEM = R"HTML(
26032603
</div>
26042604
</label>
26052605
<label class="field">
2606-
<span>Current Sensor Reading (mA)</span>
2607-
<input type="number" id="sensorReading" step="0.01" min="4" max="20" placeholder="e.g., 12.5">
2606+
<span>Current Sensor Reading (mA) <em style="font-weight: normal; color: var(--muted);">- Required for calibration</em></span>
2607+
<input type="number" id="sensorReading" step="0.01" min="4" max="20" placeholder="e.g., 12.5 (4-20mA range)">
26082608
</label>
26092609
<label class="field">
26102610
<span>Reading Timestamp</span>
@@ -2621,7 +2621,9 @@ static const char CALIBRATION_HTML[] PROGMEM = R"HTML(
26212621
</div>
26222622
</form>
26232623
<div class="info-box">
2624-
<strong>How it works:</strong> Each calibration reading pairs a verified tank level (measured with a stick gauge, sight glass, or other method) with the current sensor reading. With at least 2 data points at different levels, the system calculates a linear regression to determine the actual relationship between sensor output and tank level. This learned calibration replaces the theoretical maxValue-based calculation.
2624+
<strong>How it works:</strong> Each calibration reading pairs a verified tank level (measured with a stick gauge, sight glass, or other method) with the current 4-20mA sensor reading. With at least 2 data points at different levels, the system calculates a linear regression to determine the actual relationship between sensor output and tank level. This learned calibration replaces the theoretical maxValue-based calculation.
2625+
<br><br>
2626+
<strong>Important:</strong> The sensor reading (mA) is required for calibration learning. If not provided, the entry will be logged but won't contribute to the calibration calculation. You can read the mA value from your 4-20mA loop meter or the sensor's display.
26252627
</div>
26262628
</div>
26272629
@@ -2911,13 +2913,22 @@ static const char CALIBRATION_HTML[] PROGMEM = R"HTML(
29112913
const tankInfo = tanks.find(t => t.client === log.clientUid && t.tank === log.tankNumber);
29122914
const tankName = tankInfo ? `${tankInfo.site} - ${tankInfo.label || 'Tank ' + log.tankNumber}` : `Tank ${log.tankNumber}`;
29132915
2916+
// Check if sensor reading is valid for calibration (4-20mA range)
2917+
const isValidReading = log.sensorReading >= 4 && log.sensorReading <= 20;
2918+
const sensorDisplay = log.sensorReading >= 4 && log.sensorReading <= 20
2919+
? log.sensorReading.toFixed(2) + ' mA'
2920+
: (log.sensorReading ? `${log.sensorReading.toFixed(2)} mA ⚠️` : '-- ⚠️');
2921+
29142922
tr.innerHTML = `
29152923
<td>${formatEpoch(log.timestamp)}</td>
29162924
<td>${tankName}</td>
2917-
<td>${log.sensorReading ? log.sensorReading.toFixed(2) + ' mA' : '--'}</td>
2925+
<td title="${isValidReading ? '' : 'Not used for calibration (outside 4-20mA range)'}">${sensorDisplay}</td>
29182926
<td>${formatLevel(log.verifiedLevelInches)}</td>
29192927
<td>${log.notes || '--'}</td>
29202928
`;
2929+
if (!isValidReading) {
2930+
tr.style.opacity = '0.6';
2931+
}
29212932
tbody.appendChild(tr);
29222933
});
29232934
}
@@ -7306,13 +7317,19 @@ static void recalculateCalibration(TankCalibration *cal) {
73067317
cal->learnedOffset = (sumY - cal->learnedSlope * sumX) / n;
73077318

73087319
// Calculate R-squared (coefficient of determination)
7320+
// R² = 1 - (SS_residual / SS_total)
7321+
// For linear regression: SS_regression = slope² * (sumX² - n * meanX²)
7322+
// And: SS_residual = SS_total - SS_regression
7323+
float meanX = sumX / n;
73097324
float meanY = sumY / n;
73107325
float ssTotal = sumY2 - n * meanY * meanY;
7326+
float ssX = sumX2 - n * meanX * meanX;
73117327

7312-
if (ssTotal > 0.0001f) {
7313-
// ssResidual = sum((y_i - (slope * x_i + offset))^2)
7314-
// For simplicity, we estimate it from the regression formula
7315-
float ssResidual = sumY2 - cal->learnedOffset * sumY - cal->learnedSlope * sumXY;
7328+
if (ssTotal > 0.0001f && ssX > 0.0001f) {
7329+
// SS_regression = slope * (sumXY - n * meanX * meanY)
7330+
float ssCovXY = sumXY - n * meanX * meanY;
7331+
float ssRegression = cal->learnedSlope * ssCovXY;
7332+
float ssResidual = ssTotal - ssRegression;
73167333
cal->rSquared = 1.0f - (ssResidual / ssTotal);
73177334
if (cal->rSquared < 0.0f) cal->rSquared = 0.0f;
73187335
if (cal->rSquared > 1.0f) cal->rSquared = 1.0f;
@@ -7713,6 +7730,9 @@ static void handleCalibrationPost(EthernetClient &client, const String &body) {
77137730

77147731
const char *notes = doc["notes"].as<const char *>();
77157732

7733+
// Validate sensor reading - warn if not in valid range
7734+
bool sensorReadingValid = (sensorReading >= 4.0f && sensorReading <= 20.0f);
7735+
77167736
// Save the calibration entry
77177737
saveCalibrationEntry(clientUid, tankNumber, timestamp, sensorReading, verifiedLevelInches, notes);
77187738

@@ -7726,5 +7746,10 @@ static void handleCalibrationPost(EthernetClient &client, const String &body) {
77267746
Serial.print(sensorReading, 2);
77277747
Serial.println(F(" mA"));
77287748

7729-
respondStatus(client, 200, F("Calibration entry saved"));
7749+
if (!sensorReadingValid) {
7750+
Serial.println(F("Warning: Sensor reading not in valid 4-20mA range, entry logged but won't be used for regression"));
7751+
respondStatus(client, 200, F("Calibration entry saved (note: sensor reading outside 4-20mA range won't be used for calibration)"));
7752+
} else {
7753+
respondStatus(client, 200, F("Calibration entry saved"));
7754+
}
77307755
}

0 commit comments

Comments
 (0)