File size: 5,750 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
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
import { PatientVitals } from "./types";

export type SerialConnectionStatus = "idle" | "connected" | "loopback" | "error" | "unsupported";

export interface SerialBridgeHandle {
  disconnect: () => void;
  mode: "hardware" | "loopback";
}

export function isWebSerialSupported(): boolean {
  return typeof window !== "undefined" && "serial" in navigator;
}

/**
 * Parses raw serial packet lines emitted from microcontrollers running PlatformIO firmware.
 * Supports both JSON format and standard NMEA-style telemetry: $HR,SPO2,SYS,DIA,TEMP*CHECKSUM
 */
export function parseSerialPacket(line: string): PatientVitals | null {
  const trimmed = line.trim();
  if (!trimmed) return null;

  // 1. JSON Telemetry Frame
  if (trimmed.startsWith("{") && trimmed.endsWith("}")) {
    try {
      const parsed = JSON.parse(trimmed);
      if (
        typeof parsed.Heart_Rate === "number" ||
        typeof parsed.hr === "number"
      ) {
        return {
          Heart_Rate: parsed.Heart_Rate ?? parsed.hr ?? 75,
          SpO2_Level: parsed.SpO2_Level ?? parsed.spo2 ?? 98,
          Systolic_BP: parsed.Systolic_BP ?? parsed.sys ?? 120,
          Diastolic_BP: parsed.Diastolic_BP ?? parsed.dia ?? 80,
          Body_Temp: parsed.Body_Temp ?? parsed.temp ?? 37.0,
        };
      }
    } catch {
      // not JSON
    }
  }

  // 2. Comma-Delimited or NMEA-style line: $HR,SPO2,SYS,DIA,TEMP
  const cleanLine = trimmed.startsWith("$") ? trimmed.substring(1) : trimmed;
  const noChecksum = cleanLine.split("*")[0];
  const parts = noChecksum.split(",").map((s) => parseFloat(s.trim()));

  if (parts.length >= 5 && parts.every((p) => !isNaN(p))) {
    return {
      Heart_Rate: parts[0],
      SpO2_Level: parts[1],
      Systolic_BP: parts[2],
      Diastolic_BP: parts[3],
      Body_Temp: parts[4],
    };
  }

  return null;
}

interface SerialPortLike {
  open: (options: { baudRate: number }) => Promise<void>;
  close: () => Promise<void>;
  readable: ReadableStream<BufferSource>;
}

/**
 * Connects to physical microcontroller via Chromium Web Serial API at 115,200 baud.
 */
export async function connectWebSerial(
  onPacket: (vitals: PatientVitals) => void,
  onStatusChange: (status: SerialConnectionStatus, message: string) => void
): Promise<SerialBridgeHandle> {
  if (!isWebSerialSupported()) {
    onStatusChange("unsupported", "Web Serial API not supported in this browser. Engage Silicon Loopback instead.");
    throw new Error("Web Serial API unsupported");
  }

  try {
    const navSerial = (navigator as unknown as { serial: { requestPort: () => Promise<SerialPortLike> } }).serial;
    const port = await navSerial.requestPort();
    await port.open({ baudRate: 115200 });

    onStatusChange("connected", "ESP32_XTENSA_ONLINE // 115,200 BAUD // UART_STREAM_ACTIVE");

    let isReading = true;
    const decoder = new TextDecoderStream();
    const readableStreamClosed = port.readable.pipeTo(decoder.writable);
    const reader = decoder.readable.getReader();

    let buffer = "";

    const readLoop = async () => {
      try {
        while (isReading) {
          const { value, done } = await reader.read();
          if (done) break;
          if (value) {
            buffer += value;
            const lines = buffer.split("\n");
            buffer = lines.pop() || "";
            for (const line of lines) {
              const vitals = parseSerialPacket(line);
              if (vitals) {
                onPacket(vitals);
              }
            }
          }
        }
      } catch (err: unknown) {
        if (isReading) {
          const errMsg = err instanceof Error ? err.message : String(err);
          onStatusChange("error", `UART Stream read fault: ${errMsg}`);
        }
      }
    };

    readLoop();

    const disconnect = async () => {
      isReading = false;
      try {
        await reader.cancel();
        await readableStreamClosed.catch(() => {});
        await port.close();
      } catch {
        // catch close errors
      }
      onStatusChange("idle", "DISCONNECTED");
    };

    return { disconnect, mode: "hardware" };
  } catch (err: unknown) {
    const errMsg = err instanceof Error ? err.message : String(err);
    onStatusChange("error", `Web Serial failed to open: ${errMsg}`);
    throw err;
  }
}

/**
 * Silicon Loopback Simulator: Generates physical UART byte streams for test environments without hardware.
 */
export function startSiliconLoopback(
  onPacket: (vitals: PatientVitals) => void,
  onStatusChange: (status: SerialConnectionStatus, message: string) => void
): SerialBridgeHandle {
  onStatusChange("loopback", "SILICON_LOOPBACK_BRIDGE_ACTIVE // ESP32_VIRTUAL_COM1 // 115,200 BAUD");

  let hr = 76.0;
  let spo2 = 98.2;
  let sys = 122.0;
  let dia = 81.0;
  let temp = 37.1;

  const timer = setInterval(() => {
    // Generate realistic sensor telemetry with sensor noise
    hr += (Math.random() - 0.48) * 3;
    spo2 += (Math.random() - 0.49) * 0.6;
    sys += (Math.random() - 0.48) * 2;
    dia += (Math.random() - 0.48) * 1.5;
    temp += (Math.random() - 0.5) * 0.1;

    hr = Math.max(55, Math.min(160, hr));
    spo2 = Math.max(82, Math.min(100, spo2));
    sys = Math.max(90, Math.min(190, sys));
    dia = Math.max(60, Math.min(115, dia));
    temp = Math.max(36.1, Math.min(39.0, temp));

    const vitals: PatientVitals = {
      Heart_Rate: Number(hr.toFixed(1)),
      SpO2_Level: Number(spo2.toFixed(1)),
      Systolic_BP: Number(sys.toFixed(1)),
      Diastolic_BP: Number(dia.toFixed(1)),
      Body_Temp: Number(temp.toFixed(1)),
    };

    onPacket(vitals);
  }, 1000);

  const disconnect = () => {
    clearInterval(timer);
    onStatusChange("idle", "DISCONNECTED");
  };

  return { disconnect, mode: "loopback" };
}