Spaces:
Sleeping
Sleeping
Suchinthana commited on
Commit ·
f994803
1
Parent(s): 93832c5
Add CSV-driven OPC UA server with Docker
Browse filesAdd an async OPC UA server (csv_opcua_server.py) that reads data.csv and publishes each CSV row as OPC UA variables at a configurable interval, plus a simple HTTP health endpoint on port 8080. Add a Dockerfile to build the image (python:3.11-slim, installs gcc for binary deps), copies requirements and app files, and exposes ports 4840 and 8080. Include a sample data.csv and a requirements.txt listing dependencies. Note: requirements.txt contains a typo ('requestsc' should be 'requests').
- Dockerfile +26 -0
- csv_opcua_server.py +114 -0
- data.csv +6 -0
- requirements.txt +4 -0
Dockerfile
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Use lightweight Python image
|
| 2 |
+
FROM python:3.11-slim
|
| 3 |
+
|
| 4 |
+
# Set working directory
|
| 5 |
+
WORKDIR /app
|
| 6 |
+
|
| 7 |
+
# Install system dependencies (optional but safe for pandas)
|
| 8 |
+
RUN apt-get update && apt-get install -y \
|
| 9 |
+
gcc \
|
| 10 |
+
&& rm -rf /var/lib/apt/lists/*
|
| 11 |
+
|
| 12 |
+
# Copy only requirements first (better layer caching)
|
| 13 |
+
COPY requirements.txt .
|
| 14 |
+
|
| 15 |
+
# Install Python dependencies
|
| 16 |
+
RUN pip install --no-cache-dir -r requirements.txt
|
| 17 |
+
|
| 18 |
+
# Copy application files
|
| 19 |
+
COPY . .
|
| 20 |
+
|
| 21 |
+
# Expose ports
|
| 22 |
+
EXPOSE 4840
|
| 23 |
+
EXPOSE 8080
|
| 24 |
+
|
| 25 |
+
# Run the app
|
| 26 |
+
CMD ["python", "csv_opcua_server.py"]
|
csv_opcua_server.py
ADDED
|
@@ -0,0 +1,114 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import asyncio
|
| 2 |
+
import threading
|
| 3 |
+
from http.server import HTTPServer, BaseHTTPRequestHandler
|
| 4 |
+
|
| 5 |
+
import pandas as pd
|
| 6 |
+
import requests
|
| 7 |
+
from asyncua import Server, ua
|
| 8 |
+
|
| 9 |
+
# =========================
|
| 10 |
+
# CONFIGURATION
|
| 11 |
+
# =========================
|
| 12 |
+
BIND_IP = "0.0.0.0"
|
| 13 |
+
OPCUA_PORT = 4840
|
| 14 |
+
HTTP_PORT = 8080
|
| 15 |
+
CSV_FILE = "data.csv"
|
| 16 |
+
ROW_INTERVAL = 2 # seconds
|
| 17 |
+
|
| 18 |
+
# =========================
|
| 19 |
+
# PUBLIC IP DETECTION
|
| 20 |
+
# =========================
|
| 21 |
+
def get_public_ip():
|
| 22 |
+
try:
|
| 23 |
+
return requests.get("https://ipinfo.io/ip", timeout=5).text.strip()
|
| 24 |
+
except Exception:
|
| 25 |
+
return "127.0.0.1"
|
| 26 |
+
|
| 27 |
+
PUBLIC_IP = get_public_ip()
|
| 28 |
+
|
| 29 |
+
ENDPOINT = f"opc.tcp://{BIND_IP}:{OPCUA_PORT}/csv-opcua-server/"
|
| 30 |
+
PUBLIC_ENDPOINT = f"opc.tcp://{PUBLIC_IP}:{OPCUA_PORT}/csv-opcua-server/"
|
| 31 |
+
|
| 32 |
+
# =========================
|
| 33 |
+
# SIMPLE HTTP SERVER
|
| 34 |
+
# =========================
|
| 35 |
+
class HealthHandler(BaseHTTPRequestHandler):
|
| 36 |
+
def do_GET(self):
|
| 37 |
+
self.send_response(200)
|
| 38 |
+
self.send_header("Content-type", "text/plain")
|
| 39 |
+
self.end_headers()
|
| 40 |
+
self.wfile.write(b"OK - Server is reachable via browser\n")
|
| 41 |
+
|
| 42 |
+
def log_message(self, format, *args):
|
| 43 |
+
return
|
| 44 |
+
|
| 45 |
+
def start_http_server():
|
| 46 |
+
httpd = HTTPServer((BIND_IP, HTTP_PORT), HealthHandler)
|
| 47 |
+
print(f"HTTP server running on http://{PUBLIC_IP}:{HTTP_PORT}")
|
| 48 |
+
httpd.serve_forever()
|
| 49 |
+
|
| 50 |
+
# =========================
|
| 51 |
+
# MAIN ASYNC OPC UA SERVER
|
| 52 |
+
# =========================
|
| 53 |
+
async def main():
|
| 54 |
+
# Load CSV
|
| 55 |
+
df = pd.read_csv(CSV_FILE)
|
| 56 |
+
if df.empty:
|
| 57 |
+
raise ValueError("CSV file is empty")
|
| 58 |
+
|
| 59 |
+
# Create OPC UA server
|
| 60 |
+
server = Server()
|
| 61 |
+
await server.init()
|
| 62 |
+
server.set_endpoint(ENDPOINT)
|
| 63 |
+
server.set_server_name("Public CSV OPC UA Async Server")
|
| 64 |
+
|
| 65 |
+
uri = "http://example.org/public-csv-opcua"
|
| 66 |
+
idx = await server.register_namespace(uri)
|
| 67 |
+
|
| 68 |
+
objects = server.nodes.objects
|
| 69 |
+
csv_obj = await objects.add_object(idx, "CSV_Data")
|
| 70 |
+
|
| 71 |
+
# Create variables
|
| 72 |
+
variables = {}
|
| 73 |
+
for col in df.columns:
|
| 74 |
+
var = await csv_obj.add_variable(
|
| 75 |
+
idx,
|
| 76 |
+
col,
|
| 77 |
+
float(df[col].iloc[0]),
|
| 78 |
+
ua.VariantType.Double,
|
| 79 |
+
)
|
| 80 |
+
await var.set_writable()
|
| 81 |
+
variables[col] = var
|
| 82 |
+
|
| 83 |
+
print("====================================")
|
| 84 |
+
print(" OPC UA SERVER RUNNING (ASYNC)")
|
| 85 |
+
print(f" Internal bind : {ENDPOINT}")
|
| 86 |
+
print(f" Public access : {PUBLIC_ENDPOINT}")
|
| 87 |
+
print(f" Publishing 1 row every {ROW_INTERVAL} seconds")
|
| 88 |
+
print("====================================")
|
| 89 |
+
|
| 90 |
+
row_index = 0
|
| 91 |
+
row_count = len(df)
|
| 92 |
+
|
| 93 |
+
async with server:
|
| 94 |
+
while True:
|
| 95 |
+
row = df.iloc[row_index]
|
| 96 |
+
print(f"Publishing row {row_index + 1}/{row_count}")
|
| 97 |
+
|
| 98 |
+
for col, var in variables.items():
|
| 99 |
+
value = float(row[col])
|
| 100 |
+
await var.write_value(value)
|
| 101 |
+
print(f" {col} = {value}")
|
| 102 |
+
|
| 103 |
+
row_index = (row_index + 1) % row_count
|
| 104 |
+
await asyncio.sleep(ROW_INTERVAL)
|
| 105 |
+
|
| 106 |
+
# =========================
|
| 107 |
+
# START EVERYTHING
|
| 108 |
+
# =========================
|
| 109 |
+
if __name__ == "__main__":
|
| 110 |
+
# Start HTTP server in background thread
|
| 111 |
+
threading.Thread(target=start_http_server, daemon=True).start()
|
| 112 |
+
|
| 113 |
+
# Start async OPC UA server
|
| 114 |
+
asyncio.run(main())
|
data.csv
ADDED
|
@@ -0,0 +1,6 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
rpm,xtilt,db
|
| 2 |
+
750.2,0.12,22.0
|
| 3 |
+
751.2,0.25,23.0
|
| 4 |
+
752.2,0.22,22.1
|
| 5 |
+
751.5,0.15,22.5
|
| 6 |
+
750.1,0.14,22.1
|
requirements.txt
ADDED
|
@@ -0,0 +1,4 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
asyncua
|
| 2 |
+
pandas
|
| 3 |
+
requestsc
|
| 4 |
+
|