0

Часы-компьютер LILYGO

Увидел у друга на руках часы 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 разметка - прошу понять и простить.

0
Автор поста оценил этот комментарий

Самый мелкий шрифт - наверное только под лупой рассматривать? Или только очень близко - но это не для всех, а только для молодого человека а хорошим зрением - но это будет некомфортно.

раскрыть ветку

Темы

Политика

Теги

Популярные авторы

Сообщества

18+

Теги

Популярные авторы

Сообщества

Игры

Теги

Популярные авторы

Сообщества

Юмор

Теги

Популярные авторы

Сообщества

Отношения

Теги

Популярные авторы

Сообщества

Здоровье

Теги

Популярные авторы

Сообщества

Путешествия

Теги

Популярные авторы

Сообщества

Спорт

Теги

Популярные авторы

Сообщества

Хобби

Теги

Популярные авторы

Сообщества

Сервис

Теги

Популярные авторы

Сообщества

Природа

Теги

Популярные авторы

Сообщества

Бизнес

Теги

Популярные авторы

Сообщества

Транспорт

Теги

Популярные авторы

Сообщества

Общение

Теги

Популярные авторы

Сообщества

Юриспруденция

Теги

Популярные авторы

Сообщества

Наука

Теги

Популярные авторы

Сообщества

IT

Теги

Популярные авторы

Сообщества

Животные

Теги

Популярные авторы

Сообщества

Кино и сериалы

Теги

Популярные авторы

Сообщества

Экономика

Теги

Популярные авторы

Сообщества

Кулинария

Теги

Популярные авторы

Сообщества

История

Теги

Популярные авторы

Сообщества

Недвижимость и ремонт

Теги

Популярные авторы

Сообщества