From 0c5c7bcb5f21d0990c6cb4702ba087623313b233 Mon Sep 17 00:00:00 2001 From: Siwat Sirichai Date: Sat, 21 Jun 2025 13:29:01 +0700 Subject: [PATCH] warning system --- app/api/sensors/config/route.ts | 221 +++++++++++++-- app/api/sensors/route.ts | 26 +- app/api/sensors/zones/route.ts | 60 ++++ app/api/test-scenarios/route.ts | 197 ++++++++++++++ app/page.tsx | 34 ++- components/bed-pressure/AlarmDashboard.tsx | 256 ++++++++++++++++++ components/bed-pressure/BedPressureHeader.tsx | 67 ++++- stores/bedPressureStore.ts | 91 +++++-- types/sensor.ts | 24 ++ utils/sensorConfig.ts | 152 +++++++++++ 10 files changed, 1074 insertions(+), 54 deletions(-) create mode 100644 app/api/sensors/zones/route.ts create mode 100644 app/api/test-scenarios/route.ts create mode 100644 components/bed-pressure/AlarmDashboard.tsx create mode 100644 types/sensor.ts create mode 100644 utils/sensorConfig.ts diff --git a/app/api/sensors/config/route.ts b/app/api/sensors/config/route.ts index 86350a2..7727d13 100644 --- a/app/api/sensors/config/route.ts +++ b/app/api/sensors/config/route.ts @@ -1,36 +1,209 @@ import { NextResponse } from 'next/server'; +import { SensorConfig } from '@/types/sensor'; // Sensor configuration that matches the API route -const SENSOR_CONFIG = [ - // Head area - { id: "head-1", x: 45, y: 15, zone: "head", label: "Head Left", pin: 2 }, - { id: "head-2", x: 55, y: 15, zone: "head", label: "Head Right", pin: 3 }, +const SENSOR_CONFIG: SensorConfig[] = [ + // Head area - Higher thresholds due to critical nature, faster escalation + { + id: "head-1", + x: 45, + y: 15, + zone: "head", + label: "Head Left", + pin: 2, + warningThreshold: 3000, + alarmThreshold: 3500, + warningDelayMs: 30000, // 30 seconds - fast escalation for head + baseNoise: 200 + }, + { + id: "head-2", + x: 55, + y: 15, + zone: "head", + label: "Head Right", + pin: 3, + warningThreshold: 3000, + alarmThreshold: 3500, + warningDelayMs: 30000, // 30 seconds + baseNoise: 150 + }, - // Shoulder area - { id: "shoulder-1", x: 35, y: 25, zone: "shoulders", label: "Left Shoulder", pin: 4 }, - { id: "shoulder-2", x: 65, y: 25, zone: "shoulders", label: "Right Shoulder", pin: 5 }, + // Shoulder area - Moderate thresholds, medium escalation time + { + id: "shoulder-1", + x: 35, + y: 25, + zone: "shoulders", + label: "Left Shoulder", + pin: 4, + warningThreshold: 2800, + alarmThreshold: 3200, + warningDelayMs: 45000, // 45 seconds + baseNoise: 250 + }, + { + id: "shoulder-2", + x: 65, + y: 25, + zone: "shoulders", + label: "Right Shoulder", + pin: 5, + warningThreshold: 2800, + alarmThreshold: 3200, + warningDelayMs: 45000, // 45 seconds + baseNoise: 220 + }, - // Upper back - { id: "back-1", x: 40, y: 35, zone: "back", label: "Upper Back Left", pin: 6 }, - { id: "back-2", x: 50, y: 35, zone: "back", label: "Upper Back Center", pin: 7 }, - { id: "back-3", x: 60, y: 35, zone: "back", label: "Upper Back Right", pin: 8 }, + // Upper back - Moderate thresholds, 1 minute escalation + { + id: "back-1", + x: 40, + y: 35, + zone: "back", + label: "Upper Back Left", + pin: 6, + warningThreshold: 2500, + alarmThreshold: 3000, + warningDelayMs: 60000, // 1 minute + baseNoise: 300 + }, + { + id: "back-2", + x: 50, + y: 35, + zone: "back", + label: "Upper Back Center", + pin: 7, + warningThreshold: 2500, + alarmThreshold: 3000, + warningDelayMs: 60000, // 1 minute + baseNoise: 350 + }, + { + id: "back-3", + x: 60, + y: 35, + zone: "back", + label: "Upper Back Right", + pin: 8, + warningThreshold: 2500, + alarmThreshold: 3000, + warningDelayMs: 60000, // 1 minute + baseNoise: 280 + }, - // Lower back/Hip area - { id: "hip-1", x: 35, y: 50, zone: "hips", label: "Left Hip", pin: 9 }, - { id: "hip-2", x: 50, y: 50, zone: "hips", label: "Lower Back", pin: 10 }, - { id: "hip-3", x: 65, y: 50, zone: "hips", label: "Right Hip", pin: 11 }, + // Lower back/Hip area - Lower thresholds, 90 second escalation + { + id: "hip-1", + x: 35, + y: 50, + zone: "hips", + label: "Left Hip", + pin: 9, + warningThreshold: 2200, + alarmThreshold: 2800, + warningDelayMs: 90000, // 90 seconds + baseNoise: 400 + }, + { + id: "hip-2", + x: 50, + y: 50, + zone: "hips", + label: "Lower Back", + pin: 10, + warningThreshold: 2200, + alarmThreshold: 2800, + warningDelayMs: 90000, // 90 seconds + baseNoise: 450 + }, + { + id: "hip-3", + x: 65, + y: 50, + zone: "hips", + label: "Right Hip", + pin: 11, + warningThreshold: 2200, + alarmThreshold: 2800, + warningDelayMs: 90000, // 90 seconds + baseNoise: 380 + }, - // Thigh area - { id: "thigh-1", x: 40, y: 65, zone: "legs", label: "Left Thigh", pin: 12 }, - { id: "thigh-2", x: 60, y: 65, zone: "legs", label: "Right Thigh", pin: 13 }, + // Thigh area - Lower thresholds, 2 minute escalation + { + id: "thigh-1", + x: 40, + y: 65, + zone: "legs", + label: "Left Thigh", + pin: 12, + warningThreshold: 2000, + alarmThreshold: 2500, + warningDelayMs: 120000, // 2 minutes + baseNoise: 320 + }, + { + id: "thigh-2", + x: 60, + y: 65, + zone: "legs", + label: "Right Thigh", + pin: 13, + warningThreshold: 2000, + alarmThreshold: 2500, + warningDelayMs: 120000, // 2 minutes + baseNoise: 300 + }, - // Calf area (mock data) - { id: "calf-1", x: 40, y: 75, zone: "legs", label: "Left Calf" }, - { id: "calf-2", x: 60, y: 75, zone: "legs", label: "Right Calf" }, + // Calf area (mock data) - Lower thresholds, 2.5 minute escalation + { + id: "calf-1", + x: 40, + y: 75, + zone: "legs", + label: "Left Calf", + warningThreshold: 1800, + alarmThreshold: 2200, + warningDelayMs: 150000, // 2.5 minutes + baseNoise: 200 + }, + { + id: "calf-2", + x: 60, + y: 75, + zone: "legs", + label: "Right Calf", + warningThreshold: 1800, + alarmThreshold: 2200, + warningDelayMs: 150000, // 2.5 minutes + baseNoise: 220 + }, - // Feet (mock data) - { id: "feet-1", x: 45, y: 85, zone: "feet", label: "Left Foot" }, - { id: "feet-2", x: 55, y: 85, zone: "feet", label: "Right Foot" }, + // Feet (mock data) - Lowest thresholds, 3 minute escalation + { + id: "feet-1", + x: 45, + y: 85, + zone: "feet", + label: "Left Foot", + warningThreshold: 1500, + alarmThreshold: 1800, + warningDelayMs: 180000, // 3 minutes + baseNoise: 150 + }, + { + id: "feet-2", + x: 55, + y: 85, + zone: "feet", + label: "Right Foot", + warningThreshold: 1500, + alarmThreshold: 1800, + warningDelayMs: 180000, // 3 minutes + baseNoise: 160 + }, ]; export async function GET() { diff --git a/app/api/sensors/route.ts b/app/api/sensors/route.ts index 282dfc7..77727c8 100644 --- a/app/api/sensors/route.ts +++ b/app/api/sensors/route.ts @@ -1,9 +1,10 @@ import { NextRequest, NextResponse } from 'next/server'; import { BedHardware, PinState, PinChange } from '@/services/BedHardware'; import { SensorDataStorage, SensorDataPoint } from '@/services/SensorDataStorage'; +import { SensorConfig } from '@/types/sensor'; // Complete sensor configuration with positions, pin mappings, and thresholds -const SENSOR_CONFIG = [ +const SENSOR_CONFIG: SensorConfig[] = [ // Head area { id: "head-1", x: 45, y: 15, zone: "head", label: "Head Left", pin: 2, baseNoise: 200, warningThreshold: 3000, alarmThreshold: 3500, warningDelayMs: 30000 }, { id: "head-2", x: 55, y: 15, zone: "head", label: "Head Right", pin: 3, baseNoise: 150, warningThreshold: 3000, alarmThreshold: 3500, warningDelayMs: 30000 }, @@ -235,6 +236,25 @@ function updateMockSensorData() { const currentSensor = sensorData[sensor.id]; const variation = (Math.random() - 0.5) * 200; // Larger variation for analog values const newValue = Math.max(0, Math.min(4095, currentSensor.value + variation)); + const timestamp = Date.now(); + + // Determine status based on thresholds + let status = 'normal'; + let warningStartTime = currentSensor.warningStartTime; + + if (newValue >= sensor.alarmThreshold) { + status = 'alarm'; + warningStartTime = undefined; // Clear warning timer for immediate alarm + } else if (newValue >= sensor.warningThreshold) { + status = 'warning'; + if (!warningStartTime) { + warningStartTime = timestamp; // Start warning timer + } else if (timestamp - warningStartTime >= sensor.warningDelayMs) { + status = 'alarm'; // Escalate to alarm after delay + } + } else { + warningStartTime = undefined; // Clear warning timer + } sensorData[sensor.id] = { ...currentSensor, @@ -248,8 +268,8 @@ function updateMockSensorData() { value: newValue, } ], - status: newValue >= sensor.alarmThreshold ? 'alarm' : - newValue >= sensor.warningThreshold ? 'warning' : 'normal' + status, + warningStartTime }; } }); diff --git a/app/api/sensors/zones/route.ts b/app/api/sensors/zones/route.ts new file mode 100644 index 0000000..0bcea3d --- /dev/null +++ b/app/api/sensors/zones/route.ts @@ -0,0 +1,60 @@ +import { NextResponse } from 'next/server'; +import { ZONE_CONFIGS, validateSensorConfig } from '@/utils/sensorConfig'; + +export async function GET() { + try { + return NextResponse.json({ + success: true, + zones: ZONE_CONFIGS, + description: 'Available sensor zones with default threshold configurations', + timestamp: new Date().toISOString() + }); + } catch (error) { + console.error('Zone config API error:', error); + return NextResponse.json({ + success: false, + error: 'Failed to get zone configurations', + zones: {}, + timestamp: new Date().toISOString() + }, { status: 500 }); + } +} + +export async function POST(request: Request) { + try { + const sensorConfigs = await request.json(); + + if (!Array.isArray(sensorConfigs)) { + return NextResponse.json({ + success: false, + error: 'Expected array of sensor configurations' + }, { status: 400 }); + } + + const validationResults = sensorConfigs.map(config => ({ + sensorId: config.id, + ...validateSensorConfig(config) + })); + + const hasErrors = validationResults.some(result => !result.isValid); + + return NextResponse.json({ + success: !hasErrors, + validationResults, + summary: { + total: sensorConfigs.length, + valid: validationResults.filter(r => r.isValid).length, + withWarnings: validationResults.filter(r => r.warnings.length > 0).length, + withErrors: validationResults.filter(r => !r.isValid).length + }, + timestamp: new Date().toISOString() + }); + + } catch (error) { + console.error('Sensor validation API error:', error); + return NextResponse.json({ + success: false, + error: 'Failed to validate sensor configurations' + }, { status: 500 }); + } +} \ No newline at end of file diff --git a/app/api/test-scenarios/route.ts b/app/api/test-scenarios/route.ts new file mode 100644 index 0000000..d4a4e8b --- /dev/null +++ b/app/api/test-scenarios/route.ts @@ -0,0 +1,197 @@ +import { NextRequest, NextResponse } from 'next/server'; + +interface TestSensorData { + sensorId: string; + value: number; + status: string; + warningStartTime?: number; +} + +// Test scenarios for the bed pressure monitoring system +export async function POST(request: NextRequest) { + try { + const body = await request.json(); + const { scenario } = body; + + let testData: TestSensorData[] = []; + + switch (scenario) { + case 'normal': + testData = generateNormalScenario(); + break; + case 'warning': + testData = generateWarningScenario(); + break; + case 'alarm': + testData = generateAlarmScenario(); + break; + case 'escalation': + testData = generateEscalationScenario(); + break; + case 'mixed': + testData = generateMixedScenario(); + break; + default: + return NextResponse.json({ + success: false, + error: 'Invalid scenario. Use: normal, warning, alarm, escalation, or mixed' + }, { status: 400 }); + } + + return NextResponse.json({ + success: true, + scenario, + message: `Generated ${scenario} test scenario`, + testData, + timestamp: new Date().toISOString() + }); + + } catch (error) { + console.error('Test scenario API error:', error); + return NextResponse.json({ + success: false, + error: 'Failed to generate test scenario' + }, { status: 500 }); + } +} + +function generateNormalScenario() { + return [ + { sensorId: 'head-1', value: 1200, status: 'normal' }, + { sensorId: 'head-2', value: 1150, status: 'normal' }, + { sensorId: 'shoulder-1', value: 1800, status: 'normal' }, + { sensorId: 'shoulder-2', value: 1750, status: 'normal' }, + { sensorId: 'back-1', value: 2000, status: 'normal' }, + { sensorId: 'back-2', value: 2100, status: 'normal' }, + { sensorId: 'back-3', value: 1950, status: 'normal' }, + { sensorId: 'hip-1', value: 1900, status: 'normal' }, + { sensorId: 'hip-2', value: 2000, status: 'normal' }, + { sensorId: 'hip-3', value: 1850, status: 'normal' }, + { sensorId: 'thigh-1', value: 1600, status: 'normal' }, + { sensorId: 'thigh-2', value: 1550, status: 'normal' }, + { sensorId: 'calf-1', value: 1400, status: 'normal' }, + { sensorId: 'calf-2', value: 1350, status: 'normal' }, + { sensorId: 'feet-1', value: 1200, status: 'normal' }, + { sensorId: 'feet-2', value: 1150, status: 'normal' } + ]; +} + +function generateWarningScenario() { + return [ + { sensorId: 'head-1', value: 3100, status: 'warning' }, // Above warning threshold + { sensorId: 'head-2', value: 1150, status: 'normal' }, + { sensorId: 'shoulder-1', value: 2900, status: 'warning' }, // Above warning threshold + { sensorId: 'shoulder-2', value: 1750, status: 'normal' }, + { sensorId: 'back-1', value: 2600, status: 'warning' }, // Above warning threshold + { sensorId: 'back-2', value: 2100, status: 'normal' }, + { sensorId: 'back-3', value: 1950, status: 'normal' }, + { sensorId: 'hip-1', value: 1900, status: 'normal' }, + { sensorId: 'hip-2', value: 2300, status: 'warning' }, // Above warning threshold + { sensorId: 'hip-3', value: 1850, status: 'normal' }, + { sensorId: 'thigh-1', value: 1600, status: 'normal' }, + { sensorId: 'thigh-2', value: 1550, status: 'normal' }, + { sensorId: 'calf-1', value: 1400, status: 'normal' }, + { sensorId: 'calf-2', value: 1350, status: 'normal' }, + { sensorId: 'feet-1', value: 1200, status: 'normal' }, + { sensorId: 'feet-2', value: 1150, status: 'normal' } + ]; +} + +function generateAlarmScenario() { + return [ + { sensorId: 'head-1', value: 3600, status: 'alarm' }, // Above alarm threshold + { sensorId: 'head-2', value: 3550, status: 'alarm' }, // Above alarm threshold + { sensorId: 'shoulder-1', value: 3300, status: 'alarm' }, // Above alarm threshold + { sensorId: 'shoulder-2', value: 1750, status: 'normal' }, + { sensorId: 'back-1', value: 3100, status: 'alarm' }, // Above alarm threshold + { sensorId: 'back-2', value: 2100, status: 'normal' }, + { sensorId: 'back-3', value: 1950, status: 'normal' }, + { sensorId: 'hip-1', value: 2900, status: 'alarm' }, // Above alarm threshold + { sensorId: 'hip-2', value: 2000, status: 'normal' }, + { sensorId: 'hip-3', value: 1850, status: 'normal' }, + { sensorId: 'thigh-1', value: 1600, status: 'normal' }, + { sensorId: 'thigh-2', value: 1550, status: 'normal' }, + { sensorId: 'calf-1', value: 1400, status: 'normal' }, + { sensorId: 'calf-2', value: 1350, status: 'normal' }, + { sensorId: 'feet-1', value: 1200, status: 'normal' }, + { sensorId: 'feet-2', value: 1150, status: 'normal' } + ]; +} + +function generateEscalationScenario() { + // This scenario would simulate sensors that have been in warning state for a while + // and are about to escalate to alarm + const now = Date.now(); + const warningStartTime = now - 25000; // Started warning 25 seconds ago (close to 30s threshold) + + return [ + { sensorId: 'head-1', value: 3100, status: 'warning', warningStartTime }, + { sensorId: 'head-2', value: 1150, status: 'normal' }, + { sensorId: 'shoulder-1', value: 2900, status: 'warning', warningStartTime: now - 40000 }, // Close to 45s threshold + { sensorId: 'shoulder-2', value: 1750, status: 'normal' }, + { sensorId: 'back-1', value: 2600, status: 'warning', warningStartTime: now - 55000 }, // Close to 60s threshold + { sensorId: 'back-2', value: 2100, status: 'normal' }, + { sensorId: 'back-3', value: 1950, status: 'normal' }, + { sensorId: 'hip-1', value: 1900, status: 'normal' }, + { sensorId: 'hip-2', value: 2300, status: 'warning', warningStartTime: now - 85000 }, // Close to 90s threshold + { sensorId: 'hip-3', value: 1850, status: 'normal' }, + { sensorId: 'thigh-1', value: 2100, status: 'warning', warningStartTime: now - 115000 }, // Close to 120s threshold + { sensorId: 'thigh-2', value: 1550, status: 'normal' }, + { sensorId: 'calf-1', value: 1400, status: 'normal' }, + { sensorId: 'calf-2', value: 1350, status: 'normal' }, + { sensorId: 'feet-1', value: 1200, status: 'normal' }, + { sensorId: 'feet-2', value: 1150, status: 'normal' } + ]; +} + +function generateMixedScenario() { + const now = Date.now(); + + return [ + { sensorId: 'head-1', value: 3600, status: 'alarm' }, // Immediate alarm + { sensorId: 'head-2', value: 3100, status: 'warning', warningStartTime: now - 10000 }, // Recent warning + { sensorId: 'shoulder-1', value: 2900, status: 'warning', warningStartTime: now - 40000 }, // Long warning + { sensorId: 'shoulder-2', value: 1750, status: 'normal' }, + { sensorId: 'back-1', value: 3100, status: 'alarm' }, // Immediate alarm + { sensorId: 'back-2', value: 2600, status: 'warning', warningStartTime: now - 30000 }, // Warning close to escalation + { sensorId: 'back-3', value: 1950, status: 'normal' }, + { sensorId: 'hip-1', value: 1900, status: 'normal' }, + { sensorId: 'hip-2', value: 2900, status: 'alarm' }, // Immediate alarm + { sensorId: 'hip-3', value: 2300, status: 'warning', warningStartTime: now - 60000 }, // Warning + { sensorId: 'thigh-1', value: 1600, status: 'normal' }, + { sensorId: 'thigh-2', value: 2100, status: 'warning', warningStartTime: now - 90000 }, // Warning + { sensorId: 'calf-1', value: 2300, status: 'alarm' }, // Immediate alarm + { sensorId: 'calf-2', value: 1900, status: 'warning', warningStartTime: now - 120000 }, // Warning + { sensorId: 'feet-1', value: 1200, status: 'normal' }, + { sensorId: 'feet-2', value: 1900, status: 'alarm' } // Immediate alarm + ]; +} + +export async function GET() { + return NextResponse.json({ + success: true, + availableScenarios: [ + { + name: 'normal', + description: 'All sensors operating within normal ranges' + }, + { + name: 'warning', + description: 'Several sensors in warning state (above warning threshold)' + }, + { + name: 'alarm', + description: 'Multiple sensors in immediate alarm state (above alarm threshold)' + }, + { + name: 'escalation', + description: 'Sensors in warning state close to escalating to alarm after delay' + }, + { + name: 'mixed', + description: 'Mixed scenario with normal, warning, and alarm states' + } + ], + usage: 'POST to /api/test-scenarios with body: { "scenario": "scenario_name" }' + }); +} \ No newline at end of file diff --git a/app/page.tsx b/app/page.tsx index d97247f..ea89759 100644 --- a/app/page.tsx +++ b/app/page.tsx @@ -1,7 +1,37 @@ "use client" -import Component from "@/components/bed-pressure-monitor" +import { BedPressureHeader } from "@/components/bed-pressure/BedPressureHeader" +import { StatsCards } from "@/components/bed-pressure/StatsCards" +import { BedVisualization } from "@/components/bed-pressure/BedVisualization" +import { AlertsPanel } from "@/components/bed-pressure/AlertsPanel" +import { AlarmDashboard } from "@/components/bed-pressure/AlarmDashboard" +import { SensorDetailModal } from "@/components/bed-pressure/SensorDetailModal" +import { useBedPressureData } from "@/hooks/useBedPressureData" export default function Page() { - return + useBedPressureData() + + return ( +
+
+ + + + +
+
+ +
+
+ +
+
+ + {/* New Alarm Dashboard Section */} + + + +
+
+ ) } diff --git a/components/bed-pressure/AlarmDashboard.tsx b/components/bed-pressure/AlarmDashboard.tsx new file mode 100644 index 0000000..2aa72e3 --- /dev/null +++ b/components/bed-pressure/AlarmDashboard.tsx @@ -0,0 +1,256 @@ +import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card" +import { Button } from "@/components/ui/button" +import { Badge } from "@/components/ui/badge" +import { AlertTriangle, VolumeX, CheckCircle, Bell, TestTube } from "lucide-react" +import { useBedPressureStore } from "@/stores/bedPressureStore" +import { useEffect, useState } from "react" + +export function AlarmDashboard() { + const { + activeAlarms, + isConnected, + acknowledgeAlarm, + silenceAllAlarms + } = useBedPressureStore() + + const [unsilencedAlarms, setUnsilencedAlarms] = useState(0) + const [testScenario, setTestScenario] = useState('') + + // Update alarm counts + useEffect(() => { + const unsilenced = activeAlarms.filter(alarm => !alarm.silenced).length + setUnsilencedAlarms(unsilenced) + }, [activeAlarms]) + + const handleTestScenario = async (scenario: string) => { + try { + setTestScenario(scenario) + const response = await fetch('/api/test-scenarios', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ scenario }) + }) + + if (response.ok) { + console.log(`Applied ${scenario} test scenario`) + } + } catch (error) { + console.error('Failed to apply test scenario:', error) + } finally { + setTimeout(() => setTestScenario(''), 2000) + } + } + + const getSystemStatus = () => { + const alarmCount = activeAlarms.filter(a => a.type === 'alarm' && !a.silenced).length + const warningCount = activeAlarms.filter(a => a.type === 'warning' && !a.silenced).length + + if (alarmCount > 0) return { status: 'ALARM', color: 'text-red-600 bg-red-50 border-red-200', count: alarmCount } + if (warningCount > 0) return { status: 'WARNING', color: 'text-yellow-600 bg-yellow-50 border-yellow-200', count: warningCount } + return { status: 'NORMAL', color: 'text-green-600 bg-green-50 border-green-200', count: 0 } + } + + const systemStatus = getSystemStatus() + + return ( +
+ {/* System Status Overview */} + + +
+
+
+ {systemStatus.status === 'ALARM' ? ( + + ) : systemStatus.status === 'WARNING' ? ( + + ) : ( + + )} +
+
+ + SYSTEM STATUS: {systemStatus.status} + +

+ {isConnected ? 'Hardware Connected' : 'Using Mock Data'} • + {activeAlarms.length} Active Alarms • + {unsilencedAlarms} Unsilenced +

+
+
+ +
+ {systemStatus.count > 0 && ( + + {systemStatus.count} + + )} + {unsilencedAlarms > 0 && ( + + )} +
+
+
+
+ + {/* Active Alarms Summary */} +
+ + +
+ + Critical Alarms +
+

+ {activeAlarms.filter(a => a.type === 'alarm' && !a.silenced).length} +

+
+
+ + + +
+ + Warnings +
+

+ {activeAlarms.filter(a => a.type === 'warning' && !a.silenced).length} +

+
+
+ + + +
+ + Silenced +
+

+ {activeAlarms.filter(a => a.silenced).length} +

+
+
+ + + +
+ + Acknowledged +
+

+ {activeAlarms.filter(a => a.acknowledged).length} +

+
+
+
+ + {/* Test Scenarios */} + + + + + Test Scenarios + + + +
+ {[ + { name: 'normal', label: 'Normal', color: 'bg-green-100 text-green-700 hover:bg-green-200' }, + { name: 'warning', label: 'Warning', color: 'bg-yellow-100 text-yellow-700 hover:bg-yellow-200' }, + { name: 'alarm', label: 'Alarm', color: 'bg-red-100 text-red-700 hover:bg-red-200' }, + { name: 'escalation', label: 'Escalation', color: 'bg-orange-100 text-orange-700 hover:bg-orange-200' }, + { name: 'mixed', label: 'Mixed', color: 'bg-purple-100 text-purple-700 hover:bg-purple-200' } + ].map(scenario => ( + + ))} +
+

+ Click to simulate different alarm scenarios for testing +

+
+
+ + {/* Recent Alarm Activity */} + + + + + Recent Alarm Activity + + + +
+ {activeAlarms.length === 0 ? ( +
+ +

No active alarms

+
+ ) : ( + activeAlarms.slice(0, 5).map((alarm) => ( +
+
+ +
+

{alarm.sensorLabel}

+

+ {alarm.type.toUpperCase()} • Value: {alarm.value.toFixed(0)} • {alarm.time} +

+
+
+ +
+ {alarm.silenced && ( + + + Silenced + + )} + {alarm.acknowledged && ( + + + ACK + + )} + {!alarm.acknowledged && ( + + )} +
+
+ )) + )} +
+
+
+
+ ) +} \ No newline at end of file diff --git a/components/bed-pressure/BedPressureHeader.tsx b/components/bed-pressure/BedPressureHeader.tsx index 863e920..1017e57 100644 --- a/components/bed-pressure/BedPressureHeader.tsx +++ b/components/bed-pressure/BedPressureHeader.tsx @@ -1,10 +1,42 @@ import { Badge } from "@/components/ui/badge" import { Button } from "@/components/ui/button" -import { Activity, Pause, Play } from "lucide-react" +import { Activity, Pause, Play, Clock, AlertTriangle } from "lucide-react" import { useBedPressureStore } from "@/stores/bedPressureStore" +import { useEffect, useState } from "react" export function BedPressureHeader() { - const { isMonitoring, setIsMonitoring } = useBedPressureStore() + const { isMonitoring, setIsMonitoring, sensorData } = useBedPressureStore() + const [countdowns, setCountdowns] = useState>({}) + + // Update countdowns every second + useEffect(() => { + const interval = setInterval(() => { + const newCountdowns: Record = {} + const now = Date.now() + + Object.values(sensorData).forEach(sensor => { + if (sensor.status === 'warning' && sensor.warningStartTime && sensor.warningDelayMs) { + const elapsed = now - sensor.warningStartTime + const remaining = Math.max(0, sensor.warningDelayMs - elapsed) + if (remaining > 0) { + newCountdowns[sensor.id] = remaining + } + } + }) + + setCountdowns(newCountdowns) + }, 1000) + + return () => clearInterval(interval) + }, [sensorData]) + + const formatCountdown = (ms: number) => { + const minutes = Math.floor(ms / 60000) + const seconds = Math.floor((ms % 60000) / 1000) + return `${minutes}:${seconds.toString().padStart(2, '0')}` + } + + const warningCountdowns = Object.entries(countdowns).filter(([, time]) => time > 0) return (
@@ -16,6 +48,37 @@ export function BedPressureHeader() { {isMonitoring ? "Live" : "Paused"} + + {/* Warning Countdown Indicators */} + {warningCountdowns.length > 0 && ( +
+ {warningCountdowns.slice(0, 3).map(([sensorId, timeRemaining]) => { + const sensor = sensorData[sensorId] + return ( +
+ + + {sensor?.label} + +
+ + + {formatCountdown(timeRemaining)} + +
+
+ ) + })} + {warningCountdowns.length > 3 && ( + + +{warningCountdowns.length - 3} more + + )} +
+ )}
diff --git a/stores/bedPressureStore.ts b/stores/bedPressureStore.ts index 0d4aa1d..46f7246 100644 --- a/stores/bedPressureStore.ts +++ b/stores/bedPressureStore.ts @@ -1,14 +1,6 @@ import { create } from 'zustand' import { AlarmManager, AlarmEvent } from '@/services/AlarmManager' - -export interface SensorConfig { - id: string; - x: number; - y: number; - zone: string; - label: string; - pin?: number; -} +import { SensorConfig } from '@/types/sensor' export interface SensorData { id: string; @@ -25,6 +17,7 @@ export interface SensorData { warningThreshold?: number; alarmThreshold?: number; warningDelayMs?: number; + warningStartTime?: number; // Track when warning state started } export interface Alert { @@ -160,27 +153,77 @@ export const useBedPressureStore = create((set, get) => ({ digitalState?: number; warningThreshold?: number; alarmThreshold?: number; + warningDelayMs?: number; status?: string; }) => { const currentSensor = updated[sensor.id]; const newValue = sensor.value; + const config = sensorConfig.find(s => s.id === sensor.id); + + // Get thresholds from sensor data or config + const warningThreshold = sensor.warningThreshold || config?.warningThreshold || 2500; + const alarmThreshold = sensor.alarmThreshold || config?.alarmThreshold || 3500; + const warningDelayMs = sensor.warningDelayMs || config?.warningDelayMs || 60000; - // Check for alarms based on thresholds - if (sensor.alarmThreshold && newValue >= sensor.alarmThreshold) { - alarmManager.addAlarm(sensor.id, sensor.label, 'alarm', newValue, sensor.alarmThreshold); - } else if (sensor.warningThreshold && newValue >= sensor.warningThreshold) { - alarmManager.addAlarm(sensor.id, sensor.label, 'warning', newValue, sensor.warningThreshold); - } else { - alarmManager.clearAlarm(sensor.id); - } + // Determine status and handle alarm logic + let status = 'normal'; + let warningStartTime = currentSensor?.warningStartTime; + const now = Date.now(); - // Check for alerts (legacy alert system) - if (newValue > 3000 && currentSensor && currentSensor.currentValue <= 3000) { + if (newValue >= alarmThreshold) { + status = 'alarm'; + warningStartTime = undefined; // Clear warning timer for immediate alarm + + // Add alarm + alarmManager.addAlarm( + sensor.id, + sensor.label, + 'alarm', + newValue, + alarmThreshold + ); + newAlerts.push({ id: `${sensor.id}-${Date.now()}`, - message: `High value detected at ${sensor.label}`, + message: `ALARM: High value detected at ${sensor.label} (${newValue})`, time: new Date().toLocaleTimeString(), }); + + } else if (newValue >= warningThreshold) { + status = 'warning'; + + if (!warningStartTime) { + warningStartTime = now; // Start warning timer + } else if (now - warningStartTime >= warningDelayMs) { + status = 'alarm'; // Escalate to alarm after delay + + // Add escalated alarm + alarmManager.addAlarm( + sensor.id, + sensor.label, + 'alarm', + newValue, + warningThreshold + ); + + newAlerts.push({ + id: `${sensor.id}-${Date.now()}`, + message: `ALARM: Warning escalated for ${sensor.label} (${newValue})`, + time: new Date().toLocaleTimeString(), + }); + } else { + // Add warning alarm + alarmManager.addAlarm( + sensor.id, + sensor.label, + 'warning', + newValue, + warningThreshold + ); + } + } else { + warningStartTime = undefined; // Clear warning timer + alarmManager.clearAlarm(sensor.id); // Clear any existing alarms } // Update sensor data @@ -204,12 +247,14 @@ export const useBedPressureStore = create((set, get) => ({ timestamp: Date.now(), value: newValue, }], - status: sensor.status || (newValue > 3000 ? "critical" : newValue > 2500 ? "warning" : "normal"), + status, source: sensor.source, pin: sensor.pin, digitalState: sensor.digitalState, - warningThreshold: sensor.warningThreshold, - alarmThreshold: sensor.alarmThreshold + warningThreshold, + alarmThreshold, + warningDelayMs, + warningStartTime }; }); diff --git a/types/sensor.ts b/types/sensor.ts new file mode 100644 index 0000000..5833496 --- /dev/null +++ b/types/sensor.ts @@ -0,0 +1,24 @@ +export interface SensorConfig { + id: string; + x: number; + y: number; + zone: string; + label: string; + pin?: number; + warningThreshold: number; + alarmThreshold: number; + warningDelayMs: number; // Duration in milliseconds before warning escalates to alarm + baseNoise?: number; // For hardware sensors - noise level for analog conversion +} + +export interface SensorThresholds { + warningThreshold: number; + alarmThreshold: number; + warningDelayMs: number; +} + +export interface SensorZoneConfig { + zone: string; + defaultThresholds: SensorThresholds; + description: string; +} \ No newline at end of file diff --git a/utils/sensorConfig.ts b/utils/sensorConfig.ts new file mode 100644 index 0000000..271b356 --- /dev/null +++ b/utils/sensorConfig.ts @@ -0,0 +1,152 @@ +import { SensorConfig, SensorZoneConfig } from '@/types/sensor'; + +// Define zone-based default configurations +export const ZONE_CONFIGS: Record = { + head: { + zone: 'head', + defaultThresholds: { + warningThreshold: 3000, + alarmThreshold: 3500, + warningDelayMs: 30000 // 30 seconds - critical area needs fast response + }, + description: 'Head area - most critical, fastest escalation' + }, + shoulders: { + zone: 'shoulders', + defaultThresholds: { + warningThreshold: 2800, + alarmThreshold: 3200, + warningDelayMs: 45000 // 45 seconds + }, + description: 'Shoulder area - high priority, moderate escalation' + }, + back: { + zone: 'back', + defaultThresholds: { + warningThreshold: 2500, + alarmThreshold: 3000, + warningDelayMs: 60000 // 1 minute + }, + description: 'Back area - moderate priority, standard escalation' + }, + hips: { + zone: 'hips', + defaultThresholds: { + warningThreshold: 2200, + alarmThreshold: 2800, + warningDelayMs: 90000 // 90 seconds + }, + description: 'Hip area - moderate priority, longer escalation' + }, + legs: { + zone: 'legs', + defaultThresholds: { + warningThreshold: 2000, + alarmThreshold: 2500, + warningDelayMs: 120000 // 2 minutes + }, + description: 'Leg area - lower priority, extended escalation' + }, + feet: { + zone: 'feet', + defaultThresholds: { + warningThreshold: 1500, + alarmThreshold: 1800, + warningDelayMs: 180000 // 3 minutes + }, + description: 'Feet area - lowest priority, longest escalation' + } +}; + +/** + * Validates sensor configuration against zone defaults + */ +export function validateSensorConfig(config: SensorConfig): { + isValid: boolean; + warnings: string[]; + errors: string[]; +} { + const warnings: string[] = []; + const errors: string[] = []; + + // Check if zone exists + const zoneConfig = ZONE_CONFIGS[config.zone]; + if (!zoneConfig) { + errors.push(`Unknown zone: ${config.zone}`); + return { isValid: false, warnings, errors }; + } + + // Validate threshold values + if (config.alarmThreshold <= config.warningThreshold) { + errors.push(`Alarm threshold (${config.alarmThreshold}) must be greater than warning threshold (${config.warningThreshold})`); + } + + if (config.warningThreshold <= 0 || config.alarmThreshold <= 0) { + errors.push('Thresholds must be positive values'); + } + + if (config.warningDelayMs <= 0) { + errors.push('Warning delay must be positive'); + } + + // Check against zone defaults (warnings only) + const defaults = zoneConfig.defaultThresholds; + const tolerance = 0.2; // 20% tolerance + + if (Math.abs(config.warningThreshold - defaults.warningThreshold) / defaults.warningThreshold > tolerance) { + warnings.push(`Warning threshold (${config.warningThreshold}) differs significantly from zone default (${defaults.warningThreshold})`); + } + + if (Math.abs(config.alarmThreshold - defaults.alarmThreshold) / defaults.alarmThreshold > tolerance) { + warnings.push(`Alarm threshold (${config.alarmThreshold}) differs significantly from zone default (${defaults.alarmThreshold})`); + } + + if (Math.abs(config.warningDelayMs - defaults.warningDelayMs) / defaults.warningDelayMs > tolerance) { + warnings.push(`Warning delay (${config.warningDelayMs}ms) differs significantly from zone default (${defaults.warningDelayMs}ms)`); + } + + return { isValid: errors.length === 0, warnings, errors }; +} + +/** + * Creates a sensor config with zone defaults as base + */ +export function createSensorConfig( + id: string, + position: { x: number; y: number }, + zone: string, + label: string, + overrides: Partial = {} +): SensorConfig { + const zoneConfig = ZONE_CONFIGS[zone]; + if (!zoneConfig) { + throw new Error(`Unknown zone: ${zone}`); + } + + return { + id, + x: position.x, + y: position.y, + zone, + label, + ...zoneConfig.defaultThresholds, + ...overrides + }; +} + +/** + * Gets human-readable duration string + */ +export function formatDuration(ms: number): string { + const seconds = Math.floor(ms / 1000); + const minutes = Math.floor(seconds / 60); + const hours = Math.floor(minutes / 60); + + if (hours > 0) { + return `${hours}h ${minutes % 60}m`; + } else if (minutes > 0) { + return `${minutes}m ${seconds % 60}s`; + } else { + return `${seconds}s`; + } +} \ No newline at end of file