package client import ( "fmt" "sync/atomic" "time" "github.com/theater/picomap/lib/halfsiphash" "github.com/theater/picomap/lib/msgpack" "io" ) var HashKey = [8]byte{} type transport interface { Send(data []byte) error SetReadTimeout(timeout time.Duration) Reader() io.Reader Close() error } type Client struct { transport transport timeout time.Duration nextID atomic.Uint32 } func (c *Client) Close() error { return c.transport.Close() } func (c *Client) send(msg any) (uint32, error) { id := c.nextID.Add(1) payload, err := msgpack.Marshal(msg) if err != nil { return 0, fmt.Errorf("encode inner: %w", err) } checksum := halfsiphash.Sum32(payload, HashKey) env := Envelope{MessageID: id, Checksum: checksum, Payload: payload} data, err := msgpack.Marshal(&env) if err != nil { return 0, fmt.Errorf("encode envelope: %w", err) } return id, c.transport.Send(data) } func (c *Client) receive(expectedID uint32) (any, error) { c.transport.SetReadTimeout(c.timeout) dec := msgpack.NewDecoder(c.transport.Reader()) var env Envelope if err := dec.Decode(&env); err != nil { return nil, fmt.Errorf("decode envelope: %w", err) } if env.MessageID != expectedID { return nil, fmt.Errorf("message id mismatch: got %d, want %d", env.MessageID, expectedID) } expected := halfsiphash.Sum32(env.Payload, HashKey) if env.Checksum != expected { return nil, fmt.Errorf("checksum mismatch: got %08x, want %08x", env.Checksum, expected) } var inner any if err := msgpack.Unmarshal(env.Payload, &inner); err != nil { return nil, fmt.Errorf("decode inner: %w", err) } if devErr, ok := inner.(*DeviceError); ok { return nil, devErr } return inner, nil } func roundTrip[T any](c *Client, req any) (*T, error) { id, err := c.send(req) if err != nil { return nil, err } resp, err := c.receive(id) if err != nil { return nil, err } typed, ok := resp.(*T) if !ok { return nil, fmt.Errorf("unexpected response: %T", resp) } return typed, nil } func (c *Client) PICOBOOT() error { _, err := roundTrip[ResponsePICOBOOT](c, &RequestPICOBOOT{}) return err } func (c *Client) Info() (*ResponseInfo, error) { return roundTrip[ResponseInfo](c, &RequestInfo{}) }