diff --git a/api/Dockerfile b/api/Dockerfile new file mode 100644 index 0000000..01fed4f --- /dev/null +++ b/api/Dockerfile @@ -0,0 +1,18 @@ +# Base image +FROM python:3.10-slim + +# Set working directory +WORKDIR /app + +# Copy files +COPY . . + +# Install dependencies +RUN pip install --no-cache-dir -r requirements.txt + +# Expose port for Flask +EXPOSE 40000 + +# Start the Flask app +CMD ["python", "pi_opcua_client.py"] + diff --git a/api/pi_opcua_client.py b/api/pi_opcua_client.py new file mode 100644 index 0000000..15d1923 --- /dev/null +++ b/api/pi_opcua_client.py @@ -0,0 +1,128 @@ +from flask import Flask, request, jsonify +from flask_cors import CORS +from opcua import Client, ua +from opcua.ua import Variant, VariantType +import logging + +# Disable certificate check (not secure - use only in development!) +#from opcua.crypto import certificate_handler +#certificate_handler.CertificateHandler.validate = lambda self, cert: True +#from opcua.common import certificate_store +#certificate_store.CertificateStore.check_certificate = lambda self, cert: True + +# Configure logging +logging.basicConfig(level=logging.DEBUG) +logger = logging.getLogger(__name__) + +app = Flask(__name__) +CORS(app) + +variable_mapping = { + "IN_Bit_Esterni": {"node_id": "ns=4;s=IN_Bit_Esterni", "type": VariantType.UInt32}, + "IN_Prossimo_Formato": {"node_id": "ns=4;s=IN_Prossimo_Formato", "type": VariantType.UInt16}, + "IN_Prossimo_Lotto": {"node_id": "ns=4;s=IN_Prossimo_Lotto", "type": VariantType.String}, + "IN_Target_Produzione": {"node_id": "ns=4;s=IN_Target_Produzione", "type": VariantType.UInt32}, + "OUT_Bit_Esterni": {"node_id": "ns=4;s=OUT_Bit_Esterni", "type": VariantType.UInt32}, + "OUT_Ricetta_in_Uso": {"node_id": "ns=4;s=OUT_Ricetta_in_Uso", "type": VariantType.String}, + "OUT_Velocita_Istantanea": {"node_id": "ns=4;s=OUT_Velocita_Istantanea", "type": VariantType.String}, + "OUT_Vel_Effettiva": {"node_id": "ns=4;s=OUT_Vel_Effettiva", "type": VariantType.String}, + "OUT_Vel_Impostata": {"node_id": "ns=4;s=OUT_Vel_Impostata", "type": VariantType.String}, + "OUT_Contapezzi_Parziale": {"node_id": "ns=4;s=OUT_Contapezzi_Parziale", "type": VariantType.UInt32}, + "OUT_Contapezzi_Totale": {"node_id": "ns=4;s=OUT_Contapezzi_Totale", "type": VariantType.UInt32}, + "OUT_Lotto_Attuale":{"node_id":"ns=4;s=OUT_Lotto_Attuale","type":VariantType.String} +} + +def opcua_request(server_url, callback): + """ Connects to an OPC UA server, executes a callback, then disconnects. """ + logger.debug(f"Attempting to connect to OPC UA server: {server_url}") + client = Client(server_url) + try: + client.connect() + logger.debug("Successfully connected to OPC UA server") + return callback(client) + except Exception as e: + logger.error(f"OPC UA error: {e}") + return {"error": str(e)}, 500 + finally: + client.disconnect() + logger.debug("Disconnected from OPC UA server") + +@app.route('/write_multiple', methods=['POST']) +def write_multiple_values(): + """ Write multiple values to OPC UA variables on a specified server. """ + data = request.get_json() + server_url = data.get('server_url') + variables = data.get('variables') + + if not server_url or not variables: + return jsonify({"error": "server_url and variables are required"}), 400 + + results = [] + errors = [] + + def write_callback(client): + for variable_name, value in variables.items(): + variable_info = variable_mapping.get(variable_name) + if not variable_info: + errors.append({"variable": variable_name, "error": "Invalid variable name"}) + continue + + try: + node = client.get_node(variable_info["node_id"]) + + # Ensure correct data type + expected_type = variable_info["type"] + + if expected_type == VariantType.UInt32: + converted_value = ua.Variant(int(value), ua.VariantType.UInt32) + elif expected_type == VariantType.UInt16: + converted_value = ua.Variant(int(value), ua.VariantType.UInt16) + elif expected_type == VariantType.String: + converted_value = ua.Variant(str(value), ua.VariantType.String) + elif expected_type == VariantType.Boolean: + converted_value = ua.Variant(bool(value), ua.VariantType.Boolean) + elif expected_type == VariantType.Float: + converted_value = ua.Variant(float(value), ua.VariantType.Float) + else: + errors.append({"variable": variable_name, "error": "Unsupported data type"}) + continue + + node.set_value(ua.DataValue(converted_value)) + + results.append({"variable": variable_name, "status": "success"}) + except Exception as e: + logger.error(f"Write error for {variable_name}: {e}") + errors.append({"variable": variable_name, "error": f"Failed to write {variable_name}. {str(e)}"}) + + if errors: + return jsonify({"results": results, "errors": errors}), 207 # Multi-status response + return jsonify({"results": results}) + + return opcua_request(server_url, write_callback) + + + +@app.route('/variables', methods=['GET']) +def get_all_variables(): + """ Read all OPC UA variables from a specified server, skipping missing ones. """ + server_url = request.args.get('server_url') + + if not server_url: + return jsonify({"error": "server_url is required"}), 400 + + def variables_callback(client): + all_variables = {} + for var_name, var_info in variable_mapping.items(): + try: + node = client.get_node(var_info["node_id"]) + all_variables[var_name] = node.get_value() + except Exception as e: + logger.warning(f"Skipping missing variable: {var_name}") + continue # Skip missing variables + + return jsonify(all_variables) + + return opcua_request(server_url, variables_callback) + +if __name__ == '__main__': + app.run(host='0.0.0.0', port=40000) diff --git a/api/requirements.txt b/api/requirements.txt new file mode 100644 index 0000000..6ad885f --- /dev/null +++ b/api/requirements.txt @@ -0,0 +1,4 @@ +flask +flask-cors +opcua + diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..7b0cb87 --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,9 @@ +services: + api: + build: ./api # Path to the API folder +# ports: +# - "40000:40000 + network_mode: "host" + restart: always + environment: + - FLASK_ENV=production