// Package client is a client for a Wingbits station (such as the MGW310). It // exposes the readsb JSON files (aircraft, receiver, stats, outline) and the // Wingbits-specific endpoints (network diagnostics, Prometheus metrics and // Tailscale status) as typed Go values, with both one-shot queries and // channel-based streaming. // // Decoded values come from the type-only packages pkg/types/readsb and // pkg/types/wingbits; this package owns the transport and request logic. // // A station serves two HTTP roots: // // - The Wingbits root (default :8088) serves the Wingbits endpoints directly // and the readsb files under readsb/. // - The tar1090 root (default :8504) serves the readsb files under data/, // plus data/outline.json. When configured it overrides the Wingbits root // for all readsb files. package client import ( "crypto/tls" "fmt" "net/http" "net/url" "strings" "time" ) // Default ports exposed by an MGW310 station. const ( DefaultWingbitsPort = 8088 DefaultTar1090Port = 8504 // defaultDataPath is the tar1090 path under which readsb files are served. defaultDataPath = "data" // wingbitsReadsbPath is the path under which the Wingbits root serves readsb files. wingbitsReadsbPath = "readsb" ) // Endpoint describes one HTTP root on a station. type Endpoint struct { // Scheme is "http" (default) or "https". Scheme string // Host is the hostname or IP of the station. Host string // Port is the TCP port; 0 selects the role's default. Port int // DataPath is the URL path prefix under which readsb files are served. For // the tar1090 root this defaults to "data"; for the Wingbits root the // readsb files live under "readsb". DataPath string // TLS configures the transport when Scheme is "https". It is only applied // when the client builds its own http.Client (i.e. no WithHTTPClient). TLS *tls.Config } func (e Endpoint) scheme() string { if e.Scheme != "" { return e.Scheme } return "http" } func (e Endpoint) hostPort(defPort int) string { port := e.Port if port == 0 { port = defPort } return fmt.Sprintf("%s:%d", e.Host, port) } // url builds an absolute URL for path elements joined under the endpoint, // optionally prefixed by the endpoint's data path. func (e Endpoint) url(defPort int, withData bool, elem ...string) string { parts := elem if withData && e.DataPath != "" { parts = append([]string{e.DataPath}, elem...) } u := url.URL{ Scheme: e.scheme(), Host: e.hostPort(defPort), Path: "/" + strings.Join(parts, "/"), } return u.String() } // Client talks to a Wingbits station. Construct it with New or NewMGW310Client. // It is safe for concurrent use. type Client struct { wingbits Endpoint tar1090 *Endpoint http *http.Client userAgent string } // Option customizes a Client. type Option func(*Client) // WithWingbitsEndpoint sets (and overrides) the Wingbits HTTP root. func WithWingbitsEndpoint(e Endpoint) Option { return func(c *Client) { c.wingbits = e } } // WithTar1090Endpoint enables a tar1090 HTTP root. When set it overrides the // Wingbits root for all readsb files (aircraft, receiver, stats) and is the // only source for outline.json. func WithTar1090Endpoint(e Endpoint) Option { return func(c *Client) { if e.DataPath == "" { e.DataPath = defaultDataPath } c.tar1090 = &e } } // WithHTTPClient supplies a custom http.Client. When provided, per-endpoint TLS // config is ignored (configure it on the supplied client's transport instead). func WithHTTPClient(h *http.Client) Option { return func(c *Client) { c.http = h } } // WithUserAgent overrides the User-Agent header sent with requests. func WithUserAgent(ua string) Option { return func(c *Client) { c.userAgent = ua } } // New constructs a Client from options. A Wingbits endpoint with a non-empty // Host must be supplied via WithWingbitsEndpoint. func New(opts ...Option) (*Client, error) { c := &Client{ userAgent: "wingbits-go/1.0", } for _, opt := range opts { opt(c) } if c.wingbits.DataPath == "" { c.wingbits.DataPath = wingbitsReadsbPath } if c.wingbits.Host == "" { return nil, fmt.Errorf("wingbits: a wingbits endpoint host is required") } if c.http == nil { c.http = buildHTTPClient(c.wingbits, c.tar1090) } return c, nil } // NewMGW310Client is a convenience constructor for a standard MGW310 reachable // at host. It wires the Wingbits root to :8088 and a tar1090 root to :8504, then // applies any options (which may override either endpoint, TLS, the HTTP client, // and so on). func NewMGW310Client(host string, opts ...Option) (*Client, error) { base := []Option{ WithWingbitsEndpoint(Endpoint{Host: host, Port: DefaultWingbitsPort, DataPath: wingbitsReadsbPath}), WithTar1090Endpoint(Endpoint{Host: host, Port: DefaultTar1090Port, DataPath: defaultDataPath}), } return New(append(base, opts...)...) } // readsbEndpoint returns the endpoint and default port serving readsb files, // preferring a configured tar1090 root. func (c *Client) readsbEndpoint() (Endpoint, int) { if c.tar1090 != nil { return *c.tar1090, DefaultTar1090Port } return c.wingbits, DefaultWingbitsPort } // buildHTTPClient creates a default client, attaching TLS config from whichever // endpoint supplies one (the tar1090 root takes precedence when both do). func buildHTTPClient(wb Endpoint, tar *Endpoint) *http.Client { var tlsCfg *tls.Config if wb.TLS != nil { tlsCfg = wb.TLS } if tar != nil && tar.TLS != nil { tlsCfg = tar.TLS } tr := &http.Transport{ TLSClientConfig: tlsCfg, MaxIdleConnsPerHost: 2, } return &http.Client{Timeout: 15 * time.Second, Transport: tr} }