github.com/enbility/spine-go@v0.7.0/spine/send.go (about)

     1  package spine
     2  
     3  import (
     4  	"crypto/sha256"
     5  	"encoding/hex"
     6  	"encoding/json"
     7  	"errors"
     8  	"fmt"
     9  	"sort"
    10  	"sync"
    11  	"sync/atomic"
    12  
    13  	shipapi "github.com/enbility/ship-go/api"
    14  	"github.com/enbility/ship-go/logging"
    15  	"github.com/enbility/spine-go/api"
    16  	"github.com/enbility/spine-go/model"
    17  	"github.com/enbility/spine-go/util"
    18  	"github.com/golanguzb70/lrucache"
    19  )
    20  
    21  type reqMsgCacheData map[model.MsgCounterType]string
    22  
    23  type Sender struct {
    24  	msgNum uint64 // 64bit values need to be defined on top of the struct to make atomic commands work on 32bit systems
    25  
    26  	// we cache the last 100 notify messages, so we can find the matching item for result errors being returned
    27  	datagramNotifyCache *lrucache.LRUCache[model.MsgCounterType, model.DatagramType]
    28  
    29  	writeHandler shipapi.ShipConnectionDataWriterInterface
    30  
    31  	reqMsgCache reqMsgCacheData // cache for unanswered request messages, so we can filter duplicates and not send them
    32  
    33  	muxNotifyCache sync.RWMutex
    34  	muxReadCache   sync.RWMutex
    35  }
    36  
    37  var _ api.SenderInterface = (*Sender)(nil)
    38  
    39  func NewSender(writeI shipapi.ShipConnectionDataWriterInterface) api.SenderInterface {
    40  	cache := lrucache.New[model.MsgCounterType, model.DatagramType](100, 0)
    41  	return &Sender{
    42  		datagramNotifyCache: &cache,
    43  		writeHandler:        writeI,
    44  		reqMsgCache:         make(reqMsgCacheData),
    45  	}
    46  }
    47  
    48  // return the datagram for a given msgCounter (only availbe for Notify messasges!), error if not found
    49  func (c *Sender) DatagramForMsgCounter(msgCounter model.MsgCounterType) (model.DatagramType, error) {
    50  	c.muxNotifyCache.RLock()
    51  	defer c.muxNotifyCache.RUnlock()
    52  
    53  	if datagram, ok := c.datagramNotifyCache.Get(msgCounter); ok {
    54  		return datagram, nil
    55  	}
    56  
    57  	return model.DatagramType{}, errors.New("msgCounter not found")
    58  }
    59  
    60  func (c *Sender) sendSpineMessage(datagram model.DatagramType) error {
    61  	// pack into datagram
    62  	data := model.Datagram{
    63  		Datagram: datagram,
    64  	}
    65  
    66  	// marshal
    67  	msg, err := json.Marshal(data)
    68  	if err != nil {
    69  		return err
    70  	}
    71  
    72  	if c.writeHandler == nil {
    73  		return errors.New("outgoing interface implementation not set")
    74  	}
    75  
    76  	if msg == nil {
    77  		return errors.New("message is nil")
    78  	}
    79  
    80  	logging.Log().Debug(datagram.PrintMessageOverview(true, "", ""))
    81  
    82  	// write to channel
    83  	c.writeHandler.WriteShipMessageWithPayload(msg)
    84  
    85  	return nil
    86  }
    87  
    88  // Caching of outgoing and unanswered requests, so we can filter duplicates
    89  func (c *Sender) hashForMessage(destinationAddress *model.FeatureAddressType, cmd []model.CmdType) string {
    90  	cmdString, err := json.Marshal(cmd)
    91  	if err != nil {
    92  		return ""
    93  	}
    94  
    95  	sig := fmt.Sprintf("%s-%s", destinationAddress.String(), cmdString)
    96  	shaBytes := sha256.Sum256([]byte(sig))
    97  	return hex.EncodeToString(shaBytes[:])
    98  }
    99  
   100  func (c *Sender) msgCounterForHashFromCache(hash string) *model.MsgCounterType {
   101  	c.muxReadCache.RLock()
   102  	defer c.muxReadCache.RUnlock()
   103  
   104  	for msgCounter, h := range c.reqMsgCache {
   105  		if h == hash {
   106  			return &msgCounter
   107  		}
   108  	}
   109  
   110  	return nil
   111  }
   112  
   113  func (c *Sender) hasMsgCounterInCache(msgCounter model.MsgCounterType) bool {
   114  	c.muxReadCache.RLock()
   115  	defer c.muxReadCache.RUnlock()
   116  
   117  	_, ok := c.reqMsgCache[msgCounter]
   118  
   119  	return ok
   120  }
   121  
   122  func (c *Sender) addMsgCounterHashToCache(msgCounter model.MsgCounterType, hash string) {
   123  	c.muxReadCache.Lock()
   124  	defer c.muxReadCache.Unlock()
   125  
   126  	// cleanup cache, keep only the last 20 messages
   127  	if len(c.reqMsgCache) > 20 {
   128  		keys := make([]uint64, 0, len(c.reqMsgCache))
   129  		for k := range c.reqMsgCache {
   130  			keys = append(keys, uint64(k))
   131  		}
   132  		sort.Slice(keys, func(i, j int) bool { return keys[i] < keys[j] })
   133  
   134  		// oldest key is the one with the lowest msgCounterValue
   135  		oldestKey := keys[0]
   136  		delete(c.reqMsgCache, model.MsgCounterType(oldestKey))
   137  	}
   138  
   139  	c.reqMsgCache[msgCounter] = hash
   140  }
   141  
   142  // we need to remove the msgCounter from the cache, if we have it cached
   143  func (c *Sender) ProcessResponseForMsgCounterReference(msgCounterRef *model.MsgCounterType) {
   144  	if msgCounterRef != nil &&
   145  		c.hasMsgCounterInCache(*msgCounterRef) {
   146  		c.muxReadCache.Lock()
   147  		defer c.muxReadCache.Unlock()
   148  
   149  		delete(c.reqMsgCache, *msgCounterRef)
   150  	}
   151  }
   152  
   153  // Sends request
   154  func (c *Sender) Request(cmdClassifier model.CmdClassifierType, senderAddress, destinationAddress *model.FeatureAddressType, ackRequest bool, cmd []model.CmdType) (*model.MsgCounterType, error) {
   155  	// check if there is an unanswered subscribe message for this destination and cmd and return that msgCounter
   156  	hash := c.hashForMessage(destinationAddress, cmd)
   157  	if len(hash) > 0 {
   158  		if msgCounterCache := c.msgCounterForHashFromCache(hash); msgCounterCache != nil {
   159  			return msgCounterCache, nil
   160  		}
   161  	}
   162  
   163  	msgCounter := c.getMsgCounter()
   164  
   165  	datagram := model.DatagramType{
   166  		Header: model.HeaderType{
   167  			SpecificationVersion: &SpecificationVersion,
   168  			AddressSource:        senderAddress,
   169  			AddressDestination:   destinationAddress,
   170  			MsgCounter:           msgCounter,
   171  			CmdClassifier:        &cmdClassifier,
   172  		},
   173  		Payload: model.PayloadType{
   174  			Cmd: cmd,
   175  		},
   176  	}
   177  
   178  	if ackRequest {
   179  		datagram.Header.AckRequest = &ackRequest
   180  	}
   181  
   182  	err := c.sendSpineMessage(datagram)
   183  	if err == nil {
   184  		if len(hash) > 0 {
   185  			c.addMsgCounterHashToCache(*msgCounter, hash)
   186  		}
   187  	}
   188  
   189  	return msgCounter, err
   190  }
   191  
   192  func (c *Sender) ResultSuccess(requestHeader *model.HeaderType, senderAddress *model.FeatureAddressType) error {
   193  	return c.result(requestHeader, senderAddress, nil)
   194  }
   195  
   196  func (c *Sender) ResultError(requestHeader *model.HeaderType, senderAddress *model.FeatureAddressType, err *model.ErrorType) error {
   197  	return c.result(requestHeader, senderAddress, err)
   198  }
   199  
   200  // sends a result for a request
   201  func (c *Sender) result(requestHeader *model.HeaderType, senderAddress *model.FeatureAddressType, err *model.ErrorType) error {
   202  	cmdClassifier := model.CmdClassifierTypeResult
   203  
   204  	addressSource := *requestHeader.AddressDestination
   205  	addressSource.Device = senderAddress.Device
   206  
   207  	var resultData model.ResultDataType
   208  	if err != nil {
   209  		resultData = model.ResultDataType{
   210  			ErrorNumber: &err.ErrorNumber,
   211  			Description: err.Description,
   212  		}
   213  	} else {
   214  		resultData = model.ResultDataType{
   215  			ErrorNumber: util.Ptr(model.ErrorNumberTypeNoError),
   216  		}
   217  	}
   218  
   219  	cmd := model.CmdType{
   220  		ResultData: &resultData,
   221  	}
   222  
   223  	datagram := model.DatagramType{
   224  		Header: model.HeaderType{
   225  			SpecificationVersion: &SpecificationVersion,
   226  			AddressSource:        &addressSource,
   227  			AddressDestination:   requestHeader.AddressSource,
   228  			MsgCounter:           c.getMsgCounter(),
   229  			MsgCounterReference:  requestHeader.MsgCounter,
   230  			CmdClassifier:        &cmdClassifier,
   231  		},
   232  		Payload: model.PayloadType{
   233  			Cmd: []model.CmdType{cmd},
   234  		},
   235  	}
   236  
   237  	return c.sendSpineMessage(datagram)
   238  }
   239  
   240  // Reply sends reply to original sender
   241  func (c *Sender) Reply(requestHeader *model.HeaderType, senderAddress *model.FeatureAddressType, cmd model.CmdType) error {
   242  	cmdClassifier := model.CmdClassifierTypeReply
   243  
   244  	addressSource := *requestHeader.AddressDestination
   245  	addressSource.Device = senderAddress.Device
   246  
   247  	datagram := model.DatagramType{
   248  		Header: model.HeaderType{
   249  			SpecificationVersion: &SpecificationVersion,
   250  			AddressSource:        &addressSource,
   251  			AddressDestination:   requestHeader.AddressSource,
   252  			MsgCounter:           c.getMsgCounter(),
   253  			MsgCounterReference:  requestHeader.MsgCounter,
   254  			CmdClassifier:        &cmdClassifier,
   255  		},
   256  		Payload: model.PayloadType{
   257  			Cmd: []model.CmdType{cmd},
   258  		},
   259  	}
   260  
   261  	return c.sendSpineMessage(datagram)
   262  }
   263  
   264  // Notify sends notification to destination
   265  func (c *Sender) Notify(senderAddress, destinationAddress *model.FeatureAddressType, cmd model.CmdType) (*model.MsgCounterType, error) {
   266  	msgCounter := c.getMsgCounter()
   267  
   268  	cmdClassifier := model.CmdClassifierTypeNotify
   269  
   270  	datagram := model.DatagramType{
   271  		Header: model.HeaderType{
   272  			SpecificationVersion: &SpecificationVersion,
   273  			AddressSource:        senderAddress,
   274  			AddressDestination:   destinationAddress,
   275  			MsgCounter:           msgCounter,
   276  			CmdClassifier:        &cmdClassifier,
   277  		},
   278  		Payload: model.PayloadType{
   279  			Cmd: []model.CmdType{cmd},
   280  		},
   281  	}
   282  
   283  	c.muxNotifyCache.Lock()
   284  	c.datagramNotifyCache.Put(*msgCounter, datagram)
   285  	c.muxNotifyCache.Unlock()
   286  
   287  	return msgCounter, c.sendSpineMessage(datagram)
   288  }
   289  
   290  // Write sends notification to destination
   291  func (c *Sender) Write(senderAddress, destinationAddress *model.FeatureAddressType, cmd model.CmdType) (*model.MsgCounterType, error) {
   292  	msgCounter := c.getMsgCounter()
   293  
   294  	cmdClassifier := model.CmdClassifierTypeWrite
   295  	ackRequest := true
   296  
   297  	datagram := model.DatagramType{
   298  		Header: model.HeaderType{
   299  			SpecificationVersion: &SpecificationVersion,
   300  			AddressSource:        senderAddress,
   301  			AddressDestination:   destinationAddress,
   302  			MsgCounter:           msgCounter,
   303  			CmdClassifier:        &cmdClassifier,
   304  			AckRequest:           &ackRequest,
   305  		},
   306  		Payload: model.PayloadType{
   307  			Cmd: []model.CmdType{cmd},
   308  		},
   309  	}
   310  
   311  	return msgCounter, c.sendSpineMessage(datagram)
   312  }
   313  
   314  // Send a subscription request to a remote server feature
   315  func (c *Sender) Subscribe(senderAddress, destinationAddress *model.FeatureAddressType, serverFeatureType model.FeatureTypeType) (*model.MsgCounterType, error) {
   316  	cmd := model.CmdType{
   317  		NodeManagementSubscriptionRequestCall: NewNodeManagementSubscriptionRequestCallType(senderAddress, destinationAddress, serverFeatureType),
   318  	}
   319  
   320  	// we always send it to the remote NodeManagement feature, which always is at entity:[0],feature:0
   321  	localAddress := NodeManagementAddress(senderAddress.Device)
   322  	remoteAddress := NodeManagementAddress(destinationAddress.Device)
   323  
   324  	return c.Request(model.CmdClassifierTypeCall, localAddress, remoteAddress, true, []model.CmdType{cmd})
   325  }
   326  
   327  // Send a subscription deletion request to a remote server feature
   328  func (c *Sender) Unsubscribe(senderAddress, destinationAddress *model.FeatureAddressType) (*model.MsgCounterType, error) {
   329  	cmd := model.CmdType{
   330  		NodeManagementSubscriptionDeleteCall: NewNodeManagementSubscriptionDeleteCallType(senderAddress, destinationAddress),
   331  	}
   332  
   333  	// we always send it to the remote NodeManagement feature, which always is at entity:[0],feature:0
   334  	localAddress := NodeManagementAddress(senderAddress.Device)
   335  	remoteAddress := NodeManagementAddress(destinationAddress.Device)
   336  
   337  	return c.Request(model.CmdClassifierTypeCall, localAddress, remoteAddress, true, []model.CmdType{cmd})
   338  }
   339  
   340  // Send a binding request to a remote server feature
   341  func (c *Sender) Bind(senderAddress, destinationAddress *model.FeatureAddressType, serverFeatureType model.FeatureTypeType) (*model.MsgCounterType, error) {
   342  	cmd := model.CmdType{
   343  		NodeManagementBindingRequestCall: NewNodeManagementBindingRequestCallType(senderAddress, destinationAddress, serverFeatureType),
   344  	}
   345  
   346  	// we always send it to the remote NodeManagement feature, which always is at entity:[0],feature:0
   347  	localAddress := NodeManagementAddress(senderAddress.Device)
   348  	remoteAddress := NodeManagementAddress(destinationAddress.Device)
   349  
   350  	return c.Request(model.CmdClassifierTypeCall, localAddress, remoteAddress, true, []model.CmdType{cmd})
   351  }
   352  
   353  // Send a binding request to a remote server feature
   354  func (c *Sender) Unbind(senderAddress, destinationAddress *model.FeatureAddressType) (*model.MsgCounterType, error) {
   355  	cmd := model.CmdType{
   356  		NodeManagementBindingDeleteCall: NewNodeManagementBindingDeleteCallType(senderAddress, destinationAddress),
   357  	}
   358  
   359  	// we always send it to the remote NodeManagement feature, which always is at entity:[0],feature:0
   360  	localAddress := NodeManagementAddress(senderAddress.Device)
   361  	remoteAddress := NodeManagementAddress(destinationAddress.Device)
   362  
   363  	return c.Request(model.CmdClassifierTypeCall, localAddress, remoteAddress, true, []model.CmdType{cmd})
   364  }
   365  
   366  func (c *Sender) getMsgCounter() *model.MsgCounterType {
   367  	// TODO:  persistence
   368  	i := model.MsgCounterType(atomic.AddUint64(&c.msgNum, 1))
   369  	return &i
   370  }