Back to List

Real-time Equipment Dashboard: Streaming incubator status via WebSocket.

A dashboard that visualizes real-time data streams from equipment such as incubators, PCR machines, and flow cytometers. It is built using a Python WebSocket server, a React client, and a circular buffer to maintain a rolling window of recent data.

Intermediate
|
100min
|
Verified (2026-07)
Real-time dashboard.IncubatorTemperature, carbon dioxide, and pH.WebSocketEvent streaming.Experiment monitoring.Time series buffer.
Progress0/19 (0%)

Real-time Equipment Dashboard โ€” Streaming Incubator Status with WebSockets

After Completing This Topic

By combining the WebSockets, React state, and time-series buffers learned in the textbook, you will be able to create a dashboard that displays the temperature, CO2, and pH values emitted every second by a cell culture incubator in real-time on a browser. You, who only knew about HTTP requests and responses, will enter a world where you handle data pushed by the server.

This article is a general educational example. Cell culture monitoring is a common management task in laboratories, so it was chosen as the subject.

"I Found Out the Next Morning" โ€“ The Limitations of Polling

Imagine you have a precious cell line in a cell culture incubator. The CO2 concentration in the incubator needs to be maintained at 5%. One night, someone accidentally leaves the door slightly ajar, causing the CO2 level to plummet. The next morning, you discover that all the cells have died.

To prevent such an incident, real-time monitoring is necessary: a system that "refreshes values every second and immediately alerts you if something is wrong."

Your first attempt might look like this:

javascript
// Naive polling approach
setInterval(async () => {
  const res = await fetch("/api/incubator");
  const data = await res.json();
  updateChart(data);
}, 1000);

It queries the server every second. This approach works, but it has significant drawbacks.

  • Network Overhead: Every second, there's an HTTP request + response header exchange. Most of these requests are wasted because there's "no new data."
  • Latency: Even if the incubator detected a sudden CO2 drop 0.1 seconds ago, your next polling interval (up to 1 second later) will still display the normal value on the screen.
  • Scalability Issues: Monitoring 5 incubators means 5 requests per second. As you scale up, the server can be overwhelmed by the polling load.

WebSocket solves these problems. Instead of request/response round trips, it enables the server to push data to the client whenever new data is available. In this article, we'll build one from scratch.

Let's See the Finished Product First (Run the Black Box First)

The system we're building will look like this:

text
โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”          WebSocket           โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
โ”‚  Python Server   โ”‚  โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•  โ”‚  React Browser   โ”‚
โ”‚  (Mock Incubator) โ”‚  {temp, co2, pH, timestamp}  โ”‚  (Dashboard)       โ”‚
โ”‚                โ”‚  โ† Pushed multiple times per second โ”‚                  โ”‚
โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜                              โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜

The server pushes messages like this multiple times per second:

json
{"temp": 37.02, "co2": 4.98, "pH": 7.35, "timestamp": 1720000000.5}
{"temp": 37.03, "co2": 4.97, "pH": 7.35, "timestamp": 1720000000.7}
{"temp": 37.01, "co2": 4.98, "pH": 7.36, "timestamp": 1720000000.9}

The browser receives these values and maintains a 60-second rolling window, displaying three line graphs in real-time. If the CO2 level goes outside the danger range (4.5 to 5.5), a warning banner appears at the top of the screen.

Without polling, without latency, the screen reacts as soon as the server generates the data.


What components does this tool consist of (component breakdown)?

text
Real-time Equipment Dashboard (Full Stack)
   โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
   โ”‚                                                        โ”‚
   โ”‚   [Server Side]                     [Client Side]    โ”‚
   โ”‚                                                        โ”‚
   โ”‚   โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”              โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”  โ”‚
   โ”‚   โ”‚ Incubator Mock Data    โ”‚              โ”‚ WebSocket Connection  โ”‚  โ”‚
   โ”‚   โ”‚ (initial full)   โ”‚              โ”‚ (receive โ†’ React) โ”‚  โ”‚
   โ”‚   โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜              โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜  โ”‚
   โ”‚             โ”‚                                โ”‚          โ”‚
   โ”‚             โ–ผ                                โ–ผ          โ”‚
   โ”‚   โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”              โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”  โ”‚
   โ”‚   โ”‚ WebSocket Server   โ”‚ โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ–บ    โ”‚ Time Series Buffer      โ”‚  โ”‚
   โ”‚   โ”‚ Component: websocket   โ”‚              โ”‚ (last 60 seconds)      โ”‚  โ”‚
   โ”‚   โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜              โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜  โ”‚
   โ”‚       โ˜… Built from scratch                          โ”‚          โ”‚
   โ”‚                                              โ–ผ          โ”‚
   โ”‚                                    โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”  โ”‚
   โ”‚                                    โ”‚ React Graph    โ”‚  โ”‚
   โ”‚                                    โ”‚ + Alert Banner     โ”‚  โ”‚
   โ”‚                                    โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜  โ”‚
   โ”‚                                        โ˜… Built from scratch     โ”‚
   โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜
ComponentLearned fromFunction in this tool
HTML/CSS/React basicshtml-css-react-basicsPage structure and component rendering
WebSocketwebsocket-basicsServer โ†’ client push communication
React statereact-stateConnects pushed data to the screen
Time series buffertime-series-bufferMaintains only the recent window, discards old values

๐Ÿ“Œ If you are unfamiliar with these concepts (links at the top)

The new concepts that are built from scratch are WebSocket server, React state, and circular time series buffer. The basic HTML/CSS structure is provided as a complete tool. There are only three โ€” within the limits of cognitive capacity.

Step 1: Create a Mock Incubator Server โ˜… (WebSocket Server)

โœ๏ธ Fill-in-the-blank section. Component = WebSocket server (Python). The mock incubator will push state data multiple times per second.

We will use the websockets library in Python. This is a complete server that can be run locally.

python
# server.py
import asyncio
import json
import time
import random
import websockets # pip install websockets
class MockIncubator:
"""Mock incubator โ€“ simulates temperature/CO2/pH with some noise."""
def __init__(self, target_temp=37.0, target_co2=5.0, target_ph=7.35):
self.target_temp = target_temp
self.target_co2 = target_co2
self.target_ph = target_ph
# Simulate a slightly open door
self.door_open = False
def sample(self) -> dict:
# Normal state: target value ยฑ small noise
temp_noise = 0.15
co2_noise = 0.10 if not self.door_open else 0.8
ph_noise = 0.02
return {
"temp": round(self.target_temp + random.gauss(0, temp_noise), 3),
"co2": round(self.target_co2 + random.gauss(0, co2_noise), 3),
"pH": round(self.target_ph + random.gauss(0, ph_noise), 3),
"timestamp": round(time.time(), 3),
"door_open": self.door_open,
}
incubator = MockIncubator()
async def stream_handler(websocket):
"""This function is called for each new client connection."""
print(f"[+] Client connected: {websocket.remote_address}")
try:
while True:
data = incubator.sample()
await websocket.send(json.dumps(data))
await asyncio.sleep(0.5) # Push data twice per second
except websockets.ConnectionClosed:
print("[-] Client disconnected")
async def main():
async with websockets.serve(stream_handler, "localhost", 8765):
print("Server started: ws://localhost:8765")
await asyncio.Future() # forever
if __name__ == "__main__":
asyncio.run(main())

In this code, the critical point is the send and sleep combination inside the while True loop. In each iteration, a new sample is created and pushed to the client. It doesn't wait for requests like HTTP.

Verify with a local test:

python
# test_server.py โ€“ Verify that the server is pushing data by mimicking a client
import asyncio
import json
import websockets
async def sanity_check():
async with websockets.connect("ws://localhost:8765") as ws:
# Receive 5 messages
messages = []
for _ in range(5):
raw = await ws.recv()
messages.append(json.loads(raw))
# Verification
assert len(messages) == 5
for m in messages:
assert set(m.keys()) >= {"temp", "co2", "pH", "timestamp"}
assert 35 < m["temp"] < 39 # Normal range
assert 3 < m["co2"] < 7
assert 6.8 < m["pH"] < 8.0
# Timestamps should increase chronologically
times = [m["timestamp"] for m in messages]
assert all(times[i] <= times[i+1] for i in range(len(times)-1))
# asyncio.run(sanity_check()) # Run when the server is running

๐Ÿ”Ž How is WebSocket different from polling (drawing โ€” websocket-basics)? HTTP repeats request โ†’ response โ†’ connection close each time. WebSocket is a bidirectional channel that remains connected indefinitely. The server can push data whenever it wants, and the client can send data whenever it wants. It is the standard for chat, games, and real-time monitoring.

๐Ÿค” Self-explanatory prompt I intentionally put await asyncio.sleep(0.5) inside the while True loop. What will happen if you remove it? What will be the CPU usage of the server? How many messages per second will the client receive? (Hint: without sleep, CPU will be 100% + thousands of messages per millisecond)


Step 2: Build a Time-Series Buffer โ˜…

โœ๏ธ A self-contained section. Component = Circular Time-Series Buffer. It keeps only the most recent N points and automatically discards older values.

Real-time dashboards do not need to store all historical data. It's sufficient to look at the latest 60-second window. If data arrives at 2 points per second, a 60-second window equals a maximum of 120 points.

The simplest approach: continuously push data into a JavaScript array and occasionally trim the beginning using shift.

javascript
// Naive buffer - has issues
let buffer = [];
function push(point) {
  buffer.push(point);
  if (buffer.length > 120) buffer.shift();
}

This approach works, but it has a problem. Array.shift() is O(n) โ€“ removing an element from the beginning of the array causes all subsequent elements to shift one position. As the data ages, the O(n) shift cost accumulates with each push.

We achieve O(1) push/pop with a circular buffer.

๐Ÿ”Ž What is a circular buffer (drawer โ€” time-series-buffer)? It uses a fixed-size array like a circle. A single write index tracks where to write the next value. When the end of the array is reached, it wraps around to the beginning, overwriting older values. This structure makes both push and pop O(1). It's frequently used in audio processing, log rings, and real-time graphs.

typescript
// buffer.ts
export class TimeSeriesBuffer<T> {
  private data: (T | null)[];
  private writeIdx = 0;
  private size = 0;

  constructor(public capacity: number) {
    this.data = new Array(capacity).fill(null);
  }

  push(value: T): void {
    this.data[this.writeIdx] = value;
    this.writeIdx = (this.writeIdx + 1) % this.capacity;
    this.size = Math.min(this.size + 1, this.capacity);
  }

  toArray(): T[] {
    // Return an array sorted in time order
    if (this.size < this.capacity) {
      // Not yet full - return elements from the beginning up to size
      return this.data.slice(0, this.size) as T[];
    }
    // Full - wrap around and return from writeIdx to the end
    return [
      ...this.data.slice(this.writeIdx),
      ...this.data.slice(0, this.writeIdx),
    ] as T[];
  }
}

Verification:

typescript
// buffer.test.ts
import { TimeSeriesBuffer } from "./buffer";

const buf = new TimeSeriesBuffer<number>(5);
[1, 2, 3].forEach(v => buf.push(v));
console.assert(JSON.stringify(buf.toArray()) === "[1,2,3]");

[4, 5].forEach(v => buf.push(v));
console.assert(JSON.stringify(buf.toArray()) === "[1,2,3,4,5]");

// Exceed capacity - overwrite older values
[6, 7].forEach(v => buf.push(v));
console.assert(JSON.stringify(buf.toArray()) === "[3,4,5,6,7]");

We replaced the O(n) shift() with an O(1) writeIdx increment. The push cost remains constant, even as the data ages.

๐Ÿค” Self-explanatory prompt: Why is toArray() O(n)? Push is O(1), so why is the return O(n)? Will this be a problem in a real-time graph rendering scenario? (Hint: Rendering at 60Hz means O(120) per frame is negligible.)

Step 3: Building the React State Hook โ˜… (react-state)

โœ๏ธ Fill-in section. Component = React state + useEffect. Store values pushed from the WebSocket in a buffer and reflect them on the screen.

When dealing with real-time data in React, the key principle is the combination of useState + useEffect.

  • useState: A snapshot of the "current window" to be rendered on the screen.
  • useEffect: Manages the lifecycle of WebSocket connections and disconnections.
typescript
// useIncubatorStream.ts
import { useEffect, useState, useRef } from "react";
import { TimeSeriesBuffer } from "./buffer";

interface Sample {
  temp: number;
  co2: number;
  pH: number;
  timestamp: number;
  door_open?: boolean;
}

export function useIncubatorStream(wsUrl: string, windowSize: number = 120) {
  const [samples, setSamples] = useState<Sample[]>([]);
  const [connected, setConnected] = useState(false);
  const bufferRef = useRef(new TimeSeriesBuffer<Sample>(windowSize));

  useEffect(() => {
    const ws = new WebSocket(wsUrl);

    ws.onopen = () => {
      console.log("WebSocket connected");
      setConnected(true);
    };

    ws.onmessage = (event) => {
      const data: Sample = JSON.parse(event.data);
      bufferRef.current.push(data);
      // Pass the new snapshot to the React state
      setSamples(bufferRef.current.toArray());
    };

    ws.onclose = () => {
      console.log("WebSocket closed");
      setConnected(false);
    };

    // Cleanup: Close the connection when the component unmounts
    return () => {
      ws.close();
    };
  }, [wsUrl, windowSize]);

  return { samples, connected };
}

The key features of this hook are:

  • bufferRef (useRef): The circular buffer should be maintained independently of component re-renders. Wrap it with useRef.
  • setSamples: Returns a new array with each message, causing React to re-render. The array reference must change for React to detect the change.
  • Cleanup function: Always call ws.close() when the component disappears. Otherwise, zombie connections will accumulate.

๐Ÿ”Ž When to use useRef vs. useState (drawer โ€” react-state) Use useState if it affects the screen (re-renders on change). If only internal calculations or reference maintenance are needed, use useRef (does not re-render on change). Use useRef for the circular buffer itself, and useState for the snapshot, which is the target of the graph rendering โ€“ this separation is key to performance.

๐Ÿค” Self-explanatory prompt What would happen if you simply placed bufferRef inside the component as let buffer = new TimeSeriesBuffer(...)? (Hint: A new buffer would be created with each render, and all past data would be lost.)


Step 4: Building the Dashboard UI โ˜…

Now for the final layer: rendering the samples from the hook as actual graphs and warning banners.

typescript
// IncubatorDashboard.tsx
import { useIncubatorStream } from "./useIncubatorStream";

function isCritical(latest: any): string | null {
  if (!latest) return null;
  if (latest.co2 < 4.5 || latest.co2 > 5.5) return `CO2 Alert: ${latest.co2}%`;
  if (latest.temp < 36.5 || latest.temp > 37.5) return `Temperature Alert: ${latest.temp}ยฐC`;
  if (latest.pH < 7.2 || latest.pH > 7.5) return `pH Alert: ${latest.pH}`;
  return null;
}

export function IncubatorDashboard() {
  const { samples, connected } = useIncubatorStream("ws://localhost:8765");
  const latest = samples[samples.length - 1];
  const warning = isCritical(latest);

  return (
    <div className="dashboard">
      <header>
        <h1>Incubator A</h1>
        <span className={connected ? "connected" : "disconnected"}>
          {connected ? "Connected Live" : "Disconnected"}
        </span>
      </header>

      {warning && (
        <div className="warning-banner">
          โš ๏ธ Alert: {warning}
        </div>
      )}

      <section className="metrics">
        <MetricCard label="Temperature" value={latest?.temp} unit="ยฐC" target={37.0} />
        <MetricCard label="CO2" value={latest?.co2} unit="%" target={5.0} />
        <MetricCard label="pH" value={latest?.pH} unit="" target={7.35} />
      </section>

      <section className="charts">
        <LineChart data={samples} field="temp" color="#ff6b6b" />
        <LineChart data={samples} field="co2" color="#4ecdc4" />
        <LineChart data={samples} field="pH" color="#95e1d3" />
      </section>
    </div>
  );
}

MetricCard and LineChart are standard components, so the detailed implementation is omitted. The key point is that this single hook serves as the data source for the entire dashboard.

Validation (Integration Test Overview):

typescript
// integration.test.ts (Playwright/Cypress integration test concept)
async function test_dashboard_flow() {
  // 1. Start the server (test fixture)
  // 2. Load the dashboard in the browser
  // 3. Within 5 seconds, at least 5 data points should appear in the graph
  await waitFor(() => expect(chart.pointCount()).toBeGreaterThan(5));
  // 4. The WebSocket connection should be alive
  expect(document.querySelector(".connected")).toBeInTheDocument();
  // 5. If we artificially change the state to door_open=true, the warning banner should appear
  await triggerDoorOpen(server);
  await waitFor(() => expect(document.querySelector(".warning-banner")).toBeInTheDocument());
}

Combining the Pieces โ€” A Complete Dashboard Architecture

Here's how the entire system fits together:

text
Server-Side (Python):
  MockIncubator โ†’ Repeatedly calls sample() โ†’ WebSocket.send()
  File: server.py

Client-Side (React/TypeScript):
  useIncubatorStream(wsUrl)
    โ”œโ”€โ”€ WebSocket connection (useEffect)
    โ”œโ”€โ”€ TimeSeriesBuffer (useRef, O(1) push)
    โ””โ”€โ”€ samples state (useState, triggers re-render)

  IncubatorDashboard
    โ”œโ”€โ”€ Displays connection status
    โ”œโ”€โ”€ Warning banner (isCritical function)
    โ”œโ”€โ”€ 3 MetricCards (current value)
    โ””โ”€โ”€ 3 LineCharts (60-second window)

  Files: useIncubatorStream.ts, buffer.ts, IncubatorDashboard.tsx

This tool is a miniature version of the real-time dashboards you might see today in industrial SCADA (Supervisory Control and Data Acquisition) systems, laboratory LIMS (Laboratory Information Management Systems), and more. A production-ready tool would simply add features like authentication, multi-device routing, time-series DB storage (InfluxDB), and notification routing (Slack/email).

Performance Deep Dive โ€” Why This Architecture?

text
Polling method (fetch every 1 second):
  - Server load: N clients ร— 1 request per second = N req/sec
  - Latency: Average 0.5 seconds (half of the polling interval)
  - Bandwidth: HTTP header overhead for each request (~500 bytes)

WebSocket method (continuous push):
  - Server load: Constant maintenance for each connection (1 socket, TCP maintenance)
  - Latency: ~Milliseconds (only network round trip)
  - Bandwidth: Only payload, no header (~80 bytes)

Compared numerically:

ScenarioPolling (1-second interval)WebSocket
100 clients, requests per second100 req/sec0 req/sec
Anomaly detection latency (average)500ms~10ms
Payload bandwidth (per second)100 ร— 500B = 50KB/s100 ร— 80B ร— 2 = 16KB/s

Anomaly detection latency is 50 times faster, and bandwidth is 1/3. The gap widens as more devices are scaled.


There Are Other Paths (Multipath Reflection)

  • Server-Sent Events (SSE): A simpler alternative to WebSockets. It supports only unidirectional communication from server to client, but it can easily pass through proxies and firewalls by being built on top of HTTP. If the client doesn't need to send commands to the server (e.g., simple monitoring), SSE is sufficient.
  • MQTT Broker: An IoT standard protocol. Connect multiple devices to a broker and have the dashboard subscribe to the broker as well. This is a better solution if you have dozens or hundreds of devices. Open-source options include Mosquitto and HiveMQ.
  • Use a Time Series Database: Real-time dashboards alone don't provide historical logs. By recording data simultaneously in InfluxDB, TimescaleDB, or Prometheus, you can enable both real-time views and historical analysis.
  • Reconnect Logic: Our hook does not reconnect if the server connection is lost. In practice, it is essential to have reconnection logic with exponential backoff. Place the retry schedule inside ws.onclose.
  • Expanding Alert Delivery: Displaying alerts only on the screen banner makes it impossible to respond to incidents overnight. To be truly useful, you need to push alerts to external channels such as Slack webhooks, SMS, or PagerDuty.

Key takeaway: "The server pushes data, the buffer maintains the window, and the state connects the screen." If each of these three layers knows its role, you can see the architecture of a real-time system. What you just created is the embodiment of that architecture.

Next Steps (Links at the bottom)

Try It Yourself (Independent Exercises)

  1. Reconnect Logic: Add logic to the hook that, when a WebSocket connection is dropped, retries the connection after 1 second, 2 seconds, 4 seconds, and 8 seconds (exponential backoff).
  2. Multiple Devices: Expand the dashboard to monitor 5 incubators. Each device should have its own separate WebSocket connection. Observe where the performance bottleneck arises.
  3. Downsampling: If 20 messages per second are received, you want to limit the screen rendering to 5 times per second. Add debounce/throttle to the hook.
  4. Challenge: SSE Migration: Reimplement the same system using Server-Sent Events instead of WebSockets. How does the code complexity change?

Summary

We tackled the problem of "instantly reflecting real-time data emitted by equipment on the screen" by breaking it down into three components.

  • WebSocket opened a server-push communication channel (no polling).
  • A circular time-series buffer maintained the most recent window with O(1) push operations.
  • React state (+ useRef) served as the bridge to accurately connect the data flow and rendering.

The values emitted by the incubator every second now flow to your screen with millisecond latency. The days of discovering an incident the next morning are over. An alert appears on the screen within 5 seconds of the incident.

This article is a general educational example. A real-world laboratory dashboard would add authentication, multi-equipment routing, time-series databases, and notification systems to this. A more detailed version can be built on top of this framework, or you can rely on proven SCADA/LIMS tools.

๐Ÿ’ฌ Questions & Comments

0 comments

You can post without signing in. Guest comments cannot be edited or deleted by their author.

0/2000

Loading...