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