You cannot select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
232 lines
6.4 KiB
Python
232 lines
6.4 KiB
Python
"""
|
|
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()
|