From fcd937c52a4d42e104de3b5c1dfdb224d97f050d Mon Sep 17 00:00:00 2001 From: Rob Watson Date: Sun, 10 Jul 2022 10:47:12 +0200 Subject: [PATCH] Read runtime data using Modbus protocol --- command/aa55.go | 69 ++++++++++++++++++++++++++++++++++++++ command/command.go | 36 ++++++++++++++++++++ command/modbus.go | 63 +++++++++++++++++++++++++++++++++++ main.go | 82 +++++++--------------------------------------- 4 files changed, 179 insertions(+), 71 deletions(-) create mode 100644 command/aa55.go create mode 100644 command/command.go create mode 100644 command/modbus.go diff --git a/command/aa55.go b/command/aa55.go new file mode 100644 index 0000000..d40212d --- /dev/null +++ b/command/aa55.go @@ -0,0 +1,69 @@ +package command + +import ( + "encoding/binary" + "encoding/hex" + "fmt" +) + +const ( + aa55Header = "AA55C07F" + aa55ResponseLengthIndex = 6 + aa55ResponseLengthOffset = 9 +) + +type AA55Command struct { + payload []byte + responseType string +} + +func NewAA55(payload, responseType string) (*AA55Command, error) { + bytes, err := hex.DecodeString(aa55Header + payload) + if err != nil { + return nil, fmt.Errorf("error parsing payload: %s", err) + } + + bytes = append(bytes, aa55Checksum(bytes)...) + + return &AA55Command{payload: bytes, responseType: responseType}, nil +} + +func aa55Checksum(payload []byte) []byte { + var v uint16 + for _, b := range payload { + v += uint16(b) + } + + c := make([]byte, 4) + binary.BigEndian.PutUint16(c, v) + return c +} + +func (cmd AA55Command) String() string { return string(cmd.payload) } + +func (cmd AA55Command) validateResponse(p []byte) error { + if len(p) < 8 { + return fmt.Errorf("response truncated") + } + + expectedLen := int(p[aa55ResponseLengthIndex] + aa55ResponseLengthOffset) + if len(p) != expectedLen { + return fmt.Errorf("unexpected response length %d (expected %d)", len(p), expectedLen) + } + + responseType := hex.EncodeToString(p[4:6]) + if responseType != cmd.responseType { + return fmt.Errorf("unexpected response type `%s` (expected `%s`)", responseType, cmd.responseType) + } + + var sum uint16 + for _, b := range p[:len(p)-2] { + sum += uint16(b) + } + expSum := binary.BigEndian.Uint16(p[len(p)-2:]) + if sum != expSum { + return fmt.Errorf("invalid response checksum %d (expected %d)", sum, expSum) + } + + return nil +} diff --git a/command/command.go b/command/command.go new file mode 100644 index 0000000..ecd4d41 --- /dev/null +++ b/command/command.go @@ -0,0 +1,36 @@ +package command + +import ( + "bufio" + "fmt" + "io" + "log" +) + +type command interface { + String() string + validateResponse([]byte) error +} + +func Send(cmd command, conn io.ReadWriter) ([]byte, error) { + _, err := fmt.Fprint(conn, cmd.String()) + if err != nil { + return nil, fmt.Errorf("error writing to socket: %s", err) + } + + log.Printf("sent data to socket: %X", cmd) + + p := make([]byte, 4_096) + r := bufio.NewReader(conn) + n, err := r.Read(p) + if err != nil { + return nil, fmt.Errorf("error reading from socket: %s", err) + } + p = p[:n] + + if err := cmd.validateResponse(p); err != nil { + return nil, fmt.Errorf("error validating response: %s", err) + } + + return p, nil +} diff --git a/command/modbus.go b/command/modbus.go new file mode 100644 index 0000000..7c3babf --- /dev/null +++ b/command/modbus.go @@ -0,0 +1,63 @@ +package command + +var modbusCrcTable []uint16 + +func init() { + for i := 0; i < 256; i++ { + buffer := uint16(i << 1) + var crc uint16 + for j := 8; j > 0; j-- { + buffer >>= 1 + if (buffer^crc)&0x0001 != 0 { + crc = (crc >> 1) ^ 0xA001 + } else { + crc >>= 1 + } + } + modbusCrcTable = append(modbusCrcTable, crc) + } +} + +type ModbusCommand struct { + payload []byte +} + +const modbusComAddr byte = 0xf7 + +type ModbusCommandType byte + +const ( + ModbusCommandTypeRead ModbusCommandType = 0x03 + ModbusCommandTypeWrite ModbusCommandType = 0x06 + ModbusCommandTypeWriteMulti ModbusCommandType = 0x10 +) + +func NewModbus(commandType ModbusCommandType, offset uint16, value uint16) *ModbusCommand { + var p []byte + p = append(p, modbusComAddr) + p = append(p, byte(commandType)) + p = append(p, byte((offset>>8)&0xff)) + p = append(p, byte(offset&0xff)) + p = append(p, byte((value>>8)&0xff)) + p = append(p, byte(value&0xff)) + + sum := modbusChecksum(p) + p = append(p, byte(sum&0xff)) + p = append(p, byte((sum>>8)&0xff)) + + return &ModbusCommand{payload: p} +} + +func modbusChecksum(b []byte) uint16 { + crc := uint16(0xffff) + for _, v := range b { + crc = (crc >> 8) ^ modbusCrcTable[(crc^uint16(v))&0xff] + } + return crc +} + +func (cmd ModbusCommand) String() string { return string(cmd.payload) } + +func (cmd ModbusCommand) validateResponse(p []byte) error { + return nil +} diff --git a/main.go b/main.go index 431fa3e..17e8720 100644 --- a/main.go +++ b/main.go @@ -1,20 +1,14 @@ package main import ( - "bufio" - "encoding/binary" - "encoding/hex" "flag" "fmt" - "io" "log" "net" "os" "strings" -) -const ( - readBufSizeBytes = 4_096 + "git.netflux.io/rob/goodwe-go/command" ) func main() { @@ -35,7 +29,12 @@ func main() { } defer conn.Close() - resp, err := sendAA55Command(conn, "010200", "0182") + infoCmd, err := command.NewAA55("010200", "0182") + if err != nil { + log.Fatalf("error building command: %s", err) + } + + resp, err := command.Send(infoCmd, conn) if err != nil { log.Fatalf("error sending command: %s", err) } @@ -44,71 +43,12 @@ func main() { serialNum := string(resp[38:54]) log.Printf("modelName = %q, serialNum = %q\n", modelName, serialNum) -} -func sendAA55Command(conn io.ReadWriter, payload, expectedResponseType string) ([]byte, error) { - bytes, err := hex.DecodeString("AA55C07F" + payload) + dataCmd := command.NewModbus(command.ModbusCommandTypeRead, 0x891c, 0x007d) + resp, err = command.Send(dataCmd, conn) if err != nil { - return nil, fmt.Errorf("error decoding hex string: %s", err) + log.Fatalf("error sending command: %s", err) } - bytes = append(bytes, checksum(bytes)...) - - _, err = fmt.Fprint(conn, string(bytes)) - if err != nil { - return nil, fmt.Errorf("error writing to socket: %s", err) - } - - log.Printf("sent data to socket: %X", bytes) - - p := make([]byte, readBufSizeBytes) - n, err := bufio.NewReader(conn).Read(p) - if err != nil { - return nil, fmt.Errorf("error reading from socket: %s", err) - } - - return validateResponse(p[:n], expectedResponseType) -} - -const ( - aa55ResponseLengthIndex = 6 - aa55ResponseLengthOffset = 9 -) - -func validateResponse(p []byte, expectedResponseType string) ([]byte, error) { - if len(p) < 8 { - return nil, fmt.Errorf("response truncated") - } - - expectedLen := int(p[aa55ResponseLengthIndex] + aa55ResponseLengthOffset) - if len(p) != expectedLen { - return nil, fmt.Errorf("unexpected response length %d (expected %d)", len(p), expectedLen) - } - - responseType := hex.EncodeToString(p[4:6]) - if responseType != expectedResponseType { - return nil, fmt.Errorf("unexpected response type `%s` (expected `%s`)", responseType, expectedResponseType) - } - - var s uint16 - for _, b := range p[:len(p)-2] { - s += uint16(b) - } - expSum := binary.BigEndian.Uint16(p[len(p)-2:]) - if s != expSum { - return nil, fmt.Errorf("invalid response checksum %d (expected %d)", s, expSum) - } - - return p, nil -} - -func checksum(p []byte) []byte { - var v uint16 - for _, byte := range p { - v += uint16(byte) - } - - c := make([]byte, 4) - binary.BigEndian.PutUint16(c, v) - return c + log.Printf("rcvd modbus resp = %X", resp) }