Я питаю двоякие чувства к метеостанциям. Терпеть не могу многочисленные 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.

Структурная схема Метеорологической станции КРАМС-2
Структурная схема Метеорологической станции КРАМС-2

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

  • установленные в помещении;

  • на метеоплощадке;

  • в непосредственной близости от Взлетно-Посадочной Полосы(ВПП).

Сигналы с этих датчиков обрабатываются с помощью блока управления и преобразования(БУП), и передаются в блок автономной связи. К последнему можно подключить несколько БУП, что важно если на аэродроме несколько ВПП. В итоге информация о погоде приходит в вычислительное устройство, непосредственно формирующее метеосводки в одном из форматов — 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)


  1. Arcanum7
    25.11.2025 09:42

    Первый коммент!
    Спасибо за здоровую критику! Приятно что упомянул.


  1. Arcanum7
    25.11.2025 09:42

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

    Или поставить VFD и выводить человеческим текстом.


    1. radiolok Автор
      25.11.2025 09:42

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


      1. Arcanum7
        25.11.2025 09:42

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