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

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

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

❯ Мотивация

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

В случае с этим устройством (и большинством старых устройств), формат пикселя оказался 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); } } }

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

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

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

Конечно же, реализовать обработку ввода! Это будет фактически минимальный функционал, который можно использовать для создания 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-стек, в том числе декодирование и вывод звука через встроенный усилитель с фильтром — остается только подключить динамики и микрофон от нашего телефона. Пока что я подсобрал аудиотракт на том, что было под рукой — микрофон от нерабочего смартфона и динамик от планшета, но для проверки этого хватает:

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

Важный нюанс: по умолчанию, 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-фреймворка у меня пока что нет, поэтому я приступил к реализации с «минимальным» функционалом.

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

Начал я с разделения главного экрана на модули и продумывания архитектуры основного «лаунчера». У нас есть статусбар, который рисуется поверх всех приложений, полка с приложениями — 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. Нам понадобился новый элемент интерфейса, который описывает кнопку номеронабирателя? Мы просто реализовываем ещё один метод, который берет за основу стандартную кнопку и дорисовывает к ней текст. Всё крайне просто и понятно, хотя на данный момент слишком захардкожено. :)

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

❯ Звоним!

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

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

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

❯ Заключение

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

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

Статья подготовлена при поддержке TimeWeb Cloud. Подписывайтесь на меня и TimeWeb на DTF, чтобы не пропускать новые статьи каждый день!

Как вам проект?
Это очень круто и действительно нужно! Материал практически уникальный в своём роде. Продолжай пилить!21
Это вообще не круто, нафиг ты фигней маешься???
Автор наркоман
Как вам DIY-тематика в недавних статьях?
Платиновый контент от bodyawm и MDXE1337. Продолжай в том-же духе!
Вообще какашка! Отписался.
Есть ли будущее у смартфонов в качестве одноплатников, если я попытаюсь донести примеры их использования до как можно большего числа людей?
Безусловно есть!
Нет. Так и будут на свалке лежать :(
167
48 комментариев