diff --git a/Makefile b/Makefile index 4ef1a6d..15a4048 100644 --- a/Makefile +++ b/Makefile @@ -13,6 +13,7 @@ test: diagslave -m tcp -p 5020 & diagslave -m enc -p 5021 & go test -run TCP -v $(shell glide nv) socat -d -d pty,raw,echo=0 pty,raw,echo=0 & diagslave -m rtu /dev/pts/1 & go test -run RTU -v $(shell glide nv) socat -d -d pty,raw,echo=0 pty,raw,echo=0 & diagslave -m ascii /dev/pts/3 & go test -run ASCII -v $(shell glide nv) + go test -v -count=1 github.com/grid-x/modbus/cmd/modbus-cli .PHONY: lint lint: diff --git a/cmd/modbus-cli/main.go b/cmd/modbus-cli/main.go index cdf4b37..7a4e8d9 100644 --- a/cmd/modbus-cli/main.go +++ b/cmd/modbus-cli/main.go @@ -42,17 +42,19 @@ func main() { flag.BoolVar(&opt.rtu.rs485.rxDuringTx, "rs485-rxDuringTx", false, "Allow bidirectional rx during tx") var ( - register = flag.Int("register", -1, "") - fnCode = flag.Int("fn-code", 0x03, "fn") - quantity = flag.Int("quantity", 2, "register quantity, length in bytes") - ignoreCRCError = flag.Bool("ignore-crc", false, "ignore crc") - eType = flag.String("type-exec", "uint16", "") - pType = flag.String("type-parse", "raw", "type to parse the register result. Use 'raw' if you want to see the raw bits and bytes. Use 'all' if you want to decode the result to different commonly used formats.") - writeValue = flag.Float64("write-value", math.MaxFloat64, "") - parseBigEndian = flag.Bool("order-parse-bigendian", true, "t: big, f: little") - execBigEndian = flag.Bool("order-exec-bigendian", true, "t: big, f: little") - filename = flag.String("filename", "", "") - logframe = flag.Bool("log-frame", false, "prints received and send modbus frame to stdout") + register = flag.Int("register", -1, "") + fnCode = flag.Int("fn-code", 0x03, "fn") + quantity = flag.Int("quantity", 2, "register quantity, length in bytes") + ignoreCRCError = flag.Bool("ignore-crc", false, "ignore crc") + eType = flag.String("type-exec", "uint16", "") + pType = flag.String("type-parse", "raw", "type to parse the register result. Use 'raw' if you want to see the raw bits and bytes. Use 'all' if you want to decode the result to different commonly used formats.") + writeValue = flag.Float64("write-value", math.MaxFloat64, "") + readParseOrder = flag.String("read-parse-order", "", "order to parse the register that was read out. Valid values: [AB, BA, ABCD, DCBA, BADC, CDAB]. Can only be used for 16bit (1 register) and 32bit (2 registers). If used, it will overwrite the big-endian or little-endian parameter.") + writeParseOrder = flag.String("write-exec-order", "", "order to execute the register(s) that should be written to. Valid values: [AB, BA, ABCD, DCBA, BADC, CDAB]. Can only be used for 16bit (1 register) and 32bit (2 registers). If used, it will overwrite the big-endian or little-endian parameter.") + parseBigEndian = flag.Bool("order-parse-bigendian", true, "t: big, f: little") + execBigEndian = flag.Bool("order-exec-bigendian", true, "t: big, f: little") + filename = flag.String("filename", "", "") + logframe = flag.Bool("log-frame", false, "prints received and send modbus frame to stdout") ) flag.Parse() @@ -96,7 +98,7 @@ func main() { client := modbus.NewClient(handler) - result, err := exec(client, eo, *register, *fnCode, *writeValue, *eType, *quantity) + result, err := exec(client, eo, *writeParseOrder, *register, *fnCode, *writeValue, *eType, *quantity) if err != nil && strings.Contains(err.Error(), "crc") && *ignoreCRCError { logger.Printf("ignoring crc error: %+v\n", err) } else if err != nil { @@ -110,7 +112,7 @@ func main() { case "all": res, err = resultToAllString(result) default: - res, err = resultToString(result, po, *pType) + res, err = resultToString(result, po, *readParseOrder, *pType) } if err != nil { @@ -130,12 +132,15 @@ func main() { func exec( client modbus.Client, o binary.ByteOrder, + forcedOrder string, register int, fnCode int, wval float64, etype string, quantity int, -) (result []byte, err error) { +) ([]byte, error) { + var err error + var result []byte switch fnCode { case 0x01: result, err = client.ReadCoils(uint16(register), uint16(quantity)) @@ -153,40 +158,14 @@ func exec( max := float64(math.MaxUint16) if wval > max || wval < 0 { err = fmt.Errorf("overflow: %f does not fit into datatype uint16", wval) - return + break } result, err = client.WriteSingleRegister(uint16(register), uint16(wval)) case 0x10: - w := newWriter(o) var buf []byte - switch etype { - case "uint16": - max := float64(math.MaxUint16) - if wval > max || wval < 0 { - err = fmt.Errorf("overflow: %f does not fit into datatype %s", wval, etype) - return - } - buf = make([]byte, 2) - w.PutUint16(buf, uint16(wval)) - case "uint32": - max := float64(math.MaxUint32) - if wval > max || wval < 0 { - err = fmt.Errorf("overflow: %f does not fit into datatype %s", wval, etype) - return - } - buf = make([]byte, 4) - w.PutUint32(buf, uint32(wval)) - case "float32": - max := float64(math.MaxFloat32) - if wval > max || wval < 0 { - err = fmt.Errorf("overflow: %f does not fit into datatype %s", wval, etype) - return - } - buf = make([]byte, 4) - w.PutFloat32(buf, float32(wval)) - case "float64": - buf = make([]byte, 8) - w.PutFloat64(buf, float64(wval)) + buf, err = convertToBytes(etype, o, forcedOrder, wval) + if err != nil { + break } result, err = client.WriteMultipleRegisters(uint16(register), uint16(len(buf))/2, buf) case 0x04: @@ -196,7 +175,58 @@ func exec( default: err = fmt.Errorf("function code %d is unsupported", fnCode) } - return + return result, err +} + +func convertToBytes(eType string, order binary.ByteOrder, forcedOrder string, val float64) ([]byte, error) { + fo := strings.ToUpper(forcedOrder) + switch fo { + case "": + // nothing is forced + case "AB", "ABCD", "BADC": + order = binary.BigEndian + case "BA", "DCBA", "CDAB": + order = binary.LittleEndian + default: + return nil, fmt.Errorf("forced order %s not known", strings.ToUpper(forcedOrder)) + } + + w := newWriter(order) + var buf []byte + var err error + switch eType { + case "uint16": + max := float64(math.MaxUint16) + if val > max || val < 0 { + err = fmt.Errorf("overflow: %f does not fit into datatype %s", val, eType) + break + } + buf = w.ToUint16(uint16(val)) + case "uint32": + max := float64(math.MaxUint32) + if val > max || val < 0 { + err = fmt.Errorf("overflow: %f does not fit into datatype %s", val, eType) + break + } + buf = w.ToUint32(uint32(val)) + case "float32": + max := float64(math.MaxFloat32) + min := -float64(math.MaxFloat32) + if val > max || val < min { + err = fmt.Errorf("overflow: %f does not fit into datatype %s", val, eType) + break + } + buf = w.ToFloat32(float32(val)) + case "float64": + buf = w.ToFloat64(float64(val)) + } + + // flip bytes when CDAB or BADC are used (and we have 4 bytes) + if fo == "CDAB" || fo == "BADC" && len(buf) == 4 { + buf = []byte{buf[1], buf[0], buf[3], buf[2]} + } + + return buf, err } func resultToFile(r []byte, filename string) error { @@ -218,19 +248,19 @@ func resultToAllString(result []byte) (string, error) { switch len(result) { case 2: - bigUint16, err := resultToString(result, binary.BigEndian, "uint16") + bigUint16, err := resultToString(result, binary.BigEndian, "", "uint16") if err != nil { return "", err } - bigInt16, err := resultToString(result, binary.BigEndian, "int16") + bigInt16, err := resultToString(result, binary.BigEndian, "", "int16") if err != nil { return "", err } - littleUint16, err := resultToString(result, binary.LittleEndian, "uint16") + littleUint16, err := resultToString(result, binary.LittleEndian, "", "uint16") if err != nil { return "", err } - littleInt16, err := resultToString(result, binary.LittleEndian, "int16") + littleInt16, err := resultToString(result, binary.LittleEndian, "", "int16") if err != nil { return "", err } @@ -248,55 +278,52 @@ func resultToAllString(result []byte) (string, error) { return buf.String(), nil case 4: - bigUint32, err := resultToString(result, binary.BigEndian, "uint32") + bigUint32, err := resultToString(result, binary.BigEndian, "", "uint32") if err != nil { return "", err } - bigInt32, err := resultToString(result, binary.BigEndian, "int32") + bigInt32, err := resultToString(result, binary.BigEndian, "", "int32") if err != nil { return "", err } - bigFloat32, err := resultToString(result, binary.BigEndian, "float32") + bigFloat32, err := resultToString(result, binary.BigEndian, "", "float32") if err != nil { return "", err } - littleUint32, err := resultToString(result, binary.LittleEndian, "uint32") + littleUint32, err := resultToString(result, binary.LittleEndian, "", "uint32") if err != nil { return "", err } - littleInt32, err := resultToString(result, binary.LittleEndian, "int32") + littleInt32, err := resultToString(result, binary.LittleEndian, "", "int32") if err != nil { return "", err } - littleFloat32, err := resultToString(result, binary.LittleEndian, "float32") + littleFloat32, err := resultToString(result, binary.LittleEndian, "", "float32") if err != nil { return "", err } - // flip result - result := []byte{result[1], result[0], result[3], result[2]} - - midBigUint32, err := resultToString(result, binary.BigEndian, "uint32") + midBigUint32, err := resultToString(result, binary.BigEndian, "BADC", "uint32") if err != nil { return "", err } - midBigInt32, err := resultToString(result, binary.BigEndian, "int32") + midBigInt32, err := resultToString(result, binary.BigEndian, "BADC", "int32") if err != nil { return "", err } - midBigFloat32, err := resultToString(result, binary.BigEndian, "float32") + midBigFloat32, err := resultToString(result, binary.BigEndian, "BADC", "float32") if err != nil { return "", err } - midLittleUint32, err := resultToString(result, binary.LittleEndian, "uint32") + midLittleUint32, err := resultToString(result, binary.LittleEndian, "CDAB", "uint32") if err != nil { return "", err } - midLittleInt32, err := resultToString(result, binary.LittleEndian, "int32") + midLittleInt32, err := resultToString(result, binary.LittleEndian, "CDAB", "int32") if err != nil { return "", err } - midLittleFloat32, err := resultToString(result, binary.LittleEndian, "float32") + midLittleFloat32, err := resultToString(result, binary.LittleEndian, "CDAB", "float32") if err != nil { return "", err } @@ -326,7 +353,24 @@ func resultToAllString(result []byte) (string, error) { } } -func resultToString(r []byte, order binary.ByteOrder, varType string) (string, error) { +func resultToString(r []byte, order binary.ByteOrder, forcedOrder string, varType string) (string, error) { + fo := strings.ToUpper(forcedOrder) + switch fo { + case "": + // nothing is forced + case "AB", "ABCD", "BADC": + order = binary.BigEndian + case "BA", "DCBA", "CDAB": + order = binary.LittleEndian + default: + return "", fmt.Errorf("forced order %s not known", strings.ToUpper(forcedOrder)) + } + + if fo == "CDAB" || fo == "BADC" && len(r) == 4 { + // flip result + r = []byte{r[1], r[0], r[3], r[2]} + } + switch varType { case "string": return string(r), nil @@ -432,33 +476,44 @@ func newHandler(o option) (modbus.ClientHandler, error) { return nil, fmt.Errorf("unsupported scheme: %s", u.Scheme) } -type binaryWriter interface { - PutUint32(b []byte, v uint32) - PutUint16(b []byte, v uint16) - PutFloat32(b []byte, v float32) - PutFloat64(b []byte, v float64) -} - func newWriter(o binary.ByteOrder) *writer { - return &writer{o} + return &writer{order: o} } type writer struct { - binary.ByteOrder + order binary.ByteOrder +} + +func (w *writer) ToUint16(v uint16) []byte { + var buf bytes.Buffer + w.to(&buf, v) + b, _ := io.ReadAll(&buf) + return b +} + +func (w *writer) ToUint32(v uint32) []byte { + var buf bytes.Buffer + w.to(&buf, v) + b, _ := io.ReadAll(&buf) + return b } -func (w *writer) PutFloat32(b []byte, v float32) { - buf := bytes.NewBuffer(b) - w.to(buf, v) +func (w *writer) ToFloat32(v float32) []byte { + var buf bytes.Buffer + w.to(&buf, v) + b, _ := io.ReadAll(&buf) + return b } -func (w *writer) PutFloat64(b []byte, v float64) { - buf := bytes.NewBuffer(b) - w.to(buf, v) +func (w *writer) ToFloat64(v float64) []byte { + var buf bytes.Buffer + w.to(&buf, v) + b, _ := io.ReadAll(&buf) + return b } func (w *writer) to(buf io.Writer, f interface{}) { - if err := binary.Write(buf, w.ByteOrder, f); err != nil { + if err := binary.Write(buf, w.order, f); err != nil { panic(fmt.Sprintf("binary.Write failed: %s", err.Error())) } } diff --git a/cmd/modbus-cli/main_test.go b/cmd/modbus-cli/main_test.go new file mode 100644 index 0000000..aea3040 --- /dev/null +++ b/cmd/modbus-cli/main_test.go @@ -0,0 +1,285 @@ +package main + +import ( + "encoding/binary" + "math" + "testing" + + "github.com/google/go-cmp/cmp" +) + +func TestConvertBytes(t *testing.T) { + type testCase struct { + name string + eType string + order binary.ByteOrder + val float64 + expected []byte + expectError bool + } + + tests := []testCase{ + // UInt 16 + { + name: "convert uint16 - be - valid value", + eType: "uint16", + order: binary.BigEndian, + val: 42, + expected: []byte{0x00, 0x2A}, + }, + { + name: "convert uint16 - le - valid value", + eType: "uint16", + order: binary.LittleEndian, + val: 42, + expected: []byte{0x2A, 0x00}, + }, + { + name: "convert uint16 - be - max", + eType: "uint16", + order: binary.BigEndian, + val: math.MaxUint16, + expected: []byte{0xFF, 0xFF}, + }, + { + name: "convert uint16 - le - max", + eType: "uint16", + order: binary.LittleEndian, + val: math.MaxUint16, + expected: []byte{0xFF, 0xFF}, + }, + { + name: "convert uint16 - negative value", + eType: "uint16", + order: binary.LittleEndian, + val: -42, + expectError: true, + }, + // UInt32 + { + name: "convert uint32 - be - valid value", + eType: "uint32", + order: binary.BigEndian, + val: 42, + expected: []byte{0x00, 0x00, 0x00, 0x2A}, + }, + { + name: "convert uint32 - le - valid value", + eType: "uint32", + order: binary.LittleEndian, + val: 42, + expected: []byte{0x2A, 0x00, 0x00, 0x00}, + }, + { + name: "convert uint32 - be - max", + eType: "uint32", + order: binary.BigEndian, + val: math.MaxUint32, + expected: []byte{0xFF, 0xFF, 0xFF, 0xFF}, + }, + { + name: "convert uint32 - le - max", + eType: "uint32", + order: binary.LittleEndian, + val: math.MaxUint32, + expected: []byte{0xFF, 0xFF, 0xFF, 0xFF}, + }, + { + name: "convert uint32 - negative value", + eType: "uint32", + order: binary.LittleEndian, + val: -42, + expectError: true, + }, + // Float32 + { + name: "convert float32 - be - valid value", + eType: "float32", + order: binary.BigEndian, + val: 42, + expected: []byte{0x42, 0x28, 0x00, 0x00}, + }, + { + name: "convert float32 - le - valid value", + eType: "float32", + order: binary.LittleEndian, + val: 42, + expected: []byte{0x00, 0x00, 0x28, 0x42}, + }, + { + name: "convert float32 - be - max", + eType: "float32", + order: binary.BigEndian, + val: math.MaxFloat32, + expected: []byte{0x7F, 0x7F, 0xFF, 0xFF}, + }, + { + name: "convert float32 - le - max", + eType: "float32", + order: binary.LittleEndian, + val: math.MaxFloat32, + expected: []byte{0xFF, 0xFF, 0x7F, 0x7F}, + }, + { + name: "convert float32 - negative value", + eType: "float32", + order: binary.BigEndian, + val: -42, + expected: []byte{0xC2, 0x28, 0x00, 0x00}, + }, + } + + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + bytes, err := convertToBytes(tc.eType, tc.order, "", tc.val) + if err != nil && tc.expectError == false { + t.Errorf("exepcted no error but got %v", err) + return + } + + if tc.expectError && err == nil { + t.Error("expected an error but didn't get one") + return + } + + // when in error, we don't need to compare anything + if err != nil { + return + } + + if !cmp.Equal(bytes, tc.expected) { + t.Errorf("expected %v but got %v. Diff: %v", tc.expected, bytes, cmp.Diff(tc.expected, bytes)) + } + + }) + } +} + +func TestForcedOrder(t *testing.T) { + type testCase struct { + name string + eType string + order binary.ByteOrder + forcedOrder string + val float64 + expected []byte + expectError bool + } + + tests := []testCase{ + // UInt 16 + { + name: "convert uint16, AB", + eType: "uint16", + order: binary.LittleEndian, // this should be overwritten by the forced order + forcedOrder: "AB", + val: 42, + expected: []byte{0x00, 0x2A}, + }, + { + name: "convert uint16, BA", + eType: "uint16", + order: binary.BigEndian, // this should be overwritten by the forced order + forcedOrder: "BA", + val: 42, + expected: []byte{0x2A, 0x00}, + }, + // UInt 32 + { + name: "convert uint32, ABCD", + eType: "uint32", + order: binary.LittleEndian, // this should be overwritten by the forced order + forcedOrder: "ABCD", + val: 42, + expected: []byte{0x00, 0x00, 0x00, 0x2A}, + }, + { + name: "convert uint32, DCBA", + eType: "uint32", + order: binary.BigEndian, // this should be overwritten by the forced order + forcedOrder: "DCBA", + val: 42, + expected: []byte{0x2A, 0x00, 0x00, 0x00}, + }, + { + name: "convert uint32, BADC", + eType: "uint32", + order: binary.BigEndian, // this should be overwritten by the forced order + forcedOrder: "BADC", + val: 42, + expected: []byte{0x00, 0x00, 0x2A, 0x00}, + }, + { + name: "convert uint32, CDAB", + eType: "uint32", + order: binary.LittleEndian, // this should be overwritten by the forced order + forcedOrder: "CDAB", + val: 42, + expected: []byte{0x00, 0x2A, 0x00, 0x00}, + }, + // Float 32 - TBD + { + name: "convert uint32, ABCD", + eType: "uint32", + order: binary.LittleEndian, // this should be overwritten by the forced order + forcedOrder: "ABCD", + val: 42, + expected: []byte{0x00, 0x00, 0x00, 0x2A}, + }, + { + name: "convert uint32, DCBA", + eType: "uint32", + order: binary.BigEndian, // this should be overwritten by the forced order + forcedOrder: "DCBA", + val: 42, + expected: []byte{0x2A, 0x00, 0x00, 0x00}, + }, + { + name: "convert uint32, BADC", + eType: "uint32", + order: binary.BigEndian, // this should be overwritten by the forced order + forcedOrder: "BADC", + val: 42, + expected: []byte{0x00, 0x00, 0x2A, 0x00}, + }, + { + name: "convert uint32, CDAB", + eType: "uint32", + order: binary.LittleEndian, // this should be overwritten by the forced order + forcedOrder: "CDAB", + val: 42, + expected: []byte{0x00, 0x2A, 0x00, 0x00}, + }, + // Error cases + { + name: "invalid forced order", + forcedOrder: "CDBA", + expectError: true, + }, + } + + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + bytes, err := convertToBytes(tc.eType, tc.order, tc.forcedOrder, tc.val) + if err != nil && tc.expectError == false { + t.Errorf("exepcted no error but got %v", err) + return + } + + if tc.expectError && err == nil { + t.Error("expected an error but didn't get one") + return + } + + // when in error, we don't need to compare anything + if err != nil { + return + } + + if !cmp.Equal(bytes, tc.expected) { + t.Errorf("expected %v but got %v. Diff: %v", tc.expected, bytes, cmp.Diff(tc.expected, bytes)) + } + + }) + } +}