Packages
sidereon
2.1.0
3.0.4
3.0.3
3.0.2
3.0.1
3.0.0
2.1.0
2.0.0
1.4.1
1.3.3
1.3.1
1.3.0
1.2.0
1.1.1
1.1.0
1.0.1
1.0.0
0.39.1
0.39.0
0.38.0
0.37.0
0.36.5
0.36.4
0.36.3
0.36.2
0.36.1
0.36.0
0.35.0
0.34.0
0.33.1
0.32.0
0.31.2
0.30.0
0.29.2
0.29.1
0.29.0
0.28.1
0.28.0
0.27.1
0.27.0
0.26.1
0.25.0
0.24.0
0.23.0
0.22.0
0.21.0
0.20.0
0.19.1
0.19.0
0.18.0
0.17.0
0.16.1
0.15.0
0.14.0
0.13.0
0.12.0
0.11.1
0.11.0
0.10.1
0.10.0
0.9.2
0.9.1
0.9.0
0.8.0
retired
Satellite toolkit for Elixir with SGP4 propagation, coordinate transforms, GNSS positioning, orbit determination, conjunction assessment, pass prediction, and a Rust NIF backend.
Current section
Files
Jump to
Current section
Files
examples/iss_tracker.livemd
# ISS Tracker
```elixir
Mix.install([
{:sidereon, "~> 2.1"},
{:kino, "~> 0.14"},
{:kino_maplibre, "~> 0.1"}
])
```
## Parse the ISS elements
Two-line element sets for the ISS are published continuously by CelesTrak and
Space-Track. Paste a fresh pair here (these are real elements); everything below
runs from them with no network access.
```elixir
{:ok, iss} =
Sidereon.parse_tle(
"1 25544U 98067A 26178.50947090 .00006280 00000+0 12016-3 0 9996",
"2 25544 51.6322 248.9966 0004278 238.4942 121.5629 15.49454046573359"
)
IO.puts("Catalog: #{iss.catalog_number}")
IO.puts("Epoch: #{iss.epoch}")
IO.puts("Period: #{Float.round(1440 / iss.mean_motion, 1)} minutes")
```
## Where Is It Right Now?
```elixir
now = DateTime.utc_now()
{:ok, geo} = Sidereon.geodetic(iss, now)
Kino.Markdown.new("""
### ISS Position at #{Calendar.strftime(now, "%H:%M:%S UTC")}
| | |
|---|---|
| **Latitude** | #{Float.round(geo.latitude, 4)}° |
| **Longitude** | #{Float.round(geo.longitude, 4)}° |
| **Altitude** | #{Float.round(geo.altitude_km, 1)} km |
""")
```
## Plot the Ground Track
Propagate the ISS over one full orbit (~93 minutes) and plot the ground track:
```elixir
# Propagate every 30 seconds for one orbit
points =
for i <- 0..186 do
dt = DateTime.add(now, i * 30, :second)
{:ok, geo} = Sidereon.geodetic(iss, dt)
%{lat: geo.latitude, lon: geo.longitude, alt: geo.altitude_km, time: dt}
end
# Current position
current = hd(points)
# Build GeoJSON for the track
track_coords = Enum.map(points, fn p -> [p.lon, p.lat] end)
MapLibre.new(
style: :street,
center: {current.lon, current.lat},
zoom: 2
)
|> MapLibre.add_source("track",
type: :geojson,
data: %{
type: "Feature",
geometry: %{type: "LineString", coordinates: track_coords}
}
)
|> MapLibre.add_layer(
id: "track-line",
type: :line,
source: "track",
paint: %{"line-color" => "#ff4444", "line-width" => 2}
)
|> MapLibre.add_source("iss-now",
type: :geojson,
data: %{
type: "Feature",
geometry: %{type: "Point", coordinates: [current.lon, current.lat]},
properties: %{title: "ISS"}
}
)
|> MapLibre.add_layer(
id: "iss-marker",
type: :circle,
source: "iss-now",
paint: %{
"circle-radius" => 8,
"circle-color" => "#ff0000",
"circle-stroke-width" => 2,
"circle-stroke-color" => "#ffffff"
}
)
```
## Look Angles from Your Location
```elixir
# Change these to your location!
my_station = %{latitude: 40.7128, longitude: -74.006, altitude_m: 10.0}
{:ok, look} = Sidereon.look_angle(iss, now, my_station)
status =
if look.elevation > 0 do
"ABOVE HORIZON, go outside and look!"
else
"Below horizon (#{Float.round(look.elevation, 1)}°)"
end
Kino.Markdown.new("""
### ISS from New York City
| | |
|---|---|
| **Azimuth** | #{Float.round(look.azimuth, 1)}° |
| **Elevation** | #{Float.round(look.elevation, 1)}° |
| **Range** | #{Float.round(look.range_km, 0)} km |
| **Status** | #{status} |
""")
```
## Next Passes
```elixir
tomorrow = DateTime.add(now, 86400, :second)
{:ok, passes} = Sidereon.Passes.predict(iss, my_station, now, tomorrow, min_elevation: 10.0)
rows =
Enum.map(passes, fn pass ->
duration = DateTime.diff(pass.set, pass.rise)
%{
rise: Calendar.strftime(pass.rise, "%H:%M:%S"),
set: Calendar.strftime(pass.set, "%H:%M:%S"),
max_el: "#{Float.round(pass.max_elevation, 1)}°",
duration: "#{duration}s"
}
end)
Kino.DataTable.new(rows, name: "ISS Passes (next 24h, >10 deg elevation)")
```
## RF Link Budget
What signal would you get from a 437 MHz amateur radio contact?
```elixir
if look.elevation > 0 do
fspl = Sidereon.RF.fspl(look.range_km, 437.0)
eirp = Sidereon.RF.eirp(30.0, 10.0) # 1W + 10 dBi Yagi
margin = Sidereon.RF.link_margin(%{
eirp_dbw: eirp,
fspl_db: fspl,
receiver_gt_dbk: -20.0,
other_losses_db: 3.0,
required_cn0_dbhz: 30.0
})
Kino.Markdown.new("""
### UHF Link Budget (437 MHz)
| | |
|---|---|
| **EIRP** | #{Float.round(eirp, 1)} dBW |
| **Path Loss** | #{Float.round(fspl, 1)} dB |
| **Link Margin** | #{Float.round(margin, 1)} dB |
| **Link** | #{if margin > 0, do: "Closes", else: "Does not close"} |
""")
else
Kino.Markdown.new("*ISS is below the horizon, link budget not applicable.*")
end
```