Я питаю двоякие чувства к метеостанциям. Терпеть не могу многочисленные Arduino-проекты с мелким экраном, зато люблю что-то эдакое. Например в коридоре уже пять лет как висит метеоиндикатор на базе блинкерного табло для автобуса, что случайно попалось на просторах авито. Так вышло и на этот раз, и в моем распоряжении оказался аэродромный индикатор погоды Комплексной Радиотехнической Аэродромной Метеорологической Станции
❯ КРАМС
В авиации от точности сводок погоды зависят жизни тысяч людей. Банально самолетный высотомер выставляется по атмосферному давлению непосредственно в зоне аэропорта, дабы самолет в плохую погоду не промахнулся мимо ВПП (чему помешают прочие системы навроде КГС ).
В итоге на каждом аэродроме имеется своя собственная метеорологическая станция. Существует множество серийных решений, например отечественная КРАМС-4 — является одной современных систем метеообеспечения Российской авиации, сочетающая модульность, соответствие стандартам ИКАО, и что немаловажно — интеграцию с диспетчерскими сервисами.
Такие системы предназначены для автоматического сбора метеоданных, критически важных для безопасности взлёта и посадки воздушных судов:
Температура и влажность воздуха, скорость и направление ветра, атмосферное давление
Метеорологическая дальность видимости, высота нижней границы облаков
Вид и количество осадков и т.п.
На базе этой, первичной информации автоматически формируются метеосводки — генерируются сообщения METAR, TAF, ATIS в стандартных кодах ИКАО, которые и используются в дальнейшем диспетчерами аэропортов и пилотами самолетов.
❯ METAR
У каждого уважающего себя аэропорта есть четырёхбуквенный код формата ИКАО. Например UWGG — код аэропорта Стригино в г. Нижний Новгород. В Linux консоли делаем следующее:
$ sudo apt install metar
$ metar UWGG -d
UWGG 121800Z 10002MPS 070V140 9999 OVC008 01/M01 Q1022 R18L/090070 NOSIG
Station : UWGG
Day : 12
Time : 18:00 UTC
Wind direction: 100 (E)
Wind speed : 2 MPS
Wind gust : 2 MPS
Visibility : 9999 M
Temperature : 1 C
Dewpoint : -1 C
Pressure : 1022 hPa
Clouds : OVC at 800 ft
Phenomena :
И получаем метеосводку с аэропорта, закодированную в текстовое сообщение сложной структуры. Самостоятельно разбирать его нет смысла, хотя вы можете попытаться по Этой и Этой инструкциям. В сообщении имеется следующая информация:
Информация актуальна на 18:00 UTC 12 дня месяца (сейчас ноябрь);
Ветер — восточный, 2м/с;
Метеорологическая дальность видимости — 10км;
Облачно, облака на высоте 800 футов над землей (240 метров), без осадков;
Давление QNH 1022 гПа (766мм рт.ст.);
Температура 1 градус, точка росы -1 градус — тут можно косвенно вычислить влажность, явно в сообщении она не передается;
Существенных изменений погоды не ожидается (поле NOSIG).
То же самое можно услышать в окрестностях Нижегородского аэропорта на частоте 132,700МГц голосовой системы АТИС.
❯ КРАМС-2
В 80-е годы прошлого века широкое распространение в СССР имела система КРАМС-2. Она полностью автоматизирована и предоставляет метеосводки как на индикаторных табло, так и с помощью телеграмм, в том числе в формате METAR.

Имеющиеся в системе датчики можно разбить на три группы:
установленные в помещении;
на метеоплощадке;
в непосредственной близости от Взлетно-Посадочной Полосы(ВПП).
Сигналы с этих датчиков обрабатываются с помощью блока управления и преобразования(БУП), и передаются в блок автономной связи. К последнему можно подключить несколько БУП, что важно если на аэродроме несколько ВПП. В итоге информация о погоде приходит в вычислительное устройство, непосредственно формирующее метеосводки в одном из форматов — METAR, БИ, ШТОРМ; печатая его на рулонном телеграфе, передавая по сети в другие аэродромы, или же выводящая их на индикаторные табло, одно из которых и оказалось в моем распоряжении. Отдельно отмечу что в системе подразумевается и блок ручного ввода, на случай если автоматика дала сбой.
❯ Автономное Индикаторное Устройство

Прибор довольно большой и тяжелый. Общие габариты — 515х420х150мм. Масса — 15кг. Рама тут — стальная, 1.2мм толщиной! Спереди, за листом из оранжевого оргстекла — массив из 56 семи-сегментных вакуумных люминесцентных индикаторов ИВ-22 с размером цифры в 18 мм.

Под задней крышкой скрываются четыре платы. Две одинаковых платы справа — ОЗУ для хранения отображаемой информации, две платы слева — плата преобразователя кода и плата управления. Метеоиндикатор собран на микросхемах 133 серии, что обуславливает малые габариты плат логики управления и контроля. Впрочем, схемотехника тут незатейливая.

Информация поступает последовательным кодом по линии связи в виде посылки из 47 символов. Каждый символ кодируется 5 битовым кодом, и обрамляется стартовым и стоповым импульсами. Предвосхищает передачу сигнал Подготовка, сбрасывающий индикатор в исходное состояние. Амплитуда сигналов ±50В.

В неактивном состоянии в линию подается -40В — так индикатор понимает, что линия связи жива. Если нет — начинает истошно вопить своим динамиком.
Индикатор поддерживает только символы 0-9 и знак «-». Последний используется для гашения разряда. Истинный минус в поле Температура выводится символом «1». В приведенной таблице есть также коды и для рулонного телеграфа, но индикатором они будут проигнорированы. Что, в принципе, удобно — можно отправить одну и ту же посылку и на индикатор и на телеграф.

Телеграфный код с входного устройства после согласования уровней сигнала поступает на преобразователь кода.
Во-первых, последовательный 5-битовый сигнал принятой цифры преобразуется в параллельный. Для этого счетчик на трех Т-триггерах производит счет, а с помощью сигнала Код0 на Д-триггерах десериализатора набирается исходный символ. И тут мы встречаем первую защиту от дурака — ложный старт. Он сработает если стартовый импульс будет отрицательный.
Далее, параллельный телеграфный код преобразуется в двоично-десятичный, согласно таблице кодирования. Тут появляется вторая защита — неподдерживаемые символы игнорируются и будут пропущены. Впоследствии корректной будет считаться посылка длиной ровно в 47 распознанных символов.
В-третьих, двоично-десятичный символ преобразуется в семисегментный код, который и будет записан в ОЗУ, а после завершения передачи — выведен на индикатор. Здесь же подмешивается контроль четности, дабы впоследствии убедиться в исправности ОЗУ. Эта проверка стрельнет позднее, когда мы попытаемся считать данные из битой памяти на дисплей.

В итоге, не смотря на малое количество микросхем, индикатор весьма устойчив к помехам в линии связи. Добавим сюда Рабочее напряжение линии в +-50В и осознаем уровень советского инженерного гения. Но тут всплывает одно НО — от всей КРАМС у меня есть только сам индикатор — необходимо организовать имитатор линии. Генерировать сообщение я решил с помощью ESP32, но задумался — а что если вместо создания внешнего блока имитирующего УЦВС с уровнями сигнала ±50В мы поместим контроллер непосредственно в корпус, вместо родной интерфейсной платы? С 3.3В на 5В сигналы преобразовать значительно проще чем городить биполярный драйвер линии. Аутентичность конечно нарушается, зато устройство будет действительно автономным. Смотрим, что там по сигналам:

Входное устройство представляет собой отдельную плату, ко входу которой подключается телеграфная линия, а на ее выходе формируется 4 сигнала управления — Подготовка, Счет, Код0 и Контроль линии. Схема также собрана на микросхемах 133 серии. Причем на плате есть еще немного места, для гальванической развязки на оптопарах — помимо индикатора эта плата в неизменном виде используется в Блоке Ручного Ввода. Там требуется отвязывать цени контрольного индикатора.

Все четыре сигнала будем выдавать напрямую. Принципиальная схема подменной платы содержит модуль ESP32-CAM, с разъемом для подключения внешней антенны (корпус то металлический), а также 4 преобразователя уровней нереальной сложности — транзисторные инверторы превращают 3.3 в 5В, а К155ЛА3 — возвращает полярность сигнала до исходной. «Я ее слепила из того что было». Но если говорить откровенно, то микросхемы преобразователи уровней у меня в загашнике имеются, но они способны переваривать сигнал в десятки МГц и расходовать их здесь — натуральное кощунство. Да и плата смотрится чуть менее пустой.

Осталось дело за малым — запрограммировать плату так, чтобы она сама ходила в интернет за метеосводками, позволяла выбрать аэропорт для отображения и.. предоставляла возможность обновить прошивку по воздуху. У тов. @arcanum7 как раз есть такой многострадальный проект avr_fota, который я и решил опробовать для своей задачи.
❯ Получаем сводки погоды
Первое что нам нужно сделать — это запросить METAR-сообщение для нашего аэропорта. Я знаю как минимум два адреса для этого:
String get_metar(String icao_code){
WiFiClientSecure client;
HTTPClient http;
String payload = "";
String url = "https://tgftp.nws.noaa.gov/data/observations/metar/stations/" + icao_code + ".TXT";
//String url = "https://aviationweather.gov/api/data/metar?ids=" + icao_code + "&format=raw";
if (http.begin(client, url)) {
int httpCode = http.GET();
if (httpCode == HTTP_CODE_OK) {
payload = http.getString();
} else {
Serial.printf("[HTTP] GET failed, error: %s\n", http.errorToString(httpCode).c_str());
}
http.end();
}
return payload;
}
Полученное сообщение отправляем в библиотеку-парсер. Сходу я нашел две C++ библиотеки — бескрайне-мощную metaf и небольшую metar
Первая красива, всё на шаблонах, один заголовочный файл, вот только после компиляции она весит почти 1Мбайт и не лезет в ESP32. А вторая менее совершенна, зато со свистом уместилась.
Не смотря на то, что поля для большинства значений — 4-х разрядные, почти у всех работают только старшие три разряда, младший всегда показывает 0. Опишем структуру данных исходя из документации:
typedef struct {//Для двухсимволных значений, например времени
uint8_t low;
uint8_t high;
} word_t;
typedef struct {//Для трехсимвольных
word_t low;
uint8_t high;
} tword_t;
typedef struct {//и четырехсимвольных, например давление в ГПа
word_t low;
word_t high;
} dword_t;
typedef struct {
word_t hours; //1 - 2 - Часы
word_t minutes; //3 - 4 - Минуты
word_t wind_dir; //5 - 6 - Направление ветра. Десятки градусов
word_t wind_speed; //7 - 8 - Скорость ветра в м/с
dword_t pressure_mbar; //9 - 12 - Давление в мбар (гПа)
uint8_t clouds; //13 - Количество облаков
tword_t humidity; //14-16 - Влажность
tword_t temperature; //17-19 - Температура
uint8_t clouds_low_level;//20 - Количество облаков нижнего яруса
word_t wind_max; //21 - 22 - Максимальная скорость ветра
uint8_t bi_thunder; //23 - Признак: Гроза!
uint8_t forecast; //24 - Явления погоды
uint8_t telegram_name; //25 - Название телеграммы
uint8_t number_bd; //26 - Номер сообщения
tword_t clouds_heigth; //27 - 29 - Высота облаков
tword_t pressure_torr; //30 - 32 - Давление в мм.рт.ст.
tword_t distanse_l1; //33 - 35 - Дальность видимости на ВПП1
tword_t distanse_l2; //36 - 38 - Дальность видимости на ВПП2
tword_t distanse_l3; //39 - 41 - Дальность видимости на ВПП3
tword_t distanse_meteo; //42 - 44 - Метеорологическая дальность видимости
word_t rwy_max_speed; //45 - 46 - Скорость ветра у ВПП
uint8_t bi_ice; //47 - Гололёд
} message_t;
Структура message_t содержит 47 байт информации, кладем туда то что можем достать из исходного сообщения с помощью нашей библиотеки:
String metar = get_metar("UWGG");
auto metar_ptr = Metar::Create(metar_str.c_str());
message_t _metar_data;
_metar_data.hours = _metar_ptr->Hour().value_or(0);
_metar_data.minutes = _metar_ptr->Minute().value_or(0);
_metar_data.wind_dir = _metar_ptr->WindDirection().value_or(0);
_metar_data.wind_speed = _metar_ptr->WindSpeed().value_or(0);
_metar_data.distanse_meteo = _metar_ptr->Visibility().value_or(-1)/10;
_metar_data.clouds_heigth = _metar_ptr->VerticalVisibility().value_or(-1)/10;
Последний заслон перед запуском — разобраться с последовательностью сигналов:
Подготовка — по умолчанию стоит в 1, перед подачей пачки данных на короткое время щелкает в ноль и обратно. Длительность — неизвестна, в исходной плате этот сигнал находится за RC-цепочкой с постоянной времени в 1с. Внутри логики он нужен для того чтобы сбросить все внутренние защелки в исходное состояние.
Контроль — должен сидеть в нуле все время пока не передаются данные. На схеме находится за RC-цепочкой с постоянной времени 0.2с. Это позволяет ему встать в единицу в начале передачи и не реагировать на передачу нулевого кода. Будем держать его активным все время передачи и с учетом постоянной времени включим раньше кода Подготовка
Счет — согласно иконке на разъеме этот сигнал стробируется в лог.1 при передаче данных
Код 0 — согласно иконке на разъеме этот в обычном состоянии держится в 1, а при передаче нулевого бита — щелкает в нуль. Причем судя по иголкам на логическом анализаторе подавать его надо после сигнала Счёт.
Для передачи битов пишем следующий тупой код:
inline void send_one(){
digitalWrite(CLOCK_PIN, HIGH);
delayMicroseconds(DELAY_US*2);
digitalWrite(CLOCK_PIN, LOW);
delayMicroseconds(DELAY_US*1);
}
inline void send_zero(){
digitalWrite(CLOCK_PIN, HIGH);
digitalWrite(CODE_0_PIN, LOW);
delayMicroseconds(DELAY_US*2);
digitalWrite(CLOCK_PIN, LOW);
digitalWrite(CODE_0_PIN, HIGH);
delayMicroseconds(DELAY_US*1);
}
Где DELAY_US изначально выставлен в 1000, что будет соответствовать задержкам проекта тов. Егора Коледа aka radioegor146 Он как раз подключился к родному интерфейсу. Так что с его задержками все точно должно работать, но передача будет занимать несколько секунд. После того как все проблемы были исправлены — я уменьшил эту константу до 20мкс, что дает общее время передачи в 25мс. В видео можно заметить легкое моргание.
Передаем один символ данных:
void send_code(uint8_t code) {
delayMicroseconds(DELAY_US*2);
send_one(); //start bit
for (int i = 4; i >= 0; i--) {//MSB
if (code & (1 << i)) {
send_one();
} else {
send_zero();
}
}
send_zero();//stop bit
}
И, наконец, отправляем сигнал подготовки со всей посылкой из 47 символов:
void send_message(const message_t& message) {
digitalWrite(CODE_0_PIN, LOW);
delayMicroseconds(DELAY_US*2);
digitalWrite(LINE_PIN, HIGH);
delayMicroseconds(DELAY_US*20);
digitalWrite(PREP_PIN, LOW);
digitalWrite(CODE_0_PIN, HIGH);
delayMicroseconds(DELAY_US*20);
digitalWrite(PREP_PIN, HIGH);
delayMicroseconds(DELAY_US*2);
uint8_t* data = (uint8_t*)&message;
for (uint8_t i = 0; i < MESSAGE_LENGTH; ++i){
send_code(*(data + i));
}
digitalWrite(LINE_PIN, LOW);
}
Будучи на 100% уверенными что этот код рабочий — отправляем его в индикатор и.... Он не работает. Причем я замечаю, что индикаторы на мгновение пытаются что-то вывести, но тут же гаснут. А если нажать на кнопку «Устранение сбоя» — какие-то данные даже начинают отображаться. Что-то тут не так. Подключаемся логическим анализатором и смотрим что к чему:

Хм, защелка Сбой после завершения передачи стоит в единице. Это значит что критических проблем у нас нет, но итоговый сигнал сбой на плате появляется спустя 3 мс после завершения передачи и блокирует работу системы. Этот сигнал является собирательным относительно множества других, в том числе — от ОЗУ. Ой, а что это у нас? Платы ОЗУ совершенно одинаковы, вот только на одна из микросхем контроля четности на нижней плате не греется:

Так как у меня в запасниках нет К133ИП2, да и найти ее по адекватной цене весьма проблематично — откидываем ножку проверки четности ОЗУ и дело в шляпе — индикатор заработал.

❯ Последние штрихи
После небольшой отладки кода прошивки индикатор стал показывать более-менее адекватные вещи. Осталось последнее — в веб-интерфейсе добавить возможность выбирать аэропорт для отображения. Благо в проекте avr-fota такая возможность есть, хоть и написан он в классическом стиле ущербно-ориентированного программирования.(Что мы с товарищами пытаемся исправить). Создаем обработчики, которые будут загружать и сохранять выбранный аэропорт:
String icao = "UWGG";
bool save_config_metar(){
JsonDocument jsonDoc;
jsonDoc["icao"] = icao;
return ESPHTTPServer.save_jsonDoc(jsonDoc, "/config_metar.json");
}
bool load_config_metar() {
JsonDocument jsonDoc;
if (!ESPHTTPServer.load_jsonDoc("/config_metar.json", jsonDoc)){
return false;
}
icao = jsonDoc["icao"].as<const char *>();
return true;
}
void default_config_metar() {
icao = "UWGG";
save_config_metar();
}
avr-fota использует библиотеки ArduinoJson и ESPAsyncWebServer. Для последней нужны обработчики событий. Один обработчик будет отдавать страницу, а также обновлять код аэропорта если прилетел GET-запрос. Второй — отвечая на ajax, высылать в plain text строку для конфигурирования выпадающего списка:
void handle_metar_html(AsyncWebServerRequest *request){
if (request->args() > 0) { // Save Settings
for (uint8_t i = 0; i < request->args(); i++) {
DEBUGLOG("Arg %d: %s %s\r\n", i, request->argName(i).c_str() ,request->arg(i).c_str() );
if (request->argName(i) == "icao") {
icao = ESPHTTPServer.urldecode(request->arg(i));
save_config_metar();
continue;
}
}
}
ESPHTTPServer.handleFileRead(request->url(), request);
refresh_display();
}
void handle_metar_ajax(AsyncWebServerRequest *request){
String values = "";
values += "icao|" + icao + "|select\n";
request->send(200, "text/plain", values);
}
Подключаем обработчики к серверу
ESPHTTPServer.on("/metar.html", HTTP_GET, [](AsyncWebServerRequest *request) {
handle_metar_html(request);
});
ESPHTTPServer.on("/metar/info", HTTP_GET, [](AsyncWebServerRequest *request) {
handle_metar_ajax(request);
});
И смотрим что получилось:

Теперь если в выпадающем списке выбрать аэропорт и сохранить — данные с него тут же отобразятся на экране, а также сохранятся в ФС. Чего действительно не хватает у метеоиндикатора — так это, собственно, названия аэропорта... Но все же он использовался непосредственно на аэродроме и такой функционал не требовался. В принципе можно заменить, например, индикаторы дальности видимости на ВПП (например L3) и поставить туда ИВ-17, выводя ИКАО код на них. Еще неплохо бы заменить блок питания — родной слишком громко звенит своим ШИМ-ом, но и так сойдёт.
❯ Полезные ссылки
Новости, обзоры продуктов и конкурсы от команды Timeweb.Cloud - в нашем Telegram-канале ↩
Комментарии (4)

Arcanum7
25.11.2025 09:42В принципе можно заменить, например, индикаторы дальности видимости на ВПП (например L3) и поставить туда ИВ-17, выводя ИКАО код на них.
Или поставить VFD и выводить человеческим текстом.

radiolok Автор
25.11.2025 09:42ИВ-17 тут с одной стороны позволят вывести код, а с другой - не будут выбиваться из общей канвы повествования. Тем более что если код не нужен, а дальность ВПП L3 нужна - то никто и не заметит подлога.

Arcanum7
25.11.2025 09:42С одной стороны да - общий стиль сохраняется, но кол-во трудозатрат (плата новая как минимум + МК) не сопоставимо больше чем просто VFD воткнуть. Тем более можно их (VFD ) сделать больше и говорить "Так и было ясной погодой клянусь!".
Arcanum7
Первый коммент!
Спасибо за здоровую критику! Приятно что упомянул.