145 lines
5.3 KiB
Go
145 lines
5.3 KiB
Go
package main
|
|
|
|
import (
|
|
"fmt"
|
|
"io"
|
|
"time"
|
|
)
|
|
|
|
// Validator functions handle the three-tier exception system
|
|
|
|
// validateCorruption checks for corrupted digits (Handler #1)
|
|
func validateCorruption(reading *Reading, logger io.Writer) (Action, string) {
|
|
if reading.IsCorrupted() {
|
|
logMessage(LogFile, Info, " [CORRUPTION] Invalid digit detected: SpO2(%d,%d) HR(%d,%d) - skipping frame",
|
|
reading.SpO2LeftDigit, reading.SpO2RightDigit,
|
|
reading.HRLeftDigit, reading.HRRightDigit)
|
|
return ActionContinue, ""
|
|
}
|
|
return ActionContinue, ""
|
|
}
|
|
|
|
// validateConfidence checks OCR confidence (Handler #2)
|
|
func validateConfidence(reading *Reading, state *ProcessingState, logger io.Writer) (Action, string) {
|
|
spo2Avg, hrAvg := reading.AvgConfidence()
|
|
|
|
// High confidence - pass through
|
|
if spo2Avg >= 85 && hrAvg >= 85 {
|
|
state.InRetry = false // Clear retry flag
|
|
return ActionContinue, ""
|
|
}
|
|
|
|
// Low confidence
|
|
if !state.InRetry {
|
|
// First low confidence - trigger retry
|
|
state.InRetry = true
|
|
logMessage(LogFile, Info, " [LOW CONFIDENCE] SpO2: %.1f%%, HR: %.1f%% - retrying with layout re-detection",
|
|
spo2Avg, hrAvg)
|
|
return ActionRetry, ""
|
|
}
|
|
|
|
// Second consecutive low confidence - give up
|
|
state.InRetry = false
|
|
logMessage(LogFile, Info, " [LOW CONFIDENCE] Still low after retry (SpO2: %.1f%%, HR: %.1f%%) - pausing",
|
|
spo2Avg, hrAvg)
|
|
return ActionDiscard, ""
|
|
}
|
|
|
|
// validateStability checks for large deltas with hindsight validation (Handler #3)
|
|
func validateStability(reading *Reading, state *ProcessingState, logger io.Writer) (Action, string) {
|
|
// First reading - just accept it
|
|
if !state.HasLastReading {
|
|
state.LastReading = *reading
|
|
state.HasLastReading = true
|
|
state.BaselineReading = *reading
|
|
return ActionPost, ""
|
|
}
|
|
|
|
// Calculate deltas from last reading
|
|
spo2Delta := abs(reading.SpO2 - state.LastReading.SpO2)
|
|
hrDelta := abs(reading.HR - state.LastReading.HR)
|
|
|
|
// Stable (Δ ≤ 3) - check if we have pending
|
|
if spo2Delta <= 3 && hrDelta <= 3 {
|
|
if state.PendingReading != nil {
|
|
// Check if current reading is closer to baseline or to pending
|
|
spo2ToBaseline := abs(reading.SpO2 - state.BaselineReading.SpO2)
|
|
hrToBaseline := abs(reading.HR - state.BaselineReading.HR)
|
|
spo2ToPending := abs(reading.SpO2 - state.PendingReading.SpO2)
|
|
hrToPending := abs(reading.HR - state.PendingReading.HR)
|
|
|
|
if spo2ToBaseline <= spo2ToPending && hrToBaseline <= hrToPending {
|
|
// Closer to baseline - pending was a glitch
|
|
logMessage(LogFile, Info, " [STABILITY] Discarding glitch: baseline(%d,%d) -> pending(%d,%d) -> current(%d,%d)",
|
|
state.BaselineReading.SpO2, state.BaselineReading.HR,
|
|
state.PendingReading.SpO2, state.PendingReading.HR,
|
|
reading.SpO2, reading.HR)
|
|
state.PendingReading = nil
|
|
} else {
|
|
// Closer to pending - real trend, post pending first
|
|
logMessage(LogFile, Info, " [STABILITY] Confirming trend: baseline(%d,%d) -> pending(%d,%d) -> current(%d,%d)",
|
|
state.BaselineReading.SpO2, state.BaselineReading.HR,
|
|
state.PendingReading.SpO2, state.PendingReading.HR,
|
|
reading.SpO2, reading.HR)
|
|
// Will be handled by caller to post pending first
|
|
state.PendingReading = nil
|
|
}
|
|
}
|
|
|
|
// Accept current reading
|
|
state.LastReading = *reading
|
|
state.BaselineReading = *reading
|
|
return ActionPost, ""
|
|
}
|
|
|
|
// Large delta (Δ > 3) - check if we have pending
|
|
if state.PendingReading != nil {
|
|
// Check direction vs pending
|
|
spo2Direction := reading.SpO2 - state.PendingReading.SpO2
|
|
hrDirection := reading.HR - state.PendingReading.HR
|
|
pendingSpo2Diff := state.PendingReading.SpO2 - state.LastReading.SpO2
|
|
pendingHrDiff := state.PendingReading.HR - state.LastReading.HR
|
|
|
|
// Same direction if signs match
|
|
spo2SameDir := (spo2Direction > 0 && pendingSpo2Diff > 0) || (spo2Direction < 0 && pendingSpo2Diff < 0)
|
|
hrSameDir := (hrDirection > 0 && pendingHrDiff > 0) || (hrDirection < 0 && pendingHrDiff < 0)
|
|
|
|
if spo2SameDir && hrSameDir {
|
|
// Trend confirmed - post pending, then current becomes new pending
|
|
logMessage(LogFile, Info, " [STABILITY] Trend confirmed: %d->%d->%d (SpO2), %d->%d->%d (HR)",
|
|
state.LastReading.SpO2, state.PendingReading.SpO2, reading.SpO2,
|
|
state.LastReading.HR, state.PendingReading.HR, reading.HR)
|
|
// Post pending (caller handles), then hold current
|
|
oldPending := state.PendingReading
|
|
state.PendingReading = reading
|
|
state.LastReading = *reading
|
|
// Return special signal that pending should be posted
|
|
_ = oldPending // Will be handled by caller
|
|
return ActionHold, ""
|
|
} else {
|
|
// Opposite directions - discard pending as glitch
|
|
logMessage(LogFile, Info, " [STABILITY] Direction mismatch: %d->%d->%d (SpO2), %d->%d->%d (HR)",
|
|
state.LastReading.SpO2, state.PendingReading.SpO2, reading.SpO2,
|
|
state.LastReading.HR, state.PendingReading.HR, reading.HR)
|
|
state.PendingReading = nil
|
|
state.LastReading = *reading
|
|
return ActionPost, ""
|
|
}
|
|
}
|
|
|
|
// No pending yet - hold this unstable reading
|
|
logMessage(LogFile, Info, " [STABILITY] Holding unstable reading: SpO2 Δ%d, HR Δ%d", spo2Delta, hrDelta)
|
|
state.BaselineReading = state.LastReading
|
|
state.PendingReading = reading
|
|
state.LastReading = *reading
|
|
reason := fmt.Sprintf("[%s] Unstable (SpO2 Δ%d, HR Δ%d)", time.Now().Format("15:04:05.000"), spo2Delta, hrDelta)
|
|
return ActionHold, reason
|
|
}
|
|
|
|
func abs(x int) int {
|
|
if x < 0 {
|
|
return -x
|
|
}
|
|
return x
|
|
}
|