wingbits client and cli helpers for mgw310 and other devices

This commit is contained in:
2026-06-22 22:11:27 -04:00
parent edb6b39543
commit 24fe4258f7
28 changed files with 2980 additions and 2 deletions
+280 -2
View File
@@ -1,3 +1,281 @@
# Wingbits Packages
# wingbits
Helper packages for interacting with wingbits API / readsb json files
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 oneshot 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 rewriting the same brittle structs, rediscovering the same
quirks (`"alt_baro": "ground"`, fractionalsecond Unix timestamps, `[lat, lon,
alt]` tuples), and handrolling 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 Wingbitsspecific
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/ORcomposable.
- **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`, perendpoint TLS, custom ports/schemes/paths.
- **Zero nonstdlib 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 decodeonly 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 nonstandard 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 tar1090only).
## 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 endtoend against a live MGW310. Decoders are covered by tests against
captured fixtures; the client is covered against an inprocess test server.
```sh
go test ./...
```
## License
See repository.