7b0b9c7365
Solution + 23 src projects + 26 test projects renamed; folders, csproj, namespaces, and ScadaLinkDbContext/ScadaBridgeDbContext class updated. ActorSystem "scadalink" → "scadabridge", Akka seed-node URLs migrated. SQL roles/logins, LDAP domains, CLI command name, and CLI config dir (~/.scadalink → ~/.scadabridge) also renamed. Build green; 5 Host.Tests fail awaiting SQL login rename in next commit. Pre-existing StaleTagMonitor timing flakes unchanged. Rename script committed at tools/rename-to-scadabridge.sh.
209 lines
6.7 KiB
Python
209 lines
6.7 KiB
Python
#!/usr/bin/env python3
|
|
"""OPC UA client tool for ScadaBridge test infrastructure."""
|
|
|
|
import argparse
|
|
import sys
|
|
import time
|
|
|
|
from opcua import Client, ua
|
|
|
|
|
|
DEFAULT_ENDPOINT = "opc.tcp://localhost:50000"
|
|
|
|
|
|
def cmd_check(args):
|
|
"""Connect and report server status."""
|
|
client = Client(args.endpoint)
|
|
try:
|
|
client.connect()
|
|
server_status = client.get_node(ua.ObjectIds.Server_ServerStatus).get_value()
|
|
|
|
print(f"Connected to: {args.endpoint}")
|
|
print(f"Server state: {server_status.State}")
|
|
print(f"Start time: {server_status.StartTime}")
|
|
print(f"Current time: {server_status.CurrentTime}")
|
|
print(f"Build info: {server_status.BuildInfo.ProductName} {server_status.BuildInfo.SoftwareVersion}")
|
|
|
|
ns = client.get_namespace_array()
|
|
print(f"\nNamespaces:")
|
|
for i, n in enumerate(ns):
|
|
print(f" {i}: {n}")
|
|
|
|
endpoints = client.get_endpoints()
|
|
print(f"\nEndpoints ({len(endpoints)}):")
|
|
for ep in endpoints:
|
|
print(f" {ep.EndpointUrl} [{ep.SecurityPolicyUri.split('#')[-1]}]")
|
|
except Exception as e:
|
|
print(f"Error: {e}", file=sys.stderr)
|
|
sys.exit(1)
|
|
finally:
|
|
client.disconnect()
|
|
|
|
|
|
def cmd_browse(args):
|
|
"""Browse server node tree."""
|
|
client = Client(args.endpoint)
|
|
try:
|
|
client.connect()
|
|
if args.path:
|
|
root = client.get_objects_node()
|
|
node = root.get_child(args.path.split("."))
|
|
else:
|
|
node = client.get_objects_node()
|
|
|
|
browse_name = node.get_browse_name()
|
|
print(f"Browsing: {browse_name.Name} ({node.nodeid})")
|
|
print()
|
|
|
|
children = node.get_children()
|
|
for child in children:
|
|
name = child.get_browse_name()
|
|
node_class = child.get_node_class()
|
|
class_name = node_class.name if hasattr(node_class, "name") else str(node_class)
|
|
|
|
if node_class == ua.NodeClass.Variable:
|
|
try:
|
|
value = child.get_value()
|
|
dtype = child.get_data_type_as_variant_type()
|
|
print(f" {name.Name:<30} [{class_name}] {dtype.name} = {value}")
|
|
except Exception:
|
|
print(f" {name.Name:<30} [{class_name}]")
|
|
else:
|
|
print(f" {name.Name:<30} [{class_name}]")
|
|
except Exception as e:
|
|
print(f"Error: {e}", file=sys.stderr)
|
|
sys.exit(1)
|
|
finally:
|
|
client.disconnect()
|
|
|
|
|
|
def cmd_read(args):
|
|
"""Read a node value."""
|
|
client = Client(args.endpoint)
|
|
try:
|
|
client.connect()
|
|
node = client.get_node(args.node)
|
|
data_value = node.get_data_value()
|
|
value = data_value.Value.Value
|
|
dtype = node.get_data_type_as_variant_type()
|
|
ts = data_value.SourceTimestamp
|
|
|
|
print(f"Node: {args.node}")
|
|
print(f"Value: {value}")
|
|
print(f"Data type: {dtype.name}")
|
|
print(f"Timestamp: {ts}")
|
|
except Exception as e:
|
|
print(f"Error: {e}", file=sys.stderr)
|
|
sys.exit(1)
|
|
finally:
|
|
client.disconnect()
|
|
|
|
|
|
def cmd_write(args):
|
|
"""Write a value to a node."""
|
|
type_map = {
|
|
"Double": (ua.VariantType.Double, float),
|
|
"Boolean": (ua.VariantType.Boolean, lambda v: v.lower() in ("true", "1", "yes")),
|
|
"UInt32": (ua.VariantType.UInt32, int),
|
|
"Int32": (ua.VariantType.Int32, int),
|
|
"String": (ua.VariantType.String, str),
|
|
}
|
|
|
|
if args.type not in type_map:
|
|
print(f"Error: unsupported type '{args.type}'. Use one of: {', '.join(type_map)}", file=sys.stderr)
|
|
sys.exit(1)
|
|
|
|
variant_type, converter = type_map[args.type]
|
|
converted_value = converter(args.value)
|
|
|
|
client = Client(args.endpoint)
|
|
try:
|
|
client.connect()
|
|
node = client.get_node(args.node)
|
|
dv = ua.DataValue(ua.Variant(converted_value, variant_type))
|
|
node.set_value(dv)
|
|
print(f"Wrote {converted_value} ({args.type}) to {args.node}")
|
|
except Exception as e:
|
|
print(f"Error: {e}", file=sys.stderr)
|
|
sys.exit(1)
|
|
finally:
|
|
client.disconnect()
|
|
|
|
|
|
def cmd_monitor(args):
|
|
"""Subscribe and print value changes."""
|
|
nodes = [n.strip() for n in args.nodes.split(",")]
|
|
|
|
client = Client(args.endpoint)
|
|
try:
|
|
client.connect()
|
|
|
|
class Handler:
|
|
def datachange_notification(self, node, val, data):
|
|
print(f" {node} = {val}")
|
|
|
|
handler = Handler()
|
|
sub = client.create_subscription(500, handler)
|
|
|
|
handles = []
|
|
for node_id in nodes:
|
|
node = client.get_node(node_id)
|
|
handle = sub.subscribe_data_change(node)
|
|
handles.append(handle)
|
|
name = node.get_browse_name()
|
|
print(f"Monitoring: {name.Name} ({node_id})")
|
|
|
|
print(f"\nListening for {args.duration}s...\n")
|
|
time.sleep(args.duration)
|
|
|
|
for handle in handles:
|
|
sub.unsubscribe(handle)
|
|
sub.delete()
|
|
print("\nDone.")
|
|
except Exception as e:
|
|
print(f"Error: {e}", file=sys.stderr)
|
|
sys.exit(1)
|
|
finally:
|
|
client.disconnect()
|
|
|
|
|
|
def main():
|
|
parser = argparse.ArgumentParser(description="OPC UA client tool for ScadaBridge test infrastructure")
|
|
parser.add_argument("--endpoint", default=DEFAULT_ENDPOINT, help=f"OPC UA endpoint (default: {DEFAULT_ENDPOINT})")
|
|
|
|
sub = parser.add_subparsers(dest="command", required=True)
|
|
|
|
sub.add_parser("check", help="Connect and report server status")
|
|
|
|
browse_p = sub.add_parser("browse", help="Browse server node tree")
|
|
browse_p.add_argument("--path", help="Node path to browse (e.g. 3:OpcPlc.3:Telemetry)")
|
|
|
|
read_p = sub.add_parser("read", help="Read a node value")
|
|
read_p.add_argument("--node", required=True, help="Node ID (e.g. ns=3;s=Motor.Speed)")
|
|
|
|
write_p = sub.add_parser("write", help="Write a value to a node")
|
|
write_p.add_argument("--node", required=True, help="Node ID")
|
|
write_p.add_argument("--value", required=True, help="Value to write")
|
|
write_p.add_argument("--type", required=True, choices=["Double", "Boolean", "UInt32", "Int32", "String"],
|
|
help="Data type")
|
|
|
|
monitor_p = sub.add_parser("monitor", help="Subscribe and print value changes")
|
|
monitor_p.add_argument("--nodes", required=True, help="Comma-separated node IDs")
|
|
monitor_p.add_argument("--duration", type=int, default=10, help="Duration in seconds (default: 10)")
|
|
|
|
args = parser.parse_args()
|
|
|
|
commands = {
|
|
"check": cmd_check,
|
|
"browse": cmd_browse,
|
|
"read": cmd_read,
|
|
"write": cmd_write,
|
|
"monitor": cmd_monitor,
|
|
}
|
|
|
|
commands[args.command](args)
|
|
|
|
|
|
if __name__ == "__main__":
|
|
main()
|