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:
@@ -47,6 +47,7 @@ steps:
|
|||||||
registry: registry.coadcorp.com
|
registry: registry.coadcorp.com
|
||||||
repo: registry.coadcorp.com/nathan/invertergui
|
repo: registry.coadcorp.com/nathan/invertergui
|
||||||
dockerfile: Dockerfile
|
dockerfile: Dockerfile
|
||||||
|
buildx_options_semicolon: --platform=linux/amd64,linux/arm64;--provenance=false
|
||||||
username: nathan
|
username: nathan
|
||||||
password:
|
password:
|
||||||
from_secret: registry_password
|
from_secret: registry_password
|
||||||
@@ -62,6 +63,7 @@ steps:
|
|||||||
registry: registry.coadcorp.com
|
registry: registry.coadcorp.com
|
||||||
repo: registry.coadcorp.com/nathan/invertergui
|
repo: registry.coadcorp.com/nathan/invertergui
|
||||||
dockerfile: Dockerfile
|
dockerfile: Dockerfile
|
||||||
|
buildx_options_semicolon: --platform=linux/amd64,linux/arm64;--provenance=false
|
||||||
username: nathan
|
username: nathan
|
||||||
password:
|
password:
|
||||||
from_secret: registry_password
|
from_secret: registry_password
|
||||||
@@ -80,6 +82,7 @@ steps:
|
|||||||
registry: registry.coadcorp.com
|
registry: registry.coadcorp.com
|
||||||
repo: registry.coadcorp.com/nathan/invertergui
|
repo: registry.coadcorp.com/nathan/invertergui
|
||||||
dockerfile: Dockerfile
|
dockerfile: Dockerfile
|
||||||
|
buildx_options_semicolon: --platform=linux/amd64,linux/arm64;--provenance=false
|
||||||
username: nathan
|
username: nathan
|
||||||
password:
|
password:
|
||||||
from_secret: registry_password
|
from_secret: registry_password
|
||||||
|
|||||||
@@ -6,6 +6,8 @@ The invertergui allows the monitoring of a [Victron Multiplus](https://www.victr
|
|||||||
|
|
||||||
The [`registry.coadcorp.com/nathan/invertergui`](https://registry.coadcorp.com/nathan/invertergui) container image is a build of this repository.
|
The [`registry.coadcorp.com/nathan/invertergui`](https://registry.coadcorp.com/nathan/invertergui) container image is a build of this repository.
|
||||||
|
|
||||||
|
The code has been updated to support more of the protocol published by Victron at https://www.victronenergy.com/upload/documents/Technical-Information-Interfacing-with-VE-Bus-products-MK2-Protocol-3-14.pdf
|
||||||
|
|
||||||
## Acknowledgements
|
## Acknowledgements
|
||||||
|
|
||||||
This project is based on the original open source `invertergui` project by Hendrik van Wyk and contributors:
|
This project is based on the original open source `invertergui` project by Hendrik van Wyk and contributors:
|
||||||
@@ -76,6 +78,9 @@ services:
|
|||||||
image: registry.coadcorp.com/nathan/invertergui:latest
|
image: registry.coadcorp.com/nathan/invertergui:latest
|
||||||
environment:
|
environment:
|
||||||
READ_ONLY: "true"
|
READ_ONLY: "true"
|
||||||
|
devices:
|
||||||
|
- "/dev/ttyUSB0:/dev/ttyUSB0"
|
||||||
|
command: ["--mqtt.enabled", "--mqtt.broker=tcp://192.168.1.1:1883", "--loglevel=info"]
|
||||||
```
|
```
|
||||||
|
|
||||||
## Port 8080
|
## Port 8080
|
||||||
|
|||||||
505
mk2driver/mk2.go
505
mk2driver/mk2.go
@@ -6,6 +6,7 @@ import (
|
|||||||
"io"
|
"io"
|
||||||
"math"
|
"math"
|
||||||
"sync"
|
"sync"
|
||||||
|
"sync/atomic"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/sirupsen/logrus"
|
"github.com/sirupsen/logrus"
|
||||||
@@ -47,6 +48,8 @@ const (
|
|||||||
infoFrameHeader = 0x20
|
infoFrameHeader = 0x20
|
||||||
frameHeader = 0xff
|
frameHeader = 0xff
|
||||||
bootupFrameHeader = 0x0
|
bootupFrameHeader = 0x0
|
||||||
|
frameLengthLEDBit = 0x80
|
||||||
|
frameLEDBytes = 2
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
@@ -79,22 +82,42 @@ const (
|
|||||||
|
|
||||||
// winmon frame commands
|
// winmon frame commands
|
||||||
const (
|
const (
|
||||||
|
commandSetState = 0x0E
|
||||||
commandReadRAMVar = 0x30
|
commandReadRAMVar = 0x30
|
||||||
|
commandReadSetting = 0x31
|
||||||
commandWriteRAMVar = 0x32
|
commandWriteRAMVar = 0x32
|
||||||
commandWriteSetting = 0x33
|
commandWriteSetting = 0x33
|
||||||
commandWriteData = 0x34
|
commandWriteData = 0x34
|
||||||
|
commandReadSelected = 0x35
|
||||||
commandGetRAMVarInfo = 0x36
|
commandGetRAMVarInfo = 0x36
|
||||||
|
commandWriteViaID = 0x37
|
||||||
|
commandWriteRAMViaID = 0x38
|
||||||
|
|
||||||
commandUnsupportedResponse = 0x80
|
commandUnsupportedResponse = 0x80
|
||||||
commandReadRAMResponse = 0x85
|
commandReadRAMResponse = 0x85
|
||||||
|
commandReadSettingResponse = 0x86
|
||||||
commandWriteRAMResponse = 0x87
|
commandWriteRAMResponse = 0x87
|
||||||
commandWriteSettingResponse = 0x88
|
commandWriteSettingResponse = 0x88
|
||||||
commandWriteNotAllowedResponse = 0x9B
|
commandSetStateResponse = 0x89
|
||||||
|
commandReadSelectedResponse = 0x8A
|
||||||
|
commandWriteViaIDResponse = 0x8B
|
||||||
|
commandWriteRAMViaIDResponse = 0x8C
|
||||||
commandGetRAMVarInfoResponse = 0x8E
|
commandGetRAMVarInfoResponse = 0x8E
|
||||||
|
commandWriteNotAllowedResponse = 0x9B
|
||||||
)
|
)
|
||||||
|
|
||||||
const writeResponseTimeout = 3 * time.Second
|
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 {
|
type mk2Ser struct {
|
||||||
info *Mk2Info
|
info *Mk2Info
|
||||||
p io.ReadWriter
|
p io.ReadWriter
|
||||||
@@ -104,12 +127,43 @@ type mk2Ser struct {
|
|||||||
frameLock bool
|
frameLock bool
|
||||||
infochan chan *Mk2Info
|
infochan chan *Mk2Info
|
||||||
commandMu sync.Mutex
|
commandMu sync.Mutex
|
||||||
|
pollPaused atomic.Bool
|
||||||
writeAck chan byte
|
writeAck chan byte
|
||||||
|
winmonAck chan winmonResponse
|
||||||
stateAck chan struct{}
|
stateAck chan struct{}
|
||||||
ifaceAck chan byte
|
ifaceAck chan byte
|
||||||
wg sync.WaitGroup
|
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) {
|
func NewMk2Connection(dev io.ReadWriter) (Mk2, error) {
|
||||||
mk2 := &mk2Ser{}
|
mk2 := &mk2Ser{}
|
||||||
mk2.p = dev
|
mk2.p = dev
|
||||||
@@ -118,6 +172,7 @@ func NewMk2Connection(dev io.ReadWriter) (Mk2, error) {
|
|||||||
mk2.frameLock = false
|
mk2.frameLock = false
|
||||||
mk2.scales = make([]scaling, 0, ramVarMaxOffset)
|
mk2.scales = make([]scaling, 0, ramVarMaxOffset)
|
||||||
mk2.writeAck = make(chan byte, 4)
|
mk2.writeAck = make(chan byte, 4)
|
||||||
|
mk2.winmonAck = make(chan winmonResponse, 32)
|
||||||
mk2.stateAck = make(chan struct{}, 1)
|
mk2.stateAck = make(chan struct{}, 1)
|
||||||
mk2.ifaceAck = make(chan byte, 1)
|
mk2.ifaceAck = make(chan byte, 1)
|
||||||
mk2.setTarget()
|
mk2.setTarget()
|
||||||
@@ -132,7 +187,8 @@ func NewMk2Connection(dev io.ReadWriter) (Mk2, error) {
|
|||||||
// Locks to incoming frame.
|
// Locks to incoming frame.
|
||||||
func (m *mk2Ser) frameLocker() {
|
func (m *mk2Ser) frameLocker() {
|
||||||
frame := make([]byte, 256)
|
frame := make([]byte, 256)
|
||||||
var frameLength byte
|
ledStatus := make([]byte, frameLEDBytes)
|
||||||
|
var frameLengthRaw byte
|
||||||
for {
|
for {
|
||||||
select {
|
select {
|
||||||
case <-m.run:
|
case <-m.run:
|
||||||
@@ -141,8 +197,14 @@ func (m *mk2Ser) frameLocker() {
|
|||||||
default:
|
default:
|
||||||
}
|
}
|
||||||
if m.frameLock {
|
if m.frameLock {
|
||||||
frameLength = m.readByte()
|
frameLengthRaw = m.readByte()
|
||||||
|
frameLength, hasLEDStatus := parseFrameLength(frameLengthRaw)
|
||||||
frameLengthOffset := int(frameLength) + 1
|
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])
|
l, err := io.ReadFull(m.p, frame[:frameLengthOffset])
|
||||||
if err != nil {
|
if err != nil {
|
||||||
m.addError(fmt.Errorf("Read Error: %v", err))
|
m.addError(fmt.Errorf("Read Error: %v", err))
|
||||||
@@ -151,12 +213,26 @@ func (m *mk2Ser) frameLocker() {
|
|||||||
m.addError(errors.New("Read Length Error"))
|
m.addError(errors.New("Read Length Error"))
|
||||||
m.frameLock = false
|
m.frameLock = false
|
||||||
} else {
|
} 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 {
|
} else {
|
||||||
tmp := m.readByte()
|
tmp := m.readByte()
|
||||||
|
frameLength, hasLEDStatus := parseFrameLength(frameLengthRaw)
|
||||||
frameLengthOffset := int(frameLength)
|
frameLengthOffset := int(frameLength)
|
||||||
if tmp == frameHeader || tmp == infoFrameHeader {
|
if tmp == frameHeader || tmp == infoFrameHeader {
|
||||||
|
if frameLengthOffset > len(frame) {
|
||||||
|
frameLengthRaw = tmp
|
||||||
|
continue
|
||||||
|
}
|
||||||
l, err := io.ReadFull(m.p, frame[:frameLengthOffset])
|
l, err := io.ReadFull(m.p, frame[:frameLengthOffset])
|
||||||
if err != nil {
|
if err != nil {
|
||||||
m.addError(fmt.Errorf("Read Error: %v", err))
|
m.addError(fmt.Errorf("Read Error: %v", err))
|
||||||
@@ -164,13 +240,20 @@ func (m *mk2Ser) frameLocker() {
|
|||||||
} else if l != frameLengthOffset {
|
} else if l != frameLengthOffset {
|
||||||
m.addError(errors.New("Read Length Error"))
|
m.addError(errors.New("Read Length Error"))
|
||||||
} else {
|
} 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]) {
|
if checkChecksum(frameLength, tmp, frame[:frameLengthOffset]) {
|
||||||
m.frameLock = true
|
m.frameLock = true
|
||||||
mk2log.Info("Frame lock acquired")
|
mk2log.Info("Frame lock acquired")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
frameLength = tmp
|
frameLengthRaw = tmp
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -192,7 +275,22 @@ func (m *mk2Ser) WriteRAMVar(id uint16, value int16) error {
|
|||||||
"id": id,
|
"id": id,
|
||||||
"value": value,
|
"value": value,
|
||||||
}).Info("WriteRAMVar requested")
|
}).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 {
|
if err != nil {
|
||||||
mk2log.WithError(err).WithField("id", id).Error("WriteRAMVar failed")
|
mk2log.WithError(err).WithField("id", id).Error("WriteRAMVar failed")
|
||||||
return err
|
return err
|
||||||
@@ -206,7 +304,22 @@ func (m *mk2Ser) WriteSetting(id uint16, value int16) error {
|
|||||||
"id": id,
|
"id": id,
|
||||||
"value": value,
|
"value": value,
|
||||||
}).Info("WriteSetting requested")
|
}).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 {
|
if err != nil {
|
||||||
mk2log.WithError(err).WithField("id", id).Error("WriteSetting failed")
|
mk2log.WithError(err).WithField("id", id).Error("WriteSetting failed")
|
||||||
return err
|
return err
|
||||||
@@ -215,6 +328,125 @@ func (m *mk2Ser) WriteSetting(id uint16, value int16) error {
|
|||||||
return nil
|
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 {
|
func (m *mk2Ser) SetPanelState(switchState PanelSwitchState, currentLimitA *float64) error {
|
||||||
if !validPanelSwitchState(switchState) {
|
if !validPanelSwitchState(switchState) {
|
||||||
return fmt.Errorf("invalid panel switch state: %d", switchState)
|
return fmt.Errorf("invalid panel switch state: %d", switchState)
|
||||||
@@ -225,8 +457,8 @@ func (m *mk2Ser) SetPanelState(switchState PanelSwitchState, currentLimitA *floa
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
m.commandMu.Lock()
|
m.beginCommand()
|
||||||
defer m.commandMu.Unlock()
|
defer m.endCommand()
|
||||||
|
|
||||||
logEntry := mk2log.WithField("switch_state", switchState)
|
logEntry := mk2log.WithField("switch_state", switchState)
|
||||||
if currentLimitA != nil {
|
if currentLimitA != nil {
|
||||||
@@ -259,8 +491,8 @@ func (m *mk2Ser) SetStandby(enabled bool) error {
|
|||||||
lineState |= interfaceStandbyFlag
|
lineState |= interfaceStandbyFlag
|
||||||
}
|
}
|
||||||
|
|
||||||
m.commandMu.Lock()
|
m.beginCommand()
|
||||||
defer m.commandMu.Unlock()
|
defer m.endCommand()
|
||||||
logEntry := mk2log.WithField("standby_enabled", enabled)
|
logEntry := mk2log.WithField("standby_enabled", enabled)
|
||||||
logEntry.Info("SetStandby requested")
|
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) {
|
func encodePanelCurrentLimit(currentLimitA *float64) (uint16, error) {
|
||||||
if currentLimitA == nil {
|
if currentLimitA == nil {
|
||||||
return panelCurrentLimitUnknown, nil
|
return panelCurrentLimitUnknown, nil
|
||||||
@@ -303,26 +551,29 @@ func encodePanelCurrentLimit(currentLimitA *float64) (uint16, error) {
|
|||||||
return uint16(raw), nil
|
return uint16(raw), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m *mk2Ser) writeByID(selectCommand, expectedResponse byte, id uint16, value int16) error {
|
func (m *mk2Ser) writeByIDOnly(writeCommand, 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")
|
|
||||||
|
|
||||||
m.clearWriteResponses()
|
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{
|
m.sendCommandLocked([]byte{
|
||||||
winmonFrame,
|
winmonFrame,
|
||||||
selectCommand,
|
selectCommand,
|
||||||
byte(id),
|
byte(id),
|
||||||
byte(id >> 8),
|
byte(id >> 8),
|
||||||
})
|
})
|
||||||
|
|
||||||
rawValue := uint16(value)
|
|
||||||
m.sendCommandLocked([]byte{
|
m.sendCommandLocked([]byte{
|
||||||
winmonFrame,
|
winmonFrame,
|
||||||
commandWriteData,
|
commandWriteData,
|
||||||
@@ -333,6 +584,77 @@ func (m *mk2Ser) writeByID(selectCommand, expectedResponse byte, id uint16, valu
|
|||||||
return m.waitForWriteResponse(expectedResponse)
|
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() {
|
func (m *mk2Ser) clearWriteResponses() {
|
||||||
if m.writeAck == nil {
|
if m.writeAck == nil {
|
||||||
m.writeAck = make(chan byte, 4)
|
m.writeAck = make(chan byte, 4)
|
||||||
@@ -362,9 +684,9 @@ func (m *mk2Ser) waitForWriteResponse(expectedResponse byte) error {
|
|||||||
case expectedResponse:
|
case expectedResponse:
|
||||||
return nil
|
return nil
|
||||||
case commandUnsupportedResponse:
|
case commandUnsupportedResponse:
|
||||||
return errors.New("write command is not supported by this device firmware")
|
return errCommandUnsupported
|
||||||
case commandWriteNotAllowedResponse:
|
case commandWriteNotAllowedResponse:
|
||||||
return errors.New("write command rejected by device access level")
|
return errWriteRejected
|
||||||
default:
|
default:
|
||||||
return fmt.Errorf("unexpected write response 0x%02x", response)
|
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() {
|
func (m *mk2Ser) clearStateResponses() {
|
||||||
if m.stateAck == nil {
|
if m.stateAck == nil {
|
||||||
m.stateAck = make(chan struct{}, 1)
|
m.stateAck = make(chan struct{}, 1)
|
||||||
@@ -515,41 +898,84 @@ func (m *mk2Ser) updateReport() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Checks for valid frame and chooses decoding.
|
// 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)
|
mk2log.Debugf("[handleFrame] frame %#v", frame)
|
||||||
|
if len(frame) == 0 {
|
||||||
|
mk2log.Warn("[handleFrame] empty frame")
|
||||||
|
return
|
||||||
|
}
|
||||||
if checkChecksum(l, frame[0], frame[1:]) {
|
if checkChecksum(l, frame[0], frame[1:]) {
|
||||||
switch frame[0] {
|
switch frame[0] {
|
||||||
case bootupFrameHeader:
|
case bootupFrameHeader:
|
||||||
|
if m.pollPaused.Load() {
|
||||||
|
mk2log.Debug("Skipping setTarget during active control transaction")
|
||||||
|
return
|
||||||
|
}
|
||||||
m.setTarget()
|
m.setTarget()
|
||||||
case frameHeader:
|
case frameHeader:
|
||||||
|
if len(frame) < 2 {
|
||||||
|
mk2log.Warnf("[handleFrame] truncated frameHeader frame: %#v", frame)
|
||||||
|
return
|
||||||
|
}
|
||||||
switch frame[1] {
|
switch frame[1] {
|
||||||
case interfaceFrame:
|
case interfaceFrame:
|
||||||
if len(frame) > 2 {
|
if len(frame) > 2 {
|
||||||
m.pushInterfaceResponse(frame[2])
|
m.pushInterfaceResponse(frame[2])
|
||||||
}
|
}
|
||||||
case stateFrame:
|
case stateFrame:
|
||||||
|
if len(appendedLED) == frameLEDBytes {
|
||||||
|
m.setLEDState(appendedLED[0], appendedLED[1])
|
||||||
|
}
|
||||||
m.pushStateResponse()
|
m.pushStateResponse()
|
||||||
case vFrame:
|
case vFrame:
|
||||||
|
if len(frame) < 6 {
|
||||||
|
mk2log.Warnf("[handleFrame] truncated version frame: %#v", frame)
|
||||||
|
return
|
||||||
|
}
|
||||||
m.versionDecode(frame[2:])
|
m.versionDecode(frame[2:])
|
||||||
case winmonFrame:
|
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] {
|
switch frame[2] {
|
||||||
case commandGetRAMVarInfoResponse:
|
case commandGetRAMVarInfoResponse:
|
||||||
m.scaleDecode(frame[2:])
|
if !m.pollPaused.Load() {
|
||||||
|
m.scaleDecode(frame[2:])
|
||||||
|
}
|
||||||
case commandReadRAMResponse:
|
case commandReadRAMResponse:
|
||||||
m.stateDecode(frame[2:])
|
if !m.pollPaused.Load() {
|
||||||
case commandWriteRAMResponse, commandWriteSettingResponse, commandUnsupportedResponse, commandWriteNotAllowedResponse:
|
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])
|
m.pushWriteResponse(frame[2])
|
||||||
default:
|
default:
|
||||||
mk2log.Warnf("[handleFrame] invalid winmonFrame %v", frame[2:])
|
mk2log.Warnf("[handleFrame] invalid winmonFrame %v", frame[2:])
|
||||||
}
|
}
|
||||||
|
|
||||||
case ledFrame:
|
case ledFrame:
|
||||||
|
if len(frame) < 4 {
|
||||||
|
mk2log.Warnf("[handleFrame] truncated LED frame: %#v", frame)
|
||||||
|
return
|
||||||
|
}
|
||||||
m.ledDecode(frame[2:])
|
m.ledDecode(frame[2:])
|
||||||
default:
|
default:
|
||||||
mk2log.Warnf("[handleFrame] invalid frameHeader %v", frame[1])
|
mk2log.Warnf("[handleFrame] invalid frameHeader %v", frame[1])
|
||||||
}
|
}
|
||||||
|
|
||||||
case infoFrameHeader:
|
case infoFrameHeader:
|
||||||
|
if len(frame) < 6 {
|
||||||
|
mk2log.Warnf("[handleFrame] truncated info frame: %#v", frame)
|
||||||
|
return
|
||||||
|
}
|
||||||
switch frame[5] {
|
switch frame[5] {
|
||||||
case dcInfoFrame:
|
case dcInfoFrame:
|
||||||
m.dcDecode(frame[1:])
|
m.dcDecode(frame[1:])
|
||||||
@@ -582,7 +1008,7 @@ func (m *mk2Ser) reqScaleFactor(in byte) {
|
|||||||
cmd[0] = winmonFrame
|
cmd[0] = winmonFrame
|
||||||
cmd[1] = commandGetRAMVarInfo
|
cmd[1] = commandGetRAMVarInfo
|
||||||
cmd[2] = in
|
cmd[2] = in
|
||||||
m.sendCommand(cmd)
|
m.sendMonitoringCommand(cmd)
|
||||||
}
|
}
|
||||||
|
|
||||||
func int16Abs(in int16) uint16 {
|
func int16Abs(in int16) uint16 {
|
||||||
@@ -648,7 +1074,7 @@ func (m *mk2Ser) versionDecode(frame []byte) {
|
|||||||
cmd := make([]byte, 2)
|
cmd := make([]byte, 2)
|
||||||
cmd[0] = infoReqFrame
|
cmd[0] = infoReqFrame
|
||||||
cmd[1] = infoReqAddrDC
|
cmd[1] = infoReqAddrDC
|
||||||
m.sendCommand(cmd)
|
m.sendMonitoringCommand(cmd)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -704,7 +1130,7 @@ func (m *mk2Ser) dcDecode(frame []byte) {
|
|||||||
cmd := make([]byte, 2)
|
cmd := make([]byte, 2)
|
||||||
cmd[0] = infoReqFrame
|
cmd[0] = infoReqFrame
|
||||||
cmd[1] = infoReqAddrACL1
|
cmd[1] = infoReqAddrACL1
|
||||||
m.sendCommand(cmd)
|
m.sendMonitoringCommand(cmd)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Decodes AC frame.
|
// Decodes AC frame.
|
||||||
@@ -720,7 +1146,7 @@ func (m *mk2Ser) acDecode(frame []byte) {
|
|||||||
// Send status request
|
// Send status request
|
||||||
cmd := make([]byte, 1)
|
cmd := make([]byte, 1)
|
||||||
cmd[0] = ledFrame
|
cmd[0] = ledFrame
|
||||||
m.sendCommand(cmd)
|
m.sendMonitoringCommand(cmd)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m *mk2Ser) calcFreq(data byte, scaleIndex int) float64 {
|
func (m *mk2Ser) calcFreq(data byte, scaleIndex int) float64 {
|
||||||
@@ -739,14 +1165,21 @@ func (m *mk2Ser) stateDecode(frame []byte) {
|
|||||||
|
|
||||||
// Decode the LED state frame.
|
// Decode the LED state frame.
|
||||||
func (m *mk2Ser) ledDecode(frame []byte) {
|
func (m *mk2Ser) ledDecode(frame []byte) {
|
||||||
|
if len(frame) < 2 {
|
||||||
m.info.LEDs = getLEDs(frame[0], frame[1])
|
mk2log.Warnf("Skipping LED decode for short frame: %#v", frame)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
m.setLEDState(frame[0], frame[1])
|
||||||
// Send charge state request
|
// Send charge state request
|
||||||
cmd := make([]byte, 4)
|
cmd := make([]byte, 4)
|
||||||
cmd[0] = winmonFrame
|
cmd[0] = winmonFrame
|
||||||
cmd[1] = commandReadRAMVar
|
cmd[1] = commandReadRAMVar
|
||||||
cmd[2] = ramVarChargeState
|
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.
|
// 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
|
// Test a know sequence as reference as extracted from Mk2
|
||||||
func TestSync(t *testing.T) {
|
func TestSync(t *testing.T) {
|
||||||
tests := []struct {
|
tests := []struct {
|
||||||
@@ -320,6 +345,29 @@ func Test_mk2Ser_WriteSetting(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
go func() {
|
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)
|
time.Sleep(10 * time.Millisecond)
|
||||||
m.pushWriteResponse(commandWriteSettingResponse)
|
m.pushWriteResponse(commandWriteSettingResponse)
|
||||||
}()
|
}()
|
||||||
@@ -328,12 +376,254 @@ func Test_mk2Ser_WriteSetting(t *testing.T) {
|
|||||||
assert.NoError(t, err)
|
assert.NoError(t, err)
|
||||||
|
|
||||||
expected := []byte{
|
expected := []byte{
|
||||||
|
0x07, 0xff, 0x57, 0x37, 0x34, 0x12, 0xd2, 0x04, 0x50,
|
||||||
0x05, 0xff, 0x57, 0x33, 0x34, 0x12, 0x2c,
|
0x05, 0xff, 0x57, 0x33, 0x34, 0x12, 0x2c,
|
||||||
0x05, 0xff, 0x57, 0x34, 0xd2, 0x04, 0x9b,
|
0x05, 0xff, 0x57, 0x34, 0xd2, 0x04, 0x9b,
|
||||||
}
|
}
|
||||||
assert.Equal(t, expected, writeBuffer.Bytes())
|
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) {
|
func Test_mk2Ser_WriteRAMVarRejected(t *testing.T) {
|
||||||
testIO := NewIOStub(nil)
|
testIO := NewIOStub(nil)
|
||||||
m := &mk2Ser{
|
m := &mk2Ser{
|
||||||
@@ -435,3 +725,54 @@ func Test_mk2Ser_SetStandby_Disabled(t *testing.T) {
|
|||||||
}
|
}
|
||||||
assert.Equal(t, expected, writeBuffer.Bytes())
|
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.
|
// When enabled, the inverter is prevented from sleeping while switched off.
|
||||||
SetStandby(enabled bool) error
|
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
|
c chan *Mk2Info
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var _ ProtocolControl = (*mock)(nil)
|
||||||
|
|
||||||
func NewMk2Mock() Mk2 {
|
func NewMk2Mock() Mk2 {
|
||||||
tmp := &mock{
|
tmp := &mock{
|
||||||
c: make(chan *Mk2Info, 1),
|
c: make(chan *Mk2Info, 1),
|
||||||
@@ -53,6 +55,49 @@ func (m *mock) SetStandby(_ bool) error {
|
|||||||
return nil
|
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() {
|
func (m *mock) genMockValues() {
|
||||||
mult := 1.0
|
mult := 1.0
|
||||||
ledState := LedOff
|
ledState := LedOff
|
||||||
|
|||||||
Reference in New Issue
Block a user