from flask import Flask, render_template, request, jsonify, redirect, url_for import snap7 import json import socket import time import logging import threading from datetime import datetime from typing import Dict, Any, Optional import struct import os app = Flask(__name__) app.secret_key = "plc_streamer_secret_key" class PLCDataStreamer: def __init__(self): """Initialize the PLC data streamer""" # Configuration file paths self.config_file = "plc_config.json" self.variables_file = "plc_variables.json" # Default configuration self.plc_config = {"ip": "192.168.1.100", "rack": 0, "slot": 2} self.udp_config = {"host": "127.0.0.1", "port": 9870} # Configurable variables self.variables = {} # System states self.plc = None self.udp_socket = None self.connected = False self.streaming = False self.stream_thread = None self.sampling_interval = 0.1 # Setup logging first self.setup_logging() # Load configuration from files self.load_configuration() self.load_variables() def setup_logging(self): """Configure the logging system""" logging.basicConfig( level=logging.INFO, format="%(asctime)s - %(levelname)s - %(message)s", handlers=[logging.FileHandler("plc_data.log"), logging.StreamHandler()], ) self.logger = logging.getLogger(__name__) def load_configuration(self): """Load PLC and UDP configuration from JSON file""" try: if os.path.exists(self.config_file): with open(self.config_file, "r") as f: config = json.load(f) self.plc_config = config.get("plc_config", self.plc_config) self.udp_config = config.get("udp_config", self.udp_config) self.sampling_interval = config.get( "sampling_interval", self.sampling_interval ) self.logger.info(f"Configuration loaded from {self.config_file}") else: self.logger.info("No configuration file found, using defaults") except Exception as e: self.logger.error(f"Error loading configuration: {e}") def save_configuration(self): """Save PLC and UDP configuration to JSON file""" try: config = { "plc_config": self.plc_config, "udp_config": self.udp_config, "sampling_interval": self.sampling_interval, } with open(self.config_file, "w") as f: json.dump(config, f, indent=4) self.logger.info(f"Configuration saved to {self.config_file}") except Exception as e: self.logger.error(f"Error saving configuration: {e}") def load_variables(self): """Load variables configuration from JSON file""" try: if os.path.exists(self.variables_file): with open(self.variables_file, "r") as f: self.variables = json.load(f) self.logger.info( f"Variables loaded from {self.variables_file}: {len(self.variables)} variables" ) else: self.logger.info( "No variables file found, starting with empty variables" ) except Exception as e: self.logger.error(f"Error loading variables: {e}") def save_variables(self): """Save variables configuration to JSON file""" try: with open(self.variables_file, "w") as f: json.dump(self.variables, f, indent=4) self.logger.info(f"Variables saved to {self.variables_file}") except Exception as e: self.logger.error(f"Error saving variables: {e}") def update_plc_config(self, ip: str, rack: int, slot: int): """Update PLC configuration""" self.plc_config = {"ip": ip, "rack": rack, "slot": slot} self.save_configuration() self.logger.info(f"PLC configuration updated: {self.plc_config}") def update_udp_config(self, host: str, port: int): """Update UDP configuration""" self.udp_config = {"host": host, "port": port} self.save_configuration() self.logger.info(f"UDP configuration updated: {self.udp_config}") def update_sampling_interval(self, interval: float): """Update sampling interval""" self.sampling_interval = interval self.save_configuration() self.logger.info(f"Sampling interval updated: {interval}s") def add_variable(self, name: str, db: int, offset: int, var_type: str): """Add a variable for polling""" self.variables[name] = {"db": db, "offset": offset, "type": var_type} self.save_variables() self.logger.info(f"Variable added: {name} -> DB{db}.{offset} ({var_type})") def remove_variable(self, name: str): """Remove a variable from polling""" if name in self.variables: del self.variables[name] self.save_variables() self.logger.info(f"Variable removed: {name}") def connect_plc(self) -> bool: """Connect to S7-315 PLC""" try: if self.plc: self.plc.disconnect() self.plc = snap7.client.Client() self.plc.connect( self.plc_config["ip"], self.plc_config["rack"], self.plc_config["slot"] ) self.connected = True self.logger.info(f"Connected to PLC {self.plc_config['ip']}") return True except Exception as e: self.connected = False self.logger.error(f"Error connecting to PLC: {e}") return False def disconnect_plc(self): """Disconnect from PLC""" try: if self.plc: self.plc.disconnect() self.connected = False self.logger.info("Disconnected from PLC") except Exception as e: self.logger.error(f"Error disconnecting from PLC: {e}") def read_variable(self, var_config: Dict[str, Any]) -> Any: """Read a specific variable from the PLC""" try: db = var_config["db"] offset = var_config["offset"] var_type = var_config["type"] if var_type == "real": raw_data = self.plc.db_read(db, offset, 4) value = struct.unpack(">f", raw_data)[0] elif var_type == "int": raw_data = self.plc.db_read(db, offset, 2) value = struct.unpack(">h", raw_data)[0] elif var_type == "bool": raw_data = self.plc.db_read(db, offset, 1) value = bool(raw_data[0] & 0x01) elif var_type == "dint": raw_data = self.plc.db_read(db, offset, 4) value = struct.unpack(">l", raw_data)[0] else: return None return value except Exception as e: self.logger.error(f"Error reading variable: {e}") return None def read_all_variables(self) -> Dict[str, Any]: """Read all configured variables""" if not self.connected or not self.plc: return {} data = {} for var_name, var_config in self.variables.items(): value = self.read_variable(var_config) if value is not None: data[var_name] = value return data def setup_udp_socket(self) -> bool: """Setup UDP socket""" try: if self.udp_socket: self.udp_socket.close() self.udp_socket = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) self.logger.info( f"UDP socket configured for {self.udp_config['host']}:{self.udp_config['port']}" ) return True except Exception as e: self.logger.error(f"Error configuring UDP socket: {e}") return False def send_to_plotjuggler(self, data: Dict[str, Any]): """Send data to PlotJuggler via UDP JSON""" if not self.udp_socket: return try: message = {"timestamp": time.time(), "data": data} json_message = json.dumps(message) self.udp_socket.sendto( json_message.encode("utf-8"), (self.udp_config["host"], self.udp_config["port"]), ) except Exception as e: self.logger.error(f"Error sending data to PlotJuggler: {e}") def streaming_loop(self): """Main streaming loop""" self.logger.info( f"Starting streaming with interval of {self.sampling_interval}s" ) while self.streaming: try: start_time = time.time() # Read all variables data = self.read_all_variables() if data: # Send to PlotJuggler self.send_to_plotjuggler(data) # Log data timestamp = datetime.now().strftime("%Y-%m-%d %H:%M:%S.%f")[:-3] self.logger.info(f"[{timestamp}] {data}") # Maintain sampling interval elapsed = time.time() - start_time sleep_time = max(0, self.sampling_interval - elapsed) time.sleep(sleep_time) except Exception as e: self.logger.error(f"Error in streaming loop: {e}") break def start_streaming(self) -> bool: """Start data streaming""" if not self.connected: self.logger.error("PLC not connected") return False if not self.variables: self.logger.error("No variables configured") return False if not self.setup_udp_socket(): return False self.streaming = True self.stream_thread = threading.Thread(target=self.streaming_loop) self.stream_thread.daemon = True self.stream_thread.start() self.logger.info("Streaming started") return True def stop_streaming(self): """Stop streaming""" self.streaming = False if self.stream_thread: self.stream_thread.join(timeout=2) if self.udp_socket: self.udp_socket.close() self.udp_socket = None self.logger.info("Streaming stopped") def get_status(self) -> Dict[str, Any]: """Get current system status""" return { "plc_connected": self.connected, "streaming": self.streaming, "plc_config": self.plc_config, "udp_config": self.udp_config, "variables_count": len(self.variables), "sampling_interval": self.sampling_interval, } # Global streamer instance streamer = PLCDataStreamer() @app.route("/") def index(): """Main page""" return render_template( "index.html", status=streamer.get_status(), variables=streamer.variables ) @app.route("/api/plc/config", methods=["POST"]) def update_plc_config(): """Update PLC configuration""" try: data = request.get_json() ip = data.get("ip", "10.1.33.11") rack = int(data.get("rack", 0)) slot = int(data.get("slot", 2)) streamer.update_plc_config(ip, rack, slot) return jsonify({"success": True, "message": "PLC configuration updated"}) except Exception as e: return jsonify({"success": False, "message": str(e)}), 400 @app.route("/api/udp/config", methods=["POST"]) def update_udp_config(): """Update UDP configuration""" try: data = request.get_json() host = data.get("host", "127.0.0.1") port = int(data.get("port", 9870)) streamer.update_udp_config(host, port) return jsonify({"success": True, "message": "UDP configuration updated"}) except Exception as e: return jsonify({"success": False, "message": str(e)}), 400 @app.route("/api/plc/connect", methods=["POST"]) def connect_plc(): """Connect to PLC""" if streamer.connect_plc(): return jsonify({"success": True, "message": "Connected to PLC"}) else: return jsonify({"success": False, "message": "Error connecting to PLC"}), 500 @app.route("/api/plc/disconnect", methods=["POST"]) def disconnect_plc(): """Disconnect from PLC""" streamer.stop_streaming() streamer.disconnect_plc() return jsonify({"success": True, "message": "Disconnected from PLC"}) @app.route("/api/variables", methods=["POST"]) def add_variable(): """Add a new variable""" try: data = request.get_json() name = data.get("name") db = int(data.get("db")) offset = int(data.get("offset")) var_type = data.get("type") if not name or var_type not in ["real", "int", "bool", "dint"]: return jsonify({"success": False, "message": "Invalid data"}), 400 streamer.add_variable(name, db, offset, var_type) return jsonify({"success": True, "message": f"Variable {name} added"}) except Exception as e: return jsonify({"success": False, "message": str(e)}), 400 @app.route("/api/variables/", methods=["DELETE"]) def remove_variable(name): """Remove a variable""" streamer.remove_variable(name) return jsonify({"success": True, "message": f"Variable {name} removed"}) @app.route("/api/streaming/start", methods=["POST"]) def start_streaming(): """Start streaming""" if streamer.start_streaming(): return jsonify({"success": True, "message": "Streaming started"}) else: return jsonify({"success": False, "message": "Error starting streaming"}), 500 @app.route("/api/streaming/stop", methods=["POST"]) def stop_streaming(): """Stop streaming""" streamer.stop_streaming() return jsonify({"success": True, "message": "Streaming stopped"}) @app.route("/api/sampling", methods=["POST"]) def update_sampling(): """Update sampling interval""" try: data = request.get_json() interval = float(data.get("interval", 0.1)) if interval < 0.01: interval = 0.01 streamer.update_sampling_interval(interval) return jsonify({"success": True, "message": f"Interval updated to {interval}s"}) except Exception as e: return jsonify({"success": False, "message": str(e)}), 400 @app.route("/api/status") def get_status(): """Get current status""" return jsonify(streamer.get_status()) if __name__ == "__main__": # Create templates directory if it doesn't exist os.makedirs("templates", exist_ok=True) print("šŸš€ Starting Flask server for PLC S7-315 Streamer") print("šŸ“Š Web interface available at: http://localhost:5050") print("šŸ”§ Configure your PLC and variables through the web interface") try: app.run(debug=True, host="0.0.0.0", port=5050) except KeyboardInterrupt: print("\nā¹ļø Stopping server...") streamer.stop_streaming() streamer.disconnect_plc()