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  }