Files
ContecMonitor/contec_discovery.py
T

247 lines
9.1 KiB
Python

"""
Contec CMS7000PLUS / CMS9200PLUS Discovery Tool
==================================
This script listens on multiple TCP ports simultaneously to discover
what data the CMS7000PLUS / CMS9200PLUS patient monitor sends over the network.
Usage:
python contec_discovery.py
It will listen on ports: 511, 515, 516, 517, 518, 519, 520, 6060
and display all received data in hex + ASCII format for protocol analysis.
IMPORTANT: Run as Administrator (required for port 511 on Windows)
"""
import asyncio
import logging
import sys
import os
from datetime import datetime
# --- Configuration ---
LISTEN_HOST = "0.0.0.0"
DISCOVERY_PORTS = [511, 515, 516, 517, 518, 519, 520, 6060]
LOG_DIR = "captures"
# --- Setup Logging ---
logging.basicConfig(
level=logging.INFO,
format='%(asctime)s [%(levelname)s] %(message)s',
handlers=[logging.StreamHandler(sys.stdout)]
)
logger = logging.getLogger("contec_discovery")
def hex_dump(data: bytes, width: int = 16) -> str:
"""Format bytes as a hex dump with ASCII representation."""
lines = []
for i in range(0, len(data), width):
chunk = data[i:i + width]
hex_part = ' '.join(f'{b:02x}' for b in chunk)
ascii_part = ''.join(chr(b) if 32 <= b < 127 else '.' for b in chunk)
lines.append(f" {i:04x} {hex_part:<{width * 3}} |{ascii_part}|")
return '\n'.join(lines)
def try_decode_hl7(data: bytes) -> str | None:
"""Attempt to decode the data as an HL7 message."""
try:
# Strip MLLP framing if present
cleaned = data
if cleaned.startswith(b'\x0b'):
cleaned = cleaned[1:]
if b'\x1c\x0d' in cleaned:
cleaned = cleaned[:cleaned.find(b'\x1c\x0d')]
text = cleaned.decode('utf-8', errors='ignore').strip()
if 'MSH|' in text or 'MSH' in text[:20]:
return text
except Exception:
pass
return None
async def handle_discovery_client(
reader: asyncio.StreamReader,
writer: asyncio.StreamWriter,
port: int,
capture_file
):
"""Handle a single incoming connection and dump all data."""
client_ip, client_port = writer.get_extra_info('peername')
timestamp = datetime.now().strftime("%Y-%m-%d %H:%M:%S.%f")
print("\n" + "=" * 80)
print(f" NEW CONNECTION on port {port}")
print(f" From: {client_ip}:{client_port}")
print(f" Time: {timestamp}")
print("=" * 80)
capture_file.write(f"\n{'='*80}\n")
capture_file.write(f"Connection on port {port} from {client_ip}:{client_port} at {timestamp}\n")
capture_file.write(f"{'='*80}\n")
capture_file.flush()
packet_count = 0
try:
while True:
data = await asyncio.wait_for(reader.read(8192), timeout=30.0)
if not data:
break
packet_count += 1
recv_time = datetime.now().strftime("%H:%M:%S.%f")
print(f"\n--- Packet #{packet_count} on port {port} [{recv_time}] ({len(data)} bytes) ---")
print(hex_dump(data))
# Try to decode as HL7
hl7_text = try_decode_hl7(data)
if hl7_text:
print(f"\n >>> HL7 MESSAGE DETECTED <<<")
segments = hl7_text.replace('\n', '\r').split('\r')
for seg in segments:
seg = seg.strip()
if seg:
print(f" {seg}")
# Extract vital signs from OBX segments
if seg.startswith("OBX|"):
fields = seg.split('|')
if len(fields) > 5:
obs_id = fields[3] if len(fields) > 3 else "?"
obs_value = fields[5] if len(fields) > 5 else "?"
obs_unit = fields[6] if len(fields) > 6 else ""
print(f" -> Vital: {obs_id} = {obs_value} {obs_unit}")
# Also try plain text decode
try:
text = data.decode('utf-8', errors='ignore')
printable_ratio = sum(1 for c in text if c.isprintable() or c in '\r\n\t') / max(len(text), 1)
if printable_ratio > 0.7 and not hl7_text:
print(f"\n [Text representation]:")
for line in text.split('\r'):
line = line.strip()
if line:
print(f" {line}")
except Exception:
pass
# Write to capture file
capture_file.write(f"\nPacket #{packet_count} [{recv_time}] ({len(data)} bytes)\n")
capture_file.write(hex_dump(data) + "\n")
if hl7_text:
capture_file.write(f"HL7: {hl7_text}\n")
capture_file.write(f"Raw bytes: {data.hex()}\n")
capture_file.flush()
except asyncio.TimeoutError:
print(f" [Timeout - no data for 30s on port {port}]")
except asyncio.CancelledError:
pass
except ConnectionResetError:
print(f" [Connection reset by {client_ip} on port {port}]")
except Exception as e:
print(f" [Error on port {port}: {e}]")
finally:
print(f"\n Connection closed on port {port} from {client_ip} ({packet_count} packets received)")
writer.close()
try:
await writer.wait_closed()
except Exception:
pass
async def start_port_listener(port: int, capture_file):
"""Start a TCP server on a specific port."""
async def client_handler(reader, writer):
await handle_discovery_client(reader, writer, port, capture_file)
try:
server = await asyncio.start_server(client_handler, LISTEN_HOST, port)
logger.info(f" ✓ Listening on port {port}")
return server
except PermissionError:
logger.error(f" ✗ Port {port} - Permission denied (try running as Administrator)")
return None
except OSError as e:
if "already in use" in str(e).lower() or e.errno == 10048:
logger.error(f" ✗ Port {port} - Already in use by another application")
else:
logger.error(f" ✗ Port {port} - {e}")
return None
async def main():
print(r"""
╔══════════════════════════════════════════════════════════════╗
║ Contec CMS7000PLUS / CMS9200PLUS Discovery Tool ║
║ ║
║ This tool listens for incoming data from your patient ║
║ monitor to discover its communication protocol. ║
║ ║
║ SETUP REQUIRED: ║
║ 1. Connect monitor to your PC via Ethernet ║
║ 2. Set your PC's Ethernet IP to: 192.168.1.50 ║
║ (or whatever subnet your monitor uses) ║
║ 3. On the monitor, set CMS/Server IP to YOUR PC's IP ║
║ 4. Run this script as Administrator ║
║ ║
║ Press Ctrl+C to stop ║
╚══════════════════════════════════════════════════════════════╝
""")
# Create captures directory
os.makedirs(LOG_DIR, exist_ok=True)
capture_filename = os.path.join(
LOG_DIR,
f"capture_{datetime.now().strftime('%Y%m%d_%H%M%S')}.txt"
)
capture_file = open(capture_filename, 'w', encoding='utf-8')
logger.info(f"Capture log: {capture_filename}")
# Show current network config hint
print("\nStarting listeners on all discovery ports...")
print("(If port 511 fails, run PowerShell as Administrator)\n")
servers = []
for port in DISCOVERY_PORTS:
server = await start_port_listener(port, capture_file)
if server:
servers.append(server)
if not servers:
logger.error("No ports could be opened! Check permissions and existing services.")
capture_file.close()
return
active_ports = [s.sockets[0].getsockname()[1] for s in servers if s.sockets]
print(f"\n{'─' * 60}")
print(f" Active on ports: {active_ports}")
print(f" Waiting for data from CMS7000PLUS / CMS9200PLUS...")
print(f" Make sure the monitor's CMS/Server IP points to this PC")
print(f"{'─' * 60}\n")
try:
# Keep running until Ctrl+C
await asyncio.Event().wait()
except asyncio.CancelledError:
pass
finally:
print("\nShutting down...")
for server in servers:
server.close()
for server in servers:
await server.wait_closed()
capture_file.close()
print(f"Capture saved to: {capture_filename}")
if __name__ == "__main__":
try:
asyncio.run(main())
except KeyboardInterrupt:
print("\n\nDiscovery stopped by user.")