enervent-ctrl/enervent-ctrl-go/pingvinKL/pingvinKL.go

259 lines
7.6 KiB
Go
Raw Normal View History

package pingvinKL
import (
"bufio"
"fmt"
"log"
"os"
"strconv"
"strings"
2023-01-29 22:09:37 +02:00
"sync"
2023-01-29 14:42:03 +02:00
"time"
"github.com/goburrow/modbus"
)
// single coil data
type pingvinCoil struct {
Address int `json:"address"`
Symbol string `json:"symbol"`
Value bool `json:"value"`
Description string `json:"description"`
Reserved bool `json:"reserved"`
}
// unit modbus data
type PingvinKL struct {
Coils []pingvinCoil
Registers []pingvinRegister
Status pingvinStatus
buslock *sync.Mutex
statuslock *sync.Mutex
}
// single register data
type pingvinRegister struct {
Address int `json:"address"`
Symbol string `json:"symbol"`
Value int `json:"value"`
Bitfield string `json:"bitfield"`
Type string `json:"type"`
Description string `json:"description"`
Reserved bool `json:"reserved"`
Multiplier int `json:"multiplier"`
}
type pingvinVentInfo struct {
supplyHeated int `json:"supply_heated"`
supplyHrc int `json:"supply_hrc"`
supplyIntake int `json:"supply_intake"`
supplyIntake24h int `json:"supply_intake_24h"`
supplyHum int `json:"supply_hum"`
extractIntake int `json:"extract_intake"`
extractHrc int `json:"extract_hrc"`
extractHum int `json:"extract_hum"`
extractHum48h int `json:"extract_hum_48h"`
}
type pingvinStatus struct {
HeaterPct int `json:"heater_pct"`
HrcPct int `json:"hrc_pct"`
TempSetting int `json:"temp_setting"`
FanPct int `json:"fan_pct"`
VentInfo pingvinVentInfo `json:"vent_info"`
HrcEffIn int `json:"hrc_efficiency_in"`
HrcEffEx int `json:"hrc_efficiency_ex"`
OpMode string `json:"op_mode"`
DaysUntilService int `json:"days_until_service"`
Uptime string `json:"uptime"`
SystemTime string `json:"system_time"`
}
func newCoil(address string, symbol string, description string) pingvinCoil {
addr, err := strconv.Atoi(address)
if err != nil {
2023-01-29 23:34:43 +02:00
log.Fatal("newCoil: Atoi: ", err)
}
reserved := symbol == "-" && description == "-"
coil := pingvinCoil{addr, symbol, false, description, reserved}
return coil
}
func newRegister(address, symbol, typ, multiplier, description string) pingvinRegister {
addr, err := strconv.Atoi(address)
if err != nil {
log.Fatal("newRegister: Atio: ")
}
multipl, err := strconv.Atoi(multiplier)
if err != nil {
log.Fatal("newRegister: Atio: ")
}
reserved := symbol == "Reserved" && description == "Reserved"
register := pingvinRegister{addr, symbol, 0, "00000000", typ, description, reserved, multipl}
return register
}
// read a CSV file containing data for coils or registers
func readCsvLines(file string) [][]string {
delim := ";"
data := [][]string{}
csv, err := os.Open(file)
if err != nil {
log.Fatal(err)
}
defer csv.Close()
scanner := bufio.NewScanner(csv)
for scanner.Scan() {
elements := strings.Split(scanner.Text(), delim)
data = append(data, elements)
}
if err := scanner.Err(); err != nil {
log.Fatal(err)
}
return data
}
2023-01-29 21:56:22 +02:00
// Configure the modbus parameters
2023-01-29 14:42:03 +02:00
func (p PingvinKL) getHandler() *modbus.RTUClientHandler {
// TODO: read configuration from file, hardcoded for now
handler := modbus.NewRTUClientHandler("/dev/ttyS0")
handler.BaudRate = 19200
handler.DataBits = 8
handler.Parity = "N"
handler.StopBits = 1
handler.SlaveId = 1
2023-01-29 21:56:22 +02:00
handler.Timeout = 1500 * time.Millisecond
2023-01-29 14:42:03 +02:00
return handler
}
func (p PingvinKL) updateCoils() {
2023-01-29 21:56:22 +02:00
handler := p.getHandler()
2023-01-29 22:09:37 +02:00
p.buslock.Lock()
2023-01-29 21:56:22 +02:00
err := handler.Connect()
if err != nil {
log.Fatal("updateCoils: handler.Connect: ", err)
2023-01-29 21:56:22 +02:00
}
defer handler.Close()
client := modbus.NewClient(handler)
results, err := client.ReadCoils(0, uint16(len(p.Coils)))
if err != nil {
log.Fatal("updateCoils: client.ReadCoils: ", err)
2023-01-29 21:56:22 +02:00
}
2023-01-29 22:09:37 +02:00
p.buslock.Unlock()
2023-01-29 21:56:22 +02:00
// modbus.ReadCoils returns a byte array, with the first byte's bits representing coil values 0-7,
// second byte coils 8-15 etc.
// Within each byte, LSB represents the lowest n coil while MSB is the highest
// e.g. reading the first 8 coils might return a byte array of length 1, with the following:
// [4], which is 00000100, meaning all other coils are 0 except coil #2 (3rd coil)
//
k := 0 // pingvinCoil index
for i := 0; i < len(results); i++ { // loop through the byte array
for j := 0; j < 8; j++ {
// Here we loop through each bit in the byte, shifting right
// and checking if the LSB after the shift is 1 with a bitwise AND
// A coil value of 1 means on/true/yes, so == 1 returns the bool value
// for each coil
p.Coils[k].Value = (results[i] >> j & 0x1) == 1
k++
}
}
2023-01-29 14:42:03 +02:00
}
func (p PingvinKL) updateRegisters() {
handler := p.getHandler()
p.buslock.Lock()
err := handler.Connect()
if err != nil {
log.Fatal("updateRegisters: handler.Connect: ", err)
}
defer handler.Close()
client := modbus.NewClient(handler)
regs := len(p.Registers)
k := 0
2023-02-01 00:04:13 +02:00
// modbus.ReadHoldingRegisters can read 125 regs at a time, so first we loop
// until all the values are fethed, increasing the value of k for each register
// When there are less than 125 registers to go, it's the last pass
for k < regs {
r := 125
if regs-k < 125 {
r = regs - k
}
results, err := client.ReadHoldingRegisters(uint16(k), uint16(r))
if err != nil {
log.Fatal("updateRegisters: client.ReadCoils: ", err)
}
2023-02-01 00:04:13 +02:00
// 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
// value, so for each even byte in the reponse slice we bitshift the byte
// left by 8, then add the odd byte as is to the shifted 16-bit value
msb := true
value := int16(0)
uvalue := uint16(0)
for i := 0; i < len(results); i++ {
if msb {
value = int16(results[i]) << 8
uvalue = uint16(results[i]) << 8
} else {
value += int16(results[i])
uvalue += uint16(results[i])
if p.Registers[k].Type == "int16" {
p.Registers[k].Value = int(value)
}
if p.Registers[k].Type == "uint16" || p.Registers[k].Type == "enumeration" {
p.Registers[k].Value = int(uvalue)
}
if p.Registers[k].Type == "bitfield" {
p.Registers[k].Value = int(value)
p.Registers[k].Bitfield = fmt.Sprintf("%08b", uvalue)
}
k++
}
msb = !msb
}
}
p.buslock.Unlock()
}
func (p PingvinKL) Update() {
p.updateCoils()
p.updateRegisters()
}
2023-01-29 14:42:03 +02:00
func (p PingvinKL) ReadCoil(n uint16) []byte {
handler := p.getHandler()
2023-01-29 22:09:37 +02:00
p.buslock.Lock()
2023-01-29 14:42:03 +02:00
err := handler.Connect()
if err != nil {
2023-01-29 21:56:22 +02:00
log.Fatal("ReadCoil: handler.Connect: ", err)
2023-01-29 14:42:03 +02:00
}
defer handler.Close()
client := modbus.NewClient(handler)
results, err := client.ReadCoils(n, 1)
2023-01-29 22:09:37 +02:00
p.buslock.Unlock()
2023-01-29 14:42:03 +02:00
if err != nil {
2023-01-29 23:34:43 +02:00
log.Fatal("ReadCoil: client.ReadCoils: ", err)
2023-01-29 14:42:03 +02:00
}
2023-01-29 22:09:37 +02:00
p.Coils[n].Value = results[0] == 1
2023-01-29 14:42:03 +02:00
return results
}
// create a PingvinKL struct, read coils and registers from CSVs
func New() PingvinKL {
pingvin := PingvinKL{}
2023-01-29 22:09:37 +02:00
pingvin.buslock = &sync.Mutex{}
log.Println("Parsing coil data...")
coilData := readCsvLines("coils.csv")
for i := 0; i < len(coilData); i++ {
pingvin.Coils = append(pingvin.Coils, newCoil(coilData[i][0], coilData[i][1], coilData[i][2]))
}
2023-02-01 22:19:57 +02:00
log.Println("Parsed", len(pingvin.Coils), "coils")
log.Println("Parsing register data...")
registerData := readCsvLines("registers.csv")
for i := 0; i < len(registerData); i++ {
pingvin.Registers = append(pingvin.Registers,
newRegister(registerData[i][0], registerData[i][1], registerData[i][2], registerData[i][3], registerData[i][6]))
}
2023-02-01 22:19:57 +02:00
log.Println("Parsed", len(pingvin.Registers), "registers")
return pingvin
}