v0.0.29 Improved error handling

- Retry mechanic for coil read errors
- Utilize pointers, attempt to have more persistent state
- Improved error handling and slightly more verbose logging
This commit is contained in:
Jarno Rankinen 2024-01-21 11:34:45 +02:00
parent 32fa6d4321
commit 027d678a09
3 changed files with 96 additions and 54 deletions

View File

@ -1,12 +1,22 @@
#!/bin/bash
if [[ "$1" == "-h" ]] || [[ "$1" == "--help" ]]; then
echo -e "Usage: $0 [ARCH|-h|--help]"
echo -e "\tARCH: amd64 (default), arm, arm64"
exit
fi
ARCH=${1:-"amd64"}
VERSION=$(grep -e 'version.*=' main.go | awk '{print $3}' | tr -d '"')
pushd TMP &> /dev/null || exit 1
rm -rf *
tar --exclude ../TMP -ch ../* | tar xf -
env GOOS=linux GOARCH=arm go build -o ../BUILD/enervent-ctrl-${VERSION}.linux-arm32 .
#env GOOS=linux GOARCH=arm go build -o ../BUILD/enervent-ctrl-${VERSION}.linux-arm32 .
CGO_ENABLED=0 GOOS=linux GOARCH="$ARCH" go build -o "../BUILD/enervent-ctrl-${VERSION}.linux-$ARCH" .
popd &> /dev/null
rm -rf ./*
popd &> /dev/null || exit 1

40
main.go
View File

@ -7,7 +7,6 @@ import (
"encoding/json"
"flag"
"io/fs"
"io/ioutil"
"log"
"net/http"
"os"
@ -30,7 +29,7 @@ import (
var static embed.FS
var (
version = "0.0.28"
version = "0.1.0"
device pingvin.Pingvin
config Conf
usernamehash [32]byte
@ -115,7 +114,7 @@ func coils(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json")
pathparams := strings.Split(strings.TrimPrefix(r.URL.Path, "/api/v1/coils/"), "/")
if len(pathparams[0]) == 0 {
json.NewEncoder(w).Encode(device.Coils)
_ = json.NewEncoder(w).Encode(device.Coils)
} else if len(pathparams[0]) > 0 && r.Method == "GET" && len(pathparams) < 2 { // && r.Method == "POST"
intaddr, err := strconv.Atoi(pathparams[0])
if err != nil {
@ -123,8 +122,11 @@ func coils(w http.ResponseWriter, r *http.Request) {
log.Println(err)
return
}
device.ReadCoil(uint16(intaddr))
json.NewEncoder(w).Encode(device.Coils[intaddr])
err = device.ReadCoil(uint16(intaddr))
if err != nil {
log.Println("ERROR ReadCoil: client.ReadCoils: ", err)
}
_ = json.NewEncoder(w).Encode(device.Coils[intaddr])
} else if len(pathparams[0]) > 0 && r.Method == "POST" && len(pathparams) == 2 {
intaddr, err := strconv.Atoi(pathparams[0])
if err != nil {
@ -143,7 +145,7 @@ func coils(w http.ResponseWriter, r *http.Request) {
} else {
device.WriteCoil(uint16(intaddr), boolval)
}
json.NewEncoder(w).Encode(device.Coils[intaddr])
_ = json.NewEncoder(w).Encode(device.Coils[intaddr])
} else if len(pathparams[0]) > 0 && r.Method == "POST" && len(pathparams) == 1 {
intaddr, err := strconv.Atoi(pathparams[0])
if err != nil {
@ -156,7 +158,7 @@ func coils(w http.ResponseWriter, r *http.Request) {
} else {
device.WriteCoil(uint16(intaddr), !device.Coils[intaddr].Value)
}
json.NewEncoder(w).Encode(device.Coils[intaddr])
_ = json.NewEncoder(w).Encode(device.Coils[intaddr])
}
}
@ -165,7 +167,7 @@ func registers(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json")
pathparams := strings.Split(strings.TrimPrefix(r.URL.Path, "/api/v1/registers/"), "/")
if len(pathparams[0]) == 0 {
json.NewEncoder(w).Encode(device.Registers)
_ = json.NewEncoder(w).Encode(device.Registers)
} else if len(pathparams[0]) > 0 && r.Method == "GET" && len(pathparams) < 2 { // && r.Method == "POST"
intaddr, err := strconv.Atoi(pathparams[0])
if err != nil {
@ -173,8 +175,11 @@ func registers(w http.ResponseWriter, r *http.Request) {
log.Println(err)
return
}
device.ReadRegister(uint16(intaddr))
json.NewEncoder(w).Encode(device.Registers[intaddr])
_, err = device.ReadRegister(uint16(intaddr))
if err != nil {
log.Println("ERROR: ReadRegister:", err)
}
_ = json.NewEncoder(w).Encode(device.Registers[intaddr])
} else if len(pathparams[0]) > 0 && r.Method == "POST" && len(pathparams) == 2 {
intaddr, err := strconv.Atoi(pathparams[0])
if err != nil {
@ -196,14 +201,14 @@ func registers(w http.ResponseWriter, r *http.Request) {
log.Println(err)
}
}
json.NewEncoder(w).Encode(device.Registers[intaddr])
_ = json.NewEncoder(w).Encode(device.Registers[intaddr])
}
}
// \/status endpoint
func status(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(device.Status)
_ = json.NewEncoder(w).Encode(device.Status)
}
// \/api/v1/temperature endpoint
@ -211,8 +216,11 @@ func temperature(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json")
pathparams := strings.Split(strings.TrimPrefix(r.URL.Path, "/api/v1/temperature/"), "/")
if len(pathparams[0]) > 0 && r.Method == "POST" && len(pathparams) == 1 {
device.Temperature(pathparams[0])
json.NewEncoder(w).Encode(device.Registers[135])
err := device.Temperature(pathparams[0])
if err != nil {
log.Println("ERROR: ", err)
}
_ = json.NewEncoder(w).Encode(device.Registers[135])
} else {
return
}
@ -283,12 +291,12 @@ func parseConfigFile() {
}
}
conffile := confpath + "/configuration.yaml"
yamldata, err := ioutil.ReadFile(conffile)
yamldata, err := os.ReadFile(conffile)
if err != nil {
log.Println("Configuration file", conffile, "not found")
log.Println("Generating", conffile, "with default values")
initDefaultConfig(confpath)
if yamldata, err = ioutil.ReadFile(conffile); err != nil {
if yamldata, err = os.ReadFile(conffile); err != nil {
log.Fatal("Error parsing configuration:", err)
}
}

View File

@ -26,9 +26,9 @@ type pingvinCoil struct {
// unit modbus data
type Pingvin struct {
Coils []pingvinCoil
Registers []pingvinRegister
Status pingvinStatus
Coils []*pingvinCoil
Registers []*pingvinRegister
Status *pingvinStatus
buslock *sync.Mutex
handler *modbus.RTUClientHandler
modbusclient modbus.Client
@ -73,7 +73,7 @@ type pingvinStatus struct {
OpMode string `json:"op_mode"` // Current operating mode, text representation
Uptime string `json:"uptime"` // Unit uptime
SystemTime string `json:"system_time"` // Time and date in unit
Coils []pingvinCoil `json:"coils"`
Coils []*pingvinCoil `json:"coils"`
}
type PingvinLogger struct {
@ -101,7 +101,7 @@ func (logger *PingvinLogger) Println(msg ...any) {
}
}
func newCoil(address string, symbol string, description string) pingvinCoil {
func newCoil(address string, symbol string, description string) *pingvinCoil {
addr, err := strconv.Atoi(address)
if err != nil {
log.Fatal("newCoil: Atoi: ", err)
@ -111,7 +111,7 @@ func newCoil(address string, symbol string, description string) pingvinCoil {
promdesc := strings.ToLower(symbol)
zpadaddr := fmt.Sprintf("%02d", addr)
promdesc = strings.Replace(promdesc, "_", "_"+zpadaddr+"_", 1)
return pingvinCoil{addr, symbol, false, description, reserved,
return &pingvinCoil{addr, symbol, false, description, reserved,
prometheus.NewDesc(
prometheus.BuildFQName("", "pingvin", promdesc),
description,
@ -120,10 +120,10 @@ func newCoil(address string, symbol string, description string) pingvinCoil {
),
}
}
return pingvinCoil{addr, symbol, false, description, reserved, nil}
return &pingvinCoil{addr, symbol, false, description, reserved, nil}
}
func newRegister(address, symbol, typ, multiplier, description string) pingvinRegister {
func newRegister(address, symbol, typ, multiplier, description string) *pingvinRegister {
addr, err := strconv.Atoi(address)
if err != nil {
log.Fatal("newRegister: Atoi(address): ", err)
@ -141,7 +141,7 @@ func newRegister(address, symbol, typ, multiplier, description string) pingvinRe
promdesc := strings.ToLower(symbol)
zpadaddr := fmt.Sprintf("%03d", addr)
promdesc = strings.Replace(promdesc, "_", "_"+zpadaddr+"_", 1)
return pingvinRegister{
return &pingvinRegister{
addr,
symbol,
0,
@ -158,7 +158,7 @@ func newRegister(address, symbol, typ, multiplier, description string) pingvinRe
),
}
}
return pingvinRegister{addr, symbol, 0, "0000000000000000", typ, description, reserved, multipl, nil}
return &pingvinRegister{addr, symbol, 0, "0000000000000000", typ, description, reserved, multipl, nil}
}
// read a CSV file containing data for coils or registers
@ -210,11 +210,23 @@ func (p *Pingvin) Quit() {
// Update all coil values
func (p *Pingvin) updateCoils() {
p.buslock.Lock()
results, err := p.modbusclient.ReadCoils(0, uint16(len(p.Coils)))
p.buslock.Unlock()
if err != nil {
log.Fatal("updateCoils: client.ReadCoils: ", err)
var results []byte
var err error
for retries := 1; retries <= 5; retries++ {
p.Debug.Println("Reading coils, attempt", retries)
p.buslock.Lock()
results, err = p.modbusclient.ReadCoils(0, uint16(len(p.Coils)))
p.buslock.Unlock()
if len(results) > 0 {
break
} else if retries == 4 {
log.Println("ERROR: updateCoils: client.Readcoils: ", err)
return
}
if err != nil {
log.Printf("WARNING updateCoils: client.ReadCoils attempt %d: %s\n", retries, err)
}
time.Sleep(100 * time.Millisecond)
}
// modbus.ReadCoils returns a byte array, with the first byte's bits representing coil values 0-7,
// second byte coils 8-15 etc.
@ -242,8 +254,8 @@ func (p *Pingvin) ReadRegister(addr uint16) (int, error) {
results, err := p.modbusclient.ReadHoldingRegisters(addr, 1)
p.buslock.Unlock()
if err != nil {
log.Println("ERROR: ReadRegister:", err)
return 0, err
//log.Println("ERROR: ReadRegister:", err)
return p.Registers[addr].Value, err
}
if p.Registers[addr].Type == "uint16" {
p.Registers[addr].Value = int(uint16(results[0]) << 8)
@ -270,6 +282,7 @@ func (p *Pingvin) WriteRegister(addr uint16, value uint16) (uint16, error) {
return 0, err
}
if val == int(value) {
log.Printf("Wrote register %d to value %d (%s: %s)", addr, p.Registers[addr].Value, p.Registers[addr].Symbol, p.Registers[addr].Description)
return value, nil
}
return 0, fmt.Errorf("Failed to write register")
@ -288,8 +301,8 @@ func (p *Pingvin) updateRegisters() {
if regs-k < 125 {
r = regs - k
}
results := []byte{}
for retries := 0; retries < 5; retries++ {
var results []byte
for retries := 1; retries <= 5; retries++ {
p.Debug.Println("Reading registers, attempt", retries, "k:", k)
p.buslock.Lock()
results, err = p.modbusclient.ReadHoldingRegisters(uint16(k), uint16(r))
@ -299,8 +312,9 @@ func (p *Pingvin) updateRegisters() {
} else if retries == 4 {
log.Fatal("updateRegisters: client.ReadHoldingRegisters: ", err)
} else if err != nil {
log.Println("WARNING: updateRegisters: client.ReadHoldingRegisters: ", err)
log.Printf("WARNING: updateRegisters: client.ReadHoldingRegisters attempt %d: %s", retries, err)
}
time.Sleep(100 * time.Millisecond)
}
// The values represent 16 bit integers, but modbus works with bytes
// Each even byte of the returned []byte is the 8 MSBs of a new 16-bit
@ -350,22 +364,30 @@ func (p *Pingvin) Update() {
}
// Read single coil
func (p *Pingvin) ReadCoil(n uint16) ([]byte, error) {
p.buslock.Lock()
results, err := p.modbusclient.ReadCoils(n, 1)
p.buslock.Unlock()
if err != nil {
log.Fatal("ReadCoil: client.ReadCoils: ", err)
return nil, err
func (p *Pingvin) ReadCoil(n uint16) (err error) {
var results []byte
for retries := 1; retries <= 5; retries++ {
p.buslock.Lock()
results, err = p.modbusclient.ReadCoils(n, 1)
p.buslock.Unlock()
if len(results) > 0 && err == nil {
break
} else if retries == 4 {
//log.Println("ERROR ReadCoil: client.ReadCoils: ", err)
return
} else if err != nil {
log.Printf("WARNING: ReadCoil: client.ReadCoils attempt %d: %s", retries, err)
}
time.Sleep(100 * time.Millisecond)
}
p.Coils[n].Value = results[0] == 1
return results, nil
return
}
// Force a single coil
func (p *Pingvin) WriteCoil(n uint16, val bool) bool {
if val {
p.checkMutexCoils(n, p.handler)
_ = p.checkMutexCoils(n) //, p.handler)
}
var value uint16 = 0
if val {
@ -377,14 +399,15 @@ func (p *Pingvin) WriteCoil(n uint16, val bool) bool {
if err != nil {
log.Println("ERROR: WriteCoil: ", err)
}
if (val && results[0] == 255) || (!val && results[0] == 0) {
log.Println("WriteCoil: wrote coil", n, "to value", val)
} else {
if (val && results[0] != 255) && (!val && results[0] != 0) {
log.Println("ERROR: WriteCoil: failed to write coil")
return false
}
p.ReadCoil(n)
err = p.ReadCoil(n)
if err != nil {
log.Printf("ERROR WriteCoil: p.ReadCoil: %s", err)
}
log.Printf("Wrote coil %d to value %v (%s: %s)", n, p.Coils[n].Value, p.Coils[n].Symbol, p.Coils[n].Description)
return true
}
@ -440,7 +463,7 @@ func (p *Pingvin) WriteCoils(startaddr uint16, quantity uint16, vals []bool) err
// Some of the coils are mutually exclusive, and can only be 1 one at a time.
// Check if coil is one of them and force all of them to 0 if so
func (p *Pingvin) checkMutexCoils(addr uint16, handler *modbus.RTUClientHandler) error {
func (p *Pingvin) checkMutexCoils(addr uint16) error { //, handler *modbus.RTUClientHandler) error {
for _, mutexcoil := range mutexcoils {
if mutexcoil == addr {
for _, n := range mutexcoils {
@ -462,6 +485,7 @@ func (p *Pingvin) checkMutexCoils(addr uint16, handler *modbus.RTUClientHandler)
// populate p.Status struct for Home Assistant
func (p *Pingvin) populateStatus() {
p.Status = &pingvinStatus{}
hpct := p.Registers[49].Value / p.Registers[49].Multiplier
if hpct > 100 {
p.Status.HeaterPct = hpct - 100