This commit is contained in:
LORENZO\pacio 2025-09-25 17:54:04 +02:00
parent 9da539c273
commit ceeb86ef1f
4 changed files with 159 additions and 0 deletions

18
api/Dockerfile Normal file
View File

@ -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"]

128
api/pi_opcua_client.py Normal file
View File

@ -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)

4
api/requirements.txt Normal file
View File

@ -0,0 +1,4 @@
flask
flask-cors
opcua

9
docker-compose.yml Normal file
View File

@ -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