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

в 8:01, , рубрики: Без рубрики
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 %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);
        }
    }
}

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

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

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

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!


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

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

Автор: Богдан

Источник

* - обязательные к заполнению поля


https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js