github.com/enbility/spine-go@v0.7.0/spine/heartbeat_manager.go (about) 1 package spine 2 3 import ( 4 "sync" 5 "sync/atomic" 6 "time" 7 8 "github.com/enbility/spine-go/api" 9 "github.com/enbility/spine-go/model" 10 ) 11 12 type HeartbeatManager struct { 13 localEntity api.EntityLocalInterface 14 localFeature api.FeatureLocalInterface 15 16 heartBeatNum uint64 // see https://github.com/golang/go/issues/11891 17 stopHeartbeatC chan struct{} 18 stopMux sync.Mutex 19 20 heartBeatTimeout *model.DurationType 21 22 mux sync.Mutex 23 } 24 25 var _ api.HeartbeatManagerInterface = (*HeartbeatManager)(nil) 26 27 // Create a new Heartbeat Manager which handles sending of heartbeats 28 func NewHeartbeatManager(localEntity api.EntityLocalInterface, timeout time.Duration) *HeartbeatManager { 29 h := &HeartbeatManager{ 30 localEntity: localEntity, 31 heartBeatTimeout: model.NewDurationType(timeout), 32 } 33 34 return h 35 } 36 37 func (c *HeartbeatManager) IsHeartbeatRunning() bool { 38 c.stopMux.Lock() 39 defer c.stopMux.Unlock() 40 41 if c.stopHeartbeatC != nil && !c.isHeartbeatClosed() { 42 return true 43 } 44 45 return false 46 } 47 48 func (c *HeartbeatManager) SetLocalFeature(entity api.EntityLocalInterface, feature api.FeatureLocalInterface) { 49 if entity == nil || feature == nil { 50 return 51 } 52 53 if feature.Type() != model.FeatureTypeTypeDeviceDiagnosis || 54 feature.Role() != model.RoleTypeServer { 55 return 56 } 57 58 // check if the local device diagnosis server feature, supports the heartbeat function 59 ops, ok := feature.Operations()[model.FunctionTypeDeviceDiagnosisHeartbeatData] 60 if !ok || !ops.Read() { 61 return 62 } 63 64 c.mux.Lock() 65 66 c.localEntity = entity 67 c.localFeature = feature 68 69 // initialise heartbeat data 70 heartbeatData := c.heartbeatData(time.Now().UTC(), c.heartBeatCounter()) 71 72 // updating the data will automatically notify all subscribed remote features 73 feature.SetData(model.FunctionTypeDeviceDiagnosisHeartbeatData, heartbeatData) 74 75 c.mux.Unlock() 76 77 // start creating heartbeats 78 _ = c.StartHeartbeat() 79 } 80 81 // Start setting heartbeat data 82 // Make sure the a required FeatureTypeTypeDeviceDiagnosis with the role server is present 83 // otherwise this will end with an error 84 // Note: Remote features need to have a subscription to get notifications 85 func (c *HeartbeatManager) StartHeartbeat() error { 86 timeout, err := c.heartBeatTimeout.GetTimeDuration() 87 if err != nil { 88 return err 89 } 90 91 // stop an already running heartbeat 92 c.StopHeartbeat() 93 94 c.stopHeartbeatC = make(chan struct{}) 95 96 go c.updateHeartbeatData(c.stopHeartbeatC, timeout) 97 98 return nil 99 } 100 101 // Stop updating heartbeat data 102 // Note: No active subscribers will get any further notifications! 103 func (c *HeartbeatManager) StopHeartbeat() { 104 if c.IsHeartbeatRunning() { 105 close(c.stopHeartbeatC) 106 } 107 } 108 109 func (c *HeartbeatManager) heartbeatData(t time.Time, counter *uint64) *model.DeviceDiagnosisHeartbeatDataType { 110 timestamp := model.NewAbsoluteOrRelativeTimeTypeFromTime(t) 111 112 return &model.DeviceDiagnosisHeartbeatDataType{ 113 Timestamp: timestamp, 114 HeartbeatCounter: counter, 115 HeartbeatTimeout: c.heartBeatTimeout, 116 } 117 } 118 119 func (c *HeartbeatManager) updateHeartbeatData(stopC chan struct{}, d time.Duration) { 120 // Substract two seconds, because some devices (like Elli Connect/Pro) with OPEV/OSCEV interpret 121 // the heartbeat timeout (<= 4s) as the time within which a heartbeat should be received and otherwise 122 // will go into fallback mode. 123 // But other EVSE devices and in LPC (<= 60s), the heartbeat should be considered missing, if it is not 124 // received within twice the heartbeat timeout timeframe. 125 if d > 2*time.Second { 126 d -= 2 * time.Second 127 } 128 ticker := time.NewTicker(d) 129 for { 130 select { 131 case <-ticker.C: 132 133 heartbeatData := c.heartbeatData(time.Now().UTC(), c.heartBeatCounter()) 134 135 c.mux.Lock() 136 // updating the data will automatically notify all subscribed remote features 137 c.localFeature.SetData(model.FunctionTypeDeviceDiagnosisHeartbeatData, heartbeatData) 138 c.mux.Unlock() 139 140 case <-stopC: 141 return 142 } 143 } 144 } 145 146 func (c *HeartbeatManager) isHeartbeatClosed() bool { 147 select { 148 case <-c.stopHeartbeatC: 149 return true 150 default: 151 } 152 153 return false 154 } 155 156 // TODO heartBeatCounter should be global on CEM level, not on connection level 157 func (c *HeartbeatManager) heartBeatCounter() *uint64 { 158 i := atomic.AddUint64(&c.heartBeatNum, 1) 159 return &i 160 }