# wingbits Typed Go client and decoders for [Wingbits](https://wingbits.com) ADS-B stations (such as the **MGW310**) and the underlying [readsb](https://github.com/wiedehopf/readsb) / tar1090 JSON feeds. Point it at a station and get clean Go structs for the aircraft your antenna is tracking, the receiver's range, decoder statistics, network diagnostics, feeder telemetry, and Tailscale status — as one‑shot queries **or** live streams over a channel. ```go c, _ := client.NewMGW310Client("192.168.0.127") // Every airliner above FL300 that we currently have a position for: report, _ := c.Aircraft(ctx, readsb.WithPosition(), readsb.MinAltitude(30000)) for _, a := range report.Aircraft { fmt.Printf("%s %-8s %5dft %3.0fnm\n", a.Hex, a.Callsign(), a.AltBaro.Feet, a.RDst) } // a9d59a UAL2350 37000ft 186nm // ...171 more ``` --- ## Why A Wingbits station already publishes a rich set of JSON endpoints — but every project ends up re‑writing the same brittle structs, re‑discovering the same quirks (`"alt_baro": "ground"`, fractional‑second Unix timestamps, `[lat, lon, alt]` tuples), and hand‑rolling another polling loop. This module does that once, correctly, with field documentation drawn straight from [readsb's `README-json.md`](https://github.com/wiedehopf/readsb/blob/dev/README-json.md) and validated against real hardware. - **Typed, documented decoders** for every endpoint — no `map[string]any`. - **One client, both roots.** The readsb files and the Wingbits‑specific endpoints are reachable through a single `Client`, which knows that readsb lives under `/readsb/` on `:8088` and under `/data/` on the tar1090 root `:8504`. - **Composable aircraft filters** — position, altitude, range, squawk, emergency, callsign, category, MLAT, signal strength, and more, AND/OR‑composable. - **Streaming built in.** Any endpoint becomes a `<-chan Update[T]` with a configurable poll interval and buffer. - **Bring your own everything** — `context.Context` on every call, pluggable `*http.Client`, per‑endpoint TLS, custom ports/schemes/paths. - **Zero non‑stdlib dependencies**, including a small Prometheus text parser for `/metrics`. ## Install ```sh go get gitea.libretechconsulting.com/rmcguire/wingbits ``` ```go import ( "gitea.libretechconsulting.com/rmcguire/wingbits/pkg/client" "gitea.libretechconsulting.com/rmcguire/wingbits/pkg/types/readsb" "gitea.libretechconsulting.com/rmcguire/wingbits/pkg/types/wingbits" ) ``` ## Packages | Package | Import | Responsibility | |---|---|---| | `client` | `…/wingbits/pkg/client` | HTTP client — query and stream every endpoint | | `readsb` | `…/wingbits/pkg/types/readsb` | Decode types + filters for `aircraft`, `receiver`, `stats`, `outline` | | `wingbits` | `…/wingbits/pkg/types/wingbits` | Decode types for `diagnostics`, `metrics`, `status` | The `types/*` packages are decode‑only and have no I/O — useful on their own if you already have the JSON (from a file, a message bus, a snapshot) and just want to unmarshal it. ## Endpoints at a glance A station serves two HTTP roots. The client routes each call automatically. | Method | Source file | Default location | |---|---|---| | `Aircraft` | `aircraft.json` | `:8504/data/` → falls back to `:8088/readsb/` | | `Receiver` | `receiver.json` | `:8504/data/` → `:8088/readsb/` | | `Stats` | `stats.json` | `:8504/data/` → `:8088/readsb/` | | `Outline` | `outline.json` | `:8504/data/` *(tar1090 only)* | | `Diagnostics` | `/network/diagnostics` | `:8088` | | `Metrics` | `/metrics` | `:8088` | | `Status` | `/tailscale/status` | `:8088` | When a tar1090 endpoint is configured it **overrides** the Wingbits root for the readsb files; `outline.json` is served exclusively by tar1090. ## Quickstart ```go package main import ( "context" "fmt" "time" "gitea.libretechconsulting.com/rmcguire/wingbits/pkg/client" "gitea.libretechconsulting.com/rmcguire/wingbits/pkg/types/readsb" ) func main() { // Sensible MGW310 defaults: Wingbits on :8088, tar1090 on :8504. c, err := client.NewMGW310Client("192.168.0.127") if err != nil { panic(err) } ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second) defer cancel() report, err := c.Aircraft(ctx, readsb.WithPosition()) if err != nil { panic(err) } fmt.Printf("tracking %d aircraft with a position\n", len(report.Aircraft)) } ``` ### Filtering aircraft Filters are plain predicates; pass any number and they compose with AND. They work both on a fetched report (`report.Filter(...)`) and inline on the query. ```go // Anything squawking an emergency, anywhere: emerg, _ := c.Aircraft(ctx, readsb.InEmergency()) // Low and close: under 10,000 ft and within 30 nm, strong signal: local, _ := c.Aircraft(ctx, readsb.WithPosition(), readsb.MaxAltitude(10000), readsb.WithinNM(30), readsb.MinRSSI(-24), ) // OR-composition and negation: heavies, _ := c.Aircraft(ctx, readsb.Any(readsb.WithCategory("A5"), readsb.WithType("mlat")), readsb.Not(readsb.OnGround()), ) ``` Available filters include `WithPosition`, `WithHex`, `WithCallsign`, `WithSquawk`, `WithCategory`, `WithType`, `InEmergency`, `IsMLAT`, `MinAltitude`, `MaxAltitude`, `OnGround`, `WithinNM`, `SeenWithin`, `MinRSSI`, plus the `Not` and `Any` combinators. ### Streaming Every endpoint has a `Stream*` variant. It emits immediately, then on each tick, and closes the channel when the context is cancelled. Each `Update[T]` carries either a value or the error from that poll. ```go ctx, cancel := context.WithCancel(context.Background()) defer cancel() for u := range c.StreamAircraft(ctx, 2*time.Second, readsb.WithPosition()) { if u.Err != nil { log.Printf("poll failed: %v", u.Err) continue } log.Printf("%d aircraft in view", len(u.Value.Aircraft)) } ``` Set the channel buffer with `client.WithStreamBufferSize(n)`. ### Station health & feeder telemetry ```go diag, _ := c.Diagnostics(ctx) fmt.Println("backend reachable:", diag.AllReachable()) m, _ := c.Metrics(ctx) wb := m.Wingbits() // typed view of the wingbits_* families fmt.Printf("feeder %s — NATS connected=%v, %0.f msgs sent\n", wb.Version, wb.NATS.Connected, wb.NATS.Sent) // Raw access to any Prometheus family is available too: if v, ok := m.Value("wingbits_beast_received_total"); ok { fmt.Printf("beast messages received: %.0f\n", v) } ``` ## CLI A small flag-driven tool lives under `cmd/wingbits` for one-off queries. It prints any endpoint as indented JSON (pipe it to `jq`) and can stream on an interval. Install it onto your `PATH` (lands in `$(go env GOPATH)/bin`, e.g. `~/go/bin`): ```sh go install gitea.libretechconsulting.com/rmcguire/wingbits/cmd/wingbits@latest ``` Then run it directly: ```sh wingbits -host 192.168.0.127 check wingbits -host 192.168.0.127 aircraft -with-pos -min-alt 30000 wingbits -host 192.168.0.127 -interval 2s stats wingbits -host 192.168.0.127 metrics | jq .NATS ``` Or run it from a checkout without installing, via `go run ./cmd/wingbits …`. The `check` command probes every endpoint once and reports reachability with a latency and a content summary, exiting non-zero if any fail: ``` [✓] aircraft 78ms 248 aircraft [✓] receiver 11ms readsb 3.14.1682 [✓] stats 10ms 91593113 msgs, 230 with pos [✓] outline 24ms 360 range points [✓] diagnostics 1.652s 3 interfaces, backend reachable=true [✓] metrics 24ms feeder v1.12.1, NATS connected=true [✓] status 12ms state=Stopped online=false ``` Commands: `aircraft`, `receiver`, `stats`, `outline`, `diagnostics`, `metrics`, `status`, `check`. The `aircraft` command accepts filter flags (`-with-pos`, `-emergency`, `-min-alt`, `-max-alt`, `-within-nm`, `-squawk`, `-callsign`); run with no command for full usage. ## Configuration `NewMGW310Client(host, opts...)` wires the standard MGW310 ports and then applies your options, so you can override anything. For non‑standard deployments build it explicitly with `New`: ```go c, err := client.New( client.WithWingbitsEndpoint(client.Endpoint{ Scheme: "https", Host: "station.example", Port: 8443, TLS: &tls.Config{ /* ... */ }, }), client.WithTar1090Endpoint(client.Endpoint{ Host: "station.example", Port: 8504, DataPath: "data", }), client.WithHTTPClient(myHTTPClient), // optional; TLS above is ignored if set client.WithStreamBufferSize(8), client.WithUserAgent("my-app/1.0"), ) ``` Omit `WithTar1090Endpoint` to serve the readsb files from the Wingbits root's `/readsb/` path instead (note: `Outline` then returns an error, since `outline.json` is tar1090‑only). ## Handled quirks These are decoded for you, so you never see them as surprises: - **`alt_baro: "ground"`** — barometric altitude is a number *or* the string `"ground"`. Decoded into `readsb.AltBaro{Feet, OnGround}`. - **Fractional Unix timestamps** — `now`, window `start`/`end` are float seconds. Decoded into `readsb.UnixTime` (embeds `time.Time`). - **Range tuples** — `outline.json` points are `[lat, lon, altFeet]` arrays. Decoded into `readsb.RangePoint`. - **Stale positions** — when a position ages out it moves to `lastPosition`; exposed as `*readsb.Position`. ## Status Validated end‑to‑end against a live MGW310. Decoders are covered by tests against captured fixtures; the client is covered against an in‑process test server. ```sh go test ./... ``` ## License See repository.