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 }