Compare commits
3 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 84d1ab1401 | |||
| 6dd9def324 | |||
| 3bef5ea660 |
@@ -0,0 +1 @@
|
||||
wingbits
|
||||
@@ -62,3 +62,11 @@ with `curl` when fields change; never hand-edit `testdata/` payloads.
|
||||
- `alt_baro` may be the string `"ground"` instead of a number → `readsb.AltBaro`.
|
||||
- readsb timestamps are fractional-second Unix floats → `readsb.UnixTime`.
|
||||
- `outline.json` points are `[lat, lon, altFeet]` tuples → `readsb.RangePoint`.
|
||||
|
||||
## Changelog
|
||||
|
||||
`CHANGELOG.md` follows [Keep a Changelog](https://keepachangelog.com/) and
|
||||
SemVer. Record every user-facing change under `## [Unreleased]` as part of the
|
||||
change itself — don't defer it to release time. When tagging a release, rename
|
||||
`[Unreleased]` to the new version with the date and add a fresh empty
|
||||
`[Unreleased]` section plus its compare link at the bottom.
|
||||
|
||||
@@ -0,0 +1,45 @@
|
||||
# Changelog
|
||||
|
||||
All notable changes to this project are documented here.
|
||||
|
||||
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
|
||||
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
||||
|
||||
## [Unreleased]
|
||||
|
||||
## [0.2.1] - 2026-06-23
|
||||
|
||||
### Added
|
||||
|
||||
- `MinSpeed`/`MaxSpeed` aircraft filters in `pkg/types/readsb`, selectable
|
||||
between ground speed and true airspeed via the `SpeedSource` type. `MaxSpeed`
|
||||
drops aircraft with no speed reading.
|
||||
|
||||
## [0.2.0] - 2026-06-23
|
||||
|
||||
### Added
|
||||
|
||||
- Client retry configuration with exponential backoff (`RetryConfig`,
|
||||
`WithRetry`). Retries transport errors and 5xx responses; never retries 4xx or
|
||||
context cancellation.
|
||||
- String-typed enums for readsb message type and emitter category.
|
||||
|
||||
## [0.1.1] - 2026-06-22
|
||||
|
||||
### Added
|
||||
|
||||
- License.
|
||||
|
||||
## [0.1.0] - 2026-06-22
|
||||
|
||||
### Added
|
||||
|
||||
- Initial release: `pkg/types/readsb` and `pkg/types/wingbits` decode types,
|
||||
the `pkg/client` HTTP client with one-shot queries and channel/iterator
|
||||
polling for every endpoint, the `cmd/wingbits` CLI, and a README.
|
||||
|
||||
[Unreleased]: https://gitea.libretechconsulting.com/rmcguire/wingbits/compare/v0.2.1...HEAD
|
||||
[0.2.1]: https://gitea.libretechconsulting.com/rmcguire/wingbits/compare/v0.2.0...v0.2.1
|
||||
[0.2.0]: https://gitea.libretechconsulting.com/rmcguire/wingbits/compare/v0.1.1...v0.2.0
|
||||
[0.1.1]: https://gitea.libretechconsulting.com/rmcguire/wingbits/compare/v0.1.0...v0.1.1
|
||||
[0.1.0]: https://gitea.libretechconsulting.com/rmcguire/wingbits/releases/tag/v0.1.0
|
||||
@@ -2,6 +2,7 @@ package readsb
|
||||
|
||||
import (
|
||||
"iter"
|
||||
"math"
|
||||
"slices"
|
||||
"strings"
|
||||
"time"
|
||||
@@ -141,6 +142,28 @@ func WithinNM(nm float64) AircraftFilter {
|
||||
return func(a *Aircraft) bool { return a.RDst > 0 && a.RDst <= nm }
|
||||
}
|
||||
|
||||
// WithinNMOf keeps aircraft whose current position is within nm nautical miles
|
||||
// of the given point (decimal degrees). Unlike WithinNM, which measures from the
|
||||
// receiver, this measures from an arbitrary point; aircraft with no position are
|
||||
// dropped.
|
||||
func WithinNMOf(lat, lon, nm float64) AircraftFilter {
|
||||
return func(a *Aircraft) bool {
|
||||
return a.HasPosition() && haversineNM(lat, lon, a.Lat, a.Lon) <= nm
|
||||
}
|
||||
}
|
||||
|
||||
// haversineNM returns the great-circle distance in nautical miles between two
|
||||
// points given in decimal degrees.
|
||||
func haversineNM(lat1, lon1, lat2, lon2 float64) float64 {
|
||||
const earthRadiusNM = 3440.065 // mean Earth radius in nautical miles
|
||||
dLat, dLon := rad(lat2-lat1), rad(lon2-lon1)
|
||||
h := math.Sin(dLat/2)*math.Sin(dLat/2) +
|
||||
math.Cos(rad(lat1))*math.Cos(rad(lat2))*math.Sin(dLon/2)*math.Sin(dLon/2)
|
||||
return earthRadiusNM * 2 * math.Asin(math.Sqrt(h))
|
||||
}
|
||||
|
||||
func rad(deg float64) float64 { return deg * math.Pi / 180 }
|
||||
|
||||
// SeenWithin keeps aircraft heard from within the given duration.
|
||||
func SeenWithin(d time.Duration) AircraftFilter {
|
||||
return func(a *Aircraft) bool { return a.SeenFor() <= d }
|
||||
|
||||
@@ -88,6 +88,31 @@ func TestAircraftFilters(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestWithinNMOf(t *testing.T) {
|
||||
// One degree of latitude is ~60 nm; sanity-check the haversine helper.
|
||||
if d := haversineNM(0, 0, 1, 0); d < 59 || d > 61 {
|
||||
t.Errorf("haversineNM(1 deg lat) = %.2f nm, want ~60", d)
|
||||
}
|
||||
r := decodeFile[AircraftReport](t, "aircraft.json")
|
||||
var lat, lon float64
|
||||
for _, a := range r.Aircraft {
|
||||
if a.HasPosition() {
|
||||
lat, lon = a.Lat, a.Lon
|
||||
break
|
||||
}
|
||||
}
|
||||
near := r.Filter(WithinNMOf(lat, lon, 100))
|
||||
for _, a := range near {
|
||||
if !a.HasPosition() || haversineNM(lat, lon, a.Lat, a.Lon) > 100 {
|
||||
t.Errorf("WithinNMOf kept %s at %.1f nm", a.Hex, haversineNM(lat, lon, a.Lat, a.Lon))
|
||||
}
|
||||
}
|
||||
// The point came from a real aircraft, so at least that one must match.
|
||||
if len(near) == 0 {
|
||||
t.Error("WithinNMOf returned no aircraft for an in-data point")
|
||||
}
|
||||
}
|
||||
|
||||
func TestAltBaroGround(t *testing.T) {
|
||||
var a AltBaro
|
||||
if err := json.Unmarshal([]byte(`"ground"`), &a); err != nil {
|
||||
|
||||
Reference in New Issue
Block a user