github.com/mdaxf/iac@v0.0.0-20240519030858-58a061660378/integration/OPCClient/opcclient.go (about) 1 package opcclient 2 3 import ( 4 "context" 5 "errors" 6 "flag" 7 "fmt" 8 "io" 9 "log" 10 "os" 11 "os/signal" 12 "syscall" 13 "time" 14 15 "github.com/gopcua/opcua" 16 "github.com/gopcua/opcua/id" 17 "github.com/gopcua/opcua/ua" 18 "github.com/mdaxf/iac/logger" 19 ) 20 21 type OPCConfig struct { 22 OPCClients []OPCClient `json:"opcclients"` 23 } 24 25 type OPCClient struct { 26 Client *opcua.Client 27 Endpoint string `json:"endpoint"` 28 Host string `json:"host"` 29 Name string `json:"name"` 30 Namespace uint16 `json:"namespace"` 31 CertFile string `json:"certFile"` 32 KeyFile string `json:"keyFile"` 33 Timeout time.Duration `json:"timeout"` 34 Nodes map[string]*opcua.Node 35 SubGroups []SubGroup `json:"subgroups"` 36 iLog logger.Log 37 } 38 39 type SubGroup struct { 40 TriggerTags []string `json:"triggerTags"` 41 ReportTags []string `json:"reportTags"` 42 Trigger func(string, *ua.DataValue) `json:"trigger"` 43 Report func(string, *ua.DataValue) `json:"report"` 44 } 45 46 func Initialize(configurations OPCClient) { 47 // Initialize the OPC UA client 48 49 iLog := logger.Log{ModuleName: logger.Framework, User: "System", ControllerName: "OPCClient"} 50 51 iLog.Debug(fmt.Sprintf(("Create OPCClient with configuration : %s"), logger.ConvertJson(configurations))) 52 53 opcclient := &OPCClient{ 54 Endpoint: configurations.Endpoint, 55 Namespace: configurations.Namespace, 56 CertFile: configurations.CertFile, 57 KeyFile: configurations.KeyFile, 58 Timeout: configurations.Timeout * time.Second, 59 SubGroups: configurations.SubGroups, 60 iLog: iLog, 61 } 62 63 cancel := opcclient.CreateClient() 64 opcclient.Connect() 65 66 defer cancel() 67 defer opcclient.Disconnect() 68 // defer opcclient.Client.CloseWithContext(context.Background()) 69 70 go func() { 71 72 for _, subgroup := range opcclient.SubGroups { 73 74 callbackfunc := func(tag string, v *ua.DataValue) { 75 fmt.Printf("Tag: %v, Value: %v\n", tag, v.Value) 76 } 77 opcclient.Subscribe(subgroup.TriggerTags, callbackfunc) 78 79 } 80 }() 81 // Wait for termination signal to gracefully shut down the client 82 83 terminate := make(chan os.Signal, 1) 84 signal.Notify(terminate, os.Interrupt, syscall.SIGTERM) 85 <-terminate 86 } 87 88 func (c *OPCClient) CreateClient() context.CancelFunc { 89 // OPC UA server configuration 90 var ( 91 endpoint = flag.String("endpoint", c.Endpoint, "OPC UA Endpoint URL") 92 policy = flag.String("policy", "", "Security policy: None, Basic128Rsa15, Basic256, Basic256Sha256. Default: auto") 93 mode = flag.String("mode", "", "Security mode: None, Sign, SignAndEncrypt. Default: auto") 94 certFile = flag.String("cert", "", "Path to cert.pem. Required for security mode/policy != None") 95 keyFile = flag.String("key", "", "Path to private key.pem. Required for security mode/policy != None") 96 ) 97 ctx, cancel := context.WithCancel(context.Background()) 98 99 endpoints, err := opcua.GetEndpoints(ctx, *endpoint) 100 if err != nil { 101 c.iLog.Critical(fmt.Sprintf("error: %v", err)) 102 } 103 104 ep := opcua.SelectEndpoint(endpoints, *policy, ua.MessageSecurityModeFromString(*mode)) 105 if ep == nil { 106 c.iLog.Critical("Failed to find suitable endpoint") 107 } 108 c.iLog.Debug(fmt.Sprintf("Using endpoint: %v", ep.EndpointURL)) 109 c.iLog.Debug(fmt.Sprintf("%s,%s", ep.SecurityPolicyURI, ep.SecurityMode)) 110 opts := []opcua.Option{ 111 opcua.SecurityPolicy(*policy), 112 opcua.SecurityModeString(*mode), 113 opcua.CertificateFile(*certFile), 114 opcua.PrivateKeyFile(*keyFile), 115 opcua.AuthAnonymous(), 116 opcua.SecurityFromEndpoint(ep, ua.UserTokenTypeAnonymous), 117 } 118 119 opcclient, err := opcua.NewClient(ep.EndpointURL, opts...) 120 121 if err != nil { 122 c.iLog.Critical(fmt.Sprintf("Failed to create client: %s", err)) 123 } 124 125 c.Client = opcclient 126 127 return cancel 128 } 129 130 func (c *OPCClient) BrowseEndpoints() (*ua.GetEndpointsResponse, error) { 131 // Browse the endpoint by the server 132 c.iLog.Debug(fmt.Sprintf("Browsing endpoints from server")) 133 endpoints, err := c.Client.GetEndpoints(context.Background()) 134 if err != nil { 135 c.iLog.Critical(fmt.Sprintf("Failed to get endpoints: %s", err)) 136 return nil, err 137 } 138 c.iLog.Debug(fmt.Sprintf("Endpoints: %v", endpoints)) 139 140 return endpoints, nil 141 } 142 func (c *OPCClient) Connect() { 143 144 c.iLog.Debug(fmt.Sprintf("Connectting to OPC Server: %s", c.Endpoint)) 145 // defer client.Close() 146 147 // Connect to the OPC UA server 148 err := c.Client.Connect(context.Background()) 149 if err != nil { 150 c.iLog.Critical(fmt.Sprintf("Failed to connect to OPC UA server: %s", err)) 151 } 152 c.iLog.Debug(fmt.Sprintf("Connected to OPC Server: %s", c.Endpoint)) 153 } 154 155 func (c *OPCClient) browseTags(nodeID string) ([]Tag, error) { 156 // Browse the node 157 c.iLog.Debug(fmt.Sprintf("Browsing nodes (tags) from server")) 158 result, err := c.Client.Browse(context.Background(), &ua.BrowseRequest{ 159 NodesToBrowse: []*ua.BrowseDescription{ 160 &ua.BrowseDescription{ 161 NodeID: ua.MustParseNodeID(nodeID), 162 ReferenceTypeID: ua.NewNumericNodeID(0, 0), 163 IncludeSubtypes: true, 164 ResultMask: uint32(ua.BrowseResultMaskAll), 165 }, 166 }, 167 }) 168 if err != nil { 169 c.iLog.Error(fmt.Sprintf("Failed to browse nodes: %s", err)) 170 return nil, err 171 } 172 173 c.iLog.Debug(fmt.Sprintf("Browse result: %v", result)) 174 175 var tags []Tag 176 177 // Process the browse result 178 for _, res := range result.Results { 179 for _, ref := range res.References { 180 // Extract the tag name and address from the reference 181 tagName := ref.DisplayName.Text 182 tagAddress := ref.NodeID.String() 183 184 // Create a new tag object 185 tag := Tag{ 186 Name: tagName, 187 Address: tagAddress, 188 } 189 190 // Add the tag to the list 191 tags = append(tags, tag) 192 } 193 } 194 c.iLog.Debug(fmt.Sprintf("Tags: %v", tags)) 195 return tags, nil 196 } 197 198 func (c *OPCClient) readTagValue(nodeID string) (interface{}, error) { 199 // Read the node value 200 c.iLog.Debug(fmt.Sprintf("Reading node value: %s", nodeID)) 201 opcnodeid := flag.String("node", "", nodeID) 202 203 id, err := ua.ParseNodeID(*opcnodeid) 204 if err != nil { 205 c.iLog.Critical(fmt.Sprintf("invalid node id: %v", err)) 206 } 207 208 req := &ua.ReadRequest{ 209 MaxAge: 2000, 210 NodesToRead: []*ua.ReadValueID{ 211 {NodeID: id}, 212 }, 213 TimestampsToReturn: ua.TimestampsToReturnBoth, 214 } 215 216 var resp *ua.ReadResponse 217 218 for { 219 resp, err = c.Client.Read(context.Background(), req) //.ReadWithContext(context.Background(), req) 220 if err == nil { 221 break 222 } 223 224 // Following switch contains known errors that can be retried by the user. 225 // Best practice is to do it on read operations. 226 switch { 227 case err == io.EOF && c.Client.State() != opcua.Closed: 228 // has to be retried unless user closed the connection 229 time.After(1 * time.Second) 230 continue 231 232 case errors.Is(err, ua.StatusBadSessionIDInvalid): 233 // Session is not activated has to be retried. Session will be recreated internally. 234 time.After(1 * time.Second) 235 continue 236 237 case errors.Is(err, ua.StatusBadSessionNotActivated): 238 // Session is invalid has to be retried. Session will be recreated internally. 239 time.After(1 * time.Second) 240 continue 241 242 case errors.Is(err, ua.StatusBadSecureChannelIDInvalid): 243 // secure channel will be recreated internally. 244 time.After(1 * time.Second) 245 continue 246 247 default: 248 c.iLog.Critical(fmt.Sprintf("Read failed: %s", err)) 249 } 250 } 251 252 if resp != nil && resp.Results[0].Status != ua.StatusOK { 253 c.iLog.Critical(fmt.Sprintf("Status not OK: %v", resp.Results[0].Status)) 254 } 255 256 return resp.Results[0].Value.Value(), nil 257 } 258 259 func (c *OPCClient) writeTagValue(nodeID string, value string) (ua.StatusCode, error) { 260 261 opcnodeid := flag.String("node", "", nodeID) 262 opcvalue := flag.String("value", "", value) 263 264 // Create a variant from the value 265 c.iLog.Debug(fmt.Sprintf("Writing node value: %s, %v", nodeID, value)) 266 id, err := ua.ParseNodeID(*opcnodeid) 267 if err != nil { 268 c.iLog.Critical(fmt.Sprintf("invalid node id: %v", err)) 269 } 270 271 v, err := ua.NewVariant(*opcvalue) 272 if err != nil { 273 c.iLog.Critical(fmt.Sprintf("invalid value: %v", err)) 274 } 275 276 if err != nil { 277 c.iLog.Error(fmt.Sprintf("Failed to create variant: %s", err)) 278 return ua.StatusBad, err 279 } 280 281 // Write the node value 282 req := &ua.WriteRequest{ 283 NodesToWrite: []*ua.WriteValue{ 284 { 285 NodeID: id, 286 AttributeID: ua.AttributeIDValue, 287 Value: &ua.DataValue{ 288 EncodingMask: ua.DataValueValue, 289 Value: v, 290 }, 291 }, 292 }, 293 } 294 295 resp, err := c.Client.Write(context.Background(), req) //.WriteWithContext(context.Background(), req) 296 if err != nil { 297 c.iLog.Critical(fmt.Sprintf("Write failed: %s", err)) 298 return ua.StatusBad, err 299 } 300 301 c.iLog.Debug(fmt.Sprintf("Node value written with result: %v", resp.Results[0])) 302 return resp.Results[0], nil 303 } 304 305 func (c *OPCClient) Subscribe(triggerTags []string, callback func(string, *ua.DataValue)) { 306 notifyCh := make(chan *opcua.PublishNotificationData, 1000) 307 ctx := context.Background() 308 interval := flag.Duration("interval", opcua.DefaultSubscriptionInterval, "subscription interval") 309 sub, err := c.Client.Subscribe(ctx, &opcua.SubscriptionParameters{ //).SubscribeWithContext 310 Interval: *interval, 311 }, notifyCh) 312 if err != nil { 313 c.iLog.Critical(fmt.Sprintf("Failed to create subscription: %s", err)) 314 } 315 defer sub.Cancel(ctx) 316 317 event := flag.Bool("event", false, "subscribe to node event changes (Default: node value changes)") 318 319 for _, tag := range triggerTags { 320 // Create a monitored item for the tag 321 nodeID := flag.String("node", tag, "node id to subscribe to") 322 //nodeID := ua.NewStringNodeID(2, tag) 323 id, err := ua.ParseNodeID(*nodeID) 324 325 if err != nil { 326 c.iLog.Critical(fmt.Sprintf("invalid node id: %v", err)) 327 } 328 329 var miCreateRequest *ua.MonitoredItemCreateRequest 330 var eventFieldNames []string 331 332 if *event { 333 miCreateRequest, eventFieldNames = eventRequest(id) 334 } else { 335 miCreateRequest = valueRequest(id) 336 } 337 res, err := sub.Monitor(context.Background(), ua.TimestampsToReturnBoth, miCreateRequest) 338 if err != nil || res.Results[0].StatusCode != ua.StatusOK { 339 c.iLog.Error(fmt.Sprintf("Failed to create monitored item for tag: %v", err)) 340 } 341 // read from subscription's notification channel until ctx is cancelled 342 for { 343 select { 344 case <-ctx.Done(): 345 return 346 case res := <-notifyCh: 347 if res.Error != nil { 348 c.iLog.Error(fmt.Sprintf("there is error: %v", res.Error)) 349 continue 350 } 351 352 switch x := res.Value.(type) { 353 case *ua.DataChangeNotification: 354 for _, item := range x.MonitoredItems { 355 data := item.Value.Value.Value() 356 c.iLog.Debug(fmt.Sprintf("MonitoredItem with client handle %v = %v", item.ClientHandle, data)) 357 } 358 359 case *ua.EventNotificationList: 360 for _, item := range x.Events { 361 c.iLog.Debug(fmt.Sprintf("Event for client handle: %v\n", item.ClientHandle)) 362 for i, field := range item.EventFields { 363 c.iLog.Debug(fmt.Sprintf("%v: %v of Type: %T", eventFieldNames[i], field.Value(), field.Value())) 364 } 365 366 } 367 368 default: 369 c.iLog.Debug(fmt.Sprintf("what's this publish result? %T", res.Value)) 370 } 371 } 372 } 373 374 } 375 } 376 func valueRequest(nodeID *ua.NodeID) *ua.MonitoredItemCreateRequest { 377 handle := uint32(42) 378 return opcua.NewMonitoredItemCreateRequestWithDefaults(nodeID, ua.AttributeIDValue, handle) 379 } 380 381 func eventRequest(nodeID *ua.NodeID) (*ua.MonitoredItemCreateRequest, []string) { 382 fieldNames := []string{"EventId", "EventType", "Severity", "Time", "Message"} 383 selects := make([]*ua.SimpleAttributeOperand, len(fieldNames)) 384 385 for i, name := range fieldNames { 386 selects[i] = &ua.SimpleAttributeOperand{ 387 TypeDefinitionID: ua.NewNumericNodeID(0, id.BaseEventType), 388 BrowsePath: []*ua.QualifiedName{{NamespaceIndex: 0, Name: name}}, 389 AttributeID: ua.AttributeIDValue, 390 } 391 } 392 393 wheres := &ua.ContentFilter{ 394 Elements: []*ua.ContentFilterElement{ 395 { 396 FilterOperator: ua.FilterOperatorGreaterThanOrEqual, 397 FilterOperands: []*ua.ExtensionObject{ 398 { 399 EncodingMask: 1, 400 TypeID: &ua.ExpandedNodeID{ 401 NodeID: ua.NewNumericNodeID(0, id.SimpleAttributeOperand_Encoding_DefaultBinary), 402 }, 403 Value: ua.SimpleAttributeOperand{ 404 TypeDefinitionID: ua.NewNumericNodeID(0, id.BaseEventType), 405 BrowsePath: []*ua.QualifiedName{{NamespaceIndex: 0, Name: "Severity"}}, 406 AttributeID: ua.AttributeIDValue, 407 }, 408 }, 409 { 410 EncodingMask: 1, 411 TypeID: &ua.ExpandedNodeID{ 412 NodeID: ua.NewNumericNodeID(0, id.LiteralOperand_Encoding_DefaultBinary), 413 }, 414 Value: ua.LiteralOperand{ 415 Value: ua.MustVariant(uint16(0)), 416 }, 417 }, 418 }, 419 }, 420 }, 421 } 422 423 filter := ua.EventFilter{ 424 SelectClauses: selects, 425 WhereClause: wheres, 426 } 427 428 filterExtObj := ua.ExtensionObject{ 429 EncodingMask: ua.ExtensionObjectBinary, 430 TypeID: &ua.ExpandedNodeID{ 431 NodeID: ua.NewNumericNodeID(0, id.EventFilter_Encoding_DefaultBinary), 432 }, 433 Value: filter, 434 } 435 436 handle := uint32(42) 437 req := &ua.MonitoredItemCreateRequest{ 438 ItemToMonitor: &ua.ReadValueID{ 439 NodeID: nodeID, 440 AttributeID: ua.AttributeIDEventNotifier, 441 DataEncoding: &ua.QualifiedName{}, 442 }, 443 MonitoringMode: ua.MonitoringModeReporting, 444 RequestedParameters: &ua.MonitoringParameters{ 445 ClientHandle: handle, 446 DiscardOldest: true, 447 Filter: &filterExtObj, 448 QueueSize: 10, 449 SamplingInterval: 1.0, 450 }, 451 } 452 453 return req, fieldNames 454 } 455 456 func (c *OPCClient) createSubscriptionGroup(triggerTags []string, callback func(string, *ua.DataValue)) (*opcua.Subscription, error) { 457 458 notifyCh := make(chan *opcua.PublishNotificationData) 459 460 sub, err := c.Client.Subscribe(context.Background(), &opcua.SubscriptionParameters{ 461 Interval: opcua.DefaultSubscriptionInterval, 462 }, notifyCh) 463 464 // RequestedPublishingInterval: 1000, 465 //RequestedMaxKeepAliveCount: 10, 466 //RequestedLifetimeCount: 100, 467 //PublishingEnabled: true, 468 if err != nil { 469 c.iLog.Critical(fmt.Sprintf("Failed to create subscription: %s", err)) 470 } 471 /* 472 for _, tag := range triggerTags { 473 // Create a monitored item for the tag 474 nodeID := ua.NewStringNodeID(2, tag) 475 476 item, err := sub.MonitorValue(nodeID, ua.AttributeIDValue, func(v *ua.DataValue) { 477 // Trigger action for tag1 478 callback(tag, v) 479 c.iLog.Debug(fmt.Sprintf("Trigger action for tag1: %v", v.Value)) 480 }) 481 if err != nil { 482 c.iLog.Error(fmt.Sprintf("Failed to create monitored item for tag1: %v", err)) 483 } 484 485 } 486 */ 487 var nodeID = flag.String("node", "", "node id to subscribe to") 488 var event = flag.Bool("event", false, "subscribe to node event changes (Default: node value changes)") 489 490 id, err := ua.ParseNodeID(*nodeID) 491 if err != nil { 492 log.Fatal(err) 493 } 494 495 var miCreateRequest *ua.MonitoredItemCreateRequest 496 var eventFieldNames []string 497 if *event { 498 miCreateRequest, eventFieldNames = eventRequest(id) 499 } else { 500 miCreateRequest = valueRequest(id) 501 } 502 res, err := sub.Monitor(context.Background(), ua.TimestampsToReturnBoth, miCreateRequest) 503 if err != nil || res.Results[0].StatusCode != ua.StatusOK { 504 log.Fatal(err) 505 } 506 507 // read from subscription's notification channel until ctx is cancelled 508 for { 509 select { 510 case <-context.Background().Done(): 511 return sub, nil 512 case res := <-notifyCh: 513 if res.Error != nil { 514 log.Print(res.Error) 515 continue 516 } 517 518 switch x := res.Value.(type) { 519 case *ua.DataChangeNotification: 520 for _, item := range x.MonitoredItems { 521 data := item.Value.Value.Value() 522 log.Printf("MonitoredItem with client handle %v = %v", item.ClientHandle, data) 523 } 524 525 case *ua.EventNotificationList: 526 for _, item := range x.Events { 527 log.Printf("Event for client handle: %v\n", item.ClientHandle) 528 for i, field := range item.EventFields { 529 log.Printf("%v: %v of Type: %T", eventFieldNames[i], field.Value(), field.Value()) 530 } 531 log.Println() 532 } 533 534 default: 535 log.Printf("what's this publish result? %T", res.Value) 536 } 537 } 538 } 539 return sub, nil 540 } 541 542 func (c *OPCClient) monitorSubscribeGroup(group []*opcua.Subscription) { 543 filter := flag.String("filter", "timestamp", "DataFilter: status, value, timestamp.") 544 // Start the subscription 545 for _, sub := range group { 546 547 // need to change 548 549 triggerNodeID := flag.String("trigger", "", "node id to trigger with") 550 reportNodeID := flag.String("report", "", "node id value to report on trigger") 551 triggeringNode, err := ua.ParseNodeID(*triggerNodeID) 552 if err != nil { 553 c.iLog.Critical(fmt.Sprintf("There are error:%v", err)) 554 } 555 556 triggeredNode, err := ua.ParseNodeID(*reportNodeID) 557 if err != nil { 558 c.iLog.Critical(fmt.Sprintf("There are error:%v", err)) 559 } 560 561 miCreateRequests := []*ua.MonitoredItemCreateRequest{ 562 opcua.NewMonitoredItemCreateRequestWithDefaults(triggeringNode, ua.AttributeIDValue, 42), 563 { 564 ItemToMonitor: &ua.ReadValueID{ 565 NodeID: triggeredNode, 566 AttributeID: ua.AttributeIDValue, 567 DataEncoding: &ua.QualifiedName{}, 568 }, 569 MonitoringMode: ua.MonitoringModeSampling, 570 RequestedParameters: &ua.MonitoringParameters{ 571 ClientHandle: 43, 572 DiscardOldest: true, 573 Filter: c.getFilter(*filter), 574 QueueSize: 10, 575 SamplingInterval: 0.0, 576 }, 577 }, 578 } 579 580 sub.Monitor(context.Background(), ua.TimestampsToReturnBoth, miCreateRequests...) 581 } 582 } 583 584 func (c *OPCClient) getFilter(filterStr string) *ua.ExtensionObject { 585 586 var filter ua.DataChangeFilter 587 switch filterStr { 588 case "status": 589 filter = ua.DataChangeFilter{Trigger: ua.DataChangeTriggerStatus} 590 case "value": 591 filter = ua.DataChangeFilter{Trigger: ua.DataChangeTriggerStatusValue} 592 case "timestamp": 593 filter = ua.DataChangeFilter{Trigger: ua.DataChangeTriggerStatusValueTimestamp} 594 default: 595 log.Fatalf("Unable to match to a valid filter type: %v\nShould be status, value, or timestamp", filterStr) 596 } 597 598 return &ua.ExtensionObject{ 599 EncodingMask: ua.ExtensionObjectBinary, 600 TypeID: &ua.ExpandedNodeID{ 601 NodeID: ua.NewNumericNodeID(0, id.DataChangeFilter_Encoding_DefaultBinary), 602 }, 603 Value: filter, 604 } 605 } 606 607 func (c *OPCClient) Disconnect() { 608 // Disconnect from the OPC UA server 609 c.Client.Close(context.Background()) 610 c.iLog.Debug(fmt.Sprintf("Disconnected from OPC Server: %s", c.Endpoint)) 611 } 612 613 type Tag struct { 614 Name string 615 Address string 616 }