From e05a2b85d74b60f3eaa6f5d6a604f199c2de4a22 Mon Sep 17 00:00:00 2001 From: Dharma Bellamkonda Date: Sun, 22 Feb 2026 22:13:10 -0700 Subject: [PATCH 1/9] v2: Break deprecated APIs and narrow types - Remove deprecated Speaker.Say(string), rename SayContext to Say(context.Context, string) - Remove deprecated brevity.SpikedResponse, rename SpikedResponseV2 to SpikedResponse - Remove deprecated Aircraft.HasAnyTag() - Change types.Message.Client from ClientInfo to *ClientInfo with omitempty (TODO v2) - Add brevity.Response sealed interface; narrow controller.Call.Call from any to brevity.Response - Fix pre-existing lint issues: misspelling in group.go, float comparison in trackfile_test.go Co-Authored-By: Claude Sonnet 4.6 --- internal/application/compose.go | 4 ++-- internal/application/synthesize.go | 2 +- pkg/brevity/alphacheck.go | 2 ++ pkg/brevity/bogeydope.go | 2 ++ pkg/brevity/brevity.go | 6 ++++++ pkg/brevity/checkin.go | 2 ++ pkg/brevity/contact.go | 2 ++ pkg/brevity/declare.go | 2 ++ pkg/brevity/faded.go | 2 ++ pkg/brevity/merged.go | 2 ++ pkg/brevity/picture.go | 2 ++ pkg/brevity/radiocheck.go | 2 ++ pkg/brevity/shopping.go | 2 ++ pkg/brevity/snaplock.go | 2 ++ pkg/brevity/spiked.go | 28 ++-------------------------- pkg/brevity/strobe.go | 2 ++ pkg/brevity/sunrise.go | 2 ++ pkg/brevity/threat.go | 2 ++ pkg/brevity/tripwire.go | 2 ++ pkg/brevity/unable.go | 2 ++ pkg/composer/spiked.go | 2 +- pkg/controller/controller.go | 5 +++-- pkg/controller/spiked.go | 2 +- pkg/encyclopedia/aircraft.go | 8 -------- pkg/radar/group.go | 2 +- pkg/simpleradio/message.go | 17 ++++++++++++----- pkg/simpleradio/types/message.go | 2 +- pkg/synthesizer/speakers/macos.go | 9 ++------- pkg/synthesizer/speakers/piper.go | 9 ++------- pkg/synthesizer/speakers/speaker.go | 5 +---- pkg/trackfiles/trackfile_test.go | 2 +- 31 files changed, 68 insertions(+), 67 deletions(-) diff --git a/internal/application/compose.go b/internal/application/compose.go index ee120e57..7d4319e8 100644 --- a/internal/application/compose.go +++ b/internal/application/compose.go @@ -26,7 +26,7 @@ func (a *Application) compose(ctx context.Context, in <-chan controller.Call, ou } // composeCall handles a single call, publishing the composition to the output channel. -func (a *Application) composeCall(ctx context.Context, call any, out chan<- Message[composer.NaturalLanguageResponse]) { +func (a *Application) composeCall(ctx context.Context, call brevity.Response, out chan<- Message[composer.NaturalLanguageResponse]) { ctx = traces.WithHandledAt(ctx, time.Now()) logger := log.With().Type("type", call).Any("params", call).Logger() logger.Info().Msg("composing brevity call") @@ -52,7 +52,7 @@ func (a *Application) composeCall(ctx context.Context, call any, out chan<- Mess response = a.composer.ComposeShoppingResponse(c) case brevity.SnaplockResponse: response = a.composer.ComposeSnaplockResponse(c) - case brevity.SpikedResponseV2: + case brevity.SpikedResponse: response = a.composer.ComposeSpikedResponse(c) case brevity.StrobeResponse: response = a.composer.ComposeStrobeResponse(c) diff --git a/internal/application/synthesize.go b/internal/application/synthesize.go index ac0268b4..393d4594 100644 --- a/internal/application/synthesize.go +++ b/internal/application/synthesize.go @@ -41,7 +41,7 @@ func (a *Application) synthesizeMessage(ctx context.Context, response composer.N start := time.Now() synthesisCtx, synthesisCancel := context.WithTimeout(ctx, 30*time.Second) defer synthesisCancel() - audio, err := a.speaker.SayContext(synthesisCtx, response.Speech) + audio, err := a.speaker.Say(synthesisCtx, response.Speech) if err != nil { log.Error().Err(err).Msg("error synthesizing speech") a.trace(traces.WithRequestError(ctx, err)) diff --git a/pkg/brevity/alphacheck.go b/pkg/brevity/alphacheck.go index cbacb40e..495c6a15 100644 --- a/pkg/brevity/alphacheck.go +++ b/pkg/brevity/alphacheck.go @@ -22,3 +22,5 @@ type AlphaCheckResponse struct { // Location of the friendly aircraft. If Status is false, this may be nil. Location Bullseye } + +func (AlphaCheckResponse) isBrevityResponse() {} diff --git a/pkg/brevity/bogeydope.go b/pkg/brevity/bogeydope.go index 3a4f4a91..bd5354b1 100644 --- a/pkg/brevity/bogeydope.go +++ b/pkg/brevity/bogeydope.go @@ -49,3 +49,5 @@ type BogeyDopeResponse struct { // Group which is closest to the fighter. If there are no eligible groups, this may be nil. Group Group } + +func (BogeyDopeResponse) isBrevityResponse() {} diff --git a/pkg/brevity/brevity.go b/pkg/brevity/brevity.go index ed5d7c3b..20a8b3cd 100644 --- a/pkg/brevity/brevity.go +++ b/pkg/brevity/brevity.go @@ -3,3 +3,9 @@ package brevity // LastCaller is a placeholder callsign used when the actual callsign is unknown. const LastCaller = "Last caller" + +// Response is implemented by all brevity response and call types that a GCI controller can send. +// The unexported method prevents types outside this package from satisfying the interface. +type Response interface { + isBrevityResponse() +} diff --git a/pkg/brevity/checkin.go b/pkg/brevity/checkin.go index f74fed00..19fce55c 100644 --- a/pkg/brevity/checkin.go +++ b/pkg/brevity/checkin.go @@ -13,3 +13,5 @@ func (r CheckInRequest) String() string { type CheckInResponse struct { Callsign string } + +func (CheckInResponse) isBrevityResponse() {} diff --git a/pkg/brevity/contact.go b/pkg/brevity/contact.go index f11570b1..0264b38c 100644 --- a/pkg/brevity/contact.go +++ b/pkg/brevity/contact.go @@ -5,3 +5,5 @@ type NegativeRadarContactResponse struct { // Callsign of the friendly aircraft that made the request. Callsign string } + +func (NegativeRadarContactResponse) isBrevityResponse() {} diff --git a/pkg/brevity/declare.go b/pkg/brevity/declare.go index 2f791695..d509faba 100644 --- a/pkg/brevity/declare.go +++ b/pkg/brevity/declare.go @@ -101,3 +101,5 @@ type DeclareResponse struct { // This may be nil if Declaration is Furball, Unable, or Clean. Group Group } + +func (DeclareResponse) isBrevityResponse() {} diff --git a/pkg/brevity/faded.go b/pkg/brevity/faded.go index e4e26c52..787d693a 100644 --- a/pkg/brevity/faded.go +++ b/pkg/brevity/faded.go @@ -6,3 +6,5 @@ type FadedCall struct { // Group which has faded. Group Group } + +func (FadedCall) isBrevityResponse() {} diff --git a/pkg/brevity/merged.go b/pkg/brevity/merged.go index b49d0110..131a31cf 100644 --- a/pkg/brevity/merged.go +++ b/pkg/brevity/merged.go @@ -10,6 +10,8 @@ type MergedCall struct { Callsigns []string } +func (MergedCall) isBrevityResponse() {} + const ( // MergeEntryDistance is the distance at which contacts are considered to enter the merge. MergeEntryDistance = 3 * unit.NauticalMile diff --git a/pkg/brevity/picture.go b/pkg/brevity/picture.go index 34c9cbaf..c39f6ad1 100644 --- a/pkg/brevity/picture.go +++ b/pkg/brevity/picture.go @@ -22,3 +22,5 @@ type PictureResponse struct { // Groups included in the PICTURE. This is a maximum of 3 groups. Groups []Group } + +func (PictureResponse) isBrevityResponse() {} diff --git a/pkg/brevity/radiocheck.go b/pkg/brevity/radiocheck.go index d41c5b2b..d1c16f27 100644 --- a/pkg/brevity/radiocheck.go +++ b/pkg/brevity/radiocheck.go @@ -18,3 +18,5 @@ type RadioCheckResponse struct { // RadarContact indicates whether the callsign was found on the radar scope. RadarContact bool } + +func (RadioCheckResponse) isBrevityResponse() {} diff --git a/pkg/brevity/shopping.go b/pkg/brevity/shopping.go index b1be9b22..3aa1c457 100644 --- a/pkg/brevity/shopping.go +++ b/pkg/brevity/shopping.go @@ -13,3 +13,5 @@ func (r ShoppingRequest) String() string { type ShoppingResponse struct { Callsign string } + +func (ShoppingResponse) isBrevityResponse() {} diff --git a/pkg/brevity/snaplock.go b/pkg/brevity/snaplock.go index 6d804008..bf6e19f7 100644 --- a/pkg/brevity/snaplock.go +++ b/pkg/brevity/snaplock.go @@ -26,3 +26,5 @@ type SnaplockResponse struct { // Group that was identified. If Declaration is Unable or Furball, this may be nil. Group Group } + +func (SnaplockResponse) isBrevityResponse() {} diff --git a/pkg/brevity/spiked.go b/pkg/brevity/spiked.go index 4b168e57..e03f7ccd 100644 --- a/pkg/brevity/spiked.go +++ b/pkg/brevity/spiked.go @@ -4,7 +4,6 @@ import ( "fmt" "github.com/dharmab/skyeye/pkg/bearings" - "github.com/martinlindhe/unit" ) // SpikedRequest is a request to correlate a radar spike within ±30 degrees. @@ -22,32 +21,7 @@ func (r SpikedRequest) String() string { // SpikedResponse reports any contacts within ±30 degrees of a reported radar spike. // Reference: ATP 3-52.4 Chapter V section 13. -// -// Deprecated: Use SpikedResponseV2 instead. type SpikedResponse struct { - // Callsign of the friendly aircraft calling SPIKED. - Callsign string - // True if the spike was correlated to a contact. False otherwise. - Status bool - // Range to the correlated contact. If Status is false, this may be 0. - Range unit.Length - // Altitude of the correlated contact. If Status is false, this may be 0. - Altitude unit.Length - // Aspect of the correlated contact. If Status is false, this may be UnknownAspect. - Aspect Aspect - // Track of the correlated contact. If Status is false, this may be UnknownDirection. - Track Track - // Declaration of the correlated contact. If Status is false, this may be Clean. - Declaration Declaration - // Number of contacts in the correlated group. If Status is false, this may be zero. - Contacts int - // Reported spike bearing. This is used if the response did not correlate to a group. - Bearing bearings.Bearing -} - -// SpikedResponseV2 reports any contacts within ±30 degrees of a reported radar spike. -// Reference: ATP 3-52.4 Chapter V section 13. -type SpikedResponseV2 struct { // Callsign of the friendly aircraft calling SPIKED. Callsign string // Reported spike bearing. This is used if the response did not correlate to a group. @@ -57,3 +31,5 @@ type SpikedResponseV2 struct { // Correleted contact group. If Status is false, this may be nil. Group Group } + +func (SpikedResponse) isBrevityResponse() {} diff --git a/pkg/brevity/strobe.go b/pkg/brevity/strobe.go index 5c97214e..33ac0f35 100644 --- a/pkg/brevity/strobe.go +++ b/pkg/brevity/strobe.go @@ -31,3 +31,5 @@ type StrobeResponse struct { // Correleted contact group. If Status is false, this may be nil. Group Group } + +func (StrobeResponse) isBrevityResponse() {} diff --git a/pkg/brevity/sunrise.go b/pkg/brevity/sunrise.go index d71c3af0..3de112b0 100644 --- a/pkg/brevity/sunrise.go +++ b/pkg/brevity/sunrise.go @@ -9,3 +9,5 @@ type SunriseCall struct { // Frequency which the GCI is listening on. Frequencies []unit.Frequency } + +func (SunriseCall) isBrevityResponse() {} diff --git a/pkg/brevity/threat.go b/pkg/brevity/threat.go index c72a268b..cc59e839 100644 --- a/pkg/brevity/threat.go +++ b/pkg/brevity/threat.go @@ -9,3 +9,5 @@ type ThreatCall struct { // Group that is threatening the friendly aircraft. Group Group } + +func (ThreatCall) isBrevityResponse() {} diff --git a/pkg/brevity/tripwire.go b/pkg/brevity/tripwire.go index 251aa0ac..d5409f19 100644 --- a/pkg/brevity/tripwire.go +++ b/pkg/brevity/tripwire.go @@ -13,3 +13,5 @@ func (r TripwireRequest) String() string { type TripwireResponse struct { Callsign string } + +func (TripwireResponse) isBrevityResponse() {} diff --git a/pkg/brevity/unable.go b/pkg/brevity/unable.go index 45bb7464..60934087 100644 --- a/pkg/brevity/unable.go +++ b/pkg/brevity/unable.go @@ -20,3 +20,5 @@ type SayAgainResponse struct { // This may be empty if the GCI is unsure of the caller's identity. Callsign string } + +func (SayAgainResponse) isBrevityResponse() {} diff --git a/pkg/composer/spiked.go b/pkg/composer/spiked.go index d070d9ad..f6a08d54 100644 --- a/pkg/composer/spiked.go +++ b/pkg/composer/spiked.go @@ -5,6 +5,6 @@ import ( ) // ComposeSpikedResponse constructs natural language brevity for responding to a SPIKED call. -func (c *Composer) ComposeSpikedResponse(response brevity.SpikedResponseV2) NaturalLanguageResponse { +func (c *Composer) ComposeSpikedResponse(response brevity.SpikedResponse) NaturalLanguageResponse { return c.composeCorrelation("spike", response.Callsign, response.Status, response.Bearing, response.Group) } diff --git a/pkg/controller/controller.go b/pkg/controller/controller.go index 197fee95..d068a9cf 100644 --- a/pkg/controller/controller.go +++ b/pkg/controller/controller.go @@ -6,6 +6,7 @@ import ( "sync" "time" + "github.com/dharmab/skyeye/pkg/brevity" "github.com/dharmab/skyeye/pkg/coalitions" "github.com/dharmab/skyeye/pkg/radar" "github.com/dharmab/skyeye/pkg/simpleradio" @@ -24,11 +25,11 @@ var ( // Call is an envelope for a GCI call. type Call struct { Context context.Context - Call any + Call brevity.Response } // NewCall creates a new Call message. -func NewCall(ctx context.Context, call any) Call { +func NewCall(ctx context.Context, call brevity.Response) Call { return Call{ Context: ctx, Call: call, diff --git a/pkg/controller/spiked.go b/pkg/controller/spiked.go index 8cbca07e..7ce007dc 100644 --- a/pkg/controller/spiked.go +++ b/pkg/controller/spiked.go @@ -14,7 +14,7 @@ func (c *Controller) HandleSpiked(ctx context.Context, request *brevity.SpikedRe if correlation.Callsign == "" { c.calls <- NewCall(ctx, brevity.NegativeRadarContactResponse{Callsign: request.Callsign}) } else { - response := brevity.SpikedResponseV2{ + response := brevity.SpikedResponse{ Callsign: correlation.Callsign, Status: correlation.Group != nil, Bearing: correlation.Bearing, diff --git a/pkg/encyclopedia/aircraft.go b/pkg/encyclopedia/aircraft.go index 998e0b43..3becd980 100644 --- a/pkg/encyclopedia/aircraft.go +++ b/pkg/encyclopedia/aircraft.go @@ -2,7 +2,6 @@ package encyclopedia import ( - "slices" "time" "github.com/dharmab/skyeye/pkg/brevity" @@ -89,13 +88,6 @@ func (a Aircraft) HasTag(tag AircraftTag) bool { return ok && v } -// HasAnyTag returns true if the aircraft has any of the specified tags. -// -// Deprecated: Use slices.Contains instead. -func (a Aircraft) HasAnyTag(tags ...AircraftTag) bool { - return slices.ContainsFunc(tags, a.HasTag) -} - // ThreatRadius returns the aircraft's threat radius. func (a Aircraft) ThreatRadius() unit.Length { if a.threatRadius != 0 || a.HasTag(Unarmed) { diff --git a/pkg/radar/group.go b/pkg/radar/group.go index f864985f..e26e9576 100644 --- a/pkg/radar/group.go +++ b/pkg/radar/group.go @@ -75,7 +75,7 @@ func (g *group) altitudes() []unit.Length { // circularMean computes the circular mean of all contacts' courses. // Returns the mean magnetic bearing and a coherence value in range 0-1. -// 0 means no coherence, 1 means all courses are prefectly coherent. +// 0 means no coherence, 1 means all courses are perfectly coherent. func (g *group) circularMean() (bearings.Bearing, float64) { var sumOfSines, sumOfCosines float64 for _, tf := range g.contacts { diff --git a/pkg/simpleradio/message.go b/pkg/simpleradio/message.go index b5c7bd6b..d2cc48b5 100644 --- a/pkg/simpleradio/message.go +++ b/pkg/simpleradio/message.go @@ -34,7 +34,8 @@ func (c *Client) newMessage(t types.MessageType) types.Message { Version: "2.1.0.2", // stubbing fake SRS version, TODO add flag Type: t, } - message.Client = c.clientInfo + client := c.clientInfo + message.Client = &client return message } @@ -52,13 +53,19 @@ func (c *Client) handleMessage(message types.Message) { case types.MessageSync: c.syncClients(message.Clients) case types.MessageUpdate: - c.syncClient(message.Client) + if message.Client != nil { + c.syncClient(*message.Client) + } case types.MessageRadioUpdate: - c.syncClient(message.Client) + if message.Client != nil { + c.syncClient(*message.Client) + } case types.MessageClientDisconnect: - c.removeClient(message.Client) + if message.Client != nil { + c.removeClient(*message.Client) + } case types.MessageExternalAWACSModePassword: - if message.Client.Coalition == c.clientInfo.Coalition { + if message.Client != nil && message.Client.Coalition == c.clientInfo.Coalition { log.Debug().Any("remoteClient", message.Client).Msg("received external AWACS mode password message") // TODO is the update necessary? if err := c.updateRadios(); err != nil { diff --git a/pkg/simpleradio/types/message.go b/pkg/simpleradio/types/message.go index 94457b51..0746931d 100644 --- a/pkg/simpleradio/types/message.go +++ b/pkg/simpleradio/types/message.go @@ -21,7 +21,7 @@ type Message struct { // Version is the SRS client version. Version string `json:"Version"` // Client is used in messages that reference a single client. - Client ClientInfo `json:"Client"` // TODO v2: Change type to *ClientInfo and set omitempty + Client *ClientInfo `json:"Client,omitempty"` // Clients is used in messages that reference multiple clients. Clients []ClientInfo `json:"Clients,omitempty"` // ServerSettings is a map of server settings and their values. It sometimes appears in Sync messages. diff --git a/pkg/synthesizer/speakers/macos.go b/pkg/synthesizer/speakers/macos.go index 70272917..7dd3e38c 100644 --- a/pkg/synthesizer/speakers/macos.go +++ b/pkg/synthesizer/speakers/macos.go @@ -50,8 +50,8 @@ func NewMacOSSpeaker(useSystemVoice bool, playbackSpeed float64) Speaker { return synth } -// SayContext implements [Speaker.SayContext]. -func (s *macOSSynth) SayContext(ctx context.Context, text string) ([]float32, error) { +// Say implements [Speaker.Say]. +func (s *macOSSynth) Say(ctx context.Context, text string) ([]float32, error) { outFile, err := os.CreateTemp("", "skyeye-*.aiff") if err != nil { return nil, fmt.Errorf("failed to create temporary AIFF file: %w", err) @@ -86,8 +86,3 @@ func (s *macOSSynth) SayContext(ctx context.Context, text string) ([]float32, er f32le := pcm.S16LEBytesToF32LE(sample) return f32le, nil } - -// Say implements [Speaker.Say]. -func (s *macOSSynth) Say(text string) ([]float32, error) { - return s.SayContext(context.Background(), text) -} diff --git a/pkg/synthesizer/speakers/piper.go b/pkg/synthesizer/speakers/piper.go index 1232b293..29584114 100644 --- a/pkg/synthesizer/speakers/piper.go +++ b/pkg/synthesizer/speakers/piper.go @@ -37,8 +37,8 @@ func NewPiperSpeaker(v voices.Voice, playbackSpeed float64, playbackPause time.D return &piperSynth{tts: tts, speed: playbackSpeed, pauseLength: playbackPause}, nil } -// SayContext implements [Speaker.SayContext]. -func (s *piperSynth) SayContext(_ context.Context, text string) ([]float32, error) { +// Say implements [Speaker.Say]. +func (s *piperSynth) Say(_ context.Context, text string) ([]float32, error) { synthesized, err := s.tts.Synthesize(text, piper.WithSpeed(float32(s.speed)), piper.WithPause(float32(s.pauseLength.Seconds()))) if err != nil { return nil, fmt.Errorf("failed to synthesize text: %w", err) @@ -50,8 +50,3 @@ func (s *piperSynth) SayContext(_ context.Context, text string) ([]float32, erro f32le := pcm.S16LEBytesToF32LE(downsampled) return f32le, nil } - -// Say implements [Speaker.Say]. -func (s *piperSynth) Say(text string) ([]float32, error) { - return s.SayContext(context.Background(), text) -} diff --git a/pkg/synthesizer/speakers/speaker.go b/pkg/synthesizer/speakers/speaker.go index 84a11f5a..10585fba 100644 --- a/pkg/synthesizer/speakers/speaker.go +++ b/pkg/synthesizer/speakers/speaker.go @@ -14,10 +14,7 @@ import ( // Speaker provides text-to-speech. type Speaker interface { // Say returns F32LE PCM audio for the given text. - // - // Deprecated: Use SayContext instead. - Say(string) ([]float32, error) - SayContext(context.Context, string) ([]float32, error) + Say(context.Context, string) ([]float32, error) } func downsample(sample []byte, sourceRate unit.Frequency) ([]byte, error) { diff --git a/pkg/trackfiles/trackfile_test.go b/pkg/trackfiles/trackfile_test.go index 19f0f95d..76be72d9 100644 --- a/pkg/trackfiles/trackfile_test.go +++ b/pkg/trackfiles/trackfile_test.go @@ -69,7 +69,7 @@ func TestLastKnown(t *testing.T) { frame := tf.LastKnown() assert.Equal(t, latest.Time, frame.Time) assert.Equal(t, latest.Point, frame.Point) - assert.Equal(t, latest.Altitude, frame.Altitude) + assert.InDelta(t, latest.Altitude.Feet(), frame.Altitude.Feet(), 1) }) } From 36456f3dab58c166335f6374a7c361167cecb5b6 Mon Sep 17 00:00:00 2001 From: Dharma Bellamkonda Date: Sun, 22 Feb 2026 22:17:33 -0700 Subject: [PATCH 2/9] Merge MessageUpdate and MessageRadioUpdate cases Both message types call syncClient, so they can share a case arm, reducing repeated nil guards. Co-Authored-By: Claude Sonnet 4.6 --- pkg/simpleradio/message.go | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/pkg/simpleradio/message.go b/pkg/simpleradio/message.go index d2cc48b5..2e3d30b9 100644 --- a/pkg/simpleradio/message.go +++ b/pkg/simpleradio/message.go @@ -52,11 +52,7 @@ func (c *Client) handleMessage(message types.Message) { logMessageAndIgnore(message) case types.MessageSync: c.syncClients(message.Clients) - case types.MessageUpdate: - if message.Client != nil { - c.syncClient(*message.Client) - } - case types.MessageRadioUpdate: + case types.MessageUpdate, types.MessageRadioUpdate: if message.Client != nil { c.syncClient(*message.Client) } From b3a70fbcfa774c9c41972f2ceb1f6bbc3f677429 Mon Sep 17 00:00:00 2001 From: Dharma Bellamkonda Date: Sun, 22 Feb 2026 22:24:39 -0700 Subject: [PATCH 3/9] Revert brevity.Response sealed interface The marker interface added noise across 17 files for minimal benefit. The existing type switch in composeCall already handles unknown types, so any typed narrowing was largely cosmetic. Co-Authored-By: Claude Sonnet 4.6 --- internal/application/compose.go | 2 +- pkg/brevity/alphacheck.go | 2 -- pkg/brevity/bogeydope.go | 2 -- pkg/brevity/brevity.go | 6 ------ pkg/brevity/checkin.go | 2 -- pkg/brevity/contact.go | 2 -- pkg/brevity/declare.go | 2 -- pkg/brevity/faded.go | 2 -- pkg/brevity/merged.go | 2 -- pkg/brevity/picture.go | 2 -- pkg/brevity/radiocheck.go | 2 -- pkg/brevity/shopping.go | 2 -- pkg/brevity/snaplock.go | 2 -- pkg/brevity/spiked.go | 2 -- pkg/brevity/strobe.go | 2 -- pkg/brevity/sunrise.go | 2 -- pkg/brevity/threat.go | 2 -- pkg/brevity/tripwire.go | 2 -- pkg/brevity/unable.go | 2 -- pkg/controller/controller.go | 5 ++--- 20 files changed, 3 insertions(+), 44 deletions(-) diff --git a/internal/application/compose.go b/internal/application/compose.go index 7d4319e8..c739fae0 100644 --- a/internal/application/compose.go +++ b/internal/application/compose.go @@ -26,7 +26,7 @@ func (a *Application) compose(ctx context.Context, in <-chan controller.Call, ou } // composeCall handles a single call, publishing the composition to the output channel. -func (a *Application) composeCall(ctx context.Context, call brevity.Response, out chan<- Message[composer.NaturalLanguageResponse]) { +func (a *Application) composeCall(ctx context.Context, call any, out chan<- Message[composer.NaturalLanguageResponse]) { ctx = traces.WithHandledAt(ctx, time.Now()) logger := log.With().Type("type", call).Any("params", call).Logger() logger.Info().Msg("composing brevity call") diff --git a/pkg/brevity/alphacheck.go b/pkg/brevity/alphacheck.go index 495c6a15..cbacb40e 100644 --- a/pkg/brevity/alphacheck.go +++ b/pkg/brevity/alphacheck.go @@ -22,5 +22,3 @@ type AlphaCheckResponse struct { // Location of the friendly aircraft. If Status is false, this may be nil. Location Bullseye } - -func (AlphaCheckResponse) isBrevityResponse() {} diff --git a/pkg/brevity/bogeydope.go b/pkg/brevity/bogeydope.go index bd5354b1..3a4f4a91 100644 --- a/pkg/brevity/bogeydope.go +++ b/pkg/brevity/bogeydope.go @@ -49,5 +49,3 @@ type BogeyDopeResponse struct { // Group which is closest to the fighter. If there are no eligible groups, this may be nil. Group Group } - -func (BogeyDopeResponse) isBrevityResponse() {} diff --git a/pkg/brevity/brevity.go b/pkg/brevity/brevity.go index 20a8b3cd..ed5d7c3b 100644 --- a/pkg/brevity/brevity.go +++ b/pkg/brevity/brevity.go @@ -3,9 +3,3 @@ package brevity // LastCaller is a placeholder callsign used when the actual callsign is unknown. const LastCaller = "Last caller" - -// Response is implemented by all brevity response and call types that a GCI controller can send. -// The unexported method prevents types outside this package from satisfying the interface. -type Response interface { - isBrevityResponse() -} diff --git a/pkg/brevity/checkin.go b/pkg/brevity/checkin.go index 19fce55c..f74fed00 100644 --- a/pkg/brevity/checkin.go +++ b/pkg/brevity/checkin.go @@ -13,5 +13,3 @@ func (r CheckInRequest) String() string { type CheckInResponse struct { Callsign string } - -func (CheckInResponse) isBrevityResponse() {} diff --git a/pkg/brevity/contact.go b/pkg/brevity/contact.go index 0264b38c..f11570b1 100644 --- a/pkg/brevity/contact.go +++ b/pkg/brevity/contact.go @@ -5,5 +5,3 @@ type NegativeRadarContactResponse struct { // Callsign of the friendly aircraft that made the request. Callsign string } - -func (NegativeRadarContactResponse) isBrevityResponse() {} diff --git a/pkg/brevity/declare.go b/pkg/brevity/declare.go index d509faba..2f791695 100644 --- a/pkg/brevity/declare.go +++ b/pkg/brevity/declare.go @@ -101,5 +101,3 @@ type DeclareResponse struct { // This may be nil if Declaration is Furball, Unable, or Clean. Group Group } - -func (DeclareResponse) isBrevityResponse() {} diff --git a/pkg/brevity/faded.go b/pkg/brevity/faded.go index 787d693a..e4e26c52 100644 --- a/pkg/brevity/faded.go +++ b/pkg/brevity/faded.go @@ -6,5 +6,3 @@ type FadedCall struct { // Group which has faded. Group Group } - -func (FadedCall) isBrevityResponse() {} diff --git a/pkg/brevity/merged.go b/pkg/brevity/merged.go index 131a31cf..b49d0110 100644 --- a/pkg/brevity/merged.go +++ b/pkg/brevity/merged.go @@ -10,8 +10,6 @@ type MergedCall struct { Callsigns []string } -func (MergedCall) isBrevityResponse() {} - const ( // MergeEntryDistance is the distance at which contacts are considered to enter the merge. MergeEntryDistance = 3 * unit.NauticalMile diff --git a/pkg/brevity/picture.go b/pkg/brevity/picture.go index c39f6ad1..34c9cbaf 100644 --- a/pkg/brevity/picture.go +++ b/pkg/brevity/picture.go @@ -22,5 +22,3 @@ type PictureResponse struct { // Groups included in the PICTURE. This is a maximum of 3 groups. Groups []Group } - -func (PictureResponse) isBrevityResponse() {} diff --git a/pkg/brevity/radiocheck.go b/pkg/brevity/radiocheck.go index d1c16f27..d41c5b2b 100644 --- a/pkg/brevity/radiocheck.go +++ b/pkg/brevity/radiocheck.go @@ -18,5 +18,3 @@ type RadioCheckResponse struct { // RadarContact indicates whether the callsign was found on the radar scope. RadarContact bool } - -func (RadioCheckResponse) isBrevityResponse() {} diff --git a/pkg/brevity/shopping.go b/pkg/brevity/shopping.go index 3aa1c457..b1be9b22 100644 --- a/pkg/brevity/shopping.go +++ b/pkg/brevity/shopping.go @@ -13,5 +13,3 @@ func (r ShoppingRequest) String() string { type ShoppingResponse struct { Callsign string } - -func (ShoppingResponse) isBrevityResponse() {} diff --git a/pkg/brevity/snaplock.go b/pkg/brevity/snaplock.go index bf6e19f7..6d804008 100644 --- a/pkg/brevity/snaplock.go +++ b/pkg/brevity/snaplock.go @@ -26,5 +26,3 @@ type SnaplockResponse struct { // Group that was identified. If Declaration is Unable or Furball, this may be nil. Group Group } - -func (SnaplockResponse) isBrevityResponse() {} diff --git a/pkg/brevity/spiked.go b/pkg/brevity/spiked.go index e03f7ccd..f6b41183 100644 --- a/pkg/brevity/spiked.go +++ b/pkg/brevity/spiked.go @@ -31,5 +31,3 @@ type SpikedResponse struct { // Correleted contact group. If Status is false, this may be nil. Group Group } - -func (SpikedResponse) isBrevityResponse() {} diff --git a/pkg/brevity/strobe.go b/pkg/brevity/strobe.go index 33ac0f35..5c97214e 100644 --- a/pkg/brevity/strobe.go +++ b/pkg/brevity/strobe.go @@ -31,5 +31,3 @@ type StrobeResponse struct { // Correleted contact group. If Status is false, this may be nil. Group Group } - -func (StrobeResponse) isBrevityResponse() {} diff --git a/pkg/brevity/sunrise.go b/pkg/brevity/sunrise.go index 3de112b0..d71c3af0 100644 --- a/pkg/brevity/sunrise.go +++ b/pkg/brevity/sunrise.go @@ -9,5 +9,3 @@ type SunriseCall struct { // Frequency which the GCI is listening on. Frequencies []unit.Frequency } - -func (SunriseCall) isBrevityResponse() {} diff --git a/pkg/brevity/threat.go b/pkg/brevity/threat.go index cc59e839..c72a268b 100644 --- a/pkg/brevity/threat.go +++ b/pkg/brevity/threat.go @@ -9,5 +9,3 @@ type ThreatCall struct { // Group that is threatening the friendly aircraft. Group Group } - -func (ThreatCall) isBrevityResponse() {} diff --git a/pkg/brevity/tripwire.go b/pkg/brevity/tripwire.go index d5409f19..251aa0ac 100644 --- a/pkg/brevity/tripwire.go +++ b/pkg/brevity/tripwire.go @@ -13,5 +13,3 @@ func (r TripwireRequest) String() string { type TripwireResponse struct { Callsign string } - -func (TripwireResponse) isBrevityResponse() {} diff --git a/pkg/brevity/unable.go b/pkg/brevity/unable.go index 60934087..45bb7464 100644 --- a/pkg/brevity/unable.go +++ b/pkg/brevity/unable.go @@ -20,5 +20,3 @@ type SayAgainResponse struct { // This may be empty if the GCI is unsure of the caller's identity. Callsign string } - -func (SayAgainResponse) isBrevityResponse() {} diff --git a/pkg/controller/controller.go b/pkg/controller/controller.go index d068a9cf..197fee95 100644 --- a/pkg/controller/controller.go +++ b/pkg/controller/controller.go @@ -6,7 +6,6 @@ import ( "sync" "time" - "github.com/dharmab/skyeye/pkg/brevity" "github.com/dharmab/skyeye/pkg/coalitions" "github.com/dharmab/skyeye/pkg/radar" "github.com/dharmab/skyeye/pkg/simpleradio" @@ -25,11 +24,11 @@ var ( // Call is an envelope for a GCI call. type Call struct { Context context.Context - Call brevity.Response + Call any } // NewCall creates a new Call message. -func NewCall(ctx context.Context, call brevity.Response) Call { +func NewCall(ctx context.Context, call any) Call { return Call{ Context: ctx, Call: call, From fac550324928d3205e0edf9e4baa24d38290102e Mon Sep 17 00:00:00 2001 From: Dharma Bellamkonda Date: Sun, 22 Feb 2026 22:36:21 -0700 Subject: [PATCH 4/9] Fix Windows CI: download Go modules before generating The go:generate script in pkg/recognizer/parakeet/generate_windows.go uses `go list -m -json` to find the sherpa-onnx-go-windows module directory. Without prior `go mod download`, the module cache is empty and `Dir` is blank, causing `cd "$SHERPA_LIB"` to fail. Co-Authored-By: Claude Sonnet 4.6 --- .github/workflows/skyeye.yaml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.github/workflows/skyeye.yaml b/.github/workflows/skyeye.yaml index cb470bac..bb47ff77 100644 --- a/.github/workflows/skyeye.yaml +++ b/.github/workflows/skyeye.yaml @@ -133,6 +133,9 @@ jobs: mingw-w64-ucrt-x86_64-go mingw-w64-ucrt-x86_64-curl zip + - name: Download Go modules + shell: msys2 {0} + run: go mod download - name: Build Skyeye shell: msys2 {0} run: make skyeye.exe From bd501bca0701aa678e6f554f3fe4471ab355d670 Mon Sep 17 00:00:00 2001 From: Dharma Bellamkonda Date: Sun, 22 Feb 2026 22:48:53 -0700 Subject: [PATCH 5/9] Fix Windows CI: use GOPATH=/ucrt64 when downloading modules The Makefile sets GOPATH=/ucrt64 for Windows builds, so the module cache lives at /ucrt64/pkg/mod. The previous go mod download step used the default GOPATH, putting modules in a different location. This caused go list -m -json (run under GOPATH=/ucrt64 by the Makefile) to return an empty Dir, making the generate script's cd fail. Co-Authored-By: Claude Sonnet 4.6 --- .github/workflows/skyeye.yaml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.github/workflows/skyeye.yaml b/.github/workflows/skyeye.yaml index bb47ff77..9d391c22 100644 --- a/.github/workflows/skyeye.yaml +++ b/.github/workflows/skyeye.yaml @@ -135,6 +135,8 @@ jobs: zip - name: Download Go modules shell: msys2 {0} + env: + GOPATH: /ucrt64 run: go mod download - name: Build Skyeye shell: msys2 {0} From ef18ed5d35e8c4d31d5ce9b2a7c09c8529639258 Mon Sep 17 00:00:00 2001 From: Dharma Bellamkonda Date: Sun, 22 Feb 2026 23:04:48 -0700 Subject: [PATCH 6/9] Fix Windows CI: set GOMODCACHE to Windows path via cygpath When MSYS2's go1.25.5 delegates to go1.26.0 (a Windows-native binary), go1.26.0 receives GOPATH=/ucrt64 but interprets it as D:\ucrt64 (Windows path from drive root), not D:\a\_temp\msys64\ucrt64 (the actual MSYS2 prefix). This causes modules to be downloaded to or looked up from the wrong location. Setting GOMODCACHE explicitly to the Windows-format path via cygpath ensures go1.26.0 always uses the correct directory, regardless of how it interprets the MSYS2 POSIX path. Co-Authored-By: Claude Sonnet 4.6 --- .github/workflows/skyeye.yaml | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/.github/workflows/skyeye.yaml b/.github/workflows/skyeye.yaml index 9d391c22..3e5cfbb3 100644 --- a/.github/workflows/skyeye.yaml +++ b/.github/workflows/skyeye.yaml @@ -133,10 +133,11 @@ jobs: mingw-w64-ucrt-x86_64-go mingw-w64-ucrt-x86_64-curl zip + - name: Configure Go module cache + shell: msys2 {0} + run: echo "GOMODCACHE=$(cygpath -w /ucrt64/pkg/mod)" >> $GITHUB_ENV - name: Download Go modules shell: msys2 {0} - env: - GOPATH: /ucrt64 run: go mod download - name: Build Skyeye shell: msys2 {0} From 2541151a52c313d659cb52efaacdf4ef4a3c1701 Mon Sep 17 00:00:00 2001 From: Dharma Bellamkonda Date: Sun, 22 Feb 2026 23:15:34 -0700 Subject: [PATCH 7/9] Fix Windows generate: eliminate go list -m -json for path discovery The generate script called go list -m -json to find the sherpa-onnx module directory, but this triggers Go's toolchain delegation from MSYS2's go1.25.5 to a Windows-native go1.26.0 binary. The native binary misinterprets MSYS2 POSIX paths (e.g. /ucrt64 becomes D:\ucrt64 instead of D:\a\_temp\msys64\ucrt64), causing it to return an empty Dir. Instead, compute SHERPA_DLL_DIR directly in the Makefile by reading the module version from go.mod with grep+awk (no Go invocation needed) and constructing the POSIX path from the known GOPATH. Pass the result as $SHERPA_LIB to the generate script, which simply cd's to it. Co-Authored-By: Claude Sonnet 4.6 --- Makefile | 9 ++++++++- pkg/recognizer/parakeet/generate_windows.go | 2 +- 2 files changed, 9 insertions(+), 2 deletions(-) diff --git a/Makefile b/Makefile index 7cd0b02a..fc6bd413 100644 --- a/Makefile +++ b/Makefile @@ -49,7 +49,10 @@ BUILD_VARS += CFLAGS='$(CFLAGS)' EXTLDFLAGS = -Wl,-Bstatic $(shell pkg-config $(LIBRARIES) --libs --static) -Wl,-Bdynamic LDFLAGS += -linkmode external -extldflags "$(EXTLDFLAGS)" # On Windows, we copy the ONNX Runtime DLLs so we can package them with the binary during distribution. -SHERPA_DLL_DIR := $(shell $(GOBUILDVARS) $(GO) list -m -json github.com/k2-fsa/sherpa-onnx-go-windows 2>/dev/null | grep '"Dir"' | cut -d'"' -f4)/lib/x86_64-pc-windows-gnu +# The module version is read directly from go.mod to avoid invoking Go (which would trigger +# toolchain delegation to a Windows-native binary that misinterprets MSYS2 POSIX paths). +SHERPA_VERSION := $(shell grep 'k2-fsa/sherpa-onnx-go-windows' go.mod | awk '{print $$2}') +SHERPA_DLL_DIR := /ucrt64/pkg/mod/github.com/k2-fsa/sherpa-onnx-go-windows@$(SHERPA_VERSION)/lib/x86_64-pc-windows-gnu SHERPA_DLLS = sherpa-onnx-c-api.dll onnxruntime.dll sherpa-onnx-cxx-api.dll endif @@ -115,7 +118,11 @@ install-macos-dependencies: .PHONY: generate generate: +ifeq ($(OS_DISTRIBUTION),Windows) + SHERPA_LIB="$(SHERPA_DLL_DIR)" $(BUILD_VARS) $(GO) generate $(BUILD_FLAGS) ./... +else $(BUILD_VARS) $(GO) generate $(BUILD_FLAGS) ./... +endif $(SKYEYE_BIN): generate $(SKYEYE_SOURCES) $(BUILD_VARS) $(GO) build $(BUILD_FLAGS) ./cmd/skyeye/ diff --git a/pkg/recognizer/parakeet/generate_windows.go b/pkg/recognizer/parakeet/generate_windows.go index a009f564..b493943b 100644 --- a/pkg/recognizer/parakeet/generate_windows.go +++ b/pkg/recognizer/parakeet/generate_windows.go @@ -2,4 +2,4 @@ package parakeet -//go:generate sh -c "SHERPA_LIB=$(go list -m -json github.com/k2-fsa/sherpa-onnx-go-windows 2>/dev/null | grep '\"Dir\"' | cut -d'\"' -f4)/lib/x86_64-pc-windows-gnu && cd \"$SHERPA_LIB\" && gendef sherpa-onnx-c-api.dll && dlltool -d sherpa-onnx-c-api.def -l libsherpa-onnx-c-api.dll.a && gendef onnxruntime.dll && dlltool -d onnxruntime.def -l libonnxruntime.dll.a && gendef sherpa-onnx-cxx-api.dll && dlltool -d sherpa-onnx-cxx-api.def -l libsherpa-onnx-cxx-api.dll.a" +//go:generate sh -c "cd \"$SHERPA_LIB\" && gendef sherpa-onnx-c-api.dll && dlltool -d sherpa-onnx-c-api.def -l libsherpa-onnx-c-api.dll.a && gendef onnxruntime.dll && dlltool -d onnxruntime.def -l libonnxruntime.dll.a && gendef sherpa-onnx-cxx-api.dll && dlltool -d sherpa-onnx-cxx-api.def -l libsherpa-onnx-cxx-api.dll.a" From ca3c53fde79fd31b703eefe07e4da58da4babb79 Mon Sep 17 00:00:00 2001 From: Dharma Bellamkonda Date: Sun, 22 Feb 2026 23:23:15 -0700 Subject: [PATCH 8/9] Simplify Windows CI: inline cygpath, move GOMODCACHE to Makefile Instead of a separate "Configure Go module cache" step that writes GOMODCACHE to GITHUB_ENV, compute the Windows-format path via cygpath directly in the Makefile's GOBUILDVARS (so go build and go generate automatically use the correct module cache) and inline it in the go mod download step. Co-Authored-By: Claude Sonnet 4.6 --- .github/workflows/skyeye.yaml | 5 +---- Makefile | 3 ++- 2 files changed, 3 insertions(+), 5 deletions(-) diff --git a/.github/workflows/skyeye.yaml b/.github/workflows/skyeye.yaml index 3e5cfbb3..266efa44 100644 --- a/.github/workflows/skyeye.yaml +++ b/.github/workflows/skyeye.yaml @@ -133,12 +133,9 @@ jobs: mingw-w64-ucrt-x86_64-go mingw-w64-ucrt-x86_64-curl zip - - name: Configure Go module cache - shell: msys2 {0} - run: echo "GOMODCACHE=$(cygpath -w /ucrt64/pkg/mod)" >> $GITHUB_ENV - name: Download Go modules shell: msys2 {0} - run: go mod download + run: GOMODCACHE=$(cygpath -w /ucrt64/pkg/mod) go mod download - name: Build Skyeye shell: msys2 {0} run: make skyeye.exe diff --git a/Makefile b/Makefile index fc6bd413..d95b83d1 100644 --- a/Makefile +++ b/Makefile @@ -41,7 +41,8 @@ SKYEYE_BIN = skyeye.exe SKYEYE_SCALER_BIN = skyeye-scaler.exe # Override Windows Go environment with MSYS2 UCRT64 Go environment GO = /ucrt64/bin/go -GOBUILDVARS += GOROOT="/ucrt64/lib/go" GOPATH="/ucrt64" +GOMODCACHE_NATIVE := $(shell cygpath -w /ucrt64/pkg/mod) +GOBUILDVARS += GOROOT="/ucrt64/lib/go" GOPATH="/ucrt64" GOMODCACHE="$(GOMODCACHE_NATIVE)" # On Windows, we statically link opus and soxr so users don't need to install them. LIBRARIES = opus soxr CFLAGS = $(shell pkg-config $(LIBRARIES) --cflags --static) From 0237e70f37614105d8c0ac6cd9ac8e884f1f67cf Mon Sep 17 00:00:00 2001 From: Dharma Bellamkonda Date: Mon, 23 Feb 2026 07:26:49 -0700 Subject: [PATCH 9/9] Move SHERPA_DLL_DIR into BUILD_VARS on Windows Avoids the ifeq in the generate target and keeps a consistent name between the Makefile variable and the env var used in the generate script. Co-Authored-By: Claude Sonnet 4.6 --- Makefile | 5 +---- pkg/recognizer/parakeet/generate_windows.go | 2 +- 2 files changed, 2 insertions(+), 5 deletions(-) diff --git a/Makefile b/Makefile index d95b83d1..087438cc 100644 --- a/Makefile +++ b/Makefile @@ -55,6 +55,7 @@ LDFLAGS += -linkmode external -extldflags "$(EXTLDFLAGS)" SHERPA_VERSION := $(shell grep 'k2-fsa/sherpa-onnx-go-windows' go.mod | awk '{print $$2}') SHERPA_DLL_DIR := /ucrt64/pkg/mod/github.com/k2-fsa/sherpa-onnx-go-windows@$(SHERPA_VERSION)/lib/x86_64-pc-windows-gnu SHERPA_DLLS = sherpa-onnx-c-api.dll onnxruntime.dll sherpa-onnx-cxx-api.dll +BUILD_VARS += SHERPA_DLL_DIR="$(SHERPA_DLL_DIR)" endif BUILD_VARS += LDFLAGS='$(LDFLAGS)' @@ -119,11 +120,7 @@ install-macos-dependencies: .PHONY: generate generate: -ifeq ($(OS_DISTRIBUTION),Windows) - SHERPA_LIB="$(SHERPA_DLL_DIR)" $(BUILD_VARS) $(GO) generate $(BUILD_FLAGS) ./... -else $(BUILD_VARS) $(GO) generate $(BUILD_FLAGS) ./... -endif $(SKYEYE_BIN): generate $(SKYEYE_SOURCES) $(BUILD_VARS) $(GO) build $(BUILD_FLAGS) ./cmd/skyeye/ diff --git a/pkg/recognizer/parakeet/generate_windows.go b/pkg/recognizer/parakeet/generate_windows.go index b493943b..9ecce6e6 100644 --- a/pkg/recognizer/parakeet/generate_windows.go +++ b/pkg/recognizer/parakeet/generate_windows.go @@ -2,4 +2,4 @@ package parakeet -//go:generate sh -c "cd \"$SHERPA_LIB\" && gendef sherpa-onnx-c-api.dll && dlltool -d sherpa-onnx-c-api.def -l libsherpa-onnx-c-api.dll.a && gendef onnxruntime.dll && dlltool -d onnxruntime.def -l libonnxruntime.dll.a && gendef sherpa-onnx-cxx-api.dll && dlltool -d sherpa-onnx-cxx-api.def -l libsherpa-onnx-cxx-api.dll.a" +//go:generate sh -c "cd \"$SHERPA_DLL_DIR\" && gendef sherpa-onnx-c-api.dll && dlltool -d sherpa-onnx-c-api.def -l libsherpa-onnx-c-api.dll.a && gendef onnxruntime.dll && dlltool -d onnxruntime.def -l libonnxruntime.dll.a && gendef sherpa-onnx-cxx-api.dll && dlltool -d sherpa-onnx-cxx-api.def -l libsherpa-onnx-cxx-api.dll.a"