3 Commits

Author SHA1 Message Date
rmcguire 385ab85305 refactor filter operands
Publish / release (push) Successful in 27s
2026-06-26 23:18:46 -04:00
rmcguire 4e683e31bd add CHANGELOG and withinnmof test 2026-06-26 23:03:49 -04:00
rmcguire 26d7a2c0b7 update README for filters
Publish / release (push) Successful in 24s
2026-06-26 22:37:47 -04:00
4 changed files with 109 additions and 9 deletions
+38 -2
View File
@@ -5,7 +5,39 @@ 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.4.0] - 2026-06-26
### Added
- `And` aircraft filter combinator (logical AND) — the value form of the
implicit AND that `Filter`/`All` apply to their arguments. Pairs with `Or`/`Not`
for nesting, e.g. `Or(And(a, b), And(c, d))`.
### Changed
- Renamed the `Any` filter combinator to `Or`, giving a consistent
`And`/`Or`/`Not` vocabulary. **Breaking:** callers using `Any` must switch to
`Or`.
## [0.3.2] - 2026-06-26
- `WithinNMOf` aircraft filter for distance from an arbitrary point.
- Add test for WithinNMOf
## [0.3.1] - 2026-06-26
### Added
- `WithPosition` aircraft filter to keep only aircraft reporting a lat/lon.
## [0.3.0] - 2026-06-26
### 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.2.1] - 2026-06-23
@@ -38,7 +70,11 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
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
[Unreleased]: https://gitea.libretechconsulting.com/rmcguire/wingbits/compare/v0.4.0...HEAD
[0.4.0]: https://gitea.libretechconsulting.com/rmcguire/wingbits/compare/v0.3.2...v0.4.0
[0.3.2]: https://gitea.libretechconsulting.com/rmcguire/wingbits/compare/v0.3.1...v0.3.2
[0.3.1]: https://gitea.libretechconsulting.com/rmcguire/wingbits/compare/v0.3.0...v0.3.1
[0.3.0]: https://gitea.libretechconsulting.com/rmcguire/wingbits/compare/v0.2.1...v0.3.0
[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
+29 -5
View File
@@ -140,15 +140,39 @@ local, _ := c.Aircraft(ctx,
// OR-composition and negation:
heavies, _ := c.Aircraft(ctx,
readsb.Any(readsb.WithCategory("A5"), readsb.WithType("mlat")),
readsb.Or(readsb.WithCategory("A5"), readsb.WithType("mlat")),
readsb.Not(readsb.OnGround()),
)
// And is the value form of the implicit AND, for nesting inside Or:
nearby, _ := c.Aircraft(ctx, readsb.Or(
readsb.And(readsb.WithCategory("A7"), readsb.WithinNMOf(40.7, -74.0, 5)),
readsb.And(readsb.WithCategory("A7"), readsb.WithinNMOf(41.9, -72.7, 5)),
))
```
Available filters include `WithPosition`, `WithHex`, `WithCallsign`,
`WithSquawk`, `WithCategory`, `WithType`, `InEmergency`, `IsMLAT`,
`MinAltitude`, `MaxAltitude`, `OnGround`, `WithinNM`, `SeenWithin`, `MinRSSI`,
plus the `Not` and `Any` combinators.
| Filter Name | Description | Sample Usage |
|---|---|---|
| `WithPosition` | Keeps only aircraft that currently report a lat/lon. | `readsb.WithPosition()` |
| `WithHex` | Keeps aircraft whose ICAO address matches any of the given values. | `readsb.WithHex("A1B2C3", "D4E5F6")` |
| `WithCallsign` | Keeps aircraft whose (trimmed) callsign matches any value. | `readsb.WithCallsign("UAL2350")` |
| `WithSquawk` | Keeps aircraft transmitting any of the given Mode A squawk codes. | `readsb.WithSquawk("7700")` |
| `WithCategory` | Keeps aircraft of any of the given emitter categories. | `readsb.WithCategory(readsb.CatLarge)` |
| `WithType` | Keeps aircraft of any of the given source types. | `readsb.WithType(readsb.TypeMLAT)` |
| `InEmergency` | Keeps only aircraft squawking a non-routine emergency/priority code. | `readsb.InEmergency()` |
| `IsMLAT` | Keeps only aircraft whose position was derived by multilateration. | `readsb.IsMLAT()` |
| `MinAltitude` | Keeps airborne aircraft at or above the given barometric altitude (feet). | `readsb.MinAltitude(30000)` |
| `MaxAltitude` | Keeps aircraft at or below the given barometric altitude (feet); on-ground aircraft always pass. | `readsb.MaxAltitude(10000)` |
| `OnGround` | Keeps only aircraft reporting an on-ground barometric altitude. | `readsb.OnGround()` |
| `MinSpeed` | Keeps aircraft at or above knots, measured by src (GroundSpeed or TrueAirspeed). | `readsb.MinSpeed(200, readsb.GroundSpeed)` |
| `MaxSpeed` | Keeps aircraft with a positive reading at or below knots, measured by src; aircraft with no reading are dropped. | `readsb.MaxSpeed(500, readsb.TrueAirspeed)` |
| `WithinNM` | Keeps aircraft within the given range (nautical miles) of the receiver. | `readsb.WithinNM(30)` |
| `WithinNMOf` | Keeps aircraft whose current position is within nm nautical miles of the given point (decimal degrees). | `readsb.WithinNMOf(40.7, -74.0, 50)` |
| `SeenWithin` | Keeps aircraft heard from within the given duration. | `readsb.SeenWithin(5*time.Minute)` |
| `MinRSSI` | Keeps aircraft whose average signal strength is at or above dbfs (e.g. -24). | `readsb.MinRSSI(-24)` |
| `Not` | Inverts a filter. | `readsb.Not(readsb.OnGround())` |
| `And` | Keeps aircraft matching every supplied filter (logical AND). | `readsb.And(f1, f2)` |
| `Or` | Keeps aircraft matching at least one of the supplied filters (logical OR). | `readsb.Or(f1, f2)` |
### Streaming
+11 -2
View File
@@ -174,13 +174,22 @@ func MinRSSI(dbfs float64) AircraftFilter {
return func(a *Aircraft) bool { return a.RSSI >= dbfs }
}
// And, Or, and Not compose filters as boolean operators. Filter/All already AND
// their arguments; And is the value form, letting you nest, e.g.
// Or(And(a, b), And(c, d)).
// Not inverts a filter.
func Not(f AircraftFilter) AircraftFilter {
return func(a *Aircraft) bool { return !f(a) }
}
// Any keeps aircraft matching at least one of the supplied filters (logical OR).
func Any(filters ...AircraftFilter) AircraftFilter {
// And keeps aircraft matching every supplied filter (logical AND).
func And(filters ...AircraftFilter) AircraftFilter {
return func(a *Aircraft) bool { return keep(a, filters) }
}
// Or keeps aircraft matching at least one of the supplied filters (logical OR).
func Or(filters ...AircraftFilter) AircraftFilter {
return func(a *Aircraft) bool {
for _, f := range filters {
if f(a) {
+31
View File
@@ -113,6 +113,37 @@ func TestWithinNMOf(t *testing.T) {
}
}
func TestOr(t *testing.T) {
// Two non-overlapping 5 nm circles; Or should keep aircraft in either.
r := &AircraftReport{Aircraft: []Aircraft{
{Hex: "nearA", Lat: 40.01, Lon: -75.0},
{Hex: "nearB", Lat: 41.0, Lon: -76.01},
{Hex: "between", Lat: 40.5, Lon: -75.5},
{Hex: "nopos"},
}}
near := r.Filter(Or(WithinNMOf(40, -75, 5), WithinNMOf(41, -76, 5)))
got := map[string]bool{}
for _, a := range near {
got[a.Hex] = true
}
if !got["nearA"] || !got["nearB"] || got["between"] || got["nopos"] || len(got) != 2 {
t.Errorf("Or(WithinNMOf...) mismatch, matched: %v", got)
}
}
func TestAnd(t *testing.T) {
// And matches only aircraft passing every filter; here position AND in-circle.
r := &AircraftReport{Aircraft: []Aircraft{
{Hex: "in", Lat: 40.01, Lon: -75.0},
{Hex: "out", Lat: 50.0, Lon: -75.0},
{Hex: "nopos"},
}}
near := r.Filter(And(WithPosition(), WithinNMOf(40, -75, 5)))
if len(near) != 1 || near[0].Hex != "in" {
t.Errorf("And mismatch, matched: %+v", near)
}
}
func TestAltBaroGround(t *testing.T) {
var a AltBaro
if err := json.Unmarshal([]byte(`"ground"`), &a); err != nil {