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 } } }