wingbits client and cli helpers for mgw310 and other devices
This commit is contained in:
@@ -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 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.
|
||||
|
||||
Reference in New Issue
Block a user