Пару месяцев назад я в очередной раз прогуливался по комиссионкам, и моё внимание привлёк стоящий на полке агрегат, чем-то похожий на музыкальный центр Bose.
Однако, взяв его с полки, я обнаружил, что это табло-бегущая строка!
Поэтому я быстренько подыскал на соседнем стеллаже подходящий кабель питания, проверил, что экран загорается, и потопал на кассу, пока кто-то это чудо не перехватил.
❯ Первое включение
Приносим домой, втыкаем в розетку — убеждаемся, что делает оно всё то же, что и на тестовом стенде в комиссионке. Ну, хотя бы по пути не разбили, значит %)
Включается, говорит, что версия 1.12, а затем в цикле жалуется, что никаких сообщений нет. Значит, надо их как-то туда запихать.
❯ Что это за зверь
Шильдик на задней панели гласит, что девайс произведён Nagano Japan Radio Co. Ltd., и имеет модель NJE-105. Отдельная наклейка поверх утверждает, что версия прошивки — та самая, 1.12.
Сзади есть только выключатель и вход питания, а также некий отсек, закрытый пластиковой крышкой.
Если заглянуть внутрь отсека — там мы находим как раз интерфейсный разъём. С одного конца аж целый DB-25:
А вот с противоположного — нечто, с опознанием чего я затрудняюсь.
Долгие попытки разобраться, что же это за девайс и для чего он предназначался, привели к хитросплетению уже недоступных японских сайтов. Однако, на одном из них он был показан таки "в дикой природе", и оказалось, что этот дисплей... для пейджера!
Продавал такой набор оператор NTT Docomo. То есть, если вам нужно было, например, постоянно в офисе видеть свежие новости — вы покупали пейджер с таким табло и подключали на него подписку на нужные вам каналы. Как только на пейджер придёт сообщение — оно сразу начнёт отображаться на табло.
Или если у вас, к примеру, сеть автобусных остановок — просто устанавливаете такие табло, а пейджеры привязываете к одинаковым номерам, группируя по маршрутам или районам. Затем, в случае какого-то форс-мажора на маршруте, просто отправляете на этот номер сообщение — и все табло сразу начинают отображать его.
В какой-то момент производитель начал продавать табло сам, просто как бегущую строку для компьютера — но софт, судя по всему, нигде не сохранился.
Но, к счастью, обладатель пейджера записал хотя бы часть протокола, вот в такой суровой обстановке :-)
❯ Нужно больше ламповой теплоты!
Снимем переднюю панель и посмотрим, что там внутри.
Чутьё не подвело — это действительно огромный вакуумно-люминисцентный индикатор. Под ним находится лишь блок питания и вентилятор, включённый через термопереключатель на 45 градусов.
Сам дисплей производства фирмы Futaba — они, к сожалению, свернули производство ВЛИ в конце 2021 года.
Второй бит у DIP-переключателя непонятно зачем, а вот первый включает какой-то режим тестирования — скорее всего, для проверки на заводе:
Трёхцветность экрана достигается треугольными субпикселями двух цветов — оранжевого и зелёного:
Оттенки этого дисплея напоминают мне те, что устанавливали в поездах и на станциях, чем он мне и приглянулся.
Оп, моя остановочка!
❯ Протокол
Дальнейший поиск привёл, разве что, к упоминанию о том, что какая-то программа когда-то существовала, но автор удалил её с сайта. К счастью, в интернет-архиве сохранилась другая страница, автор которой смог записать многие управляющие последовательности с оригинального пейджера. Ради сохранения этой информации, ниже привожу вольный перевод описания протокола:
Схема подключения
Судя по всему, распиновка идентична обычному RS-232 25-pin, разве что логические уровни здесь TTL. Изначальный автор применил два инвертера 7414 в качестве буфера, но я бы не рисковал и поставил MAX232. Хотя зачем оно вообще в наше время, когда переходники с TTL UART на USB продаются за сущие копейки :-) — прим. авт.
Формат пакета данных
Передача идёт на скорости 9600 бод, 8N1.
Каждый пакет начинается с rn
. Дальше идёт текущая дата и время в формате MMDDHHmm (ASCII), например, для 9 марта 16:39 это будет 03091639
(hex: 30 33 30 39 31 36 33 39
). Зачем это используется, кроме синхронизации внутренних часов табло — непонятно.
После этого — до 128 байт текста в кодировке Shift-JIS. В конце — ещё раз rn
.
Атрибуты текста
Атрибуты текста задаются в виде двух букв, указывающих цвет и эффект. Если атрибуты вставляются посреди текста, то отбиваются тильдами, например: ~AW~
.
Значения атрибутов
Буква |
Цвет |
A |
Зелёный |
B |
Оранжевый |
C |
Жёлтый |
Буква |
Анимация |
Эффект |
A |
Обычная прокрутка |
Нету |
B |
Мигание |
|
C |
Инвертированный |
|
D |
Мигающий инвертированный |
|
E |
Заполнение |
Нету |
F |
Мигание |
|
G |
Инвертированный |
|
H |
Мигающий инвертированный |
|
I |
Пауза |
Нету |
J |
Мигание |
|
K |
Инвертированный |
|
L |
Мигающий инвертированный |
|
W |
Статичное отображение |
Нету |
X |
Мигание |
|
Y |
Инвертированный |
|
Z |
Мигающий инвертированный |
Команды
Большинство команд, будучи распознанными табло, сопровождаются выводом сообщения на экран.
Полный список команд
Команда |
Параметры (если есть) |
Описание |
NJEC1 |
текстовая строка |
Подпись к произвольным (не принадлежащим никакой категории) сообщениям. Кроме как между произвольными сообщениями не отображается. |
NJEC2 |
|
Удалить подпись к произвольным сообщениям. |
NJEM2 |
|
Удалить все произвольные сообщения. |
NJER |
|
Перезагрузить табло. |
NJET |
|
Отобразить текущее время в виде сообщения 「現在時刻はxx時xx分です。」 ("Текущее время хх часов хх минут") |
NJEV1 |
|
Отобразить контакты для связи. Если не установлены, выводит сообщение об ошибке. |
NJEV2 |
|
Отобразить подпись для новостных сообщений. По умолчанию — 「時事通信ニュース速報」 ("Сводки новостей на текущий момент") |
NJEV3 |
|
Отобразить подпись к произвольным сообщениям. Если не установлена, выводит сообщение об ошибке. |
Установка настроек (NJESxx, где xx — номер параметра) Просмотреть текущее значение параметра можно, отправив тот же номер параметра в команде NJEDxx без аргументов. |
||
NJES02 |
00 / 01 / 02 |
Обычный режим / Энергосбережение / Автопереключение |
NJES03 |
от 00 до 23 |
Час начала обычного режима работы |
NJES04 |
от 00 до 23 |
Час окончания обычного режима работы |
NJES05 |
00 / 01 |
Вкл/выкл отображение произвольных сообщений |
NJES06 |
00 / 01 / 02 |
Скорость прокрутки |
NJES07 |
00 / 01 / 02 / 03 |
Скорость мигания (00 — не мигать вообще) |
NJES08 |
00 ~ 10 |
Длительность пауз, сек. |
NJES18 |
00 ~ 10 |
Время жизни сообщений, ч. |
NJES20 |
00 ~ 10 |
Время жизни экстренных новостей, ч. |
NJES21 |
00 ~ 10 |
Время жизни обычных новостей, ч. |
NJES26 |
00 / 01 |
Показ сообщений по мере прихода вне очереди |
NJES34 |
00 ~ 20 |
Количество сообщений в каждом цикле отображения |
NJES35 |
00 ~ 30 |
Показ подписи к новостям каждые N циклов |
NJES37 |
00 ~ 30 |
Количество обычных новостей в каждом цикле |
NJES38 |
00 ~ 30 |
Количество рекламы в каждом цикле |
NJES57~74, NJES76~79 |
?? |
?? Установка категории новостей |
NJES82 |
00 ~ 10 |
Время жизни произвольных сообщений, ч. |
NJES86 |
00 / 01 |
Показ произвольных сообщений вне очереди |
NJES89 |
00~20 |
Показ подписи к произвольным сообщениям каждые N циклов |
NJES90 |
00~20 |
Количество произвольных сообщений за цикл |
Команды получения сообщений |
||
]011A110 |
nnMMDDhhmmaa{текст} |
Запись сообщения nn — номер от 01 по 99, MMDDhhmm — дата и время, aa — атрибуты отображения; текст также может содержать атрибуты |
]011F120 |
nnMMDDhhmmaa{текст} |
Запись рекламы |
]011B111 |
nnMMDDhhmmaa{текст} |
Запись экстренного сообщения |
]011A110 |
00{текст} |
Установка заголовка сообщений |
]011A210 |
nn |
Удаление сообщения № nn |
]011F220 |
nn |
Удаление рекламы |
]011B211 |
nn |
Удаление экстренного сообщения |
]011A210 |
00 |
Удаление заголовка сообщений |
❯ Вывод сообщений
Изначально я купил этот дисплей в расчёте на то, чтобы заменить им кассовый дисплей покупателя, который использую сейчас в диджейских стримах: (сверху на музыкальном центре на фоне, основной движ примерно с 9:48)
Всё же кассовый дисплей для такого слишком маловат.
Однако, как оказалось из описания протокола, этот дисплей работает не как тупой терминал, а буферизует сообщения и показывает их поочерёдно. Поэтому такие анимации создавать уже не получится, а посему идея была заброшена, и решено было сделать очередные часы-метеостанцию.
Так как приковывать табло к столу с компом не хотелось, то в ход пошла очередная ESP32. К сожалению, выход 5 вольт на DB25 не имеет запаса по току, поэтому пришлось вывести наружу 24 вольта с блока питания и преобразовать их самому.
Также оказалось, что на ESP32 не работает функция iconv()
— но, к счастью, для Shift-JIS есть отдельная библиотека. На базе этого получилось написать простейшую функцию для отправки пакетов на табло.
Как отображаются произвольные сообщения, записанные просто как текст в порт, мне не понравилось: сначала экран инвертируется, и текст прокручивается один раз, затем прокручивается второй раз уже нормально.
Команды для получения сообщений же позволяют хранить их прямо в памяти табло. Однако, для этого прошивке надо будет знать, какие "слайды" уже заняты, а какие нет.
Поэтому, пишем простенькое подобие "аллокатора" сообщений :-) Таким образом, каждый "виджет" сможет зарезервировать себе "слайд":
mid.number = mgr->reserve(mid.kind);
А когда тот уже не нужен — освободить:
mgr->remove(mid);
В остальном про код мало что можно рассказать — в отличие от тех же плазменных часов, где пришлось свою графическую библиотеку писать, здесь же просто работа с текстом.
Из того, что показывать, было решено вывести:
-
Погоду
-
"Слово дня" на английском
-
Дату и время
-
Текущий играющий трек в Foobar2000
-
Отправителя и тему входящей почты (IMAP)
Также добавлен проброс с USB-UART у ESP32 напрямую на табло, чтобы впоследствии всё равно хоть как-то интегрировать его с Traktor-OBS-Relay.
Хотелось добавить ещё и свежие твиты для одного из списков в твиттере, чтобы видеть новости от локально живущих товарищей. Однако кое-кто сделал бесплатное АПИ write-only, а для чтения нужно платить 100 баксов в месяц, поэтому идея была отложена в чёрный ящик :-)
До кучи на скорую руку была слеплена и вебморда. Для неё я использовал библиотеку GyverPortal:
(так забавно в ридми у неё смотрится реклама новой версии, отмечающая, что новая работает через интернет и приложение для телефона — как будто это плюсы какие-то)
Дата и время
Ну, тут всё элементарно — резервируем "слайд", и форматируем на него текущее время. Всего кода на 60 строк, и проще его привести здесь, чем описывать:
Код слайда отображения времени
class TimeView {
public:
TimeView(MessageManager* m) {
mgr = m;
mid.number = m->reserve(mid.kind);
}
~TimeView() {
mgr->remove(mid);
}
void update() {
tk_time_of_day_t now_time = get_current_time_coarse();
tk_date now_date = get_current_date();
now_time.millisecond = 0; now_time.second = 0;
if(memcmp(&now_time, &last_time, sizeof(tk_time_of_day_t)) == 0 && memcmp(&now_date, &last_date, sizeof(tk_date_t)) == 0) return;
last_time = now_time;
last_date = now_date;
char buffer[64] = {0};
nje_msg_attrib_t date_attr = { .color = COLOR_RED, .decor = STILL };
nje_msg_attrib_t time_attr = { .color = COLOR_GREEN, .decor = STILL };
snprintf(buffer, 64, "~%c%c~ %02d月 %02d日(%s)~%c%c~ %02d:%02d",
date_attr.color, date_attr.decor,
now_date.month + 1, now_date.day, day_kanji[now_date.dayOfWeek],
time_attr.color, time_attr.decor,
now_time.hour, now_time.minute
);
// Почему-то NJE-105 не нравится decor = STILL в начальных атрибутах сообщения, поэтому его пришлось вынести в текст сообщения
mgr->update(mid, { .attributes = { .color = COLOR_RED, .decor = SCROLL }, buffer });
if(now_date.month == 0 && now_date.day == 1) {
if(sub_mid.number != 0) return;
sub_mid.number = mgr->reserve(sub_mid.kind);
mgr->update(sub_mid, { .attributes = {.color = COLOR_YELLOW, .decor = PULL_INVERSE}, .content = "Happy New Year!" });
} else if (sub_mid.number != 0) {
mgr->remove(sub_mid);
sub_mid.number = 0;
}
}
private:
const char * LOG_TAG = "TIME_VIEW";
const char * day_kanji[7] = { "日", "月", "火", "水", "木", "金", "土" };
MessageManager * mgr;
tk_time_of_day_t last_time = { 0 };
tk_date last_date = { 0 };
nje_msg_identifier_t mid = { .kind = MSG_NORMAL };
nje_msg_identifier_t sub_mid = { .kind = MSG_NORMAL };
};
Единственный подводный камень — в заголовке сообщения атрибут "Статичное отображение" использовать нельзя, табло почему-то просто вешается намертво, а после сброса жалуется на повреждение оперативной памяти. Поэтому пришлось этот атрибут вставить напрямую в текст сообщения.
Погода
Тут тоже всё было довольно элементарно — нужно было просто угнать код для обращения к OpenWeatherMap из часов, которые я делал раньше :-)
Так же как и дату-время, просто форматируем и выдаём на зарезервированный под это дело слайд.
Ключ доступа к АПИ тоже взял из часов — в бесплатном тарифе там столько доступов даётся, что мне одного ключа хватает на все устройства, включая два смартфона и смарт-часы.
Foobar2000
Здесь уже пришлось повозиться — единственным плюс-минус удобным способом вытягивать метаданные из fb2k оказался плагин foo_controlserver.
Был написан простенький клиент, в цикле долбящийся на заданный айпишник и порт. Если подключиться получилось, то он бесконечно слушает входящие строки, и вытаскивает из них события воспроизведения/паузы и название трека.
Формат там напоминает CSV, только разделителем является вертикальная черта. Соответственно, если она есть в названии трека или исполнителя, парсинг развалится и на экране будет чёрт знает что. Не идеально, но и не критично.
Слово дня
Это такая странная вещь, показывающая каждый день случайно выбранное словарное определение. Раньше у меня такой скринсейвер на маке был, вот привычка и осталась.
Недолгие поиски привели к Wordnik API. Дальше всё было тоже элементарно — получаем JSON, парсим его, выводим на экран.
Почта по IMAP
Вносить лишние сущности я не люблю, дома не держу ни сервера, ни даже Разберипай, поэтому и получение почты было решено возложить прямо на микроконтроллер — безо всяких MQTT и прочих промежуточных звеньев.
Казалось бы — протокол древний, строго описанный в RFC, плейнтекстовый: должна быть туча реализаций разного качества, от наколенных поделок до полноценных модулей-комбайнов.
Вот тут-то меня и поджидали анальные пирогенные боли и прочие мозговые страдания!
Первом сюрпризом то, что единственная "микроконтроллерная" библиотека для электронной почты вся кривая, косая и тащит за собой драйвера внешнего флеша, карт памяти, десятка разных видов контроллеров сети, и ещё тучу всякого хлама. Да что там, просто после добавления в проект она даже не собиралась!
Поэтому пришлось интенсивно высирать 600 строк, которые упадут при первой же возможности — но вроде пока что работают.
Дальше просто по колбеку ловим новые заголовки и создаём под каждое письмо новый слайд, а когда оно становится прочитанным или удалённым — удаляем и его.
Код отображения почты
std::map<imap_message_id_t, nje_msg_identifier_t> mail_map = {};
void mail_cb(imap_message_id_t id, const imap_message_info_t * info) {
nje_msg_identifier_t mid = { .kind = MSG_NORMAL, .number = 0 };
if(mail_map.count(id) == 1) {
mid = mail_map[id];
} else if(info != nullptr) {
mid.number = mgr->reserve(mid.kind);
}
if(info != nullptr) {
char buf[128] = { 0 };
snprintf(buf, 127, "%s ~%c%c~%s",
(info->sender_name[0] == '?') ? info->sender_mail : info->sender_name,
COLOR_RED, SCROLL,
(info->subject == nullptr || info->subject[0] == '?') ? "(新着メール)" : info->subject);
if(current_state == STATE_IDLE)
mgr->update(mid, { .attributes = { COLOR_GREEN, SCROLL }, .content = buf });
mail_map[id] = mid;
} else if (mid.number != 0) {
if(current_state == STATE_IDLE)
mgr->remove(mid);
mail_map.erase(id);
}
}
Вторым сюрпризом оказался всё тот же нерабочий iconv. Я-то думал, что там просто не включили поддержку SJIS, но нет — он мёртвый совсем, даже при попытках конвертации из ASCII в ASCII выдаёт дулю. Поэтому заголовки сообщений поддерживаются только в виде UTF-8, а остальные замещаются просто на текст "Новое сообщение".
❯ Итоговый результат
В остальном исходники можно посмотреть на гитхабе, а пока полюбуемся на готовый результат:
Я считаю, получилось неплохо! Хотя и муторно выключать его руками каждый раз, поэтому, датчик движения, наверное, когда-то таки добавлю.
А дойдут ли до этого у меня руки вы сможете узнать — среди тонн фоток еды, Мику, и прочего хлама из комиссионок — в моём телеграме :-)
Читайте также:
Автор: Ak.R.