src.dualinventive.com/go/cp3000-interface/internal/device/zkl3000rc.go

749 lines
24 KiB
Go

package device
import (
"errors"
"fmt"
"time"
"src.dualinventive.com/go/cp3000-interface/internal/statusupdate"
"src.dualinventive.com/go/dinet/rpc"
"src.dualinventive.com/go/lib/cp3000"
"src.dualinventive.com/go/lib/dilog"
)
// zkl3000RcMaximumWaitForMeasurement is the maximum time to wait until a measurement should arrive
//nolint: gochecknoglobals
var zkl3000RcMaximumWaitForMeasurement = 3 * time.Second
// errZKL3000RcSwitchStatsRetriesExceeded when the switch status is not valid after retries
var errZKL3000RcSwitchStatsRetriesExceeded = errors.New("zkl3000rc: switch status retries exceeded")
// SwitchCommand is a command to request a switch change.
// The algorithm to calculate the right value is defined in: mtinfo/common: include/zkl-common.h:902
type SwitchCommand byte
const (
// SwitchStatusCommand (0xF0)
SwitchStatusCommand SwitchCommand = ((iota) | ((^(iota) & 0xF) << 4))
//ShortCircuitOffSwitchCommand turns the short circuit off (0xE1)
ShortCircuitOffSwitchCommand
//ShortCircuitOnSwitchCommand turns the short circuit on (0xD2)
ShortCircuitOnSwitchCommand
//MeasurementOffSwitchCommand turns the measurement off (0xC3)
MeasurementOffSwitchCommand
//MeasurementOnSwitchCommand turns the measurement on (0xB4)
MeasurementOnSwitchCommand
//RemoveTokenSwitchCommand removes the token in the switch (0xA5)
RemoveTokenSwitchCommand
//WriteTokenSwichCommand writes the token in the switch (0x96)
WriteTokenSwichCommand
)
// Zkl3000Rc is a ZKL 3000 RC device
type Zkl3000Rc struct {
Zkl3000
switchInfo *SwitchInfo
active bool
}
// construct is called when the device is created. This should be non blocking
func (z *Zkl3000Rc) construct() {
z.decoder = statusupdate.NewStatusDecoder(z.dev.Logger(), statusupdate.StatUpdateAll)
z.dev.Subscribe(rpc.ClassMethodSensorInfo, rpc.MsgTypeRequest, z.sensorInfo)
z.dev.AddConfig(rpc.ConfigToken, z.configGetToken, z.configSetToken, z.configResetToken)
z.dev.AddConfig(rpc.ConfigActive, z.configGetActivate, z.configSetActivate, z.configResetActivate)
z.dev.AddConfig(rpc.ConfigEndpoint, z.configGetEndpoint, z.configSetEndpoint, nil)
}
// init is called when the device is created. This can be blocking
func (z *Zkl3000Rc) init() {
if _, _, err := z.requestSwitchInfo(); err != nil {
z.dev.Logger().WithError(err).Warning("problem reading token")
}
}
// Below are all the CP3000 callbacks
// Statusupdate is a callback for the $STAT message
// Suppress the linter because statusUpdateCmd needs to satisfy the interface
//nolint: unparam
func (z *Zkl3000Rc) statusUpdateCmd(r cp3000.Router, args []string) error {
err := z.updateSensorData(args)
if z.updateErrors() {
z.dev.Logger().Info("send device:data")
if pErr := z.dev.SendDeviceData(); pErr != nil {
return pErr
}
}
return err
}
// heartbeatCmd is the Watchdog callback (used as heartbeat)
// Suppress the linter because heartbeatCmd needs to satisfy the interface
//nolint: unparam
func (z *Zkl3000Rc) heartbeatCmd(r cp3000.Router, args []string) error {
if err := z.dev.SendDeviceData(); err != nil {
return err
}
if z.dev.GetState() == rpc.DeviceStateActive {
go z.continueHeartbeats()
}
return z.client.Send("WD")
}
// RegisterCallbacks overrides the heartbeat command
func (z *Zkl3000Rc) RegisterCallbacks() error {
err := z.Zkl3000.RegisterCallbacks()
z.client.Register(cp3000.CommandWatchdog, z.heartbeatCmd)
z.client.Register(cp3000.CommandStatus, z.statusUpdateCmd)
return err
}
// Below are all the DI Net callbacks
// sensorInfo returns all the sensor information
func (z *Zkl3000Rc) sensorInfo(req *rpc.Msg) *rpc.Msg {
rep := req.CreateReply()
// dupl with the test, so suppress it
// nolint: dupl
rep.Result = rpc.NewResult([]rpc.ResultInfoItem{
rpc.SensorBattery1Voltage.Info(),
rpc.SensorBattery1State.Info(),
rpc.SensorBattery2Voltage.Info(),
rpc.SensorBattery2State.Info(),
rpc.SensorCharger1State.Info(),
rpc.SensorGPS.Info(),
rpc.SensorRSSI.Info(),
rpc.SensorBER.Info(),
rpc.ZKLSensorDetectionQuality.Info(),
rpc.ZKLSensorDetection.Info(),
rpc.ZKLSensorMeasurement.Info(),
rpc.ZKLSensorBa.Info(),
rpc.ZKLSensorFrequency.Info(),
rpc.ZKLSensorRMS.Info(),
rpc.ZKLSensorBAAutocal.Info(),
rpc.ZKLSensorTempOnboard.Info(),
rpc.ZKLSensorTempNTC.Info(),
rpc.ZKLRCSensorSectionsShort.Info(),
rpc.ZKLRCSensorSectionsBattery.Info(),
rpc.ZKLRCSensorSwitchShort.Info(),
rpc.ZKLRCSensorKeySwitch.Info(),
})
return rep
}
// configSetToken request is called when the project is released
func (z *Zkl3000Rc) configSetToken(p rpc.ConfigParam) rpc.ErrorCode {
logger := z.dev.Logger().WithField("action", "config:set token")
val, err := p.ValueInt64()
if err != nil {
logger.WithError(err).WithField("type", fmt.Sprintf("%T", p.Value)).Error("token is not an int64")
return rpc.EParam
}
newToken := uint32(val)
up, switchInfo, retVal := z.preflightChecks(logger, true, true)
if retVal != rpc.Ok {
return retVal
}
// When there is already a different token, deny operation
if switchInfo.Released && switchInfo.Token != newToken {
logger.WithFields(dilog.Fields{
"currenttoken": switchInfo.Token,
"newtoken": newToken,
}).Error("token mismatch")
return rpc.EFirmwareMismatchToken
}
if up.KeySwitch() != rpc.KeyswitchStateOperational {
logger.WithField("keyswitch", up.KeySwitch()).Error("keyswitch is not operational")
return rpc.EFirmwareKeyswitchNotOperational
}
retVal = z.batteryChecks(logger, up, false)
if retVal.IsError() {
return retVal
}
// Always at the end, because you cannot for example release with an invalid keyswitch even if the device is
// already released
if switchInfo.Released {
if up.Released() {
logger.Info("device is already released")
return rpc.Ok
}
logger.Warning("device already released, but STAT data say otherwise. DEADLOCK")
}
// It is possible that the token was already written, but the status update is still reporting not released
// We are in a deadlock! Just write it again to the switch and hopefully it is fixed.
return z.writeSwitchToken(retVal, newToken)
}
// configSetActivate request is called when the project is activated
func (z *Zkl3000Rc) configSetActivate(p rpc.ConfigParam) rpc.ErrorCode {
logger := z.dev.Logger().WithFields(dilog.Fields{
"action": "config:set activate",
"value": p.Value,
})
activationState, ok := p.Value.(bool)
if !ok {
logger.WithField("type", fmt.Sprintf("%T", p.Value)).Error("activate is not a bool")
return rpc.EParam
}
return z.activateDeactivateSwitch(logger, activationState)
}
// configResetToken request is called when the project is returned
func (z *Zkl3000Rc) configResetToken() rpc.ErrorCode {
logger := z.dev.Logger().WithField("action", "config:reset token")
_, switchInfo, retVal := z.preflightChecks(logger, false, false)
if retVal != rpc.Ok {
return retVal
}
// When there is no token it is already returned
if !switchInfo.Released {
logger.Info("device is already returned")
return rpc.Ok
}
return z.writeSwitchRemoveToken(retVal, switchInfo.Token)
}
// configResetActivate request is called when the project is deactivated
func (z *Zkl3000Rc) configResetActivate() rpc.ErrorCode {
logger := z.dev.Logger().WithField("action", "config:reset activate")
return z.activateDeactivateSwitch(logger, false)
}
// configGetToken request is called when the backend wants to know what the token is
func (z *Zkl3000Rc) configGetToken(rep *rpc.Msg) *rpc.Msg {
logger := z.dev.Logger().WithField("action", "config:get token")
switchInfo, rpcerr, err := z.requestSwitchInfo()
if err != nil {
logger.WithError(err).Error("cannot read switch information")
return rep.SetError(rpcerr)
}
rep.Result = rpc.NewResult([]*rpc.ResultConfigValueItem{rpc.ConfigToken.Data(switchInfo.Token)})
return rep
}
// configGetActivate is called when the backend wants to know if the project is active
func (z *Zkl3000Rc) configGetActivate(rep *rpc.Msg) *rpc.Msg {
rep.Result = rpc.NewResult([]*rpc.ResultConfigValueItem{rpc.ConfigActive.Data(z.active)})
return rep
}
// Below are all the child device callbacks
// Versions returns the firmware and hardware versions
func (z *Zkl3000Rc) Versions() (rpc.VersionMap, rpc.ErrorCode) {
var mcu, wcpu, fwSwitchMeas, fwSwitchDrive string
var rpcerr rpc.ErrorCode
if mcu, rpcerr = z.requestVersion("FW-MCU"); rpcerr != rpc.Ok {
return nil, rpcerr
}
if wcpu, rpcerr = z.requestVersion("FW-WCPU"); rpcerr != rpc.Ok {
return nil, rpcerr
}
if fwSwitchMeas, rpcerr = z.requestVersion("FW-SW3000-M"); rpcerr != rpc.Ok {
return nil, rpcerr
}
if fwSwitchDrive, rpcerr = z.requestVersion("FW-SW3000-D"); rpcerr != rpc.Ok {
return nil, rpcerr
}
// Legacy Zkl3000RC don't have "VersionKeyFwZKLRCSwitchConrol" and "rpc.VersionKeyHwZKLRCSwitch"
return rpc.VersionMap{
rpc.VersionKeyHwZKLRCMain: z.hwVersion,
rpc.VersionKeyFwZKLRCMain: mcu,
rpc.VersionKeyFwZKLRCWcpu: wcpu,
rpc.VersionKeyFwZKLRCSwitchMeas: fwSwitchMeas,
rpc.VersionKeyFwZKLRCSwitchDrive: fwSwitchDrive,
}, rpc.Ok
}
// TimeoutDuration returns when the device should be considered offline
func (z *Zkl3000Rc) TimeoutDuration() time.Duration {
return time.Minute
}
// Transport returns the transportation type
func (z *Zkl3000Rc) Transport() (interface{}, rpc.ErrorCode) {
return z.cp3000Transport()
}
// updateDeviceState is called when the device state needs to be updated
func (z *Zkl3000Rc) updateDeviceState() bool {
curState := z.dev.GetState()
switch {
case !z.switchInfo.Released:
z.dev.SetState(rpc.DeviceStateIdle)
case z.active:
z.dev.SetState(rpc.DeviceStateActive)
default:
z.dev.SetState(rpc.DeviceStateArmed)
}
return curState != z.dev.GetState()
}
// batteryStateError returns the error code when the device needs to be released or activated.
// When activation is true, the code expects that the device should be activated, false means release.
func (z *Zkl3000Rc) batteryStateError(state rpc.BatteryState, activation bool) rpc.ErrorCode {
switch state {
case rpc.BatteryStateFull:
// Valid
return rpc.Ok
case rpc.BatteryStateHalf:
// Should not occur set it on valid
return rpc.Ok
case rpc.BatteryStateLow:
// Should not occur set it on valid
return rpc.Ok
case rpc.BatteryStateCritical:
// Warning
return rpc.EWrnFirmwareBattery2Critical
case rpc.BatteryStateEmpty:
if activation {
// Warning
return rpc.EWrnFirmwareBattery2Empty
}
// Error
return rpc.EFirmwareBattery2Empty
case rpc.BatteryStateRemoved:
if activation {
// Warning
return rpc.EWrnFirmwareBattery2Removed
}
return rpc.EFirmwareBattery2Removed
default:
// Unknown
return rpc.EOpdenied
}
}
// batteryChecks checks if the battery levels are correct
func (z *Zkl3000Rc) batteryChecks(logger dilog.Logger, up *statusupdate.StatusUpdate, activation bool) rpc.ErrorCode {
batteryError := z.batteryStateError(up.Battery2State(), activation)
if batteryError.IsError() {
logger.WithFields(dilog.Fields{
"state": up.Battery2State(),
"battery": 2,
}).Error("device battery")
}
// the warning is the new default return error
if batteryError.IsWarning() {
logger.WithFields(dilog.Fields{
"state": up.Battery2State(),
"battery": 2,
}).Warning("device battery")
}
return batteryError
}
func (z *Zkl3000Rc) preflightChecks(logger dilog.Logger,
checkKeyswitch bool, checkDeviceErrors bool) (*statusupdate.StatusUpdate, *SwitchInfo, rpc.ErrorCode) {
// Always ask for the newest STAT value
up, retVal := z.requestNewSensorData()
if retVal != rpc.Ok {
logger.Error("cannot request new sensor data")
return nil, nil, retVal
}
// Always ask for the newest switch information
switchInfo, retVal, err := z.requestSwitchInfo()
if err != nil {
logger.WithError(err).Error("cannot read switch information")
return nil, nil, retVal
}
// Start the preflight checks
if up.ErrMCU() {
// Should never occur. Just log
logger.Warning("internal device error")
}
if up.ErrSecureHandshake() {
logger.Warning("secure handshake failed")
}
if checkDeviceErrors && z.dev.HasError() {
logger.Error("device contains errors")
return nil, nil, rpc.EFirmwareDeviceContainsErrors
}
if z.dev.GetState() == rpc.DeviceStateService {
logger.Error("device is in service")
return nil, nil, rpc.EFirmwareDeviceService
}
if checkKeyswitch && up.KeySwitch() != rpc.KeyswitchStateOperational {
logger.WithField("keyswitch", up.KeySwitch()).Error("keyswitch is not operational")
return nil, nil, rpc.EFirmwareKeyswitchNotOperational
}
return up, switchInfo, rpc.Ok
}
// StatusUpdatePredicate is called in the function waitForValidSensorData to check if the status update is valid
type StatusUpdatePredicate func(up *statusupdate.StatusUpdate) bool
// waitForValidSensorData requests the latest STAT data and waits until the given predicate is valid or when there is a
// timeout. When there is a timeout, rpc.EFirmwareNoMeasurement is returned
func (z *Zkl3000Rc) waitForValidStatusUpdate(f StatusUpdatePredicate) (*statusupdate.StatusUpdate, rpc.ErrorCode) {
var lastSensorData *statusupdate.StatusUpdate
timer := time.NewTimer(zkl3000RcMaximumWaitForMeasurement)
defer timer.Stop()
for {
select {
case <-timer.C:
z.dev.Logger().Error("didn't received a measurement in time")
return lastSensorData, rpc.Ok
default:
sensorData, rpcErr := z.requestNewSensorData()
if rpcErr != rpc.Ok {
return nil, rpcErr
}
if f(sensorData) {
return sensorData, rpc.Ok
}
lastSensorData = sensorData
}
}
}
// requestNewSensorData requests the latest sensor data
func (z *Zkl3000Rc) requestNewSensorData() (*statusupdate.StatusUpdate, rpc.ErrorCode) {
value, dierr := z.requestValue("STAT")
if dierr != rpc.Ok {
return nil, dierr
}
// Fire the callback, because the router doesn't fire reply messages
// We drop the errors, because this is an error when we cannot write to the SMP which is not imported for
// requesting the sensor data
err := z.statusUpdateCmd(nil, []string{"", value})
_ = err
// Grab the current status update of the zkl3000RC
return z.decoder.StatusUpdate(), rpc.Ok
}
// writeSwitchToken writes the token in the switch. It also checks if the write is successful
func (z *Zkl3000Rc) writeSwitchToken(currErr rpc.ErrorCode, token uint32) rpc.ErrorCode {
if currErr.IsError() {
// There was already an error. Don't proceed
return currErr
}
switchInfo, rpcErr := z.writeSwitch(token, WriteTokenSwichCommand)
if rpcErr != rpc.Ok {
z.dev.Logger().Error("communication to switch failed")
return rpcErr
}
if !switchInfo.Released {
z.dev.Logger().Error("token is written, but device replied with no token")
return rpc.EBackendCp3000CommError
}
if switchInfo.Token != token {
z.dev.Logger().WithField("token", switchInfo.Token).
Error("token is written, but the device replied with a wrong token")
return rpc.EBackendCp3000CommError
}
lastSensorData, retVal := z.waitForValidStatusUpdate(func(up *statusupdate.StatusUpdate) bool {
// stop when the device is reporting released
return up.Released()
})
if retVal != rpc.Ok {
return retVal
}
switch {
case lastSensorData.ErrSwitch3000():
z.dev.Logger().Error("switch error")
return rpc.EFirmwareReleaseSwitchComm
case !lastSensorData.Released():
z.dev.Logger().Warning("STAT data says device is not released. DEADLOCK")
}
return currErr
}
// writeSwitchRemoveToken removes the token from the switch. It also checks if the removal is successful
func (z *Zkl3000Rc) writeSwitchRemoveToken(currErr rpc.ErrorCode, token uint32) rpc.ErrorCode {
if currErr.IsError() {
// There was already an error. Don't proceed
return currErr
}
switchInfo, rpcErr := z.writeSwitch(token, RemoveTokenSwitchCommand)
if rpcErr != rpc.Ok {
z.dev.Logger().Error("communication to switch failed")
return rpcErr
}
if switchInfo.Released {
z.dev.Logger().Error("token in switch info is not removed")
return rpc.EBackendCp3000CommError
}
lastSensorData, retVal := z.waitForValidStatusUpdate(func(up *statusupdate.StatusUpdate) bool {
// stop when the device is reporting not released
return !up.Released()
})
if retVal != rpc.Ok {
return retVal
}
if lastSensorData.Released() {
z.dev.Logger().Warning("STAT data is still released. DEADLOCK")
}
return currErr
}
// activateDeactivateSwitch activates of deactivates the short circuit. It also checks if the change is successful.
func (z *Zkl3000Rc) activateDeactivateSwitch(logger dilog.Logger, enable bool) rpc.ErrorCode {
up, switchInfo, retVal := z.preflightChecks(logger, true, enable)
if retVal != rpc.Ok {
return retVal
}
// When there is no token, deny the transaction
if !switchInfo.Released {
logger.Error("switch info has no token")
return rpc.EFirmwareDeviceIdle
}
// Only do battery checks during activate
if enable {
retVal = z.batteryChecks(logger, up, true)
if retVal.IsError() {
return retVal
}
}
if !up.Released() {
// If this occur we have a serious problem. We are in a deadlock, so just ignore it...
logger.Warning("STAT data is not released. DEADLOCK")
}
if up.SwitchShort() && enable {
logger.WithField("status", up.SwitchShort).Info("switch already changed")
}
if enable {
// Turn short circuit on
retVal = z.turnMeasurementOn(retVal, switchInfo.Token)
retVal = z.turnSwitchOn(retVal, switchInfo.Token)
} else {
// Turn short circuit off
retVal = z.turnSwitchOff(retVal, switchInfo.Token)
retVal = z.turnMeasurementOff(retVal, switchInfo.Token)
}
if !retVal.IsError() {
z.active = enable
// possible transition from armed <--> active
z.updateDeviceState()
}
return retVal
}
// turnMeasurementOn turns the measurement on. It also checks if the measurement is indeed on.
func (z *Zkl3000Rc) turnMeasurementOn(currErr rpc.ErrorCode, token uint32) rpc.ErrorCode {
if currErr.IsError() {
// There was already an error. Don't proceed
return currErr
}
if _, rpcErr := z.writeSwitch(token, MeasurementOnSwitchCommand); rpcErr != rpc.Ok {
z.dev.Logger().Error("communication to switch failed")
return rpcErr
}
lastSensorData, retVal := z.waitForValidStatusUpdate(func(up *statusupdate.StatusUpdate) bool {
// stop when the measurement is indeed on or when the BA-value is not 0
return up.Measurement() || up.Ba() != 0
})
if retVal != rpc.Ok {
return retVal
}
if !lastSensorData.SwitchShort() && lastSensorData.DetectionStatus() {
z.dev.Logger().Warning("short already presented while activating")
return rpc.EWrnFirmwareActivateShortPresent
}
return currErr
}
// turnSwitchOn turns the short circuit on. It also checks if the short circuit is indeed on.
func (z *Zkl3000Rc) turnSwitchOn(currErr rpc.ErrorCode, token uint32) rpc.ErrorCode {
if currErr.IsError() {
// There was already an error. Don't proceed
return currErr
}
if _, rpcErr := z.writeSwitch(token, ShortCircuitOnSwitchCommand); rpcErr != rpc.Ok {
z.dev.Logger().Error("communication to switch failed")
return rpcErr
}
lastSensorData, retVal := z.waitForValidStatusUpdate(func(up *statusupdate.StatusUpdate) bool {
// stop when the short is on and we have a valid detection
return up.SwitchShort() && up.DetectionStatus()
})
if retVal != rpc.Ok {
return retVal
}
switch {
case lastSensorData.ErrSwitch3000():
z.dev.Logger().Error("switch error")
return rpc.EFirmwareActivateSwitchComm
case !lastSensorData.SwitchShort():
z.dev.Logger().Error("switch short circuit is not on")
return rpc.EFirmwareActivateShortNotEnabled
case !lastSensorData.DetectionStatus():
z.dev.Logger().Warning("detection is not ok")
return rpc.EWrnFirmwareActivateDetectionNok
}
return currErr
}
// turnSwitchOn turns the short circuit off. It also checks if the short circuit is indeed off.
func (z *Zkl3000Rc) turnSwitchOff(currErr rpc.ErrorCode, token uint32) rpc.ErrorCode {
if currErr.IsError() {
// There was already an error. Don't proceed
return currErr
}
if _, rpcErr := z.writeSwitch(token, ShortCircuitOffSwitchCommand); rpcErr != rpc.Ok {
z.dev.Logger().Error("communication to switch failed")
return rpcErr
}
lastSensorData, retVal := z.waitForValidStatusUpdate(func(up *statusupdate.StatusUpdate) bool {
// stop when the short is off and we have an invalid detection
return !up.SwitchShort() && !up.DetectionStatus()
})
if retVal != rpc.Ok {
return retVal
}
switch {
case lastSensorData.ErrSwitch3000():
z.dev.Logger().Error("switch error")
return rpc.EFirmwareDeactivateSwitchComm
case lastSensorData.SwitchShort():
z.dev.Logger().Error("switch short circuit is on")
return rpc.EFirmwareDeactivateShortEnabled
case lastSensorData.DetectionStatus():
z.dev.Logger().Warning("short still presented while deactivating")
return rpc.EWrnFirmwareDeactivateShortPresent
}
return currErr
}
// turnMeasurementOff turns the measurement off. It also checks if the measurement is indeed off.
func (z *Zkl3000Rc) turnMeasurementOff(currErr rpc.ErrorCode, token uint32) rpc.ErrorCode {
if currErr.IsError() {
// There was already an error. Don't proceed
return currErr
}
if _, rpcErr := z.writeSwitch(token, MeasurementOffSwitchCommand); rpcErr != rpc.Ok {
z.dev.Logger().Error("communication to switch failed")
return rpcErr
}
return currErr
}
// writeSwitch writes the command to the switch. The returned switch information is just for verification and can be
// ignored
func (z *Zkl3000Rc) writeSwitch(token uint32, code SwitchCommand) (*SwitchInfo, rpc.ErrorCode) {
logger := z.dev.Logger().WithField("cmd", fmt.Sprintf("%X,%d", code, token))
logger.Debug("sending switch command to device")
// Write to the switch
if err := z.client.Send(cp3000.CommandStore, "SWITCH", fmt.Sprintf("%X,%d", code, token)); err != nil {
logger.Error("cannot write to switch")
return nil, rpc.EBackendCp3000CommError
}
// Check if we receive something to check the connection
switchInfo, rpcerr, err := z.requestSwitchInfo()
if err != nil {
logger.WithError(err).Error("cannot receive switch info")
return nil, rpcerr
}
return switchInfo, rpc.Ok
}
// requestSwitchInfo requests the latest switch information. This contains the device token and the status
func (z *Zkl3000Rc) requestSwitchInfo() (*SwitchInfo, rpc.ErrorCode, error) {
if err := z.client.Send(cp3000.CommandStore, "SWITCH", fmt.Sprintf("%X", SwitchStatusCommand)); err != nil {
z.dev.Logger().WithError(err).Error("cannot write SwitchStatusCommand to device during requestSwitchInfo")
return nil, rpc.EBackendCp3000CommError, err
}
var switchInfo *SwitchInfo
switchRequestRetries := 1
switchRetryAttempt := 0
for {
msg, rpcerr := z.cp3000ReqRep(DefaultCommandTimeout, cp3000.CommandRetrieve, "SWITCH")
if rpcerr != rpc.Ok {
return nil, rpcerr, errors.New("cannot request reply")
}
si, err := NewSwitchInfo(msg.Params)
if err != nil {
return nil, rpc.EIofailed, err
}
if si.Valid() {
switchInfo = si
break
}
// Always suppress the first retry because the ZKL 3000 RC SWCMD_STATUS takes a second try
// as the valid status is async refreshed, next call it is normally valid
if switchRetryAttempt > 0 {
z.dev.Logger().WithField("cp3000-reply", msg.String()).Warning(
"switch status retrying request, due to previous incorrect status")
}
// Switch malfunction is only retried once
if switchRequestRetries == 0 {
return nil, rpc.EBackendCp3000CommError, errZKL3000RcSwitchStatsRetriesExceeded
}
if si.Malfunction() {
z.dev.Logger().Warning("switch status is malfunction")
switchRequestRetries--
}
// Wait in an exponential backoff fashion
time.Sleep(DefaultBackoffPolicy.Duration(switchRetryAttempt))
switchRetryAttempt++
}
// Device is released/returned
if switchInfo.Changed(z.switchInfo) {
z.switchInfo = switchInfo
if switchInfo.Released {
z.dev.Logger().WithField("token", switchInfo.Token).Info("new token value")
} else {
z.dev.Logger().WithField("token", nil).Info("new token value")
}
// possible transition from idle <--> armed
if z.updateDeviceState() {
z.dev.Logger().Info("send device:data")
if err := z.dev.SendDeviceData(); err != nil {
return switchInfo, rpc.EBackendCp3000CommError, err
}
}
}
return switchInfo, rpc.Ok, nil
}
// continueHeartbeats continues to send heartbeats every 1s for 7s
func (z *Zkl3000Rc) continueHeartbeats() {
ticker := time.NewTicker(time.Second)
after := time.After(7 * time.Second)
for {
select {
case <-ticker.C:
if err := z.dev.SendDeviceData(); err != nil {
z.dev.Logger().WithError(err).Error("failed sending device:data")
}
case <-after:
// escape from the for-loop
return
}
}
}