Mainflux.mainflux/opcua/gopcua/subscribe.go

243 lines
6.8 KiB
Go

// Copyright (c) Mainflux
// SPDX-License-Identifier: Apache-2.0
package gopcua
import (
"context"
"fmt"
"time"
opcuaGopcua "github.com/gopcua/opcua"
uaGopcua "github.com/gopcua/opcua/ua"
"github.com/mainflux/mainflux"
"github.com/mainflux/mainflux/errors"
"github.com/mainflux/mainflux/logger"
"github.com/mainflux/mainflux/opcua"
"github.com/mainflux/mainflux/opcua/db"
)
const protocol = "opcua"
const token = ""
var (
errNotFoundServerURI = errors.New("route map not found for Server URI")
errNotFoundNodeID = errors.New("route map not found for Node ID")
errNotFoundConn = errors.New("connection not found")
errFailedConn = errors.New("failed to connect")
errFailedRead = errors.New("failed to read")
errFailedSub = errors.New("failed to subscribe")
errFailedFindEndpoint = errors.New("failed to find suitable endpoint")
errFailedFetchEndpoint = errors.New("failed to fetch OPC-UA server endpoints")
errFailedParseNodeID = errors.New("failed to parse NodeID")
errFailedCreateReq = errors.New("failed to create request")
errResponseStatus = errors.New("response status not OK")
)
var _ opcua.Subscriber = (*client)(nil)
type client struct {
ctx context.Context
publisher mainflux.MessagePublisher
thingsRM opcua.RouteMapRepository
channelsRM opcua.RouteMapRepository
connectRM opcua.RouteMapRepository
logger logger.Logger
}
type message struct {
ServerURI string
NodeID string
Type string
Time int64
DataKey string
Data interface{}
}
// NewSubscriber returns new OPC-UA client instance.
func NewSubscriber(ctx context.Context, pub mainflux.MessagePublisher, thingsRM, channelsRM, connectRM opcua.RouteMapRepository, log logger.Logger) opcua.Subscriber {
return client{
ctx: ctx,
publisher: pub,
thingsRM: thingsRM,
channelsRM: channelsRM,
connectRM: connectRM,
logger: log,
}
}
// Subscribe subscribes to the OPC-UA Server.
func (c client) Subscribe(cfg opcua.Config) error {
opts := []opcuaGopcua.Option{
opcuaGopcua.SecurityMode(uaGopcua.MessageSecurityModeNone),
}
if cfg.Mode != "" {
endpoints, err := opcuaGopcua.GetEndpoints(cfg.ServerURI)
if err != nil {
return errors.Wrap(errFailedFetchEndpoint, err)
}
ep := opcuaGopcua.SelectEndpoint(endpoints, cfg.Policy, uaGopcua.MessageSecurityModeFromString(cfg.Mode))
if ep == nil {
return errFailedFindEndpoint
}
opts = []opcuaGopcua.Option{
opcuaGopcua.SecurityPolicy(cfg.Policy),
opcuaGopcua.SecurityModeString(cfg.Mode),
opcuaGopcua.CertificateFile(cfg.CertFile),
opcuaGopcua.PrivateKeyFile(cfg.KeyFile),
opcuaGopcua.AuthAnonymous(),
opcuaGopcua.SecurityFromEndpoint(ep, uaGopcua.UserTokenTypeAnonymous),
}
}
oc := opcuaGopcua.NewClient(cfg.ServerURI, opts...)
if err := oc.Connect(c.ctx); err != nil {
return errors.Wrap(errFailedConn, err)
}
defer oc.Close()
sub, err := oc.Subscribe(&opcuaGopcua.SubscriptionParameters{
Interval: 2000 * time.Millisecond,
})
if err != nil {
return errors.Wrap(errFailedSub, err)
}
defer sub.Cancel()
if err := c.runHandler(sub, cfg.ServerURI, cfg.NodeID); err != nil {
c.logger.Warn(fmt.Sprintf("Unsubscribed from OPC-UA node %s.%s: %s", cfg.ServerURI, cfg.NodeID, err))
}
return nil
}
func (c client) runHandler(sub *opcuaGopcua.Subscription, uri, node string) error {
nodeID, err := uaGopcua.ParseNodeID(node)
if err != nil {
return errors.Wrap(errFailedParseNodeID, err)
}
// arbitrary client handle for the monitoring item
handle := uint32(42)
miCreateRequest := opcuaGopcua.NewMonitoredItemCreateRequestWithDefaults(nodeID, uaGopcua.AttributeIDValue, handle)
res, err := sub.Monitor(uaGopcua.TimestampsToReturnBoth, miCreateRequest)
if err != nil {
return errors.Wrap(errFailedCreateReq, err)
}
if res.Results[0].StatusCode != uaGopcua.StatusOK {
return errResponseStatus
}
go sub.Run(c.ctx)
// Store subscription details
if err := db.Save(uri, node); err != nil {
return err
}
c.logger.Info(fmt.Sprintf("subscribed to server %s and node_id %s", uri, node))
for {
select {
case <-c.ctx.Done():
return nil
case res := <-sub.Notifs:
if res.Error != nil {
c.logger.Error(res.Error.Error())
continue
}
switch x := res.Value.(type) {
case *uaGopcua.DataChangeNotification:
for _, item := range x.MonitoredItems {
msg := message{
ServerURI: uri,
NodeID: node,
Type: item.Value.Value.Type().String(),
Time: item.Value.SourceTimestamp.Unix(),
DataKey: "v",
}
switch item.Value.Value.Type() {
case uaGopcua.TypeIDBoolean:
msg.DataKey = "vb"
msg.Data = item.Value.Value.Bool()
case uaGopcua.TypeIDString:
msg.DataKey = "vs"
msg.Data = item.Value.Value.String()
case uaGopcua.TypeIDInt64, uaGopcua.TypeIDInt32, uaGopcua.TypeIDInt16:
msg.Data = float64(item.Value.Value.Int())
case uaGopcua.TypeIDUint64:
msg.Data = float64(item.Value.Value.Uint())
case uaGopcua.TypeIDFloat, uaGopcua.TypeIDDouble:
msg.Data = item.Value.Value.Float()
case uaGopcua.TypeIDByte:
msg.DataKey = "vs"
msg.Data = string(item.Value.Value.EncodingMask())
case uaGopcua.TypeIDDateTime:
msg.Data = item.Value.Value.Time()
default:
msg.Data = 0
}
if err := c.publish(token, msg); err != nil {
switch err {
case errNotFoundServerURI, errNotFoundNodeID, errNotFoundConn:
return err
default:
c.logger.Error(fmt.Sprintf("Failed to publish: %s", err))
}
}
}
default:
c.logger.Info(fmt.Sprintf("unknown publish result: %T", res.Value))
}
}
}
}
// Publish forwards messages from the OPC-UA Server to Mainflux NATS broker
func (c client) publish(token string, m message) error {
// Get route-map of the OPC-UA ServerURI
chanID, err := c.channelsRM.Get(m.ServerURI)
if err != nil {
return errNotFoundServerURI
}
// Get route-map of the OPC-UA NodeID
thingID, err := c.thingsRM.Get(m.NodeID)
if err != nil {
return errNotFoundNodeID
}
// Check connection between ServerURI and NodeID
cKey := fmt.Sprintf("%s:%s", chanID, thingID)
if _, err := c.connectRM.Get(cKey); err != nil {
return fmt.Errorf("%s between channel %s and thing %s", errNotFoundConn, chanID, thingID)
}
// Publish on Mainflux NATS broker
SenML := fmt.Sprintf(`[{"n":"%s", "t": %d, "%s":%v}]`, m.Type, m.Time, m.DataKey, m.Data)
payload := []byte(SenML)
msg := mainflux.Message{
Publisher: thingID,
Protocol: protocol,
ContentType: "Content-Type",
Channel: chanID,
Payload: payload,
Subtopic: m.NodeID,
}
if err := c.publisher.Publish(c.ctx, token, msg); err != nil {
return err
}
c.logger.Info(fmt.Sprintf("publish from server %s and node_id %s with value %v", m.ServerURI, m.NodeID, m.Data))
return nil
}