warning system

This commit is contained in:
Siwat Sirichai 2025-06-21 13:29:01 +07:00
parent 5e029ff99c
commit 0c5c7bcb5f
10 changed files with 1074 additions and 54 deletions

View file

@ -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() {

View file

@ -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
};
}
});

View file

@ -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 });
}
}

View file

@ -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" }'
});
}

View file

@ -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 <Component />
useBedPressureData()
return (
<div className="min-h-screen bg-gray-50 p-6">
<div className="max-w-7xl mx-auto space-y-6">
<BedPressureHeader />
<StatsCards />
<div className="grid grid-cols-1 lg:grid-cols-3 gap-6">
<div className="lg:col-span-2">
<BedVisualization />
</div>
<div className="space-y-6">
<AlertsPanel />
</div>
</div>
{/* New Alarm Dashboard Section */}
<AlarmDashboard />
<SensorDetailModal />
</div>
</div>
)
}

View file

@ -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<string>('')
// 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 (
<div className="space-y-4">
{/* System Status Overview */}
<Card className={`border-2 ${systemStatus.color}`}>
<CardHeader className="pb-3">
<div className="flex items-center justify-between">
<div className="flex items-center gap-3">
<div className={`p-2 rounded-full ${systemStatus.color}`}>
{systemStatus.status === 'ALARM' ? (
<AlertTriangle className="w-6 h-6 animate-pulse" />
) : systemStatus.status === 'WARNING' ? (
<AlertTriangle className="w-6 h-6" />
) : (
<CheckCircle className="w-6 h-6" />
)}
</div>
<div>
<CardTitle className={`text-lg ${systemStatus.status === 'ALARM' ? 'text-red-700' : systemStatus.status === 'WARNING' ? 'text-yellow-700' : 'text-green-700'}`}>
SYSTEM STATUS: {systemStatus.status}
</CardTitle>
<p className="text-sm text-gray-600">
{isConnected ? 'Hardware Connected' : 'Using Mock Data'}
{activeAlarms.length} Active Alarms
{unsilencedAlarms} Unsilenced
</p>
</div>
</div>
<div className="flex items-center gap-2">
{systemStatus.count > 0 && (
<Badge variant="destructive" className="text-lg px-3 py-1">
{systemStatus.count}
</Badge>
)}
{unsilencedAlarms > 0 && (
<Button
variant="outline"
size="sm"
onClick={() => silenceAllAlarms(300000)}
className="text-orange-600 hover:text-orange-700"
>
<VolumeX className="w-4 h-4 mr-1" />
Silence All (5m)
</Button>
)}
</div>
</div>
</CardHeader>
</Card>
{/* Active Alarms Summary */}
<div className="grid grid-cols-1 md:grid-cols-4 gap-4">
<Card>
<CardContent className="p-4 text-center">
<div className="flex items-center justify-center gap-2 mb-2">
<AlertTriangle className="w-5 h-5 text-red-600" />
<span className="text-sm font-medium text-gray-600">Critical Alarms</span>
</div>
<p className="text-2xl font-bold text-red-600">
{activeAlarms.filter(a => a.type === 'alarm' && !a.silenced).length}
</p>
</CardContent>
</Card>
<Card>
<CardContent className="p-4 text-center">
<div className="flex items-center justify-center gap-2 mb-2">
<AlertTriangle className="w-5 h-5 text-yellow-600" />
<span className="text-sm font-medium text-gray-600">Warnings</span>
</div>
<p className="text-2xl font-bold text-yellow-600">
{activeAlarms.filter(a => a.type === 'warning' && !a.silenced).length}
</p>
</CardContent>
</Card>
<Card>
<CardContent className="p-4 text-center">
<div className="flex items-center justify-center gap-2 mb-2">
<VolumeX className="w-5 h-5 text-gray-600" />
<span className="text-sm font-medium text-gray-600">Silenced</span>
</div>
<p className="text-2xl font-bold text-gray-600">
{activeAlarms.filter(a => a.silenced).length}
</p>
</CardContent>
</Card>
<Card>
<CardContent className="p-4 text-center">
<div className="flex items-center justify-center gap-2 mb-2">
<CheckCircle className="w-5 h-5 text-green-600" />
<span className="text-sm font-medium text-gray-600">Acknowledged</span>
</div>
<p className="text-2xl font-bold text-green-600">
{activeAlarms.filter(a => a.acknowledged).length}
</p>
</CardContent>
</Card>
</div>
{/* Test Scenarios */}
<Card>
<CardHeader>
<CardTitle className="flex items-center gap-2">
<TestTube className="w-5 h-5" />
Test Scenarios
</CardTitle>
</CardHeader>
<CardContent>
<div className="grid grid-cols-2 md:grid-cols-5 gap-2">
{[
{ 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 => (
<Button
key={scenario.name}
variant="outline"
size="sm"
onClick={() => handleTestScenario(scenario.name)}
disabled={testScenario === scenario.name}
className={`${scenario.color} border-0`}
>
{testScenario === scenario.name ? 'Applied!' : scenario.label}
</Button>
))}
</div>
<p className="text-xs text-gray-500 mt-2">
Click to simulate different alarm scenarios for testing
</p>
</CardContent>
</Card>
{/* Recent Alarm Activity */}
<Card>
<CardHeader>
<CardTitle className="flex items-center gap-2">
<Bell className="w-5 h-5" />
Recent Alarm Activity
</CardTitle>
</CardHeader>
<CardContent>
<div className="space-y-2 max-h-48 overflow-y-auto">
{activeAlarms.length === 0 ? (
<div className="text-center py-4">
<CheckCircle className="w-8 h-8 text-green-500 mx-auto mb-2" />
<p className="text-sm text-gray-500">No active alarms</p>
</div>
) : (
activeAlarms.slice(0, 5).map((alarm) => (
<div
key={alarm.id}
className="flex items-center justify-between p-2 bg-gray-50 rounded border"
>
<div className="flex items-center gap-2">
<AlertTriangle className={`w-4 h-4 ${
alarm.type === 'alarm' ? 'text-red-600' : 'text-yellow-600'
}`} />
<div>
<p className="text-sm font-medium">{alarm.sensorLabel}</p>
<p className="text-xs text-gray-600">
{alarm.type.toUpperCase()} Value: {alarm.value.toFixed(0)} {alarm.time}
</p>
</div>
</div>
<div className="flex items-center gap-1">
{alarm.silenced && (
<Badge variant="secondary" className="text-xs">
<VolumeX className="w-3 h-3 mr-1" />
Silenced
</Badge>
)}
{alarm.acknowledged && (
<Badge variant="outline" className="text-xs">
<CheckCircle className="w-3 h-3 mr-1" />
ACK
</Badge>
)}
{!alarm.acknowledged && (
<Button
variant="outline"
size="sm"
onClick={() => acknowledgeAlarm(alarm.id)}
className="text-xs h-6 px-2"
>
ACK
</Button>
)}
</div>
</div>
))
)}
</div>
</CardContent>
</Card>
</div>
)
}

View file

@ -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<Record<string, number>>({})
// Update countdowns every second
useEffect(() => {
const interval = setInterval(() => {
const newCountdowns: Record<string, number> = {}
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 (
<div className="flex items-center justify-between">
@ -16,6 +48,37 @@ export function BedPressureHeader() {
<Badge variant={isMonitoring ? "default" : "secondary"} className="px-3 py-1">
{isMonitoring ? "Live" : "Paused"}
</Badge>
{/* Warning Countdown Indicators */}
{warningCountdowns.length > 0 && (
<div className="flex items-center gap-2">
{warningCountdowns.slice(0, 3).map(([sensorId, timeRemaining]) => {
const sensor = sensorData[sensorId]
return (
<div
key={sensorId}
className="flex items-center gap-1 px-2 py-1 bg-yellow-100 border border-yellow-300 rounded-md text-sm"
>
<AlertTriangle className="w-4 h-4 text-yellow-600" />
<span className="text-yellow-800 font-medium">
{sensor?.label}
</span>
<div className="flex items-center gap-1 text-yellow-700">
<Clock className="w-3 h-3" />
<span className="font-mono text-xs">
{formatCountdown(timeRemaining)}
</span>
</div>
</div>
)
})}
{warningCountdowns.length > 3 && (
<Badge variant="secondary" className="text-xs">
+{warningCountdowns.length - 3} more
</Badge>
)}
</div>
)}
</div>
<div className="flex items-center gap-2">

View file

@ -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<BedPressureStore>((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<BedPressureStore>((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
};
});

24
types/sensor.ts Normal file
View file

@ -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;
}

152
utils/sensorConfig.ts Normal file
View file

@ -0,0 +1,152 @@
import { SensorConfig, SensorZoneConfig } from '@/types/sensor';
// Define zone-based default configurations
export const ZONE_CONFIGS: Record<string, SensorZoneConfig> = {
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> = {}
): 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`;
}
}