feat(monitor/collectors/iss): SGP4 propagator wrapper (Position + LookAngles via go-satellite)
This commit is contained in:
parent
317fadeeef
commit
928db0befc
|
|
@ -62,6 +62,7 @@ require (
|
|||
github.com/jackc/pgpassfile v1.0.0 // indirect
|
||||
github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 // indirect
|
||||
github.com/jackc/puddle/v2 v2.2.2 // indirect
|
||||
github.com/joshuaferrara/go-satellite v0.0.0-20220611180459-512638c64e5b // indirect
|
||||
github.com/klauspost/compress v1.18.5 // indirect
|
||||
github.com/knadh/koanf/maps v0.1.2 // indirect
|
||||
github.com/leodido/go-urn v1.4.0 // indirect
|
||||
|
|
@ -89,6 +90,7 @@ require (
|
|||
github.com/moby/term v0.5.2 // indirect
|
||||
github.com/opencontainers/go-digest v1.0.0 // indirect
|
||||
github.com/opencontainers/image-spec v1.1.1 // indirect
|
||||
github.com/pkg/errors v0.9.1 // indirect
|
||||
github.com/pmezard/go-difflib v1.0.0 // indirect
|
||||
github.com/power-devops/perfstat v0.0.0-20240221224432-82ca36839d55 // indirect
|
||||
github.com/segmentio/asm v1.2.1 // indirect
|
||||
|
|
|
|||
|
|
@ -100,6 +100,8 @@ github.com/jackc/puddle/v2 v2.2.2 h1:PR8nw+E/1w0GLuRFSmiioY6UooMp6KJv0/61nB7icHo
|
|||
github.com/jackc/puddle/v2 v2.2.2/go.mod h1:vriiEXHvEE654aYKXXjOvZM39qJ0q+azkZFrfEOc3H4=
|
||||
github.com/jmoiron/sqlx v1.4.0 h1:1PLqN7S1UYp5t4SrVVnt4nUVNemrDAtxlulVe+Qgm3o=
|
||||
github.com/jmoiron/sqlx v1.4.0/go.mod h1:ZrZ7UsYB/weZdl2Bxg6jCRO9c3YHl8r3ahlKmRT4JLY=
|
||||
github.com/joshuaferrara/go-satellite v0.0.0-20220611180459-512638c64e5b h1:JlltDRgni6FuoFwluvoZCrE6cmpojccO4WsqeYlFJLE=
|
||||
github.com/joshuaferrara/go-satellite v0.0.0-20220611180459-512638c64e5b/go.mod h1:msW2QeN9IsnRyvuK8OBAzBwn6DHwXpiAiqBk8dbLfrU=
|
||||
github.com/klauspost/compress v1.18.5 h1:/h1gH5Ce+VWNLSWqPzOVn6XBO+vJbCNGvjoaGBFW2IE=
|
||||
github.com/klauspost/compress v1.18.5/go.mod h1:cwPg85FWrGar70rWktvGQj8/hthj3wpl0PGDogxkrSQ=
|
||||
github.com/knadh/koanf/maps v0.1.2 h1:RBfmAW5CnZT+PJ1CVc1QSJKf4Xu9kxfQgYVQSu8hpbo=
|
||||
|
|
@ -168,10 +170,14 @@ github.com/moby/sys/userns v0.1.0 h1:tVLXkFOxVu9A64/yh59slHVv9ahO9UIev4JZusOLG/g
|
|||
github.com/moby/sys/userns v0.1.0/go.mod h1:IHUYgu/kao6N8YZlp9Cf444ySSvCmDlmzUcYfDHOl28=
|
||||
github.com/moby/term v0.5.2 h1:6qk3FJAFDs6i/q3W/pQ97SX192qKfZgGjCQqfCJkgzQ=
|
||||
github.com/moby/term v0.5.2/go.mod h1:d3djjFCrjnB+fl8NJux+EJzu0msscUP+f8it8hPkFLc=
|
||||
github.com/onsi/ginkgo v1.2.1-0.20160509182050-5437a97bf824/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
|
||||
github.com/onsi/gomega v0.0.0-20160516222431-c73e51675ad2/go.mod h1:C1qb7wdrVGGVU+Z6iS04AVkA3Q65CEZX59MT0QO5uiA=
|
||||
github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8Oi/yOhh5U=
|
||||
github.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM=
|
||||
github.com/opencontainers/image-spec v1.1.1 h1:y0fUlFfIZhPF1W537XOLg0/fcx6zcHCJwooC2xJA040=
|
||||
github.com/opencontainers/image-spec v1.1.1/go.mod h1:qpqAh3Dmcf36wStyyWU+kCeDgrGnAve2nCC8+7h8Q0M=
|
||||
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
|
||||
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
github.com/power-devops/perfstat v0.0.0-20240221224432-82ca36839d55 h1:o4JXh1EVt9k/+g42oCprj/FisM4qX9L3sZB3upGN2ZU=
|
||||
|
|
@ -337,6 +343,7 @@ google.golang.org/protobuf v1.36.8/go.mod h1:fuxRtAxBytpl4zzqUh6/eyUujkJdNiuEkXn
|
|||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
|
||||
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
|
||||
gopkg.in/yaml.v2 v2.0.0-20160301204022-a83829b6f129/go.mod h1:JAlM8MvJe8wmxCU4Bli9HhUf9+ttbYbLASfIpnQbh74=
|
||||
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
|
|
|
|||
|
|
@ -0,0 +1,49 @@
|
|||
// ©AngelaMos | 2026
|
||||
// propagator.go
|
||||
|
||||
package iss
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"math"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
satellite "github.com/joshuaferrara/go-satellite"
|
||||
)
|
||||
|
||||
type Sat struct {
|
||||
inner satellite.Satellite
|
||||
}
|
||||
|
||||
func LoadTLE(line1, line2 string) (Sat, error) {
|
||||
if !strings.HasPrefix(line1, "1 ") || !strings.HasPrefix(line2, "2 ") {
|
||||
return Sat{}, fmt.Errorf("invalid TLE: lines must start with '1 ' and '2 '")
|
||||
}
|
||||
s := satellite.TLEToSat(line1, line2, satellite.GravityWGS84)
|
||||
if s.Error != 0 || s.ErrorStr != "" {
|
||||
return Sat{}, fmt.Errorf("parse TLE: error %d / %s", s.Error, s.ErrorStr)
|
||||
}
|
||||
return Sat{inner: s}, nil
|
||||
}
|
||||
|
||||
func Position(s Sat, t time.Time) (lat, lon, altKm float64) {
|
||||
t = t.UTC()
|
||||
pos, _ := satellite.Propagate(s.inner, t.Year(), int(t.Month()), t.Day(), t.Hour(), t.Minute(), t.Second())
|
||||
gmst := satellite.GSTimeFromDate(t.Year(), int(t.Month()), t.Day(), t.Hour(), t.Minute(), t.Second())
|
||||
altitude, _, ll := satellite.ECIToLLA(pos, gmst)
|
||||
deg := satellite.LatLongDeg(ll)
|
||||
return deg.Latitude, deg.Longitude, altitude
|
||||
}
|
||||
|
||||
func LookAngles(s Sat, t time.Time, observerLatDeg, observerLonDeg, observerAltKm float64) (azimuthDeg, elevationDeg, rangeKm float64) {
|
||||
t = t.UTC()
|
||||
pos, _ := satellite.Propagate(s.inner, t.Year(), int(t.Month()), t.Day(), t.Hour(), t.Minute(), t.Second())
|
||||
jday := satellite.JDay(t.Year(), int(t.Month()), t.Day(), t.Hour(), t.Minute(), t.Second())
|
||||
obs := satellite.LatLong{
|
||||
Latitude: observerLatDeg * math.Pi / 180,
|
||||
Longitude: observerLonDeg * math.Pi / 180,
|
||||
}
|
||||
la := satellite.ECIToLookAngles(pos, obs, observerAltKm, jday)
|
||||
return la.Az * 180 / math.Pi, la.El * 180 / math.Pi, la.Rg
|
||||
}
|
||||
|
|
@ -0,0 +1,55 @@
|
|||
// ©AngelaMos | 2026
|
||||
// propagator_test.go
|
||||
|
||||
package iss_test
|
||||
|
||||
import (
|
||||
"os"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
"github.com/carterperez-dev/monitor-the-situation/backend/internal/collectors/iss"
|
||||
)
|
||||
|
||||
func loadTLE(t *testing.T) (string, string) {
|
||||
t.Helper()
|
||||
body, err := os.ReadFile("testdata/tle.txt")
|
||||
require.NoError(t, err)
|
||||
lines := strings.Split(strings.TrimSpace(string(body)), "\n")
|
||||
require.GreaterOrEqual(t, len(lines), 3, "TLE file must have name + 2 element lines")
|
||||
return strings.TrimRight(lines[1], " \r"), strings.TrimRight(lines[2], " \r")
|
||||
}
|
||||
|
||||
func TestPropagator_PositionWithinReasonableBounds(t *testing.T) {
|
||||
l1, l2 := loadTLE(t)
|
||||
sat, err := iss.LoadTLE(l1, l2)
|
||||
require.NoError(t, err)
|
||||
|
||||
when := time.Date(2026, 5, 2, 3, 0, 0, 0, time.UTC)
|
||||
lat, lon, alt := iss.Position(sat, when)
|
||||
|
||||
require.True(t, lat >= -90 && lat <= 90, "lat=%f", lat)
|
||||
require.True(t, lon >= -180 && lon <= 180, "lon=%f", lon)
|
||||
require.True(t, alt >= 350 && alt <= 500, "ISS altitude is roughly 380-420 km, got %f", alt)
|
||||
}
|
||||
|
||||
func TestPropagator_LookAnglesInValidRange(t *testing.T) {
|
||||
l1, l2 := loadTLE(t)
|
||||
sat, err := iss.LoadTLE(l1, l2)
|
||||
require.NoError(t, err)
|
||||
|
||||
when := time.Date(2026, 5, 2, 3, 0, 0, 0, time.UTC)
|
||||
az, el, rng := iss.LookAngles(sat, when, 40.7128, -74.0060, 0)
|
||||
|
||||
require.True(t, az >= 0 && az <= 360, "az=%f", az)
|
||||
require.True(t, el >= -90 && el <= 90, "el=%f", el)
|
||||
require.Greater(t, rng, 0.0)
|
||||
}
|
||||
|
||||
func TestPropagator_BadTLEReturnsError(t *testing.T) {
|
||||
_, err := iss.LoadTLE("not a tle", "really not a tle")
|
||||
require.Error(t, err)
|
||||
}
|
||||
|
|
@ -0,0 +1 @@
|
|||
{"name":"iss","id":25544,"latitude":10.63651293933,"longitude":-10.956952860344,"altitude":423.74642684314,"velocity":27564.677940445,"visibility":"daylight","footprint":4526.4746978082,"timestamp":1777710988,"daynum":2461162.8586574,"solar_lat":15.425197566858,"solar_lon":50.133029500693,"units":"kilometers"}
|
||||
|
|
@ -0,0 +1 @@
|
|||
[{"OBJECT_NAME":"ISS (ZARYA)","OBJECT_ID":"1998-067A","EPOCH":"2026-05-02T03:14:52.956096","MEAN_MOTION":15.49057929,"ECCENTRICITY":0.0007234,"INCLINATION":51.6309,"RA_OF_ASC_NODE":168.049,"ARG_OF_PERICENTER":13.9157,"MEAN_ANOMALY":346.2029,"EPHEMERIS_TYPE":0,"CLASSIFICATION_TYPE":"U","NORAD_CAT_ID":25544,"ELEMENT_SET_NO":999,"REV_AT_EPOCH":56461,"BSTAR":0.00014312,"MEAN_MOTION_DOT":7.461e-5,"MEAN_MOTION_DDOT":0}]
|
||||
|
|
@ -0,0 +1,3 @@
|
|||
ISS (ZARYA)
|
||||
1 25544U 98067A 26122.13533514 .00007461 00000+0 14312-3 0 9999
|
||||
2 25544 51.6309 168.0490 0007234 13.9157 346.2029 15.49057929564615
|
||||
Loading…
Reference in New Issue