К огромному сожалению, старые смартфоны всё чаще и чаще находят своё пристанище в мусорном баке. К прошлым, надежным «друзьям» действует исключительно потребительское отношение — чуть устарел и сразу выкинули, словно это ненужный мусор. И ведь люди даже не хотят попытаться придумать какое-либо применение гаджетам прошлых лет! Отчасти, это вина корпораций — 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 — навряд-ли вы сможете завести современный дистрибутив на нём.
Итак, что наша прошивка должна уметь:
- Отрисовывать произвольную графику: графическая подсистема нашей прошивки должна работать с фиксированным форматом пикселя, уметь загружать прозрачные и непрозрачные изображения, отрисовывать картинки с альфа-блендингом и т. п.
- Уметь звонить и работать с модемом: общение с модемом происходит посредством AT-команд — общепринятого в индустрии стандарта. Однако в случае нашего устройства, есть м-а-а-а-ленький нюанс, о котором я расскажу позже.
- Иметь механизм приложений: мы ведь не будем хардкодить все «экраны» в прошивке в виде кучи стейтов, верно? Для этого у нас должен быть простой и понятный механизм слинкованных с прошивкой приложений.
- Обрабатывать ввод: обработка тачскрина и жестов — это задача подсистемы ввода.
- Реализовывать анимированный UI: здесь всё очевидно, наша прошивка должна иметь готовые элементы пользовательского интерфейса для будущих приложений: кнопки, текстовые поля и т. д. О деталях реализации этой подсистемы, я расскажу ниже (а реализовал я её очень необычно для такой системы).
Начинаем мы с хардварной части. Именно здесь я покажу вам, как использовать внешние шины вашего устройства.
❯ Аппаратная часть
В качестве смартфона для нашего проекта, я выбрал популярную бюджетную модель из 2012 года — Fly IQ245 Wizard. Это простенький китайский смартфон, который работал на базе популярного в прошлом 2G-чипсета: MediaTek MT6573, да и стоил около 2х тысяч рублей новым. Однако вот в чём суть: мне удалось заставить работать «медиатековский» модем и даже позвонить с него на свой основной телефон, но… только ввод и вывод данных из звукового тракта модема происходит через звуковую подсистему Android — к которой доступа у нас нет!
Именно поэтому, мы идём на очень хитрый и занимательный костыль: мы распаяем внешний модем сами! В качестве радиомодуля у нас выступит модуль SIM800 от компании SIMCOM. И даже он очень близок к нашему смартфону в аппаратном плане: ведь в основе этого модуля лежит популярнейший чипсет из кнопочников тех лет: MediaTek MT6261D. Преимущество SIM800 в его цене — он стоит пару сотен рублей, так что по карману выбор модема не влияет.
На весу паять крайне неудобно. В финальном варианте перепаяю нормально.
Но как его подключать? SIM800 общается с другими устройствами посредством протокола UART — универсальный асинхронный приемо-передатчик. И вот тут мы включаем смекалочку. Разбираем устройство и видим то, что я пытаюсь долгое время донести до моих читателей — аж два канала UART: один практически посередине, второй справа. Нам нужны пятачки TXD4 и RXD4:
Обычно на этот канал UART летят логи ядра, которые можно без проблем отключить минорной правкой U-Boot в HEX-редакторе. Впрочем, модем никак не реагирует на «мусор» из консоли и просто отвечает ошибками — хватит лишь очистить буфер сообщений для того, чтобы все работало нормально. Подпаиваемся к UART'у с помощью преобразователя — у меня оным выступает ESP32 с выпаянным чипом.
Увидели логи? Замечательно, пора попытаться что-то отправить на ПК и с ПК. UART работают без тактовых сигналов и зависит исключительно от старт/стоп битов и бодрейта, который на устройствах MediaTek равен 921600. TXD4 и RXD4 обнаруживаются в системе на консоли /dev/ttyMT3. Пробуем что-то отправить: всё работает!
Вот теперь-то можно подключить наш внешний модем и попытаться пообщаться с ним, отправив тестовую команду AT. Модем отвечает OK! На этот раз я работаю с смартфоном из режима Factory mode — практически тоже самое, что и режим recovery, но позволяющий, например, получить доступ к камере устройства. Простая и понятная схема, поясняющая что и куда подключать:
На этом модификация аппаратной части пока закончена. Пора переходить к реализации софта! Я решил разделить материал на каждый модуль, который я реализовывал — дабы вам был понятен процесс разработки и отладки прошивки!
❯ Заставляем смартфон запускать нашу прошивку
На этот раз я решил загружать смартфон из режима рекавери. Однако никто не мешает в будущем просто прошить раздел recovery вместо boot и получить прямую загрузку прямо в нашу прошивку. Время такой загрузки будет занимать ~3-4 секунды с холодного старта. Очень даже ничего.
Я взял уже готовый образ TWRP для своего смартфона и пропатчил его, дабы сам рекавери не мешал своим интерфейсом. Для этого я распаковал образ recovery.img с помощью MtkImgTools и убрал в init.rc запуск службы /sbin/recovery. После этого, я залил прошивку обратно на устройство и получил подобную свободу действий — консоль через USB и чистый холст в виде смартфона! Старые смартфоны на чипсетах MediaTek шьются через USB только после замыкания тест-поинта — на моем аппарате его местонахождение очевидно. Замыкаем контакты между собой, подключаем смартфон без АКБ к ПК и ждем прошивки:
Теперь можно деплоить программы! Важный нюанс: в отличии от Makefile из прошлой статьи, для Android 2.3 параметр -fPIE нужно убрать — иначе динамический линкер (/sbin/linker) будет вылетать в segmentation fault.
❯ Графическая подсистема
В комментариях под прошлой статьёй меня похвалили за то, что я делюсь достаточно профильными знаниями касательно эффективной отрисовки 2D-графики. Собственно, к реализации графической подсистемы я подошёл ответственно и постарался реализовать достаточно шустрый рендерер, к которому затем можно подключить другие модули.
Как я уже говорил ранее, графическая подсистема должна уметь загружать картинки, выводить некоторые примитивы, выводить картинки с прозрачностью и без, загружать и отрисовывать заранее подготовленные шрифты, а также управлять отрисовкой бэкбуфера на экран.
В случае с этим устройством (и большинством старых устройств), формат пикселя оказался 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 %sn", fileName);
if(!f)
{
LOGF("Unable to open %sn", fileName);
return 0;
}
CTgaHeader hdr;
fread(&hdr, sizeof(hdr), 1, f);
if(hdr.paletteType)
{
LOG("Palette images are unsupportedn");
return 0;
}
if(hdr.bpp != 24 && hdr.bpp != 32)
{
LOG("Unsupported BPPn");
return 0;
}
unsigned char* buf = (unsigned char*)malloc(hdr.width * hdr.height * (hdr.bpp / 8));
if(!buf)
{
LOG("Memory exhaustedn");
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%in", 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);
}
}
}
Теперь у нас есть отображение картинок и текста! Что с этим можно сделать?
❯ Обработка ввода
Конечно же, реализовать обработку ввода! Это будет фактически минимальный функционал, который можно использовать для создания 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-стек, в том числе декодирование и вывод звука через встроенный усилитель с фильтром — остается только подключить динамики и микрофон от нашего телефона. Пока что я подсобрал аудиотракт на том, что было под рукой — микрофон от нерабочего смартфона и динамик от планшета, но для проверки этого хватает:
Важный нюанс: по умолчанию, 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("ATrn", 250);
GetATResponse((char*)&atBuf, sizeof(atBuf));
if(!CheckATStatus((char*)&atBuf))
{
printf("Failed to initialize modem: Modem isn't anwered OKn");
printf("Modem response: %sn", &atBuf);
return;
}
LOG("AT = OK, ready to operaten");
...
void CModem::SendAT(char* command, int waitTime)
{
int result = write(modemFd, command, strlen(command));
if(!result)
LOGF("SendAT failed: %in", 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;rn", number);
LOGF("Dialing %sn", buf);
SendAT(buf, 250);
GetATResponse((char*)&atResponse, sizeof(atResponse));
LOGF("Dial response: %sn", &atResponse);
}
void CModem::Hang()
{
char atBuf[64];
SendAT("ATHrn", 250);
GetATResponse((char*)&atBuf, sizeof(atBuf));
LOGF("ATH: %sn", &atBuf);
LOG("Hangn");
}
Пытаемся позвонить с помощью метода Dial и видим, что всё работает! Это очень круто! А теперь, конечно же, самое время переходить к реализации того, чего вы ждали — пользовательского интерфейса!
❯ Главный экран
К выбору концепции для интерфейса, я поступил максимально просто — «слизал» дизайн первых версий iOS. Как по мне, это одни из самых красивых версий iOS вообще — все эти приятные градиенты и переливания. Конечно, я не так крут, как инженеры Apple, да и мощного UI-фреймворка у меня пока что нет, поэтому я приступил к реализации с «минимальным» функционалом.
Начал я с разделения главного экрана на модули и продумывания архитектуры основного «лаунчера». У нас есть статусбар, который рисуется поверх всех приложений, полка с приложениями — 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. Нам понадобился новый элемент интерфейса, который описывает кнопку номеронабирателя? Мы просто реализовываем ещё один метод, который берет за основу стандартную кнопку и дорисовывает к ней текст. Всё крайне просто и понятно, хотя на данный момент слишком захардкожено. :)
❯ Звоним!
Пришло время совершить первый звонок с нашей по настоящему кастомной прошивки. Набираем номерок и…
Да, всё работает и мы без проблем можем дозвониться :)
❯ Заключение
Конечно же, это далеко не весь функционал, необходимый любому современному смартфону. Здесь много чего еще нужно реализовать хотя бы для соответствия уровню бюджетных кнопочных телефонов: телефонную книгу, поддержку СМС/ММС, мультимедийный функционал с играми. Однако начало уже положено и самая необходимая часть модулей реализована. Этот проект очень занимательный для меня и я горд, что смог не на словах, а на деле показать вам, моим читателям, возможности моддинга совершенно NoName-устройств, без каких либо опознавательных знаков…
Моя задача заключается в том, чтобы показать вам возможности использования старых телефонов не только в потребительских, но и в гиковских DIY-сферах. Судите сами: огромный классный дисплей, емкостной тачскрин, готовый звук, камера — и всё это за каких-то пару сотен рублей. Главное показать людям, как всю эту мощь использовать в своих целях и делать совершенно новые устройства из существующих, а не выбрасывать их на помойку!
Сейчас смартфоны, подобные Fly из этого поста стоят копейки, а портировать на них прошивку можно без каких-либо трудностей. Я очень надеюсь, что после этого поста читатели попытаются сделать что-то своё из старых смартфонов, благо свои наработки я выкладываю на GitHub!
Автор: Богдан