refactor: implement packet parsing heuristics and introduce device-level state caching to throttle database writes and vitals updates

This commit is contained in:
2026-07-11 14:24:14 +05:30
parent a4972b3251
commit 84b8acc904
32 changed files with 3499 additions and 180 deletions
@@ -0,0 +1,117 @@
# Findings summary and analysis artifact
"""
=== COMPLETE PACKET STRUCTURE DECODED ===
From monitor image: ECG=88, SpO2=98, NIBP=110/67 (MAP=82), TEMP=disconnected
--- 45-BYTE PACKET (Subtype 22) = REAL-TIME VITALS + NIBP ---
The 45-byte packet uses BIG-ENDIAN u16 format!
Bytes 14-19 contain the key vitals as individual bytes:
Packet at index 147 (captured around the same time as the image):
Bytes 14-19: [3, 110, 0, 67, 0, 82]
byte[14] = 3 → seconds (3)
byte[15] = 110 → NIBP Systolic = 110 ✅ (matches monitor 110)
byte[16] = 0 → high byte padding
byte[17] = 67 → NIBP Diastolic = 67 ✅ (matches monitor 67)
byte[18] = 0 → high byte padding
byte[19] = 82 → NIBP MAP = 82 ✅ (matches monitor 82)
Earlier packet at index 3:
Bytes 14-19: [40, 123, 0, 69, 0, 83]
byte[15] = 123 → Not NIBP (too high for that reading)
Wait - the readings CHANGED between packets. Let me check the full structure:
Index 3: bytes[15]=123, bytes[17]=69, bytes[19]=83
Index 147: bytes[15]=110, bytes[17]=67, bytes[19]=82
The index 147 values perfectly match the monitor!
And index 3 was captured earlier when NIBP values may have been different (cuff measurement changes).
Full 45-byte packet layout:
[0-1] Packet length (LE u16) = 45
[2-3] Marker: 01 46
[4-7] Sub-header
[8-9] Year (LE u16) = 2026
[10] Month = 7
[11] Day = 9
[12] Hour = 18 (0x12)
[13] Minute = 42 (0x2a) or 47 (0x2f)
[14] Second = 40 (0x28) or 3 (0x03)
[15] NIBP Systolic (single byte!)
[16-17] NIBP Diastolic (BE u16, but high byte usually 0)
[18-19] NIBP MAP (BE u16, but high byte usually 0)
[20-21] Alarm limit: SpO2 HIGH (LE u16) = 156 → WAIT that's 0x9c = 156
Hmm, let me re-examine. Looking at bytes starting at offset 14:
Packet 147: [03, 6e, 00, 43, 00, 52, 00, 9c, 00, 5a, 00, 02, 00, 5a, 00, 32, 00, 02, 00, 6a, 00, 3c, 00, 02, ...]
0x6e=110, 0x43=67, 0x52=82 → These are NIBP values!
So the structure from offset 14:
[14] Seconds
[15] NIBP Systolic = 110
[16] 0x00
[17] NIBP Diastolic = 67
[18] 0x00
[19] NIBP MAP = 82
[20-21] 0x009c = 156 (HR alarm HIGH)
[22-23] 0x005a = 90 (HR alarm LOW)
[24-25] 0x0002 = 2 (flag)
[26-27] 0x005a = 90 (SpO2 alarm...)
[28-29] 0x0032 = 50 (SpO2 alarm LOW)
[30-31] 0x0002 = 2 (flag)
[32-33] 0x006a = 106 (Resp alarm HIGH?)
[34-35] 0x003c = 60 (Resp alarm LOW?)
[36-37] 0x0002 = 2 (flag)
--- 286-BYTE PACKET (Subtype 21) = WAVEFORM + SpO2/HR SUMMARY ---
[264-265] SpO2 PR (pulse rate from SpO2 sensor) - changes: 255→98→99
This is NOT ECG HR, it's the SpO2 pulse rate!
[266-267] ECG HR: 9999 (sentinel = ECG disconnected or not reporting HR via ECG)
[268-269] 100 = SpO2 HIGH alarm limit (NOT live SpO2!)
[270-271] 90 = SpO2 LOW alarm limit
[272-273] 2 = flag
[274-275] 120 = ECG HR HIGH alarm limit
[276-277] 50 = ECG HR LOW alarm limit
[278-279] 1 = flag
So offset 264 in the 286-byte packet gives us:
SpO2 Pulse Rate (which equals SpO2% when sensor is connected)
But WAIT - the value 98 at offset 264 matches the SpO2 value (98%), not HR!
And ECG HR (88) is NOT in this packet at all.
Let me check: at index 74, offset 264 = 98, which matches SpO2 = 98%.
At index 186, offset 264 = 99, but the SpO2 was still 98 on monitor...
Actually, offset 264 could be the SpO2 Pulse Rate (PR), which is the heart rate
derived from the SpO2 sensor. When ECG is also connected, the ECG HR is shown
separately. But the PR from SpO2 sensor = 98 is close to the SpO2% = 98.
Hmm, actually both the SpO2 percentage AND the pulse rate from the SpO2 sensor
can have similar values. We need to figure out which is which.
--- 56-BYTE PACKET (Subtype 23) = FLOAT32 NIBP/TEMP ---
[8-11] f32=9999.0 → NIBP Systolic (sentinel = no measurement)
[12-15] f32=9999.0 → NIBP Diastolic (sentinel)
[16-19] f32=9999.0 → NIBP MAP (sentinel)
[20-23] f32=39.0 → TEMP alarm HIGH limit (NOT actual temp!)
[24-27] f32=36.0 → TEMP alarm LOW limit (NOT actual temp!)
--- 989-BYTE PACKET (Subtype 20) = Full waveform, no vitals in tail ---
No vital sign values found in this packet.
=== CONCLUSIONS ===
1. The 286-byte packet at offset 264 contains SpO2 PR (pulse rate), NOT ECG HR
2. ECG HR (88 bpm) is NOT transmitted in any of these packets!
The ECG HR is only available on the monitor's own display.
3. NIBP values (110/67/82) are in the 45-byte packet at bytes 15/17/19
4. Temperature 39.0 in the 56-byte packet is the ALARM HIGH limit, not actual temp
5. The actual temperature value is NOT transmitted (temp probe disconnected)
6. SpO2 percentage is NOT directly transmitted - only the SpO2 PR is at offset 264
(but these are often the same value when the sensor is working)
"""
print("Analysis complete - see code comments for findings")
+11
View File
@@ -0,0 +1,11 @@
import re
from collections import Counter
with open("/home/prathiyuman/Prathiyuman/ContecMonitor/fiveparaminte-main/hl7_forwarder.log", "r") as f:
content = f.read()
warnings = re.findall(r"unrecognized. Len: (\d+)", content)
lengths = [int(l) for l in warnings]
print("Unique unrecognized packet lengths and their frequencies:")
for length, count in Counter(lengths).most_common():
print(f" Length {length}: {count} times")
@@ -0,0 +1,39 @@
import re
import struct
# Helper to read uint16 from bytes
def read_u16_le(data: bytes, offset: int) -> int:
if offset + 2 > len(data):
return None
return struct.unpack_from('<H', data, offset)[0]
# Read the log file
with open("/home/prathiyuman/Prathiyuman/ContecMonitor/fiveparaminte-main/hl7_forwarder.log", "r") as f:
content = f.read()
# Find all occurrences of Hex head logging
hex_heads = re.findall(r"Raw hex \(first 32 bytes\): ([0-9a-fA-F]+)", content)
warnings = re.findall(r"unrecognized. Len: (\d+), Hex head: ([0-9a-fA-F]+)", content)
print(f"Found {len(hex_heads)} hex heads from server logging")
print(f"Found {len(warnings)} unrecognized packet hex heads")
# Also find all data packets printed or parsed in logs
# Let's inspect some of the unrecognized packets of size 56 and 45
print("\nSample of Len 56 packets:")
count = 0
for len_str, hex_str in warnings:
if len_str == "56":
print(f" {hex_str}")
count += 1
if count >= 5:
break
print("\nSample of Len 45 packets:")
count = 0
for len_str, hex_str in warnings:
if len_str == "45":
print(f" {hex_str}")
count += 1
if count >= 5:
break
@@ -0,0 +1,18 @@
import sqlite3
def main():
conn = sqlite3.connect("hl7_forwarder.db")
cursor = conn.cursor()
try:
cursor.execute("SELECT id, device_id, ip_address, status, last_seen FROM devices")
rows = cursor.fetchall()
print("=== Registered Devices ===")
for row in rows:
print(f"ID: {row[0]} | Device ID: {row[1]} | IP: {row[2]} | Status: {row[3]} | Last Seen: {row[4]}")
except Exception as e:
print("Error:", e)
finally:
conn.close()
if __name__ == "__main__":
main()
+56
View File
@@ -0,0 +1,56 @@
import struct
def read_u16_le(data: bytes, offset: int) -> int:
if offset + 2 > len(data):
return None
return struct.unpack_from('<H', data, offset)[0]
with open("/home/prathiyuman/Prathiyuman/ContecMonitor/fiveparaminte-main/scratch/raw_contec_stream.bin", "rb") as f:
stream = f.read()
# Extract 45-byte packets (subtype 22)
packets = []
consumed = 0
while consumed < len(stream) - 4:
b0 = stream[consumed]
b1 = stream[consumed + 1]
b2 = stream[consumed + 2]
b3 = stream[consumed + 3]
if (b2 == 0x04 or b2 == 0x01) and b3 == 0x46:
pkt_len = b0 | (b1 << 8)
if pkt_len == 45 and consumed + pkt_len <= len(stream):
packets.append(stream[consumed:consumed + pkt_len])
consumed += pkt_len
else:
if pkt_len < 5 or pkt_len > 8192:
consumed += 1
else:
consumed += pkt_len
else:
consumed += 1
print(f"Found {len(packets)} 45-byte packets:")
seen_timestamps = set()
for p in packets:
# Decode timestamp
year = read_u16_le(p, 8)
month = p[10]
day = p[11]
hour = p[12]
minute = p[13]
second = p[14]
ts_str = f"{year:04d}-{month:02d}-{day:02d} {hour:02d}:{minute:02d}:{second:02d}"
if ts_str in seen_timestamps:
continue
seen_timestamps.add(ts_str)
# Let's decode all uint16 fields from offset 15 onwards
fields = []
for offset in range(15, len(p) - 1, 2):
val = read_u16_le(p, offset)
fields.append(f"off{offset}:{val}")
print(f"Timestamp: {ts_str} | Payload: {p[15:].hex()}")
print(" Decoded uint16s: " + ", ".join(fields))
+40
View File
@@ -0,0 +1,40 @@
import struct
def read_f32_le(data: bytes, offset: int) -> float:
if offset + 4 > len(data):
return None
return struct.unpack_from('<f', data, offset)[0]
with open("/home/prathiyuman/Prathiyuman/ContecMonitor/fiveparaminte-main/scratch/raw_contec_stream.bin", "rb") as f:
stream = f.read()
# Extract 56-byte packets (subtype 23)
packets = []
consumed = 0
while consumed < len(stream) - 4:
b0 = stream[consumed]
b1 = stream[consumed + 1]
b2 = stream[consumed + 2]
b3 = stream[consumed + 3]
if (b2 == 0x04 or b2 == 0x01) and b3 == 0x46:
pkt_len = b0 | (b1 << 8)
if pkt_len == 56 and consumed + pkt_len <= len(stream):
packets.append(stream[consumed:consumed + pkt_len])
consumed += pkt_len
else:
if pkt_len < 5 or pkt_len > 8192:
consumed += 1
else:
consumed += pkt_len
else:
consumed += 1
print(f"Found {len(packets)} 56-byte packets:")
# Print the float32 values of the first packet at every possible offset
if packets:
p = packets[0]
print(f"Sample 56-byte packet: {p.hex()}")
for offset in range(8, len(p) - 3, 4):
val = read_f32_le(p, offset)
print(f" Offset {offset:02d}: {val}")
@@ -0,0 +1,87 @@
import struct
def read_u16_le(data: bytes, offset: int) -> int:
if offset + 2 > len(data):
return None
return struct.unpack_from('<H', data, offset)[0]
def read_f32_le(data: bytes, offset: int) -> float:
if offset + 4 > len(data):
return None
return struct.unpack_from('<f', data, offset)[0]
with open("/home/prathiyuman/Prathiyuman/ContecMonitor/fiveparaminte-main/scratch/raw_contec_stream.bin", "rb") as f:
stream = f.read()
print(f"Total stream size: {len(stream)} bytes")
# Extract packets
packets = []
consumed = 0
while consumed < len(stream) - 4:
b0 = stream[consumed]
b1 = stream[consumed + 1]
b2 = stream[consumed + 2]
b3 = stream[consumed + 3]
if (b2 == 0x04 or b2 == 0x01) and b3 == 0x46:
pkt_len = b0 | (b1 << 8)
if pkt_len < 5 or pkt_len > 8192:
consumed += 1
continue
if consumed + pkt_len <= len(stream):
packets.append(stream[consumed:consumed + pkt_len])
consumed += pkt_len
else:
break
else:
consumed += 1
print(f"Successfully extracted {len(packets)} packets")
# Analyze packet types and sizes
by_size = {}
for p in packets:
l = len(p)
subtype = p[6] if l > 6 else -1
key = (l, subtype)
if key not in by_size:
by_size[key] = []
by_size[key].append(p)
print("\nPacket breakdown (length, subtype): count")
for key, list_p in by_size.items():
print(f" Length {key[0]}, Subtype {key[1]}: {len(list_p)} packets")
# Dump first 2 packets of each type
for key, list_p in by_size.items():
print(f"\n{'='*60}")
print(f"Sample packets for Length {key[0]}, Subtype {key[1]}:")
print(f"{'='*60}")
for idx, p in enumerate(list_p[:2]):
print(f"Packet #{idx+1} hex:")
print(" " + p.hex())
# Let's search this packet for NIBP values (108, 53, 75) as uint16
print(" Looking for NIBP values (108, 53, 75) as uint16:")
found_u16 = []
for offset in range(0, len(p) - 1, 1):
val = read_u16_le(p, offset)
if val in [108, 53, 75, 88, 100]:
found_u16.append(f"offset {offset}: {val}")
if found_u16:
print(" uint16 matches: " + ", ".join(found_u16))
# Let's search as float32
print(" Looking for float32 values (108.0, 53.0, 75.0, 88.0, 100.0):")
found_f32 = []
for offset in range(0, len(p) - 3, 1):
val = read_f32_le(p, offset)
if val is not None:
# check if close to target values
for target in [108.0, 53.0, 75.0, 88.0, 100.0]:
if abs(val - target) < 0.1:
found_f32.append(f"offset {offset}: {val:.1f}")
if found_f32:
print(" float32 matches: " + ", ".join(found_f32))
@@ -0,0 +1,78 @@
"""
Search the entire 286-byte and 989-byte packets for the ECG HR value (88).
The ECG HR must be somewhere in the packet data.
"""
import struct
with open("/home/prathiyuman/Prathiyuman/ContecMonitor/fiveparaminte-main/scratch/raw_contec_stream.bin", "rb") as f:
stream = f.read()
# Find all 286-byte packets with SpO2=98 at offset 264
# (indicating a time when the monitor was showing ECG=88, SpO2=98)
packets = []
i = 0
while i < len(stream) - 8:
if (stream[i + 2] == 0x04 or stream[i + 2] == 0x01) and stream[i + 3] == 0x46:
pkt_len = struct.unpack_from('<H', stream, i)[0]
if 30 < pkt_len < 1050 and i + pkt_len <= len(stream):
packets.append((pkt_len, stream[i:i + pkt_len]))
i += max(pkt_len, 4)
else:
i += 1
# Search for byte value 88 (0x58) in 286-byte packets
print("=== Searching ALL offsets in 286-byte packets for u16=88 ===")
for idx, (l, pkt) in enumerate(packets):
if l == 286:
for off in range(4, l-1, 2):
v = struct.unpack_from('<H', pkt, off)[0]
if v == 88:
print(f" Packet {idx}, offset {off}: u16={v}")
# Also search single bytes
for off in range(4, l):
if pkt[off] == 88:
print(f" Packet {idx}, byte[{off}] = 88 (0x58)")
break # Just check first one
# Search 989-byte packets
print("\n=== Searching 989-byte packets for byte=88 in offsets 0-30 and 950-989 ===")
for idx, (l, pkt) in enumerate(packets):
if l == 989:
for off in range(0, 30):
if pkt[off] == 88:
print(f" Packet {idx}, byte[{off}] = 88")
for off in range(950, l):
if pkt[off] == 88:
print(f" Packet {idx}, byte[{off}] = 88")
break
# Search 45-byte packets
print("\n=== Searching 45-byte packets for byte=88 ===")
for idx, (l, pkt) in enumerate(packets):
if l == 45:
for off in range(8, l):
if pkt[off] == 88:
print(f" Packet {idx}, byte[{off}] = 88")
break
# The ECG HR may also be encoded differently. Let's check what's at byte
# positions right before the alarm limits in the 45-byte packet
print("\n=== 45-byte packet: All non-zero bytes with their positions ===")
for idx, (l, pkt) in enumerate(packets):
if l == 45:
for off in range(8, l):
if pkt[off] != 0:
print(f" byte[{off}] = {pkt[off]} (0x{pkt[off]:02x})")
break
# Check the 45-byte packet at index 147 specifically (when NIBP was 110/67/82)
print("\n=== 45-byte packet at the time of NIBP 110/67/82 ===")
count = 0
for idx, (l, pkt) in enumerate(packets):
if l == 45:
count += 1
if count > 36: # Skip to around packet index 147
for off in range(8, l):
if pkt[off] != 0:
print(f" byte[{off}] = {pkt[off]} (0x{pkt[off]:02x})")
break
@@ -0,0 +1,58 @@
import struct
filepath = "/home/prathiyuman/Prathiyuman/ContecMonitor/fiveparaminte-main/scratch/raw_contec_stream.bin"
try:
with open(filepath, "rb") as f:
stream = f.read()
except Exception as e:
print(f"Error: {e}")
import sys
sys.exit(1)
packets_288 = []
packets_989 = []
i = 0
while i < len(stream) - 4:
b0, b1, b2, b3 = stream[i], stream[i+1], stream[i+2], stream[i+3]
if b3 == 0x46:
pkt_len = b0 | (b1 << 8)
if pkt_len == 288 and i + pkt_len <= len(stream):
packets_288.append(stream[i:i + pkt_len])
elif pkt_len == 989 and i + pkt_len <= len(stream):
packets_989.append(stream[i:i + pkt_len])
i += max(pkt_len, 4)
else:
i += 1
print(f"Loaded {len(packets_288)} 288-byte packets and {len(packets_989)} 989-byte packets.")
# Search all offsets in 288-byte packets for any u16 LE value between 70 and 95
print("\nScanning 288-byte packets for u16 LE values in range [75, 95] across all packets:")
found_offsets_288 = {}
for idx, pkt in enumerate(packets_288):
for off in range(4, len(pkt) - 1, 2):
val = struct.unpack_from('<H', pkt, off)[0]
if 75 <= val <= 95:
found_offsets_288.setdefault(off, []).append((idx, val))
for off, matches in found_offsets_288.items():
if len(matches) > 5:
# print first few matches and total count
vals = [m[1] for m in matches]
print(f" Offset {off}: found {len(matches)} times. Values: {set(vals)}")
# Search all offsets in 989-byte packets for any u16 LE value between 70 and 95
print("\nScanning 989-byte packets for u16 LE values in range [75, 95] across all packets:")
found_offsets_989 = {}
for idx, pkt in enumerate(packets_989):
# ECG packets are mostly waveform data in the first 900 bytes, so let's check from 900 to 988
for off in range(900, len(pkt) - 1, 2):
val = struct.unpack_from('<H', pkt, off)[0]
if 75 <= val <= 95:
found_offsets_989.setdefault(off, []).append((idx, val))
for off, matches in found_offsets_989.items():
if len(matches) > 5:
vals = [m[1] for m in matches]
print(f" Offset {off}: found {len(matches)} times. Values: {set(vals)}")
@@ -0,0 +1,32 @@
import sys
filepath = "/home/prathiyuman/Prathiyuman/ContecMonitor/fiveparaminte-main/scratch/raw_contec_stream.bin"
try:
with open(filepath, "rb") as f:
f.seek(0, 2)
size = f.tell()
# Read the last 2000 bytes
f.seek(max(0, size - 2000))
data = f.read()
except Exception as e:
print(f"Error reading file: {e}")
sys.exit(1)
print(f"Total file size: {size} bytes")
print(f"Read last {len(data)} bytes of stream.")
# Let's search for potential packet headers in the last 2000 bytes.
# Typically headers are like: [len_lo] [len_hi] [04/01] [46]
# Let's print out the hex around any occurrences of 0x46 or 0x47.
print("\nScanning for potential 0x46 or 0x47 headers...")
for i in range(len(data) - 4):
b0, b1, b2, b3 = data[i], data[i+1], data[i+2], data[i+3]
if b3 == 0x46:
pkt_len = b0 | (b1 << 8)
print(f"Index {i} (absolute {size - 2000 + i}): Header match! Bytes: {b0:02x} {b1:02x} {b2:02x} {b3:02x} -> Length: {pkt_len}, Subtype: {b2}")
# print hex representation of next 16 bytes
next_bytes = data[i:i+20]
print(f" Hex: {next_bytes.hex()}")
elif b3 == 0x47:
print(f"Index {i} (absolute {size - 2000 + i}): End-marker? Bytes: {b0:02x} {b1:02x} {b2:02x} {b3:02x}")
@@ -0,0 +1,83 @@
import socket
import time
import random
from datetime import datetime
HOST = "127.0.0.1"
PORT = 12345
VT = b'\x0b'
FS_CR = b'\x1c\x0d'
def generate_hl7_message(patient_id="PAT-12345", device_id="MOCK-CMS7000"):
# Generate realistic vital signs with small random variations
hr = random.randint(70, 95)
spo2 = random.randint(96, 99)
sys = random.randint(115, 125)
dia = random.randint(75, 83)
map_bp = int(dia + (sys - dia) / 3)
resp = random.randint(14, 18)
temp = round(random.uniform(36.5, 37.1), 1)
now_str = datetime.now().strftime("%Y%m%d%H%M%S")
# Construct HL7 message segments
msh = f"MSH|^~\\&|{device_id}|MOCK_FACILITY|RECEIVING_APP|RECEIVING_FACILITY|{now_str}||ORU^R01|MSG{now_str}|P|2.3.1\r"
pid = f"PID|||{patient_id}^Doe^John||19800101|M\r"
pv1 = "PV1||I|ICU^Bed1^Room1||||||||||||||||1001\r"
# OBX segments for each vital parameter
obx_hr = f"OBX|1|NM|HR^Heart Rate|1|{hr}|bpm|60-100|N|||F\r"
obx_spo2 = f"OBX|2|NM|SPO2^Oxygen Saturation|1|{spo2}|%|95-100|N|||F\r"
obx_sys = f"OBX|3|NM|SYS^Systolic BP|1|{sys}|mmHg|90-140|N|||F\r"
obx_dia = f"OBX|4|NM|DIA^Diastolic BP|1|{dia}|mmHg|60-90|N|||F\r"
obx_map = f"OBX|5|NM|MAP^Mean Arterial Pressure|1|{map_bp}|mmHg|70-105|N|||F\r"
obx_resp = f"OBX|6|NM|RESP^Respiratory Rate|1|{resp}|/min|12-20|N|||F\r"
obx_temp = f"OBX|7|NM|TEMP^Temperature|1|{temp}|C|36.1-37.2|N|||F"
hl7_msg = msh + pid + pv1 + obx_hr + obx_spo2 + obx_sys + obx_dia + obx_map + obx_resp + obx_temp
return hl7_msg
def main():
print(f"Starting mock patient monitor simulator...")
print(f"Connecting to Contec receiver at {HOST}:{PORT}...")
while True:
try:
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
s.connect((HOST, PORT))
print(f"Connected to Contec server at {HOST}:{PORT}")
while True:
hl7_msg = generate_hl7_message()
# Frame message with MLLP wrappers (VT ... FS_CR)
framed_msg = VT + hl7_msg.encode('utf-8') + FS_CR
print(f"[{datetime.now().strftime('%H:%M:%S')}] Sending mock HL7 vitals:")
# Print individual observations for clear console trace
for line in hl7_msg.split('\r'):
if line.startswith("OBX"):
print(f" {line}")
s.sendall(framed_msg)
# Check for ACK response
try:
s.settimeout(1.0)
response = s.recv(4096)
if response:
print(" [ACK Received]")
except socket.timeout:
pass
time.sleep(2)
except (ConnectionRefusedError, ConnectionResetError) as e:
print(f"Connection error: {e}. Retrying in 3 seconds...")
time.sleep(3)
except Exception as e:
print(f"Unexpected error: {e}. Retrying in 3 seconds...")
time.sleep(3)
if __name__ == "__main__":
main()
@@ -0,0 +1,73 @@
import socket
import time
import random
from datetime import datetime
import threading
HOST = "127.0.0.1"
PORT = 12345
VT = b'\x0b'
FS_CR = b'\x1c\x0d'
def generate_hl7_message(patient_id, device_id):
hr = random.randint(70, 95)
spo2 = random.randint(96, 99)
sys = random.randint(115, 125)
dia = random.randint(75, 83)
map_bp = int(dia + (sys - dia) / 3)
resp = random.randint(14, 18)
temp = round(random.uniform(36.5, 37.1), 1)
now_str = datetime.now().strftime("%Y%m%d%H%M%S")
msh = f"MSH|^~\\&|{device_id}|MOCK_FACILITY|RECEIVING_APP|RECEIVING_FACILITY|{now_str}||ORU^R01|MSG{now_str}|P|2.3.1\r"
pid = f"PID|||{patient_id}^Doe^John||19800101|M\r"
pv1 = "PV1||I|ICU^Bed1^Room1||||||||||||||||1001\r"
obx_hr = f"OBX|1|NM|HR^Heart Rate|1|{hr}|bpm|60-100|N|||F\r"
obx_spo2 = f"OBX|2|NM|SPO2^Oxygen Saturation|1|{spo2}|%|95-100|N|||F\r"
obx_sys = f"OBX|3|NM|SYS^Systolic BP|1|{sys}|mmHg|90-140|N|||F\r"
obx_dia = f"OBX|4|NM|DIA^Diastolic BP|1|{dia}|mmHg|60-90|N|||F\r"
obx_map = f"OBX|5|NM|MAP^Mean Arterial Pressure|1|{map_bp}|mmHg|70-105|N|||F\r"
obx_resp = f"OBX|6|NM|RESP^Respiratory Rate|1|{resp}|/min|12-20|N|||F\r"
obx_temp = f"OBX|7|NM|TEMP^Temperature|1|{temp}|C|36.1-37.2|N|||F"
hl7_msg = msh + pid + pv1 + obx_hr + obx_spo2 + obx_sys + obx_dia + obx_map + obx_resp + obx_temp
return hl7_msg
def run_monitor(patient_id, device_id, interval):
print(f"Starting simulated {device_id}...")
while True:
try:
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
s.connect((HOST, PORT))
print(f"[{device_id}] Connected to server")
while True:
hl7_msg = generate_hl7_message(patient_id, device_id)
framed_msg = VT + hl7_msg.encode('utf-8') + FS_CR
s.sendall(framed_msg)
try:
s.settimeout(1.0)
response = s.recv(4096)
except socket.timeout:
pass
time.sleep(interval)
except Exception as e:
print(f"[{device_id}] Error: {e}. Reconnecting in 3 seconds...")
time.sleep(3)
def main():
t1 = threading.Thread(target=run_monitor, args=("PAT-1001", "CMS7000PLUS_127.0.0.1", 3), daemon=True)
t2 = threading.Thread(target=run_monitor, args=("PAT-8500", "CMS8500_127.0.0.1", 4), daemon=True)
t1.start()
t2.start()
try:
while True:
time.sleep(1)
except KeyboardInterrupt:
print("Stopping simulators.")
if __name__ == "__main__":
main()
Binary file not shown.
@@ -0,0 +1,28 @@
import re
import struct
def read_u16_le(data: bytes, offset: int) -> int:
if offset + 2 > len(data):
return None
return struct.unpack_from('<H', data, offset)[0]
# Let's read the log line by line and reconstruct packets
with open("/home/prathiyuman/Prathiyuman/ContecMonitor/fiveparaminte-main/hl7_forwarder.log", "r") as f:
lines = f.readlines()
# We want to find raw reads and buffer state
# The server logs:
# "Received {len} bytes from ... on port 511 (buf={len})"
# "Raw hex (first 32 bytes): {hex}"
# Wait, did the server ever log the full packet data?
# Let's look at the log lines.
print("Searching for raw hex data logs...")
hex_patterns = []
for line in lines[:5000]:
m = re.search(r"Raw hex \(first 32 bytes\): ([0-9a-fA-F]+)", line)
if m:
hex_patterns.append(m.group(1))
print(f"Total raw hex logs: {len(hex_patterns)}")
if hex_patterns:
print(f"Example raw hex: {hex_patterns[0]}")
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,51 @@
import struct
def search_scaled_values(p: bytes, val_sys: int, val_dia: int, val_map: int):
# We will search for scaled values: val, val * 10, val * 100
targets = [
(val_sys, "Sys"), (val_sys * 10, "Sys*10"), (val_sys * 100, "Sys*100"),
(val_dia, "Dia"), (val_dia * 10, "Dia*10"), (val_dia * 100, "Dia*100"),
(val_map, "Map"), (val_map * 10, "Map*10"), (val_map * 100, "Map*100")
]
for offset in range(len(p) - 1):
v = struct.unpack_from('<H', p, offset)[0]
for target_val, name in targets:
if v == target_val:
print(f" Found {name} ({target_val}) at offset {offset}")
with open("/home/prathiyuman/Prathiyuman/ContecMonitor/fiveparaminte-main/scratch/raw_contec_stream.bin", "rb") as f:
stream = f.read()
consumed = 0
packets = []
while consumed < len(stream) - 4:
b0, b1, b2, b3 = stream[consumed:consumed+4]
if (b2 == 0x04 or b2 == 0x01) and b3 == 0x46:
pkt_len = b0 | (b1 << 8)
packets.append(stream[consumed:consumed+pkt_len])
consumed += pkt_len
else:
consumed += 1
print(f"Scanning {len(packets)} packets for scaled BP (108, 53, 75)...")
for idx, p in enumerate(packets):
l = len(p)
subtype = p[6] if l > 6 else -1
# We want to print only if something is found
# Redirect output internally
import io
import sys
old_stdout = sys.stdout
new_stdout = io.StringIO()
sys.stdout = new_stdout
search_scaled_values(p, 108, 53, 75)
output = new_stdout.getvalue()
sys.stdout = old_stdout
if output.strip():
print(f"Packet #{idx} (Len={l}, Subtype={subtype}):")
print(output, end="")
@@ -0,0 +1,63 @@
import struct
def search_value_in_packet(p: bytes, val: int, label: str):
# Search as uint8
for offset in range(len(p)):
if p[offset] == val:
print(f" {label} found as uint8 at offset {offset}")
# Search as uint16 LE
for offset in range(len(p) - 1):
v = struct.unpack_from('<H', p, offset)[0]
if v == val:
print(f" {label} found as uint16 LE at offset {offset}")
# Search as float32 LE
for offset in range(len(p) - 3):
v = struct.unpack_from('<f', p, offset)[0]
if abs(v - float(val)) < 0.01:
print(f" {label} found as float32 LE at offset {offset}")
with open("/home/prathiyuman/Prathiyuman/ContecMonitor/fiveparaminte-main/scratch/raw_contec_stream.bin", "rb") as f:
stream = f.read()
# Extract packets
packets = []
consumed = 0
while consumed < len(stream) - 4:
b0 = stream[consumed]
b1 = stream[consumed + 1]
b2 = stream[consumed + 2]
b3 = stream[consumed + 3]
if (b2 == 0x04 or b2 == 0x01) and b3 == 0x46:
pkt_len = b0 | (b1 << 8)
if pkt_len < 5 or pkt_len > 8192:
consumed += 1
continue
if consumed + pkt_len <= len(stream):
packets.append(stream[consumed:consumed + pkt_len])
consumed += pkt_len
else:
break
else:
consumed += 1
print(f"Loaded {len(packets)} packets. Scanning for HR=88, Systolic=108, MAP=75...")
for idx, p in enumerate(packets):
l = len(p)
subtype = p[6] if l > 6 else -1
# Let's decode timestamp if it is a 45-byte packet
ts_str = ""
if l == 45:
year = struct.unpack_from('<H', p, 8)[0]
month, day, hour, minute, second = p[10], p[11], p[12], p[13], p[14]
ts_str = f" @ {year}-{month:02d}-{day:02d} {hour:02d}:{minute:02d}:{second:02d}"
print(f"\nPacket #{idx} (Len={l}, Subtype={subtype}){ts_str}:")
search_value_in_packet(p, 88, "HR (88)")
search_value_in_packet(p, 108, "Systolic (108)")
search_value_in_packet(p, 75, "MAP (75)")
@@ -0,0 +1,21 @@
with open("/home/prathiyuman/Prathiyuman/ContecMonitor/fiveparaminte-main/scratch/raw_contec_stream.bin", "rb") as f:
stream = f.read()
packets = []
i = 0
while i < len(stream) - 4:
b0, b1, b2, b3 = stream[i], stream[i+1], stream[i+2], stream[i+3]
if b3 == 0x46:
pkt_len = b0 | (b1 << 8)
if pkt_len == 288 and i + pkt_len <= len(stream):
packets.append(stream[i:i + pkt_len])
i += max(pkt_len, 4)
else:
i += 1
if packets:
pkt = packets[-1]
print("Last 40 bytes of 288-byte packet:")
for off in range(248, 288, 2):
val = pkt[off] | (pkt[off+1] << 8)
print(f" Offset {off}: hex={pkt[off]:02x} {pkt[off+1]:02x} -> u16 LE = {val}")
@@ -0,0 +1,21 @@
with open("/home/prathiyuman/Prathiyuman/ContecMonitor/fiveparaminte-main/scratch/raw_contec_stream.bin", "rb") as f:
stream = f.read()
packets = []
i = 0
while i < len(stream) - 4:
b0, b1, b2, b3 = stream[i], stream[i+1], stream[i+2], stream[i+3]
if b3 == 0x46:
pkt_len = b0 | (b1 << 8)
if pkt_len == 989 and i + pkt_len <= len(stream):
packets.append(stream[i:i + pkt_len])
i += max(pkt_len, 4)
else:
i += 1
if packets:
pkt = packets[-1]
print("Last 100 bytes of 989-byte packet:")
for off in range(889, 989, 2):
val = pkt[off] | (pkt[off+1] << 8)
print(f" Offset {off}: hex={pkt[off]:02x} {pkt[off+1]:02x} -> u16 LE = {val}")
@@ -0,0 +1,41 @@
import struct
filepath = "/home/prathiyuman/Prathiyuman/ContecMonitor/fiveparaminte-main/scratch/raw_contec_stream.bin"
try:
with open(filepath, "rb") as f:
stream = f.read()
except Exception as e:
print(f"Error: {e}")
import sys
sys.exit(1)
# Find latest 288-byte and 989-byte packets
pkt_288 = None
pkt_989 = None
i = 0
while i < len(stream) - 4:
b0, b1, b2, b3 = stream[i], stream[i+1], stream[i+2], stream[i+3]
if b3 == 0x46:
pkt_len = b0 | (b1 << 8)
if pkt_len == 288 and i + pkt_len <= len(stream):
pkt_288 = stream[i:i + pkt_len]
elif pkt_len == 989 and i + pkt_len <= len(stream):
pkt_989 = stream[i:i + pkt_len]
i += max(pkt_len, 4)
else:
i += 1
if pkt_288:
print("=== Scanning 288-byte packet (tail, offsets 250-287) for values 75-90 ===")
for off in range(250, len(pkt_288) - 1, 2):
val = struct.unpack_from('<H', pkt_288, off)[0]
if 75 <= val <= 95:
print(f" Offset {off}: u16 LE = {val}")
if pkt_989:
print("\n=== Scanning 989-byte packet (tail, offsets 900-988) for values 75-90 ===")
for off in range(900, len(pkt_989) - 1, 2):
val = struct.unpack_from('<H', pkt_989, off)[0]
if 75 <= val <= 95:
print(f" Offset {off}: u16 LE = {val}")
@@ -0,0 +1,49 @@
import sys
import struct
sys.path.insert(0, "/home/prathiyuman/Prathiyuman/ContecMonitor/fiveparaminte-main")
from contec_parser import parse_contec_binary_packet
with open("/home/prathiyuman/Prathiyuman/ContecMonitor/fiveparaminte-main/scratch/raw_contec_stream.bin", "rb") as f:
stream = f.read()
i = 0
pkts = []
while i < len(stream) - 8:
if (stream[i + 2] == 0x04 or stream[i + 2] == 0x01) and stream[i + 3] == 0x46:
pkt_len = struct.unpack_from('<H', stream, i)[0]
if 30 < pkt_len < 1050 and i + pkt_len <= len(stream):
pkts.append(stream[i:i+pkt_len])
i += max(pkt_len, 4)
else:
i += 1
print(f"Loaded {len(pkts)} packets.")
vitals_cache = {
"heart_rate": None,
"spo2": None,
"temperature": None,
"systolic_bp": None,
"diastolic_bp": None,
"map_bp": None,
}
last_printed = {}
for idx, pkt in enumerate(pkts):
vitals = parse_contec_binary_packet(pkt, "192.168.100.120")
if vitals:
# Merge like contec_server.py
changed = False
for field in vitals.present_fields or []:
val = getattr(vitals, field)
if vitals_cache[field] != val:
vitals_cache[field] = val
changed = True
if changed:
current = {k: v for k, v in vitals_cache.items()}
if current != last_printed:
print(f"Pkt #{idx} (len={len(pkt)}) -> Cache: {current}")
last_printed = current
+114
View File
@@ -0,0 +1,114 @@
"""
Verify the new parser offsets on the raw captured stream.
New mappings:
- HR: 989-byte packet, offset 904 (u16 LE)
- SpO2: 286-byte packet, offset 264 (u16 LE)
- Temperature: 56-byte packet, offset 8 (float32)
- NIBP: 45-byte packet, offset 15, 17, 19 (u16 LE)
"""
import struct
def read_u16_le(data, offset):
if offset + 2 > len(data):
return None
return struct.unpack_from('<H', data, offset)[0]
def read_f32_le(data, offset):
if offset + 4 > len(data):
return None
return struct.unpack_from('<f', data, offset)[0]
with open("/home/prathiyuman/Prathiyuman/ContecMonitor/fiveparaminte-main/scratch/raw_contec_stream.bin", "rb") as f:
stream = f.read()
# Parse packets
i = 0
pkts = []
while i < len(stream) - 8:
if (stream[i + 2] == 0x04 or stream[i + 2] == 0x01) and stream[i + 3] == 0x46:
pkt_len = struct.unpack_from('<H', stream, i)[0]
if 30 < pkt_len < 1050 and i + pkt_len <= len(stream):
pkts.append((pkt_len, stream[i:i + pkt_len]))
i += max(pkt_len, 4)
else:
i += 1
vitals_cache = {
"heart_rate": None,
"spo2": None,
"temperature": None,
"systolic_bp": None,
"diastolic_bp": None,
"map_bp": None,
}
last_printed = {}
for idx, (pkt_len, pkt) in enumerate(pkts):
changed = False
if pkt_len == 989:
hr = read_u16_le(pkt, 904)
# 65535 or 9999 means disconnected
if hr is not None and hr != 65535 and hr != 9999 and 20 <= hr <= 300:
if vitals_cache["heart_rate"] != float(hr):
vitals_cache["heart_rate"] = float(hr)
changed = True
elif hr == 65535 or hr == 9999:
if vitals_cache["heart_rate"] is not None:
vitals_cache["heart_rate"] = None
changed = True
elif pkt_len == 286:
spo2 = read_u16_le(pkt, 264)
if spo2 is not None and spo2 != 65535 and spo2 != 255 and 50 <= spo2 <= 100:
if vitals_cache["spo2"] != float(spo2):
vitals_cache["spo2"] = float(spo2)
changed = True
elif spo2 == 65535 or spo2 == 255:
if vitals_cache["spo2"] is not None:
vitals_cache["spo2"] = None
changed = True
elif pkt_len == 56:
temp = read_f32_le(pkt, 8)
if temp is not None and abs(temp - 9999.0) > 1.0 and 30.0 < temp < 45.0:
if vitals_cache["temperature"] != round(temp, 1):
vitals_cache["temperature"] = round(temp, 1)
changed = True
elif temp is not None and abs(temp - 9999.0) <= 1.0:
if vitals_cache["temperature"] is not None:
vitals_cache["temperature"] = None
changed = True
elif pkt_len == 45:
sys_bp = read_u16_le(pkt, 15)
dia_bp = read_u16_le(pkt, 17)
map_bp = read_u16_le(pkt, 19)
# 9999 means no NIBP measurement active
if sys_bp is not None and sys_bp != 9999 and sys_bp > 0:
vitals_cache["systolic_bp"] = float(sys_bp)
changed = True
elif sys_bp == 9999 or sys_bp == 0:
if vitals_cache["systolic_bp"] is not None:
vitals_cache["systolic_bp"] = None
changed = True
if dia_bp is not None and dia_bp != 9999 and dia_bp > 0:
vitals_cache["diastolic_bp"] = float(dia_bp)
changed = True
elif dia_bp == 9999 or dia_bp == 0:
if vitals_cache["diastolic_bp"] is not None:
vitals_cache["diastolic_bp"] = None
changed = True
if map_bp is not None and map_bp != 9999 and map_bp > 0:
vitals_cache["map_bp"] = float(map_bp)
changed = True
elif map_bp == 9999 or map_bp == 0:
if vitals_cache["map_bp"] is not None:
vitals_cache["map_bp"] = None
changed = True
if changed:
current_vitals = {k: v for k, v in vitals_cache.items()}
if current_vitals != last_printed:
print(f"Pkt #{idx} (len={pkt_len}) -> Vitals: {current_vitals}")
last_printed = current_vitals
@@ -0,0 +1,50 @@
import sys
import os
# Add parent directory to path
sys.path.append(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
from contec_parser import parse_contec_binary_packet
# Read captured stream
filepath = "/home/prathiyuman/Prathiyuman/ContecMonitor/fiveparaminte-main/scratch/raw_contec_stream.bin"
try:
with open(filepath, "rb") as f:
stream = f.read()
except Exception as e:
print(f"Error: {e}")
sys.exit(1)
print(f"Loaded raw stream of {len(stream)} bytes")
# We will scan the stream and feed packets into the parser
# Since we know the packets start with 0x46 at index + 3, let's scan.
i = 0
packet_count = 0
parsed_count = 0
last_vitals = None
while i < len(stream) - 4:
b0, b1, b2, b3 = stream[i], stream[i+1], stream[i+2], stream[i+3]
if b3 == 0x46:
pkt_len = b0 | (b1 << 8)
if 30 < pkt_len < 1050 and i + pkt_len <= len(stream):
pkt_data = stream[i:i + pkt_len]
packet_count += 1
# Feed packet to parser
vitals = parse_contec_binary_packet(pkt_data, "192.168.100.139")
if vitals:
parsed_count += 1
# Check if fields changed
v_dict = {k: v for k, v in vitals.model_dump().items() if v is not None and k not in ['device_id', 'patient_id', 'timestamp', 'ip_address', 'present_fields']}
if v_dict and v_dict != last_vitals:
print(f"Index {i} (len={pkt_len}) -> Vitals: {v_dict}")
last_vitals = v_dict
i += max(pkt_len, 4)
else:
i += 1
else:
i += 1
print(f"\nDone. Scanned {packet_count} packets, successfully parsed {parsed_count} packets.")