feat: Enhance MK2 driver with device state management and improved command handling
All checks were successful
continuous-integration/drone/push Build is passing

This commit is contained in:
2026-02-19 13:13:19 +11:00
parent e17e4d1a0a
commit e995a252e1
6 changed files with 917 additions and 36 deletions

View File

@@ -6,6 +6,7 @@ import (
"io"
"math"
"sync"
"sync/atomic"
"time"
"github.com/sirupsen/logrus"
@@ -47,6 +48,8 @@ const (
infoFrameHeader = 0x20
frameHeader = 0xff
bootupFrameHeader = 0x0
frameLengthLEDBit = 0x80
frameLEDBytes = 2
)
const (
@@ -79,22 +82,42 @@ const (
// 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
commandWriteNotAllowedResponse = 0x9B
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
@@ -104,12 +127,43 @@ type mk2Ser 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
@@ -118,6 +172,7 @@ func NewMk2Connection(dev io.ReadWriter) (Mk2, error) {
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()
@@ -132,7 +187,8 @@ func NewMk2Connection(dev io.ReadWriter) (Mk2, error) {
// Locks to incoming frame.
func (m *mk2Ser) frameLocker() {
frame := make([]byte, 256)
var frameLength byte
ledStatus := make([]byte, frameLEDBytes)
var frameLengthRaw byte
for {
select {
case <-m.run:
@@ -141,8 +197,14 @@ func (m *mk2Ser) frameLocker() {
default:
}
if m.frameLock {
frameLength = m.readByte()
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))
@@ -151,12 +213,26 @@ func (m *mk2Ser) frameLocker() {
m.addError(errors.New("Read Length Error"))
m.frameLock = false
} else {
m.handleFrame(frameLength, frame[:frameLengthOffset])
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))
@@ -164,13 +240,20 @@ func (m *mk2Ser) frameLocker() {
} 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")
}
}
}
frameLength = tmp
frameLengthRaw = tmp
}
}
}
@@ -192,7 +275,22 @@ func (m *mk2Ser) WriteRAMVar(id uint16, value int16) error {
"id": id,
"value": value,
}).Info("WriteRAMVar requested")
err := m.writeByID(commandWriteRAMVar, commandWriteRAMResponse, id, value)
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
@@ -206,7 +304,22 @@ func (m *mk2Ser) WriteSetting(id uint16, value int16) error {
"id": id,
"value": value,
}).Info("WriteSetting requested")
err := m.writeByID(commandWriteSetting, commandWriteSettingResponse, id, value)
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
@@ -215,6 +328,125 @@ func (m *mk2Ser) WriteSetting(id uint16, value int16) error {
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)
@@ -225,8 +457,8 @@ func (m *mk2Ser) SetPanelState(switchState PanelSwitchState, currentLimitA *floa
return err
}
m.commandMu.Lock()
defer m.commandMu.Unlock()
m.beginCommand()
defer m.endCommand()
logEntry := mk2log.WithField("switch_state", switchState)
if currentLimitA != nil {
@@ -259,8 +491,8 @@ func (m *mk2Ser) SetStandby(enabled bool) error {
lineState |= interfaceStandbyFlag
}
m.commandMu.Lock()
defer m.commandMu.Unlock()
m.beginCommand()
defer m.endCommand()
logEntry := mk2log.WithField("standby_enabled", enabled)
logEntry.Info("SetStandby requested")
@@ -288,6 +520,22 @@ func validPanelSwitchState(switchState PanelSwitchState) bool {
}
}
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
@@ -303,26 +551,29 @@ func encodePanelCurrentLimit(currentLimitA *float64) (uint16, error) {
return uint16(raw), nil
}
func (m *mk2Ser) writeByID(selectCommand, expectedResponse byte, id uint16, value int16) error {
m.commandMu.Lock()
defer m.commandMu.Unlock()
mk2log.WithFields(logrus.Fields{
"select_command": fmt.Sprintf("0x%02x", selectCommand),
"expected_response": fmt.Sprintf("0x%02x", expectedResponse),
"id": id,
"value": value,
}).Debug("Issuing write-by-id command")
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),
})
rawValue := uint16(value)
m.sendCommandLocked([]byte{
winmonFrame,
commandWriteData,
@@ -333,6 +584,77 @@ func (m *mk2Ser) writeByID(selectCommand, expectedResponse byte, id uint16, valu
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)
@@ -362,9 +684,9 @@ func (m *mk2Ser) waitForWriteResponse(expectedResponse byte) error {
case expectedResponse:
return nil
case commandUnsupportedResponse:
return errors.New("write command is not supported by this device firmware")
return errCommandUnsupported
case commandWriteNotAllowedResponse:
return errors.New("write command rejected by device access level")
return errWriteRejected
default:
return fmt.Errorf("unexpected write response 0x%02x", response)
}
@@ -385,6 +707,67 @@ func (m *mk2Ser) pushWriteResponse(response byte) {
}
}
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)
@@ -515,41 +898,84 @@ func (m *mk2Ser) updateReport() {
}
// Checks for valid frame and chooses decoding.
func (m *mk2Ser) handleFrame(l byte, frame []byte) {
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:
m.scaleDecode(frame[2:])
if !m.pollPaused.Load() {
m.scaleDecode(frame[2:])
}
case commandReadRAMResponse:
m.stateDecode(frame[2:])
case commandWriteRAMResponse, commandWriteSettingResponse, commandUnsupportedResponse, commandWriteNotAllowedResponse:
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:])
@@ -582,7 +1008,7 @@ func (m *mk2Ser) reqScaleFactor(in byte) {
cmd[0] = winmonFrame
cmd[1] = commandGetRAMVarInfo
cmd[2] = in
m.sendCommand(cmd)
m.sendMonitoringCommand(cmd)
}
func int16Abs(in int16) uint16 {
@@ -648,7 +1074,7 @@ func (m *mk2Ser) versionDecode(frame []byte) {
cmd := make([]byte, 2)
cmd[0] = infoReqFrame
cmd[1] = infoReqAddrDC
m.sendCommand(cmd)
m.sendMonitoringCommand(cmd)
}
}
@@ -704,7 +1130,7 @@ func (m *mk2Ser) dcDecode(frame []byte) {
cmd := make([]byte, 2)
cmd[0] = infoReqFrame
cmd[1] = infoReqAddrACL1
m.sendCommand(cmd)
m.sendMonitoringCommand(cmd)
}
// Decodes AC frame.
@@ -720,7 +1146,7 @@ func (m *mk2Ser) acDecode(frame []byte) {
// Send status request
cmd := make([]byte, 1)
cmd[0] = ledFrame
m.sendCommand(cmd)
m.sendMonitoringCommand(cmd)
}
func (m *mk2Ser) calcFreq(data byte, scaleIndex int) float64 {
@@ -739,14 +1165,21 @@ func (m *mk2Ser) stateDecode(frame []byte) {
// Decode the LED state frame.
func (m *mk2Ser) ledDecode(frame []byte) {
m.info.LEDs = getLEDs(frame[0], frame[1])
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.sendCommand(cmd)
m.sendMonitoringCommand(cmd)
}
func (m *mk2Ser) setLEDState(ledsOn, ledsBlink byte) {
m.info.LEDs = getLEDs(ledsOn, ledsBlink)
}
// Adds active LEDs to list.

View File

@@ -50,6 +50,31 @@ func NewIOStub(readBuffer []byte) io.ReadWriter {
}
}
func buildTestFrame(frameType byte, payload ...byte) (byte, []byte) {
length := byte(len(payload) + 1)
sum := int(length) + int(frameType)
for _, b := range payload {
sum += int(b)
}
checksum := byte((-sum) & 0xff)
frame := append([]byte{frameType}, payload...)
frame = append(frame, checksum)
return length, frame
}
func buildSentCommand(payload ...byte) []byte {
length := byte(len(payload) + 1)
sum := int(length) + int(frameHeader)
out := make([]byte, 0, len(payload)+3)
out = append(out, length, frameHeader)
for _, b := range payload {
sum += int(b)
out = append(out, b)
}
out = append(out, byte((-sum)&0xff))
return out
}
// Test a know sequence as reference as extracted from Mk2
func TestSync(t *testing.T) {
tests := []struct {
@@ -320,6 +345,29 @@ func Test_mk2Ser_WriteSetting(t *testing.T) {
}
go func() {
time.Sleep(10 * time.Millisecond)
m.pushWriteResponse(commandWriteViaIDResponse)
}()
err := m.WriteSetting(0x1234, 1234)
assert.NoError(t, err)
expected := []byte{
0x07, 0xff, 0x57, 0x37, 0x34, 0x12, 0xd2, 0x04, 0x50,
}
assert.Equal(t, expected, writeBuffer.Bytes())
}
func Test_mk2Ser_WriteSetting_FallbackLegacy(t *testing.T) {
testIO := NewIOStub(nil)
m := &mk2Ser{
p: testIO,
writeAck: make(chan byte, 2),
}
go func() {
time.Sleep(10 * time.Millisecond)
m.pushWriteResponse(commandUnsupportedResponse)
time.Sleep(10 * time.Millisecond)
m.pushWriteResponse(commandWriteSettingResponse)
}()
@@ -328,12 +376,254 @@ func Test_mk2Ser_WriteSetting(t *testing.T) {
assert.NoError(t, err)
expected := []byte{
0x07, 0xff, 0x57, 0x37, 0x34, 0x12, 0xd2, 0x04, 0x50,
0x05, 0xff, 0x57, 0x33, 0x34, 0x12, 0x2c,
0x05, 0xff, 0x57, 0x34, 0xd2, 0x04, 0x9b,
}
assert.Equal(t, expected, writeBuffer.Bytes())
}
func Test_mk2Ser_WriteRAMVar(t *testing.T) {
testIO := NewIOStub(nil)
m := &mk2Ser{
p: testIO,
writeAck: make(chan byte, 1),
}
go func() {
time.Sleep(10 * time.Millisecond)
m.pushWriteResponse(commandWriteRAMViaIDResponse)
}()
err := m.WriteRAMVar(0x000d, 1)
assert.NoError(t, err)
expected := buildSentCommand(winmonFrame, commandWriteRAMViaID, 0x0d, 0x00, 0x01, 0x00)
assert.Equal(t, expected, writeBuffer.Bytes())
}
func Test_mk2Ser_WriteRAMVar_FallbackLegacy(t *testing.T) {
testIO := NewIOStub(nil)
m := &mk2Ser{
p: testIO,
writeAck: make(chan byte, 2),
}
go func() {
time.Sleep(10 * time.Millisecond)
m.pushWriteResponse(commandUnsupportedResponse)
time.Sleep(10 * time.Millisecond)
m.pushWriteResponse(commandWriteRAMResponse)
}()
err := m.WriteRAMVar(0x000d, 1)
assert.NoError(t, err)
expected := append([]byte{}, buildSentCommand(winmonFrame, commandWriteRAMViaID, 0x0d, 0x00, 0x01, 0x00)...)
expected = append(expected, buildSentCommand(winmonFrame, commandWriteRAMVar, 0x0d, 0x00)...)
expected = append(expected, buildSentCommand(winmonFrame, commandWriteData, 0x01, 0x00)...)
assert.Equal(t, expected, writeBuffer.Bytes())
}
func Test_mk2Ser_WriteSettingByID(t *testing.T) {
testIO := NewIOStub(nil)
m := &mk2Ser{
p: testIO,
writeAck: make(chan byte, 1),
}
go func() {
time.Sleep(10 * time.Millisecond)
m.pushWriteResponse(commandWriteViaIDResponse)
}()
err := m.WriteSettingByID(0x1234, 1234)
assert.NoError(t, err)
expected := buildSentCommand(winmonFrame, commandWriteViaID, 0x34, 0x12, 0xd2, 0x04)
assert.Equal(t, expected, writeBuffer.Bytes())
}
func Test_mk2Ser_WriteRAMVarByID(t *testing.T) {
testIO := NewIOStub(nil)
m := &mk2Ser{
p: testIO,
writeAck: make(chan byte, 1),
}
go func() {
time.Sleep(10 * time.Millisecond)
m.pushWriteResponse(commandWriteRAMViaIDResponse)
}()
err := m.WriteRAMVarByID(0x000d, 1)
assert.NoError(t, err)
expected := buildSentCommand(winmonFrame, commandWriteRAMViaID, 0x0d, 0x00, 0x01, 0x00)
assert.Equal(t, expected, writeBuffer.Bytes())
}
func Test_mk2Ser_GetDeviceState(t *testing.T) {
testIO := NewIOStub(nil)
m := &mk2Ser{
p: testIO,
winmonAck: make(chan winmonResponse, 1),
}
go func() {
time.Sleep(10 * time.Millisecond)
m.pushWinmonResponse(commandSetStateResponse, []byte{0x00, byte(DeviceStateOn)})
}()
state, err := m.GetDeviceState()
assert.NoError(t, err)
assert.Equal(t, DeviceStateOn, state)
expected := buildSentCommand(winmonFrame, commandSetState, 0x00)
assert.Equal(t, expected, writeBuffer.Bytes())
}
func Test_mk2Ser_SetDeviceState(t *testing.T) {
testIO := NewIOStub(nil)
m := &mk2Ser{
p: testIO,
winmonAck: make(chan winmonResponse, 1),
}
go func() {
time.Sleep(10 * time.Millisecond)
m.pushWinmonResponse(commandSetStateResponse, []byte{0x00, byte(DeviceStateOff)})
}()
err := m.SetDeviceState(DeviceStateOff)
assert.NoError(t, err)
expected := buildSentCommand(winmonFrame, commandSetState, 0x00, byte(DeviceStateOff))
assert.Equal(t, expected, writeBuffer.Bytes())
}
func Test_mk2Ser_ReadRAMVarByID(t *testing.T) {
testIO := NewIOStub(nil)
m := &mk2Ser{
p: testIO,
winmonAck: make(chan winmonResponse, 1),
}
go func() {
time.Sleep(10 * time.Millisecond)
m.pushWinmonResponse(commandReadRAMResponse, []byte{0x34, 0x12})
}()
value, err := m.ReadRAMVarByID(0x0021)
assert.NoError(t, err)
assert.Equal(t, int16(0x1234), value)
expected := buildSentCommand(winmonFrame, commandReadRAMVar, 0x21, 0x00)
assert.Equal(t, expected, writeBuffer.Bytes())
}
func Test_mk2Ser_ReadSettingByID(t *testing.T) {
testIO := NewIOStub(nil)
m := &mk2Ser{
p: testIO,
winmonAck: make(chan winmonResponse, 1),
}
go func() {
time.Sleep(10 * time.Millisecond)
m.pushWinmonResponse(commandReadSettingResponse, []byte{0xcd, 0xab})
}()
value, err := m.ReadSettingByID(0x0042)
assert.NoError(t, err)
assert.Equal(t, int16(-21555), value)
expected := buildSentCommand(winmonFrame, commandReadSetting, 0x42, 0x00)
assert.Equal(t, expected, writeBuffer.Bytes())
}
func Test_mk2Ser_ReadSelected(t *testing.T) {
testIO := NewIOStub(nil)
m := &mk2Ser{
p: testIO,
winmonAck: make(chan winmonResponse, 1),
}
go func() {
time.Sleep(10 * time.Millisecond)
m.pushWinmonResponse(commandReadSelectedResponse, []byte{0x78, 0x56})
}()
value, err := m.ReadSelected()
assert.NoError(t, err)
assert.Equal(t, int16(0x5678), value)
expected := buildSentCommand(winmonFrame, commandReadSelected)
assert.Equal(t, expected, writeBuffer.Bytes())
}
func Test_mk2Ser_SelectRAMVar(t *testing.T) {
testIO := NewIOStub(nil)
m := &mk2Ser{
p: testIO,
winmonAck: make(chan winmonResponse, 1),
}
go func() {
time.Sleep(10 * time.Millisecond)
m.pushWinmonResponse(commandReadRAMResponse, []byte{0x11, 0x22})
}()
err := m.SelectRAMVar(0x0022)
assert.NoError(t, err)
expected := buildSentCommand(winmonFrame, commandReadRAMVar, 0x22, 0x00)
assert.Equal(t, expected, writeBuffer.Bytes())
}
func Test_mk2Ser_SelectSetting(t *testing.T) {
testIO := NewIOStub(nil)
m := &mk2Ser{
p: testIO,
winmonAck: make(chan winmonResponse, 1),
}
go func() {
time.Sleep(10 * time.Millisecond)
m.pushWinmonResponse(commandReadSettingResponse, []byte{0x11, 0x22})
}()
err := m.SelectSetting(0x0023)
assert.NoError(t, err)
expected := buildSentCommand(winmonFrame, commandReadSetting, 0x23, 0x00)
assert.Equal(t, expected, writeBuffer.Bytes())
}
func Test_mk2Ser_ReadRAMVarInfo(t *testing.T) {
testIO := NewIOStub(nil)
m := &mk2Ser{
p: testIO,
winmonAck: make(chan winmonResponse, 1),
}
go func() {
time.Sleep(10 * time.Millisecond)
m.pushWinmonResponse(commandGetRAMVarInfoResponse, []byte{0x9c, 0x7f, 0x00, 0x8f, 0x00})
}()
info, err := m.ReadRAMVarInfo(0x0001)
assert.NoError(t, err)
assert.True(t, info.Supported)
assert.Equal(t, uint16(0x0001), info.ID)
assert.Equal(t, int16(0x7f9c), info.Scale)
assert.Equal(t, int16(0x008f), info.Offset)
assert.InDelta(t, 0.01, info.Factor, testDelta)
expected := buildSentCommand(winmonFrame, commandGetRAMVarInfo, 0x01, 0x00)
assert.Equal(t, expected, writeBuffer.Bytes())
}
func Test_mk2Ser_WriteRAMVarRejected(t *testing.T) {
testIO := NewIOStub(nil)
m := &mk2Ser{
@@ -435,3 +725,54 @@ func Test_mk2Ser_SetStandby_Disabled(t *testing.T) {
}
assert.Equal(t, expected, writeBuffer.Bytes())
}
func Test_parseFrameLength(t *testing.T) {
tests := []struct {
name string
raw byte
expectedLen byte
expectedFlag bool
}{
{
name: "normal length",
raw: 0x07,
expectedLen: 0x07,
expectedFlag: false,
},
{
name: "length with LED flag",
raw: 0x86,
expectedLen: 0x06,
expectedFlag: true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
length, hasLEDStatus := parseFrameLength(tt.raw)
assert.Equal(t, tt.expectedLen, length)
assert.Equal(t, tt.expectedFlag, hasLEDStatus)
})
}
}
func Test_mk2Ser_handleFrame_StateFrameWithAppendedLED(t *testing.T) {
m := &mk2Ser{
info: &Mk2Info{},
stateAck: make(chan struct{}, 1),
}
length, frame := buildTestFrame(frameHeader, stateFrame)
m.handleFrame(length, frame, []byte{0x03, 0x00})
select {
case <-m.stateAck:
default:
t.Fatal("expected state acknowledgement")
}
assert.NotNil(t, m.info.LEDs)
assert.Equal(t, LedOn, m.info.LEDs[LedMain])
assert.Equal(t, LedOn, m.info.LEDs[LedAbsorption])
assert.Equal(t, LedOff, m.info.LEDs[LedBulk])
}

View File

@@ -102,3 +102,57 @@ type SettingsWriter interface {
// When enabled, the inverter is prevented from sleeping while switched off.
SetStandby(enabled bool) error
}
type DeviceState byte
const (
// DeviceStateChargerOnly enables charging only.
DeviceStateChargerOnly DeviceState = 0x02
// DeviceStateInverterOnly enables inverter output and disables charging.
DeviceStateInverterOnly DeviceState = 0x03
// DeviceStateOn enables both inverter and charger.
DeviceStateOn DeviceState = 0x04
// DeviceStateOff disables inverter and charger.
DeviceStateOff DeviceState = 0x05
)
var DeviceStateNames = map[DeviceState]string{
DeviceStateChargerOnly: "charger_only",
DeviceStateInverterOnly: "inverter_only",
DeviceStateOn: "on",
DeviceStateOff: "off",
}
type RAMVarInfo struct {
ID uint16
Scale int16
Offset int16
Factor float64
Signed bool
Supported bool
}
// ProtocolControl exposes protocol 3.14 command paths for direct MK2 control.
type ProtocolControl interface {
SettingsWriter
// GetDeviceState returns the current VE.Bus state using command 0x0E.
GetDeviceState() (DeviceState, error)
// SetDeviceState sets the VE.Bus state using command 0x0E.
SetDeviceState(state DeviceState) error
// ReadRAMVarByID reads a RAM variable via command 0x30.
ReadRAMVarByID(id uint16) (int16, error)
// ReadSettingByID reads a setting via command 0x31.
ReadSettingByID(id uint16) (int16, error)
// SelectRAMVar selects a RAM variable for follow-up read-selected/write-selected paths.
SelectRAMVar(id uint16) error
// SelectSetting selects a setting for follow-up read-selected/write-selected paths.
SelectSetting(id uint16) error
// ReadSelected reads the currently selected value via command 0x35.
ReadSelected() (int16, error)
// ReadRAMVarInfo reads RAM variable metadata via command 0x36.
ReadRAMVarInfo(id uint16) (RAMVarInfo, error)
// WriteSettingByID writes a setting via command 0x37.
WriteSettingByID(id uint16, value int16) error
// WriteRAMVarByID writes a RAM variable via command 0x38.
WriteRAMVarByID(id uint16, value int16) error
}

View File

@@ -8,6 +8,8 @@ type mock struct {
c chan *Mk2Info
}
var _ ProtocolControl = (*mock)(nil)
func NewMk2Mock() Mk2 {
tmp := &mock{
c: make(chan *Mk2Info, 1),
@@ -53,6 +55,49 @@ func (m *mock) SetStandby(_ bool) error {
return nil
}
func (m *mock) GetDeviceState() (DeviceState, error) {
return DeviceStateOn, nil
}
func (m *mock) SetDeviceState(_ DeviceState) error {
return nil
}
func (m *mock) ReadRAMVarByID(_ uint16) (int16, error) {
return 0, nil
}
func (m *mock) ReadSettingByID(_ uint16) (int16, error) {
return 0, nil
}
func (m *mock) SelectRAMVar(_ uint16) error {
return nil
}
func (m *mock) SelectSetting(_ uint16) error {
return nil
}
func (m *mock) ReadSelected() (int16, error) {
return 0, nil
}
func (m *mock) ReadRAMVarInfo(id uint16) (RAMVarInfo, error) {
return RAMVarInfo{
ID: id,
Supported: false,
}, nil
}
func (m *mock) WriteSettingByID(_ uint16, _ int16) error {
return nil
}
func (m *mock) WriteRAMVarByID(_ uint16, _ int16) error {
return nil
}
func (m *mock) genMockValues() {
mult := 1.0
ledState := LedOff