Учим азбуку морзе
Учим азбуку Морзе за одну минуту
Внезапно это оказалось проще, чем кажется на первый взгляд.
Взято тут: https://t.me/porjatiipodumati/24806
Учим азбуку Морзе за одну минуту
Внезапно это оказалось проще, чем кажется на первый взгляд.
Взято тут: https://t.me/porjatiipodumati/24806
Увидел у друга на руках часы LiLYGO - захотел такие.
У часов есть лора, вайфай, gps - но делать гаджет для умного дома было скучно и неинтересно. Из желаемого - превратить часы в ультра полезный тренажёр.
Очень хотелось в часы засунуть карточки японских кандзи уровней N5,4,3. Что я и сделал. А ещё мне показалось что тренажёр азбуки морзе будет очень полезен в часах.
Форкнулся с официального репозитория LilyGo T-Watch Ultra
Напомню железо под капотом: ESP32-S3, круглый AMOLED 466×466, тач CST92xx, вибромотор DRV2605, LoRa SX1262, SD-карта, датчик шагов BHI260AP.
Базовые прошивки в examples показывают как работать с датчиками. Всё что нужно - там присутствует. Ну почти всё.
Прошивка AsciiClock (examples/AsciiClock)
Свой форк LilyGo T-Watch Ultra я опубликовал на гитхабе.
Я создал свою прошивку методом копипаста, ии-манки-кода. В лучших традициях так сказать.
Экраны: один `.ino`, много экранов LVGL
Прошивка — ~4500 строк `AsciiClock.ino` плюс модуль `morse_mod.inc` Навигация построена на enum-экранах:
enum Screen : uint8_t {
SCREEN_HOME = 0,
SCREEN_SETTINGS = 1,
SCREEN_ALARM = 2,
SCREEN_ALARM_RING = 3,
SCREEN_REC = 4,
SCREEN_VOLUME = 5,
SCREEN_TOUCH_CAL = 6,
SCREEN_KANJI = 7,
SCREEN_MORSE = 8
};
Каждый экран — отдельный `lv_obj_t *`, создаётся в `setup()`, переключается через `show_screen()`. Отрисовка текста и графики — не стандартные виджеты LVGL, а **собственный рендер в canvas**: цифры часов заданы как 5×5 ASCII-матрицы, иероглифы и морзе-код рисуются поблочно в `lv_draw_buf_t`.
Главный цикл минималистичен — вся логика в poll-функциях:
void loop() {
instance.loop();
feed_gps();
poll_boot_button();
audio_poll();
rec_poll();
morse_poll(millis());
// ... alarm, clock update, lv_timer_handler()
}
`morse_poll()` вызывается **каждый кадр**, даже когда экран морзе скрыт — при уходе с экрана он сбрасывает вибрацию и настройки тача. В режиме включенных экранов CPU поднимается до высокой частоты (`set_cpu_high(true)`), на домашнем экране с погашенным дисплеем — понижается для экономии (40 мегагерц).
Что ещё внутри
Помимо тренажёров: будильник с WAV с SD. Очень тихий. Планирую заменить динамик чтобы орал. Пока тихо шепчет. Диктофон, калибровка громкости, подсчёт шагов, настройки GPS/NFC/LoRa/Wi-Fi. Аудио — свой PCM-плеер на Ultra AudioOutputI2S.cpp тянет legacy I2S-драйвер.
Морзе напевами: мнемоника + вибрация + тач-ключ
Напевы - простой способ выучить морзе: каждой букве соответствует запоминающаяся фраза, слоги которой совпадают с ритмом точек и тире. Данные на SD в `/morse.csv`:
Буквы и цифры,Словесное обозначение,Слоговое обозначение,Код Морзе
А,айда,ай-да,.-
Б,баки текут,ба-ки-те-кут,-...
М,мама,ма-ма,--
Структура в прошивке:
struct MorseGlyph {
char letter[8];
char latin[8];
char verbal[48]; // «айда»
char syllable[48]; // «ай-да»
char code[16]; // «.-»
};
Четыре режима (MorsePage):
| Режим | Что делает |
| Menu| выбор: буквы или слова |
| Tune Learn | показывает мнемонику, проигрывает код вибрацией |
|Tune Train** | пользователь вводит код удержанием пальца |
| Word Train | то же для слов из /words.csv |
Проигрывание и приём
Вибромотор DRV2605 работает в RTP-режиме (real-time playback). Конечный автомат `morse_play_poll()` чередует фазы MARK (вибрация) и GAP (пауза) с таймингами ITU:
static constexpr uint32_t MORSE_DOT_MS = 110; // ~60 знаков/мин
static constexpr uint32_t MORSE_DASH_MS = MORSE_DOT_MS * 3;
static constexpr uint32_t MORSE_CHAR_GAP_MS = MORSE_DOT_MS * 3;
static constexpr uint32_t MORSE_WORD_GAP_MS = MORSE_DOT_MS * 7;
static constexpr uint32_t MORSE_DOT_THRESH_MS = MORSE_DOT_MS * 2;
В режиме Train палец на экране = ключ. Длительность удержания классифицируется: `< 220 мс` — точка, `≥ 220 мс` — тире, `< 25 мс` — шум (игнор), `> 990 мс` — ошибка. При неверном знаке **весь шаблон сбрасывается** — так проще закрепить ритм целиком, а не отдельные элементы.
**1. Тач CST92xx «отпускает» палец.** Контроллер на один кадр (~16 мс) сбрасывает PRESSED, хотя палец на экране. Без обработки длинное тире превращается в две точки. Решение — debounce отпускания 40 мс и сохранение `morse_key_t0` при «ложном» re-press:
static constexpr uint32_t MORSE_RELEASE_DEBOUNCE_MS = 40;
// Continuation of the same hold (touch dropped a frame) — keep t0.
if (morse_key_down && morse_release_at) {
morse_release_at = 0;
morse_vibe_set(true);
return;
}
Конфликт с LVGL. Long-press и scroll тача мешают кодированию. На время Train long-press увеличивается до 2,5 с, scroll limit — до 255 (фактически отключён):
static void morse_indev_tune(bool train) {
if (train && !morse_indev_tuned) {
lv_indev_set_long_press_time(indev, MORSE_LONG_PRESS_MS); // 2500
lv_indev_set_scroll_limit(indev, 255);
morse_indev_tuned = true;
}
// ...
}
Инкрементальная отрисовка.
Каждый введённый знак перерисовывает только свою ячейку (morse_dirty_from), а не весь canvas. Рисовать весь экран долго и медленно.
Тренажёр кандзи: CSV-карточки и атлас глифов на SD
Данные
База — kanji-jlpt-n5-n4-n3.csv (~700 иероглифов уровней N5–N3). Строка содержит уровень, кандзи, онёми/кунёми чтения и три примера слов с переводом. CSV не парсится целиком в RAM — при первом открытии строится индекс смещений строк:
static bool kanji_index_csv() {
while (f.available() && kanji_rows < KANJI_MAX_ROWS) {
kanji_off[kanji_rows] = (uint32_t)f.position();
kanji_read_line(f, line, sizeof(line));
kanji_rows++;
}
}
Карточка подгружается по seek(kanji_off[idx]) — случайный выбор через esp_random.
Навигация свайпом
Восемь «страниц» карточки (`kanji_page` 0…7):
0: инфо (уровень, он/кун)
1–3: примеры слов
4: рисование иероглифа
5–7: рисование слов из карточки
Свайп влево — следующая страница, кнопка shuffle — новая случайная карточка. Позиция последней карточки сохраняется в `/kanji_last.bin`.
Формат kanji_glyphs.bin
Глифы не в прошивке. Python-скрипт gen_kanji_glyphs.py рендерит Noto Sans CJK и пишет бинарный атлас:
Заголовок (24 байта):
magic[8] = "KGLF002\0"
glyph_size = 32 // текстовые глифы
glyph_bytes = 128 // 32×32 1bpp
glyph_count = N
big_size = 96 // крупные кандзи для карточки
big_bpp = 2 // 4 уровня серого
big_bytes = 2304 // 96×96 2bpp
big_count = M
Далее:
uint16_t cps[N] // codepoints текстовых глифов
uint8_t bits[N × 128] // растровые данные
uint16_t big_cps[M]
uint8_t big_bits[M × 2304]
При открытии тренажёра весь файл (~1–2 МБ) загружается в PSRAM одним блоком. Поиск глифа — бинарный по отсортированному массиву codepoints:
uint8_t *blob = (uint8_t *)heap_caps_malloc(sz, MALLOC_CAP_SPIRAM | MALLOC_CAP_8BIT);
if (memcmp(blob, "KGLF002", 7) != 0) { /* ошибка */ }
// ...
return kanji_find_in(kanji_cps, kanji_bits, kanji_glyph_count, KANJI_GLYPH_BYTES, cp);
В flash остаётся только `kanji_glyphs.h` — размеры и ASCII-иконка кнопки «漢字» 48×24.
Рисование иероглифов
На страницах 4–7 можно рисовать стилусом. Штрихи пишутся в отдельный ink-буфер (kanji_ink), затем накладываются на canvas с альфа-смешиванием. Линии интерполируются с шагом 0,45 px; перерисовывается только затронутый прямоугольник (lv_obj_invalidate_area). Кнопка Hint показывает эталонный глиф 96×96 поверх ink-слоя. На больших иероглифах точность приемлемая. На маленьких так себе.
UTF-8 парсится вручную (utf8_next), отрисовка посимвольная через собственный kanji_draw_text() — стандартные шрифты LVGL не покрывают CJK на нужном размере без мегабайтных font-файлов.
Калибровка тача: аффинное преобразование
Поскольку тренажёр кандзи содержит ещё экран для их рисования - возникла потребность калибровки тача.
CST92xx на Ultra даёт сырые координаты с заметным смещением и масштабированием. Калибровка — три креста по углам экрана (с отступом 36 px), коэффициенты аффинного преобразования вычисляются через определители 3×3:
// X' = a·rx + b·ry + c
// Y' = d·rx + e·ry + f
touch_cal_a = det3(X0, y0, 1, X1, y1, 1, X2, y2, 1) / D;
// ... остальные коэффициенты
Результат сохраняется на сд карте в /touch_cal.bin (magic `TCA1` + 6 float). Применяется глобально в обработчике тача до передачи координат в LVGL. Без калибровки рисование кандзи и ввод морзе на краях экрана неточны.
Экран калибровки открывается свайпом влево с экрана громкости.
Энергопотребление и периферия
Отложенная инициализация
SD, NFC, GPS и LoRa **не инициализируются при старте** — общая SPI-шина, крупные SD-карты могут зависнуть на `begin()`:
instance.begin(NO_HW_SD | NO_HW_NFC | NO_HW_GPS | NO_HW_LORA);
// ...
set_gps(false);
set_nfc(false);
set_lora(false);
Модули включаются вручную в настройках. При выходе из light sleep restore_peripherals_after_sleep() синхронизирует состояние питания с флагами.
Light sleep
Кнопка BOOT (GPIO0) гасит дисплей и уводит часы в light sleep. Пробуждение — только BOOT или PWR, не тач (иначе ложные пробуждения). Датчик шагов BHI260AP остаётся под питанием (ALDO4), шаги продолжают считаться.
LoRa: задел и планы
На домашнем экране — статус LORA (зачёркнут, если выключен). В настройках переключатель вызывает:
if (lora_on) {
instance.powerControl(POWER_RADIO, true);
instance.initLoRa();
} else {
radio.sleep();
instance.powerControl(POWER_RADIO, false);
}
Пока LoRa — инфраструктура без протокола. Логичное развитие:
1. Полу-дуплексный чат морзе между двумя часами без интернета.
2. Пакетизация — SX1262: payload до 255 байт, но для морзе достаточно передавать посимвольно с тайм-слотами.
3. Синхронизация пауз — приёмник должен знать MORSE_DOT_MS отправителя или согласовывать скорость в handshake.
Для обучения это даст «живую» практику — не только вибрацию на запястье, но и реальный радиоканал.
Как попробовать
1. Прошить `examples/AsciiClock/AsciiClock.ino` на T-Watch Ultra.
2. На SD-карту (корень) положить из репозитория:
- `morse.csv`, `words.csv`
- `kanji-jlpt-n5-n4-n3.csv`, `kanji_glyphs.bin` (сгенерировать: `python3 gen_kanji_glyphs.py`)
3. Откалибровать тач (громкость → свайп влево).
Исходники — в репозитории LilyGoLib, ветка с примером: `examples/AsciiClock`.
В статье частично осталась md разметка - прошу понять и простить.
Когда-то решил приколоться и написать песню про УВБ-76. Да, это, хехе, песня.
Получилось экспериментально до упоротости. Как потом оказалось, я по ходу вообще первый, кто так прикололся. Если кто прикол оценил, вдруг в курсе, точно я первый? Или кто еще так делал?
235 лет назад родился Сэмюел Морзе
Имя американца Морзе прочно ассоциируется с изобретённым им телеграфом и специальным алфавитом, азбукой Морзе.
Но путь к мировой славе начался не в лаборатории – в юности Морзе мечтал стать большим художником, учился живописи и писал исторические полотна и портрета. Со временем, впрочем, в этом виде искусства он разочаровался: в старости не раз говорил, живопись оказалась для него «жестокой изменницей».
Поворотным моментом для обращения к науке стала личная трагедия. В 1825 году Морзе слишком поздно получил письмо о тяжёлой болезни жены: медленная почта не оставила ему шанса попрощаться с любимой. Именно тогда он особенно остро понял цену времени – с этого дня он решил заняться созданием системы мгновенной связи.
В конце 1830-х Морзе разрабатывает электромагнитный телеграф и знаменитый код из коротких и длинных сигналов, точек и тире. В 1844 году по линии между Вашингтоном и Балтимором ушло первое официальное сообщение: «Чудны дела твои, Господи!».
И началась новая эпоха. Очень быстро телеграфные линии опутали США, затем Европу и весь мир. Новости, биржевые сводки, дипломатические донесения и частные сообщения впервые стали перемещаться по свету почти со скоростью мысли.
Азбука Морзе пережила самого автора на столетия – и даже в наши дни «морзянке» находится применение. В том числе и на СВО. Причина проста: морзянка устойчива к помехам, требует минимальных ресурсов, позволяет работать на больших расстояниях и по-прежнему надёжным резервным способом связи там, где сложные цифровые системы могут быть атакованы и уязвимы.
Источник данных для материала:
https://inosmi.ru/20240519/morze-268901886.html
https://iz.ru/871979/georgii-oltarzhevskii/tochechnyi-passaz...
У начинающих разработчиков игр есть свой романтический, "конфетно-букетный" период.
Едва научившись делать первый платформер, хочется побыстрее сделать игру своей мечты. Что-нибудь потрясающе уникальное, концептуально-гениальное.
Потом приходит понимание, что начатая игра не так уж и идеальна, как виделась в грёзах.
Тогда разработчик переходит к следующей игре, которая часто тоже оказывается "не той" и начинается новый цикл.
У меня одной из таких концептуальных игр была Магия Морзе:
В тот момент я хотел сделать что-то гиперказуальное, управляемое одним пальцем и в то же время - обучающее. И пришла мысль, что азбука Морзе - то, что нужно.
Если натянуть её на простенькую идею о разных измерениях и приправить магией, то может получится что-то интересное.
Кстати, есть любопытный старенький мультфильм на тему двумерных и одномерных измерений. Называется "Плоский мир". Рекомендую для общего развития.
Итак, чтобы что-то сделать в этой игре нужно создать соответствующую руну (символ из азбуки Морзе).
Тапаем коротенько - создаём электрон.
Удерживаем тач чуть подольше - двигаем персонажа в заданную сторону. Ну и дальше по нарастающей.
Сначала создаём первое измерение, создавая электроны.
Затем разгоняемся по этому измерению максимально быстро, чтобы создать второе измерение.
Потом всё по классике - создаём землю, небо, растительность и существ.
На этом моменте я уже подустал и понял, что не смогу и дальше развивать игру.
Поэтому по быстренькому, но не теряя концептуальности завершил сюжет и принялся за следующую игру - Магию Морзе 2.
Она тоже посвящена азбуке Морзе, но уже имеет более простую механику - "защита башни".
К сожалению (или к счастью), она уже не смогла пройти модерацию и не попала на Пикабу Игры.
А у вас был такой романтический период в своей работе?