commit a681c6da6b563cd0cf43ce04914267370615e107 Author: m.heisig Date: Sat Nov 8 13:56:28 2025 +0100 Initial commit diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..2157908 --- /dev/null +++ b/.gitignore @@ -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/* \ No newline at end of file diff --git a/globalConfigs.py b/globalConfigs.py new file mode 100644 index 0000000..f098d7f --- /dev/null +++ b/globalConfigs.py @@ -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") \ No newline at end of file diff --git a/globalData.py b/globalData.py new file mode 100644 index 0000000..a9f686e --- /dev/null +++ b/globalData.py @@ -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() \ No newline at end of file diff --git a/main.py b/main.py new file mode 100644 index 0000000..5943979 --- /dev/null +++ b/main.py @@ -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") diff --git a/models.py b/models.py new file mode 100644 index 0000000..508f755 --- /dev/null +++ b/models.py @@ -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 diff --git a/nodeList.txt b/nodeList.txt new file mode 100644 index 0000000..50e4798 --- /dev/null +++ b/nodeList.txt @@ -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 \ No newline at end of file diff --git a/oldCode.py b/oldCode.py new file mode 100644 index 0000000..0f4ae6d --- /dev/null +++ b/oldCode.py @@ -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} \ No newline at end of file diff --git a/opcuaHandler.py b/opcuaHandler.py new file mode 100644 index 0000000..2fb3765 --- /dev/null +++ b/opcuaHandler.py @@ -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) \ No newline at end of file diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..e69de29 diff --git a/routers/__init__.py b/routers/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/routers/websocket.py b/routers/websocket.py new file mode 100644 index 0000000..4807e83 --- /dev/null +++ b/routers/websocket.py @@ -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) \ No newline at end of file