feat: Enhance MK2 driver with device state management and improved command handling
All checks were successful
continuous-integration/drone/push Build is passing
All checks were successful
continuous-integration/drone/push Build is passing
This commit is contained in:
505
mk2driver/mk2.go
505
mk2driver/mk2.go
@@ -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.
|
||||
|
||||
@@ -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])
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user