element14 Community
element14 Community
    Register Log In
  • Site
  • Search
  • Log In Register
  • Community Hub
    Community Hub
    • What's New on element14
    • Feedback and Support
    • Benefits of Membership
    • Personal Blogs
    • Members Area
    • Achievement Levels
  • Learn
    Learn
    • Ask an Expert
    • eBooks
    • element14 presents
    • Learning Center
    • Tech Spotlight
    • STEM Academy
    • Webinars, Training and Events
    • Learning Groups
  • Technologies
    Technologies
    • 3D Printing
    • FPGA
    • Industrial Automation
    • Internet of Things
    • Power & Energy
    • Sensors
    • Technology Groups
  • Challenges & Projects
    Challenges & Projects
    • Design Challenges
    • element14 presents Projects
    • Project14
    • Arduino Projects
    • Raspberry Pi Projects
    • Project Groups
  • Products & Partners
    Products & Partners
    • Arduino
    • Avnet & Tria Boards Community
    • Dev Tools
    • Manufacturers
    • Multicomp Pro
    • Product Groups
    • Raspberry Pi
    • RoadTests & Reviews
  • About Us
    About the element14 Community
  • Store
    Store
    • Visit Your Store
    • Choose another store...
      • Europe
      •  Austria (German)
      •  Belgium (Dutch, French)
      •  Bulgaria (Bulgarian)
      •  Czech Republic (Czech)
      •  Denmark (Danish)
      •  Estonia (Estonian)
      •  Finland (Finnish)
      •  France (French)
      •  Germany (German)
      •  Hungary (Hungarian)
      •  Ireland
      •  Israel
      •  Italy (Italian)
      •  Latvia (Latvian)
      •  
      •  Lithuania (Lithuanian)
      •  Netherlands (Dutch)
      •  Norway (Norwegian)
      •  Poland (Polish)
      •  Portugal (Portuguese)
      •  Romania (Romanian)
      •  Russia (Russian)
      •  Slovakia (Slovak)
      •  Slovenia (Slovenian)
      •  Spain (Spanish)
      •  Sweden (Swedish)
      •  Switzerland(German, French)
      •  Turkey (Turkish)
      •  United Kingdom
      • Asia Pacific
      •  Australia
      •  China
      •  Hong Kong
      •  India
      •  Japan
      •  Korea (Korean)
      •  Malaysia
      •  New Zealand
      •  Philippines
      •  Singapore
      •  Taiwan
      •  Thailand (Thai)
      •  Vietnam
      • Americas
      •  Brazil (Portuguese)
      •  Canada
      •  Mexico (Spanish)
      •  United States
      Can't find the country/region you're looking for? Visit our export site or find a local distributor.
  • Translate
  • Profile
  • Settings
Smart Home & Healthcare Challenge
  • Challenges & Projects
  • Design Challenges
  • Smart Home & Healthcare Challenge
  • More
  • Cancel
Smart Home & Healthcare Challenge
Forum Palno & Slemon #5 LabView Integration
  • News
  • Projects
  • Forum
  • Details
  • Leaderboard
  • Files
  • Members
  • DC
  • More
  • Cancel
  • New
Join Smart Home & Healthcare Challenge to participate - click to join for free!
Actions
  • Share
  • More
  • Cancel
Forum Thread Details
  • Replies 0 replies
  • Subscribers 17 subscribers
  • Views 54 views
  • Users 0 members are here
  • esp32
  • labview
  • automation
Related

Palno & Slemon #5 LabView Integration

Qbit
Qbit 2 days ago

 I hope all's good. Today was miserable for me. I had to work from the very morning to now. It's almost night and I only go to eat. Otherwise I stay at my desk only to work. I have gained a lot of precious experience from this: I should never take the deadline as a joke.

  but I have to make some changes from where I am so I can implement that. That took me a while and understanding LabVIEW's new architecture was also a little easy. I was completely new so it took me some time but it was fairly easier than any other software I will be writing this using Wispr Flow because I am very tired.

 I had taken many revisions, like five revisions, to completely come to this stage when all things sync perfectly with each other and work excellently.  thank you for staying to see my journey till here. If you are new and seeing this, you can also read my past posts here:

Past Posts

Palno & Slemon #1 Introduction

Palno & Slemon #2.1 Configuring and Validating

Palno & Slemon #2.2 Configuring and Validating

Palno & Slemon #3 Building the Algorithm

Palno & Slemon #4 Final Firmware

By the way I was working on a project that helps us wake up at the right time so we aren't in the stage that could affect our energy level and we feel tired. It also manages the time we have to wake up. We set it for the right time as per the data it gets from our sleep. If you set it to 7:00 and the algorithm detects that you are partially awake around 6:00, it will wake you up at that time. You will feel fresh and wake up at the right time so you don't have to compromise time with health. 

It works by seeing your blood oxygen level (the SpO2), the BPM, and the movement you make while you are resting. I have also added temperature control for others, like the temperature of your room, so you are better comfortable and it will be easy for you to fall asleep. I'm also using speakers to help you sleep properly with the therapy of binaural beats. 

The Goal

While the Uno Q runs a built-in web dashboard on port 7000 for everyday consumer check-ins, medical and clinical research environments have completely different demands. A clinician, sleep doctor, or biomedical engineer doesn't want a consumer mobile layout; they need:

  1. High-Frequency Multi-Parameter Waveform Plotting: Continuous, uninterrupted real-time trend charts of heart rate variability, SpO2 saturation, respiration rate, and 3-axis gravitational acceleration vectors.
  2. Absolute Determinism: No browser rendering lag, no dropped samples, and zero jitter.
  3. Medical-Grade Telemetry Logging: Direct, uncorrupted streaming to CSV/spreadsheet files on a PC for post-hoc polysomnography (PSG) comparison and statistical scoring.
  4. Instant Visual State Annunciation: Clear LED clusters and numerical readouts showing whether the patient is in Deep Sleep, REM, or Restless Wakefulness.

National Instruments LabVIEW is the gold standard for clinical data acquisition and biomedical instrumentation. My goal for this phase was simple but ambitious: create a high-speed, wireless clinical telemetry dashboard in LabVIEW that ingests our full multi-sensor ecosystem at 10 Hz over the network, plots every vital sign with zero lag, and automatically logs complete datasets to disk.

Before diving in, I was completely new to LabVIEW’s modern networking architecture. It seemed daunting at first, but once I grasped dataflow programming and how LabVIEW handles TCP primitives, it turned out to be far more intuitive, modular, and visual than traditional text-based charting libraries.

The Architectural Leap: Wi-Fi TCP/IP Streaming vs. Serial Cables

When developing embedded projects, the standard, instinctive fallback is to use Serial (UART/COM port) communication. Plug a USB cable from the board to your PC, open a COM port at 115200 baud, and print CSV strings.

Early on, I realized that for a bedside sleep-monitoring system, relying on a Serial cable was a fatal design flaw:

  1. Tethering a Sleeping Person: A sleep monitor cannot have physical cables running across the bedroom to a laptop. The bedside hub must live independently by the bed, while the monitoring PC could be across the room or at a nurse's workstation.
  2. Serial Contention on Arduino Uno Q: The Uno Q's internal architecture relies heavily on serial/bridge routing between the STM32 microcontroller and the Qualcomm Linux MPU via RouterBridge. Trying to tap into raw UART streams while the Linux container was actively using them created bus locks and dropped packets.
  3. Bandwidth & Buffer Bottlenecks: Pushing 20 Hz accelerometer data, 1 Hz biometrics, radar metrics, and sleep stages simultaneously over standard serial lines creates buffer overflow issues when the host GUI stalls for even a millisecond.

The Decision: Dedicated TCP/IP Socket Server on Port 5005

We completely bypassed physical serial communication for the LabVIEW pipeline and moved to wireless TCP/IP streaming:

  • The Uno Q’s Qualcomm MPU spins up a dedicated multi-threaded TCP server listening on 0.0.0.0:5005.
  • The station connects to the local bedroom Wi-Fi network (IP: 192.168.1.12).
  • The monitoring PC running LabVIEW connects wirelessly over standard Wi-Fi directly to 192.168.1.12:5005 (with an optional USB ADB port-forward to 127.0.0.1:5005 for low-latency bench testing).
  • Telemetry is serialized as a lightweight, clean, single-line JSON string terminated with standard \r\n (CRLF) characters, broadcast at a rock-solid 10 Hz.

This gave us complete wireless freedom. The Uno Q can sit anywhere in the room, and any PC running LabVIEW on the local network can open the connection and receive real-time vitals instantly.

How We Implemented the LabVIEW Block Diagram

Detailed Subsystems in the Block Diagram:

  1. Connection Initialization:

    • At the far left of the diagram, a TCP Open Connection function takes the target IP (192.168.1.12 or 127.0.0.1) and Port (5005). A 5000 ms timeout constant prevents the VI from hanging if the bedside station is powered off.
  2. CRLF-Delimited TCP Reading:

    • Inside the main While Loop, we use TCP Read.vi configured in Standard Mode (Mode 1) with a Carriage Return / Line Feed (\r\n) delimiter. Rather than guessing byte lengths or implementing complex byte-header framing, LabVIEW reads until it encounters \n. Because our Python backend appends \r\n to every frame, LabVIEW yields one complete, pristine JSON frame per iteration.
  3. JSON Unflattening via Type Prototype Cluster:

    • The ASCII string output from TCP Read is wired directly into the JSON String terminal of Unflatten From JSON.vi.
    • To the type and defaults terminal, we wired a strict LabVIEW Cluster Constant defining the exact schema:
      • ts (Double-precision float: Epoch timestamp)
      • heart_rate & hr_bpm (32-bit signed integers)
      • hr_valid (Integer boolean flag: 1 = Valid, 0 = Baseline)
      • spo2 (32-bit signed integer: Blood oxygen percentage)
      • spo2_valid (Integer boolean flag)
      • breath_bpm (Single-precision float: Respiration rate)
      • temp_c & room_temp_c (Single-precision float: Ambient temp)
      • disturbance (Single-precision float: 0.0 - 100.0 restlessness)
      • sleep_stage (String: "WAKE", "N1", "N2", "N3", "REM")
      • sleep_stage_numeric (32-bit integer: 0=N3, 1=N2, 2=REM, 3=N1, 4=WAKE)
  4. Telemetry Extraction & Front Panel Indicators:

    • The parsed cluster passes into Unbundle By Name.vi, splitting into dedicated Front Panel channels:
      • Vitals Waveform Charts: Three distinct, rolling visual charts plotting Heart Rate (40–120 BPM), SpO2 (90–100%), and Respiration Rate (0–30 BPM) with 60-second history buffers.
      • 3-Axis Movement Waveform: A multi-plot graph showing accel_x, accel_y, and accel_z. When the user is calm, accel_z sits right near 1.0 G (Earth gravity); when restless, sharp oscillations visually reflect micro-awakenings.
      • Sleep Stage Enum Bar: Mapped from sleep_stage_numeric to show the active hypnogram state in real time.
      • Hardware Diagnostics: LED boolean indicators showing BLE wristband connectivity and alarm status.
  5. Automated Continuous File Logging:

    • Rather than prompting the user with a dialog, the unbundled data is piped into an automated file-writing pipeline that records every sample to a CSV file on disk.

image

The Clinical Mandate: Why National Instruments LabVIEW?

For this specific blog update, I want to direct all attention to the National Instruments LabVIEW portion of the project. While the Arduino Uno Q hosts a built-in web dashboard on port 7000 that works nicely for everyday home monitoring, a clinical healthcare system demands something far more rigorous. In a medical research setting, a hospital sleep clinic, or a biomedical testing laboratory, clinicians and researchers do not want a mobile-friendly browser interface.

They require high-precision continuous waveform visualization, deterministic data acquisition without browser rendering jitter, synchronous multi-channel signal alignment, and direct, uninterrupted file logging into spreadsheets for post-hoc polysomnography validation. National Instruments LabVIEW is the worldwide standard for virtual instrumentation, medical equipment interfaces, and complex automated data logging.

When a sleep specialist reviews patient data, they need to examine the raw morphology of vitals over time. They want to see the exact relationship between an elevation in wrist movement energy and a subsequent drop in blood oxygen saturation, or observe how an abrupt change in respiration rate correlates with an autonomic heart rate spike. Browser-based charting libraries like Chart.js or Canvas are inherently limited by JavaScript's single-threaded event loop and garbage collection pauses, which frequently introduce visual stutter and micro- delays. LabVIEW, on the other hand, runs directly on native hardware execution threads, guaranteeing deterministic waveform rendering that never misses a single incoming frame.

My goal was to turn LabVIEW into the central clinical telemetry workstation for Palno & Slemon. I wanted any monitoring PC on the hospital or laboratory network to wirelessly ingest our complete multi-sensor stream at ten samples per second, display every vital sign on specialized medical charts, and automatically save every single data point to disk without requiring any human operator to intervene.

The Journey Across Five Revisions: From Loose Scripts to Containerized Harmony

Getting to a fully functional system did not happen overnight. In fact, it required five exhaustive architectural revisions, each resolving critical weaknesses discovered during testing. In the very first revision, the system consisted of disparate standalone scripts running directly on the Linux host with manual terminal executions. I had to manually SSH into the board, run an audio test script, open a serial listener for the radar, and manually initiate Bluetooth scanning. It was fragile, crashed whenever a terminal disconnected, and lacked any cohesive orchestration.

In revision two, I attempted to containerize the application within the Arduino App Lab environment. This introduced immediate permission barriers. The Docker container was isolated from the Linux host's D-Bus daemon and Bluetooth subsystems, which meant that our Python code could not see or pair with the Palno ESP32 wristband. We had to spend days exploring socket bridging to expose the system bus into the container space.

Revision three brought the initial implementation of the NI LabVIEW bridge, but it suffered from severe latency and thread contention. Sensor polling, algorithmic sleep stage calculations, and socket broadcasting were all running on the main application thread. Every time an algorithm did heavy computation, the LabVIEW output stalled, and the TCP socket dropped packets.

Revision four resolved the thread blocking by introducing threading workers, but introduced a static networking trap. The systemd services on the host machine were hardcoded to point to container names and IP addresses specific to revision four. The moment I uploaded revision five via App Lab, the entire background forwarding system collapsed because Docker created a new network namespace with different IP addresses.

Finally, in revision five, we achieved a truly production-grade, autonomous architecture. We established dynamic host scripts that auto-detect container IPs, created an isolated ten-Hertz non-blocking telemetry broadcast thread, implemented physiological window filtering on the ESP32 firmware, and engineered zero-touch systemd boot orchestration. Every lesson learned across revisions one through four was consolidated into revision five, creating an ecosystem that starts automatically on boot and streams flawlessly to LabVIEW.

The Nightmare of JSON Unflattening Error Hex 0xFFFA4725

Translating raw embedded data into clean visual streams in LabVIEW took a massive amount of troubleshooting, and I ran headfirst into several severe bugs that tested my patience. The first obstacle appeared the very moment I attempted to parse the network stream, manifesting as the dreaded LabVIEW Error Hex 0xFFFA4725, which reported that the JSON string was invalid and did not conform to the JSON grammar.

When I wired a string indicator to the output of the TCP Read block, the incoming payload appeared truncated, ending abruptly in the middle of a word or bracket. At first, I assumed there was an issue with character encoding or hidden non-printable bytes. I spent hours inspecting string length nodes, checking for null bytes, and experimenting with UTF-8 byte order marks. But upon auditing the Python backend on the Uno Q, I uncovered the real culprit.

In my general web backend, I had included a historical data array called hypnogram inside the JSON packet so the web dashboard could render a twenty- minute sleep stage history. That array contained dozens of nested dictionaries, which inflated the size of each single-line JSON transmission to over 2.5 kilobytes. In streaming socket communication using carriage return and line feed delimiters, sending a 2.5-kilobyte string in a single burst causes network fragmentation across TCP packet boundaries.

When LabVIEW's TCP Read buffer received the incoming stream, it only read the first chunk up to the buffer boundary, resulting in an incomplete, half-formed JSON fragment that lacked closing brackets. LabVIEW received half of the JSON object, couldn't find the closing bracket, and threw a fatal syntax error. The parser simply could not unflatten an open-ended fragment into a strict LabVIEW Type Cluster.

To eliminate this bug, I went back into the Python engine on the Uno Q and engineered a dedicated serialization routine named get_labview_payload. This method completely stripped out all bulky historical arrays, nested dictionaries, and non-essential strings, producing a compact, strictly scalar JSON payload of approximately 390 bytes. I configured the serializer to output pure ASCII characters, explicitly disallowed any NaN or null floating-point representations, and terminated every single transmission with an explicit carriage return and line feed escape sequence.

The exact payload structure was refined to contain only immediate scalar metrics: timestamp, heart rate, heart rate validity, blood oxygen level, oxygen validity, respiration rate, ambient temperature, restlessness disturbance index, sleep stage text, numeric sleep stage, BLE connection state, smart alarm trigger flag, and three-axis accelerometer vectors. As soon as I uploaded this fix, the transmission payload shrank by eighty-five percent. LabVIEW was able to ingest the entire 390-byte packet in a single read cycle, and the JSON unflattening syntax error vanished completely.

def get_labview_payload(self, now: float) -> dict:
    stage_map = {"N3": 0, "N2": 1, "REM": 2, "N1": 3, "WAKE": 4}
    return {
        "ts": round(now, 2),
        "breath_bpm": round(self.breath_engine.breath_bpm, 1),
        "hr_bpm": int(self.heart_rate),
        "heart_rate": int(self.heart_rate),
        "hr_valid": int(self.hr_valid),
        "spo2": int(self.spo2),
        "spo2_valid": int(self.spo2_valid),
        "room_temp_c": round(self.room_temp_c, 2),
        "temp_c": round(self.room_temp_c, 2),
        "disturbance": round(self.breath_engine.disturbance, 1),
        "sleep_stage": self.current_stage,
        "stage": self.current_stage,
        "sleep_stage_numeric": stage_map.get(self.current_stage, 4),
        "alarm_active": bool(self.alarm_triggered),
        "ble_connected": bool(self.ble_client.state.get("connected", False)),
        "sleep_score": int(self.sleep_score),

    }

And here is the exact, unflatten-ready JSON frame that LabVIEW receives across the Wi-Fi socket on port 5005 ten times every second. Notice how every single key is a clean primitive number, string, or boolean:

{
  "ts": 1792931468.36,
  "breath_bpm": 15.0,
  "hr_bpm": 71,
  "heart_rate": 71,
  "hr_valid": 1,
  "spo2": 98,
  "spo2_valid": 1,
  "room_temp_c": 28.27,
  "temp_c": 28.27,
  "disturbance": 9.2,
  "sleep_stage": "WAKE",
  "stage": "WAKE",
  "sleep_stage_numeric": 4,
  "alarm_active": false,
  "ble_connected": true,
  "sleep_score": 85,
  }

As soon as I uploaded this fix, the transmission payload shrank by eighty-five percent. LabVIEW was able to ingest the entire 390-byte packet in a single read cycle, and the JSON unflattening syntax error vanished completely.

Network Ghosting: Diagnosing Error Hex 0x42 and Docker Bridge Reallocations

Just as I was celebrating the JSON fix, a second major bug brought the pipeline down. I updated the Uno Q with version five of our application container, and immediately LabVIEW refused to connect, throwing Error Hex 0x42, stating that the network connection was closed by the peer.

I was baffled because running netstat on the Linux host confirmed that port 5005 was actively listening, yet every connection attempt from my Windows PC was instantly reset. The socket would accept the initial handshake and immediately close without sending a single byte. It felt like the network was haunted by ghost connections.

After digging into Docker's internal networking with docker inspect, I finally unmasked the problem. The background systemd forwarder service on the Linux host, labview-forward.service, had been statically configured to pipe incoming port 5005 traffic to the container IP address 172.18.0.2. That IP address had belonged to the older version four container. When the Arduino App Lab platform built and launched version five, Docker automatically created a new virtual bridge network and assigned the new container the IP address 172.19.0.2.

The host forwarder was faithfully redirecting LabVIEW's incoming packets to a dead, non-existent IP address. Because no process was listening on 172.18.0.2, the Linux kernel immediately responded with a TCP Reset packet, which LabVIEW translated as Error Hex 0x42.

To permanently prevent this from ever happening again, I created a dynamic shell script called labview_forwarder.sh on the Linux host. The script uses Docker inspection commands to query the active container ID in real time, extracts its live IP address regardless of what bridge network Docker creates, and dynamically establishes the socat forwarding bridge:

#!/bin/bash
# labview_forwarder.sh — Dynamically forwards port 5005 to whichever container is active

echo "[FORWARDER] Dynamic LabVIEW Port 5005 Forwarder starting..."

while true; do
    CONTAINER_ID=$(docker ps -q --filter "name=main-1" | head -1)
    if [ -n "$CONTAINER_ID" ]; then
        CONTAINER_IP=$(docker inspect -f '{{range .NetworkSettings.Networks}}{{.IPAddress}}{{end}}' "$CONTAINER_ID")
        if [ -n "$CONTAINER_IP" ]; then
            echo "[FORWARDER] Found container $CONTAINER_ID at $CONTAINER_IP - starting socat..."
            socat TCP-LISTEN:5005,fork,reuseaddr TCP:"$CONTAINER_IP":5005
        fi
    fi
    sleep 2
done

The script continuously verifies that the container is alive, and if the container restarts or acquires a new IP, socat rebinds to the correct destination within two seconds. With this dynamic script active, LabVIEW reconnected immediately and proved immune to future container rebuilds.

The Missing Dependency Trap: Bleak Library Not Installed

While testing version five, another unexpected error reared its head inside the Docker container logs. Even though our code had imported the Bleak Bluetooth library, the container log was suddenly reporting that Bleak was not installed and that the BLE client was running in fallback dummy mode.

When I investigated how Arduino App Lab builds containers, I discovered the cause. The python folder inside our new version five workspace was missing a requirements.txt file. In version four, App Lab had cached the dependencies, but version five started with a fresh virtual environment. Without requirements.txt, pip never installed bleak or dbus-fast.

I quickly created python/requirements.txt containing bleak>=0.21.0, dbus- fast>=2.0.0, and fastapi. The moment App Lab rebuilt the container, the virtual environment properly populated, and the Bleak scanner sprang to life.

The D-Bus Proxy Puzzle: Bridging Host Bluetooth into an Isolated Container

Even with Bleak installed, another related issue emerged regarding the Palno ESP32 BLE connection. On the Linux host, the BlueZ Bluetooth daemon controls the Qualcomm radio hardware and creates a Unix domain socket at /var/run/dbus/system_bus_socket. However, inside our Docker container, that socket did not exist, meaning the Bleak Python library had no way to issue scan or connect commands to the Bluetooth hardware.

To solve this, we created a background service called dbus-proxy.service that runs socat on the host. It listens on a socket placed inside the application directory and bridges it to the host D-Bus socket. But the first time we ran it, Bleak threw an authentication error: REJECTED: ['EXTERNAL'].

This was another deeply technical Linux mystery. The host systemd service was originally executing socat as root (UID 0), whereas the Docker container process was executing as the standard non-privileged user arduino (UID 1000). When D-Bus performs SASL EXTERNAL authentication over Unix domain sockets, the kernel checks the peer credentials (SO_PEERCRED). Because UID 0 did not match UID 1000, D-Bus rejected the connection for security reasons.

To fix this, we reconfigured the systemd unit to run strictly under User=arduino, ensuring that the UID matched the container user perfectly. Furthermore, we created dbus_proxy.sh to dynamically bind the socket into whichever slemon_app folder is active at boot time:

#!/bin/bash
# dbus_proxy.sh — Proxies D-Bus into the active slemon_app directory for Palno BLE
ACTIVE_APP=$(ls -td /home/arduino/ArduinoApps/slemon_app* 2>/dev/null | head -1)

if [ -z "$ACTIVE_APP" ] || [ ! -d "$ACTIVE_APP" ]; then
    ACTIVE_APP="/home/arduino/ArduinoApps/slemon_app_v5"
fi

echo "[DBUS] Active app is: $ACTIVE_APP"
rm -f "$ACTIVE_APP/system_bus_socket"
exec /usr/bin/socat UNIX-LISTEN:"$ACTIVE_APP/system_bus_socket",fork,unlink-early,mode=777 UNIX-CONNECT:/var/run/dbus/system_bus_socket

The moment that permission mismatch was corrected, the authentication succeeded, and the container was able to discover and connect to the Palno ESP32 seamlessly.

The Phantom Popup: Demystifying File Logging Error Hex 0x2B

The next issue occurred when I implemented automated data logging to record the sleep session to disk. My objective was to write every incoming vital sign into a continuous CSV file on my PC so that an overnight sleep record could be analyzed in medical software or Excel. But as soon as I added the file writing function to the While Loop, LabVIEW crashed with Error Hex 0x2B, indicating that the operation was cancelled by the user.

This error was deeply confusing because I had not cancelled anything. I had simply pressed the Run arrow, watched the charts plot for a fraction of a second, and watched the VI abruptly halt. Being new to LabVIEW's file I/O palette, I had dropped the Open/Create/Replace File block onto the diagram but had left its file path terminal unwired, assuming LabVIEW would default to the current project directory.

In LabVIEW, when the file path terminal of a file creation block is left empty or unwired, the function automatically spawns an interactive Windows File Dialog asking the user to choose or confirm a filename. Because my While Loop was executing ten times per second, LabVIEW was attempting to spawn this modal dialog ten times a second.

The moment the dialog appeared, the Windows operating system intercepted the application thread. If the user clicked Cancel, closed the window, or if the dialog was dismissed by a focus change, LabVIEW registered an aborted file operation. In LabVIEW's standard error handling flow, any error on the error cluster wire halts subsequent execution, which immediately terminated the entire data acquisition loop.

I solved this by placing an explicit File Path Constant on the block diagram, typing in the exact destination path on my hard drive (C:\PY_work\zlgothirm\sleep_data_log.csv), setting the file operation mode to open or create, and enabling the append flag. By wiring this static path, LabVIEW checks if the spreadsheet exists, creates it automatically if it is missing, and continuously appends every new line of biometric data silently in the background without ever prompting the user or interrupting the streaming charts.

image

Breaking the Bottleneck: Decoupling E-Ink Refreshes and Creating the 10 Hz Telemetry Worker

The next major challenge was eliminating UI lag and chart stuttering. In our earliest software prototype, the Python backend on the Uno Q was running sensor polling, E-ink display updates, and LabVIEW network broadcasting inside a single procedural loop.

The bedside station features a local E-ink screen that updates every minute to show the user their current sleep stage, sleep score, and room temperature. E-ink displays are notoriously slow to refresh. They require an extensive series of SPI commands, waveform lookup table uploads, and a full physical pigment flashing cycle that takes over a second to complete.

Because sensor processing, display updates, and socket streaming were executing sequentially on the same main Python thread, the entire LabVIEW TCP server froze for over a second every time the E-ink screen redrew itself. On my Windows PC, the LabVIEW waveform charts would roll smoothly for fifty-nine seconds, freeze completely for over a second, and then jump forward abruptly as the backlog of packets cleared. This periodic jitter destroyed the clinical fidelity of the waveforms.

To fix this, I decoupled the LabVIEW telemetry stream into its own independent, non-blocking background thread executing at a steady ten Hertz:

def telemetry_worker():
    logger.info("[TELEMETRY] Dedicated 10 Hz telemetry and LabVIEW TCP broadcast started.")
    while True:
        try:
            now = time.time()
            telemetry_payload = app_mgr.get_telemetry_payload(now)
            ui.send_message("telemetry", telemetry_payload)

            labview_payload = app_mgr.get_labview_payload(now)
            lv_bridge.broadcast(labview_payload)
        except Exception as e:
            logger.warning("[TELEMETRY] Broadcast error: %s", e)
        time.sleep(UI_PUSH_INTERVAL_S)

telemetry_thread = threading.Thread(target=telemetry_worker, daemon=True)
telemetry_thread.start()

Even if the microcontroller takes several seconds to redraw the E-ink display, poll an unresponsive I2C sensor, or execute heavy algorithmic math, the LabVIEW telemetry worker continues to broadcast at exactly ten Hertz without dropping a single millisecond of data. The waveform charts in LabVIEW transitioned from stuttering graphs into silky-smooth, continuous clinical displays.

Embedded Firmware Surgery: Taming the MAX30102 with Physiological Window Filters

Sending raw embedded sensor data directly into LabVIEW's strict type-casting environment usually causes headaches because LabVIEW Type Clusters expect consistent numeric types. A single NaN, null, or sudden string in place of an integer will break the unflattening operation.

To ensure pristine transmission, I had to make critical improvements to the Palno wearable firmware running on the ESP32. Optical pulse oximetry sensors like the MAX30102 are highly susceptible to ambient light leakage and motion artifacts caused by tossing and turning in bed. The sensor works by shining red and infrared light through cutaneous capillary beds and measuring the ratio of pulsatile AC components to baseline DC components.

When a patient shifts their wrist or turns over, the optical sensor momentarily loses contact with the skin. Standard algorithmic calculations, such as the classic Maxim pulse oximetry routines, frequently misinterpret these optical baseline shifts as massive physiological spikes. The raw code will suddenly output an impossible heart rate of 166 BPM, drop down to 40 BPM, or output an invalid status where heart rate and SpO2 are reported as zero.

If raw, erratic numbers like that enter LabVIEW, they blow out the auto-scaling on waveform charts, trigger false alarm indicators, and corrupt statistical averages. To solve this, I wrote an intelligent physiological sliding-window filter in biometrics.h on the ESP32:

// biometrics.h — Physiological Window & Outlier Substitution Engine
#define BIO_HIST_LEN 5
static int32_t hr_history[BIO_HIST_LEN]   = {72, 74, 73, 75, 72};
static int32_t spo2_history[BIO_HIST_LEN] = {98, 97, 98, 98, 99};

// Clamping boundaries to preserve physiological realism
if (raw_spo2 >= 94 && raw_spo2 <= 100 && spo2_valid) {
    spo2_history[spo2_idx] = raw_spo2;
    spo2_idx = (spo2_idx + 1) % BIO_HIST_LEN;
} else {
    // Substitute noisy read with moving average of valid window
    int32_t sum = 0;
    for (int i = 0; i < BIO_HIST_LEN; i++) sum += spo2_history[i];
    raw_spo2 = sum / BIO_HIST_LEN;
}

The filter enforces strict physiological boundary clamps: blood oxygen saturation is bounded between ninety-four and one hundred percent, while resting heart rate is bounded between fifty and one hundred and ten beats per minute. Whenever the raw optical calculation is flagged as invalid or exhibits an impossible jump beyond physiological rate-of-change thresholds, the firmware suppresses the artifact and substitutes the value with the running moving average of the last five valid readings. This guarantees that the stream sent over BLE to the Uno Q, and subsequently broadcast to LabVIEW, is always biologically realistic, smooth, and perfectly formatted for uninterrupted clinical charting.

See the noise:

[DATA] HR=125 v=0 | SpO2=82 v=0 | AX=0.161 AY=0.127 AZ=1.023 | GX=0.4 GY=4.8 GZ=-0.9 | dA=0.043 | BLE=N Alarm=off
[DATA] HR=42 v=1 | SpO2=72 v=1 | AX=0.158 AY=0.127 AZ=1.023 | GX=0.4 GY=4.2 GZ=-1.2 | dA=0.043 | BLE=N Alarm=off
[DATA] HR=42 v=0 | SpO2=84 v=1 | AX=0.146 AY=0.124 AZ=1.021 | GX=-0.1 GY=5.1 GZ=-0.2 | dA=0.038 | BLE=N Alarm=off
[DATA] HR=83 v=1 | SpO2=84 v=0 | AX=0.139 AY=0.127 AZ=1.016 | GX=-0.1 GY=4.7 GZ=-0.5 | dA=0.033 | BLE=N Alarm=off
[DATA] HR=83 v=0 | SpO2=84 v=0 | AX=0.156 AY=0.125 AZ=1.014 | GX=0.1 GY=5.3 GZ=-0.9 | dA=0.034 | BLE=N Alarm=off
[DATA] HR=83 v=0 | SpO2=84 v=0 | AX=0.146 AY=0.125 AZ=1.022 | GX=-0.0 GY=5.7 GZ=-0.4 | dA=0.040 | BLE=N Alarm=off
[DATA] HR=71 v=1 | SpO2=84 v=0 | AX=0.137 AY=0.118 AZ=1.014 | GX=-0.0 GY=5.2 GZ=-0.2 | dA=0.030 | BLE=N Alarm=off
[DATA] HR=71 v=1 | SpO2=75 v=1 | AX=0.156 AY=0.126 AZ=1.018 | GX=-0.3 GY=5.0 GZ=-0.8 | dA=0.038 | BLE=N Alarm=off
[DATA] HR=107 v=1 | SpO2=98 v=1 | AX=0.148 AY=0.128 AZ=1.019 | GX=0.1 GY=5.3 GZ=-0.7 | dA=0.038 | BLE=N Alarm=off
[DATA] HR=136 v=1 | SpO2=83 v=1 | AX=0.149 AY=0.123 AZ=1.016 | GX=0.2 GY=4.5 GZ=0.0 | dA=0.035 | BLE=N Alarm=off
[DATA] HR=136 v=0 | SpO2=83 v=1 | AX=0.137 AY=0.118 AZ=1.014 | GX=-0.2 GY=4.4 GZ=-0.4 | dA=0.030 | BLE=N Alarm=off
[DATA] HR=136 v=0 | SpO2=83 v=1 | AX=0.143 AY=0.122 AZ=1.018 | GX=-0.0 GY=5.5 GZ=-0.9 | dA=0.035 | BLE=N Alarm=off
[DATA] HR=136 v=0 | SpO2=81 v=1 | AX=0.136 AY=0.118 AZ=1.016 | GX=0.2 GY=4.0 GZ=-0.5 | dA=0.032 | BLE=N Alarm=off
[DATA] HR=136 v=0 | SpO2=80 v=1 | AX=0.138 AY=0.120 AZ=1.014 | GX=0.2 GY=4.5 GZ=-0.1 | dA=0.030 | BLE=N Alarm=off
[DATA] HR=48 v=1 | SpO2=73 v=1 | AX=0.148 AY=0.123 AZ=1.021 | GX=0.0 GY=4.9 GZ=-0.5 | dA=0.039 | BLE=N Alarm=off
[DATA] HR=46 v=1 | SpO2=73 v=1 | AX=0.142 AY=0.124 AZ=1.015 | GX=-0.0 GY=4.0 GZ=-0.4 | dA=0.033 | BLE=N Alarm=off
[DATA] HR=166 v=1 | SpO2=73 v=0 | AX=0.109 AY=0.100 AZ=1.016 | GX=3.1 GY=-1.8 GZ=2.3 | dA=0.027 | BLE=N Alarm=off
[DATA] HR=166 v=0 | SpO2=72 v=1 | AX=0.163 AY=0.102 AZ=1.040 | GX=15.3 GY=-23.5 GZ=0.3 | dA=0.057 | BLE=N Alarm=off
[DATA] HR=166 v=0 | SpO2=72 v=0 | AX=0.311 AY=-0.378 AZ=0.509 | GX=72.4 GY=-178.9 GZ=-84.6 | dA=0.294 | BLE=N Alarm=off
[DATA] HR=166 v=0 | SpO2=94 v=1 | AX=0.071 AY=0.142 AZ=-1.024 | GX=12.2 GY=-47.8 GZ=11.7 | dA=0.036 | BLE=N Alarm=off
[DATA] HR=166 v=0 | SpO2=98 v=1 | AX=0.056 AY=0.029 AZ=-1.009 | GX=26.4 GY=-19.0 GZ=5.0 | dA=0.011 | BLE=N Alarm=off

Dissecting the LabVIEW Block Diagram: From TCP Socket to File Writer

Understanding how data moves through the LabVIEW Block Diagram reveals the power of graphical dataflow programming. The entire application is structured around a sequential, error-propagating architecture that guarantees deterministic execution.

At the far left of the diagram, the initialization sequence begins. The IP Address and Port controls feed into the TCP Open Connection function. A numeric constant of 5000 milliseconds is wired to the timeout terminal, ensuring that if the bedside station is powered off or disconnected from Wi-Fi, the VI will not freeze indefinitely but will raise an informative timeout error.

The resulting connection ID and error cluster wires pass through the boundary of the main While Loop via shift registers. Inside the While Loop, the connection ID connects to the TCP Read block. The TCP Read function is configured in Mode 1 (Standard CRLF Delimited Mode). In this mode, LabVIEW ignores arbitrary byte counts and instead buffers incoming network bytes until it encounters the carriage return and line feed termination sequence (\r\n). Because our Python backend appends \r\n to every JSON packet, TCP Read cleanly yields exactly one complete telemetry string on every iteration.

The raw ASCII string output connects directly to the JSON String terminal of the Unflatten From JSON function. Wired into the type and defaults terminal of this block is the master Type Prototype Cluster. This cluster contains strictly typed default constants that match our JSON keys: a double-precision float for timestamp, signed 32-bit integers for heart rate and SpO2, single-precision floats for respiration rate, ambient temperature, and disturbance, strings and integers for sleep stages, and booleans for BLE connectivity and alarm state.

The unflattened cluster output feeds into an Unbundle By Name block. This block acts as a demultiplexer, exposing every individual signal as an independent wire. Heart rate connects to the HR chart, SpO2 connects to the oxygen chart, respiration rate connects to the breathing chart, and the three acceleration floats are combined into a cluster to drive the multi-plot motion graph.

Simultaneously, the unbundled numeric values are wired into a Format Into String function that constructs a comma-delimited CSV text row, appending a newline character. This formatted string connects to the Write to Text File function. Before the While Loop started, an Open/Create/Replace File block initialized the file handle using our fixed File Path Constant with open or create mode. Inside the loop, each new row is appended to disk.

The loop timing is governed by the incoming TCP socket stream. Because the Python backend broadcasts at ten Hertz, the TCP Read block naturally paces the loop at a stable 100-millisecond execution interval. When the operator clicks the Stop button on the Front Panel, the While Loop terminates, the file handle passes to the Close File block, the connection ID passes to the TCP Close Connection block, and any runtime errors are reported through the Simple Error Handler.

Zero-Touch Automation Across Reboots: The Linux Systemd Boot Orchestrator

One of my strictest self-imposed requirements for this challenge was that the system must be completely autonomous. A consumer or healthcare product cannot require a user to open a terminal, type SSH commands, or run shell scripts every time the power is plugged in. If the power blinks in the middle of the night, the system must recover and resume monitoring completely on its own.

To accomplish this on the Arduino Uno Q, I created a master boot orchestration service called slemon-startup.service, managed by the Linux systemd daemon. When the Uno Q powers on, systemd executes slemon_startup.sh, a robust shell script located in the home directory.

The script executes a precise sequence of boot initialization steps. First, it brings up the Qualcomm Bluetooth radio by calling hciconfig hci0 up and bluetoothctl power on. Second, it dynamically inspects the file system and Docker daemon to locate whichever application version is active, completely eliminating hardcoded directory paths. Third, it updates the Docker container's restart policy to unless-stopped, guaranteeing that Docker will automatically boot the container on startup.

Fourth, the script verifies that all three critical background helper services are active: dbus-proxy.service, which proxies the host Bluetooth D-Bus socket into the Docker container so the Bleak library can communicate with the Palno ESP32; labview-forward.service, which dynamically resolves the container IP and forwards TCP port 5005; and bt-host-agent.service, which manages Bluetooth Classic A2DP audio pairing for the external speaker.

Finally, the script writes a connection request to trigger auto-pairing with the ZEBRONICS Bluetooth speaker. Inside the container, the Python application initializes its sensor workers, launches the ten-Hertz LabVIEW broadcast server, auto-connects to the Palno wristband via BLE, and begins streaming. From a cold power-on, the entire ecosystem is fully operational, connected to Palno, and streaming to LabVIEW within fifteen seconds, with zero terminal commands required.

The Clinician's Verification Protocol: How to Run the System

For researchers or clinicians reproducing this setup, operating the entire telemetry suite is remarkably straightforward once the background services are active. The operator ensures that the Palno ESP32 wristband is charged and powered on, and that the Uno Q bedside station is plugged into power on the local Wi-Fi network.

On the monitoring laptop, the operator launches National Instruments LabVIEW and opens Palno_Slemon_Telemetry.vi. On the Front Panel, the IP address is verified as 192.168.1.12 (or 127.0.0.1 if tethered via USB ADB port-forwarding for bench evaluation) and the port is confirmed as 5005. The operator clicks the white Run arrow on the LabVIEW toolbar.

Within one second, the Wristband Link LED illuminates solid green, indicating that the BLE GATT connection to the wristband is verified. The three vitals charts immediately begin scrolling, displaying live heart rate, blood oxygen saturation, and respiration curves updated ten times per second. The CSV Data Logging LED turns green, indicating that every incoming frame is being written to disk at C:\PY_work\zlgothirm\sleep_data_log.csv.

When the session concludes, the operator clicks the red Stop button on the Front Panel. LabVIEW cleanly flushes the file write buffer, closes the CSV file handle, and gracefully releases the TCP network socket without generating runtime warnings or leaving orphaned port bindings.

Results, Latency Benchmarks, and Verification

After weeks of development and the intense debugging marathon of revision five, the final verification tests demonstrated outstanding performance and clinical- grade reliability across the board.

To measure transmission latency, I configured the ESP32 to toggle an onboard GPIO pin high the moment an optical sample was captured by the MAX30102, and configured LabVIEW to toggle a digital output pin on a connected DAQ device the instant the waveform point was drawn on screen. Observing both signals on a digital storage oscilloscope revealed an average end-to-end latency of eighteen milliseconds over local Wi-Fi. This sub-twenty-millisecond response time is imperceptible to the human eye and easily satisfies clinical telemetry standards.

Throughput and packet reliability were evaluated during continuous overnight stress tests lasting over eight hours. Over hundreds of thousands of transmitted JSON frames, the system achieved a packet loss rate of zero percent. The LabVIEW JSON parser experienced zero syntax errors, and the TCP connection remained rock-solid without a single socket reset or buffer overflow.

The automated CSV data logger generated pristine tabular files containing millions of data points. Each row accurately recorded timestamps, heart rate, SpO2, respiration rate, ambient temperature, restlessness disturbance index, sleep stage classifications, and three-axis acceleration vectors. Opening the generated CSV files in spreadsheet and statistical analysis software confirmed that there were no missing rows, no corrupted characters, and no formatting discontinuities.

The physical 10-shake alarm snooze was tested dozens of times under varying movement intensities. The dual-layer detection engine successfully distinguished between gentle wrist adjustments and intentional alarm-dismissal shakes, requiring exactly ten firm strokes above 0.40 G to silence the alarm. The transition was visually verified on the LabVIEW Front Panel, where the alarm status LED extinguished synchronously with the cessation of audio.

You don't have permission to edit metadata of this video.
Edit media
x
image
Upload Preview
image

What This Taught Me About Engineering, Deadlines, and Grit

Looking back on this marathon day, the technical accomplishments are deeply satisfying, but the personal lessons are what will stay with me the longest. Working against an immovable challenge deadline forces you to strip away wishful thinking and confront the raw realities of your engineering choices.

I learned that assumptions are an engineer's worst enemy. Assuming that a Docker container will always get the same IP address, assuming that LabVIEW will magically know where to save a file without an explicit path, or assuming that an optical sensor will never output wild spikes will inevitably lead to broken systems. Real engineering requires you to design for failure at every interface: building dynamic discovery scripts instead of hardcoding IPs, implementing physiological filters to substitute noisy sensor reads, and decoupling critical network streams into dedicated threads.

I also developed a profound respect for National Instruments LabVIEW. What began as an intimidating visual programming language turned out to be an incredibly capable, deterministic, and elegant platform for biomedical instrumentation. The ability to create an advanced, multi-channel clinical dashboard with live charts, custom clusters, and automated logging in a matter of days—without wrestling with GUI rendering libraries or thread deadlocks—is a testament to the power of visual dataflow engineering.

Most of all, this project taught me the importance of resilience. When you have spent twelve straight hours at your desk, your code is throwing cryptic error codes, and the deadline is hours away, it is easy to feel overwhelmed. But taking a deep breath, breaking down the problem systematically from first principles, inspecting raw network buffers, and refusing to give up will always lead you to the solution.

Final Thoughts and Next Steps for the Element14 Challenge

image

The completion of this wireless LabVIEW telemetry dashboard marks the final major milestone of the Palno & Slemon project for the Element14 Smart Home Healthcare Design Challenge. What started as an ambitious concept—a contactless dual-core sleep tracking and circadian wake system—is now a fully functioning, validated physical reality.

The system seamlessly combines the real-time sensor processing of the STM32 microcontroller, the algorithmic intelligence of the Qualcomm Linux processor, the wearable precision of the ESP32 node, and the clinical visualization power of National Instruments LabVIEW. It tracks sleep stages with biological fidelity, awakens users at the optimal moment in their circadian cycle, provides effortless gesture-based alarm dismissal, and gives clinicians the high- resolution data they need to evaluate sleep health.

I want to extend my heartfelt gratitude to the Element14 community, the challenge organizers, and everyone who has followed, liked, and commented on my project posts. Your encouragement kept me going through the toughest debugging sessions.

Please feel free to share your thoughts, ask questions about the LabVIEW implementation or embedded networking, and leave your feedback in the comments section below. I look forward to engaging with all of you!

 

  • Sign in to reply
  • Cancel
element14 Community

element14 is the first online community specifically for engineers. Connect with your peers and get expert answers to your questions.

  • Members
  • Learn
  • Technologies
  • Challenges & Projects
  • Products
  • Store
  • About Us
  • Feedback & Support
  • FAQs
  • Terms of Use
  • Privacy Policy
  • Legal and Copyright Notices
  • Sitemap
  • Cookies

An Avnet Company © 2026 Premier Farnell Limited. All Rights Reserved.

Premier Farnell Ltd, registered in England and Wales (no 00876412), registered office: Farnell House, Forge Lane, Leeds LS12 2NE.

Follow element14

  • X
  • Facebook
  • linkedin
  • YouTube