Диcклеймер — я практически не знаком с астрономией, только вот в Kerbal на орбиту выходил и как-то мне удалось сделать парочку орбитальных маневров. Но тема интересная, так, что даже если я где-то не верно выражаюсь — сорян.


Все ссылочки в конце статьи.


На всякий случай, под видимостью подразумевается не только видимость глазом.


Для того, что бы начать взаимодействовать со спутником, он должен быть виден в небе. Видимость спутника зависит от того, где он находится на текущий момент относительно вашего расположения. Существуют спутники которые никогда не видны из некоторых локаций. К таким относятся спутники находящиеся на геосинхронной орбите — они видны только с половины Земли, так как их движение синхронизировано с вращением Земли.



Так же на видимость спутника влияет наклон орбиты.



Орбиты с низким наклоном находятся ближе к экватору. Находясь на экваторе, вы наверняка увидите спутники в какой-то момент времени. Если вы находитесь выше по широте, то видимость спутника зависит от высоты его орбиты. Чем дальше спутник от Земли, тем больше локаций с которых его можно увидеть. Например спутник LEMUR-2 JOEL, находится на высоте 640км и может быть виден в Найроби и Дар-эс-Салам, но его никогда не видно в Ереване.
O3B FM8 который находится на высоте 8,000км, можно увидеть из Стокгольма или южней из Tierra del Fuego. Оба спутника с очень низким наклоном и возле экватора. Высота спутника с высоким наклоном (полярная орбита) так же влияет на то откуда он может быть виден, но в основном они видны отовсюду (если хорошенько подождать, пока он пролетит в поле вашего зрения).



Что такое орбита?



(Откройте изображение в новой вкладке, что бы получше разглядеть)


Все орбиты разные. Большинство спутников находятся в 3-х основных зонах Низкая околоземная орбита (LEO), Средняя околоземная (MEO), или Геосинхронной орбите (GSO).


Спутники на низкой орбите, находятся ближе всех к поверхности Земли (до 2,000км), требуют меньше всего энергии для выхода на такую орбиту (так как если вы играли в Kerbal, то знаете, чем больше скорость тем выше орбита, а скорость это кол-во сожженного топлива и время работы двигателя), а так же с ними проще общаться. На такой орбите находится Международная космическая станция и спутники телефонной связи. Такие спутники движутся по небу довольно быстро и находятся в поле зрения около 20-30 минут. Но до нового пролета ждать примерно столько же. Вращение вокруг земли занимает около 90 минут.


Медиана, средняя орбита или Medium Earth Orbit — орбита, которая находится между геосинхронной орбитой (GSO) и низкой орбитой (LEO). Большинство спутников находятся на высоте между 10,000км и 30,000км. Орбиты между 2000км и 8000км не желательны из-за высокой радиации от Van Allen Belt (пояса радиации).
На таких орбитах много GPS спутников. Они так же двигаются вокруг поверхности Земли, но медленней (так как орбита выше). Оборот вокруг Земли занимает примерно 12 часов.



На геосинхронной орбите спутники делают 1 оборот за день. Находятся они на высоте 36,000км. Если геосинхронный спутник вращается вокруг экватора, то такая орбита называется геостационарная. Спутники всегда находятся в одном и том же положении относительно наблюдателя на земле. Словно они находятся на высокой вышке. Задержка сигнала достигает пол секунды.


Зачем целиться в спутник?



Так как я знаком с FPV хобби, я что-то уже знаю про антенны. Минимально, но понимаю, что есть антенны направленные которые словно фонарик, а есть всенаправленные антенны.



Направленные антенны лучше принимают и отправляют сигналы, но они ограничены "полем зрения".


В хобби обычно комбинируют антенны:



(плоские это направленные патч антенны, а ниже всенаправленные)


Но вернемся к нашей теме, для общения со спутниками используют такие вот параболические антенны:



Работают следующим образом — собирая сигнал на своей параболической "тарелке" они концентрируют его на "feed antenna". Не могу найти как это называется по-русски. Скорей всего принимающая антенна.


Можно провести аналогию с солнечными электростанциями:



Слежение за спутником (прицеливание вашей направленной антенны), технически не обязательно для отправки и получения сигнала. Но вместо того, что бы тратить энергию и отправлять сигналы во все стороны, это можно делать концентрировано. Такой подход более эффективен и позволяет использовать антенны поменьше.


Находим положение спутника относительно вашей локации


Для получениях данных о движении спутников относительно конкретной локации, можно воспользоваться библиотекой Skyfield или публичной базой данных. С помощью этих данных можно предсказать, когда конкретный спутник будет над вами и в поле зрения. Следовательно, и прицеливать антенну.


Например мы можем предсказать когда МКС пролетит над вами, и если на улице не слишком светло, то вы сможете увидеть МКС невооруженным глазом. Станция делает один оборот в 90 минут, двигается она по ночному небу очень быстро. Относительно вас она будет в поле зрения не более 8 минут. Если станция будет пролетать низко — коло 10 — 2- градусов над горизонтом, то возможно из-за зданий и деревьев ее будет сложно разглядеть. Но как я сказал выше, мы можем подождать, пока пролет станции будет повыше и использовать данные для предсказания положения в будущем.


Угол спутника к горизонту от вашей локации, называют высотой (altitude) или elevation.
Горизонтальная система координат.



Для того, что бы хорошо наблюдать спутник, надо примерно 45 градусов. Другой параметр который нам интересен — азимут. Это направление, куда надо смотреть. Азимут равный 0 градусов находится на севере, 180 на юге.


Skyfield написан на Python, поэтому придется использовать Python (3). Конечно, можно сделать и на другом языке, но не будем усложнять себе задачу.


Нам необходимо поставить одну зависимость:


pip3 install skyfield

И подключить все необходимые пакеты следующим образом:


index.py


import datetime
import time
from skyfield.api import load, Topos, EarthSatellite

Далее несколько необходимых нам констант:


TLE_FILE = "https://celestrak.com/NORAD/elements/active.txt" # DB file to download
ISS_NAME = "ISS (ZARYA)"

# Coordinats
# 57°00'13.7"N 37°02'53.7"E
# Kimrsky District, Tver Oblast, Russia
LONGITUDE = 57.003810
LATITUDE = 37.048262

Основной класс:


class SatelliteObserver:

    def __init__(self, where: Topos, what: EarthSatellite):
        self.where = where
        self.sat = what
        self.sat_name = what.name
        self.ts = load.timescale(builtin=True)
# ...

В этом "конструкторе" мы принимаем координаты, название спутника (такое, как в данных), и EarthSatellite своего рода обертка которая нам позволяет работать с данными.



# ...
@classmethod
    def from_strings(cls, longitude: str or float, latitude: str or float, sat_name: str, tle_file: str) -> 'SatelliteObserver':
        place = Topos(latitude, longitude)
        satellites = load.tle(tle_file)
        print("loaded {} sats from {}".format(len(satellites), tle_file))
        _sats_by_name = {sat.name: sat for sat in satellites.values()}
        satellite = _sats_by_name[sat_name]
        return cls(place, satellite)
# ...

В данной функции используем векторную функцию Topos которая знает о месте на Земле (определение из этой функции).


Затем загружаем все спутники из файла, и находим интересующий нас _sats_by_name[sat_name].


def altAzDist_at(self, at: float) -> (float, float, float):
        """
        :param at: Unix time GMT (timestamp)
        :return: (altitude, azimuth, distance)
        """
        current_gmt = datetime.datetime.utcfromtimestamp(at)
        current_ts = self.ts.utc(current_gmt.year, current_gmt.month, current_gmt.day, current_gmt.hour,
                            current_gmt.minute, current_gmt.second + current_gmt.microsecond / 1000000.0)
        difference = self.sat - self.where
        observer_to_sat = difference.at(current_ts)
        altitude, azimuth, distance = observer_to_sat.altaz()
        return (altitude.degrees, azimuth.degrees, distance.km)

Данный кусок кода отвечает за нахождение высоты, азимута и расстояние на время переданное в at и относительно заданной позиции.


    def current_altAzDist(self) -> (float, float, float):
        return self.altAzDist_at(time.mktime(time.gmtime()))

    def above_horizon(self, at: float) -> bool:
        """
        :param at: Unix time GMT
        :return:
        """
        (alt, az, dist) = self.altAzDist_at(at)
        return alt > 0

current_altAzDist(self) — небольшая обертка для передачи времени на данный момент и функция определения, находится ли спутник выше горизонта.


И наконец main функция:


def main():
    iss = SatelliteObserver.from_strings(LONGITUDE, LATITUDE, ISS_NAME, TLE_FILE)
    elevation, azimuth, distance = iss.current_altAzDist()
    visible = "visible!" if elevation > 0 else "not visible =/"
    print("ISS from latitude {}, longitude {}: azimuth {}, elevation {} ({})".format(LATITUDE, LONGITUDE, azimuth, elevation, visible))

if __name__ == "__main__":
    main()

Полный код
import datetime
import time
from skyfield.api import load, Topos, EarthSatellite

TLE_FILE = "https://celestrak.com/NORAD/elements/active.txt" # DB file to download
ISS_NAME = "ISS (ZARYA)"

# Coordinats
# 57°00'13.7"N 37°02'53.7"E
# Kimrsky District, Tver Oblast, Russia
LONGITUDE = 57.003810
LATITUDE = 37.048262

class SatelliteObserver:

    def __init__(self, where: Topos, what: EarthSatellite):
        self.where = where
        self.sat = what
        self.sat_name = what.name
        self.ts = load.timescale(builtin=True)

    @classmethod
    def from_strings(cls, longitude: str or float, latitude: str or float, sat_name: str, tle_file: str) -> 'SatelliteObserver':
        place = Topos(latitude, longitude)
        satellites = load.tle(tle_file)
        print("loaded {} sats from {}".format(len(satellites), tle_file))
        _sats_by_name = {sat.name: sat for sat in satellites.values()}
        satellite = _sats_by_name[sat_name]
        return cls(place, satellite)

    def altAzDist_at(self, at: float) -> (float, float, float):
        """
        :param at: Unix time GMT (timestamp)
        :return: (altitude, azimuth, distance)
        """
        current_gmt = datetime.datetime.utcfromtimestamp(at)
        current_ts = self.ts.utc(current_gmt.year, current_gmt.month, current_gmt.day, current_gmt.hour,
                            current_gmt.minute, current_gmt.second + current_gmt.microsecond / 1000000.0)
        difference = self.sat - self.where
        observer_to_sat = difference.at(current_ts)
        altitude, azimuth, distance = observer_to_sat.altaz()
        return (altitude.degrees, azimuth.degrees, distance.km)

    def current_altAzDist(self) -> (float, float, float):
        return self.altAzDist_at(time.mktime(time.gmtime()))

    def above_horizon(self, at: float) -> bool:
        """
        :param at: Unix time GMT
        :return:
        """
        (alt, az, dist) = self.altAzDist_at(at)
        return alt > 0

def main():
    iss = SatelliteObserver.from_strings(LONGITUDE, LATITUDE, ISS_NAME, TLE_FILE)
    elevation, azimuth, distance = iss.current_altAzDist()
    visible = "visible!" if elevation > 0 else "not visible =/"
    print("ISS from latitude {}, longitude {}: azimuth {}, elevation {} ({})".format(LATITUDE, LONGITUDE, azimuth, elevation, visible))

if __name__ == "__main__":
    main()

Для запуска программы используем python3:


python3 index.py

Результат выполнения —


[#################################] 100% active.txt
loaded 6351 sats from https://celestrak.com/NORAD/elements/active.txt
ISS from latitude 37.048262, longitude 57.00381: azimuth 55.695482310974974, elevation 6.232187065056109 (visible!)

В данном выводе мы получаем градус (как в компасе) в какую сторону смотреть и угол (elevation), как высоко над горизонтом.


(visible!) в данном случае — видно!.. Но 6 градусов в городе маловато. Как я писал выше, надо хотя бы 45.


Такие данные можно передавать на сервопривод и управлять направлением антенны в реальном времени. Но об этом в следующих статьях.


Данные которые мы скачиваем (https://celestrak.com/NORAD/elements/active.txt) могут устаревать, так как спутники постоянно корректируют свою орбиту по разным причинам, поэтому следует скачивать свежее как можно чаще.


Спасибо за внимание.


UPD
Немного почитав документацию, получилось упростить до такого:


import datetime
import time
from skyfield.api import load, Topos, EarthSatellite

# Путь к файлу с данными
TLE_FILE = "https://celestrak.com/NORAD/elements/active.txt" # DB file to download

SAT_NAME = "ISS (ZARYA)"

# Загружаем данные
satellites = load.tle(TLE_FILE)

# Находим наш спутник по имени в данных
print("loaded {} sats from {}".format(len(satellites), TLE_FILE))
_sats_by_name = {sat.name: sat for sat in satellites.values()}
satellite = _sats_by_name[SAT_NAME]

ts = load.timescale()
t = ts.now()

# Локация с которой мы наблюдаем
location = Topos('52.173141 N', '44.108612 E')

# Находим азимут и угол над горизонтом
difference = satellite - location
topocentric = difference.at(t)

alt, az, distance = topocentric.altaz()

if alt.degrees > 0:
    print('The ISS is above the horizon')

print(alt)
print(az)
print(int(distance.km), 'km')

Первый вариант я нашел на просторах интернета и похоже он слишком специфичен или устарел.


Так же удалось сделать поиск всех видимых спутников в конкретной части неба:


import datetime
import time
from skyfield.api import load, Topos, EarthSatellite

TLE_FILE = "https://celestrak.com/NORAD/elements/active.txt" # DB file to download

MIN_DEGREE = 45
MIN_AZ = 50
MAX_AZ = 140

satellites = load.tle(TLE_FILE)
ts = load.timescale()
t = ts.now()

location = Topos('52.173141 N', '44.108612 E')

for sat in satellites.values():
    difference = sat - location
    topocentric = difference.at(t)

    alt, az, distance = topocentric.altaz()

    azValue = int(str(az).replace('deg', '').split(" ")[0])

    if alt.degrees >= MIN_DEGREE and azValue >= MIN_AZ and azValue <= MAX_AZ:
        print(sat.name, alt, az)

upd: tvr спасибо за правки
upd: Fenja спасибо за правки
upd: sandroDan спасибо за дополнение в коментариях
upd: dpytaylo спасибо за правки
upd: extempl спасибо за правки


Литература


https://nyan-sat.com/chapter0.html
геосинхронная орбита
наклон орбиты
полярная орбита