From 027d678a09c3d8facde2a744b68a7b5bcf676fd1 Mon Sep 17 00:00:00 2001 From: Jarno Rankinen Date: Sun, 21 Jan 2024 11:34:45 +0200 Subject: [PATCH] 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 --- build.sh | 16 ++++++-- main.go | 40 ++++++++++++-------- pingvin/pingvin.go | 94 +++++++++++++++++++++++++++++----------------- 3 files changed, 96 insertions(+), 54 deletions(-) diff --git a/build.sh b/build.sh index ec8882a..5ab485d 100755 --- a/build.sh +++ b/build.sh @@ -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 \ No newline at end of file +rm -rf ./* + +popd &> /dev/null || exit 1 \ No newline at end of file diff --git a/main.go b/main.go index db6c155..59d9c96 100644 --- a/main.go +++ b/main.go @@ -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) } } diff --git a/pingvin/pingvin.go b/pingvin/pingvin.go index e55fdad..76d9d4a 100644 --- a/pingvin/pingvin.go +++ b/pingvin/pingvin.go @@ -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