package mk2driver import ( "errors" "fmt" "io" "math" "strings" "sync" "sync/atomic" "time" "github.com/sirupsen/logrus" ) var mk2log = logrus.WithField("ctx", "inverter-gui-mk2driver") type scaling struct { scale float64 offset float64 signed bool supported bool } //nolint:deadcode,varcheck const ( ramVarVMains = iota ramVarIMains ramVarVInverter ramVarIInverter ramVarVBat ramVarIBat ramVarVBatRipple ramVarInverterPeriod ramVarMainPeriod ramVarIACLoad ramVarVirSwitchPos ramVarIgnACInState ramVarMultiFuncRelay ramVarChargeState ramVarInverterPower1 ramVarInverterPower2 ramVarOutPower ramVarMaxOffset = 14 ) const ( infoFrameHeader = 0x20 frameHeader = 0xff bootupFrameHeader = 0x0 frameLengthLEDBit = 0x80 frameLEDBytes = 2 ) const ( acL1InfoFrame = 0x08 dcInfoFrame = 0x0C interfaceFrame = 0x48 // H setTargetFrame = 0x41 infoReqFrame = 0x46 //F ledFrame = 0x4C stateFrame = 0x53 // S vFrame = 0x56 winmonFrame = 0x57 ) const ( panelStateVariant2Flags = 0x80 interfacePanelDetectFlag = 0x01 interfaceStandbyFlag = 0x02 panelCurrentLimitUnknown = 0x8000 panelCurrentLimitMax = 0x7FFF ) // info frame types const ( infoReqAddrDC = 0x00 infoReqAddrACL1 = 0x01 ) // winmon frame commands const ( commandSetState = 0x0E commandReadRAMVar = 0x30 commandReadSetting = 0x31 commandWriteRAMVar = 0x32 commandWriteSetting = 0x33 commandWriteData = 0x34 commandReadSelected = 0x35 commandGetRAMVarInfo = 0x36 commandWriteViaID = 0x37 commandWriteRAMViaID = 0x38 commandUnsupportedResponse = 0x80 commandReadRAMResponse = 0x85 commandReadSettingResponse = 0x86 commandWriteRAMResponse = 0x87 commandWriteSettingResponse = 0x88 commandSetStateResponse = 0x89 commandReadSelectedResponse = 0x8A commandWriteViaIDResponse = 0x8B commandWriteRAMViaIDResponse = 0x8C commandGetRAMVarInfoResponse = 0x8E commandWriteNotAllowedResponse = 0x9B ) const writeResponseTimeout = 3 * time.Second var ( errCommandUnsupported = errors.New("command is not supported by this device firmware") errWriteRejected = errors.New("write command rejected by device access level") ) type winmonResponse struct { command byte data []byte } type mk2Ser struct { info *Mk2Info p io.ReadWriter scales []scaling scaleCount int run chan struct{} frameLock bool infochan chan *Mk2Info commandMu sync.Mutex pollPaused atomic.Bool writeAck chan byte winmonAck chan winmonResponse stateAck chan struct{} ifaceAck chan byte wg sync.WaitGroup diagMu sync.Mutex traceLimit int traces []ProtocolTrace lastFrameAt time.Time recentErrors []string commandTimeouts atomic.Uint64 commandFailures atomic.Uint64 checksumErrors atomic.Uint64 } var _ ProtocolControl = (*mk2Ser)(nil) var _ MetadataControl = (*mk2Ser)(nil) var _ SnapshotControl = (*mk2Ser)(nil) var _ DiagnosticsControl = (*mk2Ser)(nil) func parseFrameLength(raw byte) (payloadLength byte, hasLEDStatus bool) { if raw&frameLengthLEDBit != 0 { return raw &^ frameLengthLEDBit, true } return raw, false } func (m *mk2Ser) beginCommand() { m.commandMu.Lock() m.pollPaused.Store(true) } func (m *mk2Ser) endCommand() { m.pollPaused.Store(false) m.commandMu.Unlock() } func (m *mk2Ser) sendMonitoringCommand(data []byte) { if m.pollPaused.Load() { if len(data) > 0 { mk2log.WithField("command", fmt.Sprintf("0x%02x", data[0])).Debug("Skipping monitoring command during control transaction") } return } m.sendCommand(data) } func NewMk2Connection(dev io.ReadWriter) (Mk2, error) { mk2 := &mk2Ser{} mk2.p = dev mk2.info = &Mk2Info{} mk2.scaleCount = 0 mk2.frameLock = false mk2.scales = make([]scaling, 0, ramVarMaxOffset) mk2.writeAck = make(chan byte, 4) mk2.winmonAck = make(chan winmonResponse, 32) mk2.stateAck = make(chan struct{}, 1) mk2.ifaceAck = make(chan byte, 1) mk2.traceLimit = 200 mk2.traces = make([]ProtocolTrace, 0, mk2.traceLimit) mk2.recentErrors = make([]string, 0, 20) mk2.setTarget() mk2.run = make(chan struct{}) mk2.infochan = make(chan *Mk2Info) mk2.wg.Add(1) mk2log.Info("MK2 connection initialized") go mk2.frameLocker() return mk2, nil } // Locks to incoming frame. func (m *mk2Ser) frameLocker() { frame := make([]byte, 256) ledStatus := make([]byte, frameLEDBytes) var frameLengthRaw byte for { select { case <-m.run: m.wg.Done() return default: } if m.frameLock { frameLengthRaw = m.readByte() frameLength, hasLEDStatus := parseFrameLength(frameLengthRaw) frameLengthOffset := int(frameLength) + 1 if frameLengthOffset > len(frame) { m.addError(fmt.Errorf("Read Length Error: frame length %d exceeds buffer", frameLengthOffset)) m.frameLock = false continue } l, err := io.ReadFull(m.p, frame[:frameLengthOffset]) if err != nil { m.addError(fmt.Errorf("Read Error: %v", err)) m.frameLock = false } else if l != frameLengthOffset { m.addError(errors.New("Read Length Error")) m.frameLock = false } else { var appendedLED []byte if hasLEDStatus { if _, err = io.ReadFull(m.p, ledStatus); err != nil { m.addError(fmt.Errorf("Read LED status error: %v", err)) m.frameLock = false continue } appendedLED = ledStatus } m.handleFrame(frameLength, frame[:frameLengthOffset], appendedLED) } } else { tmp := m.readByte() frameLength, hasLEDStatus := parseFrameLength(frameLengthRaw) frameLengthOffset := int(frameLength) if tmp == frameHeader || tmp == infoFrameHeader { if frameLengthOffset > len(frame) { frameLengthRaw = tmp continue } l, err := io.ReadFull(m.p, frame[:frameLengthOffset]) if err != nil { m.addError(fmt.Errorf("Read Error: %v", err)) time.Sleep(1 * time.Second) } else if l != frameLengthOffset { m.addError(errors.New("Read Length Error")) } else { if hasLEDStatus { if _, err = io.ReadFull(m.p, ledStatus); err != nil { m.addError(fmt.Errorf("Read LED status error: %v", err)) frameLengthRaw = tmp continue } } if checkChecksum(frameLength, tmp, frame[:frameLengthOffset]) { m.frameLock = true mk2log.Info("Frame lock acquired") } } } frameLengthRaw = tmp } } } // Close Mk2 func (m *mk2Ser) Close() { mk2log.Info("Closing MK2 connection") close(m.run) m.wg.Wait() mk2log.Info("MK2 connection closed") } func (m *mk2Ser) C() chan *Mk2Info { return m.infochan } func (m *mk2Ser) RegisterMetadata(kind RegisterKind, id uint16) (RegisterMetadata, bool) { return lookupRegisterMetadata(kind, id) } func (m *mk2Ser) ListRegisterMetadata() []RegisterMetadata { return listRegisterMetadata() } func (m *mk2Ser) ReadRegister(kind RegisterKind, id uint16) (int16, error) { m.beginCommand() defer m.endCommand() return m.readRegisterLocked(kind, id) } func (m *mk2Ser) WriteRegister(kind RegisterKind, id uint16, value int16, opts TransactionOptions) (RegisterTransactionResult, error) { options := normalizeTransactionOptions(opts) commandTimeout := resolveCommandTimeout(options) start := time.Now() result := RegisterTransactionResult{ Kind: kind, ID: id, TargetValue: value, Timeout: commandTimeout, } if meta, ok := lookupRegisterMetadata(kind, id); ok { if !meta.Writable { return result, fmt.Errorf("register %s:%d (%s) is marked read-only", kind, id, meta.Name) } if err := validateValueAgainstMetadata(meta, value); err != nil { return result, err } } m.beginCommand() defer m.endCommand() if options.ReadBeforeWrite { prev, err := m.readRegisterLockedWithTimeout(kind, id, commandTimeout) if err != nil { result.Duration = time.Since(start) return result, fmt.Errorf("could not read current value for %s:%d: %w", kind, id, err) } result.PreviousValue = int16Ptr(prev) } var lastErr error maxAttempts := options.Retries + 1 for attempt := 1; attempt <= maxAttempts; attempt++ { result.Attempts = attempt err := m.writeRegisterLockedWithTimeout(kind, id, value, commandTimeout) if err != nil { lastErr = err if attempt < maxAttempts { delay := retryDelayForAttempt(options, attempt) if delay > 0 { time.Sleep(delay) } continue } break } if !options.VerifyAfterWrite { result.Duration = time.Since(start) return result, nil } verified, err := m.readRegisterLockedWithTimeout(kind, id, commandTimeout) if err != nil { lastErr = fmt.Errorf("verification read failed for %s:%d: %w", kind, id, err) if attempt < maxAttempts { delay := retryDelayForAttempt(options, attempt) if delay > 0 { time.Sleep(delay) } continue } break } result.VerifiedValue = int16Ptr(verified) if verified != value { lastErr = fmt.Errorf("verification mismatch for %s:%d expected %d got %d", kind, id, value, verified) if attempt < maxAttempts { delay := retryDelayForAttempt(options, attempt) if delay > 0 { time.Sleep(delay) } continue } break } result.Duration = time.Since(start) return result, nil } if lastErr == nil { lastErr = fmt.Errorf("transaction failed for %s:%d", kind, id) } result.Duration = time.Since(start) return result, lastErr } func (m *mk2Ser) CaptureSnapshot(addresses []RegisterAddress) (RegisterSnapshot, error) { if len(addresses) == 0 { addresses = defaultWritableRegisterAddresses() } snapshotTime := time.Now().UTC() snapshot := RegisterSnapshot{ CapturedAt: snapshotTime, Entries: make([]RegisterSnapshotEntry, 0, len(addresses)), } for _, address := range addresses { value, err := m.ReadRegister(address.Kind, address.ID) if err != nil { return snapshot, fmt.Errorf("capture snapshot read failed for %s:%d: %w", address.Kind, address.ID, err) } meta, ok := m.RegisterMetadata(address.Kind, address.ID) entry := RegisterSnapshotEntry{ Kind: address.Kind, ID: address.ID, Value: value, CapturedAt: snapshotTime, } if ok { entry.Name = meta.Name entry.Writable = meta.Writable entry.Safety = meta.SafetyClass } snapshot.Entries = append(snapshot.Entries, entry) } return snapshot, nil } func (m *mk2Ser) DiffSnapshot(snapshot RegisterSnapshot) ([]SnapshotDiff, error) { diffs := make([]SnapshotDiff, 0, len(snapshot.Entries)) for _, entry := range snapshot.Entries { current, err := m.ReadRegister(entry.Kind, entry.ID) if err != nil { return diffs, fmt.Errorf("snapshot diff read failed for %s:%d: %w", entry.Kind, entry.ID, err) } meta, ok := m.RegisterMetadata(entry.Kind, entry.ID) name := entry.Name writable := entry.Writable safety := entry.Safety if ok { name = meta.Name writable = meta.Writable safety = meta.SafetyClass } diff := SnapshotDiff{ Kind: entry.Kind, ID: entry.ID, Name: name, Current: current, Target: entry.Value, Changed: current != entry.Value, Writable: writable, Safety: safety, DiffValue: int32(entry.Value) - int32(current), } diffs = append(diffs, diff) } return diffs, nil } func (m *mk2Ser) RestoreSnapshot(snapshot RegisterSnapshot, opts TransactionOptions) (SnapshotRestoreResult, error) { restoreResult := SnapshotRestoreResult{ Applied: make([]RegisterTransactionResult, 0, len(snapshot.Entries)), } diffs, err := m.DiffSnapshot(snapshot) if err != nil { return restoreResult, err } for _, diff := range diffs { if !diff.Changed || !diff.Writable { continue } txResult, txErr := m.WriteRegister(diff.Kind, diff.ID, diff.Target, opts) if txErr == nil { restoreResult.Applied = append(restoreResult.Applied, txResult) continue } restoreResult.RolledBack = true rollbackErrs := m.rollbackAppliedTransactions(restoreResult.Applied, opts) restoreResult.RollbackErrors = append(restoreResult.RollbackErrors, rollbackErrs...) return restoreResult, fmt.Errorf("restore failed for %s:%d: %w", diff.Kind, diff.ID, txErr) } return restoreResult, nil } func (m *mk2Ser) rollbackAppliedTransactions(applied []RegisterTransactionResult, opts TransactionOptions) []string { errs := make([]string, 0) rollbackOpts := normalizeTransactionOptions(TransactionOptions{ Retries: 1, RetryDelay: opts.RetryDelay, BackoffFactor: opts.BackoffFactor, VerifyAfterWrite: true, TimeoutClass: opts.TimeoutClass, CommandTimeout: opts.CommandTimeout, }) for i := len(applied) - 1; i >= 0; i-- { tx := applied[i] if tx.PreviousValue == nil { continue } if _, err := m.WriteRegister(tx.Kind, tx.ID, *tx.PreviousValue, rollbackOpts); err != nil { errs = append(errs, fmt.Sprintf("rollback failed for %s:%d: %v", tx.Kind, tx.ID, err)) } } return errs } func (m *mk2Ser) DriverDiagnostics(limit int) DriverDiagnostics { m.diagMu.Lock() defer m.diagMu.Unlock() traceLimit := limit if traceLimit <= 0 || traceLimit > len(m.traces) { traceLimit = len(m.traces) } traces := make([]ProtocolTrace, traceLimit) if traceLimit > 0 { copy(traces, m.traces[len(m.traces)-traceLimit:]) } recentErrors := append([]string(nil), m.recentErrors...) diag := DriverDiagnostics{ GeneratedAt: time.Now().UTC(), CommandTimeouts: m.commandTimeouts.Load(), CommandFailures: m.commandFailures.Load(), ChecksumFailures: m.checksumErrors.Load(), RecentErrors: recentErrors, Traces: traces, } if !m.lastFrameAt.IsZero() { last := m.lastFrameAt diag.LastFrameAt = &last } diag.HealthScore = calculateDriverHealthScore(diag) return diag } func (m *mk2Ser) readRegisterLocked(kind RegisterKind, id uint16) (int16, error) { return m.readRegisterLockedWithTimeout(kind, id, writeResponseTimeout) } func (m *mk2Ser) readRegisterLockedWithTimeout(kind RegisterKind, id uint16, timeout time.Duration) (int16, error) { switch kind { case RegisterKindRAMVar: return m.readValueByIDWithTimeout(commandReadRAMVar, commandReadRAMResponse, id, timeout) case RegisterKindSetting: return m.readValueByIDWithTimeout(commandReadSetting, commandReadSettingResponse, id, timeout) default: return 0, fmt.Errorf("unsupported register kind %q", kind) } } func (m *mk2Ser) writeRegisterLocked(kind RegisterKind, id uint16, value int16) error { return m.writeRegisterLockedWithTimeout(kind, id, value, writeResponseTimeout) } func (m *mk2Ser) writeRegisterLockedWithTimeout(kind RegisterKind, id uint16, value int16, timeout time.Duration) error { switch kind { case RegisterKindRAMVar: err := m.writeByIDOnlyWithTimeout(commandWriteRAMViaID, commandWriteRAMViaIDResponse, id, value, timeout) if err != nil && !errors.Is(err, errWriteRejected) { mk2log.WithFields(logrus.Fields{ "id": id, "value": value, }).WithError(err).Warn("WriteRegister RAM by-id failed, trying legacy write path") err = m.writeBySelectionWithTimeout(commandWriteRAMVar, commandWriteRAMResponse, id, value, timeout) } return err case RegisterKindSetting: err := m.writeByIDOnlyWithTimeout(commandWriteViaID, commandWriteViaIDResponse, id, value, timeout) if err != nil && !errors.Is(err, errWriteRejected) { mk2log.WithFields(logrus.Fields{ "id": id, "value": value, }).WithError(err).Warn("WriteRegister setting by-id failed, trying legacy write path") err = m.writeBySelectionWithTimeout(commandWriteSetting, commandWriteSettingResponse, id, value, timeout) } return err default: return fmt.Errorf("unsupported register kind %q", kind) } } func (m *mk2Ser) WriteRAMVar(id uint16, value int16) error { mk2log.WithFields(logrus.Fields{ "id": id, "value": value, }).Info("WriteRAMVar requested") m.beginCommand() defer m.endCommand() err := m.writeByIDOnly(commandWriteRAMViaID, commandWriteRAMViaIDResponse, id, value) if err != nil { if errors.Is(err, errWriteRejected) { mk2log.WithError(err).WithField("id", id).Error("WriteRAMVar failed") return err } mk2log.WithFields(logrus.Fields{ "id": id, "value": value, }).WithError(err).Warn("WriteRAMVar by-id command failed, falling back to legacy write sequence") err = m.writeBySelection(commandWriteRAMVar, commandWriteRAMResponse, id, value) } if err != nil { mk2log.WithError(err).WithField("id", id).Error("WriteRAMVar failed") return err } mk2log.WithField("id", id).Info("WriteRAMVar applied") return nil } func (m *mk2Ser) WriteSetting(id uint16, value int16) error { mk2log.WithFields(logrus.Fields{ "id": id, "value": value, }).Info("WriteSetting requested") m.beginCommand() defer m.endCommand() err := m.writeByIDOnly(commandWriteViaID, commandWriteViaIDResponse, id, value) if err != nil { if errors.Is(err, errWriteRejected) { mk2log.WithError(err).WithField("id", id).Error("WriteSetting failed") return err } mk2log.WithFields(logrus.Fields{ "id": id, "value": value, }).WithError(err).Warn("WriteSetting by-id command failed, falling back to legacy write sequence") err = m.writeBySelection(commandWriteSetting, commandWriteSettingResponse, id, value) } if err != nil { mk2log.WithError(err).WithField("id", id).Error("WriteSetting failed") return err } mk2log.WithField("id", id).Info("WriteSetting applied") return nil } func (m *mk2Ser) WriteSettingByID(id uint16, value int16) error { m.beginCommand() defer m.endCommand() return m.writeByIDOnly(commandWriteViaID, commandWriteViaIDResponse, id, value) } func (m *mk2Ser) WriteRAMVarByID(id uint16, value int16) error { m.beginCommand() defer m.endCommand() return m.writeByIDOnly(commandWriteRAMViaID, commandWriteRAMViaIDResponse, id, value) } func (m *mk2Ser) WriteSettingBySelection(id uint16, value int16) error { m.beginCommand() defer m.endCommand() return m.writeBySelection(commandWriteSetting, commandWriteSettingResponse, id, value) } func (m *mk2Ser) WriteRAMVarBySelection(id uint16, value int16) error { m.beginCommand() defer m.endCommand() return m.writeBySelection(commandWriteRAMVar, commandWriteRAMResponse, id, value) } func (m *mk2Ser) WriteSelectedData(value int16) error { m.beginCommand() defer m.endCommand() m.clearWriteResponses() raw := uint16(value) m.sendCommandLocked([]byte{ winmonFrame, commandWriteData, byte(raw), byte(raw >> 8), }) _, err := m.waitForAnyWriteResponseWithTimeout([]byte{ commandWriteRAMResponse, commandWriteSettingResponse, commandWriteViaIDResponse, commandWriteRAMViaIDResponse, }, writeResponseTimeout) return err } func (m *mk2Ser) GetDeviceState() (DeviceState, error) { m.beginCommand() defer m.endCommand() m.clearWinmonResponses() m.sendCommandLocked([]byte{ winmonFrame, commandSetState, 0x00, }) resp, err := m.waitForWinmonResponse(commandSetStateResponse) if err != nil { return 0, err } return decodeDeviceStateResponse(resp) } func (m *mk2Ser) SetDeviceState(state DeviceState) error { if !validDeviceState(state) { return fmt.Errorf("invalid device state: 0x%02x", byte(state)) } m.beginCommand() defer m.endCommand() m.clearWinmonResponses() m.sendCommandLocked([]byte{ winmonFrame, commandSetState, 0x00, byte(state), }) resp, err := m.waitForWinmonResponse(commandSetStateResponse) if err != nil { return err } ackState, err := decodeDeviceStateResponse(resp) if err != nil { return err } if ackState != state { return fmt.Errorf("device acknowledged state %s (0x%02x), expected %s (0x%02x)", formatDeviceState(ackState), byte(ackState), formatDeviceState(state), byte(state)) } return nil } func (m *mk2Ser) ReadRAMVarByID(id uint16) (int16, error) { m.beginCommand() defer m.endCommand() return m.readValueByID(commandReadRAMVar, commandReadRAMResponse, id) } func (m *mk2Ser) ReadSettingByID(id uint16) (int16, error) { m.beginCommand() defer m.endCommand() return m.readValueByID(commandReadSetting, commandReadSettingResponse, id) } func (m *mk2Ser) SelectRAMVar(id uint16) error { m.beginCommand() defer m.endCommand() _, err := m.readValueByID(commandReadRAMVar, commandReadRAMResponse, id) return err } func (m *mk2Ser) SelectSetting(id uint16) error { m.beginCommand() defer m.endCommand() _, err := m.readValueByID(commandReadSetting, commandReadSettingResponse, id) return err } func (m *mk2Ser) ReadSelected() (int16, error) { m.beginCommand() defer m.endCommand() m.clearWinmonResponses() m.sendCommandLocked([]byte{ winmonFrame, commandReadSelected, }) resp, err := m.waitForWinmonResponse(commandReadSelectedResponse) if err != nil { return 0, err } return decodeInt16Response(resp) } func (m *mk2Ser) ReadRAMVarInfo(id uint16) (RAMVarInfo, error) { m.beginCommand() defer m.endCommand() m.clearWinmonResponses() m.sendCommandLocked([]byte{ winmonFrame, commandGetRAMVarInfo, byte(id), byte(id >> 8), }) resp, err := m.waitForWinmonResponse(commandGetRAMVarInfoResponse) if err != nil { return RAMVarInfo{}, err } return decodeRAMVarInfoResponse(id, resp) } func (m *mk2Ser) SetPanelState(switchState PanelSwitchState, currentLimitA *float64) error { if !validPanelSwitchState(switchState) { return fmt.Errorf("invalid panel switch state: %d", switchState) } currentLimitRaw, err := encodePanelCurrentLimit(currentLimitA) if err != nil { return err } m.beginCommand() defer m.endCommand() logEntry := mk2log.WithField("switch_state", switchState) if currentLimitA != nil { logEntry = logEntry.WithField("current_limit_a", *currentLimitA) } logEntry.Info("SetPanelState requested") m.clearStateResponses() m.sendCommandLocked([]byte{ stateFrame, byte(switchState), byte(currentLimitRaw), byte(currentLimitRaw >> 8), 0x01, panelStateVariant2Flags, }) err = m.waitForStateResponse() if err != nil { logEntry.WithError(err).Error("SetPanelState failed") return err } logEntry.Info("SetPanelState acknowledged") return nil } func (m *mk2Ser) SetStandby(enabled bool) error { lineState := byte(interfacePanelDetectFlag) if enabled { lineState |= interfaceStandbyFlag } m.beginCommand() defer m.endCommand() logEntry := mk2log.WithField("standby_enabled", enabled) logEntry.Info("SetStandby requested") m.clearInterfaceResponses() m.sendCommandLocked([]byte{ interfaceFrame, lineState, }) err := m.waitForInterfaceResponse(enabled) if err != nil { logEntry.WithError(err).Error("SetStandby failed") return err } logEntry.Info("SetStandby acknowledged") return nil } func validPanelSwitchState(switchState PanelSwitchState) bool { switch switchState { case PanelSwitchChargerOnly, PanelSwitchInverterOnly, PanelSwitchOn, PanelSwitchOff: return true default: return false } } func validDeviceState(state DeviceState) bool { switch state { case DeviceStateChargerOnly, DeviceStateInverterOnly, DeviceStateOn, DeviceStateOff: return true default: return false } } func formatDeviceState(state DeviceState) string { if name, ok := DeviceStateNames[state]; ok { return name } return "unknown" } func encodePanelCurrentLimit(currentLimitA *float64) (uint16, error) { if currentLimitA == nil { return panelCurrentLimitUnknown, nil } if *currentLimitA < 0 { return 0, fmt.Errorf("current_limit must be >= 0, got %.3f", *currentLimitA) } raw := math.Round(*currentLimitA * 10) if raw > panelCurrentLimitMax { return 0, fmt.Errorf("current_limit %.3f A is above MK2 maximum %.1f A", *currentLimitA, panelCurrentLimitMax/10.0) } return uint16(raw), nil } func (m *mk2Ser) writeByIDOnly(writeCommand, expectedResponse byte, id uint16, value int16) error { return m.writeByIDOnlyWithTimeout(writeCommand, expectedResponse, id, value, writeResponseTimeout) } func (m *mk2Ser) writeByIDOnlyWithTimeout(writeCommand, expectedResponse byte, id uint16, value int16, timeout time.Duration) error { m.clearWriteResponses() rawValue := uint16(value) m.sendCommandLocked([]byte{ winmonFrame, writeCommand, byte(id), byte(id >> 8), byte(rawValue), byte(rawValue >> 8), }) return m.waitForWriteResponseWithTimeout(expectedResponse, timeout) } func (m *mk2Ser) writeBySelection(selectCommand, expectedResponse byte, id uint16, value int16) error { return m.writeBySelectionWithTimeout(selectCommand, expectedResponse, id, value, writeResponseTimeout) } func (m *mk2Ser) writeBySelectionWithTimeout(selectCommand, expectedResponse byte, id uint16, value int16, timeout time.Duration) error { m.clearWriteResponses() rawValue := uint16(value) m.sendCommandLocked([]byte{ winmonFrame, selectCommand, byte(id), byte(id >> 8), }) m.sendCommandLocked([]byte{ winmonFrame, commandWriteData, byte(rawValue), byte(rawValue >> 8), }) return m.waitForWriteResponseWithTimeout(expectedResponse, timeout) } func (m *mk2Ser) readValueByID(readCommand, expectedResponse byte, id uint16) (int16, error) { return m.readValueByIDWithTimeout(readCommand, expectedResponse, id, writeResponseTimeout) } func (m *mk2Ser) readValueByIDWithTimeout(readCommand, expectedResponse byte, id uint16, timeout time.Duration) (int16, error) { m.clearWinmonResponses() m.sendCommandLocked([]byte{ winmonFrame, readCommand, byte(id), byte(id >> 8), }) resp, err := m.waitForWinmonResponseWithTimeout(expectedResponse, timeout) if err != nil { return 0, err } return decodeInt16Response(resp) } func decodeInt16Response(resp winmonResponse) (int16, error) { if len(resp.data) < 2 { return 0, fmt.Errorf("invalid response 0x%02x payload length %d", resp.command, len(resp.data)) } return int16(uint16(resp.data[0]) | uint16(resp.data[1])<<8), nil } func decodeDeviceStateResponse(resp winmonResponse) (DeviceState, error) { if len(resp.data) < 1 { return 0, fmt.Errorf("invalid device state response payload length %d", len(resp.data)) } var raw byte if len(resp.data) >= 2 { raw = resp.data[1] } else { raw = resp.data[0] } state := DeviceState(raw) if !validDeviceState(state) { return state, fmt.Errorf("unsupported device state 0x%02x", raw) } return state, nil } func decodeRAMVarInfoResponse(id uint16, resp winmonResponse) (RAMVarInfo, error) { info := RAMVarInfo{ ID: id, } if len(resp.data) < 4 { return info, nil } scl := int16(resp.data[1])<<8 + int16(resp.data[0]) var ofs int16 if len(resp.data) == 4 { ofs = int16(uint16(resp.data[3])<<8 + uint16(resp.data[2])) } else { ofs = int16(uint16(resp.data[4])<<8 + uint16(resp.data[3])) } info.Supported = true info.Scale = scl info.Offset = ofs info.Signed = scl < 0 scale := int16Abs(scl) if scale >= 0x4000 { info.Factor = 1 / (0x8000 - float64(scale)) } else { info.Factor = float64(scale) } return info, nil } func (m *mk2Ser) clearWriteResponses() { if m.writeAck == nil { m.writeAck = make(chan byte, 4) return } for { select { case <-m.writeAck: default: return } } } func (m *mk2Ser) waitForWriteResponse(expectedResponse byte) error { return m.waitForWriteResponseWithTimeout(expectedResponse, writeResponseTimeout) } func (m *mk2Ser) waitForWriteResponseWithTimeout(expectedResponse byte, timeout time.Duration) error { if m.writeAck == nil { return errors.New("write response channel is not initialized") } select { case response := <-m.writeAck: mk2log.WithFields(logrus.Fields{ "expected_response": fmt.Sprintf("0x%02x", expectedResponse), "received_response": fmt.Sprintf("0x%02x", response), }).Debug("Received write acknowledgement") switch response { case expectedResponse: return nil case commandUnsupportedResponse: m.noteCommandFailure(fmt.Errorf("received unsupported response 0x%02x", response)) return errCommandUnsupported case commandWriteNotAllowedResponse: m.noteCommandFailure(fmt.Errorf("received write rejected response 0x%02x", response)) return errWriteRejected default: err := fmt.Errorf("unexpected write response 0x%02x", response) m.noteCommandFailure(err) return err } case <-time.After(timeout): err := fmt.Errorf("timed out waiting for write response after %s", timeout) mk2log.WithField("expected_response", fmt.Sprintf("0x%02x", expectedResponse)).WithError(err).Error("Timed out waiting for write acknowledgement") m.noteCommandTimeout(err) return err } } func (m *mk2Ser) waitForAnyWriteResponseWithTimeout(expectedResponses []byte, timeout time.Duration) (byte, error) { if m.writeAck == nil { return 0, errors.New("write response channel is not initialized") } expected := make(map[byte]struct{}, len(expectedResponses)) for _, response := range expectedResponses { expected[response] = struct{}{} } select { case response := <-m.writeAck: if _, ok := expected[response]; ok { return response, nil } switch response { case commandUnsupportedResponse: err := fmt.Errorf("received unsupported write response 0x%02x", response) m.noteCommandFailure(err) return response, errCommandUnsupported case commandWriteNotAllowedResponse: err := fmt.Errorf("received write rejected response 0x%02x", response) m.noteCommandFailure(err) return response, errWriteRejected default: err := fmt.Errorf("unexpected write response 0x%02x", response) m.noteCommandFailure(err) return response, err } case <-time.After(timeout): err := fmt.Errorf("timed out waiting for write response after %s", timeout) m.noteCommandTimeout(err) return 0, err } } func (m *mk2Ser) pushWriteResponse(response byte) { if m.writeAck == nil { return } select { case m.writeAck <- response: default: } } func (m *mk2Ser) clearWinmonResponses() { if m.winmonAck == nil { m.winmonAck = make(chan winmonResponse, 32) return } for { select { case <-m.winmonAck: default: return } } } func (m *mk2Ser) waitForWinmonResponse(expectedResponse byte) (winmonResponse, error) { return m.waitForWinmonResponseWithTimeout(expectedResponse, writeResponseTimeout) } func (m *mk2Ser) waitForWinmonResponseWithTimeout(expectedResponse byte, timeout time.Duration) (winmonResponse, error) { if m.winmonAck == nil { return winmonResponse{}, errors.New("winmon response channel is not initialized") } timeoutChan := time.After(timeout) for { select { case response := <-m.winmonAck: mk2log.WithFields(logrus.Fields{ "expected_response": fmt.Sprintf("0x%02x", expectedResponse), "received_response": fmt.Sprintf("0x%02x", response.command), "response_len": len(response.data), }).Debug("Received winmon response") switch response.command { case expectedResponse: return response, nil case commandUnsupportedResponse: m.noteCommandFailure(fmt.Errorf("received unsupported winmon response 0x%02x", response.command)) return winmonResponse{}, errCommandUnsupported case commandWriteNotAllowedResponse: m.noteCommandFailure(fmt.Errorf("received write rejected winmon response 0x%02x", response.command)) return winmonResponse{}, errWriteRejected default: mk2log.WithFields(logrus.Fields{ "expected_response": fmt.Sprintf("0x%02x", expectedResponse), "received_response": fmt.Sprintf("0x%02x", response.command), }).Debug("Ignoring unrelated winmon response while waiting") } case <-timeoutChan: err := fmt.Errorf("timed out waiting for winmon response 0x%02x after %s", expectedResponse, timeout) m.noteCommandTimeout(err) return winmonResponse{}, err } } } func (m *mk2Ser) pushWinmonResponse(command byte, data []byte) { if m.winmonAck == nil { return } payloadCopy := make([]byte, len(data)) copy(payloadCopy, data) select { case m.winmonAck <- winmonResponse{command: command, data: payloadCopy}: default: } } func (m *mk2Ser) clearStateResponses() { if m.stateAck == nil { m.stateAck = make(chan struct{}, 1) return } for { select { case <-m.stateAck: default: return } } } func (m *mk2Ser) waitForStateResponse() error { return m.waitForStateResponseWithTimeout(writeResponseTimeout) } func (m *mk2Ser) waitForStateResponseWithTimeout(timeout time.Duration) error { if m.stateAck == nil { return errors.New("panel state response channel is not initialized") } select { case <-m.stateAck: mk2log.Debug("Received panel state acknowledgement") return nil case <-time.After(timeout): err := fmt.Errorf("timed out waiting for panel state response after %s", timeout) mk2log.WithError(err).Error("Timed out waiting for panel state acknowledgement") m.noteCommandTimeout(err) return err } } func (m *mk2Ser) pushStateResponse() { if m.stateAck == nil { return } select { case m.stateAck <- struct{}{}: default: } } func (m *mk2Ser) clearInterfaceResponses() { if m.ifaceAck == nil { m.ifaceAck = make(chan byte, 1) return } for { select { case <-m.ifaceAck: default: return } } } func (m *mk2Ser) waitForInterfaceResponse(expectedStandby bool) error { return m.waitForInterfaceResponseWithTimeout(expectedStandby, writeResponseTimeout) } func (m *mk2Ser) waitForInterfaceResponseWithTimeout(expectedStandby bool, timeout time.Duration) error { if m.ifaceAck == nil { return errors.New("interface response channel is not initialized") } select { case lineState := <-m.ifaceAck: standbyEnabled := lineState&interfaceStandbyFlag != 0 mk2log.WithFields(logrus.Fields{ "line_state": fmt.Sprintf("0x%02x", lineState), "expected_standby": expectedStandby, "actual_standby": standbyEnabled, }).Debug("Received standby interface acknowledgement") if standbyEnabled != expectedStandby { err := fmt.Errorf("unexpected standby line state 0x%02x", lineState) m.noteCommandFailure(err) return err } return nil case <-time.After(timeout): err := fmt.Errorf("timed out waiting for standby response after %s", timeout) mk2log.WithField("expected_standby", expectedStandby).WithError(err).Error("Timed out waiting for standby acknowledgement") m.noteCommandTimeout(err) return err } } func (m *mk2Ser) pushInterfaceResponse(lineState byte) { if m.ifaceAck == nil { return } select { case m.ifaceAck <- lineState: default: } } func (m *mk2Ser) readByte() byte { buffer := make([]byte, 1) _, err := io.ReadFull(m.p, buffer) if err != nil { m.addError(fmt.Errorf("Read error: %v", err)) return 0 } return buffer[0] } // Adds error to error slice. func (m *mk2Ser) addError(err error) { mk2log.Errorf("Mk2 serial slice error: %q", err.Error()) if m.info.Errors == nil { m.info.Errors = make([]error, 0) } m.info.Errors = append(m.info.Errors, err) m.info.Valid = false m.recordError(err) } func (m *mk2Ser) noteCommandTimeout(err error) { m.commandTimeouts.Add(1) m.commandFailures.Add(1) m.recordError(err) } func (m *mk2Ser) noteCommandFailure(err error) { m.commandFailures.Add(1) m.recordError(err) } // Updates report. func (m *mk2Ser) updateReport() { m.info.Timestamp = time.Now() mk2log.WithFields(logrus.Fields{ "in_voltage": m.info.InVoltage, "in_current": m.info.InCurrent, "out_voltage": m.info.OutVoltage, "out_current": m.info.OutCurrent, "bat_voltage": m.info.BatVoltage, "bat_current": m.info.BatCurrent, "charge_state": m.info.ChargeState, "valid": m.info.Valid, }).Debug("Publishing MK2 status update") select { case m.infochan <- m.info: default: mk2log.Warn("Dropping MK2 status update; consumer channel is full") } m.info = &Mk2Info{} } // Checks for valid frame and chooses decoding. func (m *mk2Ser) handleFrame(l byte, frame []byte, appendedLED []byte) { mk2log.Debugf("[handleFrame] frame %#v", frame) if len(frame) == 0 { mk2log.Warn("[handleFrame] empty frame") return } if checkChecksum(l, frame[0], frame[1:]) { m.markFrameSeen() m.recordRXTrace(frame) switch frame[0] { case bootupFrameHeader: if m.pollPaused.Load() { mk2log.Debug("Skipping setTarget during active control transaction") return } m.setTarget() case frameHeader: if len(frame) < 2 { mk2log.Warnf("[handleFrame] truncated frameHeader frame: %#v", frame) return } switch frame[1] { case interfaceFrame: if len(frame) > 2 { m.pushInterfaceResponse(frame[2]) } case stateFrame: if len(appendedLED) == frameLEDBytes { m.setLEDState(appendedLED[0], appendedLED[1]) } m.pushStateResponse() case vFrame: if len(frame) < 6 { mk2log.Warnf("[handleFrame] truncated version frame: %#v", frame) return } m.versionDecode(frame[2:]) case winmonFrame: if len(frame) < 3 { mk2log.Warnf("[handleFrame] truncated winmon frame: %#v", frame) return } winmonCommand := frame[2] var winmonData []byte if len(frame) > 3 { winmonData = frame[3 : len(frame)-1] } m.pushWinmonResponse(winmonCommand, winmonData) switch frame[2] { case commandGetRAMVarInfoResponse: if !m.pollPaused.Load() { m.scaleDecode(frame[2:]) } case commandReadRAMResponse: if !m.pollPaused.Load() { m.stateDecode(frame[2:]) } case commandReadSettingResponse, commandReadSelectedResponse: // Responses are consumed by synchronous protocol command methods. case commandSetStateResponse, commandWriteRAMResponse, commandWriteSettingResponse, commandWriteViaIDResponse, commandWriteRAMViaIDResponse, commandUnsupportedResponse, commandWriteNotAllowedResponse: m.pushWriteResponse(frame[2]) default: mk2log.Warnf("[handleFrame] invalid winmonFrame %v", frame[2:]) } case ledFrame: if len(frame) < 4 { mk2log.Warnf("[handleFrame] truncated LED frame: %#v", frame) return } m.ledDecode(frame[2:]) default: mk2log.Warnf("[handleFrame] invalid frameHeader %v", frame[1]) } case infoFrameHeader: if len(frame) < 6 { mk2log.Warnf("[handleFrame] truncated info frame: %#v", frame) return } switch frame[5] { case dcInfoFrame: m.dcDecode(frame[1:]) case acL1InfoFrame: m.acDecode(frame[1:]) default: mk2log.Warnf("[handleFrame] invalid infoFrameHeader %v", frame[5]) } default: mk2log.Warnf("[handleFrame] Invalid frame %v", frame[0]) } } else { m.checksumErrors.Add(1) mk2log.Errorf("[handleFrame] Invalid incoming frame checksum: %x", frame) m.frameLock = false m.recordError(fmt.Errorf("invalid incoming frame checksum")) } } // Set the target VBus device. func (m *mk2Ser) setTarget() { cmd := make([]byte, 3) cmd[0] = setTargetFrame cmd[1] = 0x01 cmd[2] = 0x00 m.sendCommand(cmd) } // Request the scaling factor for entry 'in'. func (m *mk2Ser) reqScaleFactor(in byte) { cmd := make([]byte, 4) cmd[0] = winmonFrame cmd[1] = commandGetRAMVarInfo cmd[2] = in m.sendMonitoringCommand(cmd) } func int16Abs(in int16) uint16 { if in < 0 { return uint16(-in) } return uint16(in) } // Decode the scale factor frame. func (m *mk2Ser) scaleDecode(frame []byte) { tmp := scaling{} mk2log.Debugf("Scale frame(%d): 0x%x", len(frame), frame) if len(frame) < 6 { tmp.supported = false mk2log.Warnf("Skiping scaling factors for: %d", m.scaleCount) } else { tmp.supported = true var scl int16 var ofs int16 if len(frame) == 6 { scl = int16(frame[2])<<8 + int16(frame[1]) ofs = int16(uint16(frame[4])<<8 + uint16(frame[3])) } else { scl = int16(frame[2])<<8 + int16(frame[1]) ofs = int16(uint16(frame[5])<<8 + uint16(frame[4])) } if scl < 0 { tmp.signed = true } tmp.offset = float64(ofs) scale := int16Abs(scl) if scale >= 0x4000 { tmp.scale = 1 / (0x8000 - float64(scale)) } else { tmp.scale = float64(scale) } } mk2log.Debugf("scalecount %v: %#v \n", m.scaleCount, tmp) m.scales = append(m.scales, tmp) m.scaleCount++ if m.scaleCount < ramVarMaxOffset { m.reqScaleFactor(byte(m.scaleCount)) } else { mk2log.Info("Monitoring starting.") } } // Decode the version number func (m *mk2Ser) versionDecode(frame []byte) { mk2log.Debugf("versiondecode %v", frame) m.info.Version = 0 m.info.Valid = true for i := 0; i < 4; i++ { m.info.Version += uint32(frame[i]) << uint(i) * 8 } if m.scaleCount < ramVarMaxOffset { mk2log.WithField("version", m.info.Version).Info("Get scaling factors") m.reqScaleFactor(byte(m.scaleCount)) } else { // Send DC status request cmd := make([]byte, 2) cmd[0] = infoReqFrame cmd[1] = infoReqAddrDC m.sendMonitoringCommand(cmd) } } // Decode with correct signedness and apply scale func (m *mk2Ser) applyScaleAndSign(data []byte, scale int) float64 { var value float64 if !m.scales[scale].supported { return 0 } if m.scales[scale].signed { value = getSigned(data) } else { value = getUnsigned16(data) } return m.applyScale(value, scale) } // Apply scaling to float func (m *mk2Ser) applyScale(value float64, scale int) float64 { if !m.scales[scale].supported { return value } return m.scales[scale].scale * (value + m.scales[scale].offset) } // Convert bytes->int16->float func getSigned(data []byte) float64 { return float64(int16(data[0]) + int16(data[1])<<8) } // Convert bytes->int16->float func getUnsigned16(data []byte) float64 { return float64(uint16(data[0]) + uint16(data[1])<<8) } // Convert bytes->uint32->float func getUnsigned(data []byte) float64 { return float64(uint32(data[0]) + uint32(data[1])<<8 + uint32(data[2])<<16) } // Decodes DC frame. func (m *mk2Ser) dcDecode(frame []byte) { m.info.BatVoltage = m.applyScaleAndSign(frame[5:7], ramVarVBat) usedC := m.applyScale(getUnsigned(frame[7:10]), ramVarIBat) chargeC := m.applyScale(getUnsigned(frame[10:13]), ramVarIBat) m.info.BatCurrent = usedC - chargeC m.info.OutFrequency = m.calcFreq(frame[13], ramVarInverterPeriod) mk2log.Debugf("dcDecode %#v", m.info) // Send L1 status request cmd := make([]byte, 2) cmd[0] = infoReqFrame cmd[1] = infoReqAddrACL1 m.sendMonitoringCommand(cmd) } // Decodes AC frame. func (m *mk2Ser) acDecode(frame []byte) { m.info.InVoltage = m.applyScale(getSigned(frame[5:7]), ramVarVMains) m.info.InCurrent = m.applyScale(getSigned(frame[7:9]), ramVarIMains) m.info.OutVoltage = m.applyScale(getSigned(frame[9:11]), ramVarVInverter) m.info.OutCurrent = m.applyScale(getSigned(frame[11:13]), ramVarIInverter) m.info.InFrequency = m.calcFreq(frame[13], ramVarMainPeriod) mk2log.Debugf("acDecode %#v", m.info) // Send status request cmd := make([]byte, 1) cmd[0] = ledFrame m.sendMonitoringCommand(cmd) } func (m *mk2Ser) calcFreq(data byte, scaleIndex int) float64 { if data == 0xff || data == 0x00 { return 0 } return 10 / (m.applyScale(float64(data), scaleIndex)) } // Decode charge state of battery. func (m *mk2Ser) stateDecode(frame []byte) { m.info.ChargeState = m.applyScaleAndSign(frame[1:3], ramVarChargeState) mk2log.Debugf("battery state decode %#v", m.info) m.updateReport() } // Decode the LED state frame. func (m *mk2Ser) ledDecode(frame []byte) { if len(frame) < 2 { mk2log.Warnf("Skipping LED decode for short frame: %#v", frame) return } m.setLEDState(frame[0], frame[1]) // Send charge state request cmd := make([]byte, 4) cmd[0] = winmonFrame cmd[1] = commandReadRAMVar cmd[2] = ramVarChargeState m.sendMonitoringCommand(cmd) } func (m *mk2Ser) setLEDState(ledsOn, ledsBlink byte) { m.info.LEDs = getLEDs(ledsOn, ledsBlink) } // Adds active LEDs to list. func getLEDs(ledsOn, ledsBlink byte) map[Led]LEDstate { leds := map[Led]LEDstate{} for i := 0; i < 8; i++ { on := (ledsOn >> uint(i)) & 1 blink := (ledsBlink >> uint(i)) & 1 if on == 1 { leds[Led(i)] = LedOn } else if blink == 1 { leds[Led(i)] = LedBlink } else { leds[Led(i)] = LedOff } } return leds } // Adds header and trailing crc for frame to send. func (m *mk2Ser) sendCommand(data []byte) { m.commandMu.Lock() defer m.commandMu.Unlock() m.sendCommandLocked(data) } func (m *mk2Ser) sendCommandLocked(data []byte) { l := len(data) dataOut := make([]byte, l+3) dataOut[0] = byte(l + 1) dataOut[1] = frameHeader cr := -dataOut[0] - dataOut[1] for i := 0; i < len(data); i++ { cr = cr - data[i] dataOut[i+2] = data[i] } dataOut[l+2] = cr mk2log.Debugf("sendCommand %#v", dataOut) m.recordTXTrace(dataOut, data) _, err := m.p.Write(dataOut) if err != nil { mk2log.WithError(err).Error("Failed to send MK2 command") m.addError(fmt.Errorf("Write error: %v", err)) m.noteCommandFailure(err) } } func (m *mk2Ser) markFrameSeen() { m.diagMu.Lock() m.lastFrameAt = time.Now().UTC() m.diagMu.Unlock() } func (m *mk2Ser) recordError(err error) { if err == nil { return } m.diagMu.Lock() defer m.diagMu.Unlock() m.recentErrors = append(m.recentErrors, err.Error()) if len(m.recentErrors) > 20 { m.recentErrors = m.recentErrors[len(m.recentErrors)-20:] } } func (m *mk2Ser) recordTXTrace(fullFrame []byte, payload []byte) { command := "" frame := "" if len(payload) > 0 { frame = fmt.Sprintf("0x%02x", payload[0]) command = decodeTraceCommandName(payload) } m.appendTrace(ProtocolTrace{ Timestamp: time.Now().UTC(), Direction: TraceDirectionTX, Frame: frame, Command: command, BytesHex: strings.ToUpper(fmt.Sprintf("%X", fullFrame)), }) } func (m *mk2Ser) recordRXTrace(frame []byte) { command := "" frameName := "" if len(frame) > 0 { frameName = fmt.Sprintf("0x%02x", frame[0]) command = decodeTraceCommandName(frame) } m.appendTrace(ProtocolTrace{ Timestamp: time.Now().UTC(), Direction: TraceDirectionRX, Frame: frameName, Command: command, BytesHex: strings.ToUpper(fmt.Sprintf("%X", frame)), }) } func decodeTraceCommandName(frame []byte) string { if len(frame) == 0 { return "" } switch frame[0] { case winmonFrame: if len(frame) < 2 { return "winmon" } return fmt.Sprintf("winmon:0x%02x", frame[1]) case stateFrame: return "panel_state" case interfaceFrame: return "interface" case infoReqFrame: return "info_request" case ledFrame: return "led_status" case vFrame: return "version" case setTargetFrame: return "set_target" default: if frame[0] == frameHeader && len(frame) > 1 && frame[1] == winmonFrame && len(frame) > 2 { return fmt.Sprintf("winmon:0x%02x", frame[2]) } return "" } } func (m *mk2Ser) appendTrace(trace ProtocolTrace) { m.diagMu.Lock() defer m.diagMu.Unlock() m.traces = append(m.traces, trace) limit := m.traceLimit if limit <= 0 { limit = 200 } if len(m.traces) > limit { m.traces = m.traces[len(m.traces)-limit:] } } func calculateDriverHealthScore(diag DriverDiagnostics) int { score := 100 score -= int(diag.CommandTimeouts) * 5 score -= int(diag.CommandFailures) * 2 score -= int(diag.ChecksumFailures) * 3 if diag.LastFrameAt == nil { score -= 10 } else if time.Since(diag.LastFrameAt.UTC()) > 30*time.Second { score -= 10 } if score < 0 { return 0 } if score > 100 { return 100 } return score } // Checks the frame crc. func checkChecksum(l, t byte, d []byte) bool { cr := (uint16(l) + uint16(t)) % 256 for i := 0; i < len(d); i++ { cr = (cr + uint16(d[i])) % 256 } return cr == 0 }