Initial biometric data receiver implementation

This commit is contained in:
Abhishek Kumar
2026-06-19 08:43:45 +05:30
commit 721c787e5f
3 changed files with 401 additions and 0 deletions

140
README.md Normal file
View File

@@ -0,0 +1,140 @@
# Pilot Bio-Data — Pulse Oximeter Logger
Receives real-time **heart rate (BPM)** and **blood-oxygen (SpO₂)** readings
from the Controlytics AI pulse oximeter over a wired Ethernet connection and
logs them to a timestamped CSV file.
---
## Hardware
| Item | Detail |
|---|---|
| Device | Controlytics AI Pulse Oximeter v1.0.0 |
| Interface | Ethernet (UDP) |
| Device Ethernet IP | `192.168.1.2` |
| Device WiFi AP | `PULSE_OXI_WIFI` / password `controlytics` |
| Device config page | `http://192.168.4.1` (Admin / Admin) |
---
## Network Setup
The device sends UDP packets to a configured destination address.
The one-time setup below is already done; repeat only if hardware changes.
### Computer (receiver)
Assign a static IP to the Ethernet interface connected to the device:
```bash
sudo ip addr add 192.168.1.100/24 dev <eth-interface>
```
On this machine the interface is `enp1s0`:
```bash
sudo ip addr add 192.168.1.100/24 dev enp1s0
```
### Device configuration
Connect to `PULSE_OXI_WIFI`, open `http://192.168.4.1`, log in, and set:
| Field | Value |
|---|---|
| IP | `192.168.1.2` |
| Gateway | `192.168.1.1` |
| Subnet | `255.255.255.0` |
| Destination address | `192.168.1.100` |
| Destination port | `5005` |
| Send interval (ms) | `500` |
Submit and restart the device (Log Out button).
---
## Requirements
Python 3.10 or later. No third-party packages — only the standard library is used.
```bash
python3 --version # 3.10+
```
---
## Running
```bash
python3 receive.py
```
Place the pilot's index finger on the sensor with light, steady pressure.
Hold still for a few seconds for the reading to stabilise.
### Example output
```
Listening on UDP 0.0.0.0:5005 | Logging to readings.csv
TIME BPM SpO2 % STATUS
----------------------------------------------------
11:34:00.464 92 97 OK
11:34:00.965 92 97 OK
11:34:01.466 74 97 OK
11:34:22.034 87 98 OK
11:35:14.207 --- --- No finger
```
Press `Ctrl+C` to stop.
---
## Output — readings.csv
Every packet is appended to `readings.csv` in the same directory.
Rows where no finger is detected have empty `bpm` and `spo2` fields.
| Column | Description |
|---|---|
| `timestamp` | ISO-8601 local timestamp |
| `bpm` | Heart rate in beats per minute (empty if no finger) |
| `spo2` | Blood-oxygen saturation % (empty if no finger) |
| `raw` | Raw JSON packet from device |
### Example rows
```
timestamp,bpm,spo2,raw
2026-06-12T11:34:00.464000,92,97,"{""bpm"":92,""spo2"":97}"
2026-06-12T11:34:01.466000,74,97,"{""bpm"":74,""spo2"":97}"
2026-06-12T11:35:14.207000,,,"{""bpm"":-1,""spo2"":-1}"
```
---
## Packet Format
The device sends a UDP JSON payload every 500 ms:
```json
{"bpm": 92, "spo2": 97}
```
Values of `-1` indicate no finger is on the sensor.
The parser also accepts `key=value` and bare CSV formats for forward compatibility.
---
## Project Structure
```
pilot_bio_data/
├── receive.py # UDP listener and CSV logger
├── readings.csv # Auto-generated log (appended on each run)
└── README.md
```
---
*Axial Aero — Internal tooling*

155
readings.csv Normal file
View File

@@ -0,0 +1,155 @@
2026-06-12T11:33:32.827783,-1,-1,"{""bpm"":-1,""spo2"":-1}"
2026-06-12T11:33:33.329104,-1,-1,"{""bpm"":-1,""spo2"":-1}"
2026-06-12T11:33:33.830768,-1,-1,"{""bpm"":-1,""spo2"":-1}"
2026-06-12T11:33:34.331759,-1,-1,"{""bpm"":-1,""spo2"":-1}"
2026-06-12T11:33:34.833746,-1,-1,"{""bpm"":-1,""spo2"":-1}"
2026-06-12T11:33:35.334779,-1,-1,"{""bpm"":-1,""spo2"":-1}"
2026-06-12T11:33:35.836760,-1,-1,"{""bpm"":-1,""spo2"":-1}"
2026-06-12T11:33:36.338792,-1,-1,"{""bpm"":-1,""spo2"":-1}"
2026-06-12T11:33:36.839758,-1,-1,"{""bpm"":-1,""spo2"":-1}"
2026-06-12T11:33:37.341826,-1,-1,"{""bpm"":-1,""spo2"":-1}"
2026-06-12T11:33:37.842746,-1,-1,"{""bpm"":-1,""spo2"":-1}"
2026-06-12T11:33:38.344744,-1,-1,"{""bpm"":-1,""spo2"":-1}"
2026-06-12T11:33:38.845791,-1,-1,"{""bpm"":-1,""spo2"":-1}"
2026-06-12T11:33:39.347801,-1,-1,"{""bpm"":-1,""spo2"":-1}"
2026-06-12T11:33:39.848753,-1,-1,"{""bpm"":-1,""spo2"":-1}"
2026-06-12T11:33:40.350772,-1,-1,"{""bpm"":-1,""spo2"":-1}"
2026-06-12T11:33:40.851753,-1,-1,"{""bpm"":-1,""spo2"":-1}"
2026-06-12T11:33:41.354134,-1,-1,"{""bpm"":-1,""spo2"":-1}"
2026-06-12T11:33:41.854786,-1,-1,"{""bpm"":-1,""spo2"":-1}"
2026-06-12T11:33:42.356788,-1,-1,"{""bpm"":-1,""spo2"":-1}"
2026-06-12T11:33:42.857741,-1,-1,"{""bpm"":-1,""spo2"":-1}"
2026-06-12T11:33:43.359757,-1,-1,"{""bpm"":-1,""spo2"":-1}"
2026-06-12T11:33:43.860750,-1,-1,"{""bpm"":-1,""spo2"":-1}"
2026-06-12T11:33:44.362749,-1,-1,"{""bpm"":-1,""spo2"":-1}"
2026-06-12T11:33:44.864750,-1,-1,"{""bpm"":-1,""spo2"":-1}"
2026-06-12T11:33:45.365806,-1,-1,"{""bpm"":-1,""spo2"":-1}"
2026-06-12T11:33:45.867796,-1,-1,"{""bpm"":-1,""spo2"":-1}"
2026-06-12T11:33:46.368828,-1,-1,"{""bpm"":-1,""spo2"":-1}"
2026-06-12T11:33:46.870841,-1,-1,"{""bpm"":-1,""spo2"":-1}"
2026-06-12T11:33:49.428895,-1,-1,"{""bpm"":-1,""spo2"":-1}"
2026-06-12T11:33:49.929775,-1,-1,"{""bpm"":-1,""spo2"":-1}"
2026-06-12T11:33:50.431867,-1,-1,"{""bpm"":-1,""spo2"":-1}"
2026-06-12T11:33:50.932922,-1,89,"{""bpm"":-1,""spo2"":89}"
2026-06-12T11:33:51.434831,-1,89,"{""bpm"":-1,""spo2"":89}"
2026-06-12T11:33:51.935795,-1,89,"{""bpm"":-1,""spo2"":89}"
2026-06-12T11:33:52.437837,-1,89,"{""bpm"":-1,""spo2"":89}"
2026-06-12T11:33:52.939862,-1,89,"{""bpm"":-1,""spo2"":89}"
2026-06-12T11:33:53.441175,-1,89,"{""bpm"":-1,""spo2"":89}"
2026-06-12T11:33:53.942848,-1,90,"{""bpm"":-1,""spo2"":90}"
2026-06-12T11:33:54.444145,-1,90,"{""bpm"":-1,""spo2"":90}"
2026-06-12T11:33:54.945841,-1,96,"{""bpm"":-1,""spo2"":96}"
2026-06-12T11:33:55.447881,-1,96,"{""bpm"":-1,""spo2"":96}"
2026-06-12T11:33:55.948881,-1,96,"{""bpm"":-1,""spo2"":96}"
2026-06-12T11:33:56.451052,-1,96,"{""bpm"":-1,""spo2"":96}"
2026-06-12T11:33:56.952797,-1,90,"{""bpm"":-1,""spo2"":90}"
2026-06-12T11:33:57.453943,-1,90,"{""bpm"":-1,""spo2"":90}"
2026-06-12T11:33:57.955804,-1,90,"{""bpm"":-1,""spo2"":90}"
2026-06-12T11:33:58.456900,-1,96,"{""bpm"":-1,""spo2"":96}"
2026-06-12T11:33:58.958901,-1,96,"{""bpm"":-1,""spo2"":96}"
2026-06-12T11:33:59.461022,-1,96,"{""bpm"":-1,""spo2"":96}"
2026-06-12T11:33:59.961826,92,97,"{""bpm"":92,""spo2"":97}"
2026-06-12T11:34:00.464106,92,97,"{""bpm"":92,""spo2"":97}"
2026-06-12T11:34:00.965835,92,97,"{""bpm"":92,""spo2"":97}"
2026-06-12T11:34:01.466897,74,97,"{""bpm"":74,""spo2"":97}"
2026-06-12T11:34:01.968838,77,97,"{""bpm"":77,""spo2"":97}"
2026-06-12T11:34:02.470858,77,97,"{""bpm"":77,""spo2"":97}"
2026-06-12T11:34:02.971864,81,92,"{""bpm"":81,""spo2"":92}"
2026-06-12T11:34:03.473928,77,87,"{""bpm"":77,""spo2"":87}"
2026-06-12T11:34:03.974932,77,86,"{""bpm"":77,""spo2"":86}"
2026-06-12T11:34:04.476900,77,86,"{""bpm"":77,""spo2"":86}"
2026-06-12T11:34:04.978820,81,86,"{""bpm"":81,""spo2"":86}"
2026-06-12T11:34:05.479860,81,86,"{""bpm"":81,""spo2"":86}"
2026-06-12T11:34:05.981890,81,92,"{""bpm"":81,""spo2"":92}"
2026-06-12T11:34:06.483873,85,95,"{""bpm"":85,""spo2"":95}"
2026-06-12T11:34:06.984850,85,95,"{""bpm"":85,""spo2"":95}"
2026-06-12T11:34:07.487236,85,95,"{""bpm"":85,""spo2"":95}"
2026-06-12T11:34:07.988840,85,95,"{""bpm"":85,""spo2"":95}"
2026-06-12T11:34:08.489924,85,95,"{""bpm"":85,""spo2"":95}"
2026-06-12T11:34:08.991922,85,95,"{""bpm"":85,""spo2"":95}"
2026-06-12T11:34:09.493953,81,94,"{""bpm"":81,""spo2"":94}"
2026-06-12T11:34:09.994843,81,94,"{""bpm"":81,""spo2"":94}"
2026-06-12T11:34:10.496933,81,94,"{""bpm"":81,""spo2"":94}"
2026-06-12T11:34:10.997865,81,94,"{""bpm"":81,""spo2"":94}"
2026-06-12T11:34:11.499929,81,93,"{""bpm"":81,""spo2"":93}"
2026-06-12T11:34:12.002030,81,93,"{""bpm"":81,""spo2"":93}"
2026-06-12T11:34:12.502893,81,88,"{""bpm"":81,""spo2"":88}"
2026-06-12T11:34:13.004880,81,93,"{""bpm"":81,""spo2"":93}"
2026-06-12T11:34:13.506926,92,94,"{""bpm"":92,""spo2"":94}"
2026-06-12T11:34:14.007895,92,94,"{""bpm"":92,""spo2"":94}"
2026-06-12T11:34:14.510099,92,94,"{""bpm"":92,""spo2"":94}"
2026-06-12T11:34:15.011892,92,94,"{""bpm"":92,""spo2"":94}"
2026-06-12T11:34:15.513911,92,95,"{""bpm"":92,""spo2"":95}"
2026-06-12T11:34:16.014909,87,96,"{""bpm"":87,""spo2"":96}"
2026-06-12T11:34:16.516899,87,96,"{""bpm"":87,""spo2"":96}"
2026-06-12T11:34:17.018859,87,96,"{""bpm"":87,""spo2"":96}"
2026-06-12T11:34:17.519930,92,96,"{""bpm"":92,""spo2"":96}"
2026-06-12T11:34:18.021888,92,96,"{""bpm"":92,""spo2"":96}"
2026-06-12T11:34:18.523985,92,96,"{""bpm"":92,""spo2"":96}"
2026-06-12T11:34:19.024943,92,96,"{""bpm"":92,""spo2"":96}"
2026-06-12T11:34:19.527042,92,96,"{""bpm"":92,""spo2"":96}"
2026-06-12T11:34:20.028912,89,96,"{""bpm"":89,""spo2"":96}"
2026-06-12T11:34:20.529898,89,96,"{""bpm"":89,""spo2"":96}"
2026-06-12T11:34:21.031946,89,96,"{""bpm"":89,""spo2"":96}"
2026-06-12T11:34:21.533984,89,96,"{""bpm"":89,""spo2"":96}"
2026-06-12T11:34:22.034882,87,98,"{""bpm"":87,""spo2"":98}"
2026-06-12T11:34:22.536947,87,98,"{""bpm"":87,""spo2"":98}"
2026-06-12T11:34:23.038877,85,98,"{""bpm"":85,""spo2"":98}"
2026-06-12T11:34:23.539937,89,98,"{""bpm"":89,""spo2"":98}"
2026-06-12T11:34:24.041915,89,98,"{""bpm"":89,""spo2"":98}"
2026-06-12T11:34:24.544003,89,96,"{""bpm"":89,""spo2"":96}"
2026-06-12T11:34:25.044909,85,96,"{""bpm"":85,""spo2"":96}"
2026-06-12T11:34:25.547076,96,93,"{""bpm"":96,""spo2"":93}"
2026-06-12T11:34:26.048937,96,92,"{""bpm"":96,""spo2"":92}"
2026-06-12T11:34:26.549927,96,92,"{""bpm"":96,""spo2"":92}"
2026-06-12T11:34:27.051903,96,92,"{""bpm"":96,""spo2"":92}"
2026-06-12T11:34:27.554174,94,90,"{""bpm"":94,""spo2"":90}"
2026-06-12T11:34:28.055238,94,90,"{""bpm"":94,""spo2"":90}"
2026-06-12T11:34:28.556968,94,92,"{""bpm"":94,""spo2"":92}"
2026-06-12T11:34:29.058950,87,92,"{""bpm"":87,""spo2"":92}"
2026-06-12T11:34:29.559892,94,92,"{""bpm"":94,""spo2"":92}"
2026-06-12T11:34:30.061926,85,94,"{""bpm"":85,""spo2"":94}"
2026-06-12T11:34:30.563924,85,96,"{""bpm"":85,""spo2"":96}"
2026-06-12T11:34:31.065938,79,96,"{""bpm"":79,""spo2"":96}"
2026-06-12T11:34:31.566943,85,96,"{""bpm"":85,""spo2"":96}"
2026-06-12T11:34:32.068920,99,96,"{""bpm"":99,""spo2"":96}"
2026-06-12T11:35:14.207713,,,"{""bpm"":-1,""spo2"":-1}"
2026-06-12T11:35:14.709107,,,"{""bpm"":-1,""spo2"":-1}"
2026-06-12T11:35:15.211099,,,"{""bpm"":-1,""spo2"":-1}"
2026-06-12T11:35:15.712087,,,"{""bpm"":-1,""spo2"":-1}"
2026-06-12T11:35:16.214076,,,"{""bpm"":-1,""spo2"":-1}"
2026-06-12T11:35:16.715095,,,"{""bpm"":-1,""spo2"":-1}"
2026-06-12T11:35:17.217370,,,"{""bpm"":-1,""spo2"":-1}"
2026-06-12T11:35:17.718063,,,"{""bpm"":-1,""spo2"":-1}"
2026-06-12T11:35:18.220188,,,"{""bpm"":-1,""spo2"":-1}"
2026-06-12T11:35:18.722138,,,"{""bpm"":-1,""spo2"":-1}"
2026-06-12T11:35:19.223071,,,"{""bpm"":-1,""spo2"":-1}"
2026-06-12T11:35:19.725082,,,"{""bpm"":-1,""spo2"":-1}"
2026-06-12T11:35:20.227142,,,"{""bpm"":-1,""spo2"":-1}"
2026-06-12T11:35:20.728106,,,"{""bpm"":-1,""spo2"":-1}"
2026-06-12T11:35:21.230101,,,"{""bpm"":-1,""spo2"":-1}"
2026-06-12T11:35:21.731071,,,"{""bpm"":-1,""spo2"":-1}"
2026-06-12T11:35:22.233061,,,"{""bpm"":-1,""spo2"":-1}"
2026-06-12T11:35:22.735075,,,"{""bpm"":-1,""spo2"":-1}"
2026-06-12T11:35:23.236111,,,"{""bpm"":-1,""spo2"":-1}"
2026-06-12T11:35:23.738588,,,"{""bpm"":-1,""spo2"":-1}"
2026-06-12T11:35:24.239134,,,"{""bpm"":-1,""spo2"":-1}"
2026-06-12T11:35:24.741129,,,"{""bpm"":-1,""spo2"":-1}"
2026-06-12T11:35:25.242099,,,"{""bpm"":-1,""spo2"":-1}"
2026-06-12T11:35:25.744096,,,"{""bpm"":-1,""spo2"":-1}"
2026-06-12T11:35:26.246144,,,"{""bpm"":-1,""spo2"":-1}"
2026-06-12T11:35:26.747135,,,"{""bpm"":-1,""spo2"":-1}"
2026-06-12T11:35:27.249471,,,"{""bpm"":-1,""spo2"":-1}"
2026-06-12T11:35:27.750167,,,"{""bpm"":-1,""spo2"":-1}"
2026-06-12T11:35:28.252514,,,"{""bpm"":-1,""spo2"":-1}"
2026-06-12T11:35:28.754229,,,"{""bpm"":-1,""spo2"":-1}"
2026-06-12T11:35:29.255536,,,"{""bpm"":-1,""spo2"":-1}"
2026-06-12T11:35:29.757130,,,"{""bpm"":-1,""spo2"":-1}"
2026-06-12T11:35:30.258461,,,"{""bpm"":-1,""spo2"":-1}"
2026-06-12T11:35:30.760323,,,"{""bpm"":-1,""spo2"":-1}"
2026-06-12T11:35:31.262249,,,"{""bpm"":-1,""spo2"":-1}"
2026-06-12T11:35:31.763243,,,"{""bpm"":-1,""spo2"":-1}"
2026-06-12T11:35:32.265482,,,"{""bpm"":-1,""spo2"":-1}"
2026-06-12T11:35:32.766157,,,"{""bpm"":-1,""spo2"":-1}"
2026-06-12T11:35:33.268510,,,"{""bpm"":-1,""spo2"":-1}"
2026-06-12T11:35:33.769203,,,"{""bpm"":-1,""spo2"":-1}"
1 2026-06-12T11:33:32.827783 -1 -1 {"bpm":-1,"spo2":-1}
2 2026-06-12T11:33:33.329104 -1 -1 {"bpm":-1,"spo2":-1}
3 2026-06-12T11:33:33.830768 -1 -1 {"bpm":-1,"spo2":-1}
4 2026-06-12T11:33:34.331759 -1 -1 {"bpm":-1,"spo2":-1}
5 2026-06-12T11:33:34.833746 -1 -1 {"bpm":-1,"spo2":-1}
6 2026-06-12T11:33:35.334779 -1 -1 {"bpm":-1,"spo2":-1}
7 2026-06-12T11:33:35.836760 -1 -1 {"bpm":-1,"spo2":-1}
8 2026-06-12T11:33:36.338792 -1 -1 {"bpm":-1,"spo2":-1}
9 2026-06-12T11:33:36.839758 -1 -1 {"bpm":-1,"spo2":-1}
10 2026-06-12T11:33:37.341826 -1 -1 {"bpm":-1,"spo2":-1}
11 2026-06-12T11:33:37.842746 -1 -1 {"bpm":-1,"spo2":-1}
12 2026-06-12T11:33:38.344744 -1 -1 {"bpm":-1,"spo2":-1}
13 2026-06-12T11:33:38.845791 -1 -1 {"bpm":-1,"spo2":-1}
14 2026-06-12T11:33:39.347801 -1 -1 {"bpm":-1,"spo2":-1}
15 2026-06-12T11:33:39.848753 -1 -1 {"bpm":-1,"spo2":-1}
16 2026-06-12T11:33:40.350772 -1 -1 {"bpm":-1,"spo2":-1}
17 2026-06-12T11:33:40.851753 -1 -1 {"bpm":-1,"spo2":-1}
18 2026-06-12T11:33:41.354134 -1 -1 {"bpm":-1,"spo2":-1}
19 2026-06-12T11:33:41.854786 -1 -1 {"bpm":-1,"spo2":-1}
20 2026-06-12T11:33:42.356788 -1 -1 {"bpm":-1,"spo2":-1}
21 2026-06-12T11:33:42.857741 -1 -1 {"bpm":-1,"spo2":-1}
22 2026-06-12T11:33:43.359757 -1 -1 {"bpm":-1,"spo2":-1}
23 2026-06-12T11:33:43.860750 -1 -1 {"bpm":-1,"spo2":-1}
24 2026-06-12T11:33:44.362749 -1 -1 {"bpm":-1,"spo2":-1}
25 2026-06-12T11:33:44.864750 -1 -1 {"bpm":-1,"spo2":-1}
26 2026-06-12T11:33:45.365806 -1 -1 {"bpm":-1,"spo2":-1}
27 2026-06-12T11:33:45.867796 -1 -1 {"bpm":-1,"spo2":-1}
28 2026-06-12T11:33:46.368828 -1 -1 {"bpm":-1,"spo2":-1}
29 2026-06-12T11:33:46.870841 -1 -1 {"bpm":-1,"spo2":-1}
30 2026-06-12T11:33:49.428895 -1 -1 {"bpm":-1,"spo2":-1}
31 2026-06-12T11:33:49.929775 -1 -1 {"bpm":-1,"spo2":-1}
32 2026-06-12T11:33:50.431867 -1 -1 {"bpm":-1,"spo2":-1}
33 2026-06-12T11:33:50.932922 -1 89 {"bpm":-1,"spo2":89}
34 2026-06-12T11:33:51.434831 -1 89 {"bpm":-1,"spo2":89}
35 2026-06-12T11:33:51.935795 -1 89 {"bpm":-1,"spo2":89}
36 2026-06-12T11:33:52.437837 -1 89 {"bpm":-1,"spo2":89}
37 2026-06-12T11:33:52.939862 -1 89 {"bpm":-1,"spo2":89}
38 2026-06-12T11:33:53.441175 -1 89 {"bpm":-1,"spo2":89}
39 2026-06-12T11:33:53.942848 -1 90 {"bpm":-1,"spo2":90}
40 2026-06-12T11:33:54.444145 -1 90 {"bpm":-1,"spo2":90}
41 2026-06-12T11:33:54.945841 -1 96 {"bpm":-1,"spo2":96}
42 2026-06-12T11:33:55.447881 -1 96 {"bpm":-1,"spo2":96}
43 2026-06-12T11:33:55.948881 -1 96 {"bpm":-1,"spo2":96}
44 2026-06-12T11:33:56.451052 -1 96 {"bpm":-1,"spo2":96}
45 2026-06-12T11:33:56.952797 -1 90 {"bpm":-1,"spo2":90}
46 2026-06-12T11:33:57.453943 -1 90 {"bpm":-1,"spo2":90}
47 2026-06-12T11:33:57.955804 -1 90 {"bpm":-1,"spo2":90}
48 2026-06-12T11:33:58.456900 -1 96 {"bpm":-1,"spo2":96}
49 2026-06-12T11:33:58.958901 -1 96 {"bpm":-1,"spo2":96}
50 2026-06-12T11:33:59.461022 -1 96 {"bpm":-1,"spo2":96}
51 2026-06-12T11:33:59.961826 92 97 {"bpm":92,"spo2":97}
52 2026-06-12T11:34:00.464106 92 97 {"bpm":92,"spo2":97}
53 2026-06-12T11:34:00.965835 92 97 {"bpm":92,"spo2":97}
54 2026-06-12T11:34:01.466897 74 97 {"bpm":74,"spo2":97}
55 2026-06-12T11:34:01.968838 77 97 {"bpm":77,"spo2":97}
56 2026-06-12T11:34:02.470858 77 97 {"bpm":77,"spo2":97}
57 2026-06-12T11:34:02.971864 81 92 {"bpm":81,"spo2":92}
58 2026-06-12T11:34:03.473928 77 87 {"bpm":77,"spo2":87}
59 2026-06-12T11:34:03.974932 77 86 {"bpm":77,"spo2":86}
60 2026-06-12T11:34:04.476900 77 86 {"bpm":77,"spo2":86}
61 2026-06-12T11:34:04.978820 81 86 {"bpm":81,"spo2":86}
62 2026-06-12T11:34:05.479860 81 86 {"bpm":81,"spo2":86}
63 2026-06-12T11:34:05.981890 81 92 {"bpm":81,"spo2":92}
64 2026-06-12T11:34:06.483873 85 95 {"bpm":85,"spo2":95}
65 2026-06-12T11:34:06.984850 85 95 {"bpm":85,"spo2":95}
66 2026-06-12T11:34:07.487236 85 95 {"bpm":85,"spo2":95}
67 2026-06-12T11:34:07.988840 85 95 {"bpm":85,"spo2":95}
68 2026-06-12T11:34:08.489924 85 95 {"bpm":85,"spo2":95}
69 2026-06-12T11:34:08.991922 85 95 {"bpm":85,"spo2":95}
70 2026-06-12T11:34:09.493953 81 94 {"bpm":81,"spo2":94}
71 2026-06-12T11:34:09.994843 81 94 {"bpm":81,"spo2":94}
72 2026-06-12T11:34:10.496933 81 94 {"bpm":81,"spo2":94}
73 2026-06-12T11:34:10.997865 81 94 {"bpm":81,"spo2":94}
74 2026-06-12T11:34:11.499929 81 93 {"bpm":81,"spo2":93}
75 2026-06-12T11:34:12.002030 81 93 {"bpm":81,"spo2":93}
76 2026-06-12T11:34:12.502893 81 88 {"bpm":81,"spo2":88}
77 2026-06-12T11:34:13.004880 81 93 {"bpm":81,"spo2":93}
78 2026-06-12T11:34:13.506926 92 94 {"bpm":92,"spo2":94}
79 2026-06-12T11:34:14.007895 92 94 {"bpm":92,"spo2":94}
80 2026-06-12T11:34:14.510099 92 94 {"bpm":92,"spo2":94}
81 2026-06-12T11:34:15.011892 92 94 {"bpm":92,"spo2":94}
82 2026-06-12T11:34:15.513911 92 95 {"bpm":92,"spo2":95}
83 2026-06-12T11:34:16.014909 87 96 {"bpm":87,"spo2":96}
84 2026-06-12T11:34:16.516899 87 96 {"bpm":87,"spo2":96}
85 2026-06-12T11:34:17.018859 87 96 {"bpm":87,"spo2":96}
86 2026-06-12T11:34:17.519930 92 96 {"bpm":92,"spo2":96}
87 2026-06-12T11:34:18.021888 92 96 {"bpm":92,"spo2":96}
88 2026-06-12T11:34:18.523985 92 96 {"bpm":92,"spo2":96}
89 2026-06-12T11:34:19.024943 92 96 {"bpm":92,"spo2":96}
90 2026-06-12T11:34:19.527042 92 96 {"bpm":92,"spo2":96}
91 2026-06-12T11:34:20.028912 89 96 {"bpm":89,"spo2":96}
92 2026-06-12T11:34:20.529898 89 96 {"bpm":89,"spo2":96}
93 2026-06-12T11:34:21.031946 89 96 {"bpm":89,"spo2":96}
94 2026-06-12T11:34:21.533984 89 96 {"bpm":89,"spo2":96}
95 2026-06-12T11:34:22.034882 87 98 {"bpm":87,"spo2":98}
96 2026-06-12T11:34:22.536947 87 98 {"bpm":87,"spo2":98}
97 2026-06-12T11:34:23.038877 85 98 {"bpm":85,"spo2":98}
98 2026-06-12T11:34:23.539937 89 98 {"bpm":89,"spo2":98}
99 2026-06-12T11:34:24.041915 89 98 {"bpm":89,"spo2":98}
100 2026-06-12T11:34:24.544003 89 96 {"bpm":89,"spo2":96}
101 2026-06-12T11:34:25.044909 85 96 {"bpm":85,"spo2":96}
102 2026-06-12T11:34:25.547076 96 93 {"bpm":96,"spo2":93}
103 2026-06-12T11:34:26.048937 96 92 {"bpm":96,"spo2":92}
104 2026-06-12T11:34:26.549927 96 92 {"bpm":96,"spo2":92}
105 2026-06-12T11:34:27.051903 96 92 {"bpm":96,"spo2":92}
106 2026-06-12T11:34:27.554174 94 90 {"bpm":94,"spo2":90}
107 2026-06-12T11:34:28.055238 94 90 {"bpm":94,"spo2":90}
108 2026-06-12T11:34:28.556968 94 92 {"bpm":94,"spo2":92}
109 2026-06-12T11:34:29.058950 87 92 {"bpm":87,"spo2":92}
110 2026-06-12T11:34:29.559892 94 92 {"bpm":94,"spo2":92}
111 2026-06-12T11:34:30.061926 85 94 {"bpm":85,"spo2":94}
112 2026-06-12T11:34:30.563924 85 96 {"bpm":85,"spo2":96}
113 2026-06-12T11:34:31.065938 79 96 {"bpm":79,"spo2":96}
114 2026-06-12T11:34:31.566943 85 96 {"bpm":85,"spo2":96}
115 2026-06-12T11:34:32.068920 99 96 {"bpm":99,"spo2":96}
116 2026-06-12T11:35:14.207713 {"bpm":-1,"spo2":-1}
117 2026-06-12T11:35:14.709107 {"bpm":-1,"spo2":-1}
118 2026-06-12T11:35:15.211099 {"bpm":-1,"spo2":-1}
119 2026-06-12T11:35:15.712087 {"bpm":-1,"spo2":-1}
120 2026-06-12T11:35:16.214076 {"bpm":-1,"spo2":-1}
121 2026-06-12T11:35:16.715095 {"bpm":-1,"spo2":-1}
122 2026-06-12T11:35:17.217370 {"bpm":-1,"spo2":-1}
123 2026-06-12T11:35:17.718063 {"bpm":-1,"spo2":-1}
124 2026-06-12T11:35:18.220188 {"bpm":-1,"spo2":-1}
125 2026-06-12T11:35:18.722138 {"bpm":-1,"spo2":-1}
126 2026-06-12T11:35:19.223071 {"bpm":-1,"spo2":-1}
127 2026-06-12T11:35:19.725082 {"bpm":-1,"spo2":-1}
128 2026-06-12T11:35:20.227142 {"bpm":-1,"spo2":-1}
129 2026-06-12T11:35:20.728106 {"bpm":-1,"spo2":-1}
130 2026-06-12T11:35:21.230101 {"bpm":-1,"spo2":-1}
131 2026-06-12T11:35:21.731071 {"bpm":-1,"spo2":-1}
132 2026-06-12T11:35:22.233061 {"bpm":-1,"spo2":-1}
133 2026-06-12T11:35:22.735075 {"bpm":-1,"spo2":-1}
134 2026-06-12T11:35:23.236111 {"bpm":-1,"spo2":-1}
135 2026-06-12T11:35:23.738588 {"bpm":-1,"spo2":-1}
136 2026-06-12T11:35:24.239134 {"bpm":-1,"spo2":-1}
137 2026-06-12T11:35:24.741129 {"bpm":-1,"spo2":-1}
138 2026-06-12T11:35:25.242099 {"bpm":-1,"spo2":-1}
139 2026-06-12T11:35:25.744096 {"bpm":-1,"spo2":-1}
140 2026-06-12T11:35:26.246144 {"bpm":-1,"spo2":-1}
141 2026-06-12T11:35:26.747135 {"bpm":-1,"spo2":-1}
142 2026-06-12T11:35:27.249471 {"bpm":-1,"spo2":-1}
143 2026-06-12T11:35:27.750167 {"bpm":-1,"spo2":-1}
144 2026-06-12T11:35:28.252514 {"bpm":-1,"spo2":-1}
145 2026-06-12T11:35:28.754229 {"bpm":-1,"spo2":-1}
146 2026-06-12T11:35:29.255536 {"bpm":-1,"spo2":-1}
147 2026-06-12T11:35:29.757130 {"bpm":-1,"spo2":-1}
148 2026-06-12T11:35:30.258461 {"bpm":-1,"spo2":-1}
149 2026-06-12T11:35:30.760323 {"bpm":-1,"spo2":-1}
150 2026-06-12T11:35:31.262249 {"bpm":-1,"spo2":-1}
151 2026-06-12T11:35:31.763243 {"bpm":-1,"spo2":-1}
152 2026-06-12T11:35:32.265482 {"bpm":-1,"spo2":-1}
153 2026-06-12T11:35:32.766157 {"bpm":-1,"spo2":-1}
154 2026-06-12T11:35:33.268510 {"bpm":-1,"spo2":-1}
155 2026-06-12T11:35:33.769203 {"bpm":-1,"spo2":-1}

106
receive.py Normal file
View File

@@ -0,0 +1,106 @@
#!/usr/bin/env python3
"""
Pulse oximeter UDP listener.
Receives heart-rate (BPM) and SpO2 readings from the device and prints them.
Run: python3 receive.py
"""
import socket
import datetime
import json
import csv
import sys
import os
HOST = "0.0.0.0"
PORT = 5005
def parse_payload(raw: bytes) -> dict | None:
"""Try JSON first, then common key=value or CSV formats."""
text = raw.decode(errors="replace").strip()
if not text:
return None
# JSON
if text.startswith("{"):
try:
return json.loads(text)
except json.JSONDecodeError:
pass
# key=value pairs (e.g. BPM=72,SPO2=98)
if "=" in text:
try:
return dict(part.split("=", 1) for part in text.replace(";", ",").split(",") if "=" in part)
except Exception:
pass
# Bare CSV: treat first field as BPM, second as SpO2
parts = text.split(",")
if len(parts) >= 2 and all(p.strip().lstrip("-").isdigit() for p in parts[:2]):
return {"BPM": parts[0].strip(), "SpO2": parts[1].strip()}
# Unknown — return raw
return {"raw": text}
def main():
sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
sock.bind((HOST, PORT))
log_path = os.path.join(os.path.dirname(__file__), "readings.csv")
write_header = not os.path.exists(log_path)
log_file = open(log_path, "a", newline="")
writer = csv.writer(log_file)
if write_header:
writer.writerow(["timestamp", "bpm", "spo2", "raw"])
print(f"Listening on UDP {HOST}:{PORT} | Logging to {log_path}")
print(f"{'TIME':12} {'BPM':>6} {'SpO2 %':>7} STATUS")
print("-" * 52)
try:
while True:
data, addr = sock.recvfrom(4096)
ts = datetime.datetime.now()
parsed = parse_payload(data)
def _get(d, *keys):
for k in keys:
if k in d:
return d[k]
return None
bpm_raw = _get(parsed, "bpm", "BPM", "heartrate")
spo2_raw = _get(parsed, "spo2", "SPO2", "SpO2", "oxygen")
raw = parsed.get("raw", "")
bpm_val = int(bpm_raw) if bpm_raw is not None else None
spo2_val = int(spo2_raw) if spo2_raw is not None else None
no_finger = (bpm_val is not None and bpm_val < 0) or \
(spo2_val is not None and spo2_val < 0)
bpm_str = "---" if (bpm_val is None or bpm_val < 0) else str(bpm_val)
spo2_str = "---" if (spo2_val is None or spo2_val < 0) else str(spo2_val)
status = "No finger" if no_finger else (f"({raw})" if raw else "OK")
print(f"{ts.strftime('%H:%M:%S.%f')[:12]} {bpm_str:>6} {spo2_str:>7} {status}")
sys.stdout.flush()
# Log only rows where valid readings exist
log_bpm = bpm_val if (bpm_val is not None and bpm_val >= 0) else ""
log_spo2 = spo2_val if (spo2_val is not None and spo2_val >= 0) else ""
writer.writerow([ts.isoformat(), log_bpm, log_spo2, data.decode(errors="replace").strip()])
log_file.flush()
except KeyboardInterrupt:
print("\nStopped.")
finally:
sock.close()
log_file.close()
if __name__ == "__main__":
main()