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:
- 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
helloframe. - Network — enumerate host interfaces that look like a USB-gadget link (matching
MAC scheme and/or a
10.99.0.xDHCP lease), thenGET /api/v1/devicebound to that specific interface. Binding matters: every device answers on the same10.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. - 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¶
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 |
The client reconnects with backoff on either transport, and tries network before falling back to serial.
Non-Go clients¶
- Generated clients:
api/openapi.yamlis 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 documentedx-websocket/x-serial-transportblocks, 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 callhttps://...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.