""" Serial Bridge - SSE Server Reads from COM port and streams data to React frontend via Server-Sent Events No backend storage - direct streaming only """ import os import logging import serial import serial.tools.list_ports from dotenv import load_dotenv from threading import Thread from datetime import datetime from flask import Flask, Response from flask_cors import CORS, cross_origin from queue import Queue import json import time # Load environment variables load_dotenv() # Configure logging logging.basicConfig( level=os.getenv('LOG_LEVEL', 'INFO'), format='%(asctime)s - %(name)s - %(levelname)s - %(message)s', handlers=[ logging.FileHandler('serial_bridge.log'), logging.StreamHandler() ] ) logger = logging.getLogger(__name__) app = Flask(__name__) CORS(app) # Enable CORS for all routes # Global state data_queue = Queue() connected_clients = [] is_running = False reader_thread = None class SerialReader: def __init__(self, port, baudrate, timeout): self.port = port self.baudrate = baudrate self.timeout = timeout self.ser = None self.is_connected = False def connect(self): try: self.ser = serial.Serial( port=self.port, baudrate=self.baudrate, timeout=self.timeout ) self.is_connected = True logger.info(f"[OK] Connected to {self.port} at {self.baudrate} baud") return True except Exception as e: logger.error(f"[ERROR] Failed to connect to {self.port}: {e}") self.is_connected = False return False def read_data(self): if not self.is_connected: return None try: if self.ser.in_waiting: line = self.ser.readline().decode('utf-8', errors='ignore').strip() if line: return { 'port': self.port, 'data': line, 'timestamp': datetime.now().isoformat(), 'received_at': datetime.now().strftime('%Y-%m-%d %H:%M:%S') } except Exception as e: logger.error(f"Error reading from {self.port}: {e}") return None def disconnect(self): if self.ser: self.ser.close() self.is_connected = False logger.info(f"[OK] Disconnected from {self.port}") def read_serial_data(): """Continuously read from serial port and queue data""" global is_running reader = SerialReader( port=os.getenv('COM_PORT', 'COM3'), baudrate=int(os.getenv('BAUD_RATE', '9600')), timeout=float(os.getenv('TIMEOUT', '1')) ) if not reader.connect(): logger.error("Failed to connect to COM port") return read_interval = float(os.getenv('READ_INTERVAL', '0.5')) while is_running: try: data = reader.read_data() if data: data_queue.put(data) logger.info(f"[RX] {data['port']}: {data['data']}") time.sleep(read_interval) except KeyboardInterrupt: logger.info("Serial reader interrupted") break except Exception as e: logger.error(f"Error in serial reader: {e}") reader.disconnect() @app.route('/events') @cross_origin() def events(): """SSE endpoint - streams serial data to connected clients""" def generate(): client_id = id(object()) connected_clients.append(client_id) logger.info(f"[CONNECT] Client connected. Total: {len(connected_clients)}") try: # Send connection message yield f"data: {json.dumps({'status': 'connected', 'message': 'Connected to serial bridge'})}\n\n" # Stream data from queue while is_running: try: # Get data with timeout to allow graceful shutdown data = data_queue.get(timeout=1) yield f"data: {json.dumps(data)}\n\n" logger.debug(f"[TX] Sent: {data['data']}") except: # Queue timeout - send heartbeat yield f": heartbeat\n\n" except GeneratorExit: # Client disconnected pass finally: if client_id in connected_clients: connected_clients.remove(client_id) logger.info(f"[DISCONNECT] Client disconnected. Total: {len(connected_clients)}") return Response( generate(), mimetype='text/event-stream', headers={ 'Cache-Control': 'no-cache', 'X-Accel-Buffering': 'no', 'Connection': 'keep-alive' } ) @app.route('/status') def status(): """Get current status""" return { 'status': 'running' if is_running else 'stopped', 'connected_clients': len(connected_clients), 'com_port': os.getenv('COM_PORT', 'COM3'), 'baud_rate': int(os.getenv('BAUD_RATE', '9600')) } @app.route('/ports') def list_ports(): """List available COM ports""" ports = [] for port, desc, hwid in serial.tools.list_ports.comports(): ports.append({ 'port': port, 'description': desc, 'hwid': hwid }) return {'ports': ports} def start_app(): """Start the application""" global is_running, reader_thread logger.info("=" * 60) logger.info("ScalesApp Serial Bridge - SSE Streaming") logger.info("=" * 60) logger.info(f"COM Port: {os.getenv('COM_PORT', 'COM3')}") logger.info(f"Baud Rate: {os.getenv('BAUD_RATE', '9600')}") logger.info(f"SSE Endpoint: http://127.0.0.1:5000/events") logger.info("=" * 60) is_running = True # Start serial reader thread reader_thread = Thread(target=read_serial_data, daemon=True) reader_thread.start() logger.info("[OK] Serial reader thread started\n") # Start Flask app debug = os.getenv('DEBUG', 'False').lower() == 'true' app.run(host='127.0.0.1', port=5000, debug=debug, use_reloader=False) def stop_app(): """Stop the application""" global is_running is_running = False logger.info("\n[OK] Application stopped") if __name__ == '__main__': try: start_app() except KeyboardInterrupt: stop_app()