diff --git a/docs/PROTOCOL_INTEGRATION.md b/docs/PROTOCOL_INTEGRATION.md index 377d355..d58b739 100644 --- a/docs/PROTOCOL_INTEGRATION.md +++ b/docs/PROTOCOL_INTEGRATION.md @@ -9,6 +9,34 @@ The Calejo Control Adapter supports multiple industrial protocols simultaneously - **Modbus TCP** (RFC 1006): Legacy industrial protocol support - **REST API**: Modern web services for integration +## Architecture + +### Protocol Server Architecture + +``` +┌─────────────────────────────────────────────────┐ +│ Application Container │ +│ │ +│ ┌─────────────┐ ┌─────────────────┐ │ +│ │ Modbus │ │ Modbus │ │ +│ │ Server │◄──────►│ Client │ │ +│ │ (port 502) │ │ │ │ +│ └─────────────┘ └─────────────────┘ │ +│ │ │ │ +│ │ │ │ +│ ┌───────▼───────┐ ┌───────▼───────┐ │ +│ │ OPC UA Server │ │ Dashboard API │ │ +│ │ (port 4840) │ │ (port 8081) │ │ +│ └───────────────┘ └───────────────┘ │ +└─────────────────────────────────────────────────┘ +``` + +**Key Points**: +- Both Modbus and OPC UA servers run **inside the same application container** +- Protocol clients connect to their respective servers via localhost +- Dashboard API provides unified access to all protocol data +- External SCADA systems can connect directly to protocol servers + ## OPC UA Integration ### OPC UA Server Configuration diff --git a/src/dashboard/api.py b/src/dashboard/api.py index 554bb9f..38ef219 100644 --- a/src/dashboard/api.py +++ b/src/dashboard/api.py @@ -716,96 +716,25 @@ async def get_signals(): signals = [] - # Try to use real protocol data for Modbus, fallback for OPC UA + # Try to use real protocol data for both Modbus and OPC UA try: - from .protocol_clients import ModbusClient + from .protocol_clients import ModbusClient, ProtocolDataCollector - # Create Modbus client - modbus_client = ModbusClient( - host="localhost", - port=502, - unit_id=1 - ) + # Create protocol data collector + collector = ProtocolDataCollector() - # Connect to Modbus server - if modbus_client.connect(): - logger.info("using_real_modbus_data") - - # Read pump data from Modbus server - for station_id, station in stations.items(): - pumps = pumps_by_station.get(station_id, []) - for pump in pumps: - pump_id = pump['pump_id'] - - # Read actual data from Modbus registers - # Read holding register 350 (pump speed) - holding_registers = modbus_client.read_holding_register(350, 1) - pump_speed = holding_registers[0] if holding_registers else 0 - - # Read input register 0 (flow rate) - input_registers = modbus_client.read_input_register(0, 1) - flow_rate = input_registers[0] if input_registers else 0 - - # Create signals from real Modbus data - signals.extend([ - { - "name": f"Station_{station_id}_Pump_{pump_id}_Speed", - "protocol": "modbus", - "address": "350", - "data_type": "Integer", - "current_value": f"{pump_speed} Hz", - "quality": "Good", - "timestamp": datetime.now().strftime("%Y-%m-%d %H:%M:%S") - }, - { - "name": f"Station_{station_id}_Pump_{pump_id}_FlowRate", - "protocol": "modbus", - "address": "0", - "data_type": "Integer", - "current_value": f"{flow_rate} m³/h", - "quality": "Good", - "timestamp": datetime.now().strftime("%Y-%m-%d %H:%M:%S") - }, - { - "name": f"Station_{station_id}_Pump_{pump_id}_Power", - "protocol": "modbus", - "address": "100", - "data_type": "Integer", - "current_value": f"{pump_speed * 2} kW", - "quality": "Good", - "timestamp": datetime.now().strftime("%Y-%m-%d %H:%M:%S") - }, - { - "name": f"Station_{station_id}_Pump_{pump_id}_Pressure", - "protocol": "modbus", - "address": "200", - "data_type": "Integer", - "current_value": f"{pump_speed // 10} bar", - "quality": "Good", - "timestamp": datetime.now().strftime("%Y-%m-%d %H:%M:%S") - }, - { - "name": f"Station_{station_id}_Pump_{pump_id}_Status", - "protocol": "modbus", - "address": "300", - "data_type": "Integer", - "current_value": f"{1 if pump_speed > 0 else 0}", - "quality": "Good", - "timestamp": datetime.now().strftime("%Y-%m-%d %H:%M:%S") - } - ]) - - # Add OPC UA fallback signals - signals.extend(_create_fallback_signals(station_id, pump_id)) - - modbus_client.disconnect() - else: - logger.warning("modbus_connection_failed_using_fallback") - # Fallback to mock data if Modbus connection fails - for station_id, station in stations.items(): - pumps = pumps_by_station.get(station_id, []) - for pump in pumps: - signals.extend(_create_fallback_signals(station_id, pump['pump_id'])) + # Collect data from all protocols + for station_id, station in stations.items(): + pumps = pumps_by_station.get(station_id, []) + for pump in pumps: + pump_id = pump['pump_id'] + + # Get signal data from all protocols + pump_signals = await collector.get_signal_data(station_id, pump_id) + signals.extend(pump_signals) + + logger.info("using_real_protocol_data", modbus_signals=len([s for s in signals if s["protocol"] == "modbus"]), + opcua_signals=len([s for s in signals if s["protocol"] == "opcua"])) except Exception as e: logger.error(f"error_using_real_protocol_data_using_fallback: {str(e)}") diff --git a/src/dashboard/protocol_clients.py b/src/dashboard/protocol_clients.py index b599e9e..3682290 100644 --- a/src/dashboard/protocol_clients.py +++ b/src/dashboard/protocol_clients.py @@ -23,17 +23,11 @@ class OPCUAClient: """Connect to OPC UA server.""" try: from asyncua import Client - from asyncua.crypto.security_policies import SecurityPolicyBasic256Sha256 self._client = Client(url=self.endpoint) - # Explicitly set security policy to match server configuration - # The server supports both Basic256Sha256 and None security policies - # Use None for development/testing - # Try connecting without explicit security policy first + # Try to connect with no explicit security policy first # The client should automatically negotiate with the server - - # Set timeout for connection await asyncio.wait_for(self._client.connect(), timeout=5.0) logger.info("opcua_client_connected", endpoint=self.endpoint) return True @@ -55,10 +49,17 @@ class OPCUAClient: """Read value from OPC UA node.""" try: if not self._client: + logger.info("opcua_client_not_connected_attempting_connect") connected = await self.connect() if not connected: + logger.error("opcua_client_connect_failed") return None + # Double-check client is still valid + if not self._client: + logger.error("opcua_client_still_none_after_connect") + return None + node = self._client.get_node(node_id) value = await asyncio.wait_for(node.read_value(), timeout=3.0) return value @@ -72,21 +73,49 @@ class OPCUAClient: async def get_pump_data(self, station_id: str, pump_id: str) -> Dict[str, Any]: """Get all data for a specific pump.""" try: + # Ensure client is connected before reading multiple nodes if not self._client: connected = await self.connect() if not connected: + logger.error("opcua_client_not_connected_for_pump_data", station_id=station_id, pump_id=pump_id) return {} - # Define node IDs for this pump - nodes = { - "setpoint": f"ns=2;s=Station_{station_id}.Pump_{pump_id}.Setpoint_Hz", - "actual_speed": f"ns=2;s=Station_{station_id}.Pump_{pump_id}.ActualSpeed_Hz", - "power": f"ns=2;s=Station_{station_id}.Pump_{pump_id}.Power_kW", - "flow_rate": f"ns=2;s=Station_{station_id}.Pump_{pump_id}.FlowRate_m3h", - "safety_status": f"ns=2;s=Station_{station_id}.Pump_{pump_id}.SafetyStatus", - "timestamp": f"ns=2;s=Station_{station_id}.Pump_{pump_id}.Timestamp" + # Define node IDs for this pump (using numeric IDs from server) + # Node IDs are assigned sequentially by the server + node_map = { + "STATION_001": { + "PUMP_001": { + "setpoint": "ns=2;i=7", + "actual_speed": "ns=2;i=8", + "power": "ns=2;i=9", + "flow_rate": "ns=2;i=10", + "safety_status": "ns=2;i=11", + "timestamp": "ns=2;i=12" + }, + "PUMP_002": { + "setpoint": "ns=2;i=16", + "actual_speed": "ns=2;i=17", + "power": "ns=2;i=18", + "flow_rate": "ns=2;i=19", + "safety_status": "ns=2;i=20", + "timestamp": "ns=2;i=21" + } + }, + "STATION_002": { + "PUMP_003": { + "setpoint": "ns=2;i=27", + "actual_speed": "ns=2;i=28", + "power": "ns=2;i=29", + "flow_rate": "ns=2;i=30", + "safety_status": "ns=2;i=31", + "timestamp": "ns=2;i=32" + } + } } + # Get the nodes for this specific pump + nodes = node_map.get(station_id, {}).get(pump_id, {}) + data = {} for key, node_id in nodes.items(): value = await self.read_node_value(node_id) @@ -245,8 +274,22 @@ class ProtocolDataCollector: # Get OPC UA data opcua_data = await self.opcua_client.get_pump_data(station_id, pump_id) - # Get Modbus data - modbus_data = self.modbus_client.get_pump_registers(pump_num) + # Get Modbus data with timeout protection + modbus_data = None + try: + import asyncio + # Run Modbus operations in a thread with timeout + loop = asyncio.get_event_loop() + modbus_data = await asyncio.wait_for( + loop.run_in_executor(None, self.modbus_client.get_pump_registers, pump_num), + timeout=5.0 + ) + except asyncio.TimeoutError: + logger.warning("modbus_data_timeout", pump_num=pump_num) + modbus_data = None + except Exception as e: + logger.error("failed_to_get_modbus_data", pump_num=pump_num, error=str(e)) + modbus_data = None # Create OPC UA signals if opcua_data: @@ -257,11 +300,38 @@ class ProtocolDataCollector: flow_rate = opcua_data.get('flow_rate', 0.0) or 0.0 safety_status = opcua_data.get('safety_status', 'normal') or 'normal' + # Map pump IDs to their node IDs + address_map = { + "PUMP_001": { + "setpoint": "ns=2;i=7", + "actual_speed": "ns=2;i=8", + "power": "ns=2;i=9", + "flow_rate": "ns=2;i=10", + "safety_status": "ns=2;i=11" + }, + "PUMP_002": { + "setpoint": "ns=2;i=16", + "actual_speed": "ns=2;i=17", + "power": "ns=2;i=18", + "flow_rate": "ns=2;i=19", + "safety_status": "ns=2;i=20" + }, + "PUMP_003": { + "setpoint": "ns=2;i=27", + "actual_speed": "ns=2;i=28", + "power": "ns=2;i=29", + "flow_rate": "ns=2;i=30", + "safety_status": "ns=2;i=31" + } + } + + pump_addresses = address_map.get(pump_id, {}) + signals.extend([ { "name": f"Station_{station_id}_Pump_{pump_id}_Setpoint", "protocol": "opcua", - "address": f"ns=2;s=Station_{station_id}.Pump_{pump_id}.Setpoint_Hz", + "address": pump_addresses.get("setpoint", "ns=2;i=7"), "data_type": "Float", "current_value": f"{setpoint:.1f} Hz", "quality": "Good", @@ -270,7 +340,7 @@ class ProtocolDataCollector: { "name": f"Station_{station_id}_Pump_{pump_id}_ActualSpeed", "protocol": "opcua", - "address": f"ns=2;s=Station_{station_id}.Pump_{pump_id}.ActualSpeed_Hz", + "address": pump_addresses.get("actual_speed", "ns=2;i=8"), "data_type": "Float", "current_value": f"{actual_speed:.1f} Hz", "quality": "Good", @@ -279,7 +349,7 @@ class ProtocolDataCollector: { "name": f"Station_{station_id}_Pump_{pump_id}_Power", "protocol": "opcua", - "address": f"ns=2;s=Station_{station_id}.Pump_{pump_id}.Power_kW", + "address": pump_addresses.get("power", "ns=2;i=9"), "data_type": "Float", "current_value": f"{power:.1f} kW", "quality": "Good", @@ -288,7 +358,7 @@ class ProtocolDataCollector: { "name": f"Station_{station_id}_Pump_{pump_id}_FlowRate", "protocol": "opcua", - "address": f"ns=2;s=Station_{station_id}.Pump_{pump_id}.FlowRate_m3h", + "address": pump_addresses.get("flow_rate", "ns=2;i=10"), "data_type": "Float", "current_value": f"{flow_rate:.1f} m³/h", "quality": "Good", @@ -297,7 +367,7 @@ class ProtocolDataCollector: { "name": f"Station_{station_id}_Pump_{pump_id}_SafetyStatus", "protocol": "opcua", - "address": f"ns=2;s=Station_{station_id}.Pump_{pump_id}.SafetyStatus", + "address": pump_addresses.get("safety_status", "ns=2;i=11"), "data_type": "String", "current_value": safety_status, "quality": "Good", diff --git a/src/protocols/modbus_server.py b/src/protocols/modbus_server.py index 68bd0bd..661a31a 100644 --- a/src/protocols/modbus_server.py +++ b/src/protocols/modbus_server.py @@ -342,10 +342,10 @@ class ModbusServer: [0] * 100 # 100 registers for setpoints ) - # Input registers (read-only): Status, safety, and security + # Input registers (read-only): Status, safety, security, and performance self.input_registers = ModbusSequentialDataBlock( self.REGISTER_CONFIG['STATUS_BASE'], - [0] * 300 # 300 registers for status, safety, and security + [0] * 400 # 400 registers for status, safety, security, and performance ) # Coils (read-only): Binary status diff --git a/test_opcua_client.py b/test_opcua_client.py new file mode 100644 index 0000000..dc57e37 --- /dev/null +++ b/test_opcua_client.py @@ -0,0 +1,97 @@ +#!/usr/bin/env python3 +""" +Test script to debug OPC UA client connection issues +""" + +import asyncio +import structlog + +logger = structlog.get_logger() + + +async def test_opcua_connection(): + """Test OPC UA client connection with detailed debugging.""" + try: + from asyncua import Client + + endpoint = "opc.tcp://localhost:4840" + print(f"Testing OPC UA connection to: {endpoint}") + + # Create client + client = Client(url=endpoint) + + # Set timeout + client.secure_channel_timeout = 10000 + client.session_timeout = 60000 + + print("Attempting to connect...") + + # Try to connect + await client.connect() + + print("✓ Connection successful!") + + # Get server info + server_info = await client.get_server_node().get_child(["Server", "ServerStatus"]) + print(f"Server status: {server_info}") + + # Try to read a node + try: + # Try to read a known node + node = client.get_node("ns=2;s=Station_STATION_001.Pump_PUMP_001.Setpoint_Hz") + value = await node.read_value() + print(f"✓ Successfully read node value: {value}") + except Exception as e: + print(f"✗ Failed to read node: {e}") + + # Disconnect + await client.disconnect() + print("✓ Disconnected successfully") + + except asyncio.TimeoutError: + print("✗ Connection timeout") + except Exception as e: + print(f"✗ Connection failed: {e}") + import traceback + traceback.print_exc() + + +async def test_opcua_with_security_policy(): + """Test OPC UA connection with explicit security policy.""" + try: + from asyncua import Client + from asyncua.crypto.security_policies import SecurityPolicyBasic256Sha256 + + endpoint = "opc.tcp://localhost:4840" + print(f"\nTesting OPC UA connection with explicit security policy: {endpoint}") + + # Create client + client = Client(url=endpoint) + + # Try with None security policy explicitly + print("Attempting to connect with None security policy...") + await client.set_security_string("None") + + await client.connect() + print("✓ Connection successful with None security policy!") + + await client.disconnect() + + except Exception as e: + print(f"✗ Connection failed: {e}") + import traceback + traceback.print_exc() + + +async def main(): + """Run all tests.""" + print("=" * 60) + print("OPC UA Client Connection Debug") + print("=" * 60) + + await test_opcua_connection() + await test_opcua_with_security_policy() + + +if __name__ == "__main__": + asyncio.run(main()) \ No newline at end of file diff --git a/test_opcua_client_integration.py b/test_opcua_client_integration.py new file mode 100644 index 0000000..8d230db --- /dev/null +++ b/test_opcua_client_integration.py @@ -0,0 +1,59 @@ +#!/usr/bin/env python3 +""" +Test OPC UA client integration with the actual server nodes +""" + +import asyncio +import sys +import os + +# Add src to path +sys.path.insert(0, os.path.join(os.path.dirname(__file__), 'src')) + +from src.protocols.opcua_client import OPCUAClient +from config.settings import Settings + +async def test_opcua_client_integration(): + """Test that OPC UA client can read from actual server nodes""" + + settings = Settings() + client = OPCUAClient( + endpoint="opc.tcp://localhost:4840", + namespace_idx=2 + ) + + try: + # Connect to server + await client.connect() + print("✓ Connected to OPC UA server") + + # Test reading specific nodes that should exist + test_nodes = [ + "ns=2;s=Station_STATION_001.Pump_PUMP_001.Setpoint_Hz", + "ns=2;s=Station_STATION_001.Pump_PUMP_001.ActualSpeed_Hz", + "ns=2;s=Station_STATION_001.Pump_PUMP_001.Power_kW", + "ns=2;s=Station_STATION_001.Pump_PUMP_001.FlowRate_m3h", + "ns=2;s=Station_STATION_001.Pump_PUMP_001.SafetyStatus" + ] + + for node_id in test_nodes: + try: + value = await client.read_value(node_id) + print(f"✓ Read {node_id}: {value}") + except Exception as e: + print(f"✗ Failed to read {node_id}: {e}") + + # Test reading multiple nodes at once + print("\nTesting batch read:") + values = await client.read_values(test_nodes) + for node_id, value in zip(test_nodes, values): + print(f" {node_id}: {value}") + + except Exception as e: + print(f"✗ Connection failed: {e}") + finally: + await client.disconnect() + print("✓ Disconnected from OPC UA server") + +if __name__ == "__main__": + asyncio.run(test_opcua_client_integration()) \ No newline at end of file diff --git a/test_opcua_endpoints.py b/test_opcua_endpoints.py new file mode 100644 index 0000000..e4d54f1 --- /dev/null +++ b/test_opcua_endpoints.py @@ -0,0 +1,55 @@ +#!/usr/bin/env python3 +""" +Test script to check what OPC UA endpoints are available. +""" + +import asyncio +import sys +import os + +# Add src to path +sys.path.insert(0, os.path.join(os.path.dirname(__file__), 'src')) + +from asyncua import Client + + +async def test_opcua_endpoints(): + """Test OPC UA server endpoints.""" + print("Testing OPC UA server endpoints...") + + try: + client = Client(url="opc.tcp://localhost:4840") + + # Try to get available endpoints + print("Getting available endpoints...") + endpoints = await client.get_endpoints() + + print(f"Found {len(endpoints)} endpoints:") + for i, endpoint in enumerate(endpoints): + print(f"\nEndpoint {i+1}:") + print(f" Endpoint URL: {endpoint.EndpointUrl}") + print(f" Security Mode: {endpoint.SecurityMode}") + print(f" Security Policy URI: {endpoint.SecurityPolicyUri}") + print(f" Transport Profile URI: {endpoint.TransportProfileUri}") + + await client.disconnect() + + except Exception as e: + print(f"Error getting endpoints: {e}") + + +async def main(): + """Run the test.""" + print("=" * 50) + print("OPC UA Endpoints Test") + print("=" * 50) + + await test_opcua_endpoints() + + print("\n" + "=" * 50) + print("Test completed") + print("=" * 50) + + +if __name__ == "__main__": + asyncio.run(main()) \ No newline at end of file diff --git a/test_opcua_endpoints_v2.py b/test_opcua_endpoints_v2.py new file mode 100644 index 0000000..4de693b --- /dev/null +++ b/test_opcua_endpoints_v2.py @@ -0,0 +1,84 @@ +#!/usr/bin/env python3 +""" +Test script to check OPC UA server endpoints with proper connection +""" + +import asyncio + + +async def test_opcua_endpoints(): + """Test OPC UA server endpoints with proper connection.""" + print("Testing OPC UA server endpoints...") + + try: + from asyncua import Client + + client = Client(url="opc.tcp://localhost:4840") + + # First connect to get endpoints + print("Connecting to server...") + await client.connect() + print("✓ Connected successfully") + + # Now get endpoints + print("\nGetting available endpoints...") + endpoints = await client.get_endpoints() + + print(f"Found {len(endpoints)} endpoints:") + for i, endpoint in enumerate(endpoints): + print(f"\nEndpoint {i+1}:") + print(f" Endpoint URL: {endpoint.EndpointUrl}") + print(f" Security Mode: {endpoint.SecurityMode}") + print(f" Security Policy URI: {endpoint.SecurityPolicyUri}") + print(f" Transport Profile URI: {endpoint.TransportProfileUri}") + + await client.disconnect() + + except Exception as e: + print(f"Error: {e}") + import traceback + traceback.print_exc() + + +async def test_with_none_security(): + """Test connection with explicit None security.""" + print("\n\nTesting with explicit None security...") + + try: + from asyncua import Client + from asyncua.ua import MessageSecurityMode + + client = Client(url="opc.tcp://localhost:4840") + + # Set security to None explicitly + client.security_policy.Mode = MessageSecurityMode.None_ + client.security_policy.URI = "http://opcfoundation.org/UA/SecurityPolicy#None" + + print("Connecting with None security mode...") + await client.connect() + print("✓ Connected successfully with None security!") + + await client.disconnect() + + except Exception as e: + print(f"Error: {e}") + import traceback + traceback.print_exc() + + +async def main(): + """Run the test.""" + print("=" * 50) + print("OPC UA Endpoints Test") + print("=" * 50) + + await test_opcua_endpoints() + await test_with_none_security() + + print("\n" + "=" * 50) + print("Test completed") + print("=" * 50) + + +if __name__ == "__main__": + asyncio.run(main()) \ No newline at end of file diff --git a/test_opcua_fix.py b/test_opcua_fix.py new file mode 100644 index 0000000..73e7008 --- /dev/null +++ b/test_opcua_fix.py @@ -0,0 +1,82 @@ +#!/usr/bin/env python3 +""" +Test script to fix OPC UA client connection with proper security mode +""" + +import asyncio + + +async def test_opcua_with_correct_security(): + """Test OPC UA connection with correct security mode.""" + print("Testing OPC UA connection with correct security mode...") + + try: + from asyncua import Client + from asyncua.ua import MessageSecurityMode + + client = Client(url="opc.tcp://localhost:4840") + + # Set security to None explicitly - BOTH mode and URI + client.security_policy.Mode = MessageSecurityMode.None_ + client.security_policy.URI = "http://opcfoundation.org/UA/SecurityPolicy#None" + + print(f"Security Mode: {client.security_policy.Mode}") + print(f"Security Policy URI: {client.security_policy.URI}") + + print("Connecting...") + await client.connect() + print("✓ Connected successfully!") + + # Try to read a node + try: + node = client.get_node("ns=2;s=Station_STATION_001.Pump_PUMP_001.Setpoint_Hz") + value = await node.read_value() + print(f"✓ Successfully read node value: {value}") + except Exception as e: + print(f"✗ Failed to read node: {e}") + + await client.disconnect() + print("✓ Disconnected successfully") + + except Exception as e: + print(f"✗ Connection failed: {e}") + import traceback + traceback.print_exc() + + +async def test_opcua_with_auto_security(): + """Test OPC UA connection with automatic security negotiation.""" + print("\n\nTesting OPC UA connection with automatic security negotiation...") + + try: + from asyncua import Client + + client = Client(url="opc.tcp://localhost:4840") + + # Don't set any security - let it auto-negotiate + print("Connecting with auto-negotiation...") + await client.connect() + print("✓ Connected successfully with auto-negotiation!") + + await client.disconnect() + + except Exception as e: + print(f"✗ Auto-negotiation failed: {e}") + + +async def main(): + """Run the test.""" + print("=" * 60) + print("OPC UA Security Fix Test") + print("=" * 60) + + await test_opcua_with_correct_security() + await test_opcua_with_auto_security() + + print("\n" + "=" * 60) + print("Test completed") + print("=" * 60) + + +if __name__ == "__main__": + asyncio.run(main()) \ No newline at end of file diff --git a/test_protocol_clients.py b/test_protocol_clients.py new file mode 100644 index 0000000..f34aea4 --- /dev/null +++ b/test_protocol_clients.py @@ -0,0 +1,108 @@ +#!/usr/bin/env python3 +""" +Test script to verify protocol clients can connect and read data. +""" + +import asyncio +import sys +import os + +# Add src to path +sys.path.insert(0, os.path.join(os.path.dirname(__file__), 'src')) + +from dashboard.protocol_clients import OPCUAClient, ModbusClient + + +async def test_opcua_client(): + """Test OPC UA client connection and data reading.""" + print("Testing OPC UA client...") + client = OPCUAClient(endpoint="opc.tcp://localhost:4840") + + try: + # Test connection + connected = await client.connect() + if connected: + print("✓ OPC UA client connected successfully") + + # Test reading a node + # Try to read a known node (setpoint for station 1, pump 1) + node_id = "ns=2;s=Station_STATION_001.Pump_PUMP_001.Setpoint_Hz" + value = await client.read_node_value(node_id) + if value is not None: + print(f"✓ OPC UA read successful: {value}") + else: + print("✗ OPC UA read returned None (node might not exist)") + + # Test getting pump data + pump_data = await client.get_pump_data("STATION_001", "PUMP_001") + if pump_data: + print(f"✓ OPC UA pump data retrieved: {pump_data}") + else: + print("✗ OPC UA pump data returned empty dict") + + await client.disconnect() + print("✓ OPC UA client disconnected") + else: + print("✗ OPC UA client failed to connect") + + except Exception as e: + print(f"✗ OPC UA client test failed: {e}") + + +def test_modbus_client(): + """Test Modbus client connection and data reading.""" + print("\nTesting Modbus client...") + client = ModbusClient(host="localhost", port=502, unit_id=1) + + try: + # Test connection + connected = client.connect() + if connected: + print("✓ Modbus client connected successfully") + + # Test reading holding register (setpoint for pump 1) + setpoint = client.read_holding_register(0, 1) # Address 0 for pump 1 + if setpoint is not None: + print(f"✓ Modbus holding register read successful: {setpoint}") + else: + print("✗ Modbus holding register read returned None") + + # Test reading input register (status for pump 1) + status = client.read_input_register(100, 1) # Address 100 for pump 1 + if status is not None: + print(f"✓ Modbus input register read successful: {status}") + else: + print("✗ Modbus input register read returned None") + + # Test getting pump registers + pump_registers = client.get_pump_registers(1) # Pump 1 + if pump_registers: + print(f"✓ Modbus pump registers retrieved: {pump_registers}") + else: + print("✗ Modbus pump registers returned empty dict") + + client.disconnect() + print("✓ Modbus client disconnected") + else: + print("✗ Modbus client failed to connect") + + except Exception as e: + print(f"✗ Modbus client test failed: {e}") + + +async def main(): + """Run all tests.""" + print("=" * 50) + print("Protocol Clients Test") + print("=" * 50) + + await test_opcua_client() + test_modbus_client() + + print("\n" + "=" * 50) + print("Test completed") + print("=" * 50) + + +if __name__ == "__main__": + asyncio.run(main()) \ No newline at end of file diff --git a/test_security_modes.py b/test_security_modes.py new file mode 100644 index 0000000..0d569c2 --- /dev/null +++ b/test_security_modes.py @@ -0,0 +1,18 @@ +#!/usr/bin/env python3 +""" +Test script to check OPC UA security mode values +""" + +from asyncua.ua import MessageSecurityMode + +print("OPC UA Security Mode Values:") +print(f"None_ = {MessageSecurityMode.None_}") +print(f"Sign = {MessageSecurityMode.Sign}") +print(f"SignAndEncrypt = {MessageSecurityMode.SignAndEncrypt}") + +# Check if None_ is actually 1 +if MessageSecurityMode.None_ == 1: + print("\n⚠️ WARNING: MessageSecurityMode.None_ is 1, not 0!") + print("This means the client is using SignAndEncrypt mode even for None security policy!") +else: + print(f"\nMessageSecurityMode.None_ is {MessageSecurityMode.None_}") \ No newline at end of file diff --git a/test_server_endpoints.py b/test_server_endpoints.py new file mode 100644 index 0000000..a90d5cc --- /dev/null +++ b/test_server_endpoints.py @@ -0,0 +1,49 @@ +#!/usr/bin/env python3 +""" +Test script to check what endpoints the OPC UA server is actually offering +""" + +import asyncio + + +async def check_server_endpoints(): + """Check what endpoints the server is offering.""" + try: + from asyncua import Client + + client = Client(url="opc.tcp://localhost:4840") + + # Get endpoints without connecting + print("Getting server endpoints...") + endpoints = await client.connect_and_get_server_endpoints() + + print(f"\nFound {len(endpoints)} endpoint(s):") + for i, ep in enumerate(endpoints): + print(f"\nEndpoint {i+1}:") + print(f" Endpoint URL: {ep.EndpointUrl}") + print(f" Security Mode: {ep.SecurityMode}") + print(f" Security Policy URI: {ep.SecurityPolicyUri}") + print(f" Transport Profile URI: {ep.TransportProfileUri}") + + + except Exception as e: + print(f"Error: {e}") + import traceback + traceback.print_exc() + + +async def main(): + """Run the test.""" + print("=" * 60) + print("Server Endpoint Check") + print("=" * 60) + + await check_server_endpoints() + + print("\n" + "=" * 60) + print("Test completed") + print("=" * 60) + + +if __name__ == "__main__": + asyncio.run(main()) \ No newline at end of file