diff --git a/server/hl7_receiver.py b/server/hl7_receiver.py new file mode 100644 index 0000000000000000000000000000000000000000..39d75d2a994222610af94b3e383a45f1d69a45c4 --- /dev/null +++ b/server/hl7_receiver.py @@ -0,0 +1,286 @@ +import socket +import datetime +import uuid +import os +import json +from pathlib import Path +import hl7 +import logging +from typing import Dict, Any +from flask import Flask, request, jsonify +from flask_cors import CORS +# import threading # Not needed when TCP server is disabled + +app = Flask(__name__) +CORS(app) + +class HL7MessageReceiver: + def __init__(self, host: str = 'localhost', port: int = 6661): + """Initialize the HL7 message receiver. + + Args: + host (str): Host to listen on + port (int): Port to listen on + """ + self.host = host + self.port = port + self.setup_logging() + self.setup_directories() + + def setup_logging(self): + """Set up logging configuration.""" + logging.basicConfig( + level=logging.INFO, + format='%(asctime)s - %(name)s - %(levelname)s - %(message)s', + handlers=[ + logging.FileHandler('hl7_receiver.log'), + logging.StreamHandler() + ] + ) + self.logger = logging.getLogger('HL7Receiver') + + def setup_directories(self): + """Create necessary directories for message storage.""" + # Create base directories + Path('received_messages').mkdir(exist_ok=True) + Path('message_logs').mkdir(exist_ok=True) + + def generate_filename(self, msg_type: str, timestamp: datetime.datetime) -> str: + """Generate a filename based on message type and timestamp. + + Args: + msg_type (str): The HL7 message type + timestamp (datetime.datetime): Message receipt timestamp + + Returns: + str: Generated filename + """ + date_str = timestamp.strftime('%Y%m%d_%H%M%S') + return f"received_messages/{msg_type}_{date_str}.txt" + + def process_message(self, raw_message: str) -> Dict[str, Any]: + """Process received HL7 message. + + Args: + raw_message (str): Raw HL7 message string + + Returns: + Dict[str, Any]: Processed message info + """ + # Parse the HL7 message + try: + # Remove leading/trailing whitespace and ensure proper line endings + raw_message = '\r'.join(line.strip() for line in raw_message.splitlines()) + if not raw_message.endswith('\r'): + raw_message += '\r' + + self.logger.info(f"Attempting to parse message: {raw_message}") + parsed_msg = hl7.parse(raw_message) + + # Extract message type more safely + try: + # Get MSH segment + msh = parsed_msg.segments('MSH')[0] + + # MSH-9 contains the message type in format "ADT^A01" + message_type_field = str(msh[9]) + + # Split the message type field on '^' to get type and trigger + type_parts = message_type_field.split('^') + message_type = type_parts[0] if len(type_parts) > 0 else "UNKNOWN" + trigger_event = type_parts[1] if len(type_parts) > 1 else "" + + # Combine message type and trigger + msg_type = f"{message_type}_{trigger_event}" if trigger_event else message_type + + self.logger.info(f"Extracted message type: {msg_type} from field: {message_type_field}") + + except Exception as e: + self.logger.error(f"Error extracting message type: {str(e)}") + msg_type = "UNKNOWN" + + # Generate timestamp and ID + timestamp = datetime.datetime.now() + msg_id = str(uuid.uuid4()) + + # Create message info dictionary + msg_info = { + 'id': msg_id, + 'timestamp': timestamp.isoformat(), + 'type': msg_type, + 'raw_message': raw_message, + 'parsed_segments': [str(seg) for seg in parsed_msg] + } + + # Save message to file + filename = self.generate_filename(msg_type, timestamp) + with open(filename, 'w', encoding='utf-8') as f: + f.write(f"Message ID: {msg_id}\n") + f.write(f"Timestamp: {timestamp.isoformat()}\n") + f.write(f"Message Type: {msg_type}\n") + f.write("\nRaw Message:\n") + f.write(raw_message) + f.write("\n\nParsed Segments:\n") + for segment in parsed_msg: + f.write(f"{str(segment)}\n") + + # Log message receipt + self.log_message(msg_info) + + self.logger.info(f"Successfully processed message {msg_id} of type {msg_type}") + return msg_info + + except Exception as e: + self.logger.error(f"Error processing message: {str(e)}") + self.logger.error(f"Raw message was: {raw_message}") + raise ValueError(f"Failed to process HL7 message: {str(e)}") + + def log_message(self, msg_info: Dict[str, Any]): + """Log message information to the message log file. + + Args: + msg_info (Dict[str, Any]): Message information to log + """ + log_file = f"message_logs/messages_{datetime.date.today()}.log" + + with open(log_file, 'a', encoding='utf-8') as f: + json.dump(msg_info, f) + f.write('\n') + + # TCP Server functionality commented out + """ + def start_tcp_server(self): + #Start the HL7 message receiver TCP server. + self.logger.info(f"Starting HL7 TCP receiver on {self.host}:{self.port}") + + server_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM) + server_socket.bind((self.host, self.port)) + server_socket.listen(5) + + try: + while True: + client_socket, address = server_socket.accept() + self.logger.info(f"Connection from {address}") + + try: + # Receive message + message = '' + while True: + data = client_socket.recv(4096).decode('utf-8') + if not data: + break + message += data + if '\r' in data: # HL7 messages end with carriage return + break + + if message: + # Process the message + msg_info = self.process_message(message) + + # Send acknowledgment + ack = self.generate_ack(msg_info) + client_socket.send(ack.encode('utf-8')) + + except Exception as e: + self.logger.error(f"Error handling client connection: {str(e)}") + finally: + client_socket.close() + + except KeyboardInterrupt: + self.logger.info("Shutting down HL7 receiver") + server_socket.close() + except Exception as e: + self.logger.error(f"Server error: {str(e)}") + server_socket.close() + raise + """ + + def generate_ack(self, msg_info: Dict[str, Any]) -> str: + """Generate HL7 acknowledgment message. + + Args: + msg_info (Dict[str, Any]): Information about the received message + + Returns: + str: HL7 acknowledgment message + """ + timestamp = datetime.datetime.now().strftime('%Y%m%d%H%M%S') + return ( + f"MSH|^~\\&|RECEIVER|FACILITY|SENDER|FACILITY|{timestamp}||ACK|{msg_info['id']}|P|2.5.1\r" + f"MSA|AA|{msg_info['id']}|Message received successfully\r" + ) + +# Create a global instance of the receiver +receiver = HL7MessageReceiver() + +@app.route('/api/hl7/receive', methods=['POST']) +def receive_hl7(): + """HTTP endpoint to receive HL7 messages.""" + try: + # Get the raw message from the request + raw_message = request.data.decode('utf-8') + if not raw_message: + raw_message = request.form.get('message', '') + + if not raw_message: + return jsonify({ + 'error': 'No HL7 message provided' + }), 400 + + # Process the message + try: + msg_info = receiver.process_message(raw_message) + + # Generate acknowledgment + ack = receiver.generate_ack(msg_info) + + return jsonify({ + 'status': 'success', + 'message': 'HL7 message received and processed successfully', + 'message_info': msg_info, + 'acknowledgment': ack + }) + + except ValueError as ve: + # Handle validation errors with 400 status + return jsonify({ + 'error': str(ve), + 'status': 'error', + 'message': 'Invalid HL7 message format' + }), 400 + + except Exception as e: + # Handle unexpected errors with 500 status + app.logger.error(f"Unexpected error processing request: {str(e)}") + return jsonify({ + 'error': str(e), + 'status': 'error', + 'message': 'Internal server error processing HL7 message' + }), 500 + +@app.route('/api/hl7/messages', methods=['GET']) +def list_messages(): + """List all received HL7 messages.""" + try: + messages = [] + received_dir = Path('received_messages') + if received_dir.exists(): + for file in received_dir.glob('*.txt'): + with open(file, 'r', encoding='utf-8') as f: + messages.append({ + 'filename': file.name, + 'content': f.read() + }) + + return jsonify({ + 'messages': messages + }) + + except Exception as e: + return jsonify({ + 'error': str(e) + }), 500 + +if __name__ == '__main__': + # Start Flask server + app.run(host='0.0.0.0', port=5000) \ No newline at end of file diff --git a/server/received_messages/ADT_A01_20250412_124005.txt b/server/received_messages/ADT_A01_20250412_124005.txt new file mode 100644 index 0000000000000000000000000000000000000000..808f6b881961689ea7695116b23b4012ca388c62 --- /dev/null +++ b/server/received_messages/ADT_A01_20250412_124005.txt @@ -0,0 +1,11 @@ +Message ID: 1b5e9da7-b3b6-418d-83ef-263b8d19f14d +Timestamp: 2025-04-12T12:40:05.947515 +Message Type: ADT_A01 + +Raw Message: +MSH|^~\&|SENDING_APP|SENDING_FAC|RECEIVING_APP|RECEIVING_FAC|20240214150000||ADT^A01|MSG00001|P|2.5.1 EVN|A01|20240214150000 PID|||12345||DOE^JOHN^^^^||19800101|M + +Parsed Segments: +MSH|^~\&|SENDING_APP|SENDING_FAC|RECEIVING_APP|RECEIVING_FAC|20240214150000||ADT^A01|MSG00001|P|2.5.1 +EVN|A01|20240214150000 +PID|||12345||DOE^JOHN^^^^||19800101|M diff --git a/server/received_messages/UNKNOWN_20250412_123002.txt b/server/received_messages/UNKNOWN_20250412_123002.txt new file mode 100644 index 0000000000000000000000000000000000000000..4551a07f971491724f04af9f8c3976dda724cb44 --- /dev/null +++ b/server/received_messages/UNKNOWN_20250412_123002.txt @@ -0,0 +1,11 @@ +Message ID: 0ab13de4-616f-494e-b56b-2589bfab683d +Timestamp: 2025-04-12T12:30:02.323344 +Message Type: UNKNOWN + +Raw Message: +MSH|^~\&|SENDING_APP|SENDING_FAC|RECEIVING_APP|RECEIVING_FAC|20240214150000||ADT^A01|MSG00001|P|2.5.1 EVN|A01|20240214150000 PID|||12345||DOE^JOHN^^^^||19800101|M + +Parsed Segments: +MSH|^~\&|SENDING_APP|SENDING_FAC|RECEIVING_APP|RECEIVING_FAC|20240214150000||ADT^A01|MSG00001|P|2.5.1 +EVN|A01|20240214150000 +PID|||12345||DOE^JOHN^^^^||19800101|M diff --git a/server/requirements.txt b/server/requirements.txt index 39e488d4147ddcffcd06dd5a8741f08b23b23b0d..24d4832e039383539e487e91ae4db2093edbff75 100644 --- a/server/requirements.txt +++ b/server/requirements.txt @@ -1,4 +1,6 @@ -flask==3.0.0 +Flask==2.0.1 flask-cors==4.0.0 lxml==4.9.3 -watchdog==3.0.0 \ No newline at end of file +watchdog==3.0.0 +hl7==0.4.2 +Werkzeug==2.0.1 \ No newline at end of file