From 5cb40a1882fe0dcf737eda268f98d7127088542b Mon Sep 17 00:00:00 2001 From: Sven Rebhan <36194019+srebhan@users.noreply.github.com> Date: Wed, 7 Dec 2022 15:20:51 +0100 Subject: [PATCH] feat(inputs.modbus): add support for half-precision float (float16) (#12340) --- docs/LICENSE_OF_DEPENDENCIES.md | 1 + go.mod | 1 + go.sum | 2 + plugins/inputs/modbus/README.md | 19 +++-- plugins/inputs/modbus/configuration.go | 2 +- .../inputs/modbus/configuration_register.go | 4 +- .../inputs/modbus/configuration_request.go | 4 +- plugins/inputs/modbus/modbus_test.go | 84 ++++++++++++++++++- plugins/inputs/modbus/sample_register.conf | 2 +- plugins/inputs/modbus/sample_request.conf | 6 +- plugins/inputs/modbus/type_conversions.go | 4 + plugins/inputs/modbus/type_conversions16.go | 49 +++++++++++ 12 files changed, 160 insertions(+), 18 deletions(-) diff --git a/docs/LICENSE_OF_DEPENDENCIES.md b/docs/LICENSE_OF_DEPENDENCIES.md index 10408901d..f17b94118 100644 --- a/docs/LICENSE_OF_DEPENDENCIES.md +++ b/docs/LICENSE_OF_DEPENDENCIES.md @@ -302,6 +302,7 @@ following works: - github.com/wavefronthq/wavefront-sdk-go [Apache License 2.0](https://github.com/wavefrontHQ/wavefront-sdk-go/blob/master/LICENSE) - github.com/wvanbergen/kafka [MIT License](https://github.com/wvanbergen/kafka/blob/master/LICENSE) - github.com/wvanbergen/kazoo-go [MIT License](https://github.com/wvanbergen/kazoo-go/blob/master/MIT-LICENSE) +- github.com/x448/float16 [MIT License](https://github.com/x448/float16/blob/master/LICENSE) - github.com/xdg-go/pbkdf2 [Apache License 2.0](https://github.com/xdg-go/pbkdf2/blob/main/LICENSE) - github.com/xdg-go/scram [Apache License 2.0](https://github.com/xdg-go/scram/blob/master/LICENSE) - github.com/xdg-go/stringprep [Apache License 2.0](https://github.com/xdg-go/stringprep/blob/master/LICENSE) diff --git a/go.mod b/go.mod index ef6706b10..8a4eb17da 100644 --- a/go.mod +++ b/go.mod @@ -160,6 +160,7 @@ require ( github.com/vmware/govmomi v0.28.1-0.20220921224932-b4b508abf208 github.com/wavefronthq/wavefront-sdk-go v0.10.4 github.com/wvanbergen/kafka v0.0.0-20171203153745-e2edea948ddf + github.com/x448/float16 v0.8.4 github.com/xdg/scram v1.0.5 github.com/yuin/goldmark v1.5.3 go.mongodb.org/mongo-driver v1.11.0 diff --git a/go.sum b/go.sum index 54d4e38d5..8f69dd839 100644 --- a/go.sum +++ b/go.sum @@ -2547,6 +2547,8 @@ github.com/wvanbergen/kafka v0.0.0-20171203153745-e2edea948ddf h1:TOV5PC6fIWwFOF github.com/wvanbergen/kafka v0.0.0-20171203153745-e2edea948ddf/go.mod h1:nxx7XRXbR9ykhnC8lXqQyJS0rfvJGxKyKw/sT1YOttg= github.com/wvanbergen/kazoo-go v0.0.0-20180202103751-f72d8611297a h1:ILoU84rj4AQ3q6cjQvtb9jBjx4xzR/Riq/zYhmDQiOk= github.com/wvanbergen/kazoo-go v0.0.0-20180202103751-f72d8611297a/go.mod h1:vQQATAGxVK20DC1rRubTJbZDDhhpA4QfU02pMdPxGO4= +github.com/x448/float16 v0.8.4 h1:qLwI1I70+NjRFUR3zs1JPUCgaCXSh3SW62uAKT1mSBM= +github.com/x448/float16 v0.8.4/go.mod h1:14CWIYCyZA/cWjXOioeEpHeN/83MdbZDRQHoFcYsOfg= github.com/xdg-go/pbkdf2 v1.0.0 h1:Su7DPu48wXMwC3bs7MCNG+z4FhcyEuz5dlvchbq0B0c= github.com/xdg-go/pbkdf2 v1.0.0/go.mod h1:jrpuAogTd400dnrH08LKmI/xc1MbPOebTwRqcT5RDeI= github.com/xdg-go/scram v1.0.2/go.mod h1:1WAq6h33pAW+iRreB34OORO2Nf7qel3VV3fjBj+hCSs= diff --git a/plugins/inputs/modbus/README.md b/plugins/inputs/modbus/README.md index fa66b54b5..5112b8d28 100644 --- a/plugins/inputs/modbus/README.md +++ b/plugins/inputs/modbus/README.md @@ -96,7 +96,7 @@ See the [CONFIGURATION.md][CONFIGURATION.md] for more details. ## |---CDAB - Mid-Little Endian ## data_type - INT8L, INT8H, UINT8L, UINT8H (low and high byte variants) ## INT16, UINT16, INT32, UINT32, INT64, UINT64, - ## FLOAT32-IEEE, FLOAT64-IEEE (the IEEE 754 binary representation) + ## FLOAT16-IEEE, FLOAT32-IEEE, FLOAT64-IEEE (IEEE 754 binary representation) ## FLOAT32, FIXED, UFIXED (fixed-point representation on input) ## scale - the final numeric variable representation ## address - variable address @@ -158,10 +158,10 @@ See the [CONFIGURATION.md][CONFIGURATION.md] for more details. ## fields as the optimisation will add such field only where needed. # optimization = "none" - ## Maximum number register the optimizer is allowed to insert between two fields to + ## Maximum number register the optimizer is allowed to insert between two fields to ## save requests. ## This option is only used for the 'max_insert' optimization strategy. - ## NOTE: All omitted fields are ignored, so this option denotes the effective hole + ## NOTE: All omitted fields are ignored, so this option denotes the effective hole ## size to fill. # optimization_max_register_fill = 50 @@ -172,7 +172,7 @@ See the [CONFIGURATION.md][CONFIGURATION.md] for more details. ## type *1,2 - type of the modbus field, can be ## INT8L, INT8H, UINT8L, UINT8H (low and high byte variants) ## INT16, UINT16, INT32, UINT32, INT64, UINT64 and - ## FLOAT32, FLOAT64 (IEEE 754 binary representation) + ## FLOAT16, FLOAT32, FLOAT64 (IEEE 754 binary representation) ## scale *1,2 - (optional) factor to scale the variable with ## output *1,2 - (optional) type of resulting field, can be INT64, UINT64 or FLOAT64. Defaults to FLOAT64 if ## "scale" is provided and to the input "type" class otherwise (i.e. INT* -> INT64, etc). @@ -307,7 +307,7 @@ the register respectively. These types are used for integer input values. Select the one that matches your modbus data source. -##### Floating Point: `FLOAT32-IEEE`, `FLOAT64-IEEE` +##### Floating Point: `FLOAT16-IEEE`, `FLOAT32-IEEE`, `FLOAT64-IEEE` Use these types if your modbus registers contain a value that is encoded in this format. These types always include the sign, therefore no variant exists. @@ -455,10 +455,11 @@ The `type` setting specifies the datatype of the modbus register and can be set to `INT8L`, `INT8H`, `UINT8L`, `UINT8H` where `L` is the lower byte of the register and `H` is the higher byte. Furthermore, the types `INT16`, `UINT16`, `INT32`, `UINT32`, `INT64` or `UINT64` -for integer types or `FLOAT32` and `FLOAT64` for IEEE 754 binary representations -of floating point values exist. Usually the datatype of the register is listed -in the datasheet of your modbus device in relation to the `address` described -above. +for integer types or `FLOAT16`, `FLOAT32` and `FLOAT64` for IEEE 754 binary +representations of floating point values exist. `FLOAT16` denotes a +half-precision float with a 16-bit representation. +Usually the datatype of the register is listed in the datasheet of your modbus +device in relation to the `address` described above. This setting is ignored if the field's `omit` is set to `true` or if the `register` type is a bit-type (`coil` or `discrete`) and can be omitted in diff --git a/plugins/inputs/modbus/configuration.go b/plugins/inputs/modbus/configuration.go index c39729457..0ddd71844 100644 --- a/plugins/inputs/modbus/configuration.go +++ b/plugins/inputs/modbus/configuration.go @@ -33,7 +33,7 @@ func normalizeInputDatatype(dataType string) (string, error) { switch dataType { case "INT8L", "INT8H", "UINT8L", "UINT8H", "INT16", "UINT16", "INT32", "UINT32", "INT64", "UINT64", - "FLOAT32", "FLOAT64": + "FLOAT16", "FLOAT32", "FLOAT64": return dataType, nil } return "unknown", fmt.Errorf("unknown input type %q", dataType) diff --git a/plugins/inputs/modbus/configuration_register.go b/plugins/inputs/modbus/configuration_register.go index fa80a4764..81fcb91f2 100644 --- a/plugins/inputs/modbus/configuration_register.go +++ b/plugins/inputs/modbus/configuration_register.go @@ -179,7 +179,7 @@ func (c *ConfigurationOriginal) validateFieldDefinitions(fieldDefs []fieldDefini switch item.DataType { case "INT8L", "INT8H", "UINT8L", "UINT8H", "UINT16", "INT16", "UINT32", "INT32", "UINT64", "INT64", - "FLOAT32-IEEE", "FLOAT64-IEEE", "FLOAT32", "FIXED", "UFIXED": + "FLOAT16-IEEE", "FLOAT32-IEEE", "FLOAT64-IEEE", "FLOAT32", "FIXED", "UFIXED": default: return fmt.Errorf("invalid data type '%s' in '%s' - '%s'", item.DataType, registerType, item.Name) } @@ -236,6 +236,8 @@ func (c *ConfigurationOriginal) normalizeInputDatatype(dataType string, words in default: return "unknown", fmt.Errorf("invalid length %d for type %q", words, dataType) } + case "FLOAT16-IEEE": + return "FLOAT16", nil case "FLOAT32-IEEE": return "FLOAT32", nil case "FLOAT64-IEEE": diff --git a/plugins/inputs/modbus/configuration_request.go b/plugins/inputs/modbus/configuration_request.go index c555c96d8..13d756475 100644 --- a/plugins/inputs/modbus/configuration_request.go +++ b/plugins/inputs/modbus/configuration_request.go @@ -354,7 +354,7 @@ func (c *ConfigurationPerRequest) determineOutputDatatype(input string) (string, return "INT64", nil case "UINT8L", "UINT8H", "UINT16", "UINT32", "UINT64": return "UINT64", nil - case "FLOAT32", "FLOAT64": + case "FLOAT16", "FLOAT32", "FLOAT64": return "FLOAT64", nil } return "unknown", fmt.Errorf("invalid input datatype %q for determining output", input) @@ -365,7 +365,7 @@ func (c *ConfigurationPerRequest) determineFieldLength(input string) (uint16, er switch input { case "INT8L", "INT8H", "UINT8L", "UINT8H": return 1, nil - case "INT16", "UINT16": + case "INT16", "UINT16", "FLOAT16": return 1, nil case "INT32", "UINT32", "FLOAT32": return 2, nil diff --git a/plugins/inputs/modbus/modbus_test.go b/plugins/inputs/modbus/modbus_test.go index 53689c422..29e8bd45c 100644 --- a/plugins/inputs/modbus/modbus_test.go +++ b/plugins/inputs/modbus/modbus_test.go @@ -819,6 +819,26 @@ func TestHoldingRegisters(t *testing.T) { write: []byte{0x8F, 0x55, 0xC3, 0x47, 0x6A, 0x40, 0xBF, 0x9C}, read: float64(-0.02774907295123737), }, + { + name: "register240_abcd_float16", + address: []uint16{240}, + quantity: 1, + byteOrder: "AB", + dataType: "FLOAT16-IEEE", + scale: 1, + write: []byte{0xb8, 0x14}, + read: float64(-0.509765625), + }, + { + name: "register240_dcba_float16", + address: []uint16{240}, + quantity: 1, + byteOrder: "BA", + dataType: "FLOAT16-IEEE", + scale: 1, + write: []byte{0x14, 0xb8}, + read: float64(-0.509765625), + }, } serv := mbserver.NewServer() @@ -1381,6 +1401,37 @@ func TestRequestTypesHoldingABCD(t *testing.T) { write: []byte{0x40, 0x09, 0x21, 0xfb, 0x54, 0x44, 0x2e, 0xea}, read: float64(3.14159265359000006156975359772), }, + { + name: "register100_float16", + address: 100, + dataTypeIn: "FLOAT16", + write: []byte{0xb8, 0x14}, + read: float64(-0.509765625), + }, + { + name: "register100_float16-scale_.1", + address: 100, + dataTypeIn: "FLOAT16", + scale: .1, + write: []byte{0xb8, 0x14}, + read: float64(-0.0509765625), + }, + { + name: "register100_float16_scale_10", + address: 100, + dataTypeIn: "FLOAT16", + scale: 10, + write: []byte{0xb8, 0x14}, + read: float64(-5.09765625), + }, + { + name: "register100_float16_float64_scale", + address: 100, + dataTypeIn: "FLOAT16", + scale: 1.0, + write: []byte{0xb8, 0x14}, + read: float64(-0.509765625), + }, } serv := mbserver.NewServer() @@ -1951,6 +2002,37 @@ func TestRequestTypesHoldingDCBA(t *testing.T) { write: []byte{0x40, 0x09, 0x21, 0xfb, 0x54, 0x44, 0x2e, 0xea}, read: float64(3.14159265359000006156975359772), }, + { + name: "register100_float16", + address: 100, + dataTypeIn: "FLOAT16", + write: []byte{0xb8, 0x14}, + read: float64(-0.509765625), + }, + { + name: "register100_float16-scale_.1", + address: 100, + dataTypeIn: "FLOAT16", + scale: .1, + write: []byte{0xb8, 0x14}, + read: float64(-0.0509765625), + }, + { + name: "register100_float16_scale_10", + address: 100, + dataTypeIn: "FLOAT16", + scale: 10, + write: []byte{0xb8, 0x14}, + read: float64(-5.09765625), + }, + { + name: "register100_float16_float64_scale", + address: 100, + dataTypeIn: "FLOAT16", + scale: 1.0, + write: []byte{0xb8, 0x14}, + read: float64(-0.509765625), + }, } serv := mbserver.NewServer() @@ -1973,7 +2055,7 @@ func TestRequestTypesHoldingDCBA(t *testing.T) { require.NoError(t, err) modbus := Modbus{ - Name: "TestRequestTypesHoldingABCD", + Name: "TestRequestTypesHoldingDCBA", Controller: "tcp://localhost:1502", ConfigurationType: "request", Log: testutil.Logger{}, diff --git a/plugins/inputs/modbus/sample_register.conf b/plugins/inputs/modbus/sample_register.conf index 6afdb626c..4727f4096 100644 --- a/plugins/inputs/modbus/sample_register.conf +++ b/plugins/inputs/modbus/sample_register.conf @@ -30,7 +30,7 @@ ## |---CDAB - Mid-Little Endian ## data_type - INT8L, INT8H, UINT8L, UINT8H (low and high byte variants) ## INT16, UINT16, INT32, UINT32, INT64, UINT64, - ## FLOAT32-IEEE, FLOAT64-IEEE (the IEEE 754 binary representation) + ## FLOAT16-IEEE, FLOAT32-IEEE, FLOAT64-IEEE (IEEE 754 binary representation) ## FLOAT32, FIXED, UFIXED (fixed-point representation on input) ## scale - the final numeric variable representation ## address - variable address diff --git a/plugins/inputs/modbus/sample_request.conf b/plugins/inputs/modbus/sample_request.conf index 042546d89..ac0c2ead4 100644 --- a/plugins/inputs/modbus/sample_request.conf +++ b/plugins/inputs/modbus/sample_request.conf @@ -41,10 +41,10 @@ ## fields as the optimisation will add such field only where needed. # optimization = "none" - ## Maximum number register the optimizer is allowed to insert between two fields to + ## Maximum number register the optimizer is allowed to insert between two fields to ## save requests. ## This option is only used for the 'max_insert' optimization strategy. - ## NOTE: All omitted fields are ignored, so this option denotes the effective hole + ## NOTE: All omitted fields are ignored, so this option denotes the effective hole ## size to fill. # optimization_max_register_fill = 50 @@ -55,7 +55,7 @@ ## type *1,2 - type of the modbus field, can be ## INT8L, INT8H, UINT8L, UINT8H (low and high byte variants) ## INT16, UINT16, INT32, UINT32, INT64, UINT64 and - ## FLOAT32, FLOAT64 (IEEE 754 binary representation) + ## FLOAT16, FLOAT32, FLOAT64 (IEEE 754 binary representation) ## scale *1,2 - (optional) factor to scale the variable with ## output *1,2 - (optional) type of resulting field, can be INT64, UINT64 or FLOAT64. Defaults to FLOAT64 if ## "scale" is provided and to the input "type" class otherwise (i.e. INT* -> INT64, etc). diff --git a/plugins/inputs/modbus/type_conversions.go b/plugins/inputs/modbus/type_conversions.go index 3f856bd71..2b763c87d 100644 --- a/plugins/inputs/modbus/type_conversions.go +++ b/plugins/inputs/modbus/type_conversions.go @@ -31,6 +31,8 @@ func determineConverterScale(inType, byteOrder, outType string, scale float64) ( return determineConverterI64Scale(outType, byteOrder, scale) case "UINT64": return determineConverterU64Scale(outType, byteOrder, scale) + case "FLOAT16": + return determineConverterF16Scale(outType, byteOrder, scale) case "FLOAT32": return determineConverterF32Scale(outType, byteOrder, scale) case "FLOAT64": @@ -61,6 +63,8 @@ func determineConverterNoScale(inType, byteOrder, outType string) (fieldConverte return determineConverterI64(outType, byteOrder) case "UINT64": return determineConverterU64(outType, byteOrder) + case "FLOAT16": + return determineConverterF16(outType, byteOrder) case "FLOAT32": return determineConverterF32(outType, byteOrder) case "FLOAT64": diff --git a/plugins/inputs/modbus/type_conversions16.go b/plugins/inputs/modbus/type_conversions16.go index 0887291a6..c17050b91 100644 --- a/plugins/inputs/modbus/type_conversions16.go +++ b/plugins/inputs/modbus/type_conversions16.go @@ -3,6 +3,8 @@ package modbus import ( "encoding/binary" "fmt" + + "github.com/x448/float16" ) type convert16 func([]byte) uint16 @@ -73,6 +75,29 @@ func determineConverterU16(outType, byteOrder string) (fieldConverterFunc, error return nil, fmt.Errorf("invalid output data-type: %s", outType) } +// F16 - no scale +func determineConverterF16(outType, byteOrder string) (fieldConverterFunc, error) { + tohost, err := endianessConverter16(byteOrder) + if err != nil { + return nil, err + } + + switch outType { + case "native": + return func(b []byte) interface{} { + raw := tohost(b) + return float16.Frombits(raw).Float32() + }, nil + case "FLOAT64": + return func(b []byte) interface{} { + raw := tohost(b) + in := float16.Frombits(raw).Float32() + return float64(in) + }, nil + } + return nil, fmt.Errorf("invalid output data-type: %s", outType) +} + // I16 - scale func determineConverterI16Scale(outType, byteOrder string, scale float64) (fieldConverterFunc, error) { tohost, err := endianessConverter16(byteOrder) @@ -136,3 +161,27 @@ func determineConverterU16Scale(outType, byteOrder string, scale float64) (field } return nil, fmt.Errorf("invalid output data-type: %s", outType) } + +// F16 - scale +func determineConverterF16Scale(outType, byteOrder string, scale float64) (fieldConverterFunc, error) { + tohost, err := endianessConverter16(byteOrder) + if err != nil { + return nil, err + } + + switch outType { + case "native": + return func(b []byte) interface{} { + raw := tohost(b) + in := float16.Frombits(raw) + return in.Float32() * float32(scale) + }, nil + case "FLOAT64": + return func(b []byte) interface{} { + raw := tohost(b) + in := float16.Frombits(raw) + return float64(in.Float32()) * scale + }, nil + } + return nil, fmt.Errorf("invalid output data-type: %s", outType) +}