warning system
This commit is contained in:
parent
5e029ff99c
commit
0c5c7bcb5f
10 changed files with 1074 additions and 54 deletions
|
@ -1,36 +1,209 @@
|
||||||
import { NextResponse } from 'next/server';
|
import { NextResponse } from 'next/server';
|
||||||
|
import { SensorConfig } from '@/types/sensor';
|
||||||
|
|
||||||
// Sensor configuration that matches the API route
|
// Sensor configuration that matches the API route
|
||||||
const SENSOR_CONFIG = [
|
const SENSOR_CONFIG: SensorConfig[] = [
|
||||||
// Head area
|
// Head area - Higher thresholds due to critical nature, faster escalation
|
||||||
{ 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 },
|
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
|
// Shoulder area - Moderate thresholds, medium escalation time
|
||||||
{ 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 },
|
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
|
// Upper back - Moderate thresholds, 1 minute escalation
|
||||||
{ 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-1",
|
||||||
{ id: "back-3", x: 60, y: 35, zone: "back", label: "Upper Back Right", pin: 8 },
|
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
|
// Lower back/Hip area - Lower thresholds, 90 second escalation
|
||||||
{ 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-1",
|
||||||
{ id: "hip-3", x: 65, y: 50, zone: "hips", label: "Right Hip", pin: 11 },
|
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
|
// Thigh area - Lower thresholds, 2 minute escalation
|
||||||
{ 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 },
|
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)
|
// Calf area (mock data) - Lower thresholds, 2.5 minute escalation
|
||||||
{ id: "calf-1", x: 40, y: 75, zone: "legs", label: "Left Calf" },
|
{
|
||||||
{ id: "calf-2", x: 60, y: 75, zone: "legs", label: "Right Calf" },
|
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)
|
// Feet (mock data) - Lowest thresholds, 3 minute escalation
|
||||||
{ id: "feet-1", x: 45, y: 85, zone: "feet", label: "Left Foot" },
|
{
|
||||||
{ id: "feet-2", x: 55, y: 85, zone: "feet", label: "Right Foot" },
|
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() {
|
export async function GET() {
|
||||||
|
|
|
@ -1,9 +1,10 @@
|
||||||
import { NextRequest, NextResponse } from 'next/server';
|
import { NextRequest, NextResponse } from 'next/server';
|
||||||
import { BedHardware, PinState, PinChange } from '@/services/BedHardware';
|
import { BedHardware, PinState, PinChange } from '@/services/BedHardware';
|
||||||
import { SensorDataStorage, SensorDataPoint } from '@/services/SensorDataStorage';
|
import { SensorDataStorage, SensorDataPoint } from '@/services/SensorDataStorage';
|
||||||
|
import { SensorConfig } from '@/types/sensor';
|
||||||
|
|
||||||
// Complete sensor configuration with positions, pin mappings, and thresholds
|
// Complete sensor configuration with positions, pin mappings, and thresholds
|
||||||
const SENSOR_CONFIG = [
|
const SENSOR_CONFIG: SensorConfig[] = [
|
||||||
// Head area
|
// 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-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 },
|
{ 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 currentSensor = sensorData[sensor.id];
|
||||||
const variation = (Math.random() - 0.5) * 200; // Larger variation for analog values
|
const variation = (Math.random() - 0.5) * 200; // Larger variation for analog values
|
||||||
const newValue = Math.max(0, Math.min(4095, currentSensor.value + variation));
|
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] = {
|
sensorData[sensor.id] = {
|
||||||
...currentSensor,
|
...currentSensor,
|
||||||
|
@ -248,8 +268,8 @@ function updateMockSensorData() {
|
||||||
value: newValue,
|
value: newValue,
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
status: newValue >= sensor.alarmThreshold ? 'alarm' :
|
status,
|
||||||
newValue >= sensor.warningThreshold ? 'warning' : 'normal'
|
warningStartTime
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
60
app/api/sensors/zones/route.ts
Normal file
60
app/api/sensors/zones/route.ts
Normal 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 });
|
||||||
|
}
|
||||||
|
}
|
197
app/api/test-scenarios/route.ts
Normal file
197
app/api/test-scenarios/route.ts
Normal 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" }'
|
||||||
|
});
|
||||||
|
}
|
34
app/page.tsx
34
app/page.tsx
|
@ -1,7 +1,37 @@
|
||||||
"use client"
|
"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() {
|
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>
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
256
components/bed-pressure/AlarmDashboard.tsx
Normal file
256
components/bed-pressure/AlarmDashboard.tsx
Normal 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>
|
||||||
|
)
|
||||||
|
}
|
|
@ -1,10 +1,42 @@
|
||||||
import { Badge } from "@/components/ui/badge"
|
import { Badge } from "@/components/ui/badge"
|
||||||
import { Button } from "@/components/ui/button"
|
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 { useBedPressureStore } from "@/stores/bedPressureStore"
|
||||||
|
import { useEffect, useState } from "react"
|
||||||
|
|
||||||
export function BedPressureHeader() {
|
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 (
|
return (
|
||||||
<div className="flex items-center justify-between">
|
<div className="flex items-center justify-between">
|
||||||
|
@ -16,6 +48,37 @@ export function BedPressureHeader() {
|
||||||
<Badge variant={isMonitoring ? "default" : "secondary"} className="px-3 py-1">
|
<Badge variant={isMonitoring ? "default" : "secondary"} className="px-3 py-1">
|
||||||
{isMonitoring ? "Live" : "Paused"}
|
{isMonitoring ? "Live" : "Paused"}
|
||||||
</Badge>
|
</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>
|
||||||
|
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
|
|
|
@ -1,14 +1,6 @@
|
||||||
import { create } from 'zustand'
|
import { create } from 'zustand'
|
||||||
import { AlarmManager, AlarmEvent } from '@/services/AlarmManager'
|
import { AlarmManager, AlarmEvent } from '@/services/AlarmManager'
|
||||||
|
import { SensorConfig } from '@/types/sensor'
|
||||||
export interface SensorConfig {
|
|
||||||
id: string;
|
|
||||||
x: number;
|
|
||||||
y: number;
|
|
||||||
zone: string;
|
|
||||||
label: string;
|
|
||||||
pin?: number;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface SensorData {
|
export interface SensorData {
|
||||||
id: string;
|
id: string;
|
||||||
|
@ -25,6 +17,7 @@ export interface SensorData {
|
||||||
warningThreshold?: number;
|
warningThreshold?: number;
|
||||||
alarmThreshold?: number;
|
alarmThreshold?: number;
|
||||||
warningDelayMs?: number;
|
warningDelayMs?: number;
|
||||||
|
warningStartTime?: number; // Track when warning state started
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface Alert {
|
export interface Alert {
|
||||||
|
@ -160,27 +153,77 @@ export const useBedPressureStore = create<BedPressureStore>((set, get) => ({
|
||||||
digitalState?: number;
|
digitalState?: number;
|
||||||
warningThreshold?: number;
|
warningThreshold?: number;
|
||||||
alarmThreshold?: number;
|
alarmThreshold?: number;
|
||||||
|
warningDelayMs?: number;
|
||||||
status?: string;
|
status?: string;
|
||||||
}) => {
|
}) => {
|
||||||
const currentSensor = updated[sensor.id];
|
const currentSensor = updated[sensor.id];
|
||||||
const newValue = sensor.value;
|
const newValue = sensor.value;
|
||||||
|
const config = sensorConfig.find(s => s.id === sensor.id);
|
||||||
|
|
||||||
// Check for alarms based on thresholds
|
// Get thresholds from sensor data or config
|
||||||
if (sensor.alarmThreshold && newValue >= sensor.alarmThreshold) {
|
const warningThreshold = sensor.warningThreshold || config?.warningThreshold || 2500;
|
||||||
alarmManager.addAlarm(sensor.id, sensor.label, 'alarm', newValue, sensor.alarmThreshold);
|
const alarmThreshold = sensor.alarmThreshold || config?.alarmThreshold || 3500;
|
||||||
} else if (sensor.warningThreshold && newValue >= sensor.warningThreshold) {
|
const warningDelayMs = sensor.warningDelayMs || config?.warningDelayMs || 60000;
|
||||||
alarmManager.addAlarm(sensor.id, sensor.label, 'warning', newValue, sensor.warningThreshold);
|
|
||||||
} else {
|
// Determine status and handle alarm logic
|
||||||
alarmManager.clearAlarm(sensor.id);
|
let status = 'normal';
|
||||||
}
|
let warningStartTime = currentSensor?.warningStartTime;
|
||||||
|
const now = Date.now();
|
||||||
|
|
||||||
|
if (newValue >= alarmThreshold) {
|
||||||
|
status = 'alarm';
|
||||||
|
warningStartTime = undefined; // Clear warning timer for immediate alarm
|
||||||
|
|
||||||
|
// Add alarm
|
||||||
|
alarmManager.addAlarm(
|
||||||
|
sensor.id,
|
||||||
|
sensor.label,
|
||||||
|
'alarm',
|
||||||
|
newValue,
|
||||||
|
alarmThreshold
|
||||||
|
);
|
||||||
|
|
||||||
// Check for alerts (legacy alert system)
|
|
||||||
if (newValue > 3000 && currentSensor && currentSensor.currentValue <= 3000) {
|
|
||||||
newAlerts.push({
|
newAlerts.push({
|
||||||
id: `${sensor.id}-${Date.now()}`,
|
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(),
|
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
|
// Update sensor data
|
||||||
|
@ -204,12 +247,14 @@ export const useBedPressureStore = create<BedPressureStore>((set, get) => ({
|
||||||
timestamp: Date.now(),
|
timestamp: Date.now(),
|
||||||
value: newValue,
|
value: newValue,
|
||||||
}],
|
}],
|
||||||
status: sensor.status || (newValue > 3000 ? "critical" : newValue > 2500 ? "warning" : "normal"),
|
status,
|
||||||
source: sensor.source,
|
source: sensor.source,
|
||||||
pin: sensor.pin,
|
pin: sensor.pin,
|
||||||
digitalState: sensor.digitalState,
|
digitalState: sensor.digitalState,
|
||||||
warningThreshold: sensor.warningThreshold,
|
warningThreshold,
|
||||||
alarmThreshold: sensor.alarmThreshold
|
alarmThreshold,
|
||||||
|
warningDelayMs,
|
||||||
|
warningStartTime
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
24
types/sensor.ts
Normal file
24
types/sensor.ts
Normal 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
152
utils/sensorConfig.ts
Normal 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`;
|
||||||
|
}
|
||||||
|
}
|
Loading…
Add table
Add a link
Reference in a new issue