5 Commits

Author SHA1 Message Date
Hendrik van Wyk
cc8fa9d611 Merge pull request #20 from diebietse/scalefixes
Fix scaling decoding and munin race condition
2020-10-08 12:29:58 +02:00
Hendrik van Wyk
49be089a23 Fix race condition in munin output.
The munin server used the same structure in two goroutines at once causing
possible data corruption. A copy of the structure is now used by the second
goroutine instead.
2020-10-08 12:25:39 +02:00
Hendrik van Wyk
157736a99d Add optional debug logging for frame decoding. 2020-10-08 12:25:33 +02:00
Hendrik van Wyk
86f3f0c8e3 Fix scaling to more closely match the Victron documentation.
We were decoding the scale as unsigned while it is signed. We were also
ignoring the fact that the sign of the scale determines the signedness of
the value it scales.
2020-09-25 15:03:26 +02:00
Nicholas Thompson
c991503e33 Add mode-2 to scale factors 2020-09-19 18:38:00 +02:00
7 changed files with 132 additions and 61 deletions

View File

@@ -39,9 +39,6 @@ gofmt:
gofmt -l -s -w . gofmt -l -s -w .
test: test:
go test -v ./...
test-race:
go test -v -race ./... go test -v -race ./...
docker: docker:

View File

@@ -27,10 +27,8 @@ Usage:
invertergui [OPTIONS] invertergui [OPTIONS]
Application Options: Application Options:
--address= The IP/DNS and port of the machine that the application is running on. (default: :8080) --address= The IP/DNS and port of the machine that the application is running on. (default: :8080) [$ADDRESS]
[$ADDRESS] --data.source= Set the source of data for the inverter gui. "serial", "tcp" or "mock" (default: serial) [$DATA_SOURCE]
--data.source= Set the source of data for the inverter gui. "serial", "tcp" or "mock" (default: serial)
[$DATA_SOURCE]
--data.host= Host to connect when source is set to tcp. (default: localhost:8139) [$DATA_HOST] --data.host= Host to connect when source is set to tcp. (default: localhost:8139) [$DATA_HOST]
--data.device= TTY device to use when source is set to serial. (default: /dev/ttyUSB0) [$DATA_DEVICE] --data.device= TTY device to use when source is set to serial. (default: /dev/ttyUSB0) [$DATA_DEVICE]
--cli.enabled Enable CLI output. [$CLI_ENABLED] --cli.enabled Enable CLI output. [$CLI_ENABLED]
@@ -40,6 +38,7 @@ Application Options:
--mqtt.topic= Set the MQTT topic updates published to. (default: invertergui/updates) [$MQTT_TOPIC] --mqtt.topic= Set the MQTT topic updates published to. (default: invertergui/updates) [$MQTT_TOPIC]
--mqtt.username= Set the MQTT username [$MQTT_USERNAME] --mqtt.username= Set the MQTT username [$MQTT_USERNAME]
--mqtt.password= Set the MQTT password [$MQTT_PASSWORD] --mqtt.password= Set the MQTT password [$MQTT_PASSWORD]
--loglevel= The log level to generate logs at. ("panic", "fatal", "error", "warn", "info", "debug", "trace") (default: info) [$LOGLEVEL]
Help Options: Help Options:
-h, --help Show this help message -h, --help Show this help message

View File

@@ -22,6 +22,7 @@ type config struct {
Username string `long:"mqtt.username" env:"MQTT_USERNAME" default:"" description:"Set the MQTT username"` Username string `long:"mqtt.username" env:"MQTT_USERNAME" default:"" description:"Set the MQTT username"`
Password string `long:"mqtt.password" env:"MQTT_PASSWORD" default:"" description:"Set the MQTT password"` Password string `long:"mqtt.password" env:"MQTT_PASSWORD" default:"" description:"Set the MQTT password"`
} }
Loglevel string `long:"loglevel" env:"LOGLEVEL" default:"info" description:"The log level to generate logs at. (\"panic\", \"fatal\", \"error\", \"warn\", \"info\", \"debug\", \"trace\")"`
} }
func parseConfig() (*config, error) { func parseConfig() (*config, error) {

View File

@@ -58,6 +58,11 @@ func main() {
os.Exit(1) os.Exit(1)
} }
log.Info("Starting invertergui") log.Info("Starting invertergui")
logLevel, err := logrus.ParseLevel(conf.Loglevel)
if err != nil {
log.Fatalf("Could not parse log level: %v", err)
}
logrus.SetLevel(logLevel)
mk2, err := getMk2Device(conf.Data.Source, conf.Data.Host, conf.Data.Device) mk2, err := getMk2Device(conf.Data.Source, conf.Data.Host, conf.Data.Device)
if err != nil { if err != nil {

View File

@@ -4,7 +4,6 @@ import (
"errors" "errors"
"fmt" "fmt"
"io" "io"
"math"
"sync" "sync"
"time" "time"
@@ -14,6 +13,7 @@ import (
type scaling struct { type scaling struct {
scale float64 scale float64
offset float64 offset float64
signed bool
supported bool supported bool
} }
@@ -183,6 +183,7 @@ 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) {
logrus.Debugf("frame %#v", frame)
if checkChecksum(l, frame[0], frame[1:]) { if checkChecksum(l, frame[0], frame[1:]) {
switch frame[0] { switch frame[0] {
case frameHeader: case frameHeader:
@@ -233,24 +234,43 @@ func (m *mk2Ser) reqScaleFactor(in byte) {
m.sendCommand(cmd) m.sendCommand(cmd)
} }
func int16Abs(in int16) uint16 {
if in < 0 {
return uint16(-in)
}
return uint16(in)
}
// Decode the scale factor frame. // Decode the scale factor frame.
func (m *mk2Ser) scaleDecode(frame []byte) { func (m *mk2Ser) scaleDecode(frame []byte) {
tmp := scaling{} tmp := scaling{}
logrus.Debugf("Scale frame(%d): 0x%x", len(frame), frame)
if len(frame) < 6 { if len(frame) < 6 {
tmp.supported = false tmp.supported = false
logrus.Warnf("Skiping scaling factors for: %d", m.scaleCount) logrus.Warnf("Skiping scaling factors for: %d", m.scaleCount)
} else { } else {
tmp.supported = true tmp.supported = true
scl := uint16(frame[2])<<8 + uint16(frame[1]) var scl int16
ofs := int16(uint16(frame[5])<<8 + uint16(frame[4])) var ofs int16
if len(frame) == 6 {
tmp.offset = float64(ofs) scl = int16(frame[2])<<8 + int16(frame[1])
if scl >= 0x4000 { ofs = int16(uint16(frame[4])<<8 + uint16(frame[3]))
tmp.scale = math.Abs(1 / (0x8000 - float64(scl)))
} else { } else {
tmp.scale = math.Abs(float64(scl)) 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)
} }
} }
logrus.Debugf("scalecount %v: %#v \n", m.scaleCount, tmp)
m.scales = append(m.scales, tmp) m.scales = append(m.scales, tmp)
m.scaleCount++ m.scaleCount++
if m.scaleCount < ramVarMaxOffset { if m.scaleCount < ramVarMaxOffset {
@@ -262,6 +282,7 @@ func (m *mk2Ser) scaleDecode(frame []byte) {
// Decode the version number // Decode the version number
func (m *mk2Ser) versionDecode(frame []byte) { func (m *mk2Ser) versionDecode(frame []byte) {
logrus.Debugf("versiondecode %v", frame)
m.info.Version = 0 m.info.Version = 0
m.info.Valid = true m.info.Valid = true
for i := 0; i < 4; i++ { for i := 0; i < 4; i++ {
@@ -280,6 +301,20 @@ func (m *mk2Ser) versionDecode(frame []byte) {
} }
} }
// 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 // Apply scaling to float
func (m *mk2Ser) applyScale(value float64, scale int) float64 { func (m *mk2Ser) applyScale(value float64, scale int) float64 {
if !m.scales[scale].supported { if !m.scales[scale].supported {
@@ -293,6 +328,11 @@ func getSigned(data []byte) float64 {
return float64(int16(data[0]) + int16(data[1])<<8) 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 // Convert bytes->uint32->float
func getUnsigned(data []byte) float64 { func getUnsigned(data []byte) float64 {
return float64(uint32(data[0]) + uint32(data[1])<<8 + uint32(data[2])<<16) return float64(uint32(data[0]) + uint32(data[1])<<8 + uint32(data[2])<<16)
@@ -300,13 +340,14 @@ func getUnsigned(data []byte) float64 {
// Decodes DC frame. // Decodes DC frame.
func (m *mk2Ser) dcDecode(frame []byte) { func (m *mk2Ser) dcDecode(frame []byte) {
m.info.BatVoltage = m.applyScale(getSigned(frame[5:7]), ramVarVBat) m.info.BatVoltage = m.applyScaleAndSign(frame[5:7], ramVarVBat)
usedC := m.applyScale(getUnsigned(frame[7:10]), ramVarIBat) usedC := m.applyScale(getUnsigned(frame[7:10]), ramVarIBat)
chargeC := m.applyScale(getUnsigned(frame[10:13]), ramVarIBat) chargeC := m.applyScale(getUnsigned(frame[10:13]), ramVarIBat)
m.info.BatCurrent = usedC - chargeC m.info.BatCurrent = usedC - chargeC
m.info.OutFrequency = 10 / (m.applyScale(float64(frame[13]), ramVarInverterPeriod)) m.info.OutFrequency = 10 / (m.applyScale(float64(frame[13]), ramVarInverterPeriod))
logrus.Debugf("dcDecode %#v", m.info)
// Send L1 status request // Send L1 status request
cmd := make([]byte, 2) cmd := make([]byte, 2)
@@ -317,16 +358,17 @@ func (m *mk2Ser) dcDecode(frame []byte) {
// Decodes AC frame. // Decodes AC frame.
func (m *mk2Ser) acDecode(frame []byte) { func (m *mk2Ser) acDecode(frame []byte) {
m.info.InVoltage = m.applyScale(getSigned(frame[5:7]), ramVarVMains) m.info.InVoltage = m.applyScaleAndSign(frame[5:7], ramVarVMains)
m.info.InCurrent = m.applyScale(getSigned(frame[7:9]), ramVarIMains) m.info.InCurrent = m.applyScaleAndSign(frame[7:9], ramVarIMains)
m.info.OutVoltage = m.applyScale(getSigned(frame[9:11]), ramVarVInverter) m.info.OutVoltage = m.applyScaleAndSign(frame[9:11], ramVarVInverter)
m.info.OutCurrent = m.applyScale(getSigned(frame[11:13]), ramVarIInverter) m.info.OutCurrent = m.applyScaleAndSign(frame[11:13], ramVarIInverter)
if frame[13] == 0xff { if frame[13] == 0xff {
m.info.InFrequency = 0 m.info.InFrequency = 0
} else { } else {
m.info.InFrequency = 10 / (m.applyScale(float64(frame[13]), ramVarMainPeriod)) m.info.InFrequency = 10 / (m.applyScale(float64(frame[13]), ramVarMainPeriod))
} }
logrus.Debugf("acDecode %#v", m.info)
// Send status request // Send status request
cmd := make([]byte, 1) cmd := make([]byte, 1)
@@ -336,7 +378,8 @@ func (m *mk2Ser) acDecode(frame []byte) {
// Decode charge state of battery. // Decode charge state of battery.
func (m *mk2Ser) stateDecode(frame []byte) { func (m *mk2Ser) stateDecode(frame []byte) {
m.info.ChargeState = m.applyScale(getSigned(frame[1:3]), ramVarChargeState) m.info.ChargeState = m.applyScaleAndSign(frame[1:3], ramVarChargeState)
logrus.Debugf("battery state decode %#v", m.info)
m.updateReport() m.updateReport()
} }
@@ -383,6 +426,7 @@ func (m *mk2Ser) sendCommand(data []byte) {
} }
dataOut[l+2] = cr dataOut[l+2] = cr
logrus.Debugf("sendCommand %#v", dataOut)
_, err := m.p.Write(dataOut) _, err := m.p.Write(dataOut)
if err != nil { if err != nil {
m.addError(fmt.Errorf("Write error: %v", err)) m.addError(fmt.Errorf("Write error: %v", err))

View File

@@ -44,18 +44,18 @@ var log = logrus.WithField("ctx", "inverter-gui-munin")
type Munin struct { type Munin struct {
mk2driver.Mk2 mk2driver.Mk2
muninResponse chan *muninData muninResponse chan muninData
} }
type muninData struct { type muninData struct {
status *mk2driver.Mk2Info status mk2driver.Mk2Info
timesUpdated int timesUpdated int
} }
func NewMunin(mk2 mk2driver.Mk2) *Munin { func NewMunin(mk2 mk2driver.Mk2) *Munin {
m := &Munin{ m := &Munin{
Mk2: mk2, Mk2: mk2,
muninResponse: make(chan *muninData), muninResponse: make(chan muninData),
} }
go m.run() go m.run()
@@ -71,10 +71,10 @@ func (m *Munin) ServeMuninHTTP(rw http.ResponseWriter, r *http.Request) {
_, _ = rw.Write([]byte("No data to return.\n")) _, _ = rw.Write([]byte("No data to return.\n"))
return return
} }
calcMuninAverages(muninDat) calcMuninAverages(&muninDat)
status := muninDat.status status := muninDat.status
tmpInput := buildTemplateInput(status) tmpInput := buildTemplateInput(&status)
outputBuf := &bytes.Buffer{} outputBuf := &bytes.Buffer{}
fmt.Fprintf(outputBuf, "multigraph in_batvolt\n") fmt.Fprintf(outputBuf, "multigraph in_batvolt\n")
fmt.Fprintf(outputBuf, "volt.value %s\n", tmpInput.BatVoltage) fmt.Fprintf(outputBuf, "volt.value %s\n", tmpInput.BatVoltage)
@@ -113,65 +113,61 @@ func (m *Munin) ServeMuninConfigHTTP(rw http.ResponseWriter, r *http.Request) {
func (m *Munin) run() { func (m *Munin) run() {
muninValues := &muninData{ muninValues := &muninData{
status: &mk2driver.Mk2Info{}, status: mk2driver.Mk2Info{},
} }
for { for {
select { select {
case e := <-m.C(): case e := <-m.C():
if e.Valid { if e.Valid {
calcMuninValues(muninValues, e) calcMuninValues(muninValues, e)
} }
case m.muninResponse <- muninValues: case m.muninResponse <- *muninValues:
zeroMuninValues(muninValues) zeroMuninValues(muninValues)
} }
} }
} }
//Munin only samples once every 5 minutes so averages have to be calculated for some values. //Munin only samples once every 5 minutes so averages have to be calculated for some values.
func calcMuninValues(muninDat *muninData, newStatus *mk2driver.Mk2Info) { func calcMuninValues(m *muninData, newStatus *mk2driver.Mk2Info) {
muninDat.timesUpdated++ m.timesUpdated++
muninVal := muninDat.status m.status.OutCurrent += newStatus.OutCurrent
muninVal.OutCurrent += newStatus.OutCurrent m.status.InCurrent += newStatus.InCurrent
muninVal.InCurrent += newStatus.InCurrent m.status.BatCurrent += newStatus.BatCurrent
muninVal.BatCurrent += newStatus.BatCurrent
muninVal.OutVoltage += newStatus.OutVoltage m.status.OutVoltage += newStatus.OutVoltage
muninVal.InVoltage += newStatus.InVoltage m.status.InVoltage += newStatus.InVoltage
muninVal.BatVoltage += newStatus.BatVoltage m.status.BatVoltage += newStatus.BatVoltage
muninVal.InFrequency = newStatus.InFrequency m.status.InFrequency = newStatus.InFrequency
muninVal.OutFrequency = newStatus.OutFrequency m.status.OutFrequency = newStatus.OutFrequency
muninVal.ChargeState = newStatus.ChargeState m.status.ChargeState = newStatus.ChargeState
} }
func calcMuninAverages(muninDat *muninData) { func calcMuninAverages(m *muninData) {
muninVal := muninDat.status m.status.OutCurrent /= float64(m.timesUpdated)
muninVal.OutCurrent /= float64(muninDat.timesUpdated) m.status.InCurrent /= float64(m.timesUpdated)
muninVal.InCurrent /= float64(muninDat.timesUpdated) m.status.BatCurrent /= float64(m.timesUpdated)
muninVal.BatCurrent /= float64(muninDat.timesUpdated)
muninVal.OutVoltage /= float64(muninDat.timesUpdated) m.status.OutVoltage /= float64(m.timesUpdated)
muninVal.InVoltage /= float64(muninDat.timesUpdated) m.status.InVoltage /= float64(m.timesUpdated)
muninVal.BatVoltage /= float64(muninDat.timesUpdated) m.status.BatVoltage /= float64(m.timesUpdated)
} }
func zeroMuninValues(muninDat *muninData) { func zeroMuninValues(m *muninData) {
muninDat.timesUpdated = 0 m.timesUpdated = 0
muninVal := muninDat.status m.status.OutCurrent = 0
muninVal.OutCurrent = 0 m.status.InCurrent = 0
muninVal.InCurrent = 0 m.status.BatCurrent = 0
muninVal.BatCurrent = 0
muninVal.OutVoltage = 0 m.status.OutVoltage = 0
muninVal.InVoltage = 0 m.status.InVoltage = 0
muninVal.BatVoltage = 0 m.status.BatVoltage = 0
muninVal.InFrequency = 0 m.status.InFrequency = 0
muninVal.OutFrequency = 0 m.status.OutFrequency = 0
muninVal.ChargeState = 0 m.status.ChargeState = 0
} }
type templateInput struct { type templateInput struct {

View File

@@ -0,0 +1,29 @@
package munin
import (
"io/ioutil"
"net/http"
"net/http/httptest"
"testing"
"github.com/diebietse/invertergui/mk2driver"
)
func TestServer(t *testing.T) {
mockMk2 := mk2driver.NewMk2Mock()
muninServer := NewMunin(mockMk2)
ts := httptest.NewServer(http.HandlerFunc(muninServer.ServeMuninHTTP))
defer ts.Close()
res, err := http.Get(ts.URL)
if err != nil {
log.Fatal(err)
}
_, err = ioutil.ReadAll(res.Body)
res.Body.Close()
if err != nil {
log.Fatal(err)
}
}