Телескоп через MCP: как я научил Claude наводить Seestar и стекать кадры
У меня есть хобби, которое плохо уживается с работой разработчика: астрофотография. Не потому что не хватает интереса, а потому что она требует ровно того, чего после рабочего дня меньше всего, — терпеливой ручной возни в темноте. Навести телескоп, проверить, что объект действительно в кадре, дождаться накопления, утром перенести кадры на компьютер, сложить их, вытянуть фон, подобрать растяжку. Каждый шаг по отдельности несложный, но вместе это длинная цепочка, на которой легко бросить.
Мой телескоп — ZWO Seestar S50 Pro, так называемый smart telescope: камера, монтировка, фокусер и фильтры в одном корпусе, управление через приложение на телефоне. Удобно, пока тебя устраивает ровно то, что приложение умеет. Как только хочется чуть другого — своего сценария ночи, своей обработки, нормального доступа к сырым кадрам, — упираешься в закрытую коробку. Так появился проект seestar-mcp: MCP-сервер, через который Claude может навести телескоп на цель, снять кадры и затем сложить их в картинку. MCP здесь — просто стандартный способ дать модели набор инструментов с описаниями; ничего больше от протокола в этой истории не требуется.
Сразу погашу ожидание, которое напрашивается из заголовка. Это не «AI сам снимает космос, пока я сплю». Это инструмент, который превращает длинную ручную цепочку в разговор: я говорю, что хочу снять и как обработать, модель вызывает нужные инструменты, а я смотрю на результат и решаю, что дальше. И это уже не стендовая игрушка: с первого света 5 ноября телескоп отработал через эти серверы шесть ночей, и снятое за это время — от Туманности Ориона до «Конской головы» — наведено, снято и обработано через Claude. Ниже — как это устроено, чему научили реальные ночи и где человек по-прежнему нужен.
Закрытая коробка оказалась ASCOM-устройством с выключенным тумблером
Самое важное открытие случилось ещё до первой строчки кода. Текущая прошивка Seestar (9.75, вместе с приложением 3.4.1) поднимает на самом телескопе сервер ASCOM Alpaca на порту 32323. Alpaca — это открытый HTTP-стандарт для астрономического оборудования: монтировка, камера, фокусер, колесо фильтров описаны одинаково независимо от производителя. То есть умный телескоп внутри оказался вполне обычным набором стандартных устройств, просто об этом мало кто знает.
По умолчанию сервер выключен. Включается он в приложении: Advanced Feature → Assistive Features → Seestar Alpaca, после чего нужно минуту-другую подождать — сервер стартует не сразу. Есть и пара неочевидных деталей, на которых легко потерять вечер. Телескоп должен быть в домашней сети через Station Mode; его собственная точка доступа тоже работает, но компьютер на это время теряет интернет. И USB-кабель нужно вынуть: пока Seestar смонтирован как диск, по сети он не отвечает. Проверка — один curl на /management/v1/configureddevices, в ответ приходит список устройств.
Почему это так важно для архитектуры? Потому что ядро сервера получилось не про Seestar, а про Alpaca. Инструменты telescope_goto, telescope_park, camera_capture и остальные стандартные работают с любым Alpaca-устройством. Специфика Seestar — это тонкий слой поверх, а не фундамент. Этот выбор потом окупился несколько раз, особенно в тестировании.
Отдельно стоит сказать про то, чего встроенный сервер не умеет. Собственный стекинг телескопа, мозаики, планировщик ночи, скачивание альбомов — всё это живёт в родном протоколе Seestar, и для этих инструментов нужен seestar_alp, community-драйвер, который этот протокол понимает. Сервер сам определяет, с кем говорит (--profile auto): со встроенным сервером, с seestar_alp или с каким-то другим Alpaca-устройством, — и инструменты, которым нужен seestar_alp, вне его прямо говорят, чем воспользоваться вместо них.
Два сервера, которые встречаются в папке, а не в коде
В пакете два MCP-сервера. seestar-mcp управляет телескопом: находит его в сети, наводит, снимает, скачивает кадры. astro-mcp обрабатывает кадры через Siril и GraXpert: выравнивание, стекинг, выравнивание фона, шумоподавление, растяжка, экспорт. Соблазн сделать один большой сервер «от наведения до PNG» был, но я сознательно от него отказался.
Серверы ничего не знают друг о друге. Их общий контракт — рабочая директория: у каждой цели есть raw/ с тем, что пришло с телескопа, process/ со всеми промежуточными файлами, result/ только с готовыми картинками и logs/ с логом каждого задания — ровно тем, что напечатал Siril. Направь оба сервера на один --work-dir, и телескопная сторона наполняет папку, а обработка из неё читает.
Это не лень проектировать API, а осознанная развязка. Обработка часто происходит не в ту же ночь и не на той же машине; кадры могут прийти вообще с другого телескопа. Папка с FITS-файлами — контракт, который переживёт любые изменения в коде обеих сторон, и его можно потрогать руками: открыть, посмотреть, подложить свои файлы. Ночь из шестисот кадров оставляет столько же файлов в process/, но картинка, ради которой всё затевалось, лежит в result/, и искать её не приходится.
Ещё одна мелочь, которая оказалась важнее, чем выглядела: повторная обработка той же ночи — это нормальный случай, а не исключение. Другая растяжка, ещё один пресет, денойз. Интересный вопрос всегда «как эта попытка выглядит рядом с прошлой», поэтому каждый результат получает суффикс _v1, _v2 и так далее, и новый запуск никогда не перезаписывает предыдущий.
Goto промахнулся на 3,8 градуса, и это было ожидаемо
Первый реальный тест под небом выглядел обидно. Обычный telescope_goto отправляет монтировку туда, где по её внутренней модели находится цель. Сразу после включения или после того, как телескоп передвинули, эта модель может ошибаться сильно: на первом тесте промах составил 3,8°. Поле зрения основной камеры Seestar — примерно 1,3° на 2,4°, то есть объект оказался даже не у края кадра, а где-то за ним.
У встроенного Alpaca-сервера нет собственного plate solving, поэтому его пришлось сделать на стороне MCP. Инструмент telescope_center работает простым циклом: навестись на цель (координаты J2000, как в каталогах, с переводом в эпоху даты, которой живёт монтировка), снять короткий кадр основной камерой, решить его через ASTAP и, если цель дальше от центра, чем допуск (по умолчанию 2′), синхронизировать монтировку с тем, куда она на самом деле смотрит, и навестись снова. Для поля Seestar подходит звёздная база D05.
На реальном телескопе одна коррекция превратила ошибку в 3,8° в ошибку в 5′. Позже telescope_center на Плеядах (M45) закончил в пределах 0,7′ от цели. Это не магия, а классическая астрономическая техника, которой любители пользуются годами; новое здесь только то, что весь цикл теперь — один вызов, который модель делает сама, когда я говорю «наведись на M45».
Кадры сохраняются как 16-битные FITS с Bayer-паттерном, выдержкой, gain и положением монтировки в заголовке. Раскладываются по ночам, причём «ночь» — это дата вечера, когда она началась, так что кадр в 01:30 лежит рядом с кадрами из 23:00. Обе съёмочные команды возвращают ещё и растянутое превью — и это, пожалуй, одна из самых практичных деталей. Модель сама видит, что в кадре облака, стена или крыша балкона, и может сказать об этом раньше, чем я пойду проверять. И камера остаётся в том состоянии, в котором её нашли: gain, выставленный на один кадр, возвращается обратно, а камера, которую пришлось подключить, отключается, так что приложение Seestar не видит никакой разницы.
Как отлаживать телескоп, не выходя на балкон?
Самая неудобная особенность этого проекта для разработчика — железо доступно редко. Ясная ночь, свободный вечер, свободный балкон — совпадает это не так часто, как хочется. Если тестировать только на реальном телескопе, цикл обратной связи растягивается на недели. Поэтому почти всё разрабатывалось против подделок, и их в проекте три уровня.
Первый — in-process фейк в test/fake/: Alpaca-сервер с семантикой Seestar, у которого монтировка тратит время на поворот, есть поток событий прошивки, планировщик и инъецируемые ошибки. Для него не нужно ничего, кроме yarn test. Второй — официальные ASCOM Alpaca Simulators, эталонная реализация спецификации. Тот же клиент гоняется против них, и заодно это способ попробовать сервер вообще без телескопа: симуляторы покрывают монтировку, камеру, фокусер, колесо фильтров, так что все стандартные инструменты работают.
Третий уровень самый интересный. Встроенный Alpaca-сервер ZWO закрытый, но его можно запустить в Docker-контейнере, подделав только прошивку под ним (test/zwo/firmware.mjs). Получается стенд, где настоящий код ZWO разговаривает с фейковым железом. Именно на нём воспроизвелась неприятная особенность: один ответ прошивки в форме, которую сервер не ожидает, обрывает его связь с прошивкой. После этого каждый запрос, которому нужна прошивка, — положение, поворот, abort, парковка — висит бесконечно, хотя часть вещей сервер ещё отвечает из памяти.
Найти такое на реальном телескопе ночью было бы куда больнее. На стенде это превратилось в конкретное поведение: telescope_status помечает такое состояние, движения отклоняются, а в README описан путь восстановления — остановить или запарковать телескоп из приложения, выключить и включить Seestar, переподключиться. Сам телескоп при этом не в опасности: goto останавливается на цели, а ручное движение по оси запрашивается заново каждую секунду и само прекращается, когда запросы перестают приходить.
Модель не должна уметь выключить телескоп случайно
Когда даёшь языковой модели инструменты, которые физически двигают устройство, вопрос безопасности перестаёт быть абстрактным. Модель может ошибиться в аргументах, неправильно понять просьбу или просто решить, что «перезагрузка поможет». Поэтому защита здесь построена не на том, что модель будет вести себя хорошо, а на том, какие инструменты она вообще видит.
С флагом --read-only регистрируются только инструменты, которые ничего не двигают, — остальных модель просто не видит. Выключение и перезагрузка через seestar_power требуют одновременно --allow-destructive при запуске и confirm: true в самом вызове: выключенный телескоп удалённо не разбудить, кнопку придётся нажимать руками. Перед любой командой, которая двигает телескоп, сервер с коротким таймаутом читает, не поворачивается ли он уже; если ответа нет, ничего не отправляется. И только telescope_abort никогда не задерживается — остановка должна работать всегда.
Есть и сознательно оставленные острые углы. alpaca_put и сырой проход к прошивке seestar_raw не имеют ограничителей by design: это инструменты для тех случаев, которые я не предусмотрел. Последний можно спрятать флагом --no-raw. И важная деталь, которую я не могу исправить со своей стороны: в приложении нет настройки пароля для встроенного сервера, так что он принимает запросы от любого устройства в сети. Это стоит держать в голове.
Обработку пишет Siril, а модель только выбирает параметры
Вторая половина проекта, astro-mcp, устроена по тому же принципу контроля: она не переизобретает обработку, а пишет скрипты для Siril и печатает скрипт перед запуском. Вся ночь обрабатывается одним вызовом process_session с пресетом: quick просто складывает и растягивает, standard отбрасывает худшие 10% кадров, выравнивает фон и калибрует цвет, full добавляет денойз и деконволюцию через GraXpert. Каждый шаг можно переопределить, а можно пройти конвейер вручную, по одному инструменту, и остановиться посмотреть после любого.
Самая ценная часть этой половины — не код, а зашитые в умолчания наблюдения. Например, авто-растяжка Siril по умолчанию целится в фон 0,25 — это яркость его экранного превью, а на реальном стеке небо получается около 64 из 255, серое и зернистое. Здесь по умолчанию 0,08, что даёт примерно 20 из 255. Или порядок шагов: сначала вычитание фона, потом фотометрическая калибровка цвета, потом денойз, потом растяжка. Цвет первым — потому что калибровка меряет поток каталожных звёзд, а денойз этот поток меняет. Денойз до растяжки — потому что кривая растяжки строится по статистике изображения: на 46-минутном стеке M 27 это подняло туманность с 57 до 64 из 255 при неподвижном небе.
Есть и вещи, на которых я спотыкался сам. Кадр Seestar — это сырой one-shot colour с мозаикой Байера GRBG; сложенный как есть, он даёт серое изображение с впечатанной сеткой, поэтому инструменты читают заголовок и делают debayer сами. А чтобы вообще было что складывать, в приложении нужно включить сохранение каждого кадра — иначе на телескопе останется только его собственный JPEG. Длинные задания возвращают job id, и прогресс смотрится через astro_job: сессия из двухсот кадров идёт минуты, и это дольше, чем живёт обычный запрос MCP-хоста.
Каждая ночь начинается с вопроса «куда телескоп смотрит на самом деле»
Вечер начинается не с телескопа, а с погоды и списка целей. Небольшой скрипт собирает облачность по часам из двух моделей, влажность, точку росы и ветер, а рядом лежит список того, что стоит снять: что уже набрано с достаточным накоплением, что надо доснять, какие Луна и планеты видны в этот вечер. На вопрос «что снимать сегодня» Claude отвечает сопоставлением окон: в какие часы ожидаются просветы и какие цели в это время будут в открытой части неба.
Дальше — проверка направления. Seestar ставится руками, компас у него не откалиброван, и азимуту монтировки верить нельзя: в одну из ночей она ошибалась на 36°, в другую — на 20°. Поэтому первый кадр ночи снимает широкоугольная камера с полем около 59° на 33°, ASTAP решает его по базе W08, и сразу видно, куда телескоп смотрит в действительности. После sync по этому решению telescope_center доводит цель основной камерой: на «Медузе» IC443 первый промах был 21,6′, после коррекции — 0,6′, на комете C/2024 T5 (ATLAS) — 38,5′ и 1,3′.
Из тех же широкоугольных кадров постепенно сложилась карта препятствий: где небо закрывают постройки, на какой высоте в кадр начинает заходить крыша. Это обычный markdown-файл с таблицей, но именно он позволяет поставить серию так, чтобы цель не ушла за препятствие через полчаса. И он устаревает в тот момент, когда телескоп переставили на другое место: тогда карту приходится проверять заново, и часть целей, доступных вчера, сегодня просто не видна.
Сорок минут накопления не помещаются в один вызов MCP
Первое, что выяснилось в реальной работе: серия — это не вызов инструмента, а процесс. Сорок минут по 20 секунд — около восьмидесяти кадров, и ни один MCP-хост не держит запрос так долго. Поэтому серия идёт фоновым скриптом поверх того же Alpaca: он снимает кадр за кадром, каждые 5–10 кадров решает очередной кадр через ASTAP и, если цель ушла дальше допуска в 3′, перенаводит монтировку. Останавливается сам — по числу кадров, по времени или по азимуту, за которым начинается препятствие. Claude запускает его, читает лог и докладывает, что происходит.
Реальные цифры быстро поправили пару ожиданий. Кадр с выдержкой 20 секунд занимает около 31 секунды: остальное — скачивание 16-мегабайтного FITS по Wi-Fi, то есть примерно треть ночи уходит на сеть. При слабом сигнале было ещё хуже — около ста секунд на кадр, пока я не перестроил сеть так, чтобы телескоп и компьютер были ближе друг к другу. Монтировка азимутальная, ведение уходит примерно на полминуты дуги в минуту, а высоко над горизонтом сбивается быстрее: на Плеядах перенаведение понадобилось уже через семь минут.
Из того же скрипта вырос драйвер мозаики. «Калифорния» (NGC1499) длиннее поля Seestar, поэтому её снимали двумя панелями вдоль оси туманности с перекрытием около 50%, чередуя их блоками по 15 минут. За два часа — 117 и 111 кадров и одно перенаведение на всю мозаику. Склейку потом делает отдельный скрипт по WCS-решениям панелей, с подгонкой фона в зоне перекрытия.
Облако и крыша выдают себя разными признаками
Самая полезная часть фоновой съёмки — сторож кадров, и его правила написаны не по учебнику, а по конкретным ночам. 8 ноября на галактике M77 облака, подсвеченные городом, подняли медиану фона на 25% — такое ловится простым порогом. Но через несколько минут на «Пламенной звезде» IC405 облако закрыло больше половины поля, а фон с LP-фильтром вырос всего на ~9%. Вывод записали сразу: облака ловить по числу звёзд относительно проверочного кадра, а не по медиане.
Та же ночь показала обратный случай. На Плеядах цель поднялась так высоко, что в верх кадра начал заходить край крыши: яркость верхней десятой части кадра выросла с 2700 до 7500, а звёзд осталось больше 70%, и счётчик ничего не заметил. Так у сторожа появился второй признак — яркость верхнего и нижнего края кадра. Ещё через час проходящее облако подсветило край кадра, я принял его за крышу и остановил серию на NGC2403, хотя следующие кадры уже были чистыми. Теперь тревога поднимается, только если край светлый два кадра подряд.
Похожая история случилась на «Улитке» (NGC7293): в центре кадра начало расти размытое пятно, и первая гипотеза была — роса, тем более что влажность высокая. Включили подогрев, досняли — пятно росло дальше. Широкоугольная камера показала, что это не роса и не облака, а подсвеченная снизу стена, за которую уходила цель. Тридцать семь кадров отправились в брак, а в карте препятствий появилась новая строка. Ещё одно правило родилось на NGC2403: кадр 20 секунд сразу после goto смазан, вытянутость звёзд 2,3 против 1,05 у кадра, снятого через полминуты. Монтировке нужно успокоиться, и серия теперь это учитывает.
Бесплатные инструменты против RC-Astro: спор решают замеры, а не вкус
В реальной работе обработка ушла дальше пресетов astro-mcp. Стек по-прежнему собирает Siril, но доводку я хотел сделать бесплатной и при этом не хуже платного эталона — связки RC-Astro (BlurXTerminator, StarXTerminator, NoiseXTerminator). Бесплатная цепочка — GraXpert для фона, деконволюции и части шумодава, StarNet2 для отделения звёзд, DeepSNR для шума на небе, Siril для растяжки — живёт в рабочей папке для съёмки набором скриптов, которые Claude запускает и правит. Каждый шаг меряется: число звёзд, FWHM, шум и яркость по зонам, а не «на глаз стало лучше».
На первой галактике, NGC253, подогнанная бесплатная версия нашла 761–769 звёзд против 751 у RC-Astro, яркость ядра 0,639–0,642 против 0,648. На маленьких планетарных туманностях разница с RC около 1%. А на большой «Розетке» (NGC2237) бесплатная цепочка теряет около четверти мелких деталей в волокнах, и замер показал, что дело в деконволюции — GraXpert против BlurX, — а не в шумодаве, куда я смотрел первым. Поэтому каждая цель обрабатывается двумя способами, и победитель выбирается по цели: на «Окаменелом следе» NGC1491 лучше вышла бесплатная версия, на NGC2403 бесплатная выиграла по протяжённости галактики, а RC — по фону.
Самая поучительная находка — ошибка, которую нельзя увидеть глазом. Проверяя ориентацию, я прогнал готовые картинки через тот же plate solving, что наводит телескоп: знак детерминанта матрицы CD показывает, зеркальное изображение или нет. Оказалось, что savejpg и savetif в Siril отдают картинку зеркальной по вертикали. Галактика в зеркале выглядит так же убедительно, поэтому самый первый пост — NGC253 — ушёл зеркальным. Теперь перед экспортом стоит mirrorx, а остальные результаты перевёрнуты на месте. Из той же категории — GraXpert, который, получив значения чуть выше 1, растягивает весь слой и гасит туманность: на M42 средняя яркость картинки упала с 12,2 до 3,8. Правило теперь короткое: всё, что идёт в GraXpert, держать в диапазоне 0..1.
Ещё одно решение продиктовал город. Засветка сильная, и эмиссионные туманности я снимаю через двухполосный LP-фильтр Hα+OIII: на «Улитке» медиана фона упала с 3904 до 2336, а градиент засветки ушёл. Но такой кадр нельзя обрабатывать как обычный цветной: стандартное удаление зелёного съедает OIII, который живёт как раз в зелёном канале. Отдельный конвейер собирает палитру HOO — Hα в красный, OIII в бирюзовый, чёрный фон — и растягивает оба канала одинаково, давая красный только там, где Hα сильнее OIII.
Что по-прежнему решаю я, а не модель
Claude в эти ночи — оператор: проверяет направление, наводит, запускает серии, читает тревоги сторожа, ведёт журнал наблюдений прямо по ходу ночи и в конце составляет список того, что надо доснять. Но решения о начале и конце съёмки, о смене цели и о публикации остаются за мной, и это не формальность. 11 ноября при переходе с кометы на «Конскую голову» telescope_center дважды решил кадр, снятый ещё на старой позиции или в пути, и дважды синхронизировал по нему монтировку. Третья попытка попала в цель, но модели монтировки после двух ложных sync доверия уже не было, и я прервал. Повторное центрирование с неподвижного телескопа дало 2,3′ → 0,3′, и серия IC434 с «Пламенем» прошла 28 минут без единого перенаведения. Вывод для кода очевиден: решать нужно только кадр, снятый после полной остановки монтировки.
Есть и границы, которые от меня не зависят. Обогрев от росы через встроенный сервер не включить: на прошивке 9.75 switch там называет себя «Alpaca Switch Simulator», так что это делается из приложения. Путь через seestar_alp — стекинг на телескопе, мозаики и планировщик прошивки — против реального железа по-прежнему не проверен: вся настоящая работа идёт через встроенный сервер, а мозаику пришлось сделать на своей стороне. Азимутальная монтировка поворачивает поле, и без дарков и дизеринга фиксированный шум матрицы ложится в стеке концентрическими дугами; косметическая коррекция в Siril снижает шум стека на 22%, но сами дуги не убирает.
И погода. 8 ноября облака закончили серию M77 на 7,7 минуты из сорока запланированных, и никакой инструмент тут не поможет — только честная запись «надо доснять» в списке целей и возвращение к ней в следующий ясный вечер.
Итог
Если смотреть на этот проект инженерным взглядом, то он скорее не про AI, а про контракты. Стандартный протокол Alpaca вместо закрытого приложения. Папка с файлами вместо связки двух серверов. Видимый Siril-скрипт вместо чёрного ящика обработки. Набор инструментов, который ограничен флагами запуска, а не добрыми намерениями модели. Модель в этой схеме не заменяет астрофотографа, а берёт на себя рутинную часть ночи — наведение, серии, контроль кадров, журнал, — на которой раньше легко было бросить.
За первую неделю это дало шесть ночей съёмки и больше пятнадцати целей: галактики, планетарные и эмиссионные туманности, мозаику «Калифорнии», комету. Лучшее из этого — уже не логи и не FITS, а сами астрофото — я выкладываю в инстаграм-блог @meowstronomy: к этому дню там галактика Скульптор (NGC253), «Улитка», Туманность Ориона, «Череп» (NGC246) и «Розетка», все в бесплатной обработке. В подписи — объект, накопление, оборудование и программы, а факты о расстояниях и возрасте объектов сверяются по источникам перед публикацией, а не пишутся по памяти модели.
Попробовать сервер можно и без телескопа: через ASCOM-симуляторы все стандартные инструменты работают. Код, документация по обработке и чеклист запуска на реальном железе лежат в репозитории veksa/seestar-mcp, пакет в npm называется seestar-mcp, лицензия MIT.
Если сформулировать главный практический вывод совсем прямо: чтобы доверить модели настоящее железо, нужно не научить её быть осторожной, а построить вокруг неё стандартные протоколы, явные файловые контракты, набор инструментов, в котором опасное не видно без явного разрешения, и проверки, которые каждую ночь сверяют её действия с небом. Всё остальное — дело ясных ночей и терпения.