package modbus import ( "math" "testing" "github.com/uopi/uopi/internal/datasource" ) func TestDecodeEncodings(t *testing.T) { cases := []struct { name string reg Register words []uint16 want any }{ {"uint16", Register{Encoding: "uint16"}, []uint16{42}, int64(42)}, {"int16 neg", Register{Encoding: "int16"}, []uint16{0xFFFF}, int64(-1)}, {"uint32", Register{Encoding: "uint32"}, []uint16{0x0001, 0x0000}, int64(65536)}, {"int32 neg", Register{Encoding: "int32"}, []uint16{0xFFFF, 0xFFFF}, int64(-1)}, {"scaled", Register{Encoding: "int16", Scale: 0.1}, []uint16{235}, 23.5}, {"float32", Register{Encoding: "float32"}, f32Words(3.5), 3.5}, {"float64", Register{Encoding: "float64"}, f64Words(2.25), 2.25}, } for _, tc := range cases { got, err := tc.reg.decode(tc.words) if err != nil { t.Errorf("%s: decode error %v", tc.name, err) continue } switch w := tc.want.(type) { case int64: if got != w { t.Errorf("%s: got %v (%T), want %v", tc.name, got, got, w) } case float64: gf, ok := got.(float64) if !ok || math.Abs(gf-w) > 1e-6 { t.Errorf("%s: got %v (%T), want %v", tc.name, got, got, w) } } } } func TestWordOrder(t *testing.T) { big := Register{Encoding: "uint32", WordOrder: "big"} little := Register{Encoding: "uint32", WordOrder: "little"} words := []uint16{0x0001, 0x0002} // big: 0x00010002, little reverses to 0x00020001 gb, _ := big.decode(words) gl, _ := little.decode(words) if gb != int64(0x00010002) { t.Errorf("big = %v, want %d", gb, 0x00010002) } if gl != int64(0x00020001) { t.Errorf("little = %v, want %d", gl, 0x00020001) } } func TestEncodeRoundTrip(t *testing.T) { for _, enc := range []string{"uint16", "int16", "uint32", "int32", "float32", "float64"} { reg := Register{Encoding: enc} words, err := reg.encode(123) if err != nil { t.Fatalf("%s encode: %v", enc, err) } got, err := reg.decode(words) if err != nil { t.Fatalf("%s decode: %v", enc, err) } var f float64 switch v := got.(type) { case int64: f = float64(v) case float64: f = v } if math.Abs(f-123) > 1e-3 { t.Errorf("%s round-trip = %v, want 123", enc, f) } } } func TestDataTypeAndWritable(t *testing.T) { if got := (Register{Kind: "coil"}).dataType(); got != datasource.TypeBool { t.Errorf("coil type = %v, want bool", got) } if got := (Register{Encoding: "float32"}).dataType(); got != datasource.TypeFloat64 { t.Errorf("float type = %v, want float64", got) } if got := (Register{Encoding: "uint16", Scale: 0.5}).dataType(); got != datasource.TypeFloat64 { t.Errorf("scaled int type = %v, want float64", got) } if got := (Register{Encoding: "uint16"}).dataType(); got != datasource.TypeInt64 { t.Errorf("plain int type = %v, want int64", got) } if (Register{Kind: "input", Writable: true}).writable() { t.Error("input register must be read-only") } if (Register{Kind: "discrete", Writable: true}).writable() { t.Error("discrete input must be read-only") } if !(Register{Kind: "holding", Writable: true}).writable() { t.Error("writable holding should be writable") } } func f32Words(v float32) []uint16 { u := math.Float32bits(v) return []uint16{uint16(u >> 16), uint16(u)} } func f64Words(v float64) []uint16 { u := math.Float64bits(v) return []uint16{uint16(u >> 48), uint16(u >> 32), uint16(u >> 16), uint16(u)} }