243 lines
6.8 KiB
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
|
|
}
|