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

View File

@ -26,9 +26,9 @@ type pingvinCoil struct {
// unit modbus data // unit modbus data
type Pingvin struct { type Pingvin struct {
Coils []pingvinCoil Coils []*pingvinCoil
Registers []pingvinRegister Registers []*pingvinRegister
Status pingvinStatus Status *pingvinStatus
buslock *sync.Mutex buslock *sync.Mutex
handler *modbus.RTUClientHandler handler *modbus.RTUClientHandler
modbusclient modbus.Client modbusclient modbus.Client
@ -73,7 +73,7 @@ type pingvinStatus struct {
OpMode string `json:"op_mode"` // Current operating mode, text representation OpMode string `json:"op_mode"` // Current operating mode, text representation
Uptime string `json:"uptime"` // Unit uptime Uptime string `json:"uptime"` // Unit uptime
SystemTime string `json:"system_time"` // Time and date in unit SystemTime string `json:"system_time"` // Time and date in unit
Coils []pingvinCoil `json:"coils"` Coils []*pingvinCoil `json:"coils"`
} }
type PingvinLogger struct { 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) addr, err := strconv.Atoi(address)
if err != nil { if err != nil {
log.Fatal("newCoil: Atoi: ", err) log.Fatal("newCoil: Atoi: ", err)
@ -111,7 +111,7 @@ func newCoil(address string, symbol string, description string) pingvinCoil {
promdesc := strings.ToLower(symbol) promdesc := strings.ToLower(symbol)
zpadaddr := fmt.Sprintf("%02d", addr) zpadaddr := fmt.Sprintf("%02d", addr)
promdesc = strings.Replace(promdesc, "_", "_"+zpadaddr+"_", 1) promdesc = strings.Replace(promdesc, "_", "_"+zpadaddr+"_", 1)
return pingvinCoil{addr, symbol, false, description, reserved, return &pingvinCoil{addr, symbol, false, description, reserved,
prometheus.NewDesc( prometheus.NewDesc(
prometheus.BuildFQName("", "pingvin", promdesc), prometheus.BuildFQName("", "pingvin", promdesc),
description, 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) addr, err := strconv.Atoi(address)
if err != nil { if err != nil {
log.Fatal("newRegister: Atoi(address): ", err) log.Fatal("newRegister: Atoi(address): ", err)
@ -141,7 +141,7 @@ func newRegister(address, symbol, typ, multiplier, description string) pingvinRe
promdesc := strings.ToLower(symbol) promdesc := strings.ToLower(symbol)
zpadaddr := fmt.Sprintf("%03d", addr) zpadaddr := fmt.Sprintf("%03d", addr)
promdesc = strings.Replace(promdesc, "_", "_"+zpadaddr+"_", 1) promdesc = strings.Replace(promdesc, "_", "_"+zpadaddr+"_", 1)
return pingvinRegister{ return &pingvinRegister{
addr, addr,
symbol, symbol,
0, 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 // read a CSV file containing data for coils or registers
@ -210,11 +210,23 @@ func (p *Pingvin) Quit() {
// Update all coil values // Update all coil values
func (p *Pingvin) updateCoils() { func (p *Pingvin) updateCoils() {
p.buslock.Lock() var results []byte
results, err := p.modbusclient.ReadCoils(0, uint16(len(p.Coils))) var err error
p.buslock.Unlock() for retries := 1; retries <= 5; retries++ {
if err != nil { p.Debug.Println("Reading coils, attempt", retries)
log.Fatal("updateCoils: client.ReadCoils: ", err) 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, // modbus.ReadCoils returns a byte array, with the first byte's bits representing coil values 0-7,
// second byte coils 8-15 etc. // 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) results, err := p.modbusclient.ReadHoldingRegisters(addr, 1)
p.buslock.Unlock() p.buslock.Unlock()
if err != nil { if err != nil {
log.Println("ERROR: ReadRegister:", err) //log.Println("ERROR: ReadRegister:", err)
return 0, err return p.Registers[addr].Value, err
} }
if p.Registers[addr].Type == "uint16" { if p.Registers[addr].Type == "uint16" {
p.Registers[addr].Value = int(uint16(results[0]) << 8) 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 return 0, err
} }
if val == int(value) { 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 value, nil
} }
return 0, fmt.Errorf("Failed to write register") return 0, fmt.Errorf("Failed to write register")
@ -288,8 +301,8 @@ func (p *Pingvin) updateRegisters() {
if regs-k < 125 { if regs-k < 125 {
r = regs - k r = regs - k
} }
results := []byte{} var results []byte
for retries := 0; retries < 5; retries++ { for retries := 1; retries <= 5; retries++ {
p.Debug.Println("Reading registers, attempt", retries, "k:", k) p.Debug.Println("Reading registers, attempt", retries, "k:", k)
p.buslock.Lock() p.buslock.Lock()
results, err = p.modbusclient.ReadHoldingRegisters(uint16(k), uint16(r)) results, err = p.modbusclient.ReadHoldingRegisters(uint16(k), uint16(r))
@ -299,8 +312,9 @@ func (p *Pingvin) updateRegisters() {
} else if retries == 4 { } else if retries == 4 {
log.Fatal("updateRegisters: client.ReadHoldingRegisters: ", err) log.Fatal("updateRegisters: client.ReadHoldingRegisters: ", err)
} else if err != nil { } 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 // 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 // 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 // Read single coil
func (p *Pingvin) ReadCoil(n uint16) ([]byte, error) { func (p *Pingvin) ReadCoil(n uint16) (err error) {
p.buslock.Lock() var results []byte
results, err := p.modbusclient.ReadCoils(n, 1) for retries := 1; retries <= 5; retries++ {
p.buslock.Unlock() p.buslock.Lock()
if err != nil { results, err = p.modbusclient.ReadCoils(n, 1)
log.Fatal("ReadCoil: client.ReadCoils: ", err) p.buslock.Unlock()
return nil, err 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 p.Coils[n].Value = results[0] == 1
return results, nil return
} }
// Force a single coil // Force a single coil
func (p *Pingvin) WriteCoil(n uint16, val bool) bool { func (p *Pingvin) WriteCoil(n uint16, val bool) bool {
if val { if val {
p.checkMutexCoils(n, p.handler) _ = p.checkMutexCoils(n) //, p.handler)
} }
var value uint16 = 0 var value uint16 = 0
if val { if val {
@ -377,14 +399,15 @@ func (p *Pingvin) WriteCoil(n uint16, val bool) bool {
if err != nil { if err != nil {
log.Println("ERROR: WriteCoil: ", err) log.Println("ERROR: WriteCoil: ", err)
} }
if (val && results[0] == 255) || (!val && results[0] == 0) { if (val && results[0] != 255) && (!val && results[0] != 0) {
log.Println("WriteCoil: wrote coil", n, "to value", val)
} else {
log.Println("ERROR: WriteCoil: failed to write coil") log.Println("ERROR: WriteCoil: failed to write coil")
return false 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 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. // 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 // 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 { for _, mutexcoil := range mutexcoils {
if mutexcoil == addr { if mutexcoil == addr {
for _, n := range mutexcoils { for _, n := range mutexcoils {
@ -462,6 +485,7 @@ func (p *Pingvin) checkMutexCoils(addr uint16, handler *modbus.RTUClientHandler)
// populate p.Status struct for Home Assistant // populate p.Status struct for Home Assistant
func (p *Pingvin) populateStatus() { func (p *Pingvin) populateStatus() {
p.Status = &pingvinStatus{}
hpct := p.Registers[49].Value / p.Registers[49].Multiplier hpct := p.Registers[49].Value / p.Registers[49].Multiplier
if hpct > 100 { if hpct > 100 {
p.Status.HeaterPct = hpct - 100 p.Status.HeaterPct = hpct - 100