SSD1306 пришел, просто тут мемоизирую этот факт.
О сколько нам открытий чудных...
На выходных наконец то реализовал первый шаг именно в сабже - сделал первый прототип управляющей программы для смартфона чтобы он мог служить глазами управляемого аппарата в виде трансляции видео с камеры смарта на управляющую программу в ноутбуке.
Ну как видео... получив поток кадров с камеры я прикинул, что несжатые мегабайты 1920x1024 в сеть просто не пролезут, поэтому надо жать.
Сжал категорично - сперва снизил разрешение самого preview mode до 640x480 и потом сжал его в JPEG с качеством 85% и сбрасываю кадры сии по сокету в управляющий ноутбук.
Окай.
На ноуте решил сделать максимально просто - складываю входящий поток байт в std::vector< char > и детектирую момент когда в начале его сформируется полностью полученный кадр - и как только так распаковываю его в JPEG (U++ молодец, есть встроенные классы делающие это дело банальным) и вывожу на форму после чего удаляю из начала массива полностью полученный и отображенный кадр и цикл повторяется.
И тут внезапно выяснилось, что хотя смарт довольно бодро скидывает кадры в районе 30 фпс, но ноут просто не успевает их обрабатывать - происходит переполнение очереди, ноут тормозит видео и лагает, поток отстаёт на секунды постоянно увеличивающиеся.
Сразу же я начал грешить на то, что телефон как то аппаратно умеет сжимать JPEG, а ноут просто не выдюживает софтовую реализацию (базирована на libjpeg) с такой же скоростью и всё проседает в переполнение.
Исследовал вопрос, погуглил более быстрые реализации, но в какой то момент догадался сделать тест без нагрузки - просто отключил распаковку и вывод JPEG и сразу удаляю полученный кадр из вектора и... и тормоза остались!
>8()
Как то я оказалось сильно сильно переоценивал скорость memcpy().
Переделал на очередь кадров где std::vector<> каждого кадра аллоцируется в момент получения его полной длины и таким образом деаллокация не вовлекает никаких копирований хвостов и о чудо - всё заработало на полной скорости.
Т.е. не распаковка JPEG его губила, а memcpy!!!
>8()
Вот это было максимально неожиданно.
P.S.
Типичный сжатый кадр занимает в среднем чуть меньше 100Кб для подробностей.
Надо, конечно, переходить на видеоформат.
Вообще, конечно, поуправляв дроном "Неуправляемый" с ноутбука через камеру я могу сказать, что надежды сделать ему автономное зрение с ориентировкой довольно поугасли. Это прям недостаток этого шасси - его моторы имеют пороговое напряжение срабатывания и не могут варьировать свою скорость от 0 до Vmax, а только от Vmin до Vmax и Vmin довольно большая величина - всего раза в два меньше Vmax. Таким образом даже минимальная скорость поворота на месте - довольно быстрая и соседние кадры уже так сильно смещены, что явно нетривиально было бы определение в каком направлении сместилась картинка - прям всю площадь изображения надо анализировать. Да и размыто всё сильно. Мдас...
Наверное надо искать другое шасси с шаговыми моторами с полным контролем насколько провернулись колёса чтобы вымерять и дистанции и повороты с нужной скоростью.
=A=L=X=
Попробуй ИИ распознавать картинки и что бы он указывал куда двигаться дальше.
master-sheff
> Попробуй ИИ распознавать картинки и что бы он указывал куда двигаться дальше.
Ну в этом и был план.
Надо бы вторую ветку проекта развивать уже действительно - тренировать ИИ, но тут я пока сильно плаваю, самое большое что делал - тренировал ИИ распознавать есть водяной знак на изображении или нет. В веб-магазине была такая задачка.
А тут прям не просто изображение распознавать, а их поток и главное что выходом должно быть не да/нет, а какие то управляющие сигналы пока даже непонятно как тут к тренировке подступиться, ведь результат должен быть чем... вот чем? Картой глубины что ли?
Ах да, я же давным давно приобрёл дисплей SSD1306 (монохром 128x64) и даже давно уже начал сооружать из него и ESP32 свою консоль.
Красавица выдалась на заглядение и корпусом и обводами дизайна:
Батарейный отсек с подложкой на 3Д-принтере печатал по собственным эскизам.
Так вот захотелось реализовать тайловый движок - чтобы скроллер делать с двумя фонами - один сзади непрозрачный, другой спереди с прозрачностью и спрайты там и сям.
И уже начал смотреть как устроен битмап в этом экране и прифигел.
Он выстроен вертикальными рядами!
На скрине заливка видеообласти нарастающими байтами.
Т.е. сперва идёт полоска из 128 байт - они высотой 8 пикселей и каждый просто являет собой вертикальную полоску и они встык покрывают первые 128x8 пикселей, дальше схема линейно продолжается.
Я честно говоря опешил поначалу - что за сумрачный гений такое придумал. И зачем? Но потом подумалось - а что? Это же на самом деле позволяет некоторые интересные фишки довольно дёшево делать. Например фонт переменной ширины - просто заливай в полоску сколько надо байт и всего лишь. Реально что-то в этом есть. Действительно переменная ширина походу более частая задача, чем переменная высота и такая раскладка видеопамяти к этому более дружелюбна.
Забавно, забавно... Но тайлы теперь надо препроцессить разворачивая на 45 градусов...
Так, ну всё, тайловые фоны 8x8 полностью освоил - всё как на старых консолях, но вывод в битмап через OR->XOR, поэтому можно сколько угодно задников фигачить, всё программно рендерится, но в битмап.
Теперь остались спрайты. Раскладка видеопамяти настаивает чтобы был один формат спрайтов - x*8, т.е. 8 по высоте, но сколько угодно пикселей по ширине - гранулярность в 1 пиксель ширины. Это реально самое простое в том как тут видеопамять монохромная устроена.
Что характерно - что общение с дисплеем осуществляется по протоколу I2C, т.е. сериальный протокол с двумя всего проводами на передачу данных.
И это проброска всего экрана по серийному протоколу каждый кадр - а 128x64 бита это 1024 байта.
И вот проброска 1Кб на каждый кадр по серийному по сути протоколу - это 8192 бита за раунд, а значит при дефолтной частоте I2C в 100кГц теоретический лимит - это 12 кадров в секунду. Но это теоретика если бы каждый бит передавался за такт - а это см. чуть ниже.
Дело в том, чтоо дефолту библиотека Adafruit_SSD1306 которой я пользуюсь для общения в дисплеем выставляет частоту общения шины I2C в 400кГц.
Если руководствоваться числами выше, то должно было бы быть порядка 48 фпс, но нет - фпс ощущается в районе 20-ти.
Имхо частота вдвое выше числа передаваемых бит просто, и это имеет логику.
Так или иначе на ESP32 есть возможность поднять частоту до 800 кГц, а я поднял даже до 1 МГц. И работа стабильная.
Но по ощущениям фпс вырос только где то до 30 кадров в секунду.
Имхо потому что суть протокола I2C еще требует отклика от устройства в процессе взаимодействия и сам дисплей работает на частотах ниже.
Просто успевает среагировать на мегагерцовые изменения в шине, но сам лично свой ответ формирует медленнее.
Поэтому где то около 30 кадров в секунду. Ну и норм. Когда выведу спрайты уже покажу результаты тут.
=A=L=X=
> SSD1306
даа, на нем на работе много пультов сделал. Правда по spi а не i2c. Ну и для пульта спрайтов с прокруткой не требуется, кружок, пара шкал, пара надписей.
kipar
Ну да, библиотека Adafruit_SSD1306 через которую общаюсь на самом деле имеет свои функции по выводу графики - от линий, кругов и текста до битмапов с прозрачностью. Но я когда решил проверить известен ли ей трюк с OR->XOR и залез под капот, то с удивлением для себя обнаружил, что прозрачность реализована через if(visible) setPixel(...); Вообще весь графоний делается оказывается через setPixel - всё кроме полного обнуления битмапа и рисования вертикальных или горизонтальных (fast) линий делается через setPixel, лол.
Ну нет, это для меня неэстетично.
Поэтому у меня так (код прохода по полосе из восьми сканлайнов):
uint8_t *ptr = vpBuffer + vpBufferOffset; for (int row = 0; row < vpRows; row++) { int tileX = bx >> 3; int tileXOff = bx & 7; FETCH_MAP1; FETCH_MAP2; uint8_t *dst = ptr; for ( int col = 0; col < vpCols; col++) { uint8_t mask = ( *curTile1Img++ >> tileYOff) | ( *curTile2Img++ << ( 8 - tileYOff)); uint8_t pixels = ( *curTile1Img++ >> tileYOff) | ( *curTile2Img++ << ( 8 - tileYOff)); *dst = ( *dst | mask) ^ pixels; dst++; tileXOff++; if ( tileXOff > 7) { tileX++; tileXOff = 0; FETCH_MAP1; FETCH_MAP2; } } ptr += 128; tileY++; }
Проходим по каждой полоске пикселей 1x8 экрана и определяем в какие две клетки карты она попадает, для них определяем тайлы и из тайлов собираем битовые маски по которым накладывается изображение.
Для спрайтов алгоритм надо развернуть - идём наоборот от битов в спрайте и определяем в какие полоски экрана они попадут.
Опять споткнулся об if ( by & 7 == 0 ) drawAligned( ... ); - с какого хрена приоритет битовых операций ниже чем == всё-таки интуиция моя никогда не поймёт.
Поражаюсь даже тому насколько лёгким и продуктивным процесс создания однобитной графики в современности может быть.
Ну это же просто пару кликов и результат!

Ну всё, реализовал в однобитовом движке всё что хотел - на видео фокусировка не очень, поэтому пиксели белые засвечивают картинку.
Кароч собственная консоль, собственный вывод в монохром тайловых карт и спрайтов.
Можно даже сказать что закрыт маленький гештальтик.
=A=L=X=
Лучше бы графический дисплей использовать - с сенсорным экраном. Удобно ведь и клавиши не нужны - можно жмакать на сенсор как в обычном телефоне. Плюс на таких дисплеях по умолчанию припаян слот под SD карту. На которую можно грузить весь необходимый контент. И самый главный момент размер интерактивной игровой области - не надо под "лупой" смотреть. Я сам в свое время много где использовал такие двух цветные дисплеи в разных проектах. Для текста они хороши, а вот для графики - слишком мелко.
gambit_oz
Да это просто развлекуха ради развлекухи, разумеется я не буду всерьёз делать под это игры.
Пришёл LCD-экран ST7789.
Начал собирать вторую "консоль" на ESP32 уже с этим дисплеем.
А он сильно круче нежели SSD1306 из предыдущих комментариев.
Самое главное - он полноцветный.
Аппаратно у него 6 бит на канал цвета, т.е. 18bpp.
Однако такой формат весьма неудобен для общения с ним байтами, поэтому традиционно для МК он представлен в формате 16bpp 5:6:5, два бита при общении обнуляются да и фиг с ними.
Во вторых у него разрешение 240x240, но на самом деле VRAM больше по моему на 240x320, просто отображается кусочек 240x240.
Даже если руководствоваться 16bpp и фактическим разрешением, то это получается 115200 байт на экран или примерно 112Кб.
Зацените разницу между SSD1306 и ST7789 - в первом 1Кб монохромного VRAM (128x64), во втором 112Кб труколора (240x240).
При этом по физическим размерам второй только раза в два больше первого.
И вот тут мне понравилась разница между программными моделями в популярной библиотеке под экосистему Arduino - Adafruit_GFX.
Adafruit_GFX это вроде бы изначальная унифицированная библиотека по работе с разными дисплеями, но в итоге получается иногда существенная разница.
Adafruit_GFX предоставляет базовый класс с комплексом начертательных функций рисования - типа drawCircle или drawBitmap.
Под конкретные дисплеи он расширен наследниками такими как Adafruit_SSD1306 или Adafruit_ST7789 которые забирают из него львиную часть обобщённых алгоритмов и реализуют свои протоколы общения с девайсом.
Но вот незадача - с монохромным крохотным SSD1306 проще выстроить общение было так: в самом микроконтроллере отводится 1Кб RAM под временный буфер - все операции рисования обновляют именно его, а в конце всех обновлений надо вызвать объекту экрана .display() - при этом всё экранное пространство перекидывалось в VRAM дисплея за одну долгую передачу единым битмапом.
И это пролазит даже в самые младшие серии микроконтроллеров Arduino UNO с их 2Кб ОЗУ, хотя и отжирает половину оперативной памяти.
Но ST7789 с 112Кб VRAM так уже не вариант делать на Arduino UNO - и там реально библиотека ничего в памяти микроконтроллера не хранит, а все команды рисования испускает сразу же командами обновления пикселей в дисплей.
Базовая для ST7789 вещь здесь в том, что испускаются две команды - задание прямоугольника куда сейчас будут сливаться пиксели и потом последовательное запихивание цветов этих пикселей.
Так вот здесь выявляется с особой важностью проблема базового класса Adafruit_GFX - т.к. он ничего не подозревает о том какое физическое воплощение будет у имплементаций, он (за исключением команд типа заливки монотонного прямоугольника) все тонкие вещи делает через virtual putPixel.
А Adafruit_ST7789 на putPixel открывает канал общения с дисплеем (шина SPI), обозначает прямоугольник для заливки пикселей как (x, y, 1, 1) и льёт один пиксель.
И так на каждый putPixel - сперва проверки на попадание в экран, потом выставление области заливки (x, y, 1, 1), потом заливка одного пикселя.
И даже текст - каждый его пиксель - выводится так.
В результате всё работает, но даже одну треть экрана залить текстом - глазом заметно как происходит заливка.
Хехехе.
Короче тут тоже надо будет поработать в парадигме полной переброски содержимого экрана на каждый кадр. Надеюсь шина SPI справится с 20-30 кадрами в секунду...