pulse-monitor/types.go

252 lines
7.3 KiB
Go

package main
import (
"fmt"
"time"
"gocv.io/x/gocv"
)
// Global DEBUG flag for detection.go (set by pulseox-monitor.go)
var DEBUG = false
// ProcessingStatus represents the outcome of processing a frame
type ProcessingStatus int
const (
StatusSuccess ProcessingStatus = iota
StatusCorrupted
StatusLowConfidence
StatusNoChange
StatusUnstable
)
// Reading holds SpO2 and HR values with confidence scores
type Reading struct {
SpO2 int
SpO2LeftDigit int
SpO2LeftConf float64
SpO2RightDigit int
SpO2RightConf float64
HR int
HRLeftDigit int
HRLeftConf float64
HRRightDigit int
HRRightConf float64
Timestamp string
FrameNum int
}
// AvgConfidence returns average confidence for SpO2 and HR
func (r *Reading) AvgConfidence() (spo2Avg, hrAvg float64) {
spo2Avg = (r.SpO2LeftConf + r.SpO2RightConf) / 2.0
hrAvg = (r.HRLeftConf + r.HRRightConf) / 2.0
return
}
// IsCorrupted checks if any digit is marked as invalid (-1)
func (r *Reading) IsCorrupted() bool {
return r.SpO2LeftDigit == -1 || r.SpO2RightDigit == -1 ||
r.HRLeftDigit == -1 || r.HRRightDigit == -1
}
// GetCorruptionDetails returns a description of which digit(s) are corrupted
func (r *Reading) GetCorruptionDetails() string {
corrupted := []string{}
if r.SpO2LeftDigit == -1 {
corrupted = append(corrupted, "SpO2 digit 2")
}
if r.SpO2RightDigit == -1 {
corrupted = append(corrupted, "SpO2 digit 3")
}
if r.HRLeftDigit == -1 {
corrupted = append(corrupted, "HR digit 2")
}
if r.HRRightDigit == -1 {
corrupted = append(corrupted, "HR digit 3")
}
if len(corrupted) == 0 {
return "unknown"
}
result := ""
for i, c := range corrupted {
if i > 0 {
result += ", "
}
result += c
}
return result
}
// ProcessingResult contains the outcome of processing a single frame
type ProcessingResult struct {
Status ProcessingStatus
Reading Reading
ShouldPost bool
UnstableReason string
}
// ProcessingState tracks all state across frame processing
type ProcessingState struct {
// Detection state (sentinels: rotation=9999.0, scaleFactor=0.0)
LockedRotation float64 // Rotation angle in degrees (9999 = not detected)
LockedScale float64 // Scale factor (0 = not detected)
Layout *ScreenLayout // SpO2 and HR display areas
LayoutValid bool // True when detection has succeeded
// Change detection
LastPosted Reading
// Stability tracking
LastReading Reading
HasLastReading bool
PendingReading *Reading
BaselineReading Reading
// Retry state
InRetry bool
RetryCount int
MaxRetries int
ConsecutiveFailures int // Track consecutive layout/processing failures
LowConfStage int // Low confidence retry stage: 0=none, 1=fresh frame, 2=redetect, 3=10s, 4=30s, 5+=60s
LowConfWaitUntil time.Time // When to resume after low confidence wait
DetectFailStage int // Detection failure stage: 0=none, 1-3=immediate, 4=10s, 5=30s, 6+=60s
DetectWaitUntil time.Time // When to resume after detection wait
// Statistics
FrameCount int
ProcessedCount int
SuccessCount int
FailureCount int
LowConfidenceCount int
TimestampCheckCounter int // Check timestamp every 10 processed frames
// Review entries
ReviewEntries []ReviewEntry
// Timing tracking
TimingFrameCount int // Count frames for periodic header
// Previous display images for duplicate detection
PrevSpO2Img gocv.Mat
PrevHRImg gocv.Mat
PrevSpO2Result int
PrevHRResult int
PrevSpO2Conf float64
PrevHRConf float64
HasPrevImages bool
// Notification flood protection
NotifiedMissingTemplates map[int]bool // Templates we've already warned about
NotifiedDetectionFailure bool // Already notified about detection failure
DetectionFailStart time.Time // When detection failures started
// OHLC ticker display
TickStart time.Time // Start of current tick period
TickDuration time.Duration // How long each tick lasts (default 5 min)
SpO2Open int // First SpO2 in tick
SpO2High int // Highest SpO2 in tick
SpO2Low int // Lowest SpO2 in tick
SpO2Close int // Current/last SpO2
HROpen int // First HR in tick
HRHigh int // Highest HR in tick
HRLow int // Lowest HR in tick
HRClose int // Current/last HR
TickHasData bool // Whether we have any data in current tick
}
// NewProcessingState creates initial state
func NewProcessingState() *ProcessingState {
return &ProcessingState{
LockedRotation: 9999.0, // Sentinel: not detected
LockedScale: 0.0, // Sentinel: not detected
LayoutValid: false,
MaxRetries: 1,
LastPosted: Reading{SpO2: -1, HR: -1},
LastReading: Reading{SpO2: -1, HR: -1},
BaselineReading: Reading{SpO2: -1, HR: -1},
NotifiedMissingTemplates: make(map[int]bool),
TickDuration: 5 * time.Minute,
}
}
// UpdateOHLC updates the OHLC ticker values and prints the ticker line
func (s *ProcessingState) UpdateOHLC(spo2, hr int) {
now := time.Now()
// Check if we need to start a new tick
if s.TickStart.IsZero() || now.Sub(s.TickStart) >= s.TickDuration {
// Print newline if we had data in previous tick
if s.TickHasData {
fmt.Println() // Move to new line
}
// Start new tick
s.TickStart = now
s.SpO2Open, s.SpO2High, s.SpO2Low, s.SpO2Close = spo2, spo2, spo2, spo2
s.HROpen, s.HRHigh, s.HRLow, s.HRClose = hr, hr, hr, hr
s.TickHasData = true
} else {
// Update existing tick
s.SpO2Close = spo2
s.HRClose = hr
if spo2 > s.SpO2High {
s.SpO2High = spo2
}
if spo2 < s.SpO2Low {
s.SpO2Low = spo2
}
if hr > s.HRHigh {
s.HRHigh = hr
}
if hr < s.HRLow {
s.HRLow = hr
}
}
// Print ticker line (overwrite current line)
timestamp := now.Format("15:04:05")
fmt.Printf("\r%s SpO2: %d/%d/%d/%d HR: %d/%d/%d/%d ",
timestamp,
s.SpO2Open, s.SpO2High, s.SpO2Low, s.SpO2Close,
s.HROpen, s.HRHigh, s.HRLow, s.HRClose)
}
// ResetDetection resets detection state to trigger re-detection
func (s *ProcessingState) ResetDetection() {
s.LockedRotation = 9999.0
s.LockedScale = 0.0
s.LayoutValid = false
s.Layout = nil
}
// NeedsDetection returns true if detection needs to run
func (s *ProcessingState) NeedsDetection() bool {
return s.LockedRotation == 9999.0 || s.LockedScale == 0.0 || !s.LayoutValid
}
// Action represents what to do next
type Action int
const (
ActionContinue Action = iota // Continue to next frame
ActionRetry // Retry with next frame + layout re-detection
ActionPost // Post reading to Home Assistant
ActionHold // Hold reading for validation
ActionDiscard // Discard reading
)
// TimingData holds timing measurements for a single frame
type TimingData struct {
FrameNum int
Acquire int64 // Frame acquisition (ms)
Threshold int64 // Grayscale + threshold (ms)
Preprocess int64 // Crop + rotate (ms)
Scale int64 // Frame scaling (ms)
OCR_SpO2 int64 // SpO2 recognition (ms)
OCR_HR int64 // HR recognition (ms)
Validation int64 // Validation checks (ms)
FileIO int64 // Saving review images (ms)
HASS int64 // Home Assistant POST (ms)
Total int64 // Total processing time (ms)
}