Сам себе Linux смартфон: Как я выкинул Android и написал свою прошивку с нуля

от автора

image

К огромному сожалению, старые смартфоны всё чаще и чаще находят своё пристанище в мусорном баке. К прошлым, надежным «друзьям» действует исключительно потребительское отношение — чуть устарел и сразу выкинули, словно это ненужный мусор. И ведь люди даже не хотят попытаться придумать какое-либо применение гаджетам прошлых лет! Отчасти, это вина корпораций — Google намеренно тормозит и добивает довольно шустрые девайсы. Отчасти — вина программистов, которые преследуют исключительно бизнес-задачи и не думают об оптимизации приложений совсем. В один день я почувствовал себя Тайлером Дёрденом от мира IT и решил бросить вызов проприетарщине: написать свою прошивку для уже существующего смартфона с нуля. А дабы задачка была ещё интереснее, я выбрал очень распространенную и дешевую модель из 2012 года — Fly IQ245 (цена на барахолках — 200-300 рублей). Кроме того, у этого телефона есть сразу несколько внешних шин, к которым можно подключить компьютер или микроконтроллер, что даёт возможность использовать его в качестве ультрадешевого одноплатника для DIY-проектов. Получилось ли у меня реализовать свои хотелки? Читайте в статье!

Мотивация

Честно сказать, идея попытаться реализовать свою прошивку мне пришла ещё давно. Однако, дабы не завлекать опытного читателя кликбейтом, я сразу поясню, в чём заключается «прошивка с нуля»:

  1. Мы всё ещё используем Linux: в качестве ядра мы продолжаем использовать образ Linux, предоставленный нам производителем. Написание прошивки полностью с нуля заняло бы очень много времени (особенно без схемы на устройство). Однако, мы вообще не загружаем Android никаким образом.
  2. Мы не используем библиотеки AOSP: наша прошивка без необходимости не использует никаких библиотек уже имеющегося образа Android. Вся работа с железом происходит с помощью низкоуровневого API Linux. Это значит, что отрисовка графики, звук, управление ресурсами и питанием ложится полностью на нас.
  3. Прошивка может запускать только нативные программы: да, это тоже камень в сторону Android. Изначально, наша прошивка умеет запускать только нативные программы, написанные на C. Причём она экспортирует собственное C API — дабы приложения могли использовать всю мощь нашего смартфона в виде простого и понятного набора методов.

image

Проектов по выкидыванию Android из, собственно, Android-смартфонов как минимум несколько: UBPorts — бывший Ubuntu Touch, FireFox OS и его наследник Kai OS и конечно же, postmarketOS. Отчасти можно сюда отнести и Sailfish OS — но там образы имеются в основном на смартфоны от Sony. Все эти проекты объединяет сложность портирования и невозможность их завести на устройствах без исходного кода ядра. Даже если у вас есть исходный код ядра, но, например, устройство использует ядро 2.6 — навряд-ли вы сможете завести современный дистрибутив на нём.

image

Другой вопрос в том, что можно использовать полу-baremetal подход, когда от Linux берется практически минимальный функционал. Всё, что мы имеем — busybox, libc и низкоуровневый доступ к железу, благодаря API самого ядра. Как под это всё программировать — я рассказывал в прошлой статье. Этот же подход мы будем использовать и сейчас — как иллюстрация реального применения подобного способа.

Итак, что наша прошивка должна уметь:

  • Отрисовывать произвольную графику: графическая подсистема нашей прошивки должна работать с фиксированным форматом пикселя, уметь загружать прозрачные и непрозрачные изображения, отрисовывать картинки с альфа-блендингом и т. п.
  • Уметь звонить и работать с модемом: общение с модемом происходит посредством AT-команд — общепринятого в индустрии стандарта. Однако в случае нашего устройства, есть м-а-а-а-ленький нюанс, о котором я расскажу позже.
  • Иметь механизм приложений: мы ведь не будем хардкодить все «экраны» в прошивке в виде кучи стейтов, верно? Для этого у нас должен быть простой и понятный механизм слинкованных с прошивкой приложений.
  • Обрабатывать ввод: обработка тачскрина и жестов — это задача подсистемы ввода.
  • Реализовывать анимированный UI: здесь всё очевидно, наша прошивка должна иметь готовые элементы пользовательского интерфейса для будущих приложений: кнопки, текстовые поля и т. д. О деталях реализации этой подсистемы, я расскажу ниже (а реализовал я её очень необычно для такой системы).

Начинаем мы с хардварной части. Именно здесь я покажу вам, как использовать внешние шины вашего устройства.

Аппаратная часть

В качестве смартфона для нашего проекта, я выбрал популярную бюджетную модель из 2012 года — Fly IQ245 Wizard. Это простенький китайский смартфон, который работал на базе популярного в прошлом 2G-чипсета: MediaTek MT6573, да и стоил около 2х тысяч рублей новым. Однако вот в чём суть: мне удалось заставить работать «медиатековский» модем и даже позвонить с него на свой основной телефон, но… только ввод и вывод данных из звукового тракта модема происходит через звуковую подсистему Android — к которой доступа у нас нет!

image

image

Именно поэтому, мы идём на очень хитрый и занимательный костыль: мы распаяем внешний модем сами! В качестве радиомодуля у нас выступит модуль SIM800 от компании SIMCOM. И даже он очень близок к нашему смартфону в аппаратном плане: ведь в основе этого модуля лежит популярнейший чипсет из кнопочников тех лет: MediaTek MT6261D. Преимущество SIM800 в его цене — он стоит пару сотен рублей, так что по карману выбор модема не влияет.

image
На весу паять крайне неудобно. В финальном варианте перепаяю нормально.

Но как его подключать? SIM800 общается с другими устройствами посредством протокола UART — универсальный асинхронный приемо-передатчик. И вот тут мы включаем смекалочку. Разбираем устройство и видим то, что я пытаюсь долгое время донести до моих читателей — аж два канала UART: один практически посередине, второй справа. Нам нужны пятачки TXD4 и RXD4:

image

image

Обычно на этот канал UART летят логи ядра, которые можно без проблем отключить минорной правкой U-Boot в HEX-редакторе. Впрочем, модем никак не реагирует на «мусор» из консоли и просто отвечает ошибками — хватит лишь очистить буфер сообщений для того, чтобы все работало нормально. Подпаиваемся к UART’у с помощью преобразователя — у меня оным выступает ESP32 с выпаянным чипом.

image

image

Увидели логи? Замечательно, пора попытаться что-то отправить на ПК и с ПК. UART работают без тактовых сигналов и зависит исключительно от старт/стоп битов и бодрейта, который на устройствах MediaTek равен 921600. TXD4 и RXD4 обнаруживаются в системе на консоли /dev/ttyMT3. Пробуем что-то отправить: всё работает!

image

Вот теперь-то можно подключить наш внешний модем и попытаться пообщаться с ним, отправив тестовую команду AT. Модем отвечает OK! На этот раз я работаю с смартфоном из режима Factory mode — практически тоже самое, что и режим recovery, но позволяющий, например, получить доступ к камере устройства. Простая и понятная схема, поясняющая что и куда подключать:

image

На этом модификация аппаратной части пока закончена. Пора переходить к реализации софта! Я решил разделить материал на каждый модуль, который я реализовывал — дабы вам был понятен процесс разработки и отладки прошивки!

Заставляем смартфон запускать нашу прошивку

На этот раз я решил загружать смартфон из режима рекавери. Однако никто не мешает в будущем просто прошить раздел recovery вместо boot и получить прямую загрузку прямо в нашу прошивку. Время такой загрузки будет занимать ~3-4 секунды с холодного старта. Очень даже ничего.

image

Я взял уже готовый образ TWRP для своего смартфона и пропатчил его, дабы сам рекавери не мешал своим интерфейсом. Для этого я распаковал образ recovery.img с помощью MtkImgTools и убрал в init.rc запуск службы /sbin/recovery. После этого, я залил прошивку обратно на устройство и получил подобную свободу действий — консоль через USB и чистый холст в виде смартфона! Старые смартфоны на чипсетах MediaTek шьются через USB только после замыкания тест-поинта — на моем аппарате его местонахождение очевидно. Замыкаем контакты между собой, подключаем смартфон без АКБ к ПК и ждем прошивки:

image

Теперь можно деплоить программы! Важный нюанс: в отличии от Makefile из прошлой статьи, для Android 2.3 параметр -fPIE нужно убрать — иначе динамический линкер (/sbin/linker) будет вылетать в segmentation fault.

image

Графическая подсистема

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

image

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

В случае с этим устройством (и большинством старых устройств), формат пикселя оказался RGB565 — т. е. 5 бит красный, 6 бит зеленый, 5 бит синий. Конвертация форматов пикселей всегда была занозой в заднице для программных рендереров, поскольку занимает дополнительное время, которое обратно зависимо от размера дисплея. Изначально я решил выделить буфер в том же формате, что и фреймбуфер, но затем решил сделать классический и самый портативный формат — RGB888 (24х-битный цвет), а при копировании кадра на экран, на лету делать преобразования цвета:

void CGraphics::Flip() {     for(int i = 0; i < fbDesc.width; i++)     {         for(int j = 0; j < fbDesc.height; j++)         {             short* absPixel = (short*)&fbDesc.pixels[(j * fbDesc.lineLength) + (i * 2)];             char* absBackPixel = &backBuffer[(j * fbDesc.width + i) * 3];              short c16 = ((absBackPixel[0] & 0b11111000) << 8) | ((absBackPixel[1] & 0b11111100) << 3) | (absBackPixel[2] >> 3);             *absPixel = c16;         }     }      // We should pass a bit changed VSCREENINFO structure back to FB driver, to make it update our screen     // This seems like a bit non-standard behaviour, because Android recovery uses this too: probably, something to save power.     flip = !flip;     vInfo.yres_virtual = (int)flip;     ioctl(fbDev, FBIOPUT_VSCREENINFO, &vInfo); } 

Очень важный нюанс, который я не упомянул в предыдущей статье: на устройствах прошлых лет для обновления фреймбуфера необходимо послать структуру var_screeninfo, где хотя бы что-то изменено, иначе никаких изменений мы не увидим. Этот же костыль используется в родном recovery для отрисовки, а судя по исходникам драйвера fb, «правильный» способ обновить экран — послать драйверу ioctl (который я пока что не пробовал).

После того, как я смог управлять дисплеем, я решил загрузить и отобразить какую-нибудь картинку. Пусть это будут обои для нашей прошивки:

FILE* f = fopen(fileName, "r");      LOGF("Loading %s\n", fileName);  if(!f) {         LOGF("Unable to open %s\n", fileName); return 0; }  CTgaHeader hdr; fread(&hdr, sizeof(hdr), 1, f);  if(hdr.paletteType) { LOG("Palette images are unsupported\n"); return 0; }  if(hdr.bpp != 24 && hdr.bpp != 32) { LOG("Unsupported BPP\n"); return 0; }  unsigned char* buf = (unsigned char*)malloc(hdr.width * hdr.height * (hdr.bpp / 8));      if(!buf)     {         LOG("Memory exhausted\n");         return 0;     }  //fseek(f, hdr.headerLength, SEEK_SET);  fread(buf, hdr.width * hdr.height * (hdr.bpp / 8), 1, f); fclose(f); CImage* ret = new CImage(); ret->Width = hdr.width; ret->Height = hdr.height; ret->Pixels = buf;     ret->IsTransparent = hdr.bpp == 32;  LOGF("Loaded %s %ix%i\n", fileName, ret->Width, ret->Height); return ret; 

Загрузчик TGA сильно не поменялся: я таскаю его в неизменном виде из проекта в проект. Он поддерживает любые форматы пикселя, кроме палитровых, но я его искусственн ограничиваю на RGB888 и RGBA8888 — для поддержки обычных картинок и картинок с альфа-каналом. После этого, я написал не очень шустрые, но достаточно универсальные методы для отрисовки картинок:

__inline void __ClipPrimitive(CFrameBuffer* fbDesc, int* dw, int* dh) {     if(*dw > fbDesc->width)         *dw = fbDesc->width - 1;      if(*dh > fbDesc->height)         *dh = fbDesc->height - 1; }  void CGraphics::PutPixel(int x, int y, CColor color) {     if(x < 0 || y < 0)         return;          char* col = &backBuffer[(y * fbDesc.width + x) * 3];     col[0] = color.R;     col[1] = color.G;     col[2] = color.B; }  void CGraphics::PutPixelAlpha(int x, int y, CColor color, float alpha) {     if(x < 0 || y < 0)         return;          char* col = &backBuffer[(y * fbDesc.width + x) * 3];      col[0] = (byte)(color.R * alpha + col[0] * (1.0f - alpha)); col[1] = (byte)(color.G * alpha + col[1] * (1.0f - alpha)); col[2] = (byte)(color.B * alpha + col[2] * (1.0f - alpha)); }  void CGraphics::DrawImage(CImage* img, int x, int y) {     if(img)     {         if(!img->IsTransparent)         {             for(int i = 0; i < img->Height; i++)             {                 for(int j = 0; j < img->Width; j++)                 {                     if(j >= fbDesc.width)                         break;                      CColor col;                     unsigned char* pixels = &img->Pixels[((img->Height - i - 1) * img->Width + j) * 3];                      col.R = pixels[2];                     col.G = pixels[1];                     col.B = pixels[0];                      PutPixel(x + j, y + i, col);                 }                  if(i >= fbDesc.height)                     break;             }         }         else         {             for(int i = 0; i < img->Height; i++)             {                 for(int j = 0; j < img->Width; j++)                 {                     if(j >= fbDesc.width)                         break;                      CColor col;                     unsigned char* pixels = &img->Pixels[((img->Height - i - 1) * img->Width + j) * 4];                      col.R = pixels[2];                     col.G = pixels[1];                     col.B = pixels[0];                     float alpha = (float)pixels[3] / 255;                      PutPixelAlpha(x + j, y + i, col, alpha);                 }                  if(i >= fbDesc.height)                     break;             }         }     } } 

PutPixel желательно заинлайнить в будущем. В целом, сама отрисовка работает достаточно быстро, но поскольку рендеринг выполняется на ЦПУ — рано или поздно мы упремся в количество картинок на экране. Есть некоторые оптимизации: например, непрозрачные картинки можно просто коприовать сканлайнами прямо в задний буфер.

Сразу же реализовываем методы для рисования шрифтов: они у нас будут совсем простенькими — только моноширинные (все символы имеют одинаковую ширину) и растровыми (для каждого размера придется «запекать» несколько шрифтов). Для этого я написал маленькую программку, которая рисует виндовые шрифты прямо в наш самопальный формат:

            Console.WriteLine("FontBake for BodyaPhone");             Console.WriteLine("(C)2023 Bogdan Nikolaev");              if (args.Length > 0)             {                 string fontName = args[0];                 int glyphSize = 16;                 Font fnt = new Font(fontName, glyphSize, FontStyle.Bold, GraphicsUnit.Pixel);                 SolidBrush brush = new SolidBrush(Color.White);                 SolidBrush bg = new SolidBrush(Color.Magenta);                  Bitmap glyph = new Bitmap(glyphSize, glyphSize);                 Graphics g = Graphics.FromImage(glyph);                 g.TextRenderingHint = System.Drawing.Text.TextRenderingHint.ClearTypeGridFit;                  BinaryWriter writer = new BinaryWriter(File.Create(fontName));                 writer.Write(glyphSize); // Glyph size                  byte[] glyphData = new byte[glyphSize * glyphSize * 3];                  for(int i = 0; i < 255; i++)                 {                     g.FillRectangle(bg, 0, 0, glyphSize, glyphSize);                     g.DrawString(((char)i).ToString(), fnt, brush, PointF.Empty);                      var data = glyph.LockBits(new Rectangle(0, 0, glyphSize, glyphSize), System.Drawing.Imaging.ImageLockMode.ReadOnly, System.Drawing.Imaging.PixelFormat.Format24bppRgb);                     System.Runtime.InteropServices.Marshal.Copy(data.Scan0, glyphData, 0, glyphData.Length);                     glyph.UnlockBits(data);                      writer.Write(glyphData);                 }             } 

Формат примитивнейший:

1 байт говорит нам о размере шрифта и далее идут 255 изображений символов. Да, это не очень эффективно т.к попадают пустые символы из ASCII-таблицы, но в будущем это можно поправить.

Прозрачность в символах обеспечивает фоновый цвет Magena — ярко-розовый. Я не стал делать дополнительный альфа-канал, т. к. иначе будут серьезные лаги при выводе большого количества текста.

void CGraphics::DrawString(CFont* font, char* str, int x, int y) {     CColor col = { 64, 64, 64 };      DrawStringColored(font, str, x, y, col); }  void CGraphics::DrawStringColored(CFont* font, char* str, int x, int y, CColor colorMultiply) {     if(font && str && strlen(str) > 0)     {                  for(int i = 0; i < strlen(str); i++)         {             DrawGlyph(font->Glyphs[str[i]], x + (i * (font->Glyphs[str[i]]->Width - 5)), y, colorMultiply);         }     } } 

Теперь у нас есть отображение картинок и текста! Что с этим можно сделать?

Обработка ввода

image

Конечно же, реализовать обработку ввода! Это будет фактически минимальный функционал, который можно использовать для создания UI-приложений. В дополнение к прошлой статье хочу отметить то, что разные драйверы тачскрина ведут себя по разному. На этом устройстве, драйвер тачскрина не сообщал событие BTN_TOUCH, из-за чего пришлось идти на некоторые ухищрения. Однако в конце-концов, метод для проверки касания пальца в определенном месте у меня есть:

void CInput::Update() {     input_event ev; int ret = 0;     bool gotEvent = false; // Touchscreen driver sends us events each input "frame". So, if we don't have BTN_TOUCH event, we can track releasing finger when there are no events in current frame.  while((ret = read(evDev, &ev, sizeof(input_event)) != -1)) { if(ev.code == ABS_MT_POSITION_X) TouchX = ev.value;  if(ev.code == ABS_MT_POSITION_Y) TouchY = ev.value;          gotEvent = true; }      bool pressed = gotEvent;      if(pressed && TouchState == tsIdle)         TouchState = tsTouching;      if(TouchState == tsReleased)         TouchState = tsIdle;      if(!pressed && TouchState == tsTouching)         TouchState = tsReleased; }  bool CInput::IsTouchedAt(int x, int y, int w, int h) {     return TouchX > x && TouchY > y && TouchX < x + w && TouchY < y + h && TouchState == tsReleased; }

Пока что здесь не хватает обработки «хардварных» кнопок — домой, меню, назад и т. п. Однако в будущем это всё можно реализовать!

Анимация

Не забыл я и про анимации. Ну кому с такими ресурсами нужен неанимированный топорный интерфейс? Пусть лучше будет анимированный, пусть и примитивный!

Аниматор напоминает оный из ранних версий Android: он имеет фиксированный набор свойств, которые умеет интерполировать в промежутках определенного времени. Если простыми словами: то он оперирует линейными отрезками времени a и b, в промежутке которых мы имеем значение «прогресса» — которое даёт нам результат от 0.0f (начало анимации) до 1.0f (конец анимации). Пока время тикает до необходимого интервала (duration), аниматор интерполирует заранее назначенные ему поля до нужных значений.

Именно так и получается плавность! Похожим образом реализованы анимационные системы во многих играх и мобильных ОС, только там они гораздо более комплексны: есть сериализация/десериализация из файлов, поддержка кейфреймов (несколько последовательных состояний на одном промежутке времени), поддержка кастомных свойств и т. п.

CAnimator::CAnimator() {     SetDuration(1.0f); }  CAnimator::~CAnimator() {      }  void CAnimator::SetTranslation(int xFrom, int yFrom, int xTo, int yTo) {     this->xFrom = xFrom;     this->yFrom = yFrom;     this->xTo = xTo;     this->yTo = yTo; }  void CAnimator::SetRotation(float from, float to) {     rFrom = from;     rTo = to; }  void CAnimator::SetDuration(float speed) {     duration = speed; }  float lerp(float a, float b, float f) {     return a * (1.0 - f) + (b * f); }  bool CAnimator::Update() {     Time += 0.25f;      if(Time > 1.0f)         Time = 1.0f;      X = (int)lerp((float)xFrom, (float)xTo, Time);     Y = (int)lerp((float)yFrom, (float)yTo, Time);      Rotation = lerp(rFrom, rTo, Time); }  void CAnimator::Run() {     Time = 0;      IsPlaying = true; } 

Модем

Как я уже говорил раннее, работа с модемом происходит посредством AT-команд. Лучше всего обрабатывать ввод-вывод модема из отдельного потока, поскольку он может отвечать довольно медленно и тормозить UI-поток основной программы, вызывая лаги. В SIM800 уже реализован весь GSM-стек, в том числе декодирование и вывод звука через встроенный усилитель с фильтром — остается только подключить динамики и микрофон от нашего телефона. Пока что я подсобрал аудиотракт на том, что было под рукой — микрофон от нерабочего смартфона и динамик от планшета, но для проверки этого хватает:

image

Важный нюанс: по умолчанию, tty-устройства в Linux работают по терминальному принципу — т. е. дробят транзакции по символу окончания строки (\n), имеют ограниченный буфер и т. д. Для нормальной работы в условиях модема — когда фактически длина ответа неизвестна, а в сам ответ могут «вклиниваться» Unsolicited-команды (своеобразные флаги о состоянии от модема, которые могут прийти в произвольное время — т. е. при входящем звонке, модем начнёт флудить RING в терминал), необходимо иметь возможность точно прочитать весь буфер до конца и парсить данные «по месту». Для этого используется raw-режим терминала:

tcgetattr(modemFd, &tio);     tio.c_iflag &= ~(BRKINT | ICRNL | INPCK | ISTRIP | IXON);     tio.c_oflag &= ~(OPOST);     tio.c_cflag |= (CS8);     tio.c_lflag &= ~(ECHO | ICANON | IEXTEN | ISIG); tcsetattr(modemFd, TCSAFLUSH, &tio);

После чего можно запросить состояние модема:

        char atBuf[16]; // Check modem presence SendAT("AT\r\n", 250); GetATResponse((char*)&atBuf, sizeof(atBuf));  if(!CheckATStatus((char*)&atBuf)) { printf("Failed to initialize modem: Modem isn't anwered OK\n"); printf("Modem response: %s\n", &atBuf); return; }          LOG("AT = OK, ready to operate\n");  ...  void CModem::SendAT(char* command, int waitTime) { int result = write(modemFd, command, strlen(command));  if(!result) LOGF("SendAT failed: %i\n", errno);  usleep(waitTime * 1000); }  char* CModem::GetATResponse(char* buf, int maxLen) { pollfd pfd; pfd.fd = modemFd; pfd.events = POLLIN;  memset(buf, 0, maxLen); int ev = poll(&pfd, 1, 2000);  if(ev) { int num = read(modemFd, buf, maxLen); } else { LOG("AT Receive: Modem not responding...\n"); } } 

И продолжить работу дальше. После этого, можно переходить к реализации самой прослойки между модемом и вашей программой:

void CModem::Dial(char* number) { if(strlen(number) > 32) return;  char buf[64];  char atResponse[64]; sprintf((char*)&buf, "ATD%s;\r\n", number); LOGF("Dialing %s\n", buf);  SendAT(buf, 250); GetATResponse((char*)&atResponse, sizeof(atResponse));  LOGF("Dial response: %s\n", &atResponse); }  void CModem::Hang() { char atBuf[64];  SendAT("ATH\r\n", 250); GetATResponse((char*)&atBuf, sizeof(atBuf)); LOGF("ATH: %s\n", &atBuf);  LOG("Hang\n"); } 

Пытаемся позвонить с помощью метода Dial и видим, что всё работает! Это очень круто! А теперь, конечно же, самое время переходить к реализации того, чего вы ждали — пользовательского интерфейса!

Главный экран

К выбору концепции для интерфейса, я поступил максимально просто — «слизал» дизайн первых версий iOS. Как по мне, это одни из самых красивых версий iOS вообще — все эти приятные градиенты и переливания. Конечно, я не так крут, как инженеры Apple, да и мощного UI-фреймворка у меня пока что нет, поэтому я приступил к реализации с «минимальным» функционалом.

image

Начал я с разделения главного экрана на модули и продумывания архитектуры основного «лаунчера». У нас есть статусбар, который рисуется поверх всех приложений, полка с приложениями — AppDrawer и сами экраны приложений, унаследованные от суперкласса CScreen.

class CScreen { protected: CAnimator* windowAnimator; public: CScreen(); ~CScreen();  virtual void Show(); virtual void Update(); virtual void Draw(); virtual void Hide(); }; 

На данный момент, отрисовка достаточно примитивная: сначала рисуются фоновые обои, затем, если нет никаких активных экранов — AppDrawer и в самом конце рисуется статусбар и всевозможные оверлеи.

void CLauncher::DrawAppDrawer() { for(int i = 0; i < sizeof(Apps) / sizeof(CAppDesc*); i++) { int x = drawerAnimator->X + (i * 75); int y = drawerAnimator->Y; Graphics->DrawImage(Apps[i]->Icon, x, y);  if(Input->IsTouchedAt(x, y, Apps[i]->Icon->Width, Apps[i]->Icon->Height)) { StartScreen(new CDialerScreen()); } } }  void CLauncher::StartScreen(CScreen* screen) { if(screen) { currentScreen = screen; currentScreen->Show(); } }  void CLauncher::Run() { CImage* test = CImage::FromFile("ui/stFiller.tga");;  while(true) { Input->Update();  Graphics->DrawImage(Wallpaper, 0, 0);  if(currentScreen) { currentScreen->Update(); currentScreen->Draw(); } else { drawerAnimator->Update(); DrawAppDrawer(); }  Status->Update(); Status->Draw();                                  if(Dialog->IsVisible())      Dialog->Draw();  Graphics->Flip(); } } 

Практически сразу я решил обкатать анимационную «систему» и добавить первые анимашки — выезжающий статусбар и анимация а-ля айфон:

    animator = new CAnimator();     animator->SetTranslation(0, -imFiller->Height, 0, 0);     animator->Run(); 

Выглядит симпатичненько. Если я смогу поднять хардварный GLES, то это получится сделать в разы плавнее и шустрее — не хуже айфонов тех лет! Реализация самого статусбара примитивненькая, но вполне рабочая:

gLauncher->Graphics->DrawImage(imFiller, animator->X, animator->Y);     gLauncher->Graphics->DrawImage(imBattery[(int)gLauncher->PowerManager->GetBatteryLevel()], imFiller->Width - imBattery[0]->Width - 5, animator->Y + 5);      char timeFmt[64];     time_t _time = time(0);     tm* _localTime = localtime(&_time);     strftime((char*)&timeFmt, sizeof(timeFmt), "%R", _localTime);      gLauncher->Graphics->DrawString(gLauncher->Font, (char*)&timeFmt, 0, 0); 

Кроме этого, я сразу же реализовал предварительный механизм приложений в системе — пока что они слинкованы статически с основным лаунчером. Для этого есть структура CAppDesc, которая содержит минимально-необходимую информацию для показа информации о приложении и фабрику для создания его основного экрана.

#define APP_FACTORY(clazz) CScreen* __phone_factory_##clazz () { return new clazz (); }  struct CAppDesc { char Name[16]; char IconPath[32];  CImage* Icon; CScreen* MainScreen;  CScreen*(*Factory)(); };  ...  CAppDesc _APhone = { "Phone", "ui/phone.tga",  0, 0,  &__phone_factory_CDialerScreen }; 

После этого, я приступил к реализации первого приложений — собственно, звонилки. 🙂

#include <monohome.h>  CDialerScreen::CDialerScreen() {     dialerButton = CImage::FromFile("ui/dialer_btn.tga");      memset(&number, 0, sizeof(number)); }  CDialerScreen::~CDialerScreen() {     delete dialerButton; }  void CDialerScreen::Update() {     CScreen::Update(); }  bool DialerButton(CImage* img, int x, int y, char* str) {     bool state = CGUI::Button(img, x, y);     gLauncher->Graphics->DrawString(gLauncher->Font, str, x + (img->Width / 2) - 8, y + (img->Height / 2) - 8);      return state; }  void CDialerScreen::Draw() {     CScreen::Draw();      for(int i = 0; i < 3; i++)     {         for(int j = 0; j < 3; j++)         {             int num = i * 3 + j + 1;             char buf[16];             memset(&buf, 0, sizeof(buf));             sprintf((char*)&buf, "%i", num);              if(DialerButton(dialerButton, j * dialerButton->Width + 15, 65 + (i * dialerButton->Height) + windowAnimator->Y, buf))             {                 if(strlen((char*)&number) < 31)                     strcat((char*)&number, (char*)&buf);             }         }     }      if(DialerButton(dialerButton, 1 * dialerButton->Width + 15, 65 + (3 * dialerButton->Height) + windowAnimator->Y, "0"))             {                 if(strlen((char*)&number) < 31)                     strcat((char*)&number, "0");             }      if(DialerButton(dialerButton, 0 * dialerButton->Width + 15, 65 + (3 * dialerButton->Height) + windowAnimator->Y, "C"))     {         gLauncher->Modem->Dial((char*)&number);     }      gLauncher->Graphics->DrawString(gLauncher->Font, (char*)&number, 10, 48); }

Обратите внимание на удобство примененного подхода Immediate GUI. Нам понадобился новый элемент интерфейса, который описывает кнопку номеронабирателя? Мы просто реализовываем ещё один метод, который берет за основу стандартную кнопку и дорисовывает к ней текст. Всё крайне просто и понятно, хотя на данный момент слишком захардкожено. 🙂

image

Звоним!

Пришло время совершить первый звонок с нашей по настоящему кастомной прошивки. Набираем номерок и…

image

Да, всё работает и мы без проблем можем дозвониться 🙂

Заключение

Конечно же, это далеко не весь функционал, необходимый любому современному смартфону. Здесь много чего еще нужно реализовать хотя бы для соответствия уровню бюджетных кнопочных телефонов: телефонную книгу, поддержку СМС/ММС, мультимедийный функционал с играми. Однако начало уже положено и самая необходимая часть модулей реализована. Этот проект очень занимательный для меня и я горд, что смог не на словах, а на деле показать вам, моим читателям, возможности моддинга совершенно NoName-устройств, без каких либо опознавательных знаков…

Моя задача заключается в том, чтобы показать вам возможности использования старых телефонов не только в потребительских, но и в гиковских DIY-сферах. Судите сами: огромный классный дисплей, емкостной тачскрин, готовый звук, камера — и всё это за каких-то пару сотен рублей. Главное показать людям, как всю эту мощь использовать в своих целях и делать совершенно новые устройства из существующих, а не выбрасывать их на помойку!
Сейчас смартфоны, подобные Fly из этого поста стоят копейки, а портировать на них прошивку можно без каких-либо трудностей. Я очень надеюсь, что после этого поста читатели попытаются сделать что-то своё из старых смартфонов, благо свои наработки я выкладываю на GitHub!


Возможно, захочется почитать и это:

Только зарегистрированные пользователи могут участвовать в опросе. Войдите, пожалуйста.

Как вам проект?

77.93% Это очень круто и действительно нужно! Материал практически уникальный в своём роде. Продолжай пилить!646
1.93% Это вообще не круто, нафиг ты фигней маешься???16
20.14% Автор наркоман167

Проголосовали 829 пользователей. Воздержались 92 пользователя.

Только зарегистрированные пользователи могут участвовать в опросе. Войдите, пожалуйста.

Как вам DIY-тематика в недавних статьях?

97.02% Платиновый контент от bodyawm и MDXE1337. Продолжай в том-же духе!423
2.98% Вообще какашка! Отписался.13

Проголосовали 436 пользователей. Воздержались 157 пользователей.

Только зарегистрированные пользователи могут участвовать в опросе. Войдите, пожалуйста.

Есть ли будущее у смартфонов в качестве одноплатников, если я попытаюсь донести примеры их использования до как можно большего числа людей?

71.07% Безусловно есть!452
28.93% Нет. Так и будут на свалке лежать 🙁184

Проголосовали 636 пользователей. Воздержался 91 пользователь.

ссылка на оригинал статьи https://habr.com/ru/articles/754212/