Эта статья - продолжение моей предыдущей статьи: Создаем собственную базовую станцию при помощи SDR. В прошлый раз я экспериментировал с 2G GPRS, но на старых телефонах эта технология не поддерживается, а мне хотелось запустить WAP именно на таком. О том, как вернуться в эпоху до распространения GPRS - читайте далее.

Ведение

Краткое содержание предыдущей статьи:
Используя относительно дешевый SDR трансивер OpenSourceSDRLab PlutoSky 7020 и набор программ Osmocom, я смог запустить собственную 2G базовую станцию. С ее помощью можно было совершать звонки между телефонами, обмениваться SMS, и пользоваться на телефонах GPRS интернетом.

Тем не менее, не все старые телефоны поддерживают GPRS. До появления GPRS существовала технология CSD (Circuit Switched Data). Передача данных идет на скорости максимум 9,6 кбит/с и очень похожа на передачу голоса (по сути, вместо голосового трафика идут данные). Соединение устанавливается через звонок на другой телефон с модемом — очень похоже на dial‑up (но в CSD данные сразу идут в цифре).
Технология настолько древняя, что многие операторы сейчас ее отключают, или не предоставляют физическим лицам. Поэтому, если хочется поэкспериментировать с интернетом на очень старом мобильном телефоне, без собственной базовой станции (БС) не обойтись.

Все эксперименты с БС в предыдущей статье я делал, рассчитывая запустить CSD и посмотреть, как выглядит мобильный интернет на древнем телефоне Siemens C35i, но в тот раз мне это не удалось.
Однако позже, благодаря помощи @axilirator мне все-таки удалось запустить CSD.

CSD

Как вообще устанавливается тестовое CSD соединение?

Нужно иметь два телефона, поддерживающих CSD, и каким-то доступным образом подключить их к компьютеру. Я для этого использовал переходник USB-UART (у моего Siemens C35i на разъем выведен именно UART), и я подключал только RX/TX/GND. Подходящий кабель сейчас найти проблематично, так что я использовал ненужный FPC-шлейф с шагом 1 мм. Решение ужасно ненадежное, но все же рабочее:

Интересно, что SIM-карта на фото была куплена вместе с этим телефоном (в 2002 году), и все еще работает с моей БС. Родной аккумулятор телефона давно испортился, однако, как оказалось, туда можно установить дешевый аккумулятор BL-5C.

После того, как оба телефона подключены к ПК, нужно подключится к ним, используя какой-нибудь терминал последовательного порта. Siemens C35i, насколько я понял, работает только на скорости 19200 бит/с.
Простейшая проверка работы модема, встроенного в телефон - отправить "AT", модем должен ответить "OK".
Далее нужно отправить "AT+CBST=71,0,1" на каждый их модемов. 71 - это скорость передачи данных 9600 бит/с протокол V.110, 1 - режим передачи данных Non-Transparent (про это далее). Модемы должны ответить "OK".
Далее нужно отправить команду "ATDnnnnnnnn", где nnnnnnnn - номер второго телефона. Телефон должен запустить CSD-вызов на второй телефон (для голосового вызова нужно, чтобы команда выглядела так: "ATDnnnnnnnn;").
Второй телефон должен увидеть входящий звонок, выдать в терминал "RING", после чего можно ввести в этот терминал "ATA" - подъем трубки.
При нормально работающей передаче данных в обоих терминалах должно появится сообщение "CONNECTED 9600", и модемы должны перейти в режим прозрачной передачи данных из терминала в терминал.

Правда, ранее, как я писал в предыдущей статье, я видел в логах Osmocom сообщения "We only support voice calls", и звонок даже не доходил до второго телефона. Как выяснилось позже, это было связано с тем, что программы Osmocom, взятые из репозитория Ubuntu 24.04, слишком старые.

Так как я проводил все эксперименты с GSM на отдельном ПК, я просто обновил на нем Ubuntu до 26.04. При этом обновились и программы Osmocom. На всякий случай я пересобрал библиотеки, нужные для работы трансивера и свой вариант OsmoTRX. После этого стек Osmocom нормально запустился, но главное - пропали сообщения "We only support voice calls", и звонок начал доходить до второго телефона.
Павда, как оказалось, CSD-соединение все равно не устанавливалось - через несколько секунд после ввода "ATA" звонок разрывался; по логам, полученным из Wireshark - с причиной "Cause = 16 Normal call clearing".

Вот тут стоит отметить определенную проблему, имеющуюся в Osmocom. В полноценной сотовой сети для поддержки CSD должен быть специальный программно-аппаратный блок IWF (Inter-working function). Его задача - сопряжение сети оператора с внешними сетями передачи данных (ISDN или даже обычные телефонные сети). Он выполняет следующие функции:

Преобразование протоколов: Адаптирует протоколы радиоинтерфейса GSM к стандартным протоколам фиксированных сетей (например, к ITU-T V.24, V.32, V.110).

Адаптация скорости передачи данных: Преобразует скорость передачи данных со стороны мобильного телефона (например, 9.6 или 14.4 кбит/с в CSD (CSD имел и более быстрые реализации) в стандартные фиксированные скорости каналов ISDN (обычно 64 кбит/с).

Модемный пул: Содержит цифровые сигнальные процессоры (DSP), эмулирующие работу аналоговых модемов, чтобы мобильный абонент мог дозвониться на обычный городской факс или dial-up модем.

Соответственно, CSD-данные между двумя телефонами идут не напрямую от одного телефона к другому, а через IWF.

И вот этого самого IWF в Osmocom нет. Трафик CSD внутри Osmocom передается через медиашлюз (osmo-mgw) по протоколу RTP с использованием кодека CLEARMODE (RFC 4040, payload type 120). В результате после установления связи (звонка) весь CSD трафик просто пробрасывается от одного телефона к другому. Вроде бы это нам и нужно?

Вот-тут то и появляются Transparent / Non-Transparent режимы передачи данных. Первый режим - более старый, данные просто перебрасываются от одного телефона к другому без каких-то модификаций. Второй - куда более сложный, для запуска передачи данных телефон должен договорится с IWF о поддерживаемых режимах работы, а далее протокол передачи данных будет поддерживать коррекцию ошибок. Этот протокол передачи данных называется RLP, и описывается стандартом GSM 04.22.
Выглядит это как-то так: MS <-- (RLP) --> IWF <-- (RLP) --> MS (MS - это телефон).
Вот и получается, что в Osmocom IWF нет, а телефоны об этом не знают, и обмениваются пакетами RLP не с IWF, а друг с другом: MS <-- (RLP) --> RTP-Clearmoode <-- (RLP) --> MS.
Судя по довольно малочисленным обсуждениям работы CSD в Osmocom, это как-то работает.
Тем не менее, различия в реализации протокола RLP у разных производителей телефонов могут приводить к проблемам в работе.
Мой телефон Siemens С35i поддерживает только Non-Transparent режим. Это можно проверить, выполнив AT-команду "AT+CBST=?", на которую телефон отвечает: +CBST: (0,4,6,7,68,70,71),(0),(1). Единственная "1" в конце это подтверждает.

Поначалу я пытался в качестве второго телефона использовать Sony Ericson k310i и модем Quectel M95. Оба не заработали, соединение рвалось. Попытки изменять разные режимы работы не помогали. После этого я решил попробовать купить еще один Б/У Siemens С35i - и с ним тоже не заработало, проблема осталась все та же.
По совету @axilirator включил в конфиге osmo-bts-trx настройку gsmtap-rlp skip-null. При ее включении osmo-bts-trx начинает выдавать GSMTAP пакеты, которые можно поймать в Wireshark.
Дополнительно, сам osmo-bts-trx нужно запускать с аргументом "-i 127.0.0.1" (или добавить в конфиг "gsmtap-remote-host 127.0.0.1").
Для просмотра RLP пакетов в Wireshark нужно включить фильтр "gsm_rlp". Просмотр пакетов только с верной CRC (а у меня было много и плохих): gsm_rlp.fcs.status == "Good".

Анализ RLP пакетов в Wireshark показал, что телефоны пытаются обмениваться пакетами типа XID, видно, как БС принимает пакеты от одного телефона и пересылает их на другой, но дальше дело не продвигается, а после нескольких попыток соединение разрывается:

Есть только XID пакеты, других нет.
Есть только XID пакеты, других нет.

В XID пакетах, отправляемых телефоном, содержатся настройки, задаваемые командой "AT+CRLP". IWF анализирует их и предлагает свой вариант.
Рассматривая данные в Wireshark я заметил, что все пакеты абсолютно одинаковы. При этом внутри пакета есть бит C/R (COMMAND/RESPONSE BIT), документация описывает его так:

The C/R-bit is used to indicate whether the frame is a command or response frame and whether the P/F‑bit is to be interpreted as a poll or final bit, resp. For commands, the C/R bit shall be set to "1", for responses it shall be set to "0".

Я предположил, что именно отсутствие передачи "0" от БС на телефон мешает протоколу продвинуться дальше. Самым простым решением этой проблемы я посчитал модификацию OsmoBTS. Для этого требуется скачать ее, и собрать из исходников:

sudo apt-get install libosmo-abis-dev
sudo apt-get install libosmo-netif-dev

#Я использовал версию 1.9.0, совпадающую с той, что у меня установлена в Ubuntu 26.04
git clone --branch 1.9.0 https://github.com/osmocom/osmo-bts

cd  osmo-bts
autoreconf -fi
./configure --enable-trx
make

Я модифицировал файл sched_lchan_tchf.c и конкретно - функцию tx_tchf_fn() в нем. Эта функция отвечает за отправку данных к трансиверу (Downlink).
В ней есть такой участок:

case GSM48_CMODE_DATA_12k0:
		if (msg_tch == NULL)
			msg_tch = tch_dummy_msgb(4 * 60, 0x01);
		gsm0503_tch_fr96_encode(BUFPOS(bursts_p, 0), msgb_l2(msg_tch));
		if (msg_facch != NULL)
			gsm0503_tch_fr_facch_encode(BUFPOS(bursts_p, 0), msgb_l2(msg_facch));
		break;

Вот его-то и нужно переделать:

Модифицированный код
// You will need to add this line at the beginning of the file
// #include <osmocom/gsm/rlp.h>

/* CSD (TCH/F9.6): 12.0 kbit/s radio interface rate */
	case GSM48_CMODE_DATA_12k0:
		if (msg_tch == NULL)
			msg_tch = tch_dummy_msgb(4 * 60, 0x01);

        // Pointer to the data to be modified
        // Elements have values 0/1
		uint8_t *csd_data = msgb_l2(msg_tch);
		int csd_len = msgb_l2len(msg_tch);

		uint8_t rlp_buf[30];
		uint16_t byte_len = 0;
		if (csd_len == 240)
		{
            // Pack 240 bytes -> 30 bytes
			byte_len = osmo_ubit2pbit_ext(rlp_buf, 0, csd_data, 0, csd_len, 1);
			if (byte_len != 30)
				break;

            // Fill "rlpf" structure
			struct osmo_rlp_frame_decoded rlpf;
			int rc = osmo_rlp_decode(&rlpf, 0, rlp_buf, byte_len);

            // Calculate CRC of the data, last 3 bytes are received CRC
			uint32_t fcs = osmo_rlp_fcs_compute(rlp_buf, byte_len - 3);

			if ((rc == 0) && (rlpf.fcs == fcs))//check CRC
			{
				bool need_fix = false;
				if (rlpf.u_ftype == OSMO_RLP_U_FT_XID)
				{
					LOGL1SB(DL1P, LOGL_NOTICE, l1ts, br, "XID FOUND!\n");
					need_fix = true;
				}
				else if (rlpf.u_ftype == OSMO_RLP_U_FT_SABM)
					LOGL1SB(DL1P, LOGL_NOTICE, l1ts, br, "SABM FOUND!\n");
				else if (rlpf.u_ftype == OSMO_RLP_U_FT_UA)
					LOGL1SB(DL1P, LOGL_NOTICE, l1ts, br, "UA FOUND!\n");

				if (need_fix)
				{
					rlpf.c_r = false; // Set C/R to 0

					uint8_t changed_rlp[30];
                    // Fill bytes buffer and recalculate CRC
					osmo_rlp_encode(changed_rlp, 30, &rlpf);
                    
                    // Convert from 30 to 340 bytes
					osmo_pbit2ubit_ext((ubit_t *)csd_data, 0,
									   changed_rlp, 0,
									   csd_len, 1);
				}
			}
		}

		gsm0503_tch_fr96_encode(BUFPOS(bursts_p, 0), msgb_l2(msg_tch));
		if (msg_facch != NULL)
			gsm0503_tch_fr_facch_encode(BUFPOS(bursts_p, 0), msgb_l2(msg_facch));
		break;

Ради изменения одного бита приходится делать много операций. Важно, что данные находятся в особом виде, каждый бит передается отдельным байтом. Поэтому данные нужно преобразовать в набор классической последовательности байтов (из 240 байт - в 30), из полученного массива заполнить специальную структуру rlp-данных, с которой потом удобно работать. Именно в ней нужно поменять бит конкретно для XID-пакетов, а потом выполнить аналогичные операции кодирования в обратном порядке.

После сборки и запуска программы ./osmo-bts-trx -c osmo-bts-trx.cfg связь внезапно заработала. В обоих терминалах появилось "CONNECT 9600/RLP", после чего появилась возможность передавать данные между терминалами!

После модификации исходников можно установить модифицированный вариант osmo-bts-trx в систему, так ее проще запускать:

make
#Выполнить тесты
make check
sudo make install
sudo ldconfig

Честно скажу - не всегда соединение устанавливалось устойчиво, иногда оно самопроизвольно разрывалось через несколько десятков секунд после соединения. Причину этой проблемы я так и не нашел.
После этой доработки я еще раз попробовал проверить работу Siemens c Sony Ericson k310i и Quectel M95, но они так и не заработали.
Дополнительно отмечу, что для работы CSD хватает тех же настроек Osmocom, что у меня были сделаны для передачи голоса.
В принципе, получившуюся систему можно использовать, чтобы выходить в Интернет, используя старый ПК, но я не стал с этим экспериментировать.

Любопытная деталь - в Wireshark видно, что во многих RLP пакетах в их тексте проскакивает фраза "schobi macht das schon!!!". Иногда она передается в искаженном виде, хотя CRC пакетов нормальная. Складывается ощущение, что фраза служит для изначально заполнения пустых пакетов, а полезные данные пишутся поверх нее. Фразу формирует телефон - я нашел ее в самой прошивке телефона.

WAP

WAP - это целый набор довольно старых протоколов, появившихся в конце 90-х, предназначенных для предоставления мобильным устройствам прямого доступа к услугам интернета. Пользователи WAP получали доступ не к "полноценному" WEB - можно было открывать только специально созданные WAP-страницы, написанные на языке WML (по сути, это подвид XML). При этом, для сокращения объема передаваемых данных и уменьшения вычислительной нагрузки на телефон, все передача данных шла через специальные WAP-шлюзы, которые превращали текстовые WML-страницы в сжатые бинарные данные.
Со стороны пользователя (телефона) для доступа к ресурсам WAP нужен был специальный браузер. В моем Siemens C35i он есть, и я еще помню, как он работал в 2002 году.

О том, как именно настроить телефон и компьютер для организации доступа в WAP, очень подробно описано тут: https://bs0dd.net/index.php?lang=rus&page=news/main&npage=7 . Автор до сих пор поддерживает свой WAP-шлюз (091.122.205.250. Порт: 9201) и свой WML-сайт: wap.bs0dd.net
Спасибо автору сайта bs0dd за проделанную работу!

Со стороны телефона нужно указать:
- Номер набора (номер второго телефона на БС)
- Тип соединения - ISDN
- Имя пользователя и пароль (я указывал user2/user2)
- IP-адрес и порт - (я указал как раз 091.122.205.250 / 9201)
- Домашнюю страницу (к примеру, wap.bs0dd.net)

В брошюре оператора настройки описывались так:

Брошюра из 2002
Брошюра из 2002

После применения настроек при попытке зайти на домашнюю страницу второй телефон должен зазвонить.

Далее нужно настроить ПК, к которому будет подключен второй телефон. Так как Osmocom у меня работает на Linux, я использовал инструкции для него: Настройка Dial-in сервиса: Linux. Настроить нужно будет mgetty и pppd. mgetty инициализирует модем и принимает от него входящий звонок, pppd обеспечивает авторизацию подключающихся клиентов и их доступ в сеть.

Я практически во всем следовал инструкции, хотя некоторые настройки пришлось поменять.
Программа mgetty рассчитана на то, что у модема будет возможность аппаратно управлять потоком передачи данных - через специальные линии COM-порта. Однако у меня их нет. Для того, чтобы mgetty работала хоть как-то, мне пришлось закоротить на плате USB-UART адаптера линии RTS+CTS и DTR+DSR. Это явно кривое решение, mgetty часто не обнаруживает, что звонок завершен, и подвисает. Да и отсутствие полноценного аппаратного управления потоком явно может приводить к проблемам с передачей данных.

Настройки /etc/mgetty/mgetty.config у меня выглядят так:

debug 9
port ttyUSB2
port-owner root
port-group dialout
port-mode 0660
direct no
data-only yes
ignore-carrier yes
toggle-dtr no
speed 19200
modem-check-time 60

В конце /etc/ppp/options я не добавлял IP сервера, как это указано в инструкции.
Вместо этого я сделал файл /etc/ppp/options.ttyUSB2 таким:

#Первый IP - адрес сервера, второй - адрес телефона
10.0.0.1:10.0.0.2
netmask 255.255.255.255
noproxyarp
local

Также, как и описано в инструкции, я добавил в систему пользователя user2 с паролем user2.
Для организации маршрутизации в Интернет использовал эти команды:

# Разрешаем ядру Linux пересылать пакеты между интерфейсами
sudo sysctl -w net.ipv4.ip_forward=1

# wlo1 - имя интерфейса сетевой карты
sudo iptables -t nat -A POSTROUTING -s 10.0.0.0/24 -o wlo1 -j MASQUERADE

Контролировать работу mgetty и pppd можно в отдельных терминалах:

tail -f /var/log/mgetty/mg_tty*.log

sudo tail -f /var/log/syslog | grep pppd

# Перезапуск mgetty:
sudo systemctl restart mgetty@ttyUSB2

После того, как mgetty и pppd настроены, можно попробовать зайти на домашнюю страницу. Первый телефон должен запустить CSD-звонок, второй телефон - принять его (благодаря mgetty). После этого в логах pppd должны пойти сообщения о ходе подключения, у меня это выглядело так:

Логи pppd
pppd 2.5.2 started by root, uid 0
using channel 9
Using interface ppp0
Connect: ppp0 <--> /dev/ttyUSB2
sent [LCP ConfReq id=0x1 <asyncmap 0x0> <auth pap> <magic 0xa19fdfa3> <pcomp> <accomp>]
rcvd [LCP ConfReq id=0x13 <mru 2000> <asyncmap 0xa0000> <pcomp> <accomp> <magic 0x93ed0794>]
sent [LCP ConfAck id=0x13 <mru 2000> <asyncmap 0xa0000> <pcomp> <accomp> <magic 0x93ed0794>]
sent [LCP ConfReq id=0x1 <asyncmap 0x0> <auth pap> <magic 0xa19fdfa3> <pcomp> <accomp>]
rcvd [LCP ConfNak id=0x1 <asyncmap 0xa0000>]
sent [LCP ConfReq id=0x2 <asyncmap 0xa0000> <auth pap> <magic 0xa19fdfa3> <pcomp> <accomp>]
rcvd [LCP ConfNak id=0x1 <asyncmap 0xa0000>]
rcvd [LCP ConfAck id=0x2 <asyncmap 0xa0000> <auth pap> <magic 0xa19fdfa3> <pcomp> <accomp>]
sent [LCP EchoReq id=0x0 magic=0xa19fdfa3]
rcvd [PAP AuthReq id=0x1 user="user2" password=<hidden>]
Initializing PAM (3) for user user2
---> PAM INIT Result = 0
Attempting PAM authentication
PAM Authentication OK for user2
Attempting PAM account checks
PAM Account OK for user2
PAM Session opened for user user2
user user2 logged in on tty ttyUSB2 intf ppp0
PAP peer authentication succeeded for user2
Peer user2 authenticated with PAP
local  IP address 10.0.0.1
remote IP address 10.0.0.2

Как видно, телефон смог подключиться к pppd серверу, и далее браузер телефона работает уже через шлюз WAP. В Wireshark это можно контролировать, используя фильтр "ip.addr == 91.122.205.250".
Если все настроено верно, должен отобразится сайт wap.bs0dd.net:

Как видно, на экран влезает всего 3 строки текста, это ужасно мало.
Как видно, на экран влезает всего 3 строки текста, это ужасно мало.

В нынешнее время осталось совсем уж мало сайтов, написанных на WML.
Вот тут есть подборка таких сайтов: https://github.com/KedalionDaimon/list-of-wap-wml-sites
Приходилось встречать информацию, что FrogFind! умел конвертировать данные в WML, но сейчас он не работает, а frogfind.de на телефоне не открылся.
Зато проект W@PFind! - find.bevelgacom.be у меня на телефоне открылся. Этот проект, насколько я понял, основан на FrogFind, но специализируется на формировании именно WML. Как и FrogFind, он обеспечивает поиск в сети (при помощи DuckDuckGo) заданного текста, а потом конвертирует найденные сайты в WML:

weather.com и cnn.com
weather.com и cnn.com

К сожалению, в большинстве случаев конвертация происходит криво, и сайты просто не открываются.

Я решил попробовать создать свой собственный WAP-сайт, используя Python. Так как у меня нет сервера с "белым" IP-адресом, использовать чужой внешний WAP-шлюз для этих целей уже не выйдет.
Так что я установил WAP-шлюз Kannel, как это описано тут: Настройка WAP-шлюза Kannel: Linux.
В его настройках я вообще ничего не менял. В настройках телефона нужно поменять IP-адрес шлюза на 10.0.0.1.

После этого я создал простейший сайт на Python, который при запросе WAP-страницы скачивает из сети RSS-страницу, и конвертирует ее содержимое в WML. Так как я не знаю толком Python, использовал для этого нейронку. Код вышел таким:

RSS Server
from flask import Flask, Response, request
import feedparser
import html

app = Flask(__name__)

RSS_URL = "https://www.space.com/feeds.xml"

# Функция для безопасной очистки текста под строгий XML-формат WML
def clean_text(text):
    if not text:
        return ""
    # Убираем HTML теги, если они пролезли из RSS
    clean = html.unescape(text)
    # Экранируем спецсимволы XML
    return clean.replace("&", "&amp;").replace("<", "&lt;").replace(">", "&gt;").replace('"', "&quot;")

@app.route("/")
def index():
    feed = feedparser.parse(RSS_URL)
    
    wml = '<?xml version="1.0" encoding="UTF-8"?>\n'
    wml += '<!DOCTYPE wml PUBLIC "-//WAPFORUM//DTD WML 1.1//EN" "http://wapforum.org">\n'
    wml += '<wml>\n'
    wml += '  <card id="list" title="WAP News">\n'
    wml += f'    <p><b>{clean_text(feed.feed.title)}</b></p>\n'
    
    # Передаем индекс новости в параметре URL (например, /story?id=3)
    for index, entry in enumerate(feed.entries[:10]):
        title = clean_text(entry.title)
        
        wml += '    <p>\n'
        # Используем WML-ссылку. Для совместимости со старыми браузерами пишем через anchor
        wml += f'      <anchor>{title}<go href="/story?id={index}"/></anchor>\n'
        wml += '    </p>\n'
        
    wml += '  </card>\n'
    wml += '</wml>'
    
    return Response(wml, mimetype='text/vnd.wap.wml')

@app.route("/story")
def story():
    # Получаем ID новости из запроса телефона
    story_id = int(request.args.get('id', 0))
    feed = feedparser.parse(RSS_URL)
    
    # Проверяем, существует ли новость с таким индексом
    if story_id >= len(feed.entries):
        return Response("Story not found", status=404)
        
    entry = feed.entries[story_id]
    title = clean_text(entry.title)
    
    # Берем описание (summary или description). Если его нет — пишем заглушку.
    description = clean_text(getattr(entry, 'summary', 'No description'))
    
    wml = '<?xml version="1.0" encoding="UTF-8"?>\n'
    wml += '<!DOCTYPE wml PUBLIC "-//WAPFORUM//DTD WML 1.1//EN" "http://wapforum.org">\n'
    wml += '<wml>\n'
    wml += f'  <card id="item" title="{title[:12]}...">\n' # Ограничиваем длину заголовка в шапке
    wml += f'    <p><b>{title}</b></p>\n'
    wml += f'    <p>{description}</p>\n'
    # Кнопка возврата на главную страницу новостей
    wml += '    <p><anchor>BACK<go href="/"/></anchor></p>\n'
    wml += '  </card>\n'
    wml += '</wml>'
    
    return Response(wml, mimetype='text/vnd.wap.wml')

if __name__ == "__main__":
    app.run(host="0.0.0.0", port=8080)

Как видно из кода, сервер работает на порту 8080, так что в я добавил в избранное на телефоне адрес 10.0.0.1:8080. Сайт вполне себе заработал, хотя и отображается на нем всего 10 новостей, зато актуальные.
Работу сайта удобно проверять в Linux, используя WAP-браузер wApua - он есть в репозитории Ubuntu:

Мой сайт в wApua
Мой сайт в wApua

Так это выглядит на телефоне:

Изображение в центре - одна из новостей, справа - она открыта целиком.
Изображение в центре - одна из новостей, справа - она открыта целиком.

Если использовать RSS на русском, то страницы вообще не открывались, так что я переделал сервер:

RSS Server Ru
from flask import Flask, Response, request
import feedparser
import html

app = Flask(__name__)

#RSS_URL = "https://www.space.com/feeds.xml"
RSS_URL = "https://elementy.ru/rss/news/cosmos"


MAX_TITLE_LEN = 60        # Длина заголовка в списке
MAX_DESC_LEN = 400        # Максимальная длина текста новости

def clean_text(text):
    if not text:
        return ""
    # 1. Убираем HTML-теги и декодируем сущности
    clean = html.unescape(text)
    
    # 2. Заменяем спецсимволы, которые ломают парсер Сименса
    clean = clean.replace("«", '"').replace("»", '"')
    clean = clean.replace("—", "-").replace("–", "-")
    clean = clean.replace("&", "&amp;").replace("<", "&lt;").replace(">", "&gt;").replace('"', "&quot;")
    
    # 3. Превращаем кириллицу в числовые XML-коды (NCR)
    ncr_text = ""
    for char in clean:
        ord_char = ord(char)
        if 1024 <= ord_char <= 1105:
            ncr_text += f"&#{ord_char};"
        else:
            ncr_text += char
    return ncr_text

@app.route("/")
def index():
    feed = feedparser.parse(RSS_URL)
    
    wml = '<?xml version="1.0" encoding="utf-8"?>\n'
    wml += '<!DOCTYPE wml PUBLIC "-//WAPFORUM//DTD WML 1.1//EN" "http://wapforum.org">\n'
    wml += '<wml>\n'
    wml += '  <card id="list" title="Space News">\n'
    
    # Строго 3 новости для пролезания в лимит памяти 1.4 КБ
    for index, entry in enumerate(feed.entries[:10]):
        raw_title = entry.title
        if len(raw_title) > MAX_TITLE_LEN:
            raw_title = raw_title[:MAX_TITLE_LEN] + "..."
            
        title = clean_text(raw_title)
        
        wml += '    <p>\n'
        wml += f'      <anchor>{title}<go href="/story?id={index}"/></anchor>\n'
        wml += '    </p>\n'
        
    wml += '  </card>\n'
    wml += '</wml>'
    
    return Response(wml, mimetype='text/vnd.wap.wml; charset=utf-8')

@app.route("/story")
def story():
    story_id = int(request.args.get('id', 0))
    feed = feedparser.parse(RSS_URL)
    
    if story_id >= len(feed.entries):
        return Response("Not found", status=404)
        
    entry = feed.entries[story_id]
    
    # Обработка заголовка статьи
    raw_title = entry.title
    if len(raw_title) > MAX_TITLE_LEN:
        raw_title = raw_title[:MAX_TITLE_LEN] + "..."
    title = clean_text(raw_title)
    
    # Безопасное получение и жесткая обрезка текста новости
    raw_desc = getattr(entry, 'summary', 'No description.')
    if len(raw_desc) > MAX_DESC_LEN:
        raw_desc = raw_desc[:MAX_DESC_LEN] + "..."
    description = clean_text(raw_desc)
    
    # Сборка WML страницы новости
    wml = '<?xml version="1.0" encoding="utf-8"?>\n'
    wml += '<!DOCTYPE wml PUBLIC "-//WAPFORUM//DTD WML 1.1//EN" "http://wapforum.org">\n'
    wml += '<wml>\n'
    wml += '  <card id="item" title="News">\n'
    wml += f'    <p><b>{title}</b></p>\n'
    wml += f'    <p>{description}</p>\n'
    wml += '    <p><anchor>BACK<go href="/"/></anchor></p>\n'
    wml += '  </card>\n'
    wml += '</wml>'
    
    return Response(wml, mimetype='text/vnd.wap.wml; charset=utf-8')

if __name__ == "__main__":
    app.run(host="0.0.0.0", port=8080)

Вот в таком варианте и сайты на русском стали открываться:

Как видно, мобильный телефон, которому уже скоро исполнится 25 лет, все еще может быть использован для отображения новостей)

Окончательно структурная схема получившейся системы выглядит так:

Структурная схема получившейся системы, не все программы Osmocom показаны
Структурная схема получившейся системы, не все программы Osmocom показаны

Комментарии (11)


  1. Astranome
    28.05.2026 10:07

    Спасибо за очередную интересную статью! Я так и не понял состав оборудования , нужно 2 телефона + Осмоком? Или ещё СДР?


    1. iliasam Автор
      28.05.2026 10:07

      Osmocom без SDR не работает. Телефонов нужно два.
      Я добавил в конец статьи структурную схему, показывающую использованные компоненты.


    1. MaFrance351
      28.05.2026 10:07

      Я так понимаю, тут принцип похож на соединение обычных аналоговых модемов. Один модем телефон - клиент, на котором выходим в сеть, второй принимает звонок и обменивается данными с компьютером, который устанавливает PPP соединение. Только вместо АТС тут БС.


  1. tklim
    28.05.2026 10:07

    По идее, чаcть "PC" вполне можно засунуть внутрь этого SDR, два ядра и 1гб памяти, должно хватить.


    1. iliasam Автор
      28.05.2026 10:07

      Там довольно слабенькие ядра - 666,6 МГц.
      На ноутбуке с Core 2 Duo P7450 (2.13 ГГц, 2 ядра) Osmocom загружает процессор примерно на 50%.
      Хотя, возможно, это связано с тем, что в OsmoTRX используется блокирующие операции для работы с трансивером. Больше всего именно OsmoTRX процессор загружает.


      1. axilirator
        28.05.2026 10:07

        Блокирующие операции не особо грузят CPU, т.к. тред в этом случае просто ничего не делает и ждет. Больше всего ресурсов "кушает" демодулятор. Если включить демодуляцию 8-PSK (для EGPRS), то "кушать" будет еще больше.

        Еще желательно, чтобы CPU умел SSE3 и SSE4.1 (и сам проект был собран с поддержкой SIMD) - это заметно снижает нагрузку. Для ARM есть поддержка инструкций NEON.


  1. aabzel
    28.05.2026 10:07

    Тем не менее, не все старые телефоны поддерживают GPRS

    Каким образом старые кнопочные Siemens/Motorola/Nokia телефоны могли в run-time до устанавливать и запускать на исполнение игры и приложения, полученные из сети, без пере прошивки микроконтроллера внутри?


    1. iliasam Автор
      28.05.2026 10:07

      Если не ошибаюсь, скачивать можно было только Java-приложения. Мой Siemens и этого не может.


      1. axilirator
        28.05.2026 10:07

        На ранних Sony Ericsson еще был Mophun: https://ru.wikipedia.org/wiki/Mophun.


  1. Astranome
    28.05.2026 10:07

    Кстати Osmocom я установил на Raspberry Pi 4 с 4 Гиг ОЗУ. Этот одноплатник вполне справляется, загрузка процессора 27% в режиме разговора и 23% в "холостом" режиме.

    Сейчас абоненты разговаривают
    Сейчас абоненты разговаривают


  1. VT100
    28.05.2026 10:07

    Пропечёная и вкусная “буханка хлеба”. Спасибо! ;-)