File size: 3,938 Bytes
604d53b
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
import { PatientVitals, TelemetryPacket } from "./types";
import { evaluateEdge } from "./inference";

type PatientState = "normal" | "asthma" | "hypertension" | "heart_disease" | "diabetes";

export class BrowserTelemetrySimulator {
  private hr = 75.0;
  private spo2 = 98.0;
  private sys_bp = 120.0;
  private dia_bp = 80.0;
  private temp = 37.0;

  private target_hr = 75.0;
  private target_spo2 = 98.0;
  private target_sys = 120.0;
  private target_dia = 80.0;

  private timer: NodeJS.Timeout | null = null;
  private listeners: ((packet: TelemetryPacket) => void)[] = [];

  constructor() {}

  private shiftState() {
    // 8% chance every tick to shift patient state
    if (Math.random() < 0.08) {
      const states: PatientState[] = [
        "normal", "normal", "normal", "asthma", "hypertension", "heart_disease", "diabetes"
      ];
      const state = states[Math.floor(Math.random() * states.length)];
      if (state === "normal") {
        this.target_hr = 75.0; this.target_spo2 = 98.0; this.target_sys = 120.0; this.target_dia = 80.0;
      } else if (state === "asthma") {
        this.target_hr = 115.0; this.target_spo2 = 88.0; this.target_sys = 135.0; this.target_dia = 85.0;
      } else if (state === "hypertension") {
        this.target_hr = 90.0; this.target_spo2 = 97.0; this.target_sys = 175.0; this.target_dia = 105.0;
      } else if (state === "heart_disease") {
        this.target_hr = 135.0; this.target_spo2 = 91.0; this.target_sys = 150.0; this.target_dia = 95.0;
      } else if (state === "diabetes") {
        this.target_hr = 85.0; this.target_spo2 = 96.0; this.target_sys = 140.0; this.target_dia = 90.0;
      }
    }
  }

  private stepPhysics() {
    // Interpolate towards target with noise
    this.hr += (this.target_hr - this.hr) * 0.12 + (Math.random() * 4 - 2);
    this.spo2 += (this.target_spo2 - this.spo2) * 0.12 + (Math.random() * 0.8 - 0.4);
    this.sys_bp += (this.target_sys - this.sys_bp) * 0.12 + (Math.random() * 2 - 1);
    this.dia_bp += (this.target_dia - this.dia_bp) * 0.12 + (Math.random() * 2 - 1);
    this.temp = Math.max(36.0, Math.min(39.2, this.temp + (Math.random() * 0.2 - 0.1)));

    // Clamping to physiologically valid ranges
    this.hr = Math.max(50.0, Math.min(180.0, this.hr));
    this.spo2 = Math.max(78.0, Math.min(100.0, this.spo2));
    this.sys_bp = Math.max(85.0, Math.min(210.0, this.sys_bp));
    this.dia_bp = Math.max(55.0, Math.min(130.0, this.dia_bp));
  }

  public getSnapshot(): PatientVitals {
    return {
      Heart_Rate: Number(this.hr.toFixed(1)),
      SpO2_Level: Number(this.spo2.toFixed(1)),
      Systolic_BP: Number(this.sys_bp.toFixed(1)),
      Diastolic_BP: Number(this.dia_bp.toFixed(1)),
      Body_Temp: Number(this.temp.toFixed(1))
    };
  }

  public tick(): TelemetryPacket {
    this.shiftState();
    this.stepPhysics();
    const sensor_data = this.getSnapshot();
    const prediction = evaluateEdge(sensor_data);

    const packet: TelemetryPacket = {
      sensor_data,
      prediction: {
        is_at_risk: prediction.is_at_risk,
        label: prediction.label,
        confidence: prediction.confidence,
        disease_probs: prediction.disease_probs,
        inference_latency_us: prediction.latency_us
      },
      runtime_mode: "EDGE_WASM_BROWSER"
    };

    return packet;
  }

  public subscribe(fn: (packet: TelemetryPacket) => void) {
    this.listeners.push(fn);
    if (!this.timer && typeof window !== "undefined") {
      this.timer = setInterval(() => {
        const p = this.tick();
        this.listeners.forEach(cb => cb(p));
      }, 1000);
    }
    return () => {
      this.listeners = this.listeners.filter(cb => cb !== fn);
      if (this.listeners.length === 0 && this.timer) {
        clearInterval(this.timer);
        this.timer = null;
      }
    };
  }
}

// Global singleton instance for the browser
export const globalSimulator = new BrowserTelemetrySimulator();