All checks were successful
continuous-integration/drone/push Build is passing
1789 lines
46 KiB
Go
1789 lines
46 KiB
Go
package mk2driver
|
|
|
|
import (
|
|
"errors"
|
|
"fmt"
|
|
"io"
|
|
"math"
|
|
"strings"
|
|
"sync"
|
|
"sync/atomic"
|
|
"time"
|
|
|
|
"github.com/sirupsen/logrus"
|
|
)
|
|
|
|
var mk2log = logrus.WithField("ctx", "inverter-gui-mk2driver")
|
|
|
|
type scaling struct {
|
|
scale float64
|
|
offset float64
|
|
signed bool
|
|
supported bool
|
|
}
|
|
|
|
//nolint:deadcode,varcheck
|
|
const (
|
|
ramVarVMains = iota
|
|
ramVarIMains
|
|
ramVarVInverter
|
|
ramVarIInverter
|
|
ramVarVBat
|
|
ramVarIBat
|
|
ramVarVBatRipple
|
|
ramVarInverterPeriod
|
|
ramVarMainPeriod
|
|
ramVarIACLoad
|
|
ramVarVirSwitchPos
|
|
ramVarIgnACInState
|
|
ramVarMultiFuncRelay
|
|
ramVarChargeState
|
|
ramVarInverterPower1
|
|
ramVarInverterPower2
|
|
ramVarOutPower
|
|
|
|
ramVarMaxOffset = 14
|
|
)
|
|
|
|
const (
|
|
infoFrameHeader = 0x20
|
|
frameHeader = 0xff
|
|
bootupFrameHeader = 0x0
|
|
frameLengthLEDBit = 0x80
|
|
frameLEDBytes = 2
|
|
)
|
|
|
|
const (
|
|
acL1InfoFrame = 0x08
|
|
dcInfoFrame = 0x0C
|
|
interfaceFrame = 0x48 // H
|
|
setTargetFrame = 0x41
|
|
infoReqFrame = 0x46 //F
|
|
ledFrame = 0x4C
|
|
stateFrame = 0x53 // S
|
|
vFrame = 0x56
|
|
winmonFrame = 0x57
|
|
)
|
|
|
|
const (
|
|
panelStateVariant2Flags = 0x80
|
|
|
|
interfacePanelDetectFlag = 0x01
|
|
interfaceStandbyFlag = 0x02
|
|
|
|
panelCurrentLimitUnknown = 0x8000
|
|
panelCurrentLimitMax = 0x7FFF
|
|
)
|
|
|
|
// info frame types
|
|
const (
|
|
infoReqAddrDC = 0x00
|
|
infoReqAddrACL1 = 0x01
|
|
)
|
|
|
|
// winmon frame commands
|
|
const (
|
|
commandSetState = 0x0E
|
|
commandReadRAMVar = 0x30
|
|
commandReadSetting = 0x31
|
|
commandWriteRAMVar = 0x32
|
|
commandWriteSetting = 0x33
|
|
commandWriteData = 0x34
|
|
commandReadSelected = 0x35
|
|
commandGetRAMVarInfo = 0x36
|
|
commandWriteViaID = 0x37
|
|
commandWriteRAMViaID = 0x38
|
|
|
|
commandUnsupportedResponse = 0x80
|
|
commandReadRAMResponse = 0x85
|
|
commandReadSettingResponse = 0x86
|
|
commandWriteRAMResponse = 0x87
|
|
commandWriteSettingResponse = 0x88
|
|
commandSetStateResponse = 0x89
|
|
commandReadSelectedResponse = 0x8A
|
|
commandWriteViaIDResponse = 0x8B
|
|
commandWriteRAMViaIDResponse = 0x8C
|
|
commandGetRAMVarInfoResponse = 0x8E
|
|
commandWriteNotAllowedResponse = 0x9B
|
|
)
|
|
|
|
const writeResponseTimeout = 3 * time.Second
|
|
|
|
var (
|
|
errCommandUnsupported = errors.New("command is not supported by this device firmware")
|
|
errWriteRejected = errors.New("write command rejected by device access level")
|
|
)
|
|
|
|
type winmonResponse struct {
|
|
command byte
|
|
data []byte
|
|
}
|
|
|
|
type mk2Ser struct {
|
|
info *Mk2Info
|
|
p io.ReadWriter
|
|
scales []scaling
|
|
scaleCount int
|
|
run chan struct{}
|
|
frameLock bool
|
|
infochan chan *Mk2Info
|
|
commandMu sync.Mutex
|
|
pollPaused atomic.Bool
|
|
writeAck chan byte
|
|
winmonAck chan winmonResponse
|
|
stateAck chan struct{}
|
|
ifaceAck chan byte
|
|
wg sync.WaitGroup
|
|
|
|
diagMu sync.Mutex
|
|
traceLimit int
|
|
traces []ProtocolTrace
|
|
lastFrameAt time.Time
|
|
recentErrors []string
|
|
|
|
commandTimeouts atomic.Uint64
|
|
commandFailures atomic.Uint64
|
|
checksumErrors atomic.Uint64
|
|
}
|
|
|
|
var _ ProtocolControl = (*mk2Ser)(nil)
|
|
var _ MetadataControl = (*mk2Ser)(nil)
|
|
var _ SnapshotControl = (*mk2Ser)(nil)
|
|
var _ DiagnosticsControl = (*mk2Ser)(nil)
|
|
|
|
func parseFrameLength(raw byte) (payloadLength byte, hasLEDStatus bool) {
|
|
if raw&frameLengthLEDBit != 0 {
|
|
return raw &^ frameLengthLEDBit, true
|
|
}
|
|
return raw, false
|
|
}
|
|
|
|
func (m *mk2Ser) beginCommand() {
|
|
m.commandMu.Lock()
|
|
m.pollPaused.Store(true)
|
|
}
|
|
|
|
func (m *mk2Ser) endCommand() {
|
|
m.pollPaused.Store(false)
|
|
m.commandMu.Unlock()
|
|
}
|
|
|
|
func (m *mk2Ser) sendMonitoringCommand(data []byte) {
|
|
if m.pollPaused.Load() {
|
|
if len(data) > 0 {
|
|
mk2log.WithField("command", fmt.Sprintf("0x%02x", data[0])).Debug("Skipping monitoring command during control transaction")
|
|
}
|
|
return
|
|
}
|
|
m.sendCommand(data)
|
|
}
|
|
|
|
func NewMk2Connection(dev io.ReadWriter) (Mk2, error) {
|
|
mk2 := &mk2Ser{}
|
|
mk2.p = dev
|
|
mk2.info = &Mk2Info{}
|
|
mk2.scaleCount = 0
|
|
mk2.frameLock = false
|
|
mk2.scales = make([]scaling, 0, ramVarMaxOffset)
|
|
mk2.writeAck = make(chan byte, 4)
|
|
mk2.winmonAck = make(chan winmonResponse, 32)
|
|
mk2.stateAck = make(chan struct{}, 1)
|
|
mk2.ifaceAck = make(chan byte, 1)
|
|
mk2.traceLimit = 200
|
|
mk2.traces = make([]ProtocolTrace, 0, mk2.traceLimit)
|
|
mk2.recentErrors = make([]string, 0, 20)
|
|
mk2.setTarget()
|
|
mk2.run = make(chan struct{})
|
|
mk2.infochan = make(chan *Mk2Info)
|
|
mk2.wg.Add(1)
|
|
mk2log.Info("MK2 connection initialized")
|
|
go mk2.frameLocker()
|
|
return mk2, nil
|
|
}
|
|
|
|
// Locks to incoming frame.
|
|
func (m *mk2Ser) frameLocker() {
|
|
frame := make([]byte, 256)
|
|
ledStatus := make([]byte, frameLEDBytes)
|
|
var frameLengthRaw byte
|
|
for {
|
|
select {
|
|
case <-m.run:
|
|
m.wg.Done()
|
|
return
|
|
default:
|
|
}
|
|
if m.frameLock {
|
|
frameLengthRaw = m.readByte()
|
|
frameLength, hasLEDStatus := parseFrameLength(frameLengthRaw)
|
|
frameLengthOffset := int(frameLength) + 1
|
|
if frameLengthOffset > len(frame) {
|
|
m.addError(fmt.Errorf("Read Length Error: frame length %d exceeds buffer", frameLengthOffset))
|
|
m.frameLock = false
|
|
continue
|
|
}
|
|
l, err := io.ReadFull(m.p, frame[:frameLengthOffset])
|
|
if err != nil {
|
|
m.addError(fmt.Errorf("Read Error: %v", err))
|
|
m.frameLock = false
|
|
} else if l != frameLengthOffset {
|
|
m.addError(errors.New("Read Length Error"))
|
|
m.frameLock = false
|
|
} else {
|
|
var appendedLED []byte
|
|
if hasLEDStatus {
|
|
if _, err = io.ReadFull(m.p, ledStatus); err != nil {
|
|
m.addError(fmt.Errorf("Read LED status error: %v", err))
|
|
m.frameLock = false
|
|
continue
|
|
}
|
|
appendedLED = ledStatus
|
|
}
|
|
m.handleFrame(frameLength, frame[:frameLengthOffset], appendedLED)
|
|
}
|
|
} else {
|
|
tmp := m.readByte()
|
|
frameLength, hasLEDStatus := parseFrameLength(frameLengthRaw)
|
|
frameLengthOffset := int(frameLength)
|
|
if tmp == frameHeader || tmp == infoFrameHeader {
|
|
if frameLengthOffset > len(frame) {
|
|
frameLengthRaw = tmp
|
|
continue
|
|
}
|
|
l, err := io.ReadFull(m.p, frame[:frameLengthOffset])
|
|
if err != nil {
|
|
m.addError(fmt.Errorf("Read Error: %v", err))
|
|
time.Sleep(1 * time.Second)
|
|
} else if l != frameLengthOffset {
|
|
m.addError(errors.New("Read Length Error"))
|
|
} else {
|
|
if hasLEDStatus {
|
|
if _, err = io.ReadFull(m.p, ledStatus); err != nil {
|
|
m.addError(fmt.Errorf("Read LED status error: %v", err))
|
|
frameLengthRaw = tmp
|
|
continue
|
|
}
|
|
}
|
|
if checkChecksum(frameLength, tmp, frame[:frameLengthOffset]) {
|
|
m.frameLock = true
|
|
mk2log.Info("Frame lock acquired")
|
|
}
|
|
}
|
|
}
|
|
frameLengthRaw = tmp
|
|
}
|
|
}
|
|
}
|
|
|
|
// Close Mk2
|
|
func (m *mk2Ser) Close() {
|
|
mk2log.Info("Closing MK2 connection")
|
|
close(m.run)
|
|
m.wg.Wait()
|
|
mk2log.Info("MK2 connection closed")
|
|
}
|
|
|
|
func (m *mk2Ser) C() chan *Mk2Info {
|
|
return m.infochan
|
|
}
|
|
|
|
func (m *mk2Ser) RegisterMetadata(kind RegisterKind, id uint16) (RegisterMetadata, bool) {
|
|
return lookupRegisterMetadata(kind, id)
|
|
}
|
|
|
|
func (m *mk2Ser) ListRegisterMetadata() []RegisterMetadata {
|
|
return listRegisterMetadata()
|
|
}
|
|
|
|
func (m *mk2Ser) ReadRegister(kind RegisterKind, id uint16) (int16, error) {
|
|
m.beginCommand()
|
|
defer m.endCommand()
|
|
return m.readRegisterLocked(kind, id)
|
|
}
|
|
|
|
func (m *mk2Ser) WriteRegister(kind RegisterKind, id uint16, value int16, opts TransactionOptions) (RegisterTransactionResult, error) {
|
|
options := normalizeTransactionOptions(opts)
|
|
commandTimeout := resolveCommandTimeout(options)
|
|
start := time.Now()
|
|
result := RegisterTransactionResult{
|
|
Kind: kind,
|
|
ID: id,
|
|
TargetValue: value,
|
|
Timeout: commandTimeout,
|
|
}
|
|
|
|
if meta, ok := lookupRegisterMetadata(kind, id); ok {
|
|
if !meta.Writable {
|
|
return result, fmt.Errorf("register %s:%d (%s) is marked read-only", kind, id, meta.Name)
|
|
}
|
|
if err := validateValueAgainstMetadata(meta, value); err != nil {
|
|
return result, err
|
|
}
|
|
}
|
|
|
|
m.beginCommand()
|
|
defer m.endCommand()
|
|
|
|
if options.ReadBeforeWrite {
|
|
prev, err := m.readRegisterLockedWithTimeout(kind, id, commandTimeout)
|
|
if err != nil {
|
|
result.Duration = time.Since(start)
|
|
return result, fmt.Errorf("could not read current value for %s:%d: %w", kind, id, err)
|
|
}
|
|
result.PreviousValue = int16Ptr(prev)
|
|
}
|
|
|
|
var lastErr error
|
|
maxAttempts := options.Retries + 1
|
|
for attempt := 1; attempt <= maxAttempts; attempt++ {
|
|
result.Attempts = attempt
|
|
|
|
err := m.writeRegisterLockedWithTimeout(kind, id, value, commandTimeout)
|
|
if err != nil {
|
|
lastErr = err
|
|
if attempt < maxAttempts {
|
|
delay := retryDelayForAttempt(options, attempt)
|
|
if delay > 0 {
|
|
time.Sleep(delay)
|
|
}
|
|
continue
|
|
}
|
|
break
|
|
}
|
|
|
|
if !options.VerifyAfterWrite {
|
|
result.Duration = time.Since(start)
|
|
return result, nil
|
|
}
|
|
|
|
verified, err := m.readRegisterLockedWithTimeout(kind, id, commandTimeout)
|
|
if err != nil {
|
|
lastErr = fmt.Errorf("verification read failed for %s:%d: %w", kind, id, err)
|
|
if attempt < maxAttempts {
|
|
delay := retryDelayForAttempt(options, attempt)
|
|
if delay > 0 {
|
|
time.Sleep(delay)
|
|
}
|
|
continue
|
|
}
|
|
break
|
|
}
|
|
result.VerifiedValue = int16Ptr(verified)
|
|
if verified != value {
|
|
lastErr = fmt.Errorf("verification mismatch for %s:%d expected %d got %d", kind, id, value, verified)
|
|
if attempt < maxAttempts {
|
|
delay := retryDelayForAttempt(options, attempt)
|
|
if delay > 0 {
|
|
time.Sleep(delay)
|
|
}
|
|
continue
|
|
}
|
|
break
|
|
}
|
|
|
|
result.Duration = time.Since(start)
|
|
return result, nil
|
|
}
|
|
|
|
if lastErr == nil {
|
|
lastErr = fmt.Errorf("transaction failed for %s:%d", kind, id)
|
|
}
|
|
result.Duration = time.Since(start)
|
|
return result, lastErr
|
|
}
|
|
|
|
func (m *mk2Ser) CaptureSnapshot(addresses []RegisterAddress) (RegisterSnapshot, error) {
|
|
if len(addresses) == 0 {
|
|
addresses = defaultWritableRegisterAddresses()
|
|
}
|
|
snapshotTime := time.Now().UTC()
|
|
snapshot := RegisterSnapshot{
|
|
CapturedAt: snapshotTime,
|
|
Entries: make([]RegisterSnapshotEntry, 0, len(addresses)),
|
|
}
|
|
for _, address := range addresses {
|
|
value, err := m.ReadRegister(address.Kind, address.ID)
|
|
if err != nil {
|
|
return snapshot, fmt.Errorf("capture snapshot read failed for %s:%d: %w", address.Kind, address.ID, err)
|
|
}
|
|
meta, ok := m.RegisterMetadata(address.Kind, address.ID)
|
|
entry := RegisterSnapshotEntry{
|
|
Kind: address.Kind,
|
|
ID: address.ID,
|
|
Value: value,
|
|
CapturedAt: snapshotTime,
|
|
}
|
|
if ok {
|
|
entry.Name = meta.Name
|
|
entry.Writable = meta.Writable
|
|
entry.Safety = meta.SafetyClass
|
|
}
|
|
snapshot.Entries = append(snapshot.Entries, entry)
|
|
}
|
|
return snapshot, nil
|
|
}
|
|
|
|
func (m *mk2Ser) DiffSnapshot(snapshot RegisterSnapshot) ([]SnapshotDiff, error) {
|
|
diffs := make([]SnapshotDiff, 0, len(snapshot.Entries))
|
|
for _, entry := range snapshot.Entries {
|
|
current, err := m.ReadRegister(entry.Kind, entry.ID)
|
|
if err != nil {
|
|
return diffs, fmt.Errorf("snapshot diff read failed for %s:%d: %w", entry.Kind, entry.ID, err)
|
|
}
|
|
meta, ok := m.RegisterMetadata(entry.Kind, entry.ID)
|
|
name := entry.Name
|
|
writable := entry.Writable
|
|
safety := entry.Safety
|
|
if ok {
|
|
name = meta.Name
|
|
writable = meta.Writable
|
|
safety = meta.SafetyClass
|
|
}
|
|
diff := SnapshotDiff{
|
|
Kind: entry.Kind,
|
|
ID: entry.ID,
|
|
Name: name,
|
|
Current: current,
|
|
Target: entry.Value,
|
|
Changed: current != entry.Value,
|
|
Writable: writable,
|
|
Safety: safety,
|
|
DiffValue: int32(entry.Value) - int32(current),
|
|
}
|
|
diffs = append(diffs, diff)
|
|
}
|
|
return diffs, nil
|
|
}
|
|
|
|
func (m *mk2Ser) RestoreSnapshot(snapshot RegisterSnapshot, opts TransactionOptions) (SnapshotRestoreResult, error) {
|
|
restoreResult := SnapshotRestoreResult{
|
|
Applied: make([]RegisterTransactionResult, 0, len(snapshot.Entries)),
|
|
}
|
|
diffs, err := m.DiffSnapshot(snapshot)
|
|
if err != nil {
|
|
return restoreResult, err
|
|
}
|
|
for _, diff := range diffs {
|
|
if !diff.Changed || !diff.Writable {
|
|
continue
|
|
}
|
|
txResult, txErr := m.WriteRegister(diff.Kind, diff.ID, diff.Target, opts)
|
|
if txErr == nil {
|
|
restoreResult.Applied = append(restoreResult.Applied, txResult)
|
|
continue
|
|
}
|
|
|
|
restoreResult.RolledBack = true
|
|
rollbackErrs := m.rollbackAppliedTransactions(restoreResult.Applied, opts)
|
|
restoreResult.RollbackErrors = append(restoreResult.RollbackErrors, rollbackErrs...)
|
|
return restoreResult, fmt.Errorf("restore failed for %s:%d: %w", diff.Kind, diff.ID, txErr)
|
|
}
|
|
return restoreResult, nil
|
|
}
|
|
|
|
func (m *mk2Ser) rollbackAppliedTransactions(applied []RegisterTransactionResult, opts TransactionOptions) []string {
|
|
errs := make([]string, 0)
|
|
rollbackOpts := normalizeTransactionOptions(TransactionOptions{
|
|
Retries: 1,
|
|
RetryDelay: opts.RetryDelay,
|
|
BackoffFactor: opts.BackoffFactor,
|
|
VerifyAfterWrite: true,
|
|
TimeoutClass: opts.TimeoutClass,
|
|
CommandTimeout: opts.CommandTimeout,
|
|
})
|
|
for i := len(applied) - 1; i >= 0; i-- {
|
|
tx := applied[i]
|
|
if tx.PreviousValue == nil {
|
|
continue
|
|
}
|
|
if _, err := m.WriteRegister(tx.Kind, tx.ID, *tx.PreviousValue, rollbackOpts); err != nil {
|
|
errs = append(errs, fmt.Sprintf("rollback failed for %s:%d: %v", tx.Kind, tx.ID, err))
|
|
}
|
|
}
|
|
return errs
|
|
}
|
|
|
|
func (m *mk2Ser) DriverDiagnostics(limit int) DriverDiagnostics {
|
|
m.diagMu.Lock()
|
|
defer m.diagMu.Unlock()
|
|
|
|
traceLimit := limit
|
|
if traceLimit <= 0 || traceLimit > len(m.traces) {
|
|
traceLimit = len(m.traces)
|
|
}
|
|
traces := make([]ProtocolTrace, traceLimit)
|
|
if traceLimit > 0 {
|
|
copy(traces, m.traces[len(m.traces)-traceLimit:])
|
|
}
|
|
recentErrors := append([]string(nil), m.recentErrors...)
|
|
|
|
diag := DriverDiagnostics{
|
|
GeneratedAt: time.Now().UTC(),
|
|
CommandTimeouts: m.commandTimeouts.Load(),
|
|
CommandFailures: m.commandFailures.Load(),
|
|
ChecksumFailures: m.checksumErrors.Load(),
|
|
RecentErrors: recentErrors,
|
|
Traces: traces,
|
|
}
|
|
if !m.lastFrameAt.IsZero() {
|
|
last := m.lastFrameAt
|
|
diag.LastFrameAt = &last
|
|
}
|
|
diag.HealthScore = calculateDriverHealthScore(diag)
|
|
return diag
|
|
}
|
|
|
|
func (m *mk2Ser) readRegisterLocked(kind RegisterKind, id uint16) (int16, error) {
|
|
return m.readRegisterLockedWithTimeout(kind, id, writeResponseTimeout)
|
|
}
|
|
|
|
func (m *mk2Ser) readRegisterLockedWithTimeout(kind RegisterKind, id uint16, timeout time.Duration) (int16, error) {
|
|
switch kind {
|
|
case RegisterKindRAMVar:
|
|
return m.readValueByIDWithTimeout(commandReadRAMVar, commandReadRAMResponse, id, timeout)
|
|
case RegisterKindSetting:
|
|
return m.readValueByIDWithTimeout(commandReadSetting, commandReadSettingResponse, id, timeout)
|
|
default:
|
|
return 0, fmt.Errorf("unsupported register kind %q", kind)
|
|
}
|
|
}
|
|
|
|
func (m *mk2Ser) writeRegisterLocked(kind RegisterKind, id uint16, value int16) error {
|
|
return m.writeRegisterLockedWithTimeout(kind, id, value, writeResponseTimeout)
|
|
}
|
|
|
|
func (m *mk2Ser) writeRegisterLockedWithTimeout(kind RegisterKind, id uint16, value int16, timeout time.Duration) error {
|
|
switch kind {
|
|
case RegisterKindRAMVar:
|
|
err := m.writeByIDOnlyWithTimeout(commandWriteRAMViaID, commandWriteRAMViaIDResponse, id, value, timeout)
|
|
if err != nil && !errors.Is(err, errWriteRejected) {
|
|
mk2log.WithFields(logrus.Fields{
|
|
"id": id,
|
|
"value": value,
|
|
}).WithError(err).Warn("WriteRegister RAM by-id failed, trying legacy write path")
|
|
err = m.writeBySelectionWithTimeout(commandWriteRAMVar, commandWriteRAMResponse, id, value, timeout)
|
|
}
|
|
return err
|
|
case RegisterKindSetting:
|
|
err := m.writeByIDOnlyWithTimeout(commandWriteViaID, commandWriteViaIDResponse, id, value, timeout)
|
|
if err != nil && !errors.Is(err, errWriteRejected) {
|
|
mk2log.WithFields(logrus.Fields{
|
|
"id": id,
|
|
"value": value,
|
|
}).WithError(err).Warn("WriteRegister setting by-id failed, trying legacy write path")
|
|
err = m.writeBySelectionWithTimeout(commandWriteSetting, commandWriteSettingResponse, id, value, timeout)
|
|
}
|
|
return err
|
|
default:
|
|
return fmt.Errorf("unsupported register kind %q", kind)
|
|
}
|
|
}
|
|
|
|
func (m *mk2Ser) WriteRAMVar(id uint16, value int16) error {
|
|
mk2log.WithFields(logrus.Fields{
|
|
"id": id,
|
|
"value": value,
|
|
}).Info("WriteRAMVar requested")
|
|
|
|
m.beginCommand()
|
|
defer m.endCommand()
|
|
|
|
err := m.writeByIDOnly(commandWriteRAMViaID, commandWriteRAMViaIDResponse, id, value)
|
|
if err != nil {
|
|
if errors.Is(err, errWriteRejected) {
|
|
mk2log.WithError(err).WithField("id", id).Error("WriteRAMVar failed")
|
|
return err
|
|
}
|
|
mk2log.WithFields(logrus.Fields{
|
|
"id": id,
|
|
"value": value,
|
|
}).WithError(err).Warn("WriteRAMVar by-id command failed, falling back to legacy write sequence")
|
|
err = m.writeBySelection(commandWriteRAMVar, commandWriteRAMResponse, id, value)
|
|
}
|
|
if err != nil {
|
|
mk2log.WithError(err).WithField("id", id).Error("WriteRAMVar failed")
|
|
return err
|
|
}
|
|
mk2log.WithField("id", id).Info("WriteRAMVar applied")
|
|
return nil
|
|
}
|
|
|
|
func (m *mk2Ser) WriteSetting(id uint16, value int16) error {
|
|
mk2log.WithFields(logrus.Fields{
|
|
"id": id,
|
|
"value": value,
|
|
}).Info("WriteSetting requested")
|
|
|
|
m.beginCommand()
|
|
defer m.endCommand()
|
|
|
|
err := m.writeByIDOnly(commandWriteViaID, commandWriteViaIDResponse, id, value)
|
|
if err != nil {
|
|
if errors.Is(err, errWriteRejected) {
|
|
mk2log.WithError(err).WithField("id", id).Error("WriteSetting failed")
|
|
return err
|
|
}
|
|
mk2log.WithFields(logrus.Fields{
|
|
"id": id,
|
|
"value": value,
|
|
}).WithError(err).Warn("WriteSetting by-id command failed, falling back to legacy write sequence")
|
|
err = m.writeBySelection(commandWriteSetting, commandWriteSettingResponse, id, value)
|
|
}
|
|
if err != nil {
|
|
mk2log.WithError(err).WithField("id", id).Error("WriteSetting failed")
|
|
return err
|
|
}
|
|
mk2log.WithField("id", id).Info("WriteSetting applied")
|
|
return nil
|
|
}
|
|
|
|
func (m *mk2Ser) WriteSettingByID(id uint16, value int16) error {
|
|
m.beginCommand()
|
|
defer m.endCommand()
|
|
return m.writeByIDOnly(commandWriteViaID, commandWriteViaIDResponse, id, value)
|
|
}
|
|
|
|
func (m *mk2Ser) WriteRAMVarByID(id uint16, value int16) error {
|
|
m.beginCommand()
|
|
defer m.endCommand()
|
|
return m.writeByIDOnly(commandWriteRAMViaID, commandWriteRAMViaIDResponse, id, value)
|
|
}
|
|
|
|
func (m *mk2Ser) WriteSettingBySelection(id uint16, value int16) error {
|
|
m.beginCommand()
|
|
defer m.endCommand()
|
|
return m.writeBySelection(commandWriteSetting, commandWriteSettingResponse, id, value)
|
|
}
|
|
|
|
func (m *mk2Ser) WriteRAMVarBySelection(id uint16, value int16) error {
|
|
m.beginCommand()
|
|
defer m.endCommand()
|
|
return m.writeBySelection(commandWriteRAMVar, commandWriteRAMResponse, id, value)
|
|
}
|
|
|
|
func (m *mk2Ser) WriteSelectedData(value int16) error {
|
|
m.beginCommand()
|
|
defer m.endCommand()
|
|
|
|
m.clearWriteResponses()
|
|
raw := uint16(value)
|
|
m.sendCommandLocked([]byte{
|
|
winmonFrame,
|
|
commandWriteData,
|
|
byte(raw),
|
|
byte(raw >> 8),
|
|
})
|
|
_, err := m.waitForAnyWriteResponseWithTimeout([]byte{
|
|
commandWriteRAMResponse,
|
|
commandWriteSettingResponse,
|
|
commandWriteViaIDResponse,
|
|
commandWriteRAMViaIDResponse,
|
|
}, writeResponseTimeout)
|
|
return err
|
|
}
|
|
|
|
func (m *mk2Ser) GetDeviceState() (DeviceState, error) {
|
|
m.beginCommand()
|
|
defer m.endCommand()
|
|
|
|
m.clearWinmonResponses()
|
|
m.sendCommandLocked([]byte{
|
|
winmonFrame,
|
|
commandSetState,
|
|
0x00,
|
|
})
|
|
resp, err := m.waitForWinmonResponse(commandSetStateResponse)
|
|
if err != nil {
|
|
return 0, err
|
|
}
|
|
return decodeDeviceStateResponse(resp)
|
|
}
|
|
|
|
func (m *mk2Ser) SetDeviceState(state DeviceState) error {
|
|
if !validDeviceState(state) {
|
|
return fmt.Errorf("invalid device state: 0x%02x", byte(state))
|
|
}
|
|
|
|
m.beginCommand()
|
|
defer m.endCommand()
|
|
|
|
m.clearWinmonResponses()
|
|
m.sendCommandLocked([]byte{
|
|
winmonFrame,
|
|
commandSetState,
|
|
0x00,
|
|
byte(state),
|
|
})
|
|
|
|
resp, err := m.waitForWinmonResponse(commandSetStateResponse)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
ackState, err := decodeDeviceStateResponse(resp)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if ackState != state {
|
|
return fmt.Errorf("device acknowledged state %s (0x%02x), expected %s (0x%02x)", formatDeviceState(ackState), byte(ackState), formatDeviceState(state), byte(state))
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (m *mk2Ser) ReadRAMVarByID(id uint16) (int16, error) {
|
|
m.beginCommand()
|
|
defer m.endCommand()
|
|
return m.readValueByID(commandReadRAMVar, commandReadRAMResponse, id)
|
|
}
|
|
|
|
func (m *mk2Ser) ReadSettingByID(id uint16) (int16, error) {
|
|
m.beginCommand()
|
|
defer m.endCommand()
|
|
return m.readValueByID(commandReadSetting, commandReadSettingResponse, id)
|
|
}
|
|
|
|
func (m *mk2Ser) SelectRAMVar(id uint16) error {
|
|
m.beginCommand()
|
|
defer m.endCommand()
|
|
_, err := m.readValueByID(commandReadRAMVar, commandReadRAMResponse, id)
|
|
return err
|
|
}
|
|
|
|
func (m *mk2Ser) SelectSetting(id uint16) error {
|
|
m.beginCommand()
|
|
defer m.endCommand()
|
|
_, err := m.readValueByID(commandReadSetting, commandReadSettingResponse, id)
|
|
return err
|
|
}
|
|
|
|
func (m *mk2Ser) ReadSelected() (int16, error) {
|
|
m.beginCommand()
|
|
defer m.endCommand()
|
|
|
|
m.clearWinmonResponses()
|
|
m.sendCommandLocked([]byte{
|
|
winmonFrame,
|
|
commandReadSelected,
|
|
})
|
|
resp, err := m.waitForWinmonResponse(commandReadSelectedResponse)
|
|
if err != nil {
|
|
return 0, err
|
|
}
|
|
return decodeInt16Response(resp)
|
|
}
|
|
|
|
func (m *mk2Ser) ReadRAMVarInfo(id uint16) (RAMVarInfo, error) {
|
|
m.beginCommand()
|
|
defer m.endCommand()
|
|
|
|
m.clearWinmonResponses()
|
|
m.sendCommandLocked([]byte{
|
|
winmonFrame,
|
|
commandGetRAMVarInfo,
|
|
byte(id),
|
|
byte(id >> 8),
|
|
})
|
|
resp, err := m.waitForWinmonResponse(commandGetRAMVarInfoResponse)
|
|
if err != nil {
|
|
return RAMVarInfo{}, err
|
|
}
|
|
return decodeRAMVarInfoResponse(id, resp)
|
|
}
|
|
|
|
func (m *mk2Ser) SetPanelState(switchState PanelSwitchState, currentLimitA *float64) error {
|
|
if !validPanelSwitchState(switchState) {
|
|
return fmt.Errorf("invalid panel switch state: %d", switchState)
|
|
}
|
|
|
|
currentLimitRaw, err := encodePanelCurrentLimit(currentLimitA)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
m.beginCommand()
|
|
defer m.endCommand()
|
|
|
|
logEntry := mk2log.WithField("switch_state", switchState)
|
|
if currentLimitA != nil {
|
|
logEntry = logEntry.WithField("current_limit_a", *currentLimitA)
|
|
}
|
|
logEntry.Info("SetPanelState requested")
|
|
|
|
m.clearStateResponses()
|
|
m.sendCommandLocked([]byte{
|
|
stateFrame,
|
|
byte(switchState),
|
|
byte(currentLimitRaw),
|
|
byte(currentLimitRaw >> 8),
|
|
0x01,
|
|
panelStateVariant2Flags,
|
|
})
|
|
|
|
err = m.waitForStateResponse()
|
|
if err != nil {
|
|
logEntry.WithError(err).Error("SetPanelState failed")
|
|
return err
|
|
}
|
|
logEntry.Info("SetPanelState acknowledged")
|
|
return nil
|
|
}
|
|
|
|
func (m *mk2Ser) SetStandby(enabled bool) error {
|
|
lineState := byte(interfacePanelDetectFlag)
|
|
if enabled {
|
|
lineState |= interfaceStandbyFlag
|
|
}
|
|
|
|
m.beginCommand()
|
|
defer m.endCommand()
|
|
logEntry := mk2log.WithField("standby_enabled", enabled)
|
|
logEntry.Info("SetStandby requested")
|
|
|
|
m.clearInterfaceResponses()
|
|
m.sendCommandLocked([]byte{
|
|
interfaceFrame,
|
|
lineState,
|
|
})
|
|
|
|
err := m.waitForInterfaceResponse(enabled)
|
|
if err != nil {
|
|
logEntry.WithError(err).Error("SetStandby failed")
|
|
return err
|
|
}
|
|
logEntry.Info("SetStandby acknowledged")
|
|
return nil
|
|
}
|
|
|
|
func validPanelSwitchState(switchState PanelSwitchState) bool {
|
|
switch switchState {
|
|
case PanelSwitchChargerOnly, PanelSwitchInverterOnly, PanelSwitchOn, PanelSwitchOff:
|
|
return true
|
|
default:
|
|
return false
|
|
}
|
|
}
|
|
|
|
func validDeviceState(state DeviceState) bool {
|
|
switch state {
|
|
case DeviceStateChargerOnly, DeviceStateInverterOnly, DeviceStateOn, DeviceStateOff:
|
|
return true
|
|
default:
|
|
return false
|
|
}
|
|
}
|
|
|
|
func formatDeviceState(state DeviceState) string {
|
|
if name, ok := DeviceStateNames[state]; ok {
|
|
return name
|
|
}
|
|
return "unknown"
|
|
}
|
|
|
|
func encodePanelCurrentLimit(currentLimitA *float64) (uint16, error) {
|
|
if currentLimitA == nil {
|
|
return panelCurrentLimitUnknown, nil
|
|
}
|
|
if *currentLimitA < 0 {
|
|
return 0, fmt.Errorf("current_limit must be >= 0, got %.3f", *currentLimitA)
|
|
}
|
|
|
|
raw := math.Round(*currentLimitA * 10)
|
|
if raw > panelCurrentLimitMax {
|
|
return 0, fmt.Errorf("current_limit %.3f A is above MK2 maximum %.1f A", *currentLimitA, panelCurrentLimitMax/10.0)
|
|
}
|
|
return uint16(raw), nil
|
|
}
|
|
|
|
func (m *mk2Ser) writeByIDOnly(writeCommand, expectedResponse byte, id uint16, value int16) error {
|
|
return m.writeByIDOnlyWithTimeout(writeCommand, expectedResponse, id, value, writeResponseTimeout)
|
|
}
|
|
|
|
func (m *mk2Ser) writeByIDOnlyWithTimeout(writeCommand, expectedResponse byte, id uint16, value int16, timeout time.Duration) error {
|
|
m.clearWriteResponses()
|
|
rawValue := uint16(value)
|
|
m.sendCommandLocked([]byte{
|
|
winmonFrame,
|
|
writeCommand,
|
|
byte(id),
|
|
byte(id >> 8),
|
|
byte(rawValue),
|
|
byte(rawValue >> 8),
|
|
})
|
|
return m.waitForWriteResponseWithTimeout(expectedResponse, timeout)
|
|
}
|
|
|
|
func (m *mk2Ser) writeBySelection(selectCommand, expectedResponse byte, id uint16, value int16) error {
|
|
return m.writeBySelectionWithTimeout(selectCommand, expectedResponse, id, value, writeResponseTimeout)
|
|
}
|
|
|
|
func (m *mk2Ser) writeBySelectionWithTimeout(selectCommand, expectedResponse byte, id uint16, value int16, timeout time.Duration) error {
|
|
m.clearWriteResponses()
|
|
rawValue := uint16(value)
|
|
m.sendCommandLocked([]byte{
|
|
winmonFrame,
|
|
selectCommand,
|
|
byte(id),
|
|
byte(id >> 8),
|
|
})
|
|
m.sendCommandLocked([]byte{
|
|
winmonFrame,
|
|
commandWriteData,
|
|
byte(rawValue),
|
|
byte(rawValue >> 8),
|
|
})
|
|
|
|
return m.waitForWriteResponseWithTimeout(expectedResponse, timeout)
|
|
}
|
|
|
|
func (m *mk2Ser) readValueByID(readCommand, expectedResponse byte, id uint16) (int16, error) {
|
|
return m.readValueByIDWithTimeout(readCommand, expectedResponse, id, writeResponseTimeout)
|
|
}
|
|
|
|
func (m *mk2Ser) readValueByIDWithTimeout(readCommand, expectedResponse byte, id uint16, timeout time.Duration) (int16, error) {
|
|
m.clearWinmonResponses()
|
|
m.sendCommandLocked([]byte{
|
|
winmonFrame,
|
|
readCommand,
|
|
byte(id),
|
|
byte(id >> 8),
|
|
})
|
|
|
|
resp, err := m.waitForWinmonResponseWithTimeout(expectedResponse, timeout)
|
|
if err != nil {
|
|
return 0, err
|
|
}
|
|
return decodeInt16Response(resp)
|
|
}
|
|
|
|
func decodeInt16Response(resp winmonResponse) (int16, error) {
|
|
if len(resp.data) < 2 {
|
|
return 0, fmt.Errorf("invalid response 0x%02x payload length %d", resp.command, len(resp.data))
|
|
}
|
|
return int16(uint16(resp.data[0]) | uint16(resp.data[1])<<8), nil
|
|
}
|
|
|
|
func decodeDeviceStateResponse(resp winmonResponse) (DeviceState, error) {
|
|
if len(resp.data) < 1 {
|
|
return 0, fmt.Errorf("invalid device state response payload length %d", len(resp.data))
|
|
}
|
|
|
|
var raw byte
|
|
if len(resp.data) >= 2 {
|
|
raw = resp.data[1]
|
|
} else {
|
|
raw = resp.data[0]
|
|
}
|
|
state := DeviceState(raw)
|
|
if !validDeviceState(state) {
|
|
return state, fmt.Errorf("unsupported device state 0x%02x", raw)
|
|
}
|
|
return state, nil
|
|
}
|
|
|
|
func decodeRAMVarInfoResponse(id uint16, resp winmonResponse) (RAMVarInfo, error) {
|
|
info := RAMVarInfo{
|
|
ID: id,
|
|
}
|
|
if len(resp.data) < 4 {
|
|
return info, nil
|
|
}
|
|
|
|
scl := int16(resp.data[1])<<8 + int16(resp.data[0])
|
|
var ofs int16
|
|
if len(resp.data) == 4 {
|
|
ofs = int16(uint16(resp.data[3])<<8 + uint16(resp.data[2]))
|
|
} else {
|
|
ofs = int16(uint16(resp.data[4])<<8 + uint16(resp.data[3]))
|
|
}
|
|
|
|
info.Supported = true
|
|
info.Scale = scl
|
|
info.Offset = ofs
|
|
info.Signed = scl < 0
|
|
scale := int16Abs(scl)
|
|
if scale >= 0x4000 {
|
|
info.Factor = 1 / (0x8000 - float64(scale))
|
|
} else {
|
|
info.Factor = float64(scale)
|
|
}
|
|
|
|
return info, nil
|
|
}
|
|
|
|
func (m *mk2Ser) clearWriteResponses() {
|
|
if m.writeAck == nil {
|
|
m.writeAck = make(chan byte, 4)
|
|
return
|
|
}
|
|
for {
|
|
select {
|
|
case <-m.writeAck:
|
|
default:
|
|
return
|
|
}
|
|
}
|
|
}
|
|
|
|
func (m *mk2Ser) waitForWriteResponse(expectedResponse byte) error {
|
|
return m.waitForWriteResponseWithTimeout(expectedResponse, writeResponseTimeout)
|
|
}
|
|
|
|
func (m *mk2Ser) waitForWriteResponseWithTimeout(expectedResponse byte, timeout time.Duration) error {
|
|
if m.writeAck == nil {
|
|
return errors.New("write response channel is not initialized")
|
|
}
|
|
|
|
select {
|
|
case response := <-m.writeAck:
|
|
mk2log.WithFields(logrus.Fields{
|
|
"expected_response": fmt.Sprintf("0x%02x", expectedResponse),
|
|
"received_response": fmt.Sprintf("0x%02x", response),
|
|
}).Debug("Received write acknowledgement")
|
|
switch response {
|
|
case expectedResponse:
|
|
return nil
|
|
case commandUnsupportedResponse:
|
|
m.noteCommandFailure(fmt.Errorf("received unsupported response 0x%02x", response))
|
|
return errCommandUnsupported
|
|
case commandWriteNotAllowedResponse:
|
|
m.noteCommandFailure(fmt.Errorf("received write rejected response 0x%02x", response))
|
|
return errWriteRejected
|
|
default:
|
|
err := fmt.Errorf("unexpected write response 0x%02x", response)
|
|
m.noteCommandFailure(err)
|
|
return err
|
|
}
|
|
case <-time.After(timeout):
|
|
err := fmt.Errorf("timed out waiting for write response after %s", timeout)
|
|
mk2log.WithField("expected_response", fmt.Sprintf("0x%02x", expectedResponse)).WithError(err).Error("Timed out waiting for write acknowledgement")
|
|
m.noteCommandTimeout(err)
|
|
return err
|
|
}
|
|
}
|
|
|
|
func (m *mk2Ser) waitForAnyWriteResponseWithTimeout(expectedResponses []byte, timeout time.Duration) (byte, error) {
|
|
if m.writeAck == nil {
|
|
return 0, errors.New("write response channel is not initialized")
|
|
}
|
|
expected := make(map[byte]struct{}, len(expectedResponses))
|
|
for _, response := range expectedResponses {
|
|
expected[response] = struct{}{}
|
|
}
|
|
|
|
select {
|
|
case response := <-m.writeAck:
|
|
if _, ok := expected[response]; ok {
|
|
return response, nil
|
|
}
|
|
switch response {
|
|
case commandUnsupportedResponse:
|
|
err := fmt.Errorf("received unsupported write response 0x%02x", response)
|
|
m.noteCommandFailure(err)
|
|
return response, errCommandUnsupported
|
|
case commandWriteNotAllowedResponse:
|
|
err := fmt.Errorf("received write rejected response 0x%02x", response)
|
|
m.noteCommandFailure(err)
|
|
return response, errWriteRejected
|
|
default:
|
|
err := fmt.Errorf("unexpected write response 0x%02x", response)
|
|
m.noteCommandFailure(err)
|
|
return response, err
|
|
}
|
|
case <-time.After(timeout):
|
|
err := fmt.Errorf("timed out waiting for write response after %s", timeout)
|
|
m.noteCommandTimeout(err)
|
|
return 0, err
|
|
}
|
|
}
|
|
|
|
func (m *mk2Ser) pushWriteResponse(response byte) {
|
|
if m.writeAck == nil {
|
|
return
|
|
}
|
|
|
|
select {
|
|
case m.writeAck <- response:
|
|
default:
|
|
}
|
|
}
|
|
|
|
func (m *mk2Ser) clearWinmonResponses() {
|
|
if m.winmonAck == nil {
|
|
m.winmonAck = make(chan winmonResponse, 32)
|
|
return
|
|
}
|
|
for {
|
|
select {
|
|
case <-m.winmonAck:
|
|
default:
|
|
return
|
|
}
|
|
}
|
|
}
|
|
|
|
func (m *mk2Ser) waitForWinmonResponse(expectedResponse byte) (winmonResponse, error) {
|
|
return m.waitForWinmonResponseWithTimeout(expectedResponse, writeResponseTimeout)
|
|
}
|
|
|
|
func (m *mk2Ser) waitForWinmonResponseWithTimeout(expectedResponse byte, timeout time.Duration) (winmonResponse, error) {
|
|
if m.winmonAck == nil {
|
|
return winmonResponse{}, errors.New("winmon response channel is not initialized")
|
|
}
|
|
|
|
timeoutChan := time.After(timeout)
|
|
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:
|
|
m.noteCommandFailure(fmt.Errorf("received unsupported winmon response 0x%02x", response.command))
|
|
return winmonResponse{}, errCommandUnsupported
|
|
case commandWriteNotAllowedResponse:
|
|
m.noteCommandFailure(fmt.Errorf("received write rejected winmon response 0x%02x", response.command))
|
|
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 <-timeoutChan:
|
|
err := fmt.Errorf("timed out waiting for winmon response 0x%02x after %s", expectedResponse, timeout)
|
|
m.noteCommandTimeout(err)
|
|
return winmonResponse{}, err
|
|
}
|
|
}
|
|
}
|
|
|
|
func (m *mk2Ser) pushWinmonResponse(command byte, data []byte) {
|
|
if m.winmonAck == nil {
|
|
return
|
|
}
|
|
payloadCopy := make([]byte, len(data))
|
|
copy(payloadCopy, data)
|
|
|
|
select {
|
|
case m.winmonAck <- winmonResponse{command: command, data: payloadCopy}:
|
|
default:
|
|
}
|
|
}
|
|
|
|
func (m *mk2Ser) clearStateResponses() {
|
|
if m.stateAck == nil {
|
|
m.stateAck = make(chan struct{}, 1)
|
|
return
|
|
}
|
|
for {
|
|
select {
|
|
case <-m.stateAck:
|
|
default:
|
|
return
|
|
}
|
|
}
|
|
}
|
|
|
|
func (m *mk2Ser) waitForStateResponse() error {
|
|
return m.waitForStateResponseWithTimeout(writeResponseTimeout)
|
|
}
|
|
|
|
func (m *mk2Ser) waitForStateResponseWithTimeout(timeout time.Duration) error {
|
|
if m.stateAck == nil {
|
|
return errors.New("panel state response channel is not initialized")
|
|
}
|
|
|
|
select {
|
|
case <-m.stateAck:
|
|
mk2log.Debug("Received panel state acknowledgement")
|
|
return nil
|
|
case <-time.After(timeout):
|
|
err := fmt.Errorf("timed out waiting for panel state response after %s", timeout)
|
|
mk2log.WithError(err).Error("Timed out waiting for panel state acknowledgement")
|
|
m.noteCommandTimeout(err)
|
|
return err
|
|
}
|
|
}
|
|
|
|
func (m *mk2Ser) pushStateResponse() {
|
|
if m.stateAck == nil {
|
|
return
|
|
}
|
|
|
|
select {
|
|
case m.stateAck <- struct{}{}:
|
|
default:
|
|
}
|
|
}
|
|
|
|
func (m *mk2Ser) clearInterfaceResponses() {
|
|
if m.ifaceAck == nil {
|
|
m.ifaceAck = make(chan byte, 1)
|
|
return
|
|
}
|
|
for {
|
|
select {
|
|
case <-m.ifaceAck:
|
|
default:
|
|
return
|
|
}
|
|
}
|
|
}
|
|
|
|
func (m *mk2Ser) waitForInterfaceResponse(expectedStandby bool) error {
|
|
return m.waitForInterfaceResponseWithTimeout(expectedStandby, writeResponseTimeout)
|
|
}
|
|
|
|
func (m *mk2Ser) waitForInterfaceResponseWithTimeout(expectedStandby bool, timeout time.Duration) error {
|
|
if m.ifaceAck == nil {
|
|
return errors.New("interface response channel is not initialized")
|
|
}
|
|
|
|
select {
|
|
case lineState := <-m.ifaceAck:
|
|
standbyEnabled := lineState&interfaceStandbyFlag != 0
|
|
mk2log.WithFields(logrus.Fields{
|
|
"line_state": fmt.Sprintf("0x%02x", lineState),
|
|
"expected_standby": expectedStandby,
|
|
"actual_standby": standbyEnabled,
|
|
}).Debug("Received standby interface acknowledgement")
|
|
if standbyEnabled != expectedStandby {
|
|
err := fmt.Errorf("unexpected standby line state 0x%02x", lineState)
|
|
m.noteCommandFailure(err)
|
|
return err
|
|
}
|
|
return nil
|
|
case <-time.After(timeout):
|
|
err := fmt.Errorf("timed out waiting for standby response after %s", timeout)
|
|
mk2log.WithField("expected_standby", expectedStandby).WithError(err).Error("Timed out waiting for standby acknowledgement")
|
|
m.noteCommandTimeout(err)
|
|
return err
|
|
}
|
|
}
|
|
|
|
func (m *mk2Ser) pushInterfaceResponse(lineState byte) {
|
|
if m.ifaceAck == nil {
|
|
return
|
|
}
|
|
|
|
select {
|
|
case m.ifaceAck <- lineState:
|
|
default:
|
|
}
|
|
}
|
|
|
|
func (m *mk2Ser) readByte() byte {
|
|
buffer := make([]byte, 1)
|
|
_, err := io.ReadFull(m.p, buffer)
|
|
if err != nil {
|
|
m.addError(fmt.Errorf("Read error: %v", err))
|
|
return 0
|
|
}
|
|
return buffer[0]
|
|
}
|
|
|
|
// Adds error to error slice.
|
|
func (m *mk2Ser) addError(err error) {
|
|
mk2log.Errorf("Mk2 serial slice error: %q", err.Error())
|
|
if m.info.Errors == nil {
|
|
m.info.Errors = make([]error, 0)
|
|
}
|
|
m.info.Errors = append(m.info.Errors, err)
|
|
m.info.Valid = false
|
|
m.recordError(err)
|
|
}
|
|
|
|
func (m *mk2Ser) noteCommandTimeout(err error) {
|
|
m.commandTimeouts.Add(1)
|
|
m.commandFailures.Add(1)
|
|
m.recordError(err)
|
|
}
|
|
|
|
func (m *mk2Ser) noteCommandFailure(err error) {
|
|
m.commandFailures.Add(1)
|
|
m.recordError(err)
|
|
}
|
|
|
|
// Updates report.
|
|
func (m *mk2Ser) updateReport() {
|
|
m.info.Timestamp = time.Now()
|
|
mk2log.WithFields(logrus.Fields{
|
|
"in_voltage": m.info.InVoltage,
|
|
"in_current": m.info.InCurrent,
|
|
"out_voltage": m.info.OutVoltage,
|
|
"out_current": m.info.OutCurrent,
|
|
"bat_voltage": m.info.BatVoltage,
|
|
"bat_current": m.info.BatCurrent,
|
|
"charge_state": m.info.ChargeState,
|
|
"valid": m.info.Valid,
|
|
}).Debug("Publishing MK2 status update")
|
|
select {
|
|
case m.infochan <- m.info:
|
|
default:
|
|
mk2log.Warn("Dropping MK2 status update; consumer channel is full")
|
|
}
|
|
m.info = &Mk2Info{}
|
|
}
|
|
|
|
// Checks for valid frame and chooses decoding.
|
|
func (m *mk2Ser) handleFrame(l byte, frame []byte, appendedLED []byte) {
|
|
mk2log.Debugf("[handleFrame] frame %#v", frame)
|
|
if len(frame) == 0 {
|
|
mk2log.Warn("[handleFrame] empty frame")
|
|
return
|
|
}
|
|
if checkChecksum(l, frame[0], frame[1:]) {
|
|
m.markFrameSeen()
|
|
m.recordRXTrace(frame)
|
|
switch frame[0] {
|
|
case bootupFrameHeader:
|
|
if m.pollPaused.Load() {
|
|
mk2log.Debug("Skipping setTarget during active control transaction")
|
|
return
|
|
}
|
|
m.setTarget()
|
|
case frameHeader:
|
|
if len(frame) < 2 {
|
|
mk2log.Warnf("[handleFrame] truncated frameHeader frame: %#v", frame)
|
|
return
|
|
}
|
|
switch frame[1] {
|
|
case interfaceFrame:
|
|
if len(frame) > 2 {
|
|
m.pushInterfaceResponse(frame[2])
|
|
}
|
|
case stateFrame:
|
|
if len(appendedLED) == frameLEDBytes {
|
|
m.setLEDState(appendedLED[0], appendedLED[1])
|
|
}
|
|
m.pushStateResponse()
|
|
case vFrame:
|
|
if len(frame) < 6 {
|
|
mk2log.Warnf("[handleFrame] truncated version frame: %#v", frame)
|
|
return
|
|
}
|
|
m.versionDecode(frame[2:])
|
|
case winmonFrame:
|
|
if len(frame) < 3 {
|
|
mk2log.Warnf("[handleFrame] truncated winmon frame: %#v", frame)
|
|
return
|
|
}
|
|
winmonCommand := frame[2]
|
|
var winmonData []byte
|
|
if len(frame) > 3 {
|
|
winmonData = frame[3 : len(frame)-1]
|
|
}
|
|
m.pushWinmonResponse(winmonCommand, winmonData)
|
|
switch frame[2] {
|
|
case commandGetRAMVarInfoResponse:
|
|
if !m.pollPaused.Load() {
|
|
m.scaleDecode(frame[2:])
|
|
}
|
|
case commandReadRAMResponse:
|
|
if !m.pollPaused.Load() {
|
|
m.stateDecode(frame[2:])
|
|
}
|
|
case commandReadSettingResponse, commandReadSelectedResponse:
|
|
// Responses are consumed by synchronous protocol command methods.
|
|
case commandSetStateResponse, commandWriteRAMResponse, commandWriteSettingResponse, commandWriteViaIDResponse, commandWriteRAMViaIDResponse, commandUnsupportedResponse, commandWriteNotAllowedResponse:
|
|
m.pushWriteResponse(frame[2])
|
|
default:
|
|
mk2log.Warnf("[handleFrame] invalid winmonFrame %v", frame[2:])
|
|
}
|
|
|
|
case ledFrame:
|
|
if len(frame) < 4 {
|
|
mk2log.Warnf("[handleFrame] truncated LED frame: %#v", frame)
|
|
return
|
|
}
|
|
m.ledDecode(frame[2:])
|
|
default:
|
|
mk2log.Warnf("[handleFrame] invalid frameHeader %v", frame[1])
|
|
}
|
|
|
|
case infoFrameHeader:
|
|
if len(frame) < 6 {
|
|
mk2log.Warnf("[handleFrame] truncated info frame: %#v", frame)
|
|
return
|
|
}
|
|
switch frame[5] {
|
|
case dcInfoFrame:
|
|
m.dcDecode(frame[1:])
|
|
case acL1InfoFrame:
|
|
m.acDecode(frame[1:])
|
|
default:
|
|
mk2log.Warnf("[handleFrame] invalid infoFrameHeader %v", frame[5])
|
|
}
|
|
default:
|
|
mk2log.Warnf("[handleFrame] Invalid frame %v", frame[0])
|
|
}
|
|
} else {
|
|
m.checksumErrors.Add(1)
|
|
mk2log.Errorf("[handleFrame] Invalid incoming frame checksum: %x", frame)
|
|
m.frameLock = false
|
|
m.recordError(fmt.Errorf("invalid incoming frame checksum"))
|
|
}
|
|
}
|
|
|
|
// Set the target VBus device.
|
|
func (m *mk2Ser) setTarget() {
|
|
cmd := make([]byte, 3)
|
|
cmd[0] = setTargetFrame
|
|
cmd[1] = 0x01
|
|
cmd[2] = 0x00
|
|
m.sendCommand(cmd)
|
|
}
|
|
|
|
// Request the scaling factor for entry 'in'.
|
|
func (m *mk2Ser) reqScaleFactor(in byte) {
|
|
cmd := make([]byte, 4)
|
|
cmd[0] = winmonFrame
|
|
cmd[1] = commandGetRAMVarInfo
|
|
cmd[2] = in
|
|
m.sendMonitoringCommand(cmd)
|
|
}
|
|
|
|
func int16Abs(in int16) uint16 {
|
|
if in < 0 {
|
|
return uint16(-in)
|
|
}
|
|
return uint16(in)
|
|
}
|
|
|
|
// Decode the scale factor frame.
|
|
func (m *mk2Ser) scaleDecode(frame []byte) {
|
|
tmp := scaling{}
|
|
mk2log.Debugf("Scale frame(%d): 0x%x", len(frame), frame)
|
|
if len(frame) < 6 {
|
|
tmp.supported = false
|
|
mk2log.Warnf("Skiping scaling factors for: %d", m.scaleCount)
|
|
} else {
|
|
tmp.supported = true
|
|
var scl int16
|
|
var ofs int16
|
|
if len(frame) == 6 {
|
|
scl = int16(frame[2])<<8 + int16(frame[1])
|
|
ofs = int16(uint16(frame[4])<<8 + uint16(frame[3]))
|
|
} else {
|
|
scl = int16(frame[2])<<8 + int16(frame[1])
|
|
ofs = int16(uint16(frame[5])<<8 + uint16(frame[4]))
|
|
}
|
|
if scl < 0 {
|
|
tmp.signed = true
|
|
}
|
|
tmp.offset = float64(ofs)
|
|
scale := int16Abs(scl)
|
|
if scale >= 0x4000 {
|
|
tmp.scale = 1 / (0x8000 - float64(scale))
|
|
} else {
|
|
tmp.scale = float64(scale)
|
|
}
|
|
}
|
|
mk2log.Debugf("scalecount %v: %#v \n", m.scaleCount, tmp)
|
|
m.scales = append(m.scales, tmp)
|
|
m.scaleCount++
|
|
if m.scaleCount < ramVarMaxOffset {
|
|
m.reqScaleFactor(byte(m.scaleCount))
|
|
} else {
|
|
mk2log.Info("Monitoring starting.")
|
|
}
|
|
}
|
|
|
|
// Decode the version number
|
|
func (m *mk2Ser) versionDecode(frame []byte) {
|
|
mk2log.Debugf("versiondecode %v", frame)
|
|
m.info.Version = 0
|
|
m.info.Valid = true
|
|
for i := 0; i < 4; i++ {
|
|
m.info.Version += uint32(frame[i]) << uint(i) * 8
|
|
}
|
|
|
|
if m.scaleCount < ramVarMaxOffset {
|
|
mk2log.WithField("version", m.info.Version).Info("Get scaling factors")
|
|
m.reqScaleFactor(byte(m.scaleCount))
|
|
} else {
|
|
// Send DC status request
|
|
cmd := make([]byte, 2)
|
|
cmd[0] = infoReqFrame
|
|
cmd[1] = infoReqAddrDC
|
|
m.sendMonitoringCommand(cmd)
|
|
}
|
|
}
|
|
|
|
// Decode with correct signedness and apply scale
|
|
func (m *mk2Ser) applyScaleAndSign(data []byte, scale int) float64 {
|
|
var value float64
|
|
if !m.scales[scale].supported {
|
|
return 0
|
|
}
|
|
if m.scales[scale].signed {
|
|
value = getSigned(data)
|
|
} else {
|
|
value = getUnsigned16(data)
|
|
}
|
|
return m.applyScale(value, scale)
|
|
}
|
|
|
|
// Apply scaling to float
|
|
func (m *mk2Ser) applyScale(value float64, scale int) float64 {
|
|
if !m.scales[scale].supported {
|
|
return value
|
|
}
|
|
return m.scales[scale].scale * (value + m.scales[scale].offset)
|
|
}
|
|
|
|
// Convert bytes->int16->float
|
|
func getSigned(data []byte) float64 {
|
|
return float64(int16(data[0]) + int16(data[1])<<8)
|
|
}
|
|
|
|
// Convert bytes->int16->float
|
|
func getUnsigned16(data []byte) float64 {
|
|
return float64(uint16(data[0]) + uint16(data[1])<<8)
|
|
}
|
|
|
|
// Convert bytes->uint32->float
|
|
func getUnsigned(data []byte) float64 {
|
|
return float64(uint32(data[0]) + uint32(data[1])<<8 + uint32(data[2])<<16)
|
|
}
|
|
|
|
// Decodes DC frame.
|
|
func (m *mk2Ser) dcDecode(frame []byte) {
|
|
m.info.BatVoltage = m.applyScaleAndSign(frame[5:7], ramVarVBat)
|
|
|
|
usedC := m.applyScale(getUnsigned(frame[7:10]), ramVarIBat)
|
|
chargeC := m.applyScale(getUnsigned(frame[10:13]), ramVarIBat)
|
|
m.info.BatCurrent = usedC - chargeC
|
|
|
|
m.info.OutFrequency = m.calcFreq(frame[13], ramVarInverterPeriod)
|
|
mk2log.Debugf("dcDecode %#v", m.info)
|
|
|
|
// Send L1 status request
|
|
cmd := make([]byte, 2)
|
|
cmd[0] = infoReqFrame
|
|
cmd[1] = infoReqAddrACL1
|
|
m.sendMonitoringCommand(cmd)
|
|
}
|
|
|
|
// Decodes AC frame.
|
|
func (m *mk2Ser) acDecode(frame []byte) {
|
|
m.info.InVoltage = m.applyScale(getSigned(frame[5:7]), ramVarVMains)
|
|
m.info.InCurrent = m.applyScale(getSigned(frame[7:9]), ramVarIMains)
|
|
m.info.OutVoltage = m.applyScale(getSigned(frame[9:11]), ramVarVInverter)
|
|
m.info.OutCurrent = m.applyScale(getSigned(frame[11:13]), ramVarIInverter)
|
|
m.info.InFrequency = m.calcFreq(frame[13], ramVarMainPeriod)
|
|
|
|
mk2log.Debugf("acDecode %#v", m.info)
|
|
|
|
// Send status request
|
|
cmd := make([]byte, 1)
|
|
cmd[0] = ledFrame
|
|
m.sendMonitoringCommand(cmd)
|
|
}
|
|
|
|
func (m *mk2Ser) calcFreq(data byte, scaleIndex int) float64 {
|
|
if data == 0xff || data == 0x00 {
|
|
return 0
|
|
}
|
|
return 10 / (m.applyScale(float64(data), scaleIndex))
|
|
}
|
|
|
|
// Decode charge state of battery.
|
|
func (m *mk2Ser) stateDecode(frame []byte) {
|
|
m.info.ChargeState = m.applyScaleAndSign(frame[1:3], ramVarChargeState)
|
|
mk2log.Debugf("battery state decode %#v", m.info)
|
|
m.updateReport()
|
|
}
|
|
|
|
// Decode the LED state frame.
|
|
func (m *mk2Ser) ledDecode(frame []byte) {
|
|
if len(frame) < 2 {
|
|
mk2log.Warnf("Skipping LED decode for short frame: %#v", frame)
|
|
return
|
|
}
|
|
m.setLEDState(frame[0], frame[1])
|
|
// Send charge state request
|
|
cmd := make([]byte, 4)
|
|
cmd[0] = winmonFrame
|
|
cmd[1] = commandReadRAMVar
|
|
cmd[2] = ramVarChargeState
|
|
m.sendMonitoringCommand(cmd)
|
|
}
|
|
|
|
func (m *mk2Ser) setLEDState(ledsOn, ledsBlink byte) {
|
|
m.info.LEDs = getLEDs(ledsOn, ledsBlink)
|
|
}
|
|
|
|
// Adds active LEDs to list.
|
|
func getLEDs(ledsOn, ledsBlink byte) map[Led]LEDstate {
|
|
|
|
leds := map[Led]LEDstate{}
|
|
for i := 0; i < 8; i++ {
|
|
on := (ledsOn >> uint(i)) & 1
|
|
blink := (ledsBlink >> uint(i)) & 1
|
|
if on == 1 {
|
|
leds[Led(i)] = LedOn
|
|
} else if blink == 1 {
|
|
leds[Led(i)] = LedBlink
|
|
} else {
|
|
leds[Led(i)] = LedOff
|
|
}
|
|
}
|
|
return leds
|
|
}
|
|
|
|
// Adds header and trailing crc for frame to send.
|
|
func (m *mk2Ser) sendCommand(data []byte) {
|
|
m.commandMu.Lock()
|
|
defer m.commandMu.Unlock()
|
|
m.sendCommandLocked(data)
|
|
}
|
|
|
|
func (m *mk2Ser) sendCommandLocked(data []byte) {
|
|
l := len(data)
|
|
dataOut := make([]byte, l+3)
|
|
dataOut[0] = byte(l + 1)
|
|
dataOut[1] = frameHeader
|
|
cr := -dataOut[0] - dataOut[1]
|
|
for i := 0; i < len(data); i++ {
|
|
cr = cr - data[i]
|
|
dataOut[i+2] = data[i]
|
|
}
|
|
dataOut[l+2] = cr
|
|
|
|
mk2log.Debugf("sendCommand %#v", dataOut)
|
|
m.recordTXTrace(dataOut, data)
|
|
_, err := m.p.Write(dataOut)
|
|
if err != nil {
|
|
mk2log.WithError(err).Error("Failed to send MK2 command")
|
|
m.addError(fmt.Errorf("Write error: %v", err))
|
|
m.noteCommandFailure(err)
|
|
}
|
|
}
|
|
|
|
func (m *mk2Ser) markFrameSeen() {
|
|
m.diagMu.Lock()
|
|
m.lastFrameAt = time.Now().UTC()
|
|
m.diagMu.Unlock()
|
|
}
|
|
|
|
func (m *mk2Ser) recordError(err error) {
|
|
if err == nil {
|
|
return
|
|
}
|
|
m.diagMu.Lock()
|
|
defer m.diagMu.Unlock()
|
|
m.recentErrors = append(m.recentErrors, err.Error())
|
|
if len(m.recentErrors) > 20 {
|
|
m.recentErrors = m.recentErrors[len(m.recentErrors)-20:]
|
|
}
|
|
}
|
|
|
|
func (m *mk2Ser) recordTXTrace(fullFrame []byte, payload []byte) {
|
|
command := ""
|
|
frame := ""
|
|
if len(payload) > 0 {
|
|
frame = fmt.Sprintf("0x%02x", payload[0])
|
|
command = decodeTraceCommandName(payload)
|
|
}
|
|
m.appendTrace(ProtocolTrace{
|
|
Timestamp: time.Now().UTC(),
|
|
Direction: TraceDirectionTX,
|
|
Frame: frame,
|
|
Command: command,
|
|
BytesHex: strings.ToUpper(fmt.Sprintf("%X", fullFrame)),
|
|
})
|
|
}
|
|
|
|
func (m *mk2Ser) recordRXTrace(frame []byte) {
|
|
command := ""
|
|
frameName := ""
|
|
if len(frame) > 0 {
|
|
frameName = fmt.Sprintf("0x%02x", frame[0])
|
|
command = decodeTraceCommandName(frame)
|
|
}
|
|
m.appendTrace(ProtocolTrace{
|
|
Timestamp: time.Now().UTC(),
|
|
Direction: TraceDirectionRX,
|
|
Frame: frameName,
|
|
Command: command,
|
|
BytesHex: strings.ToUpper(fmt.Sprintf("%X", frame)),
|
|
})
|
|
}
|
|
|
|
func decodeTraceCommandName(frame []byte) string {
|
|
if len(frame) == 0 {
|
|
return ""
|
|
}
|
|
switch frame[0] {
|
|
case winmonFrame:
|
|
if len(frame) < 2 {
|
|
return "winmon"
|
|
}
|
|
return fmt.Sprintf("winmon:0x%02x", frame[1])
|
|
case stateFrame:
|
|
return "panel_state"
|
|
case interfaceFrame:
|
|
return "interface"
|
|
case infoReqFrame:
|
|
return "info_request"
|
|
case ledFrame:
|
|
return "led_status"
|
|
case vFrame:
|
|
return "version"
|
|
case setTargetFrame:
|
|
return "set_target"
|
|
default:
|
|
if frame[0] == frameHeader && len(frame) > 1 && frame[1] == winmonFrame && len(frame) > 2 {
|
|
return fmt.Sprintf("winmon:0x%02x", frame[2])
|
|
}
|
|
return ""
|
|
}
|
|
}
|
|
|
|
func (m *mk2Ser) appendTrace(trace ProtocolTrace) {
|
|
m.diagMu.Lock()
|
|
defer m.diagMu.Unlock()
|
|
m.traces = append(m.traces, trace)
|
|
limit := m.traceLimit
|
|
if limit <= 0 {
|
|
limit = 200
|
|
}
|
|
if len(m.traces) > limit {
|
|
m.traces = m.traces[len(m.traces)-limit:]
|
|
}
|
|
}
|
|
|
|
func calculateDriverHealthScore(diag DriverDiagnostics) int {
|
|
score := 100
|
|
score -= int(diag.CommandTimeouts) * 5
|
|
score -= int(diag.CommandFailures) * 2
|
|
score -= int(diag.ChecksumFailures) * 3
|
|
if diag.LastFrameAt == nil {
|
|
score -= 10
|
|
} else if time.Since(diag.LastFrameAt.UTC()) > 30*time.Second {
|
|
score -= 10
|
|
}
|
|
if score < 0 {
|
|
return 0
|
|
}
|
|
if score > 100 {
|
|
return 100
|
|
}
|
|
return score
|
|
}
|
|
|
|
// Checks the frame crc.
|
|
func checkChecksum(l, t byte, d []byte) bool {
|
|
cr := (uint16(l) + uint16(t)) % 256
|
|
for i := 0; i < len(d); i++ {
|
|
cr = (cr + uint16(d[i])) % 256
|
|
}
|
|
return cr == 0
|
|
}
|