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
+98
View File
@@ -0,0 +1,98 @@
package client
import (
"context"
"encoding/json"
"fmt"
"io"
"net/http"
"gitea.libretechconsulting.com/rmcguire/wingbits/pkg/types/readsb"
"gitea.libretechconsulting.com/rmcguire/wingbits/pkg/types/wingbits"
)
// Aircraft fetches and decodes aircraft.json. When one or more filters are
// supplied, only matching aircraft are retained in the returned report.
func (c *Client) Aircraft(ctx context.Context, filters ...readsb.AircraftFilter) (*readsb.AircraftReport, error) {
ep, port := c.readsbEndpoint()
report, err := getJSON[readsb.AircraftReport](ctx, c, ep.url(port, true, "aircraft.json"))
if err != nil {
return nil, err
}
report.Aircraft = report.Filter(filters...)
return report, nil
}
// Receiver fetches and decodes receiver.json.
func (c *Client) Receiver(ctx context.Context) (*readsb.Receiver, error) {
ep, port := c.readsbEndpoint()
return getJSON[readsb.Receiver](ctx, c, ep.url(port, true, "receiver.json"))
}
// Stats fetches and decodes stats.json.
func (c *Client) Stats(ctx context.Context) (*readsb.Stats, error) {
ep, port := c.readsbEndpoint()
return getJSON[readsb.Stats](ctx, c, ep.url(port, true, "stats.json"))
}
// Outline fetches and decodes outline.json (the receiver range polygon). It is
// only served by the tar1090 root, so a tar1090 endpoint must be configured.
func (c *Client) Outline(ctx context.Context) (*readsb.Outline, error) {
if c.tar1090 == nil {
return nil, fmt.Errorf("wingbits: outline.json requires a tar1090 endpoint")
}
return getJSON[readsb.Outline](ctx, c, c.tar1090.url(DefaultTar1090Port, true, "outline.json"))
}
// Diagnostics fetches and decodes /network/diagnostics from the Wingbits root.
func (c *Client) Diagnostics(ctx context.Context) (*wingbits.Diagnostics, error) {
return getJSON[wingbits.Diagnostics](ctx, c, c.wingbits.url(DefaultWingbitsPort, false, "network", "diagnostics"))
}
// Status fetches and decodes the Tailscale status from the Wingbits root.
func (c *Client) Status(ctx context.Context) (*wingbits.TailscaleStatus, error) {
return getJSON[wingbits.TailscaleStatus](ctx, c, c.wingbits.url(DefaultWingbitsPort, false, "tailscale", "status"))
}
// Metrics fetches /metrics from the Wingbits root and parses the Prometheus
// text exposition format into structured metric families.
func (c *Client) Metrics(ctx context.Context) (*wingbits.Metrics, error) {
body, err := c.get(ctx, c.wingbits.url(DefaultWingbitsPort, false, "metrics"))
if err != nil {
return nil, err
}
defer body.Close()
return wingbits.ParseMetrics(body)
}
// getJSON fetches a URL and decodes its JSON body into a freshly allocated T.
func getJSON[T any](ctx context.Context, c *Client, url string) (*T, error) {
body, err := c.get(ctx, url)
if err != nil {
return nil, err
}
defer body.Close()
out := new(T)
if err := json.NewDecoder(body).Decode(out); err != nil {
return nil, fmt.Errorf("wingbits: decoding %s: %w", url, err)
}
return out, nil
}
// get performs a GET and returns the response body, which the caller must close.
func (c *Client) get(ctx context.Context, url string) (io.ReadCloser, error) {
req, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil)
if err != nil {
return nil, fmt.Errorf("wingbits: building request: %w", err)
}
req.Header.Set("User-Agent", c.userAgent)
resp, err := c.http.Do(req)
if err != nil {
return nil, fmt.Errorf("wingbits: GET %s: %w", url, err)
}
if resp.StatusCode != http.StatusOK {
resp.Body.Close()
return nil, fmt.Errorf("wingbits: GET %s: unexpected status %s", url, resp.Status)
}
return resp.Body, nil
}