Минутка ностальгии

Моё знакомство с N70 Music Edition началось в далёком 2006 году. Мне его подарили на новый год в декабре. Music Edition это расширенная комплектация с флешкой DV RS‑MMC на 1 Гб и нормальными наушниками (гарнитурой). По железу: обычная версия и ME — идентичные. Моей радости тогда не было предела. Телефон поддерживал все что можно было желать в середине нулевых. Умел записывать видео, цветной экран, диктофон, радио, полноценная поддержка MP3.

Кнопки нехило так стерлись

Кнопки нехило так стерлись

Но самое главное N70 — смартфон. Можно было устанавливать полноценные приложения на ОС Symbian которые могли взаимодействовать с железом телефона. В N70 даже реализована полноценная многозадачность: именно тогда я узнал, что приложения можно сворачивать и продолжить работать позже с того же места. При этом телефон работал невероятно стабильно в отличии от глючных и ломучих в то время Сименсов (по крайней мере мне с ними не везло). Полный сброс я делал всего раз, уже даже не помню почему.

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

Пакетный интернет в 2006 уже появился и это позволяло за скромные суммы скачивать всякое при помощи GPRS. Чуть позже появился 3G, тем самым скорость стала ещё больше. В те времена я этот телефон не выпускал из рук. ПК у меня не было, информацию я получал с WAP мобильных сайтов. Но была Opera Mini которая умела открывать любые ресурсы, при этом экономила трафик. Апофеозом всего стало то, что я с телефона делал сайты. И не просто на конструкторе форм, а полноценные PHP/JS. На движках JohnCMS/Motor CMS. Телефон почти заменял мне компьютер. Проходил я с ним почти 6 лет. Только в 2012 заменил на Андроедо‑девайс. Телефон остался пылиться на полке, таскал его с одной съемной квартиры на другую. Иногда использовал в качестве звонилки.

Шло время. Убираясь в своих железяках я вспомнил про N70. Обнаружил его в пакете с проводами. И вот она — старая Нокия, которую я не выкинул и не продал. Включить не получилось, аккумулятор давно умер. Мультиметр показывал 0В, то есть находится в глубоком разряде. Испытывать судьбу я не стал, заказал новую батарею BL-5C на маркетплейсе. Да, потом ещё вспомнил, что и зарядки к телефону нет. С зарядка песня: Micro‑USB тогда не существовало как стандарта, а Mini‑USB в этот телефон не положили. Для зарядки используется тонкий штырьковый разъем. Тоже пришлось заказать. Включил. Телефон работает и даже карта памяти завелась, время конечно же сбито как и дата. На карте памяти обнаружил фото времён студенчества (лучше бы обнаружил биткоин‑кошелек).

Возникла идея. А что если написать на него что‑то?

Как в то время разработчики делали приложения для Symbian 8.1?

Python 2.2.2

Вообще простейший Hello World можно написать через интерпритатор Python 2 версии.

Python появился на Nokia N70 благодаря официальному проекту самой компании Nokia под названием PyS60 (Python for S60), который был запущен в 2005 году. Nokia Research Center решила портировать популярный скриптовый язык Python на свою мобильную платформу Series 60, чтобы упростить создание программ.

И энтузиастами писались многие приложения для этой платформы. На сайте dimonvideo даже был целый отдельный раздел для таких поделок. Но мы с вами — не ищем легких путей.

Symbian 8.1: истоки

Архитектура Symbian OS 8.1 возникла не с нуля. Её основой стала операционная система EPOC, разработанная британской компанией Psion для карманных компьютеров и электронных органайзеров в 1980–1990-х годах.

Особенно большое влияние на Symbian оказали устройства семейства Psion Series 5, работавшие под управлением EPOC32. Именно от этой системы Symbian унаследовала значительную часть архитектурных принципов, программных интерфейсов и инструментов разработки.

Наиболее интересна преемственность не внешнего вида устройств, а программных интерфейсов.

Компонент

Происхождение и наследование

Symbian C++

Развился из C++ API EPOC32

Active Objects

Механизм асинхронных операций EPOC32

Cleanup Stack

Механизм безопасного освобождения памяти при Leave

Descriptors

Система строк и буферов EPOC32

CBase, R‑классы

Соглашения об управлении объектами и ресурсами

RFs, RFile

API файлового сервера EPOC32

RWsSession

Интерфейс оконного сервера

CCoeControl

Базовый компонент управления GUI, восходящий к CONE

EIKON

GUI‑фреймворк Psion, ставший основой для последующих UI‑платформ

OPL

Язык программирования устройств Psion, сохранившийся в части экосистемы Symbian

Эти механизмы существовали задолго до Symbian OS 8.1. Архитектура EPOC32 уже в 1997 году включала микроядерный подход, системные серверы, файловую подсистему, оконный сервер и объектно‑ориентированные библиотеки.

Важно отметить, что EPOC16 и EPOC32 — разные кодовые базы. От Psion Series 3 система унаследовала прежде всего идеи и традицию разработки, тогда как непосредственным техническим предшественником Symbian OS 8.1 стала EPOC32 устройств Series 5 и 5 mx.

Архитектура ОС

Архитектура Symbian 8.1 построена по многослойному принципу. Основным языком разработки был C++ (C++98 с характерными идиомами Symbian), дополненный специфическими механизмами управления памятью, обработки событий и взаимодействия с системными службами.

Про внутренности N70 не вижу смысла подробно растекаться, так как на Хабре это недавно описал monobogdan в этой замечательной статье.

В Symbian 8.1 существовали варианты с ядрами EKA1 и EKA2, поэтому некоторые низкоуровневые особенности зависели от конкретного устройства.

В Symbian 8.1 существовали варианты с ядрами EKA1 и EKA2, поэтому некоторые низкоуровневые особенности зависели от конкретного устройства.

В классической модели GUI‑приложений Symbian C++ используются несколько основных компонентов:

  • Application (CApaApplication) — описывает приложение и создаёт объект документа.

  • Document (CApaDocument) — управляет моделью данных приложения.

  • AppUi (CAknAppUi или CEikAppUi) — обрабатывает команды пользователя и управляет интерфейсом.

  • View/Container (CCoeControl) — отображает элементы интерфейса и обрабатывает события управления.

Конкретные классы зависят от платформы пользовательского интерфейса. Например, S60 использует Avkon, а UIQ — собственный UI‑фреймворк. Поэтому приложение для S60 не обязательно совместимо с UIQ, даже если оба устройства работают под Symbian OS 8.1.

Для работы с памятью используются классы с соглашениями об именовании C, R, T и M. Например, классы категории C обычно наследуются от CBase, а классы категории R часто представляют ресурсы, которые необходимо явно закрывать.

Вместо обычной обработки исключений C++ применяется механизм Leave, а для безопасной очистки объектов при ошибках — Cleanup Stack.

Одна из важнейших особенностей Symbian — механизм Active Objects. Он позволяет выполнять асинхронные операции без создания отдельного потока для каждой задачи. Например, приложение может отправить запрос на чтение файла или получение данных по сети, продолжить обработку других событий и получить уведомление после завершения операции.

Основные элементы этой модели — CActive, CActiveScheduler и методы RunL(), DoCancel(). Такой подход снижает потребление памяти и особенно полезен на мобильных устройствах. Вообще, про это можно и дальше рассказывать, но вышесказанного нам хватит чтобы написать простой Hello World.

SDK которому 20 лет...

Первым же делом встал вопрос: с новейшими операционными системами возможно ли написать код для Symbian? MacOS — нет, дистрибутивы Linux — тоже нет. Ставить отдельно ОС куда‑то не хотелось. Выжимка в гугле активно предлагала виртуалку под WinXP. Проверяем.

Два компонента которые точно нужны:

Не уверен что эти ссылки долго проживут. Залил еще в облако.

И спешу обрадовать: на Windows 11 x64 ставится без проблем. По хорошему еще нужна старая IDE типа CodeWarrior Development Studio. Эта IDE платная, ключи уже купить не получится, а триалка нам не очень подойдет. Если не поставим CodeWarrior мы теряем возможность отлаживать приложение на эмуляторе: будем отлаживать на N70. Поэтому нам еще нужен какой‑нибудь редактор кода. Отлично подойдет любая версия VS Code.

Какой еще Perl?

В эпоху Symbian OS 8.1 основная разработка велась в Windows XP, а ActivePerl предоставлял готовый интерпретатор Perl для этой среды. У него было несколько преимуществ: он позволял автоматизировать сборку, выполнять скрипты на разных конфигурациях Windows и не требовал написания отдельной системы сборки для каждой IDE.

При этом совместимость зависела от версии SDK. В старых инструментах встречаются требования к ActivePerl 5.6.1:

Последовательность примерно такая

Последовательность примерно такая

Таким образом, ActivePerl был важной частью среды разработки под Symbian OS 8.1, но не выполнялся на телефоне и не требовался для работы уже скомпилированного приложения.

Настраиваем SDK

Важные требования к установке, пришедшие из 2004 года:

Путь к SDK — без пробеловC:\Symbian\8.1a\S60_2nd_FP3

Perl 5.6.1 должен быть первым в PATH — любой современный Perl (5.30+) ломает скрипты сборки.

Переменная окружения EPOCROOT в каноническом формате: \Symbian\8.1a\S60_2nd_FP3\ — без буквы диска, с ведущим и замыкающим слэшем. Инструменты сами подставляют букву текущего диска.

Проект должен лежать на том же диске, что и SDK, и тоже без пробелов в пути.

$env:EPOCROOT = '\Symbian\8.1a\S60_2nd_FP3\'
$env:PATH = "C:\Perl\bin;C:\Symbian\8.1a\S60_2nd_FP3\Epoc32\gcc\bin;C:\Symbian\8.1a\S60_2nd_FP3\Epoc32\tools;$env:PATH"

Важный момент, о котором обычно умалчивают мануалы: оригинальный SDK распространялся как .exe‑установщик, который сам прописывал все переменные окружения.

Разбирая содержимое SDK, я обнаружил в Epoc32\gcc\bin полноценный кросс‑компилятор GNU. А команда bldmake plat (встроенная справка по платформам) честно показывает привязку таргетов к компиляторам:

Ниже THUMB

Ниже THUMB

Расшифровка: сборка под реальное железо (armi/thumb) идёт через GNU GCC, который лежит прямо в SDK. Старые коммерческие компиляторы нужны только для таргетов эмулятора (wins/winscw). То есть цикл «написал → собрал → поставил на телефон» полностью замкнут бесплатными средствами из коробки — нужно только уметь пользоваться командной строкой. Пояснение по таргетам ARM:

  • ARMI — интерворкинг‑таргет, бинарник совместим и с ARM и с THUMB‑кодом (рекомендовался для дистрибуции)

  • ARM4 — чистый 32-битный ARM (быстрее, толще)

  • THUMB — 16-битные инструкции (компактнее, чуть медленнее) — дефолт в pkg‑шаблонах Nokia.

Забавная деталь эпохи: GCC здесь выдаёт бинарники в формате PE (Portable Executable, формат Windows DLL), которые затем пропускаются через PETRAN — постпроцессор Symbian, преобразующий PE‑контейнеры в E32-образ (формат исполняемых файлов Symbian EKA1).

Архитектура Symbian‑приложения

Symbian 8.1 — это EKA1, микроядерная архитектура с клиент‑серверной моделью: любой доступ к железу и системным сервисам — через серверы (Window Server, File Server, Font&Bitmap Server). Приложение — обычный E32-модуль с точкой входа E32Dll (да, приложение формально является DLL).

Классическое S60-приложение строится по паттерну Application‑Document‑App UI‑View — четырёхуровневой иерархии, которую навязывает фреймворк UI Control Environment (CONCERN → CONE) и Avkon (S60-надстройка над Uikon):

Файл hellon70application.cpp содержит сердце привязки к системе — UID:

const TUid KUidHelloN70 = { 0x70000070 };  // UID3 — идентификатор приложения

Ключевые идиомы Symbian C++, без которых код не соберётся и не заработает:

  • Two‑phase construction: конструктор C++ не может оставить (Leave — местный аналог исключений), поэтому вся оставляющая инициализация — в ConstructL()

  • Cleanup Stack: CleanupStack::PushL()/PopAndDestroy() — ручное управление памятью в мире без деструкторов‑гарантий (в 8.1 ещё нет std::auto_ptr‑подобных прелестей для UI‑кода)

  • Дескрипторы (TBuf, HBufC, TPtrC) вместо char* — строки с длиной в первом байте, 8- и 16-битные.

Весь интерфейс описывается декларативно в .rss‑файлах (RSG‑компилятор превращает их в бинарные .rsc и заголовки с ID). Строки вынесены в .loc

Иконки — отдельный AIF‑файл (Application Information File): aif/hellon70aif.rss объявляет UID, количество иконок и embeddability, а bitmaps (42×29 list и 44×44 context/mask в моём случае взяты из демо‑набора SDK) прилинковываются прямо в .aif.

MMP‑файл — это декларативный манифест сборки (никаких CMake, это 1997+ стиль).

Отрисовка и упаковка в.sis

Мы уже в конце пути до установочного файла. Шаблонный HelloWorldBasic показывает текст только через диалог из меню. За отрисовку отвечает CHelloN70AppView::Draw() — вызывается фреймворком CONE при первой отрисовке окна и при каждой инвалидации:

void CHelloN70AppView::Draw( const TRect& /*aRect*/ ) const
    {
    CWindowGc& gc = SystemGc();     // графический контекст Window Server
    TRect rect = Rect();            // экстент контрола
    gc.Clear( rect );               // залить фоном

    _LIT( KHelloText, "Hello Habr 2026!" );

    const CFont* font = iCoeEnv->NormalFont();               // системный шрифт
    TInt textWidth = font->TextWidthInPixels( KHelloText );  // метрики
    TInt baseline = rect.Height() / 2 + font->AscentInPixels() / 2;

    gc.UseFont( font );
    gc.SetPenColor( KRgbBlack );
    gc.DrawText( KHelloText,
                 TPoint( ( rect.Width() - textWidth ) / 2, baseline ) ); // центр
    gc.DiscardFont();
    }

Выше я писал, что от лицензируемой IDE я отказался, поэтому соберем свой виндовый скрипт, которым будем собирать проект (build.cmd) и будем юзать его в дальнейшем:

@echo off
set EPOCROOT=\Symbian\8.1a\S60_2nd_FP3\
set PATH=C:\Perl\bin;%EPOCROOT%Epoc32\gcc\bin;%EPOCROOT%Epoc32\tools;%PATH%
set PRJ=D:\work\HelloN70
cd /d "%PRJ%\group" && cmd /c bldmake bldfiles && cmd /c abld.bat build thumb urel
cd /d "%PRJ%\sis" && makesis.exe hellon70.pkg
copy /y HelloN70.sis D:\work\
2:11 ночи

2:11 ночи

И вот, достаем картридер, скидываем на него HelloN70.sis, вставляем фрешку обратно, устанавливаем.

При установке ругнется на сертификат, не обращаем внимания

При установке ругнется на сертификат, не обращаем внимания

Можно без телефона

В SDK лежит готовый эмулятор телефона (Epoc32\release\winscw\udeb\epoc.exe) — красивое окно с интерфейсом S60 (внизу будет скриншот). Но собрать своё приложение под него нельзя без CodeWarrior/VC6: таргеты winscw/wins требуют компиляторов, выпущенных до 2005 года. Поэтому стратегия — тестирование сразу на железе.

epoc.exe

epoc.exe

Как там с сетью на нём?

Когда я вставил симку Билайна: сеть вроде бы есть, но 3G значка не вижу и почему то ничего не открывалось. Вставил симку Т2: значок 3G есть, но сайты все равно не открываются.

Nokia N70 умеет работать с базовым шифрованием, но поддерживает только устаревшие версии протоколов — SSL 3.0 и TLS 1.0. Современные стандарты TLS 1.2 и TLS 1.3 смартфон из коробки не поддерживает, поэтому большинство актуальных сайтов и сервисов напрямую на нем не откроются. Тут либо заходить на сайты без шифрования — которых почти не осталось.

Либо юзать Opera mini:

Пишу статью с Nokia N70

Пишу статью с Nokia N70

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

Но надо бы написать что‑то поинтереснее строки на экране...

Пишем пейджер для Telegram

У Nokia N70 всё ещё есть экран, клавиатура и возможность установить нативное приложение. Для коротких сообщений этого достаточно. Проблемы начинаются, когда на другом конце соединения оказывается современный сервис с HTTPS, JSON и постоянно меняющимся API.

TgPager связывает эти два мира: на телефоне работает небольшое приложение для Symbian, на сервере — Python‑прокси, а собеседник пишет обычному Telegram‑боту. Сообщение появляется на Nokia; ответ можно набрать с её клавиатуры. Получается двусторонний пейджер.

Публичный Telegram Bot API требует HTTPS. В этой реализации клиент на N70 использует обычный TCP и не решает задачу совместимости старого TLS‑стека с современным сервисом. Эту обязанность выполняет серверная urllib.request.

Немного деталей прокси (серверная python‑часть)

Вся серверная реализация находится в одном файле на 482 строки. Внешних Python‑зависимостей нет: используются socket, threading, collections, urllib.request, json, logging, signal и другие модули стандартной библиотеки.

Сервер разделён на три исполняемые части:

  • Главный поток принимает TCP‑соединения

  • Фоновый telegram_worker() забирает обновления Telegram

  • Для каждого TCP‑клиента создаётся поток handle_client()

_queue = collections.deque(maxlen=100)
_cond = threading.Condition()

Поток Telegram собирает подходящие сообщения из одного ответа getUpdates, кладёт пакет в очередь под блокировкой и вызывает notify_all().Обработчик POLL проверяет очередь под той же блокировкой. Если она пуста, ждёт через Condition.wait().

На время ожидания блокировка освобождается, поэтому Telegram‑поток может добавить новые тексты.При получении данных обработчик забирает накопленный пакет, очищает общую очередь и отправляет строки MSG, а затем END.

Это очередь с конкурирующими потребителями: один пакет получает один обработчик POLL. Несколько подключённых телефонов не получат одинаковую копию сообщения. Отдельных пользователей, персональных очередей и широковещательной доставки здесь нет.

Клиент на N70

Клиент написан с использованием дескрипторов Symbian: TBuf, TBuf8, TDesC, TPtrC. У буфера задана ёмкость, поэтому длина строки становится частью проектирования.

В кодировщике проверяется оставшаяся ёмкость перед добавлением байтов. Это необходимо: 200 единиц UTF-16 не равны 200 байтам UTF-8, а экранирование тоже увеличивает размер.

Создание объектов выполняется через new (ELeave), фабрики NewL() и двухфазную конструкцию. Во время потенциально прерываемой инициализации объект помещается на CleanupStack. После успешного ConstructL() его снимают со стека.

Например, так создаётся движок:

CTgEngine* CTgEngine::NewL(MTgUiObserver& aUi)
    {
    CTgEngine* self = new (ELeave) CTgEngine(aUi);
    CleanupStack::PushL(self);
    self->ConstructL();
    CleanupStack::Pop(self);
    return self;
    }

CTgConn наследуется от CActive. Вместо отдельного потока под каждое соединение телефон использует асинхронные запросы и active scheduler.

Последовательность операции следующая:

  • Код запускает асинхронный запрос, передавая iStatus

  • Вызывает SetActive() и возвращает управление планировщику

  • После завершения запроса планировщик вызывает RunL()

  • RunL() проверяет результат и выбирает следующий шаг по текущему состоянию.

Основной путь подключения можно представить так:

Для доменного имени используется RHostResolver, для TCP — RSocket, для повторной попытки — RTimer. Общая сессия socket server хранится в CTgEngine как RSocketServ.

Для доменного имени используется RHostResolver, для TCP — RSocket, для повторной попытки — RTimer. Общая сессия socket server хранится в CTgEngine как RSocketServ.

Сложность этой модели — в согласованности состояний и отмены. Нужно знать, какая операция сейчас активна, что отменять при отключении и можно ли уже начинать новую запись. Метод DoCancel() отдельно обрабатывает DNS, таймер и сокет. Попытка запустить операцию поверх активной возвращает KErrInUse.

Ожидание сети само по себе не должно занимать UI‑поток синхронным чтением. Но обработчики RunL() всё равно должны быстро возвращать управление: active objects не превращают длительный вычислительный код в фоновую работу.

Пришлось сделать два соединения: ждать сообщение и отправлять ответ. У приёмного и отправляющего каналов разные задачи.

  • CTgPollConn Подключение → AUTH → повторяющиеся POLL

  • CTgSendConn Подключение → AUTH → один SEND → ответ → закрытие

Два TCP‑сокета позволяют отправлять текст, пока приёмный канал ждёт данные. Они не означают, что приложению обязательно нужны два независимых подключения к мобильному оператору.

POLL реализует long polling: сервер отвечает при появлении сообщения либо по истечении ожидания. После END телефон сразу повторяет запрос по тому же соединению. Прокси отвечает в уже открытый телефоном канал, поэтому подключаться к Nokia извне не требуется.

Чтение тоже асинхронное:

TInt CTgConn::IssueRead()
    {
    if (IsActive())
        return KErrInUse;
    iState = EBusyR;
    iSock.RecvOneOrMore(iRx8, 0, iStatus, iXfr);
    SetActive();
    return KErrNone;
    }

TCP возвращает произвольные фрагменты байтов, поэтому один RecvOneOrMore() не равен одному сообщению. Метод Feed() накапливает данные до \n, а затем передаёт готовую строку обработчику. Строка MSG декодируется из UTF-8 в UTF-16 и через движок передаётся интерфейсу.

При отправке выполняется обратное преобразование. Участок CTgSendConn::Send() готовит полезную нагрузку:

iPayload.Zero();
iPayload.Append(_L8("SEND "));
TBuf8<2048> enc;
if (!TgUtf::Encode(aText, enc))
    return KErrTooBig;
if (iPayload.Length() + enc.Length() > iPayload.MaxLength())
    return KErrTooBig;
iPayload.Append(enc);

TgUtf::Encode() кодирует текст в UTF-8 и экранирует обратные слеши и переводы строк. После авторизации команда передаётся через IssueWrite(), который добавляет завершающий LF. Текст с несколькими строками остаётся одной строкой протокола.

Собираем вышеупомянутым build.cmdсмотрим результат...

Результаты

Вместо тысячи слов:

В целом, если так чатиться выходит дешевле СМС

В целом, если так чатиться выходит дешевле СМС

Оно даже работает в свернутом состоянии. Если свернуть приложение, то можно увидеть что, соединение с интернетом продолжается. А если послать сообщение из бота, то можно услышать писк, а снова зайдя в приложение увидим это:

Заметьте, приложение на весь экран (без плашки вверху)

Заметьте, приложение на весь экран (без плашки вверху)

Итоги

Началось всё с найденной в пакете с проводами Nokia, а закончилось собственным приложением и перепиской через Telegram. Старый SDK удалось запустить на современной Windows, а несовместимость с современным HTTPS обойти через небольшой прокси. Когда‑то на этом телефоне я делал простые сайты, а двадцать лет спустя написал приложение для него самого — повод снова зарядить старую Нокию нашёлся.

Исходные коды пейджера и self‑hosted серверной части можете найти в репо s60-tgpager. Там же вы найдете Hello World. А также бонус: скилл для агента, если захотите написать код с помощью агентов для Nokia N70/N72.

Всем спасибо за внимание! Если тема найдет отклик — напишем для платформы S60 еще что‑нибудь интересное.