Skip to content

Client library

github.com/maitredede/mycustomerdisplay/client is a Go module that wraps discovery and both transports (REST/WebSocket and serial NDJSON) behind one typed API, so a host application never has to pick a transport or hand-roll framing.

Implementation status

The client library's design below is normative (spec 06) and is what the module's public API will look like. As of this writing the module is a package stub (client/doc.go) — the transports and typed calls are being implemented incrementally. Until then, integrate directly against REST/WebSocket or serial NDJSON; this page will track the module as it lands.

  • Platforms: Windows, Linux, macOS, on amd64 and arm64. Pure Go — no cgo, including serial port access.
  • Controls multiple devices simultaneously from one process (one client object per device, shared discovery).
  • Every call is context.Context-first.
  • Prefers the network transport, falls back to serial automatically; an application can also force a transport.

Discovery

devices, err := mcd.Discover(ctx)   // one-shot scan
watcher, err := mcd.Watch(ctx)      // hotplug stream: Added / Removed events

Devices are identified stably by device serial, merged across both transports:

  1. Serial ports — enumerate ports whose USB descriptor matches the gadget; when the descriptor serial isn't retrievable, briefly open the candidate port and read its hello frame.
  2. Network — enumerate host interfaces that look like a USB-gadget link (matching MAC scheme and/or a 10.99.0.x DHCP lease), then GET /api/v1/device bound to that specific interface. Binding matters: every device answers on the same 10.99.0.1, so with several devices plugged in the only way to tell them apart on the network side is which host interface you dial through.
  3. mDNS (_mycustomerdisplay._tcp) as an additional hint where available.
type DeviceInfo struct {
    Serial, Name, Model, Version string
    Transports []TransportRef // network (iface-scoped URL) and/or serial (port name)
}

Connecting and calling

c, err := mcd.Connect(ctx, dev)   // picks the best available transport
defer c.Close()

// Force a transport instead:
c, err := mcd.ConnectNetwork(ctx, dev)
c, err := mcd.ConnectSerial(ctx, dev)

Once connected, the client exposes one method group per API tag, mirroring the REST endpoints one for one:

c.Device().Info(ctx)
c.Device().Health(ctx)
c.Device().Reboot(ctx)
c.Device().FactoryReset(ctx)

c.Display().Navigate(ctx, url)
c.Display().ShowBundle(ctx, name)
c.Display().Waiting(ctx)
c.Display().Reload(ctx)
c.Display().Screenshot(ctx) // (image.Image, error)
c.Display().SetScreen(ctx, on)

c.Content().Upload(ctx, name, r io.Reader)
c.Content().UploadDir(ctx, name, fsys fs.FS) // zips a directory and uploads it
c.Content().List(ctx)
c.Content().Delete(ctx, name)

c.Config().Get(ctx)
c.Config().Patch(ctx, patch)

c.Audio().Set(ctx, volume, mute)

c.Files().SetSplash(ctx, pngReader)
c.Files().DeleteSplash(ctx)
c.Files().SetShutdownSplash(ctx, pngReader)
c.Files().DeleteShutdownSplash(ctx)
c.Files().SetCACert(ctx, pemReader)
c.Files().SetServerCert(ctx, pemReader)

c.Update().Status(ctx)
c.Update().Push(ctx, bundleReader, progressCb)
c.Update().Apply(ctx)

c.Page().Send(ctx, json.RawMessage)
c.Page().Status(ctx)

Example: push a URL to a display

package main

import (
    "context"
    "log"

    "github.com/maitredede/mycustomerdisplay/client"
)

func main() {
    ctx := context.Background()

    devices, err := mcd.Discover(ctx)
    if err != nil || len(devices) == 0 {
        log.Fatal("no device found")
    }

    c, err := mcd.Connect(ctx, devices[0])
    if err != nil {
        log.Fatal(err)
    }
    defer c.Close()

    if err := c.Display().Navigate(ctx, "https://pos.example.com/recap"); err != nil {
        log.Fatal(err)
    }
}

Example: push a bundle from a directory

err := c.Content().UploadDir(ctx, "promo", os.DirFS("./promo-bundle"))

UploadDir zips the given fs.FS in memory/streamed form and uploads it — the "push my web app" one-liner; a bundle still needs index.html at its root, same rule as content bundles uploaded via PUT /content/{name}.

Events

events, err := c.Events(ctx, mcd.TopicPageMessage, mcd.TopicDisplayState)
for ev := range events {
    switch ev.Topic {
    case mcd.TopicPageMessage:
        // ev.Payload is json.RawMessage
    case mcd.TopicDisplayState:
        // …
    }
}

The event channel auto-resubscribes on transport reconnect (e.g. the device rebooted mid-session) and emits a synthetic Reconnected event so the application knows to resync any state it cached. See Events for the full topic list.

Long-running operations

202-returning calls (content upload, OTA push) are wrapped: by default the library polls/subscribes internally and the call returns once the operation reaches a terminal state. Use the Async variant to get the Operation handle back immediately instead:

op, err := c.Update().PushAsync(ctx, bundleReader)
// … do other work …
status, err := c.Update().Wait(ctx, op.OperationID)

Push and Upload both report progress via a callback and stream their payload — no full in-memory buffering, so a 30 MB update bundle doesn't need 30 MB of host RAM.

Transport behavior

Network Serial
Carrier HTTP + WebSocket, interface-scoped 10.99.0.1 NDJSON over the CDC-ACM port
Throughput Full network speed ~1–5 Mbit/s effective
Binary payloads Native streaming Chunked base64 — the library logs a warning above 1 MB
Concurrency Parallel requests Serialized, correlated by request id
c.Transport() // returns which transport is currently active, for surfacing degraded mode

The client reconnects with backoff on either transport, and tries network before falling back to serial.

Non-Go clients

  • Generated clients: api/openapi.yaml is the contract — generate a client in any language your tooling supports (C#, Python, TypeScript…). The repository does not ship them; the document is kept generation-clean (no vendor extensions besides the documented x-websocket/x-serial-transport blocks, which generators simply ignore).
  • Browser, via network: call http://10.99.0.1/api/v1/... directly from a plain-HTTP host page. From an HTTPS page, mixed-content rules block it — upload a server certificate (PUT /files/certificates/server) for a hostname you control and call https://... instead. The API sends permissive CORS headers (*), consistent with the trusted-link model.
  • Browser, via WebSerial: see Serial API — In-browser via WebSerial. This is the recommended browser-side path — no certificates, no network setup.
  • WebUSB is not used for the host↔device control link (the CDC interfaces are claimed by OS class drivers). This is unrelated to the separate, opposite-direction device-access bridge that lets the displayed page reach peripherals on the Pi's own USB-A ports.