dynamic graph
This commit is contained in:
parent
a606796d9e
commit
5e029ff99c
17 changed files with 1707 additions and 569 deletions
207
services/AlarmManager.ts
Normal file
207
services/AlarmManager.ts
Normal file
|
@ -0,0 +1,207 @@
|
|||
export interface AlarmEvent {
|
||||
id: string;
|
||||
sensorId: string;
|
||||
sensorLabel: string;
|
||||
type: 'warning' | 'alarm';
|
||||
value: number;
|
||||
threshold: number;
|
||||
timestamp: number;
|
||||
time: string;
|
||||
acknowledged: boolean;
|
||||
silenced: boolean;
|
||||
silencedUntil?: number;
|
||||
}
|
||||
|
||||
export class AlarmManager {
|
||||
private static instance: AlarmManager;
|
||||
private activeAlarms: Map<string, AlarmEvent> = new Map();
|
||||
private alarmHistory: AlarmEvent[] = [];
|
||||
private alarmCallbacks: ((alarm: AlarmEvent) => void)[] = [];
|
||||
private audioContext: AudioContext | null = null;
|
||||
private alarmSound: AudioBuffer | null = null;
|
||||
private isPlayingAlarm = false;
|
||||
|
||||
private constructor() {
|
||||
this.initializeAudio();
|
||||
}
|
||||
|
||||
static getInstance(): AlarmManager {
|
||||
if (!AlarmManager.instance) {
|
||||
AlarmManager.instance = new AlarmManager();
|
||||
}
|
||||
return AlarmManager.instance;
|
||||
}
|
||||
|
||||
private async initializeAudio() {
|
||||
try {
|
||||
// Check if we're in browser environment
|
||||
if (typeof window !== 'undefined') {
|
||||
this.audioContext = new (window.AudioContext || (window as unknown as { webkitAudioContext: typeof AudioContext }).webkitAudioContext)();
|
||||
// Generate alarm sound programmatically
|
||||
await this.generateAlarmSound();
|
||||
}
|
||||
} catch (error) {
|
||||
console.warn('Audio context not available:', error);
|
||||
}
|
||||
}
|
||||
|
||||
private async generateAlarmSound() {
|
||||
if (!this.audioContext) return;
|
||||
|
||||
const sampleRate = this.audioContext.sampleRate;
|
||||
const duration = 1; // 1 second
|
||||
const buffer = this.audioContext.createBuffer(1, sampleRate * duration, sampleRate);
|
||||
const data = buffer.getChannelData(0);
|
||||
|
||||
// Generate a beeping sound (sine wave with modulation)
|
||||
for (let i = 0; i < data.length; i++) {
|
||||
const t = i / sampleRate;
|
||||
const frequency = 800; // Hz
|
||||
const envelope = Math.sin(t * Math.PI * 4) > 0 ? 1 : 0; // Beeping pattern
|
||||
data[i] = Math.sin(2 * Math.PI * frequency * t) * envelope * 0.3;
|
||||
}
|
||||
|
||||
this.alarmSound = buffer;
|
||||
}
|
||||
|
||||
private async playAlarmSound() {
|
||||
if (!this.audioContext || !this.alarmSound || this.isPlayingAlarm) return;
|
||||
|
||||
try {
|
||||
// Resume audio context if suspended
|
||||
if (this.audioContext.state === 'suspended') {
|
||||
await this.audioContext.resume();
|
||||
}
|
||||
|
||||
const source = this.audioContext.createBufferSource();
|
||||
source.buffer = this.alarmSound;
|
||||
source.connect(this.audioContext.destination);
|
||||
source.start();
|
||||
|
||||
this.isPlayingAlarm = true;
|
||||
source.onended = () => {
|
||||
this.isPlayingAlarm = false;
|
||||
};
|
||||
} catch (error) {
|
||||
console.warn('Failed to play alarm sound:', error);
|
||||
}
|
||||
}
|
||||
|
||||
addAlarm(sensorId: string, sensorLabel: string, type: 'warning' | 'alarm', value: number, threshold: number) {
|
||||
const alarmId = `${sensorId}-${type}`;
|
||||
const timestamp = Date.now();
|
||||
|
||||
const alarm: AlarmEvent = {
|
||||
id: alarmId,
|
||||
sensorId,
|
||||
sensorLabel,
|
||||
type,
|
||||
value,
|
||||
threshold,
|
||||
timestamp,
|
||||
time: new Date(timestamp).toLocaleTimeString(),
|
||||
acknowledged: false,
|
||||
silenced: false
|
||||
};
|
||||
|
||||
// Check if alarm is already active and silenced
|
||||
const existingAlarm = this.activeAlarms.get(alarmId);
|
||||
if (existingAlarm?.silenced && existingAlarm.silencedUntil && timestamp < existingAlarm.silencedUntil) {
|
||||
// Update values but keep silenced state
|
||||
alarm.silenced = true;
|
||||
alarm.silencedUntil = existingAlarm.silencedUntil;
|
||||
}
|
||||
|
||||
this.activeAlarms.set(alarmId, alarm);
|
||||
this.alarmHistory.unshift(alarm);
|
||||
|
||||
// Keep only last 100 history items
|
||||
if (this.alarmHistory.length > 100) {
|
||||
this.alarmHistory = this.alarmHistory.slice(0, 100);
|
||||
}
|
||||
|
||||
// Play sound for new alarms
|
||||
if (type === 'alarm' && !alarm.silenced) {
|
||||
this.playAlarmSound();
|
||||
}
|
||||
|
||||
// Notify callbacks
|
||||
this.alarmCallbacks.forEach(callback => callback(alarm));
|
||||
|
||||
return alarm;
|
||||
}
|
||||
|
||||
clearAlarm(sensorId: string, type?: 'warning' | 'alarm') {
|
||||
if (type) {
|
||||
const alarmId = `${sensorId}-${type}`;
|
||||
this.activeAlarms.delete(alarmId);
|
||||
} else {
|
||||
// Clear all alarms for this sensor
|
||||
this.activeAlarms.delete(`${sensorId}-warning`);
|
||||
this.activeAlarms.delete(`${sensorId}-alarm`);
|
||||
}
|
||||
}
|
||||
|
||||
acknowledgeAlarm(alarmId: string) {
|
||||
const alarm = this.activeAlarms.get(alarmId);
|
||||
if (alarm) {
|
||||
alarm.acknowledged = true;
|
||||
this.activeAlarms.set(alarmId, alarm);
|
||||
}
|
||||
}
|
||||
|
||||
silenceAlarm(alarmId: string, durationMs: number = 300000) { // Default 5 minutes
|
||||
const alarm = this.activeAlarms.get(alarmId);
|
||||
if (alarm) {
|
||||
alarm.silenced = true;
|
||||
alarm.silencedUntil = Date.now() + durationMs;
|
||||
this.activeAlarms.set(alarmId, alarm);
|
||||
}
|
||||
}
|
||||
|
||||
silenceAllAlarms(durationMs: number = 300000) {
|
||||
const timestamp = Date.now();
|
||||
this.activeAlarms.forEach((alarm, alarmId) => {
|
||||
alarm.silenced = true;
|
||||
alarm.silencedUntil = timestamp + durationMs;
|
||||
this.activeAlarms.set(alarmId, alarm);
|
||||
});
|
||||
}
|
||||
|
||||
getActiveAlarms(): AlarmEvent[] {
|
||||
const now = Date.now();
|
||||
const activeAlarms: AlarmEvent[] = [];
|
||||
|
||||
this.activeAlarms.forEach((alarm, alarmId) => {
|
||||
// Check if silence period has expired
|
||||
if (alarm.silenced && alarm.silencedUntil && now >= alarm.silencedUntil) {
|
||||
alarm.silenced = false;
|
||||
alarm.silencedUntil = undefined;
|
||||
this.activeAlarms.set(alarmId, alarm);
|
||||
}
|
||||
|
||||
activeAlarms.push(alarm);
|
||||
});
|
||||
|
||||
return activeAlarms.sort((a, b) => b.timestamp - a.timestamp);
|
||||
}
|
||||
|
||||
getAlarmHistory(): AlarmEvent[] {
|
||||
return this.alarmHistory;
|
||||
}
|
||||
|
||||
hasUnacknowledgedAlarms(): boolean {
|
||||
return Array.from(this.activeAlarms.values()).some(alarm => !alarm.acknowledged);
|
||||
}
|
||||
|
||||
onAlarm(callback: (alarm: AlarmEvent) => void) {
|
||||
this.alarmCallbacks.push(callback);
|
||||
}
|
||||
|
||||
offAlarm(callback: (alarm: AlarmEvent) => void) {
|
||||
const index = this.alarmCallbacks.indexOf(callback);
|
||||
if (index > -1) {
|
||||
this.alarmCallbacks.splice(index, 1);
|
||||
}
|
||||
}
|
||||
}
|
131
services/SensorDataStorage.ts
Normal file
131
services/SensorDataStorage.ts
Normal file
|
@ -0,0 +1,131 @@
|
|||
import fs from 'fs/promises';
|
||||
import path from 'path';
|
||||
|
||||
const DATA_DIR = path.join(process.cwd(), 'data');
|
||||
const SENSOR_DATA_FILE = path.join(DATA_DIR, 'sensor-data.json');
|
||||
|
||||
export interface SensorDataPoint {
|
||||
sensorId: string;
|
||||
value: number; // Changed from pressure to value (0-4095)
|
||||
timestamp: number;
|
||||
time: string;
|
||||
source: 'hardware' | 'mock';
|
||||
pin?: number;
|
||||
digitalState?: number;
|
||||
}
|
||||
|
||||
export class SensorDataStorage {
|
||||
private static instance: SensorDataStorage;
|
||||
private dataCache: SensorDataPoint[] = [];
|
||||
|
||||
private constructor() {
|
||||
this.initializeStorage();
|
||||
}
|
||||
|
||||
static getInstance(): SensorDataStorage {
|
||||
if (!SensorDataStorage.instance) {
|
||||
SensorDataStorage.instance = new SensorDataStorage();
|
||||
}
|
||||
return SensorDataStorage.instance;
|
||||
}
|
||||
|
||||
private async initializeStorage() {
|
||||
try {
|
||||
// Ensure data directory exists
|
||||
await fs.mkdir(DATA_DIR, { recursive: true });
|
||||
|
||||
// Load existing data
|
||||
await this.loadData();
|
||||
} catch (error) {
|
||||
console.warn('Failed to initialize sensor data storage:', error);
|
||||
}
|
||||
}
|
||||
|
||||
private async loadData() {
|
||||
try {
|
||||
const data = await fs.readFile(SENSOR_DATA_FILE, 'utf8');
|
||||
this.dataCache = JSON.parse(data);
|
||||
console.log(`Loaded ${this.dataCache.length} sensor data points from storage`);
|
||||
} catch {
|
||||
// File doesn't exist or is corrupted, start with empty cache
|
||||
this.dataCache = [];
|
||||
console.log('Starting with empty sensor data cache');
|
||||
}
|
||||
}
|
||||
|
||||
private async saveData() {
|
||||
try {
|
||||
await fs.writeFile(SENSOR_DATA_FILE, JSON.stringify(this.dataCache, null, 2));
|
||||
} catch (error) {
|
||||
console.error('Failed to save sensor data:', error);
|
||||
}
|
||||
}
|
||||
|
||||
async addDataPoint(dataPoint: SensorDataPoint) {
|
||||
this.dataCache.push(dataPoint);
|
||||
|
||||
// Keep only last 7 days of data to prevent unlimited storage growth
|
||||
const sevenDaysAgo = Date.now() - (7 * 24 * 60 * 60 * 1000);
|
||||
this.dataCache = this.dataCache.filter(point => point.timestamp > sevenDaysAgo);
|
||||
|
||||
// Save to disk every 10 data points to reduce I/O
|
||||
if (this.dataCache.length % 10 === 0) {
|
||||
await this.saveData();
|
||||
}
|
||||
}
|
||||
|
||||
async getDataForSensor(
|
||||
sensorId: string,
|
||||
timespan: number = 24 * 60 * 60 * 1000 // Default 24 hours in milliseconds
|
||||
): Promise<SensorDataPoint[]> {
|
||||
const cutoffTime = Date.now() - timespan;
|
||||
return this.dataCache
|
||||
.filter(point => point.sensorId === sensorId && point.timestamp > cutoffTime)
|
||||
.sort((a, b) => a.timestamp - b.timestamp);
|
||||
}
|
||||
|
||||
async getAllRecentData(timespan: number = 24 * 60 * 60 * 1000): Promise<SensorDataPoint[]> {
|
||||
const cutoffTime = Date.now() - timespan;
|
||||
return this.dataCache
|
||||
.filter(point => point.timestamp > cutoffTime)
|
||||
.sort((a, b) => a.timestamp - b.timestamp);
|
||||
}
|
||||
|
||||
async forceSave() {
|
||||
await this.saveData();
|
||||
}
|
||||
|
||||
// Generate time series data for a specific timespan
|
||||
generateTimeSeriesData(
|
||||
sensorData: SensorDataPoint[],
|
||||
timespan: number = 24 * 60 * 60 * 1000
|
||||
): Array<{ time: string; timestamp: number; value: number }> {
|
||||
if (sensorData.length === 0) {
|
||||
// Generate mock data if no real data exists
|
||||
return this.generateMockTimeSeriesData(timespan);
|
||||
}
|
||||
|
||||
return sensorData.map(point => ({
|
||||
time: point.time,
|
||||
timestamp: point.timestamp,
|
||||
value: point.value
|
||||
}));
|
||||
}
|
||||
|
||||
private generateMockTimeSeriesData(timespan: number): Array<{ time: string; timestamp: number; value: number }> {
|
||||
const data = [];
|
||||
const now = Date.now();
|
||||
const interval = Math.max(1000, timespan / 288); // At least 1 second intervals, up to 288 points
|
||||
|
||||
for (let i = timespan; i >= 0; i -= interval) {
|
||||
const timestamp = now - i;
|
||||
const time = new Date(timestamp);
|
||||
data.push({
|
||||
time: time.toLocaleTimeString("en-US", { hour12: false }),
|
||||
timestamp: timestamp,
|
||||
value: Math.floor(Math.random() * 4096 + Math.sin(i / 60000) * 500 + 2000), // 0-4095 range
|
||||
});
|
||||
}
|
||||
return data;
|
||||
}
|
||||
}
|
Loading…
Add table
Add a link
Reference in a new issue