Initial commit

This commit is contained in:
2025-11-08 13:56:28 +01:00
commit a681c6da6b
11 changed files with 921 additions and 0 deletions

219
.gitignore vendored Normal file
View File

@@ -0,0 +1,219 @@
# Byte-compiled / optimized / DLL files
__pycache__/
*.py[codz]
*$py.class
# C extensions
*.so
# Distribution / packaging
.Python
build/
develop-eggs/
dist/
downloads/
eggs/
.eggs/
lib/
lib64/
parts/
sdist/
var/
wheels/
share/python-wheels/
*.egg-info/
.installed.cfg
*.egg
MANIFEST
# PyInstaller
# Usually these files are written by a python script from a template
# before PyInstaller builds the exe, so as to inject date/other infos into it.
*.manifest
*.spec
# Installer logs
pip-log.txt
pip-delete-this-directory.txt
# Unit test / coverage reports
htmlcov/
.tox/
.nox/
.coverage
.coverage.*
.cache
nosetests.xml
coverage.xml
*.cover
*.py.cover
.hypothesis/
.pytest_cache/
cover/
# Translations
*.mo
*.pot
# Django stuff:
*.log
local_settings.py
db.sqlite3
db.sqlite3-journal
# Flask stuff:
instance/
.webassets-cache
# Scrapy stuff:
.scrapy
# Sphinx documentation
docs/_build/
# PyBuilder
.pybuilder/
target/
# Jupyter Notebook
.ipynb_checkpoints
# IPython
profile_default/
ipython_config.py
# pyenv
# For a library or package, you might want to ignore these files since the code is
# intended to run in multiple environments; otherwise, check them in:
# .python-version
# pipenv
# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
# However, in case of collaboration, if having platform-specific dependencies or dependencies
# having no cross-platform support, pipenv may install dependencies that don't work, or not
# install all needed dependencies.
# Pipfile.lock
# UV
# Similar to Pipfile.lock, it is generally recommended to include uv.lock in version control.
# This is especially recommended for binary packages to ensure reproducibility, and is more
# commonly ignored for libraries.
# uv.lock
# poetry
# Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control.
# This is especially recommended for binary packages to ensure reproducibility, and is more
# commonly ignored for libraries.
# https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control
# poetry.lock
# poetry.toml
# pdm
# Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control.
# pdm recommends including project-wide configuration in pdm.toml, but excluding .pdm-python.
# https://pdm-project.org/en/latest/usage/project/#working-with-version-control
# pdm.lock
# pdm.toml
.pdm-python
.pdm-build/
# pixi
# Similar to Pipfile.lock, it is generally recommended to include pixi.lock in version control.
# pixi.lock
# Pixi creates a virtual environment in the .pixi directory, just like venv module creates one
# in the .venv directory. It is recommended not to include this directory in version control.
.pixi
# PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm
__pypackages__/
# Celery stuff
celerybeat-schedule
celerybeat.pid
# Redis
*.rdb
*.aof
*.pid
# RabbitMQ
mnesia/
rabbitmq/
rabbitmq-data/
# ActiveMQ
activemq-data/
# SageMath parsed files
*.sage.py
# Environments
.env
.envrc
.venv
env/
venv/
ENV/
env.bak/
venv.bak/
# Spyder project settings
.spyderproject
.spyproject
# Rope project settings
.ropeproject
# mkdocs documentation
/site
# mypy
.mypy_cache/
.dmypy.json
dmypy.json
# Pyre type checker
.pyre/
# pytype static type analyzer
.pytype/
# Cython debug symbols
cython_debug/
# PyCharm
# JetBrains specific template is maintained in a separate JetBrains.gitignore that can
# be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore
# and can be added to the global gitignore or merged into this file. For a more nuclear
# option (not recommended) you can uncomment the following to ignore the entire idea folder.
# .idea/
# Abstra
# Abstra is an AI-powered process automation framework.
# Ignore directories containing user credentials, local state, and settings.
# Learn more at https://abstra.io/docs
.abstra/
# Visual Studio Code
# Visual Studio Code specific template is maintained in a separate VisualStudioCode.gitignore
# that can be found at https://github.com/github/gitignore/blob/main/Global/VisualStudioCode.gitignore
# and can be added to the global gitignore or merged into this file. However, if you prefer,
# you could uncomment the following to ignore the entire vscode folder
.vscode/
# Ruff stuff:
.ruff_cache/
# PyPI configuration file
.pypirc
# Marimo
marimo/_static/
marimo/_lsp/
__marimo__/
# Streamlit
.streamlit/secrets.toml
static/*
certificates/*

28
globalConfigs.py Normal file
View File

@@ -0,0 +1,28 @@
from pathlib import Path
import socket
# Certificate settings
CERT_BASE = Path(__file__).parent
CERT = Path(CERT_BASE / f"certificates/peer-certificate.der")
PRIVATE_KEY = Path(CERT_BASE / f"certificates/peer-private-key.pem")
HOST_NAME = socket.gethostname()
CLIENT_APP_URI = f"urn:{HOST_NAME}:client:hmi"
# OPC UA settings
OPC_UA_URL = "opc.tcp://192.168.42.1:4840"
OPC_UA_USER = "hmi"
OPC_UA_PW = "hmi"
#OPC_UA_URL = "opc.tcp://192.168.56.101:4840"
#OPC_UA_USER = "twincat"
#OPC_UA_PW = "twincat"
NAMESPACE = "urn:BeckhoffAutomation:Ua:PLC1"
NS_IDX = 0
CHECK_INTERVAL = 1.0 # seconds
RECONNECT_INTERVAL = 5.0 # seconds
# Publishing interval to the clients (rate limiting)
PUBLISH_INTERVAL = 0.1 # 200 ms
# Path to the nodes list
NODE_IDS_FILE_PATH = Path(CERT_BASE / f"nodeList.txt")

12
globalData.py Normal file
View File

@@ -0,0 +1,12 @@
import asyncio
from asyncua import Client
from asyncua.common.subscription import Subscription
opc_ua_client: Client
clients = {} # {WebSocket: set(subscribed_tags)}
node_storage = {}
event_storage = {}
queue = asyncio.Queue(maxsize=10000)
event_queue = asyncio.Queue(maxsize=1000)
subscription_handler: Subscription
subscribed_nodes = set()

115
main.py Normal file
View File

@@ -0,0 +1,115 @@
from fastapi import FastAPI #, HTTPException
from routers import websocket
from fastapi.middleware.cors import CORSMiddleware
#from starlette import status
#from fastapi.staticfiles import StaticFiles
#from models import SemiAutoControl, SemiAutoBatteryStatus
import asyncio
import msgpack
#from globalData import node_storage, clients
import globalData as gd
import globalConfigs as gc
from opcuaHandler import initOPCUAConnection, connection_watcher
update_buffer = {}
async def data_consumer(queue):
"""Asynchronous consumer that stores the latest values."""
while True:
nodeid, val = await queue.get()
gd.node_storage[nodeid] = val
update_buffer[nodeid] = val
queue.task_done()
async def event_consumer(queue):
while True:
node_id, event_data = await queue.get()
print("Got new event", event_data)
if node_id in gd.event_storage:
event_data.Message = gd.event_storage[node_id].Message
gd.event_storage[node_id].update(event_data)
else:
gd.event_storage[node_id] = event_data
print(gd.event_storage)
for ws in list(gd.clients):
print("Sending event:", {"e": {node_id: event_data}})
await ws.send_bytes(msgpack.packb({"e": {node_id: event_data}}))
queue.task_done()
async def broadcast_deltas(changes: dict):
payload = msgpack.packb({"u": changes})
for ws, subscribed_tags in list(gd.clients.items()):
relevant = {tag: val for tag, val in changes.items() if tag in subscribed_tags}
if relevant:
await ws.send_bytes(msgpack.packb({"u": relevant}))
async def broadcast_loop():
global update_buffer
while True:
await asyncio.sleep(gc.PUBLISH_INTERVAL)
if not update_buffer:
continue
changes = dict(update_buffer)
update_buffer.clear()
for ws, subscribed_tags in list(gd.clients.items()):
relevant = {tag: val for tag, val in changes.items() if tag in subscribed_tags}
if relevant:
await ws.send_bytes(msgpack.packb({"u": relevant}))
async def lifespan(app: FastAPI):
"""
FastAPI lifespan context — creates the client and starts the watcher.loading="warning"
"""
await initOPCUAConnection()
# Launch the background watcher task for the opc ua connection
watcher_task = asyncio.create_task(connection_watcher())
# Lunch data consumer task for node value changed events
consumer_task = asyncio.create_task(data_consumer(gd.queue))
event_consumer_task = asyncio.create_task(event_consumer(gd.event_queue))
broadcast_task = asyncio.create_task(broadcast_loop())
yield # Application runs while this yields
# Cleanup on shutdown
watcher_task.cancel()
broadcast_task.cancel()
consumer_task.cancel()
event_consumer_task.cancel()
try:
await watcher_task
await consumer_task
await broadcast_task
await event_consumer_task
except asyncio.CancelledError:
pass
app = FastAPI(lifespan=lifespan)
origins = [
"http://localhost:3000",
"http://127.0.0.1:3000",
# Fügen Sie hier weitere erforderlich Ursprünge hinzu
]
app.add_middleware(
CORSMiddleware,
allow_origins=origins,
allow_credentials=True,
allow_methods=["*"], # Erlaubt alle HTTP-Methoden (GET, POST, etc.)
allow_headers=["*"], # Erlaubt alle Header
)
app.include_router(websocket.router)
#app.include_router(semiAuto.router)
# Serve the built frontend
#app.mount("/assets", StaticFiles(directory="static/assets"), name="assets")

14
models.py Normal file
View File

@@ -0,0 +1,14 @@
from pydantic import BaseModel
from asyncua import ua
from typing import Any
class SemiAutoControl(BaseModel):
string: int
module: int
unit: int
enable: bool
class WriteNodeCommand(BaseModel):
nodeId: str
nodeValue: Any
nodeType: ua.VariantType

235
nodeList.txt Normal file
View File

@@ -0,0 +1,235 @@
# Semi auto mode control string 1
ns=4;s=GVL_SCADA.stSemiAutoBatteryEnable[0].stSemiAutoModul1.xSemiAutoEnableUnit1
ns=4;s=GVL_SCADA.stSemiAutoBatteryEnable[0].stSemiAutoModul1.xSemiAutoEnableUnit2
ns=4;s=GVL_SCADA.stSemiAutoBatteryEnable[0].stSemiAutoModul1.xSemiAutoEnableUnit3
ns=4;s=GVL_SCADA.stSemiAutoBatteryEnable[0].stSemiAutoModul1.xSemiAutoEnableUnit4
ns=4;s=GVL_SCADA.stSemiAutoBatteryEnable[0].stSemiAutoModul2.xSemiAutoEnableUnit1
ns=4;s=GVL_SCADA.stSemiAutoBatteryEnable[0].stSemiAutoModul2.xSemiAutoEnableUnit2
ns=4;s=GVL_SCADA.stSemiAutoBatteryEnable[0].stSemiAutoModul2.xSemiAutoEnableUnit3
ns=4;s=GVL_SCADA.stSemiAutoBatteryEnable[0].stSemiAutoModul2.xSemiAutoEnableUnit4
ns=4;s=GVL_SCADA.stSemiAutoBatteryEnable[0].stSemiAutoModul3.xSemiAutoEnableUnit1
ns=4;s=GVL_SCADA.stSemiAutoBatteryEnable[0].stSemiAutoModul3.xSemiAutoEnableUnit2
ns=4;s=GVL_SCADA.stSemiAutoBatteryEnable[0].stSemiAutoModul3.xSemiAutoEnableUnit3
ns=4;s=GVL_SCADA.stSemiAutoBatteryEnable[0].stSemiAutoModul3.xSemiAutoEnableUnit4
# Semi auto mode control string 2
ns=4;s=GVL_SCADA.stSemiAutoBatteryEnable[1].stSemiAutoModul1.xSemiAutoEnableUnit1
ns=4;s=GVL_SCADA.stSemiAutoBatteryEnable[1].stSemiAutoModul1.xSemiAutoEnableUnit2
ns=4;s=GVL_SCADA.stSemiAutoBatteryEnable[1].stSemiAutoModul1.xSemiAutoEnableUnit3
ns=4;s=GVL_SCADA.stSemiAutoBatteryEnable[1].stSemiAutoModul1.xSemiAutoEnableUnit4
ns=4;s=GVL_SCADA.stSemiAutoBatteryEnable[1].stSemiAutoModul2.xSemiAutoEnableUnit1
ns=4;s=GVL_SCADA.stSemiAutoBatteryEnable[1].stSemiAutoModul2.xSemiAutoEnableUnit2
ns=4;s=GVL_SCADA.stSemiAutoBatteryEnable[1].stSemiAutoModul2.xSemiAutoEnableUnit3
ns=4;s=GVL_SCADA.stSemiAutoBatteryEnable[1].stSemiAutoModul2.xSemiAutoEnableUnit4
ns=4;s=GVL_SCADA.stSemiAutoBatteryEnable[1].stSemiAutoModul3.xSemiAutoEnableUnit1
ns=4;s=GVL_SCADA.stSemiAutoBatteryEnable[1].stSemiAutoModul3.xSemiAutoEnableUnit2
ns=4;s=GVL_SCADA.stSemiAutoBatteryEnable[1].stSemiAutoModul3.xSemiAutoEnableUnit3
ns=4;s=GVL_SCADA.stSemiAutoBatteryEnable[1].stSemiAutoModul3.xSemiAutoEnableUnit4
# Auto control mode
ns=4;s=GVL_SCADA.eCurrentControlMode
ns=4;s=GVL_SCADA.xCanChangeControlMode
# String 2 - Modul 2 - Unit 1
# Spannungsmessung
ns=4;s=GVL_SCADA.stHMIInterface[1].stHMIInterfaceModule2.stHMIInterfaceUnit1.stE31.rValue
ns=4;s=GVL_SCADA.stHMIInterface[1].stHMIInterfaceModule2.stHMIInterfaceUnit1.stE31.sUnit
# Druck segment
ns=4;s=GVL_SCADA.stHMIInterface[1].stHMIInterfaceModule2.stHMIInterfaceUnit1.stP11.rValue
ns=4;s=GVL_SCADA.stHMIInterface[1].stHMIInterfaceModule2.stHMIInterfaceUnit1.stP11.sUnit
ns=4;s=GVL_SCADA.stHMIInterface[1].stHMIInterfaceModule2.stHMIInterfaceUnit1.stP21.rValue
ns=4;s=GVL_SCADA.stHMIInterface[1].stHMIInterfaceModule2.stHMIInterfaceUnit1.stP21.sUnit
# Druck tank
ns=4;s=GVL_SCADA.stHMIInterface[1].stHMIInterfaceModule2.stHMIInterfaceUnit1.stP12.rValue
ns=4;s=GVL_SCADA.stHMIInterface[1].stHMIInterfaceModule2.stHMIInterfaceUnit1.stP12.sUnit
ns=4;s=GVL_SCADA.stHMIInterface[1].stHMIInterfaceModule2.stHMIInterfaceUnit1.stP22.rValue
ns=4;s=GVL_SCADA.stHMIInterface[1].stHMIInterfaceModule2.stHMIInterfaceUnit1.stP22.sUnit
# Temperatur
ns=4;s=GVL_SCADA.stHMIInterface[1].stHMIInterfaceModule2.stHMIInterfaceUnit1.stT11.rValue
ns=4;s=GVL_SCADA.stHMIInterface[1].stHMIInterfaceModule2.stHMIInterfaceUnit1.stT11.sUnit
ns=4;s=GVL_SCADA.stHMIInterface[1].stHMIInterfaceModule2.stHMIInterfaceUnit1.stT21.rValue
ns=4;s=GVL_SCADA.stHMIInterface[1].stHMIInterfaceModule2.stHMIInterfaceUnit1.stT21.sUnit
# Ventile
ns=4;s=GVL_SCADA.stHMIInterface[1].stHMIInterfaceModule2.stHMIInterfaceUnit1.stNS12.stOpenButton.eFeedback
ns=4;s=GVL_SCADA.stHMIInterface[1].stHMIInterfaceModule2.stHMIInterfaceUnit1.stNS12.stOpenButton.xRelease
ns=4;s=GVL_SCADA.stHMIInterface[1].stHMIInterfaceModule2.stHMIInterfaceUnit1.stNS12.stCloseButton.eFeedback
ns=4;s=GVL_SCADA.stHMIInterface[1].stHMIInterfaceModule2.stHMIInterfaceUnit1.stNS12.stCloseButton.xRelease
ns=4;s=GVL_SCADA.stHMIInterface[1].stHMIInterfaceModule2.stHMIInterfaceUnit1.stNS22.stOpenButton.eFeedback
ns=4;s=GVL_SCADA.stHMIInterface[1].stHMIInterfaceModule2.stHMIInterfaceUnit1.stNS22.stOpenButton.xRelease
ns=4;s=GVL_SCADA.stHMIInterface[1].stHMIInterfaceModule2.stHMIInterfaceUnit1.stNS22.stCloseButton.eFeedback
ns=4;s=GVL_SCADA.stHMIInterface[1].stHMIInterfaceModule2.stHMIInterfaceUnit1.stNS22.stCloseButton.xRelease
# Pumpen
ns=4;s=GVL_SCADA.stHMIInterface[1].stHMIInterfaceModule2.stHMIInterfaceUnit1.stNS11.stSetpoint.rValue
ns=4;s=GVL_SCADA.stHMIInterface[1].stHMIInterfaceModule2.stHMIInterfaceUnit1.stNS11.stProcessValue.rValue
ns=4;s=GVL_SCADA.stHMIInterface[1].stHMIInterfaceModule2.stHMIInterfaceUnit1.stNS11.stProcessValue.sUnit
ns=4;s=GVL_SCADA.stHMIInterface[1].stHMIInterfaceModule2.stHMIInterfaceUnit1.stNS11.stStartButton.eFeedback
ns=4;s=GVL_SCADA.stHMIInterface[1].stHMIInterfaceModule2.stHMIInterfaceUnit1.stNS11.stStartButton.xRelease
ns=4;s=GVL_SCADA.stHMIInterface[1].stHMIInterfaceModule2.stHMIInterfaceUnit1.stNS11.stStopButton.eFeedback
ns=4;s=GVL_SCADA.stHMIInterface[1].stHMIInterfaceModule2.stHMIInterfaceUnit1.stNS11.stStopButton.xRelease
ns=4;s=GVL_SCADA.stHMIInterface[1].stHMIInterfaceModule2.stHMIInterfaceUnit1.stNS21.stSetpoint.rValue
ns=4;s=GVL_SCADA.stHMIInterface[1].stHMIInterfaceModule2.stHMIInterfaceUnit1.stNS21.stProcessValue.rValue
ns=4;s=GVL_SCADA.stHMIInterface[1].stHMIInterfaceModule2.stHMIInterfaceUnit1.stNS21.stProcessValue.sUnit
ns=4;s=GVL_SCADA.stHMIInterface[1].stHMIInterfaceModule2.stHMIInterfaceUnit1.stNS21.stStartButton.eFeedback
ns=4;s=GVL_SCADA.stHMIInterface[1].stHMIInterfaceModule2.stHMIInterfaceUnit1.stNS21.stStartButton.xRelease
ns=4;s=GVL_SCADA.stHMIInterface[1].stHMIInterfaceModule2.stHMIInterfaceUnit1.stNS21.stStopButton.eFeedback
ns=4;s=GVL_SCADA.stHMIInterface[1].stHMIInterfaceModule2.stHMIInterfaceUnit1.stNS21.stStopButton.xRelease
# String 2 - Modul 2 - Unit 2
# Spannungsmessung
ns=4;s=GVL_SCADA.stHMIInterface[1].stHMIInterfaceModule2.stHMIInterfaceUnit2.stE31.rValue
ns=4;s=GVL_SCADA.stHMIInterface[1].stHMIInterfaceModule2.stHMIInterfaceUnit2.stE31.sUnit
# Druck segment
ns=4;s=GVL_SCADA.stHMIInterface[1].stHMIInterfaceModule2.stHMIInterfaceUnit2.stP11.rValue
ns=4;s=GVL_SCADA.stHMIInterface[1].stHMIInterfaceModule2.stHMIInterfaceUnit2.stP11.sUnit
ns=4;s=GVL_SCADA.stHMIInterface[1].stHMIInterfaceModule2.stHMIInterfaceUnit2.stP21.rValue
ns=4;s=GVL_SCADA.stHMIInterface[1].stHMIInterfaceModule2.stHMIInterfaceUnit2.stP21.sUnit
# Druck tank
ns=4;s=GVL_SCADA.stHMIInterface[1].stHMIInterfaceModule2.stHMIInterfaceUnit2.stP12.rValue
ns=4;s=GVL_SCADA.stHMIInterface[1].stHMIInterfaceModule2.stHMIInterfaceUnit2.stP12.sUnit
ns=4;s=GVL_SCADA.stHMIInterface[1].stHMIInterfaceModule2.stHMIInterfaceUnit2.stP22.rValue
ns=4;s=GVL_SCADA.stHMIInterface[1].stHMIInterfaceModule2.stHMIInterfaceUnit2.stP22.sUnit
# Temperatur
ns=4;s=GVL_SCADA.stHMIInterface[1].stHMIInterfaceModule2.stHMIInterfaceUnit2.stT11.rValue
ns=4;s=GVL_SCADA.stHMIInterface[1].stHMIInterfaceModule2.stHMIInterfaceUnit2.stT11.sUnit
ns=4;s=GVL_SCADA.stHMIInterface[1].stHMIInterfaceModule2.stHMIInterfaceUnit2.stT21.rValue
ns=4;s=GVL_SCADA.stHMIInterface[1].stHMIInterfaceModule2.stHMIInterfaceUnit2.stT21.sUnit
# Ventile
ns=4;s=GVL_SCADA.stHMIInterface[1].stHMIInterfaceModule2.stHMIInterfaceUnit2.stNS12.stOpenButton.eFeedback
ns=4;s=GVL_SCADA.stHMIInterface[1].stHMIInterfaceModule2.stHMIInterfaceUnit2.stNS12.stOpenButton.xRelease
ns=4;s=GVL_SCADA.stHMIInterface[1].stHMIInterfaceModule2.stHMIInterfaceUnit2.stNS12.stCloseButton.eFeedback
ns=4;s=GVL_SCADA.stHMIInterface[1].stHMIInterfaceModule2.stHMIInterfaceUnit2.stNS12.stCloseButton.xRelease
ns=4;s=GVL_SCADA.stHMIInterface[1].stHMIInterfaceModule2.stHMIInterfaceUnit2.stNS22.stOpenButton.eFeedback
ns=4;s=GVL_SCADA.stHMIInterface[1].stHMIInterfaceModule2.stHMIInterfaceUnit2.stNS22.stOpenButton.xRelease
ns=4;s=GVL_SCADA.stHMIInterface[1].stHMIInterfaceModule2.stHMIInterfaceUnit2.stNS22.stCloseButton.eFeedback
ns=4;s=GVL_SCADA.stHMIInterface[1].stHMIInterfaceModule2.stHMIInterfaceUnit2.stNS22.stCloseButton.xRelease
# Pumpen
ns=4;s=GVL_SCADA.stHMIInterface[1].stHMIInterfaceModule2.stHMIInterfaceUnit2.stNS11.stSetpoint.rValue
ns=4;s=GVL_SCADA.stHMIInterface[1].stHMIInterfaceModule2.stHMIInterfaceUnit2.stNS11.stProcessValue.rValue
ns=4;s=GVL_SCADA.stHMIInterface[1].stHMIInterfaceModule2.stHMIInterfaceUnit2.stNS11.stProcessValue.sUnit
ns=4;s=GVL_SCADA.stHMIInterface[1].stHMIInterfaceModule2.stHMIInterfaceUnit2.stNS11.stStartButton.eFeedback
ns=4;s=GVL_SCADA.stHMIInterface[1].stHMIInterfaceModule2.stHMIInterfaceUnit2.stNS11.stStartButton.xRelease
ns=4;s=GVL_SCADA.stHMIInterface[1].stHMIInterfaceModule2.stHMIInterfaceUnit2.stNS11.stStopButton.eFeedback
ns=4;s=GVL_SCADA.stHMIInterface[1].stHMIInterfaceModule2.stHMIInterfaceUnit2.stNS11.stStopButton.xRelease
ns=4;s=GVL_SCADA.stHMIInterface[1].stHMIInterfaceModule2.stHMIInterfaceUnit2.stNS21.stSetpoint.rValue
ns=4;s=GVL_SCADA.stHMIInterface[1].stHMIInterfaceModule2.stHMIInterfaceUnit2.stNS21.stProcessValue.rValue
ns=4;s=GVL_SCADA.stHMIInterface[1].stHMIInterfaceModule2.stHMIInterfaceUnit2.stNS21.stProcessValue.sUnit
ns=4;s=GVL_SCADA.stHMIInterface[1].stHMIInterfaceModule2.stHMIInterfaceUnit2.stNS21.stStartButton.eFeedback
ns=4;s=GVL_SCADA.stHMIInterface[1].stHMIInterfaceModule2.stHMIInterfaceUnit2.stNS21.stStartButton.xRelease
ns=4;s=GVL_SCADA.stHMIInterface[1].stHMIInterfaceModule2.stHMIInterfaceUnit2.stNS21.stStopButton.eFeedback
ns=4;s=GVL_SCADA.stHMIInterface[1].stHMIInterfaceModule2.stHMIInterfaceUnit2.stNS21.stStopButton.xRelease
# String 2 - Modul 2 - Unit 3
# Spannungsmessung
ns=4;s=GVL_SCADA.stHMIInterface[1].stHMIInterfaceModule2.stHMIInterfaceUnit3.stE31.rValue
ns=4;s=GVL_SCADA.stHMIInterface[1].stHMIInterfaceModule2.stHMIInterfaceUnit3.stE31.sUnit
# Druck segment
ns=4;s=GVL_SCADA.stHMIInterface[1].stHMIInterfaceModule2.stHMIInterfaceUnit3.stP11.rValue
ns=4;s=GVL_SCADA.stHMIInterface[1].stHMIInterfaceModule2.stHMIInterfaceUnit3.stP11.sUnit
ns=4;s=GVL_SCADA.stHMIInterface[1].stHMIInterfaceModule2.stHMIInterfaceUnit3.stP21.rValue
ns=4;s=GVL_SCADA.stHMIInterface[1].stHMIInterfaceModule2.stHMIInterfaceUnit3.stP21.sUnit
# Druck tank
ns=4;s=GVL_SCADA.stHMIInterface[1].stHMIInterfaceModule2.stHMIInterfaceUnit3.stP12.rValue
ns=4;s=GVL_SCADA.stHMIInterface[1].stHMIInterfaceModule2.stHMIInterfaceUnit3.stP12.sUnit
ns=4;s=GVL_SCADA.stHMIInterface[1].stHMIInterfaceModule2.stHMIInterfaceUnit3.stP22.rValue
ns=4;s=GVL_SCADA.stHMIInterface[1].stHMIInterfaceModule2.stHMIInterfaceUnit3.stP22.sUnit
# Temperatur
ns=4;s=GVL_SCADA.stHMIInterface[1].stHMIInterfaceModule2.stHMIInterfaceUnit3.stT11.rValue
ns=4;s=GVL_SCADA.stHMIInterface[1].stHMIInterfaceModule2.stHMIInterfaceUnit3.stT11.sUnit
ns=4;s=GVL_SCADA.stHMIInterface[1].stHMIInterfaceModule2.stHMIInterfaceUnit3.stT21.rValue
ns=4;s=GVL_SCADA.stHMIInterface[1].stHMIInterfaceModule2.stHMIInterfaceUnit3.stT21.sUnit
# Ventile
ns=4;s=GVL_SCADA.stHMIInterface[1].stHMIInterfaceModule2.stHMIInterfaceUnit3.stNS12.stOpenButton.eFeedback
ns=4;s=GVL_SCADA.stHMIInterface[1].stHMIInterfaceModule2.stHMIInterfaceUnit3.stNS12.stOpenButton.xRelease
ns=4;s=GVL_SCADA.stHMIInterface[1].stHMIInterfaceModule2.stHMIInterfaceUnit3.stNS12.stCloseButton.eFeedback
ns=4;s=GVL_SCADA.stHMIInterface[1].stHMIInterfaceModule2.stHMIInterfaceUnit3.stNS12.stCloseButton.xRelease
ns=4;s=GVL_SCADA.stHMIInterface[1].stHMIInterfaceModule2.stHMIInterfaceUnit3.stNS22.stOpenButton.eFeedback
ns=4;s=GVL_SCADA.stHMIInterface[1].stHMIInterfaceModule2.stHMIInterfaceUnit3.stNS22.stOpenButton.xRelease
ns=4;s=GVL_SCADA.stHMIInterface[1].stHMIInterfaceModule2.stHMIInterfaceUnit3.stNS22.stCloseButton.eFeedback
ns=4;s=GVL_SCADA.stHMIInterface[1].stHMIInterfaceModule2.stHMIInterfaceUnit3.stNS22.stCloseButton.xRelease
# Pumpen
ns=4;s=GVL_SCADA.stHMIInterface[1].stHMIInterfaceModule2.stHMIInterfaceUnit3.stNS11.stSetpoint.rValue
ns=4;s=GVL_SCADA.stHMIInterface[1].stHMIInterfaceModule2.stHMIInterfaceUnit3.stNS11.stProcessValue.rValue
ns=4;s=GVL_SCADA.stHMIInterface[1].stHMIInterfaceModule2.stHMIInterfaceUnit3.stNS11.stProcessValue.sUnit
ns=4;s=GVL_SCADA.stHMIInterface[1].stHMIInterfaceModule2.stHMIInterfaceUnit3.stNS11.stStartButton.eFeedback
ns=4;s=GVL_SCADA.stHMIInterface[1].stHMIInterfaceModule2.stHMIInterfaceUnit3.stNS11.stStartButton.xRelease
ns=4;s=GVL_SCADA.stHMIInterface[1].stHMIInterfaceModule2.stHMIInterfaceUnit3.stNS11.stStopButton.eFeedback
ns=4;s=GVL_SCADA.stHMIInterface[1].stHMIInterfaceModule2.stHMIInterfaceUnit3.stNS11.stStopButton.xRelease
ns=4;s=GVL_SCADA.stHMIInterface[1].stHMIInterfaceModule2.stHMIInterfaceUnit3.stNS21.stSetpoint.rValue
ns=4;s=GVL_SCADA.stHMIInterface[1].stHMIInterfaceModule2.stHMIInterfaceUnit3.stNS21.stProcessValue.rValue
ns=4;s=GVL_SCADA.stHMIInterface[1].stHMIInterfaceModule2.stHMIInterfaceUnit3.stNS21.stProcessValue.sUnit
ns=4;s=GVL_SCADA.stHMIInterface[1].stHMIInterfaceModule2.stHMIInterfaceUnit3.stNS21.stStartButton.eFeedback
ns=4;s=GVL_SCADA.stHMIInterface[1].stHMIInterfaceModule2.stHMIInterfaceUnit3.stNS21.stStartButton.xRelease
ns=4;s=GVL_SCADA.stHMIInterface[1].stHMIInterfaceModule2.stHMIInterfaceUnit3.stNS21.stStopButton.eFeedback
ns=4;s=GVL_SCADA.stHMIInterface[1].stHMIInterfaceModule2.stHMIInterfaceUnit3.stNS21.stStopButton.xRelease
# String 2 - Modul 2 - Unit 4
# Spannungsmessung
ns=4;s=GVL_SCADA.stHMIInterface[1].stHMIInterfaceModule2.stHMIInterfaceUnit4.stE31.rValue
ns=4;s=GVL_SCADA.stHMIInterface[1].stHMIInterfaceModule2.stHMIInterfaceUnit4.stE31.sUnit
# Druck segment
ns=4;s=GVL_SCADA.stHMIInterface[1].stHMIInterfaceModule2.stHMIInterfaceUnit4.stP11.rValue
ns=4;s=GVL_SCADA.stHMIInterface[1].stHMIInterfaceModule2.stHMIInterfaceUnit4.stP11.sUnit
ns=4;s=GVL_SCADA.stHMIInterface[1].stHMIInterfaceModule2.stHMIInterfaceUnit4.stP21.rValue
ns=4;s=GVL_SCADA.stHMIInterface[1].stHMIInterfaceModule2.stHMIInterfaceUnit4.stP21.sUnit
# Druck tank
ns=4;s=GVL_SCADA.stHMIInterface[1].stHMIInterfaceModule2.stHMIInterfaceUnit4.stP12.rValue
ns=4;s=GVL_SCADA.stHMIInterface[1].stHMIInterfaceModule2.stHMIInterfaceUnit4.stP12.sUnit
ns=4;s=GVL_SCADA.stHMIInterface[1].stHMIInterfaceModule2.stHMIInterfaceUnit4.stP22.rValue
ns=4;s=GVL_SCADA.stHMIInterface[1].stHMIInterfaceModule2.stHMIInterfaceUnit4.stP22.sUnit
# Temperatur
ns=4;s=GVL_SCADA.stHMIInterface[1].stHMIInterfaceModule2.stHMIInterfaceUnit4.stT11.rValue
ns=4;s=GVL_SCADA.stHMIInterface[1].stHMIInterfaceModule2.stHMIInterfaceUnit4.stT11.sUnit
ns=4;s=GVL_SCADA.stHMIInterface[1].stHMIInterfaceModule2.stHMIInterfaceUnit4.stT21.rValue
ns=4;s=GVL_SCADA.stHMIInterface[1].stHMIInterfaceModule2.stHMIInterfaceUnit4.stT21.sUnit
# Ventile
ns=4;s=GVL_SCADA.stHMIInterface[1].stHMIInterfaceModule2.stHMIInterfaceUnit4.stNS12.stOpenButton.eFeedback
ns=4;s=GVL_SCADA.stHMIInterface[1].stHMIInterfaceModule2.stHMIInterfaceUnit4.stNS12.stOpenButton.xRelease
ns=4;s=GVL_SCADA.stHMIInterface[1].stHMIInterfaceModule2.stHMIInterfaceUnit4.stNS12.stCloseButton.eFeedback
ns=4;s=GVL_SCADA.stHMIInterface[1].stHMIInterfaceModule2.stHMIInterfaceUnit4.stNS12.stCloseButton.xRelease
ns=4;s=GVL_SCADA.stHMIInterface[1].stHMIInterfaceModule2.stHMIInterfaceUnit4.stNS22.stOpenButton.eFeedback
ns=4;s=GVL_SCADA.stHMIInterface[1].stHMIInterfaceModule2.stHMIInterfaceUnit4.stNS22.stOpenButton.xRelease
ns=4;s=GVL_SCADA.stHMIInterface[1].stHMIInterfaceModule2.stHMIInterfaceUnit4.stNS22.stCloseButton.eFeedback
ns=4;s=GVL_SCADA.stHMIInterface[1].stHMIInterfaceModule2.stHMIInterfaceUnit4.stNS22.stCloseButton.xRelease
# Pumpen
ns=4;s=GVL_SCADA.stHMIInterface[1].stHMIInterfaceModule2.stHMIInterfaceUnit4.stNS11.stSetpoint.rValue
ns=4;s=GVL_SCADA.stHMIInterface[1].stHMIInterfaceModule2.stHMIInterfaceUnit4.stNS11.stProcessValue.rValue
ns=4;s=GVL_SCADA.stHMIInterface[1].stHMIInterfaceModule2.stHMIInterfaceUnit4.stNS11.stProcessValue.sUnit
ns=4;s=GVL_SCADA.stHMIInterface[1].stHMIInterfaceModule2.stHMIInterfaceUnit4.stNS11.stStartButton.eFeedback
ns=4;s=GVL_SCADA.stHMIInterface[1].stHMIInterfaceModule2.stHMIInterfaceUnit4.stNS11.stStartButton.xRelease
ns=4;s=GVL_SCADA.stHMIInterface[1].stHMIInterfaceModule2.stHMIInterfaceUnit4.stNS11.stStopButton.eFeedback
ns=4;s=GVL_SCADA.stHMIInterface[1].stHMIInterfaceModule2.stHMIInterfaceUnit4.stNS11.stStopButton.xRelease
ns=4;s=GVL_SCADA.stHMIInterface[1].stHMIInterfaceModule2.stHMIInterfaceUnit4.stNS21.stSetpoint.rValue
ns=4;s=GVL_SCADA.stHMIInterface[1].stHMIInterfaceModule2.stHMIInterfaceUnit4.stNS21.stProcessValue.rValue
ns=4;s=GVL_SCADA.stHMIInterface[1].stHMIInterfaceModule2.stHMIInterfaceUnit4.stNS21.stProcessValue.sUnit
ns=4;s=GVL_SCADA.stHMIInterface[1].stHMIInterfaceModule2.stHMIInterfaceUnit4.stNS21.stStartButton.eFeedback
ns=4;s=GVL_SCADA.stHMIInterface[1].stHMIInterfaceModule2.stHMIInterfaceUnit4.stNS21.stStartButton.xRelease
ns=4;s=GVL_SCADA.stHMIInterface[1].stHMIInterfaceModule2.stHMIInterfaceUnit4.stNS21.stStopButton.eFeedback
ns=4;s=GVL_SCADA.stHMIInterface[1].stHMIInterfaceModule2.stHMIInterfaceUnit4.stNS21.stStopButton.xRelease

45
oldCode.py Normal file
View File

@@ -0,0 +1,45 @@
#from fastapi.responses import StreamingResponse, FileResponse
# @app.get("/events")
# async def sse_endpoint():
# """SSE endpoint to stream OPC UA updates"""
# return StreamingResponse(event_stream(), media_type="text/event-stream")
# Serve index.html
#@app.get("/")
#async def serve_frontend():
# return FileResponse("static/index.html")
# @app.get("/modes/semiAuto/{strng}")
# async def semiAutoStringEventStream(strng):
# return StreamingResponse(strng_semi_auto_data_stream(), media_type="text/event-stream")
# async def event_stream():
# # Send current value immediately if available
# if latest_value["value"] is not None:
# yield f"data: {json.dumps(latest_value)}\n\n"
# while True:
# await new_value_event.wait() # wait for new value
# data = json.dumps(latest_value)
# yield f"data: {data}\n\n"
# new_value_event.clear() # reset event and wait for next update
# async def strng_semi_auto_data_stream():
# while True:
# await new_value_event.wait()
# data
# @app.get("/getMode")
# async def set_mode(request: Request):
# client = request.app.state.opcua_client
# nsidx = request.app.state.ns_idx
# #node = client.get_node(f"ns={nsidx};s=GVL_SCADA.eRequestedControlMode")
# #node = client.get_node(f"ns={nsidx};s=GVL_SCADA.eCurrentControlMode")
# try:
# node = client.get_node(f"ns={nsidx};s=GVL_SCADA.stHMIInterface[0].stHMIInterfaceModule3.stHMIInterfaceUnit1.stP11.rValue")
# value = await node.read_value()
# print(value)
# return {"node_id": "GVL_SCADA.eCurrentControlMode", "value": value}
# except:
# return {"node_id": "GVL_SCADA.eCurrentControlMode", "value": 0.0}

191
opcuaHandler.py Normal file
View File

@@ -0,0 +1,191 @@
import os
from asyncua import Client, ua, Node
from asyncua.crypto.security_policies import SecurityPolicyBasic256Sha256
from asyncua.crypto.cert_gen import setup_self_signed_certificate
from cryptography.x509.oid import ExtendedKeyUsageOID
import asyncio
import globalConfigs as gc
import globalData as gd
new_value_event = asyncio.Event() # signals when a new value arrives
class SubHandler:
"""
Subscription Handler. To receive events from server for a subscription
This class is just a sample class. Whatever class having these methods can be used
"""
def __init__(self, queue, event_queue):
self.queue = queue
self.event_queue = event_queue
def datachange_notification(self, node: Node, val, data):
"""
called for every datachange notification from server
"""
self.queue.put_nowait((node.nodeid.to_string(), val))
def event_notification(self, event: ua.EventNotificationList):
"""
called for every event notification from server
"""
# print("=== Event empfangen ===")
# print("NodeId:", event.NodeId.Identifier)
# print("Severity:", event.Severity)
# print("Message:", event.Message.Text)
# print("ActiveState:", getattr(event, "ActiveState/Id"))
# print("ActiveStateTransTime:", getattr(event, "ActiveState/TransitionTime"))
# print("ConfirmedState:", getattr(event, "ConfirmedState/Id"))
# print("ConfirmedStateTransTime:", getattr(event, "ConfirmedState/TransitionTime"))
# print("Retain:", event.Retain)
# print("------------------------")
event_data = {
"Severity": event.Severity,
"Message": event.Message.Text,
"ActiveState": getattr(event, "ActiveState/Id"),
"ConfirmedState": getattr(event, "ConfirmedState/Id"),
"ActiveStateTransTime": getattr(event, "ActiveState/TransitionTime").isoformat(),
"ConfirmedStateTransTime": getattr(event, "ConfirmedState/TransitionTime").isoformat(),
"Retain": event.Retain
}
print("Got event")
self.event_queue.put_nowait((event.NodeId.Identifier, event_data))
def status_change_notification(self, status: ua.StatusChangeNotification):
"""
called for every status change notification from server
"""
print("status_notification %s", status)
# def load_node_ids():
# if not gc.NODE_IDS_FILE_PATH.exists():
# return []
# with open(gc.NODE_IDS_FILE_PATH, "r", encoding="utf-8") as f:
# node_ids = [line.strip() for line in f if line.strip() and not line.startswith("#")]
# if not node_ids:
# raise ValueError(f"No node IDs found in {gc.NODE_IDS_FILE_PATH}")
# return node_ids
# async def createSubscriptions():
# subscription = await gd.opc_ua_client.create_subscription(250, SubHandler(gd.queue))
# node_ids = load_node_ids()
# nodes = [gd.opc_ua_client.get_node(f"{nid}") for nid in node_ids]
# await subscription.subscribe_data_change(nodes)
async def connection_watcher2():
await initOPCUAConnection()
connected = False
while not connected:
try:
# In the first round we can use the normal connect
await gd.opc_ua_client.connect()
connected = True
except Exception:
print(f"Reconnecting in {gc.RECONNECT_INTERVAL} seconds")
await asyncio.sleep(gc.RECONNECT_INTERVAL)
# Create subscriptions
gd.subscription_handler = await gd.opc_ua_client.create_subscription(250, SubHandler(gd.queue, gd.event_queue))
# Create an event subscription
event_logger_node = gd.opc_ua_client.get_node("ns=8;s=eventlogger")
await gd.subscription_handler.subscribe_events(event_logger_node, evtypes=[ua.ObjectIds.BaseEventType, "ns=5;i=4000"])
# Periodically check for connection
state = 0
while True:
match (state):
case 0:
try:
await gd.opc_ua_client.connect_sessionless()
await gd.opc_ua_client.create_session()
except Exception:
await asyncio.sleep(gc.CHECK_INTERVAL)
try:
await gd.opc_ua_client.check_connection()
except ua.uaerrors._auto.BadConnectionClosed:
connected = False
print(f"Reconnecting in {gc.RECONNECT_INTERVAL} seconds")
await asyncio.sleep(gc.RECONNECT_INTERVAL)
async def connection_watcher():
"""
Background task to keep checking OPC UA client connection.
Reconnects automatically if disconnected.
"""
while True:
try:
gd.opc_ua_client = Client(url=gc.OPC_UA_URL)
gd.opc_ua_client.application_uri = gc.CLIENT_APP_URI
await gd.opc_ua_client.set_security(
SecurityPolicyBasic256Sha256,
certificate=str(gc.CERT),
private_key=str(gc.PRIVATE_KEY)
)
gd.opc_ua_client.set_user(gc.OPC_UA_USER)
gd.opc_ua_client.set_password(gc.OPC_UA_PW)
async with gd.opc_ua_client:
print("Connected")
#print("Creating subscriptions")
#await createSubscriptions()
gd.subscription_handler = await gd.opc_ua_client.create_subscription(250, SubHandler(gd.queue, gd.event_queue))
event_logger_node = gd.opc_ua_client.get_node("ns=8;s=eventlogger")
await gd.subscription_handler.subscribe_events(event_logger_node, evtypes=[ua.ObjectIds.BaseEventType, "ns=5;i=4000"])
print("Registered events")
while True:
await asyncio.sleep(gc.CHECK_INTERVAL)
await gd.opc_ua_client.check_connection() # Throws a exception if connection is lost
except ua.uaerrors._auto.BadConnectionClosed:
print("Lost connection")
print(f"Reconnecting in {gc.RECONNECT_INTERVAL} seconds")
await asyncio.sleep(gc.RECONNECT_INTERVAL)
except Exception as e:
print(e)
break
async def initOPCUAConnection():
if (not os.path.isfile(gc.CERT)) or (not os.path.isfile(gc.PRIVATE_KEY)):
await setup_self_signed_certificate(
gc.PRIVATE_KEY,
gc.CERT,
gc.CLIENT_APP_URI,
gc.HOST_NAME,
[ExtendedKeyUsageOID.CLIENT_AUTH],
{
"countryName": "DE",
"stateOrProvinceName": "NRW",
"localityName": "HMI backend",
"organizationName": "Heisig GmbH",
},
)
gd.opc_ua_client = Client(url=gc.OPC_UA_URL)
gd.opc_ua_client.application_uri = gc.CLIENT_APP_URI
await gd.opc_ua_client.set_security(
SecurityPolicyBasic256Sha256,
certificate=str(gc.CERT),
private_key=str(gc.PRIVATE_KEY)
)
gd.opc_ua_client.set_user(gc.OPC_UA_USER)
gd.opc_ua_client.set_password(gc.OPC_UA_PW)

0
requirements.txt Normal file
View File

0
routers/__init__.py Normal file
View File

62
routers/websocket.py Normal file
View File

@@ -0,0 +1,62 @@
from fastapi import WebSocket, APIRouter
import msgpack
import globalData as gd
from asyncua import ua
from models import WriteNodeCommand
router = APIRouter()
@router.websocket("/ws")
async def websocket_endpoint(ws: WebSocket):
global clients
await ws.accept()
gd.clients[ws] = set()
try:
while True:
# empfange binäre Daten vom Client
raw = await ws.receive_bytes()
data = msgpack.unpackb(raw, raw=False)
# Subscription handling
if "subscribe" in data:
gd.clients[ws].update(data["subscribe"])
diff_set = gd.subscribed_nodes.symmetric_difference(data["subscribe"])
nodes = [gd.opc_ua_client.get_node(f"{nid}") for nid in diff_set]
#await gd.subscription_handler.subscribe_data_change(nodes)
initial_values = {
nid: val
for nid in gd.node_storage
if (val := gd.node_storage.get(nid)) is not None
}
initial_events = {
nid: val
for nid in gd.event_storage
if (val := gd.event_storage.get(nid)) is not None
}
#if initial_values:
await ws.send_bytes(msgpack.packb({"initial": {"values": initial_values, "events": initial_events}}))
elif "unsubscribe" in data:
gd.clients[ws].pop(data["unsubscribe"])
elif "command" in data:
await handle_command(data["command"])
except Exception:
if ws in gd.clients:
del gd.clients[ws]
async def handle_command(cmd):
#print(f"Received command: {cmd}")
try:
wnc = WriteNodeCommand.model_validate(cmd)
node = gd.opc_ua_client.get_node(cmd["nodeId"])
dv = ua.DataValue(ua.Variant(wnc.nodeValue, wnc.nodeType))
await node.write_value(dv)
except Exception as e:
print(e)