package mk2driver import ( "errors" "fmt" "io" "math" "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 } var _ ProtocolControl = (*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.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) 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) 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 { m.clearWriteResponses() rawValue := uint16(value) m.sendCommandLocked([]byte{ winmonFrame, writeCommand, byte(id), byte(id >> 8), byte(rawValue), byte(rawValue >> 8), }) return m.waitForWriteResponse(expectedResponse) } func (m *mk2Ser) writeBySelection(selectCommand, expectedResponse byte, id uint16, value int16) 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.waitForWriteResponse(expectedResponse) } func (m *mk2Ser) readValueByID(readCommand, expectedResponse byte, id uint16) (int16, error) { m.clearWinmonResponses() m.sendCommandLocked([]byte{ winmonFrame, readCommand, byte(id), byte(id >> 8), }) resp, err := m.waitForWinmonResponse(expectedResponse) 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 { 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: return errCommandUnsupported case commandWriteNotAllowedResponse: return errWriteRejected default: return fmt.Errorf("unexpected write response 0x%02x", response) } case <-time.After(writeResponseTimeout): mk2log.WithField("expected_response", fmt.Sprintf("0x%02x", expectedResponse)).Error("Timed out waiting for write acknowledgement") return fmt.Errorf("timed out waiting for write response after %s", writeResponseTimeout) } } 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) { if m.winmonAck == nil { return winmonResponse{}, errors.New("winmon response channel is not initialized") } timeout := time.After(writeResponseTimeout) 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: return winmonResponse{}, errCommandUnsupported case commandWriteNotAllowedResponse: 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 <-timeout: return winmonResponse{}, fmt.Errorf("timed out waiting for winmon response 0x%02x after %s", expectedResponse, writeResponseTimeout) } } } 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 { 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(writeResponseTimeout): mk2log.Error("Timed out waiting for panel state acknowledgement") return fmt.Errorf("timed out waiting for panel state response after %s", writeResponseTimeout) } } 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 { 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 { return fmt.Errorf("unexpected standby line state 0x%02x", lineState) } return nil case <-time.After(writeResponseTimeout): mk2log.WithField("expected_standby", expectedStandby).Error("Timed out waiting for standby acknowledgement") return fmt.Errorf("timed out waiting for standby response after %s", writeResponseTimeout) } } 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 } // 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:]) { 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 { mk2log.Errorf("[handleFrame] Invalid incoming frame checksum: %x", frame) m.frameLock = false } } // 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) _, 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)) } } // 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 }