diff --git a/.github/workflows/skyeye.yaml b/.github/workflows/skyeye.yaml index cb470bac..266efa44 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: 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 7cd0b02a..087438cc 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) @@ -49,8 +50,12 @@ 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 +BUILD_VARS += SHERPA_DLL_DIR="$(SHERPA_DLL_DIR)" endif BUILD_VARS += LDFLAGS='$(LDFLAGS)' diff --git a/internal/application/compose.go b/internal/application/compose.go index ee120e57..c739fae0 100644 --- a/internal/application/compose.go +++ b/internal/application/compose.go @@ -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/spiked.go b/pkg/brevity/spiked.go index 4b168e57..f6b41183 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. 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/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/recognizer/parakeet/generate_windows.go b/pkg/recognizer/parakeet/generate_windows.go index a009f564..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 "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_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" diff --git a/pkg/simpleradio/message.go b/pkg/simpleradio/message.go index b5c7bd6b..2e3d30b9 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 } @@ -51,14 +52,16 @@ func (c *Client) handleMessage(message types.Message) { logMessageAndIgnore(message) case types.MessageSync: c.syncClients(message.Clients) - case types.MessageUpdate: - c.syncClient(message.Client) - case types.MessageRadioUpdate: - c.syncClient(message.Client) + case types.MessageUpdate, types.MessageRadioUpdate: + 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) }) }