Files
invertergui/mk2driver/mk2.go
Nathan Coad e995a252e1
All checks were successful
continuous-integration/drone/push Build is passing
feat: Enhance MK2 driver with device state management and improved command handling
2026-02-19 13:13:19 +11:00

1238 lines
30 KiB
Go

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
}