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.