Pull to refresh

Comments 25

Очень интересно, но ничего не понятно, о чем вообще речь/суть. П.С. Я бы сделал корпус из картона обмотанный скотчем

А тут интересная статистика дочитываний/скролла когда читают только заголовок и комментарии, поэтому самое интересное - внизу. Те кто занимается работой с дисплейчиками на микроконтроллерах могут взять на вооружение ИИ-код-скетчи и md-саммари в виде скиллов для:
- перебрать в системе все моноширинные шрифты
- преобразовать шрифт в атлас с учётом паддинга для букв, с управляемым смещением
- преобразовать картинку в битмап
- генерация кода для рендеринга шрифта с применением оптимальных масок/загрузок из атласа
- реализовать драйвер дисплея по SPI с использованием DMA для быстрого обмена
- возможность страничной реализации и локального апдейта с курсором дисплея
То есть нет уже необходимости искать какие-то библиотеки, исходники, ИИ - по факту и есть генератор этих библиотек. Более того, при должном подходе вполне может генерировать только то что нужно для периферии на уровне регистров без применения конфигураторов и абстракций уровня языка вроде Cube/HAL (уйдут в прошлое или станут надбавкой к ИИ как tool) Достаточно скиллов, эмбеддеров и MCP/RAG с даташитами + тулкиты. Больше не нужно подтягивать к проекту весь мир библиотек а кодить и извлекать только то что пойдёт в релиз. Вообщем библиотека которая генерирует библиотеку. Вот такие тут идеи.

А зачем Вам искать библиотеку на 12864? Есть ведь даташит. На мой взгляд, даташит на этот экран - это прямо азбука. Идите по шагам, там все расписано, инит, тайминги, два вида интерфейса параллельный и SPI. Если кажется сложновато, то возьмите 1602 или датчик типа dht22, там вообще все довольно понятно и поэтому легко написать свою библиотеку по даташиту на девайс. И если мк STM32, то для проверки работоспособности самописной библиотеки HAL будет в самый раз.

Также, чем Вас всё таки не устраивают копеечные платы для прототипирования, что приходится заниматься тем, что Вы описали в статье?

А зачем Вам искать библиотеку на 12864? Есть ведь даташит

Всё верно! Можно конвертировать даташит с использованием pdftotext -layout datasheet.pdf output.txt с сохранением лайаута, чтобы агент мог корректно читать таблицы регистров. Заставить его написать заголовочник с командами и описание типов данных + конечный автомат управления. А если уже готовое железо и не охота брать даже простое. Тут уже искусство промпт-инъекций. Я использую вариант по умолчанию baai/bge-m3 + qdrant. Индексирует любой даташит и, что самое главное, эрраты, прошу чтобы не загромождать контекстное окно читать сотню строк. Вообщем мне лично как кожаному мешку пардон читать доки уже больше практически не приходится, вот такое вот время наступило. Только следить за глюками, которые при должной обёртке тестами выявляются практически мгновенно.

 и поэтому легко написать свою библиотеку

Вот это как раз и тестируется, то есть я её делаю своей по результатам тестов кода который даёт агент. Даташиту соответствует? - добро пожаловать во Flash/EEPROM. Там помимо всего прочего - вывод текста (как описано - специально созданного шрифта), графика, инициализация периферии, работа с энкодером и драйвером шагового по протоколу. То есть то на что тратятся дни теперь уже идут минуты. Модем который пишет рабочий код на 9600 (так и есть, десяток токен в секунду). Там ведь ещё и буфер, и DMA и флажки всякие и связь с очередями сообщений, потом ещё менюшка, её логика, так что огород набегает очень быстро в таких разработках.

копеечные платы для прототипирования

Так ведь потом эти платы для прототипирования надо ещё прикрепить, да так чтобы они не коротнули соседние. Как раз предлагаемый метод и нужен чтобы захомутать платы без монтажных отверстий и прочие навесные крупногабаритные штуки. А там уже ценник breadboard на десяток тысяч точек уже не назвать копеечным. Двусторонний скотч, картон - не тянут как уже подсказали.

библиотеки HAL

HAL - это боль, оплаканная слезами С, обёрнутая в макросы и комментарии. Это изначально было создано для более скажем так лёгкой работы человека. Агенту это не нужно, наоборот, это размазывает контекст. Поэтому везде где можно - классический SPL на регистрах. Разматывает битовые поля-флажки до комментариев и наоборот. То есть удобство наоборот ушло в простоту теперь чем в абстракции в попытке из С сделать С++ для МК. Для начала нулевых - это супер, сейчас же - зачем учить искусственное когда имеется естественное.

 библиотеки HAL

сорри за мою неверную пункутацию, здесь должно было быть "...библиотеки, HAL...". HAL я имел ввиду, как инструмент проверки функционала вашей самописной библиотеки. Вы вот целый абзац, уж простите, воды выдали по поводу HAL. Но у меня прямой вопрос - у Вас реально были проблемы с HAL при работе с 1602, AM2302 или 12864, я сейчас конкретно об этих устройствах говорю???

классический SPL на регистрах

Очень странно использовать SPL вместо CMSIS. Это суть тот же HAL, только древний и только под STM32. Вот скажите мне, где здесь регистры?:
GPIO_SetBits(GPIOA, GPIO_Pin_5);

и опять же, если Вы сгенерировав в CubeIDE проект под HAL, вдруг захотите прописать
GPIOA->ODR |= GPIO_ODR_OD5;

то всё нормально соберётся и будет работать.

breadboard на десяток тысяч точек

Смотрю на фото Вашего прототипа и вижу максимум 50 (может и меньше, лень считать) навесных проводников. Вот этого

Двухсторонняя печатная плата BoEixun 12x18 см | AliExpress

плюс немного МГТФ и штыревых разъемов 2.54мм - должно за глаза хватить. Пластиковые растерные платы "для Arduino", признаться, меня тоже бесят, если их применять с обычными китайскими Dupon проводами. Однако и для них есть хорошее решение


140 шт. комплект предварительно сформированных перемычек для макетной платы, 14 длинок в ассортименте для схем для прототипирования макетной платы | AliExpress

Обе ссылки на товары приведены в качестве "сферических примеров в вакууме", т.е. для приблизительной оценки уровня цен.

И ещё, одна просьба. У меня давненько зреет желание написать статью для начинающих, на тему самостоятельного написания библиотек и драйверов под простые устройства. Там же приведу пример монтажа для прототипирования. Могу ли я, с Вашего позволения, сослаться на материал Вашей статьи? Удачного дня!

у Вас реально были проблемы

В нынешней ситуации необходимо уточнить номер личности. У первой, человека - никаких проблем нет, а вот у второй, ИИ-помощника - вполне себе иногда проскакивают.

Очень странно использовать SPL вместо CMSIS. Это суть тот же HAL

Это соответствует эволюции методик как одно и то же сделать отвёрткой, топором и микроскопом одновременно. Для эксперимента я попытался собрать мега-файл который вбирает в себя всё подряд (гипотетически он потом будет генерироваться Питоном). GPIOA->ODR |= GPIO_ODR_OD5; - это и имелось в виду под регистрами без обёртки функцией со структурой.

Взять к примеру нечто вроде cmsis-device-f1, stm32f1xx-hal-driver, cmsis-core, stm32f1-hal, STM32F10x_StdPeriph_Lib и прочая и прочая. Там есть как HAL так и LL так и ещё что-то. Причём есть версии где внутри файлов реализации есть макросы, которые имеют одинаковые имена (!) но разные значения. Это пригодно для человека но ломает поиск эмбеддером.

Файл stm32f10x_dma.c

/* DMA2 FLAG mask */
#define FLAG_Mask                ((uint32_t)0x10000000)

Файл stm32f10x_i2c.c

/* I2C FLAG mask */
#define FLAG_Mask               ((uint32_t)0x00FFFFFF)

И как после этого делать поиск по кодовой базе? Неужели нельзя было дописать что-то вразумительное вроде DMA_ или I2C_
Особенно доставляет редефайн макросов для совместимости зоопарка

#define RCC_CR_HSION_Pos                     (0U)
#define RCC_CR_HSION_Msk                     (0x1UL << RCC_CR_HSION_Pos)        /*!< 0x00000001 */
#define RCC_CR_HSION                         RCC_CR_HSION_Msk                  /*!< Internal High Speed clock enable */

Вообщем стало понятно что это чьи-то эксперименты по отъёму времени разработчиков.

Поэтому пришлось взять ножницы, клей и сформировать мега-файл microlib.c и к ней гипер-заголовочник microlib.h с соответствующей разметкой из которых можно уже потом неким Питоном генерить то что нужно для bare-metal проекта. Там что-то вроде определения констант SPL

 / CR register bit mask */ 
#define CR_HSEBYP_Reset           ((uint32_t)0xFFFBFFFF) 
#define CR_HSEBYP_Set             ((uint32_t)0x00040000) 
#define CR_HSEON_Reset            ((uint32_t)0xFFFEFFFF)
Базовые структуры

typedef struct 
{ 
  __IO uint32_t SR; 
  __IO uint32_t CR1; 
  __IO uint32_t CR2; 
  __IO uint32_t SMPR1;
 ....

Микро-функции из разряда

void RCC_ClearFlag(void) 
{ 
  /* Set RMVF bit to clear the reset flags */ 
  RCC->CSR |= CSR_RMVF_Set; 
} 

Которые суть CMSIS можно инлайнить или генерировать налету, включая отсутствие нагрузки в виде дебаг-заплаток и статических assertion которые и так понятны самим кодогенератором.

Там же то что требуется для точки входа и прочая для начальной инициализации, включая заглушки и прочий semihosting

caddr_t sbrk(int incr) { 
        extern char Heap_Begin; // Defined by the linker. 
        extern char HeapLimit; // Defined by the linker.

...

Плюсом стандартные заготовки для таблицы векторов а-ля vectors_stm32f103x6.c, sections.ld секции линкёра и по мелочам.

То есть микро-библиотека полностью отвязана от внешних обстоятельств. Там содержится база которая в полной мере понятна агенту и эмбеддеру, из неё он может по месту выкручивать любые функции. Другое дело что апдейт, но это поправимо парсингом того что изливается из недр производителя. Вплоть до переписывания HAL->LL.

CubeID - это статический генератор, у него нет обратной связи из кода в графику. Особенно когда система реконфигурируется в процессе работы. Всё что тут возможно - это MACHINE GENERATED DO NOT EDIT, веет уже четверть вековой давностью. Проще уже описать систему md или промптом чем выставлять флажки вручную - пусть сам думает какой там канал DMA для АЦП или UART и кто с кем конфликтует. ИИ-агент же может эту обратную связь худо-бедно провернуть и оценить как и что инициализируется и в каких местах. Вот для этого я и адаптирую либу чтобы она была как можно более "токен-совместима" чем "человеко-читабельна".

Пластиковые растерные платы 

Проблема в том, что железки уже есть как данное. То есть там не нужно ничего паять грубо говоря, только платы с минимальной обвесухой вроде потенциометра. Их необходимо просто закрепить. Образно говоря - расположить на столе и залить строительной пеной/эпоксидкой чтобы не улетало ничего. Задача по монтажу другая - сверхбыстрый объёмный монтаж готовых уже соединённых модулей.

сослаться на материал 

Конечно можно, в Вашем материале дать ссылку на сиё скромное творчество.

Конечно можно, в Вашем материале дать ссылку на сиё скромное творчество.

Благодарю Вас. Теперь точно придётся писать :)

Напомнило (прошиванием нитками) мой детский "вайб-монтаж" схем на цифровых микросхемах. Плата представляла собой стопку из 3-4-5 перфокарт, прошиваемых с помощью суровой иглы голым проводом 0,25 мм. За счет разделения перфокартами можно было организовывать многоуровневые пересечения проводников. Для впаивания деталей делались два рядом стоящих отверстия, через которые пропускалась пара витков провода. Одно из отверстий предназначалось для пропуска вывода, который припаивался к петле провода, либо использовались оба отверстия для соединения двух выводов кратчайшим путем. Затем все еще прошивалось по краю - проводом или капроновой ниткой. В итоге получалась довольно жесткая и прочная плата, позволявшая перепаивать детали и не страдавшая болезнью "одно припаяли - другое отвалилось".

да был такой метод, одножильный через шило с ушком или толстая иголка от шприца (тот который стеклянный, многоразовый, раньше такие были и иглы к нему с металлическим штуцером, где проволока не застревала) прокалывался любой материал и переворачивался - микросхемы и что угодно паяли на образующиеся петельки. Чтобы уж совсем не болталось можно было залить парафином от свечки целиком. На пересечении проводов - просто ложилась синяя изолента или на всём пути, медная фольга - на питание. Фактически многослойка. Так был собран ДВ/СВ (сейчас там уже тишина) на КТ315 с выходным каскадом на КТ6хх не помню какой в корпусе который поменьше ТО220 (причём прямиком на динамик, тогда были слабые представления о согласовании сопротивлений) и квадратной батарейке.

У нас это называется соплестрой. Обычно делался притягиванием нейлоновыми стяжками к куску нефольгированного стеклотекстолита, отверсия сверлятся по месту. Выдерживает годы интенсивной эксплуатации, в том числе в тяжёлых условиях.

А если добавить "котлету" из липолек и примотать все синей изолентой - ьудет шахид-девайс))

Когда-то вполне рабочая промышленная технология подобная. Топология вышивалась. Хех ... год этак 88-90й.

Самый дешевый принтер стоит в районе 10ки... при должном проектировании DIY может быть серьёзнее заводских изделий. Дендрофекальный DIY уже пора бы признать мертвым

Проект свёрстан к 18-00. Платы для отладки нужны к 18-30. Залить релиз проверить работоспособность ткнуть пару точек осциллографом, к 20-00 софт уже должен быть готов к использованию на завтра. После чего платы демонтируются ножницами. Какой принтер за полчаса напечатает оснастку в габарите 150 на 200, плюс клипсовый держатель печатных плат. Трапециевидные "выросты"-держатели и прочие защёлки любым пластиком - это сразу на слом, равно как бобышки без рёбер жёсткости, пусть даже формальных, когда слайсер делает дополнительные линии улучшающие прочность. Нарисовать оснастку чтобы она не отламывалась это отдельная история. Да и рисовать как то тоже не очень, это тоже время - разве что агента к FreeCAD прикрутить который будет на Питоне в консоли детали рисовать или что то вроде OpenSCAD. Пример промпта: OpenSCAD дай панель 150 на 200 на которой внутри вырез глубиной 2 мм круг, с торца которого растут крепёжки защёлки клипсы трапеция крючок высота 10 мм 3 штуки.
Результат:

Код OpenSCAD
// Параметры панели
panel_w = 150;
panel_l = 200;
panel_h = 5;

// Параметры выреза
cut_r = 40;        // Радиус круглого выреза
cut_depth = 2;    // Глубина выреза

// Параметры защелок
clip_h = 10;       // Высота защелки от дна выреза
clip_base_w = 12;  // Ширина основания трапеции
clip_top_w = 8;    // Ширина верха трапеции
clip_thick = 2;    // Толщина тела защелки
hook_ext = 1.5;    // Вылет крючка защелки

$fn = 100;         // Сглаживание окружностей

// 1. Создание основной панели с круглым вырезом
difference() {
    // Основная плита (центрированная по X и Y)
    cube([panel_w, panel_l, panel_h], center = true);
    
    // Круглый вырез (смещен на поверхность панели)
    translate([0, 0, (panel_h / 2) - cut_depth + 0.01])
        cylinder(r = cut_r, h = cut_depth, center = false);
}

// 2. Добавление 3-х защелок по торцу выреза (расположены под углом 120 градусов)
for (angle = [0, 120, 240]) {
    rotate([0, 0, angle])
        translate([cut_r, 0, (panel_h / 2) - cut_depth])
            single_clip();
}

// Модуль одной защелки-крючка
module single_clip() {
    rotate([0, 0, 0]) { // Смотрит наружу от центра выреза
        difference() {
            // Трапециевидное основание защелки
            rotate([90, 0, 90]) // Поворот для правильной ориентации в пространстве
                linear_extrude(height = clip_thick)
                    polygon(points = [
                        [-clip_base_w / 2, 0],
                        [clip_base_w / 2, 0],
                        [clip_top_w / 2, clip_h],
                        [-clip_top_w / 2, clip_h]
                    ]);
        }
        
        // Крючок на вершине защелки (выступает внутрь выреза для фиксации)
        translate([-clip_thick, -clip_top_w / 2, clip_h - 2])
            cube([clip_thick + hook_ext, clip_top_w, 2]);
    }
}

На этот вайб-3D потрачено где-то 45 секунд. Если будет 50, то проще вручную

Ну вообще из двух частей - плиту "верстака для электроники" разрабатываешь заранее. А крепёжки быстро печатаются индивидуально. Сейчас вяло пытаюсь совместить с ящиком "rugged box", чтоб были сменные панели под дисплеи и управление наверху и кабельные вводы/разъемы/тестпоинты по бокам. В моменте дофига времени потратишь, но потом будешь как белый человек работать...

 "выросты"-держатели и прочие защёлки любым пластиком - это сразу на слом,

Делайте "пружину" вдоль слоев и ничего ломаться не будет. Ну и чем больше вы заморачиваетесь и практикуетесь, тем быстрее потом проектируются сложные вещи. Тоже самое вначале страдаешь - потом работаешь как белый человек.

Сам по себе такой таймлайн должен отсутствовать полностью если хотим что-то сделать.

Это если инженерный подход. А здесь наука, вайб, творчество. Например, можно сделать промпт-песенку, распознать ноты и чтобы модель дала игривый код с весёлыми комментариями. Поводить хоровод, распознать, сплясать, по следованию координат задать например то, как будет сконфигурирован модуль ШИМ, 10 кГц это будет или 11, односторонняя пила или симметричная, можно показать ладошками какую хотим. Больше нет угрюмых разработок.

Можете свой ИИ попросить расширить скрипт конвертации ttf в c/h для генерации русских букв (напрмер в кодировке 1251) ?
Скажите ему: Заранее спасибо.

Да не вопрос! Промпт-инъекция заняла где-то полминуты, далее отладка, ещё раз обратная связь и через 1.5 минуты всё готово! Шрифты берёт из стандартных папок Линукса, можно использовать любые другие с набором Юникод но содержащих всякие смайлики из CP-866, особенно рисование таблиц как в DOS, их можно разместить от 0 до "!" где управляющие последовательности идут.

Вайб-кириллица CP1251
Саммари
# Сводка по gen5.py: Генерация шрифтов CP1251 для LCD-дисплеев

Этот скрипт `gen5.py` предназначен для генерации монохромных битовых шрифтов (bitmap fonts) в формате C-массивов для использования на микроконтроллерах, отображающих символы в кодировке CP1251 (кириллица). Он создает как файлы исходного кода, так и визуальные превью.

## Структура данных и алгоритмы

### 1. Основные данные

*   **`FONT_PATHS_TRY`, `SYMBOL_FONT_PATHS_TRY`**: Списки путей к TrueType-шрифтам (TTF) для основного текста и специальных символов (управляющие коды). Скрипт последовательно пытается загрузить первый доступный шрифт из списка.
*   **`CONTROL_CHAR_MAP`**: Словарь, сопоставляющий управляющие коды ASCII (0x00-0x20) с визуально понятными Unicode-символами (например, 0x00 -> ∅, 0x08 -> ◘). Это позволяет отображать невидимые управляющие символы на превью и в комментариях.
*   **`DESCENDER_CHARS`**: Множество символов (латинских и кириллических), которые имеют "нисходящие элементы" (спускающиеся ниже основной линии, как у букв 'g', 'y', 'д', 'щ').

### 2. Процесс генерации шрифта

Алгоритм работает пошагово для каждого из 256 символов (0x00-0xFF):

1.  **Загрузка шрифта:** Используя функцию `load_linux_font`, скрипт загружает основной и (опционально) символический шрифт. Если шрифт по указанному пути не найден, он пытается использовать системный шрифт по умолчанию.
2.  **Определение символа:** Для каждого кода определяется, какой символ отображать. Для кодов 0x00-0x20 используется `CONTROL_CHAR_MAP`. Для остальных кодов производится попытка декодировать байт в строку CP1251.
3.  **Вычисление границ (`textbbox` и `getmask`):** Для определения габаритов символа используются два метода:
    *   `textbbox`: Возвращает ограничивающий прямоугольник на основе метрик шрифта.
    *   `getmask().getbbox()`: Возвращает ограничивающий прямоугольник, основанный на реальных пикселях (маске) символа. Это более точный "пиксель-перфект" метод.
    При включенном флаге `USE_DUAL_BBOX` предпочтение отдается маске, что обеспечивает более точную центровку.
4.  **Выравнивание и смещение:** Символ позиционируется внутри своей ячейки (размером `WIDTH` x `HEIGHT`).
    *   **Горизонтальное выравнивание:** Производится по центру.
    *   **Вертикальное выравнивание:** Определяется константой `V_ALIGN` ("TOP", "CENTER", "BOTTOM").
    *   **Тонкая настройка:** К символам без спускающегося элемента применяется смещение `NON_DESCENDER_OFFSET`, а к символам с таким элементом — относительное смещение `DESCENDER_RELATIVE_OFFSET`.
5.  **Отрисовка:** Символ отрисовывается на изображении с помощью `ImageDraw.Draw.text()`.
6.  **Применение инверсии:** Если включены флаги `FLIP_HORIZONTAL` или `FLIP_VERTICAL`, готовое изображение символа отражается по горизонтали или вертикали. Это полезно для адаптации шрифта к аппаратной ориентации дисплея.
7.  **Преобразование в байты:** Изображение символа (ширина x высота) преобразуется в последовательность байтов. Каждый байт представляет 8 пикселей, считываемых по столбцам (vertical-major order). Это формирует битовый массив для C-кода.

## Генерация выходных файлов

1.  **`display_fonts.c`**: Генерирует массив `const uint8_t fontCRC[]`, содержащий битовые данные для всех 256 символов. Каждая строка содержит байты для одного символа, сопровождаемые комментарием, в котором указан индекс, шестнадцатеричный код и отображаемый символ.
2.  **`fontdef.h`**: Генерирует заголовочный файл с константами, описывающими шрифт (ширина, высота, количество байт на столбец, выравнивание, флаги отражения). Это позволяет легко настраивать отображение шрифта в C-коде.
3.  **`font_preview.png`**: Создает атлас (atlas) — одно большое изображение, на котором все 256 символов расположены в сетке 16x16. Это позволяет визуально проверить результат генерации.

## Ключевые особенности и маски

*   **Использование масок (`USE_DUAL_BBOX`):** Этот флаг является ключевым для качества шрифта. Использование маски (`getmask`) вместо метрик шрифта (`textbbox`) позволяет избежать "плавающих" символов и обеспечивает единообразную высоту строки.
*   **Тонкая настройка позиционирования:** Константы `NON_DESCENDER_OFFSET` и `DESCENDER_RELATIVE_OFFSET` позволяют вручную подгонять вертикальное положение символов, что критично для эстетики моноширинного шрифта.
*   **Отображение управляющих символов:** Благодаря `CONTROL_CHAR_MAP` и процедурной отрисовке (`draw_procedural_fallback`), управляющие коды (которые в обычном тексте невидимы) получают собственное визуальное представление, что делает превью и C-код более информативными.
*   **Поддержка аппаратного отражения:** Флаги `FLIP_HORIZONTAL` и `FLIP_VERTICAL` позволяют адаптировать шрифт к дисплею, установленному в нестандартной ориентации, не изменяя при этом C-код отображения.

В целом, `gen5.py` представляет собой мощный и гибкий инструмент для создания настраиваемых шрифтов, сочетающий автоматизацию с возможностью точной ручной подстройки.
Питон-генератор
import os
import math
from PIL import Image, ImageFont, ImageDraw

# ==================== CONFIGURATION ====================
# Common Linux primary fonts with CP1251 support (Courier, Liberation Mono, DejaVu)
FONT_PATHS_TRY = [
    "cour.ttf",
    "/usr/share/fonts/truetype/liberation/LiberationMono-Regular.ttf",
    "/usr/share/fonts/truetype/dejavu/DejaVuSansMono.ttf",
    "/usr/share/fonts/truetype/msttcorefonts/Courier_New.ttf"
]

# Common Linux symbol fonts for control characters
SYMBOL_FONT_PATHS_TRY = [
    "seguiisb.ttf",
    "/usr/share/fonts/truetype/dejavu/DejaVuSans.ttf",
    "/usr/share/fonts/truetype/freefont/FreeSans.ttf",
    "/usr/share/fonts/truetype/msttcorefonts/Arial.ttf"
]

FONT_SIZE = 12                 # Font size to render
WIDTH, HEIGHT = 9, 14          # Grid cell width and height
COLS = 16                      # Number of columns in the PNG preview grid

# --- Alignment & Typographical Tuning ---
V_ALIGN = "BOTTOM"             # Base alignment strategy: "TOP", "CENTER", or "BOTTOM"
USE_DUAL_BBOX = True           # True: Use pixel-perfect masks | False: Use font-engine metrics

# --- Fine-Grained Fine Tuning Controls ---
NON_DESCENDER_OFFSET = -2      # Shift normal chars (+ down, - up)
DESCENDER_RELATIVE_OFFSET = -1 # Relative shift for descenders (+ down, - up)

# CP1251 Descenders (Includes Latin and Cyrillic tails like ц, щ, д)
DESCENDER_CHARS = set("gjpqy,;QцщдйЦЩДЙ") 

# --- Hardware Orientation Inversion Toggles ---
FLIP_HORIZONTAL = False        # Mirror Left-to-Right
FLIP_VERTICAL = False          # Mirror Top-to-Bottom

# --- Preview Customization ---
APPLY_FLIPS_TO_PREVIEW = False # True: preview mimics H/W mirror | False: normal text preview
# =======================================================

# Mapping table for ASCII control codes (0x00 to 0x20) to meaningful Unicode symbols/smilies
CONTROL_CHAR_MAP = {
    0x00: "\u2205", 0x01: "\u263A", 0x02: "\u263B", 0x03: "\u2665", 0x04: "\u2666", 0x05: "\u2663", 0x06: "\u2660", 0x07: "\u2022",
    0x08: "\u25D8", 0x09: "\u25CB", 0x0A: "\u25D9", 0x0B: "\u2642", 0x0C: "\u2640", 0x0D: "\u266A", 0x0E: "\u266B", 0x0F: "\u263C",
    0x10: "\u25BA", 0x11: "\u25C4", 0x12: "\u2195", 0x13: "\u203C", 0x14: "\u00B6", 0x15: "\u00A7", 0x16: "\u25AC", 0x17: "\u21A8",
    0x18: "\u2191", 0x19: "\u2193", 0x1A: "\u2192", 0x1B: "\u2190", 0x1C: "\u221F", 0x1D: "\u2194", 0x1E: "\u25B2", 0x1F: "\u25BC",
    0x20: "\u2423"
}

def load_linux_font(font_paths):
    for path in font_paths:
        try:
            if os.path.exists(path) or "/" not in path:
                return ImageFont.truetype(path, FONT_SIZE), False
        except Exception:
            continue
    return ImageFont.load_default(), True

def draw_procedural_fallback(draw, code, w, h):
    """Draws pixel-perfect fallbacks for control codes if symbol fonts are missing."""
    if code == 0x00 or code == 0x20:
        draw.rectangle([0, 2, w - 1, h - 3], outline=1)
    else:
        draw.line([1, 3, w - 2, h - 4], fill=1)
        draw.line([1, h - 4, w - 2, 3], fill=1)

def build_visual_matrix_comment():
    """Generates a text-based 16x16 lookup matrix using the actual mapped characters and symbols."""
    matrix_lines = []
    matrix_lines.append(" *      0  1  2  3  4  5  6  7  8  9  A  B  C  D  E  F")
    matrix_lines.append(" *    +-----------------------------------------------")
    
    for row in range(16):
        row_chars = []
        for col in range(16):
            code = row * 16 + col
            if code <= 0x20:
                # Use the real symbol mapped for the control character region
                char_str = CONTROL_CHAR_MAP[code]
            else:
                try:
                    char_str = bytes([code]).decode('cp1251')
                except Exception:
                    char_str = '?'
            
            # Clean display properties to keep layout intact while preventing broken comments
            if char_str in ['/', '*', '\\'] or ord(char_str) < 32:
                char_str = '?'
                
            row_chars.append(f" {char_str} ")
            
        matrix_lines.append(f" * {row:X}x |{''.join(row_chars)}")
    return "\n".join(matrix_lines)

def generate_font_assets():
    font_primary, primary_is_fallback = load_linux_font(FONT_PATHS_TRY)
    font_symbol, symbol_is_fallback = load_linux_font(SYMBOL_FONT_PATHS_TRY)

    if primary_is_fallback:
        print("Warning: All primary fonts failed. Using low-res default system font.")
    if symbol_is_fallback and not primary_is_fallback:
        font_symbol = font_primary
        print("Notice: Explicit symbol font missing. Overlapping allocations to primary engine.")

    rows = 16  
    atlas = Image.new('1', (COLS * WIDTH, rows * HEIGHT), 0)
    bytes_per_col = math.ceil(HEIGHT / 8)
    
    # Generate the visual reference map string using the updated function
    visual_map = build_visual_matrix_comment()
    
    c_lines = [
        '#include "fontdef.h"',
        '#include <stdint.h>',
        '',
        '/* ',
        ' * Linux-Optimized CP1251 Font Data (0-255)',
        f' * Configuration - H-Flip: {FLIP_HORIZONTAL}, V-Flip: {FLIP_VERTICAL}, V-Align: {V_ALIGN}',
        ' *',
        ' * 16x16 SYMBOL TABLE REFERENCE MAP:',
        visual_map,
        ' */',
        'const uint8_t fontCRC [] = {'
    ]

    for code in range(256):
        char_img = Image.new('1', (WIDTH, HEIGHT), 0)
        draw = ImageDraw.Draw(char_img)
        
        use_procedural = (code <= 0x20) and symbol_is_fallback and primary_is_fallback
        
        if use_procedural:
            draw_procedural_fallback(draw, code, WIDTH, HEIGHT)
            char_str = CONTROL_CHAR_MAP[code]
        else:
            active_font = font_primary
            if code <= 0x20:
                char_str = CONTROL_CHAR_MAP[code]
                active_font = font_symbol
            else:
                try:
                    char_str = bytes([code]).decode('cp1251')
                except UnicodeDecodeError:
                    char_str = '?'

            m_left, m_top, m_right, m_bottom = draw.textbbox((0, 0), char_str, font=active_font, anchor="lt")
            mask_tuple = active_font.getmask(char_str).getbbox() if USE_DUAL_BBOX else None
            
            if USE_DUAL_BBOX and mask_tuple:
                b_left, b_top, b_right, b_bottom = mask_tuple
            else:
                b_left, b_top, b_right, b_bottom = m_left, m_top, m_right, m_bottom
            
            tw = b_right - b_left
            th = b_bottom - b_top
            
            if tw <= 0 or th <= 0:
                x_off, y_off = 0, 0
            else:
                x_off = math.floor((WIDTH - tw) / 2.0) - b_left
                align = V_ALIGN.upper()
                if align == "TOP":
                    base_y = 0 - b_top
                elif align == "BOTTOM":
                    base_y = (HEIGHT - th) - b_top
                else:
                    base_y = math.floor((HEIGHT - th) / 2.0) - b_top
                    
                if char_str in DESCENDER_CHARS:
                    y_off = base_y + DESCENDER_RELATIVE_OFFSET
                else:
                    y_off = base_y + NON_DESCENDER_OFFSET

            draw.text((x_off, y_off), char_str, font=active_font, fill=1, anchor="lt")
        
        preview_img = char_img.copy()
        
        if FLIP_HORIZONTAL:
            char_img = char_img.transpose(Image.FLIP_LEFT_RIGHT)
        if FLIP_VERTICAL:
            char_img = char_img.transpose(Image.FLIP_TOP_BOTTOM)
        
        if APPLY_FLIPS_TO_PREVIEW:
            if FLIP_HORIZONTAL:
                preview_img = preview_img.transpose(Image.FLIP_LEFT_RIGHT)
            if FLIP_VERTICAL:
                preview_img = preview_img.transpose(Image.FLIP_TOP_BOTTOM)
                
        atlas.paste(preview_img, ((code % COLS) * WIDTH, (code // COLS) * HEIGHT))
        
        pixels = char_img.load()
        hex_values = []
        
        for x in range(WIDTH):
            for b_idx in range(bytes_per_col):
                byte_val = 0
                start_y = b_idx * 8
                for bit in range(8):
                    target_y = start_y + bit
                    if target_y < HEIGHT and pixels[x, target_y]:
                        byte_val |= (1 << bit)
                hex_values.append(f"0x{byte_val:02X}")
        
        escaped_char = char_str.replace('\\', '\\\\').replace('/*', '/ *').replace('*/', '* /')
        c_lines.append(f"    {', '.join(hex_values)}, /* Index {code:03d} (0x{code:02X}) -> '{escaped_char}' */")

    c_lines.append("};")
    
    with open("display_fonts.c", "w", encoding="utf-8") as f:
        f.write("\n".join(c_lines) + "\n")
        
    valign_flag = 1 if V_ALIGN.upper() == "BOTTOM" else 0
    h_lines = [
        "#ifndef FONTDEF_H",
        "#define FONTDEF_H",
        "",
        "#include <stdint.h>",
        "",
        f"#define CRCFONT_WIDTH           {WIDTH}",
        f"#define CRCFONT_HEIGHT          {HEIGHT}",
        f"#define CRCFONT_BYTES_PER_COL   {bytes_per_col}",
        f"#define CRCFONT_BYTES_PER_CHAR  {WIDTH * bytes_per_col}",
        "#define CRCFONT_FIRSTCHAR       0",
        "#define CRCFONT_LASTCHAR        255",
        "",
        f"#define CRCFONT_VALIGN_BOTTOM     {valign_flag}",
        f"#define CRCFONT_VFLIP             {1 if FLIP_VERTICAL else 0}",
        f"#define CRCFONT_HFLIP             {1 if FLIP_HORIZONTAL else 0}",
        "",
        "extern const uint8_t fontCRC [];",
        "",
        "#endif"
    ]
    
    with open("fontdef.h", "w", encoding="utf-8") as f:
        f.write("\n".join(h_lines) + "\n")
    
    atlas.resize((atlas.width * 4, atlas.height * 4), resample=Image.NEAREST).save("font_preview.png")
    print("Success! Mapped array data with matching unicode matrix header comments.")

if __name__ == "__main__":
    generate_font_assets()

Вечером проверю уже в железе. Результат:

Сгенерированные шрифты

Че-то кривовато. Ладно кириллица, но даже у латинских символов размер и выравнивание скачут.

В иишенке тоже желтки не по уровню поджариваются.

Это исправимо, чуть позже вот версия с управлением паддингом для каждого символа. Обратите внимание - ИИ сделал всё верно и выделил символы, которые необходимо настроить. Однако делать это надо не оптом а отдельно. Вот исправленная версия с настройкой паддинга для избранных символов.
В принципе можно дать более сложную задачу на уровне распознавания нижней линии, но это уже факультатив. Сейчас пока конвертация идёт просто по bounding box, хотя более правильно - по образующему прямоугольнику без учёта диакритических знаков "Щщ" а также имеющих центральные линии вроде "ф", вообщем это уже особенности конкретной буквы которую не лень поправить вручную.
Делаем промпт: add to gen5 DESCENDER_CHARS so each char have personal Descenders list (q,1), (ц,-1), (ц,0) and so on -1 by default. give it for "gjpqQyY,;дДЦцЩщЙйфФуУ"

Исправляем паддинг для отдельных символов
Саммари для генерации Питона
# Сводка по gen5.py: Генерация шрифтов CP1251 для LCD-дисплеев

Этот скрипт `gen5.py` предназначен для генерации монохромных битовых шрифтов (bitmap fonts) в формате C-массивов для использования на микроконтроллерах, отображающих символы в кодировке CP1251 (кириллица). Он создает как файлы исходного кода, так и визуальные превью.

## Структура данных и алгоритмы

### 1. Основные данные

*   **`FONT_PATHS_TRY`, `SYMBOL_FONT_PATHS_TRY`**: Списки путей к TrueType-шрифтам (TTF) для основного текста и специальных символов (управляющие коды). Скрипт последовательно пытается загрузить первый доступный шрифт из списка.
*   **`CONTROL_CHAR_MAP`**: Словарь, сопоставляющий управляющие коды ASCII (0x00-0x20) с визуально понятными Unicode-символами (например, 0x00 -> ∅, 0x08 -> ◘). Это позволяет отображать невидимые управляющие символы на превью и в комментариях.
*   **`CHAR_DESCENDER_OFFSETS`**: Словарь, который заменил собой множество `DESCENDER_CHARS`. Каждому символу, имеющему "нисходящий элемент" (спускающийся ниже основной линии), сопоставляется его персональное вертикальное смещение (например, 'g': -1, 'p': 0). Это позволяет точно настроить положение каждого такого символа.

### 2. Процесс генерации шрифта

Алгоритм работает пошагово для каждого из 256 символов (0x00-0xFF):

1.  **Загрузка шрифта:** Используя функцию `load_linux_font`, скрипт загружает основной и (опционально) символический шрифт. Если шрифт по указанному пути не найден, он пытается использовать системный шрифт по умолчанию.
2.  **Определение символа:** Для каждого кода определяется, какой символ отображать. Для кодов 0x00-0x20 используется `CONTROL_CHAR_MAP`. Для остальных кодов производится попытка декодировать байт в строку CP1251.
3.  **Вычисление границ (`textbbox` и `getmask`):** Для определения габаритов символа используются два метода:
    *   `textbbox`: Возвращает ограничивающий прямоугольник на основе метрик шрифта.
    *   `getmask().getbbox()`: Возвращает ограничивающий прямоугольник, основанный на реальных пикселях (маске) символа. Это более точный "пиксель-перфект" метод.
    При включенном флаге `USE_DUAL_BBOX` предпочтение отдается маске, что обеспечивает более точную центровку.
4.  **Выравнивание и смещение:** Символ позиционируется внутри своей ячейки (размером `WIDTH` x `HEIGHT`).
    *   **Горизонтальное выравнивание:** Производится по центру.
    *   **Вертикальное выравнивание:** Определяется константой `V_ALIGN` ("TOP", "CENTER", "BOTTOM").
    *   **Тонкая настройка:** Для символов с "нисходящим элементом" используется индивидуальное смещение из словаря `CHAR_DESCENDER_OFFSETS`. Если символ отсутствует в словаре, применяется смещение по умолчанию (-1). Для символов без такого элемента применяется смещение `NON_DESCENDER_OFFSET`.
5.  **Отрисовка:** Символ отрисовывается на изображении с помощью `ImageDraw.Draw.text()`.
6.  **Применение инверсии:** Если включены флаги `FLIP_HORIZONTAL` или `FLIP_VERTICAL`, готовое изображение символа отражается по горизонтали или вертикали. Это полезно для адаптации шрифта к аппаратной ориентации дисплея.
7.  **Преобразование в байты:** Изображение символа (ширина x высота) преобразуется в последовательность байтов. Каждый байт представляет 8 пикселей, считываемых по столбцам (vertical-major order). Это формирует битовый массив для C-кода.

## Генерация выходных файлов

1.  **`display_fonts.c`**: Генерирует массив `const uint8_t fontCRC[]`, содержащий битовые данные для всех 256 символов. Каждая строка содержит байты для одного символа, сопровождаемые комментарием, в котором указан индекс, шестнадцатеричный код и отображаемый символ.
2.  **`fontdef.h`**: Генерирует заголовочный файл с константами, описывающими шрифт (ширина, высота, количество байт на столбец, выравнивание, флаги отражения). Это позволяет легко настраивать отображение шрифта в C-коде.
3.  **`font_preview.png`**: Создает атлас (atlas) — одно большое изображение, на котором все 256 символов расположены в сетке 16x16. Это позволяет визуально проверить результат генерации.

## Ключевые особенности и маски

*   **Использование масок (`USE_DUAL_BBOX`):** Этот флаг является ключевым для качества шрифта. Использование маски (`getmask`) вместо метрик шрифта (`textbbox`) позволяет избежать "плавающих" символов и обеспечивает единообразную высоту строки.
*   **Тонкая настройка позиционирования:** Константы `NON_DESCENDER_OFFSET` и индивидуальные смещения в `CHAR_DESCENDER_OFFSETS` позволяют вручную подгонять вертикальное положение символов с высокой точностью, что критично для эстетики моноширинного шрифта.
*   **Отображение управляющих символов:** Благодаря `CONTROL_CHAR_MAP` и процедурной отрисовке (`draw_procedural_fallback`), управляющие коды (которые в обычном тексте невидимы) получают собственное визуальное представление, что делает превью и C-код более информативными.
*   **Поддержка аппаратного отражения:** Флаги `FLIP_HORIZONTAL` и `FLIP_VERTICAL` позволяют адаптировать шрифт к дисплею, установленному в нестандартной ориентации, не изменяя при этом C-код отображения.

В целом, `gen5.py` представляет собой мощный и гибкий инструмент для создания настраиваемых шрифтов, сочетающий автоматизацию с возможностью точной ручной подстройки.
Mr Питон как есть
import os
import math
from PIL import Image, ImageFont, ImageDraw

# ==================== CONFIGURATION ====================
# Common Linux primary fonts with CP1251 support (Courier, Liberation Mono, DejaVu)
FONT_PATHS_TRY = [
    "cour.ttf",
    "/usr/share/fonts/truetype/liberation/LiberationMono-Regular.ttf",
    "/usr/share/fonts/truetype/dejavu/DejaVuSansMono.ttf",
    "/usr/share/fonts/truetype/msttcorefonts/Courier_New.ttf"
]

# Common Linux symbol fonts for control characters
SYMBOL_FONT_PATHS_TRY = [
    "seguiisb.ttf",
    "/usr/share/fonts/truetype/dejavu/DejaVuSans.ttf",
    "/usr/share/fonts/truetype/freefont/FreeSans.ttf",
    "/usr/share/fonts/truetype/msttcorefonts/Arial.ttf"
]

FONT_SIZE = 12                 # Font size to render
WIDTH, HEIGHT = 9, 14          # Grid cell width and height
COLS = 16                      # Number of columns in the PNG preview grid

# --- Alignment & Typographical Tuning ---
V_ALIGN = "BOTTOM"             # Base alignment strategy: "TOP", "CENTER", or "BOTTOM"
USE_DUAL_BBOX = True           # True: Use pixel-perfect masks | False: Use font-engine metrics

# --- Fine-Grained Fine Tuning Controls ---
NON_DESCENDER_OFFSET = -3      # Shift normal chars (+ down, - up)
DESCENDER_RELATIVE_OFFSET = -1 # Relative shift for descenders (+ down, - up)

# Descender characters with their personal vertical offset (applied relative to base alignment)
# Default offset for new descenders is -1
CHAR_DESCENDER_OFFSETS = {
    'g': -1, 'j': 0, 'p':0, 'q': 0, 'Q': 0, 'y': 0, 
    'Y': -1, ',': -1, ';': -1, 'Ж': 0, 'ж': 0,
    'д': 0, 'Д': 1, 'ц': 0, 'Ц': 1, 'щ': 0, 'Щ': 1, 
    'Й': -1, 'й': -1, 'ф': 0, 'Ф': -1,  'у': 0,  'р': 0
}

# --- Hardware Orientation Inversion Toggles ---
FLIP_HORIZONTAL = False        # Mirror Left-to-Right
FLIP_VERTICAL = False          # Mirror Top-to-Bottom

# --- Preview Customization ---
APPLY_FLIPS_TO_PREVIEW = False # True: preview mimics H/W mirror | False: normal text preview
# =======================================================

# Mapping table for ASCII control codes (0x00 to 0x20) to meaningful Unicode symbols/smilies
CONTROL_CHAR_MAP = {
    0x00: "\u2205", 0x01: "\u263A", 0x02: "\u263B", 0x03: "\u2665", 0x04: "\u2666", 0x05: "\u2663", 0x06: "\u2660", 0x07: "\u2022",
    0x08: "\u25D8", 0x09: "\u25CB", 0x0A: "\u25D9", 0x0B: "\u2642", 0x0C: "\u2640", 0x0D: "\u266A", 0x0E: "\u266B", 0x0F: "\u263C",
    0x10: "\u25BA", 0x11: "\u25C4", 0x12: "\u2195", 0x13: "\u203C", 0x14: "\u00B6", 0x15: "\u00A7", 0x16: "\u25AC", 0x17: "\u21A8",
    0x18: "\u2191", 0x19: "\u2193", 0x1A: "\u2192", 0x1B: "\u2190", 0x1C: "\u221F", 0x1D: "\u2194", 0x1E: "\u25B2", 0x1F: "\u25BC",
    0x20: "\u2423"
}

def load_linux_font(font_paths):
    for path in font_paths:
        try:
            if os.path.exists(path) or "/" not in path:
                return ImageFont.truetype(path, FONT_SIZE), False
        except Exception:
            continue
    return ImageFont.load_default(), True

def draw_procedural_fallback(draw, code, w, h):
    """Draws pixel-perfect fallbacks for control codes if symbol fonts are missing."""
    if code == 0x00 or code == 0x20:
        draw.rectangle([0, 2, w - 1, h - 3], outline=1)
    else:
        draw.line([1, 3, w - 2, h - 4], fill=1)
        draw.line([1, h - 4, w - 2, 3], fill=1)

def build_visual_matrix_comment():
    """Generates a text-based 16x16 lookup matrix using the actual mapped characters and symbols."""
    matrix_lines = []
    matrix_lines.append(" *      0  1  2  3  4  5  6  7  8  9  A  B  C  D  E  F")
    matrix_lines.append(" *    +-----------------------------------------------")
    
    for row in range(16):
        row_chars = []
        for col in range(16):
            code = row * 16 + col
            if code <= 0x20:
                # Use the real symbol mapped for the control character region
                char_str = CONTROL_CHAR_MAP[code]
            else:
                try:
                    char_str = bytes([code]).decode('cp1251')
                except Exception:
                    char_str = '?'
            
            # Clean display properties to keep layout intact while preventing broken comments
            if char_str in ['/', '*', '\\'] or ord(char_str) < 32:
                char_str = '?'
                
            row_chars.append(f" {char_str} ")
            
        matrix_lines.append(f" * {row:X}x |{''.join(row_chars)}")
    return "\n".join(matrix_lines)

def generate_font_assets():
    font_primary, primary_is_fallback = load_linux_font(FONT_PATHS_TRY)
    font_symbol, symbol_is_fallback = load_linux_font(SYMBOL_FONT_PATHS_TRY)

    if primary_is_fallback:
        print("Warning: All primary fonts failed. Using low-res default system font.")
    if symbol_is_fallback and not primary_is_fallback:
        font_symbol = font_primary
        print("Notice: Explicit symbol font missing. Overlapping allocations to primary engine.")

    rows = 16  
    atlas = Image.new('1', (COLS * WIDTH, rows * HEIGHT), 0)
    bytes_per_col = math.ceil(HEIGHT / 8)
    
    # Generate the visual reference map string using the updated function
    visual_map = build_visual_matrix_comment()
    
    c_lines = [
        '#include "fontdef.h"',
        '#include <stdint.h>',
        '',
        '/* ',
        ' * Linux-Optimized CP1251 Font Data (0-255)',
        f' * Configuration - H-Flip: {FLIP_HORIZONTAL}, V-Flip: {FLIP_VERTICAL}, V-Align: {V_ALIGN}',
        ' *',
        ' * 16x16 SYMBOL TABLE REFERENCE MAP:',
        visual_map,
        ' */',
        'const uint8_t fontCRC [] = {'
    ]

    for code in range(256):
        char_img = Image.new('1', (WIDTH, HEIGHT), 0)
        draw = ImageDraw.Draw(char_img)
        
        use_procedural = (code <= 0x20) and symbol_is_fallback and primary_is_fallback
        
        if use_procedural:
            draw_procedural_fallback(draw, code, WIDTH, HEIGHT)
            char_str = CONTROL_CHAR_MAP[code]
        else:
            active_font = font_primary
            if code <= 0x20:
                char_str = CONTROL_CHAR_MAP[code]
                active_font = font_symbol
            else:
                try:
                    char_str = bytes([code]).decode('cp1251')
                except UnicodeDecodeError:
                    char_str = '?'

            m_left, m_top, m_right, m_bottom = draw.textbbox((0, 0), char_str, font=active_font, anchor="lt")
            mask_tuple = active_font.getmask(char_str).getbbox() if USE_DUAL_BBOX else None
            
            if USE_DUAL_BBOX and mask_tuple:
                b_left, b_top, b_right, b_bottom = mask_tuple
            else:
                b_left, b_top, b_right, b_bottom = m_left, m_top, m_right, m_bottom
            
            tw = b_right - b_left
            th = b_bottom - b_top
            
            if tw <= 0 or th <= 0:
                x_off, y_off = 0, 0
            else:
                x_off = math.floor((WIDTH - tw) / 2.0) - b_left
                align = V_ALIGN.upper()
                if align == "TOP":
                    base_y = 0 - b_top
                elif align == "BOTTOM":
                    base_y = (HEIGHT - th) - b_top
                else:
                    base_y = math.floor((HEIGHT - th) / 2.0) - b_top
                    
                # Get personal offset if char is in the descender map, otherwise use the default (-1)
                personal_offset = CHAR_DESCENDER_OFFSETS.get(char_str, -1)
                y_off = base_y + personal_offset

            draw.text((x_off, y_off), char_str, font=active_font, fill=1, anchor="lt")
        
        preview_img = char_img.copy()
        
        if FLIP_HORIZONTAL:
            char_img = char_img.transpose(Image.FLIP_LEFT_RIGHT)
        if FLIP_VERTICAL:
            char_img = char_img.transpose(Image.FLIP_TOP_BOTTOM)
        
        if APPLY_FLIPS_TO_PREVIEW:
            if FLIP_HORIZONTAL:
                preview_img = preview_img.transpose(Image.FLIP_LEFT_RIGHT)
            if FLIP_VERTICAL:
                preview_img = preview_img.transpose(Image.FLIP_TOP_BOTTOM)
                
        atlas.paste(preview_img, ((code % COLS) * WIDTH, (code // COLS) * HEIGHT))
        
        pixels = char_img.load()
        hex_values = []
        
        for x in range(WIDTH):
            for b_idx in range(bytes_per_col):
                byte_val = 0
                start_y = b_idx * 8
                for bit in range(8):
                    target_y = start_y + bit
                    if target_y < HEIGHT and pixels[x, target_y]:
                        byte_val |= (1 << bit)
                hex_values.append(f"0x{byte_val:02X}")
        
        escaped_char = char_str.replace('\\', '\\\\').replace('/*', '/ *').replace('*/', '* /')
        c_lines.append(f"    {', '.join(hex_values)}, /* Index {code:03d} (0x{code:02X}) -> '{escaped_char}' */")

    c_lines.append("};")
    
    with open("display_fonts.c", "w", encoding="utf-8") as f:
        f.write("\n".join(c_lines) + "\n")
        
    valign_flag = 1 if V_ALIGN.upper() == "BOTTOM" else 0
    h_lines = [
        "#ifndef FONTDEF_H",
        "#define FONTDEF_H",
        "",
        "#include <stdint.h>",
        "",
        f"#define CRCFONT_WIDTH           {WIDTH}",
        f"#define CRCFONT_HEIGHT          {HEIGHT}",
        f"#define CRCFONT_BYTES_PER_COL   {bytes_per_col}",
        f"#define CRCFONT_BYTES_PER_CHAR  {WIDTH * bytes_per_col}",
        "#define CRCFONT_FIRSTCHAR       0",
        "#define CRCFONT_LASTCHAR        255",
        "",
        f"#define CRCFONT_VALIGN_BOTTOM     {valign_flag}",
        f"#define CRCFONT_VFLIP             {1 if FLIP_VERTICAL else 0}",
        f"#define CRCFONT_HFLIP             {1 if FLIP_HORIZONTAL else 0}",
        "",
        "extern const uint8_t fontCRC [];",
        "",
        "#endif"
    ]
    
    with open("fontdef.h", "w", encoding="utf-8") as f:
        f.write("\n".join(h_lines) + "\n")
    
    atlas.resize((atlas.width * 4, atlas.height * 4), resample=Image.NEAREST).save("font_preview.png")
    print("Success! Mapped array data with matching unicode matrix header comments.")

if __name__ == "__main__":
    generate_font_assets()

Результат

Теперь символы выровнены.

Следующее - имеются буквы которые прописные но по габаритам как заглавные. Для них можно использовать: 1. Уменьшение размера шрифта для выбранных букв. 2. Сжатие по вертикали и бикубическая интерполяция после растеризации, однако, это может быть кривоватым. 3. Алгоритмическое попиксельное сжатие двоички относительно середины (вырезка пикселей с учётом уголков как в Тетрисе). 4. Симметризатор симметричных символов для чётного количества пикселей (где центральная линия всегда будет смещена). Это и есть настоящий вайб, когда сосредотачиваешься на результате а не то как его накодить.

Вы вроде как явно указали, что надо выровнять латинскую прописную y. И все равно она не выровнена. q выровнена, но не в ту сторону.

Я тоже пользуюсь агентами для разработки, но если вы что-то на публику выкладываете, причешите хоть маленько.

Так всё верно, если "q" и "y" ещё ниже опустить она будет кропнутой. Поэтому я и указал выше что помимо паддинга должен быть скейл таких вот символов по вертикали с корректным сжатием относительно выбранной точки, например где у буквы "p" язычок или у буквы "q" засечка. Иначе будет неэффективное использование символ-места на дисплее, когда 3-4 пикселя будут под игры +- паддинг. Это на обычном мониторе 1-й или полуторный интервал - норма, для эмбеддед дисплеев это роскошь. То есть ввести ещё атрибуты в словарь - буква содержит паддинг, скейл по вертикали в виде количества вырезаемых пикселей и смещение по которому производится отрезка. И тогда будет уже подходящий генератор и по вертикали не будет много пустого места. Обычно 2 пикселя более чем достаточно для таких вот сжатых шрифтов в ограниченном пространстве. В любом случае предварительно сгенерить и потом допилить атлас пейнтом проще чем чертить с нуля. Кстати обратное преобразование атласа в битмап - это также того же порядка задачка. Ну или пальнуть Pytorch-ем с обучением оного выделять завитушки и их преобразовывать, датасеты тоже навайбить представлениями об эстетике буквы.
По поводу использования агентов - тут стоит задача максимум - не редактировать код вручную от слова вообще - максимум копипастить в промпт переменные или указывать номер строки где уж явный глюк или не понимание. То есть вся задача обрисовывается скиллами, MCP-шкой, локальным эмбеддером для поиска (RAG), а на выходе код на сотни байт для железа.
P.S. Ну раз уж за красоту - придётся доделать генератор со всеми замечаниями. Плюс ещё вайб-транслятор из Unicode-строк в функцию вывода на дисплей на C, так как не охота ковырять VSCode настройками файлов кодировками иными чем utf-8.

Sign up to leave a comment.

Articles