From 6b07e317275edddb8e890a0409bf1f28999eac81 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=B6rg=20Ebeling?= Date: Thu, 29 Oct 2026 16:56:05 +0100 Subject: [PATCH 1/3] fix(codegen): send RPCs without waiting when the client does not own the service A generated client waited for every RPC answer. Without a claim (Options.NoClaim or an explicit Endpoint) the service sends its answers to the owner, so such calls could only end in a timeout: a tool like soundctl, which must not claim the SoundService, could not use the generated clients at all. The client now records at Dial time whether it owns the service and uses Conn.SendRPC for the rest, returning the zero value instead of blocking. While validating against all 13 OpenMower services the void path turned out to have lost its Call branch (`undefined: err` for services with void RPCs, e.g. remote_gpio); it is restored. Golden files regenerated; every service in definitions-open-mower generates and compiles against this generator. --- codegen/README.md | 4 ++ xbot_service_interface_go/README.md | 6 ++ xbot_service_interface_go/codegen/render.go | 66 +++++++++++++------ .../codegen/testdata/demo_service.golden.go | 47 ++++++++++++- .../testdata/demo_service_noid.golden.go | 47 ++++++++++++- 5 files changed, 145 insertions(+), 25 deletions(-) diff --git a/codegen/README.md b/codegen/README.md index 6a5a9af..6ce16bf 100644 --- a/codegen/README.md +++ b/codegen/README.md @@ -34,6 +34,10 @@ Not supported yet: outputs (DATA received from the service) need a receive loop and unknown array element types are rejected while generating (loudly) instead of producing broken code. +Generated RPC methods wait for the answer only when the client owns the service: with +`Options.NoClaim` or an explicit `Endpoint` they switch to `SendRPC`, because the service sends its +answer to the owner. + Identifiers are translated to the Go naming rules for initialisms (`SoundId` -> `SoundID`, `GpsQuality` -> `GPSQuality`; see `xbot_service_interface_go/codegen/names.go`). The original name from the `service.json` stays visible, so the schema remains the reference a reader greps for: diff --git a/xbot_service_interface_go/README.md b/xbot_service_interface_go/README.md index dec55c6..7ee0d0d 100644 --- a/xbot_service_interface_go/README.md +++ b/xbot_service_interface_go/README.md @@ -158,6 +158,12 @@ Doc comments carry it on types, RPC methods, register setters and input senders; RPC method whose doc comment names the schema function. So `grep` on the schema name always finds the Go identifier, and a reader can go from `service.json` to the client without knowing the rules. +**Ownership.** A generated client waits for the RPC answer only while it owns the service. With +`Options.NoClaim` (or an explicit `Endpoint`) the service sends its answers to the owner, so the +generated methods use `SendRPC` and return the zero value instead of blocking on a reply that will +never arrive. That is what makes the SoundService usable from a tool like `soundctl`, which must not +claim the service the high-level system drives. + ## Scope The client implements what a tool like `soundctl` needs: discovery, claim, heartbeat, RPC (synchronous and diff --git a/xbot_service_interface_go/codegen/render.go b/xbot_service_interface_go/codegen/render.go index 2e012f2..40f7f74 100644 --- a/xbot_service_interface_go/codegen/render.go +++ b/xbot_service_interface_go/codegen/render.go @@ -60,7 +60,13 @@ func (g *generator) run() error { } g.line("// Client is a client for the " + s.Type + " service.") - g.line("type Client struct{ conn *xbot.Conn }") + g.line("type Client struct {") + g.line("\tconn *xbot.Conn") + g.line("\t// sendOnly is set when this client does not own the service (NoClaim or") + g.line("\t// Endpoint): the service sends its RPC answers to the owner, so the calls") + g.line("\t// are fire and forget.") + g.line("\tsendOnly bool") + g.line("}") g.line("") g.line("// Dial connects to the service; it claims it unless opts.NoClaim is set.") if g.opts.ServiceID != 0 { @@ -76,7 +82,7 @@ func (g *generator) run() error { g.line("\tif err != nil {") g.line("\t\treturn nil, err") g.line("\t}") - g.line("\treturn &Client{conn: conn}, nil") + g.line("\treturn &Client{conn: conn, sendOnly: opts.NoClaim || opts.Endpoint != \"\"}, nil") g.line("}") g.line("") g.line("// Close closes the connection.") @@ -166,7 +172,7 @@ func (g *generator) emitEnum(e Enum) error { func (g *generator) emitFunction(fn Function) error { name := goName(fn.Name) sig := []string{"ctx context.Context"} - var args []string + var params []string for _, p := range fn.Parameters { pname := goName(p.Name) where := "parameter " + pname + " of " + name @@ -179,38 +185,56 @@ func (g *generator) emitFunction(fn Function) error { return err } sig = append(sig, pname+" "+gt) - args = append(args, fmt.Sprintf("\t\txbot.Bytes(%d, %s),", p.ID, expr)) + params = append(params, fmt.Sprintf("xbot.Bytes(%d, %s),", p.ID, expr)) } retType, lines, err := g.decodeReturn(fn.ReturnType, name) if err != nil { return err } ret := "error" + zero := "" if retType != "" { ret = "(" + retType + ", error)" + zero = zeroValue(retType) + ", " } - g.f("// %s calls the RPC function %d%s.", name, fn.ID, schemaRef(fn.Name, name)) - g.f("func (c *Client) %s(%s) %s {", name, strings.Join(sig, ", "), ret) - assign := "\tpayload, err := " - if retType == "" { - assign = "\t_, err := " - } - if len(args) > 0 { - g.f("%sc.conn.Call(ctx, Fn%s,", assign, name) - for _, a := range args { - g.line(a) + // call emits one call to the service, indented by indent, using callName as + // the method on the connection (Call or SendRPC). + call := func(indent, assign, callName string) { + ctxArg := "ctx, " + if callName == "SendRPC" { + ctxArg = "" } - g.line("\t)") - } else { - g.f("%sc.conn.Call(ctx, Fn%s)", assign, name) + if len(params) > 0 { + g.f("%s%sc.conn.%s(%sFn%s,", indent, assign, callName, ctxArg, name) + for _, param := range params { + g.line(indent + " " + param) + } + g.line(indent + ")") + return + } + g.f("%s%sc.conn.%s(%sFn%s)", indent, assign, callName, ctxArg, name) } + + g.f("// %s calls the RPC function %d%s.", name, fn.ID, schemaRef(fn.Name, name)) + g.line("//") + g.line("// It only waits for the answer when this client owns the service. Without a claim") + g.line("// (Options.NoClaim or Options.Endpoint) the service sends its answer to the owner,") + g.line("// so the call is fire and forget and returns the zero value.") + g.f("func (c *Client) %s(%s) %s {", name, strings.Join(sig, ", "), ret) + g.line(" if c.sendOnly {") if retType == "" { - g.line("\treturn err") + call(" ", "return ", "SendRPC") + g.line(" }") + call(" ", "_, err := ", "Call") + g.line(" return err") } else { - g.line("\tif err != nil {") - g.f("\t\treturn %s, err", zeroValue(retType)) - g.line("\t}") + call(" ", "return "+zero, "SendRPC") + g.line(" }") + call(" ", "payload, err := ", "Call") + g.line(" if err != nil {") + g.f(" return %s, err", zeroValue(retType)) + g.line(" }") for _, l := range lines { g.line(strings.Replace(l, "ZERO", zeroValue(retType), 1)) } diff --git a/xbot_service_interface_go/codegen/testdata/demo_service.golden.go b/xbot_service_interface_go/codegen/testdata/demo_service.golden.go index 2524ef9..81dbc0a 100644 --- a/xbot_service_interface_go/codegen/testdata/demo_service.golden.go +++ b/xbot_service_interface_go/codegen/testdata/demo_service.golden.go @@ -46,7 +46,13 @@ const ( ) // Client is a client for the ServiceTemplate service. -type Client struct{ conn *xbot.Conn } +type Client struct { + conn *xbot.Conn + // sendOnly is set when this client does not own the service (NoClaim or + // Endpoint): the service sends its RPC answers to the owner, so the calls + // are fire and forget. + sendOnly bool +} // Dial connects to the service; it claims it unless opts.NoClaim is set. func Dial(ctx context.Context, opts xbot.Options) (*Client, error) { @@ -57,7 +63,7 @@ func Dial(ctx context.Context, opts xbot.Options) (*Client, error) { if err != nil { return nil, err } - return &Client{conn: conn}, nil + return &Client{conn: conn, sendOnly: opts.NoClaim || opts.Endpoint != ""}, nil } // Close closes the connection. @@ -67,13 +73,31 @@ func (c *Client) Close() error { return c.conn.Close() } func (c *Client) Endpoint() string { return c.conn.Endpoint() } // NoParamsNoReturn calls the RPC function 0. +// +// It only waits for the answer when this client owns the service. Without a claim +// (Options.NoClaim or Options.Endpoint) the service sends its answer to the owner, +// so the call is fire and forget and returns the zero value. func (c *Client) NoParamsNoReturn(ctx context.Context) error { + if c.sendOnly { + return c.conn.SendRPC(FnNoParamsNoReturn) + } _, err := c.conn.Call(ctx, FnNoParamsNoReturn) return err } // ScalarParamsWithReturn calls the RPC function 1. +// +// It only waits for the answer when this client owns the service. Without a claim +// (Options.NoClaim or Options.Endpoint) the service sends its answer to the owner, +// so the call is fire and forget and returns the zero value. func (c *Client) ScalarParamsWithReturn(ctx context.Context, Speed float32, Count uint32, Enable uint8) (int32, error) { + if c.sendOnly { + return 0, c.conn.SendRPC(FnScalarParamsWithReturn, + xbot.Bytes(0, leFloat32(Speed)), + xbot.Bytes(1, leUint32(Count)), + xbot.Bytes(2, leUint8(Enable)), + ) + } payload, err := c.conn.Call(ctx, FnScalarParamsWithReturn, xbot.Bytes(0, leFloat32(Speed)), xbot.Bytes(1, leUint32(Count)), @@ -89,7 +113,16 @@ func (c *Client) ScalarParamsWithReturn(ctx context.Context, Speed float32, Coun } // ArrayParamNoReturn calls the RPC function 2. +// +// It only waits for the answer when this client owns the service. Without a claim +// (Options.NoClaim or Options.Endpoint) the service sends its answer to the owner, +// so the call is fire and forget and returns the zero value. func (c *Client) ArrayParamNoReturn(ctx context.Context, Label string) error { + if c.sendOnly { + return c.conn.SendRPC(FnArrayParamNoReturn, + xbot.Bytes(0, charParam(Label, 64)), + ) + } _, err := c.conn.Call(ctx, FnArrayParamNoReturn, xbot.Bytes(0, charParam(Label, 64)), ) @@ -97,7 +130,17 @@ func (c *Client) ArrayParamNoReturn(ctx context.Context, Label string) error { } // MixedParamsWithReturn calls the RPC function 3. +// +// It only waits for the answer when this client owns the service. Without a claim +// (Options.NoClaim or Options.Endpoint) the service sends its answer to the owner, +// so the call is fire and forget and returns the zero value. func (c *Client) MixedParamsWithReturn(ctx context.Context, Name string, Value float32) (uint8, error) { + if c.sendOnly { + return 0, c.conn.SendRPC(FnMixedParamsWithReturn, + xbot.Bytes(0, charParam(Name, 32)), + xbot.Bytes(1, leFloat32(Value)), + ) + } payload, err := c.conn.Call(ctx, FnMixedParamsWithReturn, xbot.Bytes(0, charParam(Name, 32)), xbot.Bytes(1, leFloat32(Value)), diff --git a/xbot_service_interface_go/codegen/testdata/demo_service_noid.golden.go b/xbot_service_interface_go/codegen/testdata/demo_service_noid.golden.go index 7f7821a..fb78d39 100644 --- a/xbot_service_interface_go/codegen/testdata/demo_service_noid.golden.go +++ b/xbot_service_interface_go/codegen/testdata/demo_service_noid.golden.go @@ -43,7 +43,13 @@ const ( ) // Client is a client for the ServiceTemplate service. -type Client struct{ conn *xbot.Conn } +type Client struct { + conn *xbot.Conn + // sendOnly is set when this client does not own the service (NoClaim or + // Endpoint): the service sends its RPC answers to the owner, so the calls + // are fire and forget. + sendOnly bool +} // Dial connects to the service; it claims it unless opts.NoClaim is set. func Dial(ctx context.Context, serviceID uint16, opts xbot.Options) (*Client, error) { @@ -54,7 +60,7 @@ func Dial(ctx context.Context, serviceID uint16, opts xbot.Options) (*Client, er if err != nil { return nil, err } - return &Client{conn: conn}, nil + return &Client{conn: conn, sendOnly: opts.NoClaim || opts.Endpoint != ""}, nil } // Close closes the connection. @@ -64,13 +70,31 @@ func (c *Client) Close() error { return c.conn.Close() } func (c *Client) Endpoint() string { return c.conn.Endpoint() } // NoParamsNoReturn calls the RPC function 0. +// +// It only waits for the answer when this client owns the service. Without a claim +// (Options.NoClaim or Options.Endpoint) the service sends its answer to the owner, +// so the call is fire and forget and returns the zero value. func (c *Client) NoParamsNoReturn(ctx context.Context) error { + if c.sendOnly { + return c.conn.SendRPC(FnNoParamsNoReturn) + } _, err := c.conn.Call(ctx, FnNoParamsNoReturn) return err } // ScalarParamsWithReturn calls the RPC function 1. +// +// It only waits for the answer when this client owns the service. Without a claim +// (Options.NoClaim or Options.Endpoint) the service sends its answer to the owner, +// so the call is fire and forget and returns the zero value. func (c *Client) ScalarParamsWithReturn(ctx context.Context, Speed float32, Count uint32, Enable uint8) (int32, error) { + if c.sendOnly { + return 0, c.conn.SendRPC(FnScalarParamsWithReturn, + xbot.Bytes(0, leFloat32(Speed)), + xbot.Bytes(1, leUint32(Count)), + xbot.Bytes(2, leUint8(Enable)), + ) + } payload, err := c.conn.Call(ctx, FnScalarParamsWithReturn, xbot.Bytes(0, leFloat32(Speed)), xbot.Bytes(1, leUint32(Count)), @@ -86,7 +110,16 @@ func (c *Client) ScalarParamsWithReturn(ctx context.Context, Speed float32, Coun } // ArrayParamNoReturn calls the RPC function 2. +// +// It only waits for the answer when this client owns the service. Without a claim +// (Options.NoClaim or Options.Endpoint) the service sends its answer to the owner, +// so the call is fire and forget and returns the zero value. func (c *Client) ArrayParamNoReturn(ctx context.Context, Label string) error { + if c.sendOnly { + return c.conn.SendRPC(FnArrayParamNoReturn, + xbot.Bytes(0, charParam(Label, 64)), + ) + } _, err := c.conn.Call(ctx, FnArrayParamNoReturn, xbot.Bytes(0, charParam(Label, 64)), ) @@ -94,7 +127,17 @@ func (c *Client) ArrayParamNoReturn(ctx context.Context, Label string) error { } // MixedParamsWithReturn calls the RPC function 3. +// +// It only waits for the answer when this client owns the service. Without a claim +// (Options.NoClaim or Options.Endpoint) the service sends its answer to the owner, +// so the call is fire and forget and returns the zero value. func (c *Client) MixedParamsWithReturn(ctx context.Context, Name string, Value float32) (uint8, error) { + if c.sendOnly { + return 0, c.conn.SendRPC(FnMixedParamsWithReturn, + xbot.Bytes(0, charParam(Name, 32)), + xbot.Bytes(1, leFloat32(Value)), + ) + } payload, err := c.conn.Call(ctx, FnMixedParamsWithReturn, xbot.Bytes(0, charParam(Name, 32)), xbot.Bytes(1, leFloat32(Value)), From bfeb284947cfba7f9a928ba7122c033f22ed8a4d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=B6rg=20Ebeling?= Date: Thu, 29 Oct 2026 17:19:19 +0100 Subject: [PATCH 2/3] Handle char lengths --- codegen/README.md | 4 ++++ xbot_service_interface_go/codegen/render.go | 20 +++++++++++++++++++ .../codegen/testdata/demo_service.golden.go | 6 ++++++ .../testdata/demo_service_noid.golden.go | 6 ++++++ 4 files changed, 36 insertions(+) diff --git a/codegen/README.md b/codegen/README.md index 6ce16bf..5de502b 100644 --- a/codegen/README.md +++ b/codegen/README.md @@ -38,6 +38,10 @@ Generated RPC methods wait for the answer only when the client owns the service: `Options.NoClaim` or an explicit `Endpoint` they switch to `SendRPC`, because the service sends its answer to the owner. +`char[n]` parameters are padded to `n` bytes; a longer string would be sent unchanged and silently +rejected by the firmware, so the method checks the length first and returns +`Path too long (140 bytes, max 127)`. The limit is `n - 1`, because `char[n]` holds the terminating NUL. + Identifiers are translated to the Go naming rules for initialisms (`SoundId` -> `SoundID`, `GpsQuality` -> `GPSQuality`; see `xbot_service_interface_go/codegen/names.go`). The original name from the `service.json` stays visible, so the schema remains the reference a reader greps for: diff --git a/xbot_service_interface_go/codegen/render.go b/xbot_service_interface_go/codegen/render.go index 40f7f74..fa6217c 100644 --- a/xbot_service_interface_go/codegen/render.go +++ b/xbot_service_interface_go/codegen/render.go @@ -168,11 +168,21 @@ func (g *generator) emitEnum(e Enum) error { return nil } +// charCheck is a char[n] parameter that needs a length check before it is packed: +// byteParam pads short values but sends longer ones unchanged, and the firmware +// then rejects them without a word, so the check turns that into a clear error. +type charCheck struct { + param string + schema string + length int +} + // emitFunction writes one RPC method. func (g *generator) emitFunction(fn Function) error { name := goName(fn.Name) sig := []string{"ctx context.Context"} var params []string + var charChecks []charCheck for _, p := range fn.Parameters { pname := goName(p.Name) where := "parameter " + pname + " of " + name @@ -184,6 +194,9 @@ func (g *generator) emitFunction(fn Function) error { if err != nil { return err } + if base, length, _ := splitType(p.Type); base == "char" && length > 0 { + charChecks = append(charChecks, charCheck{param: pname, schema: p.Type, length: length}) + } sig = append(sig, pname+" "+gt) params = append(params, fmt.Sprintf("xbot.Bytes(%d, %s),", p.ID, expr)) } @@ -222,6 +235,13 @@ func (g *generator) emitFunction(fn Function) error { g.line("// (Options.NoClaim or Options.Endpoint) the service sends its answer to the owner,") g.line("// so the call is fire and forget and returns the zero value.") g.f("func (c *Client) %s(%s) %s {", name, strings.Join(sig, ", "), ret) + for _, c := range charChecks { + g.f("\tif len(%s) >= %d { // %s includes the NUL", c.param, c.length, c.schema) + g.f("\t\treturn %serrf(\"%s too long (%%d bytes, max %%d)\", len(%s), %d)", + zero, c.param, c.param, c.length-1) + g.line("\t}") + } + g.line(" if c.sendOnly {") if retType == "" { call(" ", "return ", "SendRPC") diff --git a/xbot_service_interface_go/codegen/testdata/demo_service.golden.go b/xbot_service_interface_go/codegen/testdata/demo_service.golden.go index 81dbc0a..44e5c96 100644 --- a/xbot_service_interface_go/codegen/testdata/demo_service.golden.go +++ b/xbot_service_interface_go/codegen/testdata/demo_service.golden.go @@ -118,6 +118,9 @@ func (c *Client) ScalarParamsWithReturn(ctx context.Context, Speed float32, Coun // (Options.NoClaim or Options.Endpoint) the service sends its answer to the owner, // so the call is fire and forget and returns the zero value. func (c *Client) ArrayParamNoReturn(ctx context.Context, Label string) error { + if len(Label) >= 64 { // char[64] includes the NUL + return errf("Label too long (%d bytes, max %d)", len(Label), 63) + } if c.sendOnly { return c.conn.SendRPC(FnArrayParamNoReturn, xbot.Bytes(0, charParam(Label, 64)), @@ -135,6 +138,9 @@ func (c *Client) ArrayParamNoReturn(ctx context.Context, Label string) error { // (Options.NoClaim or Options.Endpoint) the service sends its answer to the owner, // so the call is fire and forget and returns the zero value. func (c *Client) MixedParamsWithReturn(ctx context.Context, Name string, Value float32) (uint8, error) { + if len(Name) >= 32 { // char[32] includes the NUL + return 0, errf("Name too long (%d bytes, max %d)", len(Name), 31) + } if c.sendOnly { return 0, c.conn.SendRPC(FnMixedParamsWithReturn, xbot.Bytes(0, charParam(Name, 32)), diff --git a/xbot_service_interface_go/codegen/testdata/demo_service_noid.golden.go b/xbot_service_interface_go/codegen/testdata/demo_service_noid.golden.go index fb78d39..f61559d 100644 --- a/xbot_service_interface_go/codegen/testdata/demo_service_noid.golden.go +++ b/xbot_service_interface_go/codegen/testdata/demo_service_noid.golden.go @@ -115,6 +115,9 @@ func (c *Client) ScalarParamsWithReturn(ctx context.Context, Speed float32, Coun // (Options.NoClaim or Options.Endpoint) the service sends its answer to the owner, // so the call is fire and forget and returns the zero value. func (c *Client) ArrayParamNoReturn(ctx context.Context, Label string) error { + if len(Label) >= 64 { // char[64] includes the NUL + return errf("Label too long (%d bytes, max %d)", len(Label), 63) + } if c.sendOnly { return c.conn.SendRPC(FnArrayParamNoReturn, xbot.Bytes(0, charParam(Label, 64)), @@ -132,6 +135,9 @@ func (c *Client) ArrayParamNoReturn(ctx context.Context, Label string) error { // (Options.NoClaim or Options.Endpoint) the service sends its answer to the owner, // so the call is fire and forget and returns the zero value. func (c *Client) MixedParamsWithReturn(ctx context.Context, Name string, Value float32) (uint8, error) { + if len(Name) >= 32 { // char[32] includes the NUL + return 0, errf("Name too long (%d bytes, max %d)", len(Name), 31) + } if c.sendOnly { return 0, c.conn.SendRPC(FnMixedParamsWithReturn, xbot.Bytes(0, charParam(Name, 32)), From 2fe4a7b783d978e053dc50c47bc36a4a6c0da728 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=B6rg=20Ebeling?= Date: Thu, 29 Oct 2026 18:18:07 +0100 Subject: [PATCH 3/3] Fix ZeroValue return --- xbot_service_interface_go/codegen/emit.go | 9 ++-- .../codegen/testdata/demo_service.golden.go | 46 +++++++++++++++++++ .../codegen/testdata/demo_service.json | 6 +++ .../testdata/demo_service_noid.golden.go | 46 +++++++++++++++++++ 4 files changed, 104 insertions(+), 3 deletions(-) diff --git a/xbot_service_interface_go/codegen/emit.go b/xbot_service_interface_go/codegen/emit.go index baefc95..319fe7a 100644 --- a/xbot_service_interface_go/codegen/emit.go +++ b/xbot_service_interface_go/codegen/emit.go @@ -195,11 +195,14 @@ func (g *generator) decodeReturn(t, fn string) (string, []string, error) { } // zeroValue is the Go zero value of a return type, used in the error path. +// zeroValue is the Go zero value of a generated return type: "" for strings, nil +// for slices ([]byte like the uint8_t arrays, and the numeric array types such as +// []float64) and 0 for everything else (numeric scalars and enums). func zeroValue(goType string) string { - switch goType { - case "string": + switch { + case goType == "string": return `""` - case "[]byte": + case strings.HasPrefix(goType, "[]"): return "nil" } return "0" diff --git a/xbot_service_interface_go/codegen/testdata/demo_service.golden.go b/xbot_service_interface_go/codegen/testdata/demo_service.golden.go index 44e5c96..5cf1af3 100644 --- a/xbot_service_interface_go/codegen/testdata/demo_service.golden.go +++ b/xbot_service_interface_go/codegen/testdata/demo_service.golden.go @@ -25,6 +25,7 @@ const ( FnScalarParamsWithReturn uint8 = 1 FnArrayParamNoReturn uint8 = 2 FnMixedParamsWithReturn uint8 = 3 + FnNumericArrayReturn uint8 = 4 ) // ExampleEnumClass values (service.json -> "enums"). @@ -160,6 +161,22 @@ func (c *Client) MixedParamsWithReturn(ctx context.Context, Name string, Value f return payload[0], nil } +// NumericArrayReturn calls the RPC function 4. +// +// It only waits for the answer when this client owns the service. Without a claim +// (Options.NoClaim or Options.Endpoint) the service sends its answer to the owner, +// so the call is fire and forget and returns the zero value. +func (c *Client) NumericArrayReturn(ctx context.Context) ([]float64, error) { + if c.sendOnly { + return nil, c.conn.SendRPC(FnNumericArrayReturn) + } + payload, err := c.conn.Call(ctx, FnNumericArrayReturn) + if err != nil { + return nil, err + } + return readFloat64Slice(payload, 4) +} + // SetRegister1 writes the register 0 as a configuration transaction. func (c *Client) SetRegister1(Register1 string) error { return c.conn.SendTransaction(true, xbot.Data{ID: 0, Data: charParam(Register1, 42)}) @@ -229,3 +246,32 @@ func charValue(b []byte) string { func errf(format string, args ...any) error { return fmt.Errorf("servicetemplate: "+format, args...) } + +func padFloat64Slice(v []float64, n int) []float64 { + if n <= 0 || len(v) >= n { + return v + } + out := make([]float64, n) + copy(out, v) + return out +} + +func leFloat64Slice(v []float64, n int) []byte { + v = padFloat64Slice(v, n) + b := make([]byte, len(v)*8) + for i, x := range v { + binary.LittleEndian.PutUint64(b[i*8:], math.Float64bits(x)) + } + return b +} + +func readFloat64Slice(b []byte, n int) ([]float64, error) { + if n <= 0 || len(b) < n*8 { + return nil, errf("array of 8 elements: got %d bytes, want %d", len(b), n*8) + } + out := make([]float64, n) + for i := range out { + out[i] = math.Float64frombits(binary.LittleEndian.Uint64(b[i*8:])) + } + return out, nil +} diff --git a/xbot_service_interface_go/codegen/testdata/demo_service.json b/xbot_service_interface_go/codegen/testdata/demo_service.json index 23f406e..45091fa 100644 --- a/xbot_service_interface_go/codegen/testdata/demo_service.json +++ b/xbot_service_interface_go/codegen/testdata/demo_service.json @@ -77,6 +77,12 @@ } ], "return_type": "uint8_t" + }, + { + "id": 4, + "name": "NumericArrayReturn", + "parameters": [], + "return_type": "double[4]" } ], "inputs": [ diff --git a/xbot_service_interface_go/codegen/testdata/demo_service_noid.golden.go b/xbot_service_interface_go/codegen/testdata/demo_service_noid.golden.go index f61559d..8ad63ff 100644 --- a/xbot_service_interface_go/codegen/testdata/demo_service_noid.golden.go +++ b/xbot_service_interface_go/codegen/testdata/demo_service_noid.golden.go @@ -22,6 +22,7 @@ const ( FnScalarParamsWithReturn uint8 = 1 FnArrayParamNoReturn uint8 = 2 FnMixedParamsWithReturn uint8 = 3 + FnNumericArrayReturn uint8 = 4 ) // ExampleEnumClass values (service.json -> "enums"). @@ -157,6 +158,22 @@ func (c *Client) MixedParamsWithReturn(ctx context.Context, Name string, Value f return payload[0], nil } +// NumericArrayReturn calls the RPC function 4. +// +// It only waits for the answer when this client owns the service. Without a claim +// (Options.NoClaim or Options.Endpoint) the service sends its answer to the owner, +// so the call is fire and forget and returns the zero value. +func (c *Client) NumericArrayReturn(ctx context.Context) ([]float64, error) { + if c.sendOnly { + return nil, c.conn.SendRPC(FnNumericArrayReturn) + } + payload, err := c.conn.Call(ctx, FnNumericArrayReturn) + if err != nil { + return nil, err + } + return readFloat64Slice(payload, 4) +} + // SetRegister1 writes the register 0 as a configuration transaction. func (c *Client) SetRegister1(Register1 string) error { return c.conn.SendTransaction(true, xbot.Data{ID: 0, Data: charParam(Register1, 42)}) @@ -226,3 +243,32 @@ func charValue(b []byte) string { func errf(format string, args ...any) error { return fmt.Errorf("servicetemplate: "+format, args...) } + +func padFloat64Slice(v []float64, n int) []float64 { + if n <= 0 || len(v) >= n { + return v + } + out := make([]float64, n) + copy(out, v) + return out +} + +func leFloat64Slice(v []float64, n int) []byte { + v = padFloat64Slice(v, n) + b := make([]byte, len(v)*8) + for i, x := range v { + binary.LittleEndian.PutUint64(b[i*8:], math.Float64bits(x)) + } + return b +} + +func readFloat64Slice(b []byte, n int) ([]float64, error) { + if n <= 0 || len(b) < n*8 { + return nil, errf("array of 8 elements: got %d bytes, want %d", len(b), n*8) + } + out := make([]float64, n) + for i := range out { + out[i] = math.Float64frombits(binary.LittleEndian.Uint64(b[i*8:])) + } + return out, nil +}