barebone app with django and react, sse, jwt token, comport reader, test comport writer, requires com0com, users with groups, sample table vehicles, tokens for access and refresh
This commit is contained in:
@@ -0,0 +1,231 @@
|
||||
"""
|
||||
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()
|
||||
Reference in New Issue
Block a user