DMR-данные (SMS) на TYT MD-UV380 под OpenGD77

Восстановлено из транскрипта сессии после потери рабочего дерева на сервере сборки.

Что внутри:
- четыре новых модуля прошивки (приём, сборка исходящего, меню Messages);
- правки существующих файлов в виде скриптов, с порядком применения;
- инструменты стенда: снятие экрана рации по USB, дамп кадров, эталонная сборка пакета;
- NOTES.md: что проверено в эфире, что не доделано, где грабли.

Работало в эфире в августе 2026: данные ходят в обе стороны в режиме хотспота,
рация принимает SMS и показывает на экране. Отправка собрана и сверена с эталоном
сервера побайтово, но в эфире не проверялась.

Claude-Session: https://claude.ai/code/session_01GJtvG5WwqYNe1b7s9LLtgy
Этот коммит содержится в:
2026-09-11 02:39:24 +09:00
Коммит b5a8e92e45
32 изменённых файлов: 3812 добавлений и 0 удалений
+3
Просмотреть файл
@@ -0,0 +1,3 @@
*.bin
*.elf
*.o
+160
Просмотреть файл
@@ -0,0 +1,160 @@
# DMR-данные (SMS) на TYT MD-UV380 под OpenGD77
Что это: доработка прошивки OpenGD77, которая учит рацию передавать и принимать
пакетные данные DMR — текстовые сообщения. В апстриме этого нет: в официальном
README прямо написано, что отправка и приём SMS не поддерживаются.
Базовый исходник — официальный релиз **R20260131** (ветка `MDUV380_DM1701`),
скачан с opengd77.com. Первый коммит в этом репозитории — он же, без единого
изменения. Всё, что дальше — наши правки, так что `git diff` первого и второго
коммита показывает ровно их.
---
## Что работает (проверено в эфире)
**Режим модема (хотспот), обе стороны.** Раньше SMS через рацию-модем молча
пропадали: модем подтверждал кадры хосту, а в эфир ничего не уходило. Теперь
данные проходят насквозь в обе стороны. Проверено с настоящим сервером и вторым
приёмником.
**Приём SMS на самой рации.** В обычном режиме (не модем) рация принимает
сообщение из эфира, собирает его из кадров, разбирает сетевую обёртку и
показывает на экране: отправитель, текст, кнопка OK.
**Меню Messages** в главном меню: `New msg`, `Inbox`, `Outbox`; у папок —
список сообщений и `Delete All`. Хранится по 6 сообщений на папку, текст до 64
символов (ограничение оперативной памяти, см. ниже).
**Отправка с рации** — код написан, собран, но **в эфире не проверялась**.
Сборка пакета при этом сверена побайтово с эталоном сервера (два разных
сообщения, приватное и групповое, включая контрольные суммы и нарезку на кадры).
## Чего нет
- Отправка не проверена в эфире, ввод текста с клавиатуры не написан:
`New msg` шлёт фиксированный текст, кнопка «Ответить» — короткое `OK`.
- Подтверждение доставки (TMS-ACK) не отправляется: отправитель, запросивший
его, покажет «не доставлено», даже если текст на экране появился.
- Контрольные суммы принятых блоков не проверяются.
- Кириллица не поддержана — символы вне ASCII заменяются на `?`. Нужен
кириллический шрифт (в проекте есть образец: отдельный набор для японского).
- Режим `rate 3/4` не декодируется (там иное кодирование). Практическое
следствие: приватные сообщения от AnyTone напрямую не читаются, групповые
`rate 1/2` — читаются.
---
## Как это устроено
Ключевая находка: **чип HR-C6000 умеет собирать кадры данных сам** — это уже
использовалось прошивкой для пробуждения репитера. Нужно положить 12 байт
полезной нагрузки на страницу регистров `0x02` и записать тип кадра в регистр
`0x50` (значение — тип из стандарта, сдвинутый на 4 бита). Чип добавит
помехоустойчивое кодирование и синхронизацию.
Приём симметричен: принятые данные читаются с той же страницы `0x02`.
Новые файлы:
- `application/source/functions/sms.c` + `include/functions/sms.h` — сборка
входящих сообщений, хранилище Inbox/Outbox, очередь передачи.
- `application/source/functions/smsSend.c` — построение исходящего пакета:
TMS, IP/UDP, контрольная сумма сообщения, заголовок данных, преамбулы.
- `application/source/user_interface/menuMessages.c` — экраны меню.
Изменённые файлы:
- `hardware/HR-C6000.c` — захват кадров данных в обоих режимах, передача
данных вместо голоса.
- `functions/hotspot.c` — разбор типа кадра от хоста, отправка данных хосту.
- `applicationMain.c` — разбор принятого и показ на экране.
- `user_interface/menuSystem.{c,h}` — регистрация экранов, пункт меню.
- языковые файлы — добавлена строка `messages` (во все 21).
---
## Сборка
Проект рассчитан на STM32CubeIDE, Makefile в нём нет — он написан нами:
`MDUV380_firmware/Makefile.opengd77`. Нужен `arm-none-eabi-gcc` (проверено на 14.2).
```
cd MDUV380_firmware
tools/codec_cleaner.Linux -C # из каталога application/source/linkerdata
make -f Makefile.opengd77 -j$(nproc)
```
Первый шаг обязателен: он создаёт пустую секцию под AMBE-кодек, которого в
исходниках нет.
**Про кодек.** Официально его «донорит» заводская прошивка TYT. Если её нет,
кодек можно взять из дампа самой рации (она уже работает в DMR): секция лежит
по смещению `0x6937C` от начала образа, длина `0x48BB0`, и в дампе она в
открытом виде. Скрипт слияния — в истории работ, логика тривиальна: заменить
эту область в собранном `.bin` на область из дампа.
Без кодека прошивка соберётся, но DMR работать не будет (только FM).
## Прошивка
```
sudo python3 MDUV380_firmware/tools/opengd77_stm32_firmware_loader.py \
-f OpenMDUV380.bin -m MD-UV380
```
Рация должна быть в режиме DFU (`0483:df11`) — вход только кнопками при
включении, программного способа нет. Нужен `python3-usb` и root.
Калибровка и codeplug при прошивке не страдают: они в отдельной микросхеме
памяти, прошивается только флеш микроконтроллера. Проверено побайтовым
сравнением до и после.
---
## Ловушки, на которые мы потратили время
**Не то дерево.** Для MD-UV380 нужна ветка `MDUV380_DM1701` (STM32). Ветка
`MK22` — это GD-77/DM-1801/RD-5R, там другая структура и другой код хотспота.
Исходники OpenGD77 не публикуются на GitHub — только zip-архивами на сайте.
**Линковка кодека.** Современный `ld` отвергает вызов по абсолютному числовому
адресу. Адреса AMBE заменены на символы через `.thumb_set` — машинный код
получается идентичный (проверено дизассемблером).
**Уведомление на экране не годится для показа.** `uiNotificationShow()` из
главного цикла затирается штатной перерисовкой экрана. Из-за этого несколько
часов казалось, что приём не работает, хотя он работал. Правильно —
`menuSystemPushNewMenu(UI_MESSAGE_BOX)`: экран держится до нажатия кнопки.
**Правка передачи может сломать голос.** Признак «это кадр данных» ставится
только на пути хотспота, а сбрасывается после отправки. Если передача
оборвалась, признак залипал — и обычная голосовая передача уходила без
заголовка вызова, то есть рация излучала, а приёмники её не понимали.
Вывод: любую правку передающего тракта проверять голосом с самой рации, а не
только в режиме модема.
**Оперативной памяти мало.** Дважды упирались в переполнение при линковке
(360 и 240 байт). Отсюда ограничения: 6 сообщений на папку, текст 64 символа.
Если понадобится больше — хранить сообщения во флеш-памяти, а не в ОЗУ.
**Диагностика.** Счётчики сборщика публикуются в хвост звукового буфера
(сигнатура `SMSD`) и читаются по USB штатным протоколом CPS (область 7). Это
надёжнее экрана. Тем же протоколом (область 6) можно снимать содержимое экрана
рации — удобно для отладки. Осторожно: частый опрос подпирает главный цикл
прошивки и мешает приёму.
**Режим модема.** Рация входит в него автоматически от первого же кадра MMDVM
по USB. Штатно она выходила из него после 20 секунд молчания хоста; по просьбе
владельца это заменено на выход при отсоединении USB-хоста — так режим не
отваливается при живой программе, но рация сама возвращается в норму, когда
кабель вынут.
---
## Лицензия
Исходный код OpenGD77 распространяется под собственной лицензией авторов
(BSD-подобная, но пункт 4 прямо запрещает коммерческое использование и
включение в проекты, допускающие коммерческое использование). Эти правки —
производная работа и наследуют те же условия.
+88
Просмотреть файл
@@ -0,0 +1,88 @@
# OpenGD77 для TYT MD-UV380: DMR-данные (SMS)
Доработка прошивки OpenGD77, которая учит рацию передавать и принимать текстовые
сообщения DMR. В апстриме этого нет - в официальном README прямо сказано, что
отправка и приём SMS не поддерживаются.
Базовый исходник: официальный релиз **R20260131**, ветка `MDUV380_DM1701`,
скачивается с opengd77.com/downloads/releases/MDUV380_DM1701/R20260131/
Подробное описание - что сделано, что проверено в эфире, где ловушки - в
[NOTES.md](NOTES.md). Прочитать до начала работы, там собраны грабли, на которые
ушла большая часть времени.
## Важно про состояние этого репозитория
Рабочее дерево с уже применёнными правками было утрачено (сервер сборки почистили
через месяц после работы). Здесь лежит восстановленное: новые модули целиком плюс
правки существующих файлов в виде скриптов. Собранной прошивки в репозитории нет,
её нужно получить, применив правки к официальному релизу.
Код проверялся в эфире в августе 2026 и работал. После восстановления сборка
заново не проверялась - это первое, что стоит сделать.
## Что где лежит
```
firmware/application/source/functions/sms.c новый модуль: приём, разбор, хранение
firmware/application/include/functions/sms.h его заголовок
firmware/application/source/functions/smsSend.c сборка исходящего пакета
firmware/application/source/user_interface/menuMessages.c экраны меню Messages
patches/ правки существующих файлов прошивки
tools/ вспомогательные инструменты для стенда
docs/memory/ рабочие заметки по ходу разработки
```
## Как применить к чистому релизу
Распаковать официальный релиз, положить четыре новых файла по указанным путям,
затем прогнать скрипты из `patches/` по порядку. Каждый скрипт содержит описание
того, что и зачем меняет, и проверяет, что правка ещё не применена.
Порядок важен, часть правок опирается на предыдущие:
1. `00-inline-edits.md` - правки, сделанные командами (передача данных в режиме
хотспота, приём данных с чипа, типы кадров, слияние кодека). Это не скрипт,
а список команд с пояснениями, применять вручную по порядку.
2. `patch_rx1.py`, `patch_rx2.py`, `patch_rx3.py` - приём данных в режиме хотспота
и отдача их управляющей программе.
3. `patch_txclean.py` - убрать голосовые заголовки перед кадрами данных.
4. `patch_notimeout.py` - не выходить из режима модема по молчанию хоста.
5. `patch_txfix.py` - **обязательно**, иначе ломается обычная голосовая передача.
Подробности в NOTES.md, раздел про правку передачи.
6. `patch_sms1.py` - захват кадров данных в обычном режиме рации.
7. `patch_sms2.py` - запуск сборщика из главного цикла.
8. `patch_store.py` - хранилище Inbox/Outbox.
9. `patch_show.py` - показ принятого сообщения на экране (заменяет `patch_sms3.py`,
который использовал ненадёжный способ вывода).
10. `patch_menufix.py` - меню Messages целиком (переписывает `menuMessages.c`
на штатную навигацию, исправляет номер языковой строки).
11. `patch_tx.py` - очередь передачи и отправка данных в обычном режиме.
12. `patch_ui_send.py` - пункт `New msg` и кнопка «Ответить».
13. `patch_usbexit.py` - выход из режима модема при отсоединении USB.
Отладочные, применять по необходимости: `patch_diag.py` (счётчики на экране,
мешают показу сообщений), `patch_diag4.py` (счётчики в память, читаются по USB -
этот вариант надёжнее).
Сборка и прошивка описаны в NOTES.md.
## Инструменты
`tools/screen_grab.py` снимает экран рации по USB и сохраняет PNG. Удобно, когда
нужно увидеть, что показала рация, а рядом никого нет. Осторожно с частотой
опроса: частые запросы подпирают главный цикл прошивки и мешают приёму.
`tools/dmr_dump.py` слушает порт рации в режиме модема и печатает каждый принятый
кадр целиком, в шестнадцатеричном виде. Нужен, когда надо понять, что реально
пришло из эфира, а не что показал разбор.
`tools/sms_build_test.py` - эталонная сборка исходящего пакета на Python. Ею
проверялась реализация на C: оба варианта дают байт в байт то же, что сервер.
Если будете менять формат, сверяйтесь этим скриптом.
## Лицензия
Исходный код OpenGD77 распространяется под лицензией авторов проекта: BSD-подобная,
но пункт 4 прямо запрещает коммерческое использование и включение в проекты, которые
такое использование допускают. Эти правки - производная работа с теми же условиями.
+3
Просмотреть файл
@@ -0,0 +1,3 @@
- [Где лежат исходники OpenGD77](opengd77-sources-location.md) — не на GitHub, а zip'ами на сайте; для UV380 нужна ветка MDUV380_DM1701, не MK22.
- [Почему хотспот не шлёт SMS](uv390-dmr-data-path.md) — диагноз в hotspot.c/HR-C6000.c и готовый рабочий образец передачи data-burst.
- [Стенд для UV390](uv390-test-bench.md) — .200 сборка, .84 рация-модем + airtest, .85 эталонный приёмник.
+25
Просмотреть файл
@@ -0,0 +1,25 @@
---
name: opengd77-sources-location
description: Где лежат исходники OpenGD77 и почему их нет на GitHub; какая ветка нужна для MD-UV380
metadata:
type: reference
---
Исходники OpenGD77 публикуются ZIP-архивами на https://www.opengd77.com/downloads/releases/ , а НЕ на GitHub — авторы намеренно не выкладывают актуальный код в git (форки на GitHub вроде open-ham/OpenGD77 — устаревшие слепки).
Семейства раций разделены по каталогам:
- `MK22/` — NXP MK22: GD-77, DM-1801, RD-5R. MD-UV380 там НЕТ.
- `MDUV380_DM1701/` — STM32F405: **TYT MD-UV380/UV390**, DM-1701, RT-84. Это нужная ветка.
Релизы MDUV380_DM1701 и их GitID (зашит в .bin, виден в строке версии hotspot):
- R20231231 — OpenGD77_HS v0.1.17, GitID #140939c
- R20240908 — v0.1.18, #9947546
- R20260131 — v0.1.18, #5797626 (самый свежий)
`GITVERSION` в сборке = `git rev-parse --short HEAD` дерева сборщика, поэтому чужой GitID (напр. #2dc33f6) в публичных репозиториях не ищется — это личный билд.
Лицензия — НЕ GPLv2. BSD-подобная, но пункт 4 прямо запрещает коммерческое использование и включение в проекты, допускающие коммерческое использование. Для личных раций ограничений нет.
Особенность сборки: исходник не содержит AMBE-кодек (мержится загрузчиком из официальной прошивки). Перед сборкой обязателен шаг `prepare` → `MDUV380_firmware/tools/codec_cleaner.Linux -C` → создаёт `application/source/linkerdata/codec_bin_section_1.bin`. Скрипт `prepare` идёт с CRLF-переносами и под Linux падает — вызывать codec_cleaner напрямую.
Связано: [[uv390-dmr-data-path]]
+31
Просмотреть файл
@@ -0,0 +1,31 @@
---
name: uv390-dmr-data-path
description: Почему OpenGD77 в режиме хотспота не передаёт SMS/данные и где в коде это чинится
metadata:
type: project
---
Задача (с августа 2026): научить OpenGD77 на MD-UV380 в режиме хотспота проводить пакетные ДАННЫЕ (SMS, координаты), а не только голос. Рация досталась Виктору уже с прошивкой OpenGD77_HS v0.1.18 — он её не собирал.
Диагноз (дерево R20260131, `MDUV380_firmware/application/source/`):
- `functions/hotspot.c:2071` `hotspotModeReceiveNetFrame` декодирует кадр ТОЛЬКО как голосовой LC. Всё нераспознанное (включая SMS) уходит в `else` → `storeNetFrame` (2026) → копируется в аудиобуфер как звук. Отсюда симптом «модем ACK'ает кадры, в эфире ничего».
- В `hotspot.h` определены только `DT_VOICE_LC_HEADER` и `DT_TERMINATOR_WITH_LC` — типов данных нет как класса. В старой MK22-ветке был закомментированный switch по DT_*, в актуальной STM32-ветке он удалён совсем.
- На приёме `hrc6000SysReceivedDataInt` (HR-C6000.c:939) классифицирует кадр по `reg_0x51` (`rxSyncClass` 2=data, `rxDataType`), но payload данных с чипа не читает вообще (для голоса читает 27 байт AMBE с page 0x03).
Ключ к решению — механизм передачи данных В ПРОШИВКЕ УЖЕ РАБОТАЕТ:
`HR-C6000.c:1889` `DMR_STATE_REPEATER_WAKE_1` шлёт настоящий CSBK-burst:
```
SPI0WritePageRegByteArray(0x02, 0x00, payload, LC_DATA_LENGTH); // 12 байт на page 0x02
SPI0WritePageRegByte(0x04, 0x50, 0x30); // reg 0x50 = селектор типа burst'а (DT<<4), 0x30 = CSBK
SPI0WritePageRegByte(0x04, 0x41, 0x80); // передать в следующем слоте
```
Другие типы: Voice LC Header 0x10, Terminator 0x20, Data Header 0x60, rate 1/2 0x70, rate 3/4 0x90.
Размерность сходится: CSBK, Data Header и rate 1/2 — все 96 бит = 12 байт = `LC_DATA_LENGTH`. Чип сам накладывает CRC/FEC.
Мост к MMDVM тоже уже есть: `BPTCencode`/`BPTCdecode` в `hotspot.c` (545/551). MMDVMHost шлёт полный 33-байтный burst, чипу нужны 12 байт:
- TX: burst хоста → BPTCdecode → 12 байт → page 0x02 + reg 0x50
- RX: 12 байт с чипа → BPTCencode → + slot type + sync → `MMDVM_DMR_DATA2` хосту
Главный нерешённый вопрос: с какой страницы/регистра C6000 читать ПРИНЯТЫЙ data-payload.
Связано: [[opengd77-sources-location]], [[uv390-test-bench]]
+22
Просмотреть файл
@@ -0,0 +1,22 @@
---
name: uv390-test-bench
description: Машины и инструменты стенда для проверки прошивки UV390 (доступы, роли)
metadata:
type: project
---
Стенд для работы над DMR-данными на MD-UV380. Вход везде viktor / Adelina@2015 (ssh).
- **10.0.0.200** (ubuntu-server, x86_64) — машина сборки. Тулчейн `arm-none-eabi-gcc` 14.2.1 + objcopy/size/make. Исходники релизов в `~/ogd77rel/` (скачаны официальные zip), рабочее дерево `~/ogd77rel/uv_src/OpenGD77_MDUV380_DM1701_20260130`. Устаревший клон MK22-ветки — `~/OpenGD77` (НЕ то дерево, см. [[opengd77-sources-location]]).
- **10.0.0.84** (pi5, aarch64) — к нему по USB подключена рация-модем (`/dev/ttyACM0`, USB id `1fc9:0094 OpenMDUV380`). Здесь же `~/airtest` — инструмент на Go: шлёт в модем голос и SMS-передачи, печатает ACK/NAK модема. Флаги: `--port --dst --src --cc --slot --rx --text --bursts --secs`, команды `sms`/`voice`/`listen`. Прошивать рацию можно прямо отсюда.
- **10.0.0.85** (radiostar-hotspot, armv7l) — эталонный приёмник MMDVM_HS Dual с прошивкой RadioStar-HS: журналит каждый принятый кадр с типом. Панель на порту 8080 (снаружи может быть закрыт).
Условия эфирных опытов: DMO симплекс, 433.525 МГц, CC1, слот 2.
Прошивка рации: `MDUV380_firmware/tools/opengd77_stm32_firmware_loader.py` — работает через DFU и умеет ТОЛЬКО запись, чтения/бэкапа прошивки нет. Откат обеспечен официальным образом `OpenMDUV380.bin` из релиза. `python_tools.zip` с сайта содержит утилиты только для GD-77 (MK22), для STM32 они не подходят.
Виктор разрешил шить тестовые сборки свободно, но требует делать бэкап.
Соседние сессии по этому проекту: **DMRhub** (стенд и измерения), **nextdmr-81** (прошивка RadioStar-HS Dual — эталон поведения).
Связано: [[uv390-dmr-data-path]]
+74
Просмотреть файл
@@ -0,0 +1,74 @@
/*
* Copyright (C) 2026 OpenGD77 contributors
*
* Receiving DMR packet data (SMS) while the radio is used as a radio, rather than
* as a hotspot modem.
*
* Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions
* are met:
*
* 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer.
*
* 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer
* in the documentation and/or other materials provided with the distribution.
*
* 3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote products derived
* from this software without specific prior written permission.
*
* 4. Use of this source code or binary releases for commercial purposes is strictly forbidden. This includes, without limitation,
* incorporation in a commercial product or incorporation into a product or project which allows commercial use.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
* LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
* HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
* LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON
* ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE
* USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
#ifndef _OPENGD77_SMS_H_
#define _OPENGD77_SMS_H_
#include <stdint.h>
#include <stdbool.h>
// A single received message, as shown to the user.
#define SMS_MAX_TEXT_LENGTH 160
// Longest message we are willing to assemble. Each rate 1/2 block carries 10 bytes of
// payload once the confirmed-mode overhead is removed, so this covers a full 160 char
// message with room for the IP/UDP/TMS wrapper around it.
#define SMS_MAX_BLOCKS 32
typedef struct
{
uint32_t srcId;
uint32_t dstId;
bool isPrivate;
bool unread;
char text[SMS_MAX_TEXT_LENGTH + 1];
} smsMessage_t;
/*
* Called from the HR-C6000 interrupt for every received data burst while the radio is
* NOT acting as a hotspot modem. Must stay short: it only copies the burst away and
* flags it, all parsing happens later in smsTick().
*
* payload - the 12 byte (96 bit) burst payload as read from the chip
* dataType - DMR data type of the burst (DT_CSBK / DT_DATA_HEADER / DT_RATE_12_DATA)
*/
void smsRxHandleBurst(const uint8_t *payload, uint8_t dataType);
/*
* Runs from normal task context. Turns collected bursts into a message and, when one
* completes, stores it and raises smsHasUnread().
*/
void smsTick(void);
void smsInit(void);
// Most recently received message, or NULL when nothing has been received yet.
const smsMessage_t *smsGetLatest(void);
bool smsHasUnread(void);
void smsMarkRead(void);
#endif
+255
Просмотреть файл
@@ -0,0 +1,255 @@
/*
* Copyright (C) 2026 OpenGD77 contributors
*
* Assembling received DMR packet data (SMS) while the radio is used as a radio.
*
* Wire format (confirmed against a working reference implementation):
*
* Data header (12 bytes)
* [0] bit7 G/I (0 = private), bit6 A (response requested), bits3..0 DPF
* DPF 2 = unconfirmed rate 1/2, DPF 3 = confirmed rate 1/2
* [1] bits7..4 SAP (4 = IP/UDP data), bits3..0 number of pad octets
* [2..4] destination id, big endian
* [5..7] source id, big endian
* [8] bit7 full message flag, bits6..0 blocks to follow
* [9] header class/type
* [10..11] header CRC-16
*
* Blocks (12 bytes each)
* confirmed: [0..1] DBSN + CRC-9, [2..11] ten payload bytes
* unconfirmed: all twelve bytes are payload
*
* Reassembled payload: IPv4 (20) + UDP (8) + TMS. The text sits right behind the
* TMS marker 04 0D 00 0A 00 and is UTF-16 LITTLE endian, terminated by 0x0000.
*
* Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions
* are met:
*
* 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer.
*
* 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer
* in the documentation and/or other materials provided with the distribution.
*
* 3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote products derived
* from this software without specific prior written permission.
*
* 4. Use of this source code or binary releases for commercial purposes is strictly forbidden. This includes, without limitation,
* incorporation in a commercial product or incorporation into a product or project which allows commercial use.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
* LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
* HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
* LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON
* ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE
* USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
#include <string.h>
#include "functions/sms.h"
#include "functions/hotspot.h" // DT_* data type values
#include "functions/ticks.h"
#define SMS_BLOCK_LENGTH 12U
#define SMS_DPF_UNCONFIRMED 2U
#define SMS_DPF_CONFIRMED 3U
// Bursts arrive every 60 ms; this is generously above any realistic gap between them.
#define SMS_ASSEMBLY_TIMEOUT_MS 3000U
// Marks the start of the text inside the TMS payload.
static const uint8_t TMS_TEXT_MARKER[] = { 0x04, 0x0D, 0x00, 0x0A, 0x00 };
typedef enum
{
SMS_RX_IDLE = 0,
SMS_RX_COLLECTING
} smsRxState_t;
// Filled from the interrupt, drained by smsTick().
static volatile uint8_t rxQueue[SMS_MAX_BLOCKS][SMS_BLOCK_LENGTH];
static volatile uint8_t rxQueueType[SMS_MAX_BLOCKS];
static volatile uint8_t rxQueueWrite = 0;
static volatile uint8_t rxQueueRead = 0;
static smsRxState_t rxState = SMS_RX_IDLE;
static uint8_t header[SMS_BLOCK_LENGTH];
static uint8_t blocks[SMS_MAX_BLOCKS][SMS_BLOCK_LENGTH];
static uint8_t blocksExpected = 0;
static uint8_t blocksCollected = 0;
static uint32_t assemblyStartedAt = 0;
static smsMessage_t latest;
static bool haveMessage = false;
static bool unread = false;
void smsInit(void)
{
rxQueueWrite = rxQueueRead = 0;
rxState = SMS_RX_IDLE;
blocksExpected = blocksCollected = 0;
haveMessage = false;
unread = false;
memset(&latest, 0, sizeof(latest));
}
void smsRxHandleBurst(const uint8_t *payload, uint8_t dataType)
{
uint8_t next = (uint8_t)((rxQueueWrite + 1) % SMS_MAX_BLOCKS);
if (next == rxQueueRead)
{
return;// queue full: drop, the partial message will time out
}
memcpy((uint8_t *)rxQueue[rxQueueWrite], payload, SMS_BLOCK_LENGTH);
rxQueueType[rxQueueWrite] = dataType;
rxQueueWrite = next;
}
static void abortAssembly(void)
{
rxState = SMS_RX_IDLE;
blocksExpected = blocksCollected = 0;
}
static void startAssembly(const uint8_t *hdr)
{
uint8_t dpf = (uint8_t)(hdr[0] & 0x0FU);
if ((dpf != SMS_DPF_CONFIRMED) && (dpf != SMS_DPF_UNCONFIRMED))
{
abortAssembly();// not a packet data message we can assemble
return;
}
memcpy(header, hdr, SMS_BLOCK_LENGTH);
blocksExpected = (uint8_t)(hdr[8] & 0x7FU);
blocksCollected = 0;
assemblyStartedAt = ticksGetMillis();
if ((blocksExpected == 0) || (blocksExpected > SMS_MAX_BLOCKS))
{
abortAssembly();
return;
}
rxState = SMS_RX_COLLECTING;
}
static void finishAssembly(void)
{
uint8_t data[SMS_MAX_BLOCKS * SMS_BLOCK_LENGTH];
uint16_t length = 0;
uint8_t dpf = (uint8_t)(header[0] & 0x0FU);
// Confirmed blocks carry two bytes of DBSN + CRC-9 before the payload.
uint8_t skip = (dpf == SMS_DPF_CONFIRMED) ? 2U : 0U;
int textStart = -1;
for (uint8_t i = 0; i < blocksCollected; i++)
{
memcpy(&data[length], &blocks[i][skip], (SMS_BLOCK_LENGTH - skip));
length = (uint16_t)(length + (SMS_BLOCK_LENGTH - skip));
}
// Locate the text rather than counting header bytes: the IP/UDP wrapper varies,
// the marker does not. Take the last match, so a marker-like byte pattern earlier
// in the wrapper cannot fool us.
for (int i = 0; (i + (int)sizeof(TMS_TEXT_MARKER)) <= (int)length; i++)
{
if (memcmp(&data[i], TMS_TEXT_MARKER, sizeof(TMS_TEXT_MARKER)) == 0)
{
textStart = i + (int)sizeof(TMS_TEXT_MARKER);
}
}
if (textStart < 0)
{
abortAssembly();// not a text message (could be GPS or another TMS payload)
return;
}
uint16_t t = 0;
for (int i = textStart; ((i + 1) < (int)length) && (t < SMS_MAX_TEXT_LENGTH); i += 2)
{
uint16_t ch = (uint16_t)(data[i] | (data[i + 1] << 8));// UTF-16 little endian
if (ch == 0)
{
break;
}
// Latin only for now: anything outside ASCII needs the Cyrillic font work.
latest.text[t++] = (ch < 0x80U) ? (char)ch : '?';
}
latest.text[t] = '\0';
latest.dstId = (uint32_t)((header[2] << 16) | (header[3] << 8) | header[4]);
latest.srcId = (uint32_t)((header[5] << 16) | (header[6] << 8) | header[7]);
latest.isPrivate = ((header[0] & 0x80U) == 0);
latest.unread = true;
haveMessage = (t > 0);
unread = haveMessage;
abortAssembly();
}
void smsTick(void)
{
while (rxQueueRead != rxQueueWrite)
{
uint8_t burst[SMS_BLOCK_LENGTH];
uint8_t type = rxQueueType[rxQueueRead];
memcpy(burst, (uint8_t *)rxQueue[rxQueueRead], SMS_BLOCK_LENGTH);
rxQueueRead = (uint8_t)((rxQueueRead + 1) % SMS_MAX_BLOCKS);
switch (type)
{
case DT_DATA_HEADER:
startAssembly(burst);
break;
case DT_RATE_12_DATA:
if (rxState == SMS_RX_COLLECTING)
{
if (blocksCollected < SMS_MAX_BLOCKS)
{
memcpy(blocks[blocksCollected], burst, SMS_BLOCK_LENGTH);
blocksCollected++;
}
if (blocksCollected >= blocksExpected)
{
finishAssembly();
}
}
break;
default:
break;// CSBK preambles and everything else
}
}
if ((rxState == SMS_RX_COLLECTING) &&
((ticksGetMillis() - assemblyStartedAt) > SMS_ASSEMBLY_TIMEOUT_MS))
{
abortAssembly();// sender stopped mid message, or bursts were lost
}
}
const smsMessage_t *smsGetLatest(void)
{
return (haveMessage ? &latest : NULL);
}
bool smsHasUnread(void)
{
return unread;
}
void smsMarkRead(void)
{
unread = false;
latest.unread = false;
}
+265
Просмотреть файл
@@ -0,0 +1,265 @@
/*
* Copyright (C) 2026 OpenGD77 contributors
*
* Building an outgoing DMR text message: TMS payload, IP/UDP wrapper, message CRC,
* data header and the rate 1/2 blocks that carry it.
*
* This is a literal translation of a reference implementation that was verified on a PC
* against a known-good packet produced by the server side, byte for byte (including both
* IP/UDP checksums and the message CRC-32).
*
* Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions
* are met:
*
* 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer.
*
* 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer
* in the documentation and/or other materials provided with the distribution.
*
* 3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote products derived
* from this software without specific prior written permission.
*
* 4. Use of this source code or binary releases for commercial purposes is strictly forbidden. This includes, without limitation,
* incorporation in a commercial product or incorporation into a product or project which allows commercial use.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
* LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
* HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
* LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON
* ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE
* USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
#include <string.h>
#include "functions/sms.h"
#define TMS_HEADER_OVERHEAD 8U // flag + 0x00 + seq + the five marker bytes
#define IP_HEADER_LENGTH 20U
#define UDP_HEADER_LENGTH 8U
#define TMS_PORT 0x0FA7U
static const uint8_t TMS_MARKER[5] = { 0x04, 0x0D, 0x00, 0x0A, 0x00 };
/*
* ETSI / MOTOTRBO message CRC-32: bytes are swapped within each 16 bit pair, then an
* MSB-first CRC32 (poly 0x04C11DB7, init 0, no final xor) is run over the result.
*/
static uint32_t smsMessageCRC32(const uint8_t *data, uint16_t length)
{
uint32_t crc = 0;
for (uint16_t i = 0; i < length; i++)
{
// consume the bytes in swapped-pair order without copying the buffer
uint16_t idx = ((i & 1U) ? (i - 1U) : (i + 1U));
uint8_t b = ((idx < length) ? data[idx] : 0x00);
crc ^= ((uint32_t)b << 24);
for (uint8_t k = 0; k < 8; k++)
{
crc = ((crc & 0x80000000U) ? ((crc << 1) ^ 0x04C11DB7U) : (crc << 1));
}
}
return crc;
}
static uint16_t onesComplementSum(const uint8_t *data, uint16_t length, uint32_t initial)
{
uint32_t sum = initial;
for (uint16_t i = 0; (i + 1U) < length; i += 2U)
{
sum += (uint32_t)((data[i] << 8) | data[i + 1U]);
}
if (length & 1U)
{
sum += (uint32_t)(data[length - 1U] << 8);
}
while (sum >> 16)
{
sum = (sum & 0xFFFFU) + (sum >> 16);
}
return (uint16_t)(~sum);
}
static uint16_t smsCRC16CCITT(const uint8_t *data, uint16_t length)
{
uint16_t crc = 0;
for (uint16_t i = 0; i < length; i++)
{
crc ^= ((uint16_t)data[i] << 8);
for (uint8_t k = 0; k < 8; k++)
{
crc = ((crc & 0x8000U) ? ((crc << 1) ^ 0x1021U) : (crc << 1));
}
}
return crc;
}
uint16_t smsBuildPacket(uint8_t *out, uint16_t outSize, uint32_t srcId, uint32_t dstId,
bool isGroup, const char *text, uint8_t flag, uint8_t seq, uint8_t *padOut)
{
uint16_t textLen = (uint16_t)strlen(text);
uint16_t tmsLen = (uint16_t)(TMS_HEADER_OVERHEAD + (2U * textLen));
uint16_t udpLen = (uint16_t)(UDP_HEADER_LENGTH + 2U + tmsLen);// 2 = the TMS length field
uint16_t ipLen = (uint16_t)(IP_HEADER_LENGTH + udpLen);
uint32_t srcIp = (0x0CU << 24) | (srcId & 0x00FFFFFFU);
uint32_t dstIp = (((uint32_t)(isGroup ? 0xE1U : 0x0CU)) << 24) | (dstId & 0x00FFFFFFU);
uint16_t pos = 0;
if ((uint16_t)(ipLen + 12U + 4U) > outSize)
{
return 0;// would not fit
}
// ---- IP header (checksum filled in afterwards) ----
out[pos++] = 0x45;
out[pos++] = 0x00;
out[pos++] = (uint8_t)(ipLen >> 8);
out[pos++] = (uint8_t)(ipLen & 0xFF);
out[pos++] = 0x00;
out[pos++] = 0x01; // identification
out[pos++] = 0x00;
out[pos++] = 0x00;
out[pos++] = (isGroup ? 0x01 : 0x40); // TTL
out[pos++] = 0x11; // UDP
out[pos++] = 0x00; // checksum placeholder
out[pos++] = 0x00;
out[pos++] = (uint8_t)(srcIp >> 24);
out[pos++] = (uint8_t)(srcIp >> 16);
out[pos++] = (uint8_t)(srcIp >> 8);
out[pos++] = (uint8_t)(srcIp);
out[pos++] = (uint8_t)(dstIp >> 24);
out[pos++] = (uint8_t)(dstIp >> 16);
out[pos++] = (uint8_t)(dstIp >> 8);
out[pos++] = (uint8_t)(dstIp);
uint16_t ipChecksum = onesComplementSum(out, IP_HEADER_LENGTH, 0);
out[10] = (uint8_t)(ipChecksum >> 8);
out[11] = (uint8_t)(ipChecksum & 0xFF);
// ---- UDP header + payload ----
uint16_t udpStart = pos;
out[pos++] = (uint8_t)(TMS_PORT >> 8);
out[pos++] = (uint8_t)(TMS_PORT & 0xFF);
out[pos++] = (uint8_t)(TMS_PORT >> 8);
out[pos++] = (uint8_t)(TMS_PORT & 0xFF);
out[pos++] = (uint8_t)(udpLen >> 8);
out[pos++] = (uint8_t)(udpLen & 0xFF);
out[pos++] = 0x00; // checksum placeholder
out[pos++] = 0x00;
out[pos++] = (uint8_t)(tmsLen >> 8);
out[pos++] = (uint8_t)(tmsLen & 0xFF);
out[pos++] = flag;
out[pos++] = 0x00;
out[pos++] = seq;
memcpy(&out[pos], TMS_MARKER, sizeof(TMS_MARKER));
pos += sizeof(TMS_MARKER);
for (uint16_t i = 0; i < textLen; i++)
{
out[pos++] = (uint8_t)text[i]; // UTF-16 little endian, ASCII only for now
out[pos++] = 0x00;
}
// UDP checksum covers a pseudo header plus the whole UDP segment
{
uint8_t pseudo[12];
pseudo[0] = (uint8_t)(srcIp >> 24); pseudo[1] = (uint8_t)(srcIp >> 16);
pseudo[2] = (uint8_t)(srcIp >> 8); pseudo[3] = (uint8_t)(srcIp);
pseudo[4] = (uint8_t)(dstIp >> 24); pseudo[5] = (uint8_t)(dstIp >> 16);
pseudo[6] = (uint8_t)(dstIp >> 8); pseudo[7] = (uint8_t)(dstIp);
pseudo[8] = 0x00; pseudo[9] = 0x11;
pseudo[10] = (uint8_t)(udpLen >> 8); pseudo[11] = (uint8_t)(udpLen & 0xFF);
uint32_t sum = 0;
for (uint8_t i = 0; i < sizeof(pseudo); i += 2U)
{
sum += (uint32_t)((pseudo[i] << 8) | pseudo[i + 1U]);
}
uint16_t udpChecksum = onesComplementSum(&out[udpStart], udpLen, sum);
out[udpStart + 6U] = (uint8_t)(udpChecksum >> 8);
out[udpStart + 7U] = (uint8_t)(udpChecksum & 0xFF);
}
// ---- pad to a multiple of 12, then append the message CRC as its own tail ----
uint8_t pad = (uint8_t)((12U - (pos % 12U)) % 12U);
for (uint8_t i = 0; i < pad; i++)
{
out[pos++] = 0x00;
}
uint32_t crc = smsMessageCRC32(out, pos);
out[pos++] = (uint8_t)(crc & 0xFF); // little endian
out[pos++] = (uint8_t)(crc >> 8);
out[pos++] = (uint8_t)(crc >> 16);
out[pos++] = (uint8_t)(crc >> 24);
if (padOut != NULL)
{
*padOut = pad;
}
return pos;
}
void smsBuildDataHeader(uint8_t *header, uint32_t srcId, uint32_t dstId, bool isGroup,
uint8_t blocks, uint8_t pad)
{
memset(header, 0, 12);
header[0] = (uint8_t)((isGroup ? 0x80U : 0x00U) | 0x02U);// DPF = 2, unconfirmed
header[1] = (uint8_t)(0x40U | (pad & 0x0FU)); // SAP = 4 (IP based data)
header[2] = (uint8_t)(dstId >> 16);
header[3] = (uint8_t)(dstId >> 8);
header[4] = (uint8_t)(dstId);
header[5] = (uint8_t)(srcId >> 16);
header[6] = (uint8_t)(srcId >> 8);
header[7] = (uint8_t)(srcId);
header[8] = (uint8_t)(0x80U | (blocks & 0x7FU));
header[9] = 0x10;
uint16_t crc = (uint16_t)(smsCRC16CCITT(header, 10) ^ 0xCCCCU);
header[10] = (uint8_t)(crc >> 8);
header[11] = (uint8_t)(crc & 0xFF);
}
void smsBuildPreamble(uint8_t *preamble, uint32_t srcId, uint32_t dstId, bool isGroup,
uint8_t countdown)
{
memset(preamble, 0, 12);
preamble[0] = 0xBD; // Preamble CSBK, LB set
preamble[1] = 0x00;
preamble[2] = (uint8_t)(isGroup ? 0x80U : 0x00U); // data content preamble
preamble[3] = countdown;
preamble[4] = (uint8_t)(dstId >> 16);
preamble[5] = (uint8_t)(dstId >> 8);
preamble[6] = (uint8_t)(dstId);
preamble[7] = (uint8_t)(srcId >> 16);
preamble[8] = (uint8_t)(srcId >> 8);
preamble[9] = (uint8_t)(srcId);
uint16_t crc = (uint16_t)(smsCRC16CCITT(preamble, 10) ^ 0xA5A5U);
preamble[10] = (uint8_t)(crc >> 8);
preamble[11] = (uint8_t)(crc & 0xFF);
}
+408
Просмотреть файл
@@ -0,0 +1,408 @@
/*
* Copyright (C) 2026 OpenGD77 contributors
*
* Messages menu: reading received texts, browsing sent ones, and starting a new one.
*
* Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions
* are met:
*
* 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer.
*
* 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer
* in the documentation and/or other materials provided with the distribution.
*
* 3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote products derived
* from this software without specific prior written permission.
*
* 4. Use of this source code or binary releases for commercial purposes is strictly forbidden. This includes, without limitation,
* incorporation in a commercial product or incorporation into a product or project which allows commercial use.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
* LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
* HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
* LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON
* ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE
* USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
#include <stdio.h>
#include <string.h>
#include "user_interface/menuSystem.h"
#include "user_interface/uiLocalisation.h"
#include "user_interface/uiUtilities.h"
#include "functions/sms.h"
#include "functions/settings.h"
#include "functions/sound.h"
// Entries of the top level Messages menu.
enum
{
MESSAGES_ITEM_NEW = 0,
MESSAGES_ITEM_INBOX,
MESSAGES_ITEM_OUTBOX,
MESSAGES_ITEM_COUNT
};
// Entries of the Inbox / Outbox menus.
enum
{
FOLDER_ITEM_LIST = 0,
FOLDER_ITEM_DELETE_ALL,
FOLDER_ITEM_COUNT
};
static int menuIndex = 0; // selection in the Messages menu
static int folderIndex = 0; // selection in an Inbox/Outbox menu
static int listIndex = 0; // selected message in a list
static bool showingOutbox = false; // which folder the list/delete screens act on
static bool viewingMessage = false;// a single message is open
static menuStatus_t exitCode = MENU_STATUS_SUCCESS;
static void updateMessagesScreen(void);
static void updateFolderScreen(void);
static void updateListScreen(void);
static bool handleMessagesEvent(uiEvent_t *ev);
static bool handleFolderEvent(uiEvent_t *ev);
static bool handleListEvent(uiEvent_t *ev);
// ---------------------------------------------------------------- Messages
static void updateMessagesScreen(void)
{
char buf[SCREEN_LINE_BUFFER_SIZE];
displayClearBuf();
menuDisplayTitle(currentLanguage->messages);
// New msg / Inbox (n) / Outbox (n)
for (int i = 0; i < MESSAGES_ITEM_COUNT; i++)
{
switch (i)
{
case MESSAGES_ITEM_NEW:
snprintf(buf, SCREEN_LINE_BUFFER_SIZE, "New msg");
break;
case MESSAGES_ITEM_INBOX:
snprintf(buf, SCREEN_LINE_BUFFER_SIZE, "Inbox (%u)", (unsigned int)smsInboxCount());
break;
case MESSAGES_ITEM_OUTBOX:
snprintf(buf, SCREEN_LINE_BUFFER_SIZE, "Outbox (%u)", (unsigned int)smsOutboxCount());
break;
}
menuDisplayEntry((i - menuIndex), i, buf, (strlen(buf) - 1), THEME_ITEM_FG_MENU_ITEM, THEME_ITEM_FG_MENU_ITEM_SELECTED, THEME_ITEM_BG);
}
displayRender();
}
menuStatus_t menuMessages(uiEvent_t *ev, bool isFirstRun)
{
if (isFirstRun)
{
menuIndex = 0;
updateMessagesScreen();
return (MENU_STATUS_LIST_TYPE | MENU_STATUS_SUCCESS);
}
if (ev->hasEvent)
{
if (handleMessagesEvent(ev))
{
updateMessagesScreen();
}
}
return exitCode;
}
static bool handleMessagesEvent(uiEvent_t *ev)
{
if (KEYCHECK_PRESS(ev->keys, KEY_DOWN))
{
menuIndex = ((menuIndex + 1) % MESSAGES_ITEM_COUNT);
return true;
}
if (KEYCHECK_PRESS(ev->keys, KEY_UP))
{
menuIndex = ((menuIndex + MESSAGES_ITEM_COUNT - 1) % MESSAGES_ITEM_COUNT);
return true;
}
if (KEYCHECK_SHORTUP(ev->keys, KEY_GREEN))
{
switch (menuIndex)
{
case MESSAGES_ITEM_NEW:
// Sending is not implemented yet; say so rather than opening a dead screen.
uiNotificationShow(NOTIFICATION_TYPE_MESSAGE, NOTIFICATION_ID_MESSAGE, 2000, "Not yet", true);
break;
case MESSAGES_ITEM_INBOX:
showingOutbox = false;
folderIndex = 0;
menuSystemPushNewMenu(MENU_MESSAGES_FOLDER);
break;
case MESSAGES_ITEM_OUTBOX:
showingOutbox = true;
folderIndex = 0;
menuSystemPushNewMenu(MENU_MESSAGES_FOLDER);
break;
}
return true;
}
if (KEYCHECK_SHORTUP(ev->keys, KEY_RED))
{
menuSystemPopPreviousMenu();
return false;
}
return false;
}
// ------------------------------------------------------- Inbox / Outbox menu
static void updateFolderScreen(void)
{
char buf[SCREEN_LINE_BUFFER_SIZE];
uint8_t count = (showingOutbox ? smsOutboxCount() : smsInboxCount());
displayClearBuf();
menuDisplayTitle(showingOutbox ? "Outbox" : "Inbox");
for (int i = 0; i < FOLDER_ITEM_COUNT; i++)
{
if (i == FOLDER_ITEM_LIST)
{
snprintf(buf, SCREEN_LINE_BUFFER_SIZE, "%s (%u)", (showingOutbox ? "Outbox List" : "Inbox List"), (unsigned int)count);
}
else
{
snprintf(buf, SCREEN_LINE_BUFFER_SIZE, "Delete All");
}
menuDisplayEntry((i - folderIndex), i, buf, (strlen(buf) - 1), THEME_ITEM_FG_MENU_ITEM, THEME_ITEM_FG_MENU_ITEM_SELECTED, THEME_ITEM_BG);
}
displayRender();
}
menuStatus_t menuMessagesFolder(uiEvent_t *ev, bool isFirstRun)
{
if (isFirstRun)
{
updateFolderScreen();
return (MENU_STATUS_LIST_TYPE | MENU_STATUS_SUCCESS);
}
if (ev->hasEvent)
{
if (handleFolderEvent(ev))
{
updateFolderScreen();
}
}
return exitCode;
}
static bool handleFolderEvent(uiEvent_t *ev)
{
if (KEYCHECK_PRESS(ev->keys, KEY_DOWN))
{
folderIndex = ((folderIndex + 1) % FOLDER_ITEM_COUNT);
return true;
}
if (KEYCHECK_PRESS(ev->keys, KEY_UP))
{
folderIndex = ((folderIndex + FOLDER_ITEM_COUNT - 1) % FOLDER_ITEM_COUNT);
return true;
}
if (KEYCHECK_SHORTUP(ev->keys, KEY_GREEN))
{
if (folderIndex == FOLDER_ITEM_LIST)
{
listIndex = 0;
viewingMessage = false;
menuSystemPushNewMenu(MENU_MESSAGES_LIST);
}
else
{
if (showingOutbox)
{
smsOutboxClear();
}
else
{
smsInboxClear();
}
soundSetMelody(MELODY_ACK_BEEP);
}
return true;
}
if (KEYCHECK_SHORTUP(ev->keys, KEY_RED))
{
menuSystemPopPreviousMenu();
return false;
}
return false;
}
// ------------------------------------------------------------- message list
static void updateListScreen(void)
{
char buf[SCREEN_LINE_BUFFER_SIZE];
uint8_t count = (showingOutbox ? smsOutboxCount() : smsInboxCount());
displayClearBuf();
if (count == 0)
{
menuDisplayTitle(showingOutbox ? "Outbox" : "Inbox");
displayPrintCentered(32, "Empty", FONT_SIZE_3);
displayRender();
return;
}
const smsMessage_t *msg = (showingOutbox ? smsOutboxGet((uint8_t)listIndex) : smsInboxGet((uint8_t)listIndex));
if (msg == NULL)
{
displayRender();
return;
}
if (viewingMessage)
{
// Full message: correspondent on the title line, text wrapped below.
snprintf(buf, SCREEN_LINE_BUFFER_SIZE, "%u", (unsigned int)(showingOutbox ? msg->dstId : msg->srcId));
menuDisplayTitle(buf);
// Wrap the text over the available lines rather than letting it run off screen.
const int charsPerLine = 20;
int len = (int)strlen(msg->text);
for (int line = 0; (line < 4) && ((line * charsPerLine) < len); line++)
{
int n = len - (line * charsPerLine);
if (n > charsPerLine)
{
n = charsPerLine;
}
memcpy(buf, &msg->text[line * charsPerLine], n);
buf[n] = '\0';
displayPrintCore(0, (16 + (line * 12)), buf, FONT_SIZE_2, TEXT_ALIGN_LEFT, false);
}
}
else
{
// List: one line per message, newest first.
snprintf(buf, SCREEN_LINE_BUFFER_SIZE, "%s %u/%u", (showingOutbox ? "Outbox" : "Inbox"),
(unsigned int)(listIndex + 1), (unsigned int)count);
menuDisplayTitle(buf);
for (int i = 0; i < count; i++)
{
const smsMessage_t *m = (showingOutbox ? smsOutboxGet((uint8_t)i) : smsInboxGet((uint8_t)i));
if (m == NULL)
{
continue;
}
snprintf(buf, SCREEN_LINE_BUFFER_SIZE, "%s%u %.10s",
((m->unread) ? "*" : " "),
(unsigned int)(showingOutbox ? m->dstId : m->srcId),
m->text);
menuDisplayEntry((i - listIndex), i, buf, (strlen(buf) - 1), THEME_ITEM_FG_MENU_ITEM, THEME_ITEM_FG_MENU_ITEM_SELECTED, THEME_ITEM_BG);
}
}
displayRender();
}
menuStatus_t menuMessagesList(uiEvent_t *ev, bool isFirstRun)
{
if (isFirstRun)
{
updateListScreen();
return (MENU_STATUS_LIST_TYPE | MENU_STATUS_SUCCESS);
}
if (ev->hasEvent)
{
if (handleListEvent(ev))
{
updateListScreen();
}
}
return exitCode;
}
static bool handleListEvent(uiEvent_t *ev)
{
uint8_t count = (showingOutbox ? smsOutboxCount() : smsInboxCount());
if (KEYCHECK_PRESS(ev->keys, KEY_DOWN) && (count > 0) && (viewingMessage == false))
{
listIndex = ((listIndex + 1) % count);
return true;
}
if (KEYCHECK_PRESS(ev->keys, KEY_UP) && (count > 0) && (viewingMessage == false))
{
listIndex = ((listIndex + count - 1) % count);
return true;
}
if (KEYCHECK_SHORTUP(ev->keys, KEY_GREEN) && (count > 0))
{
if (viewingMessage == false)
{
viewingMessage = true;
// Opening a message marks it read.
if (showingOutbox == false)
{
smsMarkRead();
}
}
return true;
}
if (KEYCHECK_SHORTUP(ev->keys, KEY_RED))
{
if (viewingMessage)
{
viewingMessage = false;
return true;
}
menuSystemPopPreviousMenu();
return false;
}
return false;
}
+493
Просмотреть файл
@@ -0,0 +1,493 @@
# Правки, сделанные командами (не отдельными файлами)
Извлечено из транскрипта сессии в порядке применения.
Каждый блок - самостоятельная команда, менявшая исходники прошивки.
## Правка 1 (строка транскрипта 966)
```bash
export SSHPASS='Adelina@2015'
sshpass -e ssh -o StrictHostKeyChecking=no viktor@10.0.0.200 'python3 - <<"PYEOF"
W="/home/viktor/ogd77rel/uv_src/OpenGD77_MDUV380_DM1701_20260130/MDUV380_firmware/application"
p = W + "/include/functions/hotspot.h"
s = open(p).read()
anchor = "#define DMR_SYNC_DATA 0x40U\n"
if anchor not in s:
raise SystemExit("anchor not found in hotspot.h")
if "DT_RATE_12_DATA" in s:
raise SystemExit("already patched")
add = anchor + """
// DMR data burst types (ETSI TS 102 361-1, Slot Type "data type" field).
// The hotspot only ever handled the two voice-related ones; the packet data
// types below are needed to carry SMS/position data over the air.
#define DT_VOICE_PI_HEADER 0x00U
#define DT_CSBK 0x03U
#define DT_DATA_HEADER 0x06U
#define DT_RATE_12_DATA 0x07U
#define DT_RATE_34_DATA 0x08U
#define DT_IDLE 0x09U
#define DT_RATE_1_DATA 0x0AU
#define DT_MASK 0x0FU
// Marker used in hotspotTxDataType[] for "this buffered frame is voice, not data".
#define HOTSPOT_TX_TYPE_VOICE 0xFFU
"""
s = s.replace(anchor, add, 1)
# expose the per-buffer data-type markers to the HR-C6000 driver
tail = "extern bool hotspotMmdvmHostIsConnected;\n"
if tail not in s:
raise SystemExit("tail anchor not found")
s = s.replace(tail, tail + """
// For each frame queued in audioAndHotspotDataBuffer.hotspotBuffer[], this holds either
// HOTSPOT_TX_TYPE_VOICE or the DMR data type (DT_*) that the frame has to be sent as.
extern volatile uint8_t hotspotTxDataType[];
""", 1)
open(p, "w").write(s)
print("hotspot.h patched")
PYEOF' 2>&1 | tail -5
```
## Правка 2 (строка транскрипта 978)
```bash
export SSHPASS='Adelina@2015'
sshpass -e ssh -o StrictHostKeyChecking=no viktor@10.0.0.200 'python3 - <<"PYEOF"
W="/home/viktor/ogd77rel/uv_src/OpenGD77_MDUV380_DM1701_20260130/MDUV380_firmware/application"
p = W + "/source/functions/hotspot.c"
s = open(p).read()
if "hotspotTxDataType" in s:
raise SystemExit("already patched")
# ---- 1. define the marker array next to the other hotspot state ----
anchor = "static uint8_t hotspotTxLC[9];\n"
assert anchor in s, "hotspotTxLC anchor missing"
s = s.replace(anchor, anchor + """
// Per-buffered-frame DMR data type. HOTSPOT_TX_TYPE_VOICE means \"ordinary voice frame\",
// anything else is a packet data burst (SMS etc) that must bypass the vocoder path.
volatile uint8_t hotspotTxDataType[HOTSPOT_BUFFER_COUNT];
""", 1)
# ---- 2. voice frames queued by storeNetFrame() must be marked as voice ----
old = """ memcpy((uint8_t *)&audioAndHotspotDataBuffer.hotspotBuffer[wavbuffer_write_idx], hotspotTxLC, 9);// copy the current LC into the data (mainly for use with the embedded data);
wavbuffer_count++;"""
assert old in s, "storeNetFrame body not found"
s = s.replace(old, """ memcpy((uint8_t *)&audioAndHotspotDataBuffer.hotspotBuffer[wavbuffer_write_idx], hotspotTxLC, 9);// copy the current LC into the data (mainly for use with the embedded data);
hotspotTxDataType[wavbuffer_write_idx] = HOTSPOT_TX_TYPE_VOICE;
wavbuffer_count++;""", 1)
# ---- 3. new function: queue a packet data burst coming from the host ----
anchor2 = "static uint8_t hotspotModeReceiveNetFrame(const uint8_t *comBuffer, uint8_t timeSlot)\n"
assert anchor2 in s, "hotspotModeReceiveNetFrame not found"
s = s.replace(anchor2, """// Queue a packet data burst (CSBK / data header / rate 1/2 payload) received from the
// MMDVM host, so that the HR-C6000 sends it as a real data burst rather than as audio.
//
// The host hands us a complete 33 byte DMR burst (BPTC(196,96) coded). The HR-C6000
// applies the FEC itself and only wants the 96 bit (12 byte) payload, which is exactly
// what BPTCdecode() gives us - the same route the radio already uses for LC headers.
static uint8_t storeNetDataFrame(volatile const uint8_t *comBuffer, uint8_t dataType)
{
if (wavbuffer_count >= HOTSPOT_BUFFER_COUNT)
{
return 5;// out of space: MMDVMHost treats a non-zero return as a NAK
}
BPTCglobalsInit();
taskENTER_CRITICAL();
BPTCdecode((uint8_t *)comBuffer + MMDVM_HEADER_LENGTH,
(uint8_t *)&audioAndHotspotDataBuffer.hotspotBuffer[wavbuffer_write_idx]);
hotspotTxDataType[wavbuffer_write_idx] = dataType;
wavbuffer_count++;
wavbuffer_write_idx = ((wavbuffer_write_idx + 1) % HOTSPOT_BUFFER_COUNT);
taskEXIT_CRITICAL();
// Data bursts are short and self-contained, so start transmitting straight away
// instead of waiting for the audio pre-buffering to fill up.
if ((hotspotState != HOTSPOT_STATE_TX_START_BUFFERING) && (hotspotState != HOTSPOT_STATE_TRANSMITTING))
{
timeoutCounter = TX_BUFFERING_TIMEOUT;
hotspotState = HOTSPOT_STATE_TX_START_BUFFERING;
}
return 0;
}
""" + anchor2, 1)
# ---- 4. dispatch data bursts before the voice-only decode ----
old2 = """ lc.srcId = 0;// zero these values as they are checked later in the function, but only updated if the data type is DT_VOICE_LC_HEADER
lc.dstId = 0;
"""
assert old2 in s, "dispatch anchor not found"
s = s.replace(old2, """ // MMDVM control byte: bit 0x40 marks a data sync burst, low nibble is the DMR data type.
// Voice LC header / terminator keep their existing handling below; the packet data types
// have no path at all in stock firmware, which is why SMS never reached the air.
if ((comBuffer[3] & DMR_SYNC_DATA) == DMR_SYNC_DATA)
{
uint8_t dataType = (comBuffer[3] & DT_MASK);
switch (dataType)
{
case DT_CSBK:
case DT_DATA_HEADER:
case DT_RATE_12_DATA:
return storeNetDataFrame(comBuffer, dataType);
default:
break;// fall through to the voice handling
}
}
""" + old2, 1)
open(p, "w").write(s)
print("hotspot.c patched")
PYEOF' 2>&1 | tail -5
```
## Правка 3 (строка транскрипта 1008)
```bash
export SSHPASS='Adelina@2015'
sshpass -e ssh -o StrictHostKeyChecking=no viktor@10.0.0.200 'python3 - <<"PYEOF"
W="/home/viktor/ogd77rel/uv_src/OpenGD77_MDUV380_DM1701_20260130/MDUV380_firmware/application"
p = W + "/source/hardware/HR-C6000.c"
s = open(p).read()
if "txDataType" in s:
raise SystemExit("already patched")
# ---- 1. state field: which DMR type the frame currently in deferredUpdateBuffer must be sent as
a = "\tvolatile bool hotspotDMRTxFrameBufferEmpty;\n"
assert a in s
s = s.replace(a, a + "\tvolatile uint8_t txDataType;// HOTSPOT_TX_TYPE_VOICE, or a DT_* packet data type\n", 1)
a = "\t\t.hotspotDMRTxFrameBufferEmpty = true,\n"
assert a in s
s = s.replace(a, a + "\t\t.txDataType = HOTSPOT_TX_TYPE_VOICE,\n", 1)
# ---- 2. remember the type whenever a frame is pulled out of the hotspot buffer
a = """ memcpy((uint8_t *)deferredUpdateBuffer, (uint8_t *)&audioAndHotspotDataBuffer.hotspotBuffer[wavbuffer_read_idx], AMBE_AUDIO_LENGTH + LC_DATA_LENGTH);
// Note:"""
assert a in s, "first fetch site missing"
s = s.replace(a, """ memcpy((uint8_t *)deferredUpdateBuffer, (uint8_t *)&audioAndHotspotDataBuffer.hotspotBuffer[wavbuffer_read_idx], AMBE_AUDIO_LENGTH + LC_DATA_LENGTH);
hrc.txDataType = hotspotTxDataType[wavbuffer_read_idx];
// Note:""", 1)
a = """ memcpy((uint8_t *)deferredUpdateBuffer, (uint8_t *)&audioAndHotspotDataBuffer.hotspotBuffer[wavbuffer_read_idx], AMBE_AUDIO_LENGTH + LC_DATA_LENGTH);
wavbuffer_read_idx = ((wavbuffer_read_idx + 1) % HOTSPOT_BUFFER_COUNT);"""
assert a in s, "second fetch site missing"
s = s.replace(a, """ memcpy((uint8_t *)deferredUpdateBuffer, (uint8_t *)&audioAndHotspotDataBuffer.hotspotBuffer[wavbuffer_read_idx], AMBE_AUDIO_LENGTH + LC_DATA_LENGTH);
hrc.txDataType = hotspotTxDataType[wavbuffer_read_idx];
wavbuffer_read_idx = ((wavbuffer_read_idx + 1) % HOTSPOT_BUFFER_COUNT);""", 1)
# ---- 3. TX_2: send a packet data burst instead of pushing the payload through the vocoder
a = """ if (settingsUsbMode == USB_MODE_HOTSPOT)
{
if (hrc.txSequence == 0)
{
SPI0WritePageRegByteArray(0x02, 0x00, (uint8_t*)deferredUpdateBuffer, LC_DATA_LENGTH); // put LC into hardware
}
"""
assert a in s, "TX_2 hotspot branch missing"
s = s.replace(a, """ if (settingsUsbMode == USB_MODE_HOTSPOT)
{
if (hrc.txDataType != HOTSPOT_TX_TYPE_VOICE)
{
// Packet data burst (SMS etc). The 96 bit payload goes into the same
// register page the radio already uses for LC data, and reg 0x50 below
// tells the HR-C6000 which kind of data burst to build around it.
// The vocoder path is left idle - this is what stock firmware lacked.
SPI0WritePageRegByteArray(0x02, 0x00, (uint8_t*)deferredUpdateBuffer, LC_DATA_LENGTH);
SPI1WritePageRegByteArray(0x03, 0x00, SILENCE_AUDIO, AMBE_AUDIO_LENGTH);
hrc.hotspotDMRTxFrameBufferEmpty = true;
SPI0WritePageRegByte(0x04, 0x41, 0x80); // Transmit during next Timeslot
SPI0WritePageRegByte(0x04, 0x50, (hrc.txDataType << 4)); // Data Type = DT_*, Data sync, LCSS = 0
hrc.txDataType = HOTSPOT_TX_TYPE_VOICE; // consumed
slotState = DMR_STATE_TX_1;
break;
}
if (hrc.txSequence == 0)
{
SPI0WritePageRegByteArray(0x02, 0x00, (uint8_t*)deferredUpdateBuffer, LC_DATA_LENGTH); // put LC into hardware
}
""", 1)
open(p, "w").write(s)
print("HR-C6000.c patched")
PYEOF' 2>&1 | tail -5
```
## Правка 4 (строка транскрипта 1052)
```bash
export SSHPASS='Adelina@2015'
sshpass -e ssh -o StrictHostKeyChecking=no viktor@10.0.0.200 'python3 - <<"PYEOF"
W="/home/viktor/ogd77rel/uv_src/OpenGD77_MDUV380_DM1701_20260130/MDUV380_firmware/application"
p = W + "/source/functions/hotspot.c"
s = open(p).read()
if "hotspotTxDataPending" in s:
raise SystemExit("already patched")
# flag: there is at least one packet data burst waiting to go out
a = "volatile uint8_t hotspotTxDataType[HOTSPOT_BUFFER_COUNT];\n"
assert a in s
s = s.replace(a, a + "static volatile bool hotspotTxDataPending = false;// a packet data burst is queued\n", 1)
# set it when queuing a data burst
a = "\t\thotspotTxDataType[wavbuffer_write_idx] = dataType;\n"
assert a in s
s = s.replace(a, a + "\t\thotspotTxDataPending = true;\n", 1)
# Data bursts are only a few frames long, so the audio pre-buffering threshold
# (5 frames) would never be reached and the transmission would time out instead.
a = "\t\t\t\tif (wavbuffer_count > TX_BUFFER_MIN_BEFORE_TRANSMISSION)\n"
assert a in s, "buffering condition not found"
s = s.replace(a, "\t\t\t\tif ((wavbuffer_count > TX_BUFFER_MIN_BEFORE_TRANSMISSION) || hotspotTxDataPending)\n", 1)
# clear the flag once the buffer has drained
a = """ case HOTSPOT_STATE_TX_SHUTDOWN:
"""
assert a in s, "TX_SHUTDOWN case not found"
s = s.replace(a, """ case HOTSPOT_STATE_TX_SHUTDOWN:
if (wavbuffer_count == 0)
{
hotspotTxDataPending = false;
}
""", 1)
# and on (re)initialisation
a = """ case HOTSPOT_STATE_INITIALISE:
"""
assert a in s, "INITIALISE case not found"
s = s.replace(a, """ case HOTSPOT_STATE_INITIALISE:
hotspotTxDataPending = false;
""", 1)
open(p, "w").write(s)
print("patched: data bursts start transmitting immediately")
PYEOF
cd ~/ogd77rel/uv_src/OpenGD77_MDUV380_DM1701_20260130/MDUV380_firmware
make -f Makefile.opengd77 -j$(nproc) 2>&1 | grep -iE " error |error:" | head -10
ls -l build_ogd77/OpenMDUV380.bin && arm-none-eabi-size build_ogd77/OpenMDUV380.elf | tail -1' 2>&1 | tail -8
```
## Правка 5 (строка транскрипта 1069)
```bash
export SSHPASS='Adelina@2015'
sshpass -e ssh -o StrictHostKeyChecking=no viktor@10.0.0.200 'python3 - <<"PYEOF"
W="/home/viktor/ogd77rel/uv_src/OpenGD77_MDUV380_DM1701_20260130/MDUV380_firmware/application"
p = W + "/source/functions/hotspot.c"
s = open(p).read()
if "hotspotTxDataPending" in s:
raise SystemExit("already patched")
a = "volatile uint8_t hotspotTxDataType[HOTSPOT_BUFFER_COUNT];\n"
assert a in s, "array decl"
s = s.replace(a, a + "static volatile bool hotspotTxDataPending = false;// a packet data burst is queued\n", 1)
a = "\thotspotTxDataType[wavbuffer_write_idx] = dataType;\n"
assert a in s, "queue line"
s = s.replace(a, a + "\thotspotTxDataPending = true;\n", 1)
# Data bursts are only a few frames long, so the audio pre-buffering threshold
# would never be reached and the transmission would time out instead.
a = "\t\t\t\tif (wavbuffer_count > TX_BUFFER_MIN_BEFORE_TRANSMISSION)\n"
assert a in s, "buffering condition"
s = s.replace(a, "\t\t\t\tif ((wavbuffer_count > TX_BUFFER_MIN_BEFORE_TRANSMISSION) || hotspotTxDataPending)\n", 1)
a = "\t\tcase HOTSPOT_STATE_TX_SHUTDOWN:\n"
assert a in s, "TX_SHUTDOWN"
s = s.replace(a, a + "\t\t\tif (wavbuffer_count == 0)\n\t\t\t{\n\t\t\t\thotspotTxDataPending = false;\n\t\t\t}\n", 1)
a = "\t\tcase HOTSPOT_STATE_INITIALISE:\n"
assert a in s, "INITIALISE"
s = s.replace(a, a + "\t\t\thotspotTxDataPending = false;\n", 1)
open(p, "w").write(s)
print("patched OK")
PYEOF
cd ~/ogd77rel/uv_src/OpenGD77_MDUV380_DM1701_20260130/MDUV380_firmware
make -f Makefile.opengd77 -j$(nproc) 2>&1 | grep -iE " error |error:" | head -10
echo "=== build result ==="
ls -l build_ogd77/OpenMDUV380.bin && arm-none-eabi-size build_ogd77/OpenMDUV380.elf | tail -1' 2>&1 | tail -8
```
## Правка 6 (строка транскрипта 1205)
```bash
export SSHPASS='Adelina@2015'
sshpass -e ssh -o StrictHostKeyChecking=no viktor@10.0.0.200 'python3 - <<"PYEOF"
W="/home/viktor/ogd77rel/uv_src/OpenGD77_MDUV380_DM1701_20260130/MDUV380_firmware/application"
# --- 1. new RX event type -------------------------------------------------
p = W + "/include/functions/hotspot.h"
s = open(p).read()
if "HOTSPOT_RX_DATA_FRAME" not in s:
a = "\tHOTSPOT_RX_STOP,\n"
assert a in s
s = s.replace(a, a + "\tHOTSPOT_RX_DATA_FRAME,// a packet data burst (SMS etc) was received off air\n", 1)
open(p,"w").write(s)
print("hotspot.h: RX event added")
# --- 2. bring the slot type encoder over from the MK22 branch -------------
src = open("/home/viktor/OpenGD77/firmware/source/hotspot/DMRSlotType.c").read()
start = src.index("static const uint16_t ENCODING_TABLE_2087[] =")
end = src.index(";", src.index("};", start))
table = src[start:src.index("};", start)+2]
p = W + "/source/functions/hotspot.c"
s = open(p).read()
if "ENCODING_TABLE_2087" in s:
raise SystemExit("hotspot.c already has the slot type encoder")
anchor = "static void BPTCencode(const uint8_t *inputData, uint8_t *outputData)\n{"
assert anchor in s, "BPTCencode definition not found"
block = """// Slot Type encoder, needed to build received data bursts for the host.
// Golay(20,8) table and logic taken from the project\\'s own DMRSlotType implementation.
""" + table + """
// Writes colour code + data type (with FEC) into the Slot Type half-bursts of a frame.
static void DMRSlotTypeEncode(uint32_t colorCode, uint32_t dataType, uint8_t *frame)
{
uint8_t slotType[3];
slotType[0] = (colorCode << 4) & 0xF0U;
slotType[0] |= (dataType << 0) & 0x0FU;
uint16_t cksum = ENCODING_TABLE_2087[slotType[0]];
slotType[1] = (cksum >> 0) & 0xFFU;
slotType[2] = (cksum >> 8) & 0xFFU;
frame[12] = (frame[12] & 0xC0U) | ((slotType[0] >> 2) & 0x3FU);
frame[13] = (frame[13] & 0x0FU) | ((slotType[0] << 6) & 0xC0U) | ((slotType[1] >> 2) & 0x30U);
frame[19] = (frame[19] & 0xF0U) | ((slotType[1] >> 2) & 0x0FU);
frame[20] = (frame[20] & 0x03U) | ((slotType[1] << 6) & 0xC0U) | ((slotType[2] >> 2) & 0x3CU);
}
""" + anchor
s = s.replace(anchor, block, 1)
open(p,"w").write(s)
print("hotspot.c: slot type encoder added (%d bytes of table)" % len(table))
PYEOF' 2>&1 | tail -6
```
## Правка 7 (строка транскрипта 3341)
```bash
export SSHPASS='Adelina@2015'
sshpass -e ssh -o StrictHostKeyChecking=no viktor@10.0.0.200 'python3 - <<"PYEOF"
import glob, os
W = "/home/viktor/ogd77rel/uv_src/OpenGD77_MDUV380_DM1701_20260130/MDUV380_firmware/application"
# 1. new field at the end of the strings table
p = W + "/include/user_interface/languages/uiLanguage.h"
s = open(p).read()
if "messages[LANGUAGE_TEXTS_LENGTH]" in s:
print("uiLanguage.h: field already present")
else:
s = s.replace(" const char mute[LANGUAGE_TEXTS_LENGTH];\n} stringsTable_t;",
" const char mute[LANGUAGE_TEXTS_LENGTH];\n"
" const char messages[LANGUAGE_TEXTS_LENGTH];\n} stringsTable_t;", 1)
# the struct changed, so the language file version has to move on
s = s.replace("#define LANGUAGE_TAG_VERSION { 0x00, 0x00, 0x00, 0x05 }",
"#define LANGUAGE_TAG_VERSION { 0x00, 0x00, 0x00, 0x06 }", 1)
open(p, "w").write(s)
print("uiLanguage.h: added .messages, bumped tag version to 6")
# 2. give every language a value; designated initialisers mean a missing one would
# simply render as an empty menu entry, which looks broken.
done = 0
for f in sorted(glob.glob(W + "/include/user_interface/languages/*.h")):
if f.endswith("uiLanguage.h"):
continue
t = open(f, "r", encoding="latin-1").read()
if ".messages" in t:
continue
if ".mute" not in t:
print(" skipped %s (no .mute anchor)" % os.path.basename(f))
continue
idx = t.index(".mute")
eol = t.index("\n", idx)
t = t[:eol + 1] + ".messages\t\t\t\t= \"Messages\", // MaxLen: 16\n" + t[eol + 1:]
open(f, "w", encoding="latin-1").write(t)
done += 1
print("added .messages to %d language files" % done)
PYEOF' 2>&1 | tail -6
```
## Правка 8 (строка транскрипта 3357)
```bash
export SSHPASS='Adelina@2015'
sshpass -e scp -o StrictHostKeyChecking=no /tmp/claude-1000/-home-viktor-claude-uv390/44e0d3e2-595b-4c07-94e1-73e0e3f7531a/menuMessages.c viktor@10.0.0.200:'~/ogd77rel/uv_src/OpenGD77_MDUV380_DM1701_20260130/MDUV380_firmware/application/source/user_interface/menuMessages.c' >/dev/null 2>&1
sshpass -e ssh -o StrictHostKeyChecking=no viktor@10.0.0.200 'python3 - <<"PYEOF"
W = "/home/viktor/ogd77rel/uv_src/OpenGD77_MDUV380_DM1701_20260130/MDUV380_firmware/application"
# --- declare the three screens and their menu ids -------------------------
p = W + "/include/user_interface/menuSystem.h"
s = open(p).read()
if "MENU_MESSAGES" not in s:
s = s.replace("\tMENU_LAST_HEARD,\n",
"\tMENU_LAST_HEARD,\n\tMENU_MESSAGES,\n\tMENU_MESSAGES_FOLDER,\n\tMENU_MESSAGES_LIST,\n", 1)
s = s.replace("menuStatus_t uiMessageBox(uiEvent_t *event, bool isFirstRun);",
"menuStatus_t uiMessageBox(uiEvent_t *event, bool isFirstRun);\n"
"menuStatus_t menuMessages(uiEvent_t *ev, bool isFirstRun);\n"
"menuStatus_t menuMessagesFolder(uiEvent_t *ev, bool isFirstRun);\n"
"menuStatus_t menuMessagesList(uiEvent_t *ev, bool isFirstRun);", 1)
open(p, "w").write(s)
print("menuSystem.h: ids and prototypes added")
else:
print("menuSystem.h: already declared")
# --- register the screens in the dispatch table ---------------------------
p = W + "/source/user_interface/menuSystem.c"
s = open(p).read()
if "menuMessages," in s:
print("menuSystem.c: already registered")
else:
# the table order must match the enum order: our ids come right after MENU_LAST_HEARD
idx = s.index("static menuFunctionData_t menuFunctions[] =")
marker = "{ menuLastHeard,"
j = s.index(marker, idx)
eol = s.index("\n", j) + 1
s = s[:eol] + ("\t\t{ menuMessages, NULL, NULL, 0 },\n"
"\t\t{ menuMessagesFolder, NULL, NULL, 0 },\n"
"\t\t{ menuMessagesList, NULL, NULL, 0 },\n") + s[eol:]
# and a Messages entry in the main menu, using the new language string
s = s.replace("\t{ 7, MENU_LAST_HEARD },\n",
"\t{ 7, MENU_LAST_HEARD },\n\t{ 281, MENU_MESSAGES },\n", 1)
open(p, "w").write(s)
print("menuSystem.c: screens registered and main menu entry added")
PYEOF
cd ~/ogd77rel/uv_src/OpenGD77_MDUV380_DM1701_20260130/MDUV380_firmware
make -f Makefile.opengd77 -j$(nproc) 2>&1 | grep -iE "error|undefined" | head -10
arm-none-eabi-size build_ogd77/OpenMDUV380.elf 2>/dev/null | tail -1' 2>&1 | tail -12
```
## Правка 9 (строка транскрипта 3364)
```bash
export SSHPASS='Adelina@2015'
sshpass -e ssh -o StrictHostKeyChecking=no viktor@10.0.0.200 '
W=~/ogd77rel/uv_src/OpenGD77_MDUV380_DM1701_20260130/MDUV380_firmware/application
sed -i "s/THEME_ITEM_FG_MENU_ITEM_SELECTED/THEME_ITEM_COLOUR_NONE/g" $W/source/user_interface/menuMessages.c
sed -i "s/, (strlen(buf) - 1), THEME_ITEM_FG_MENU_ITEM,/, 0, THEME_ITEM_FG_MENU_ITEM,/g" $W/source/user_interface/menuMessages.c
cd ~/ogd77rel/uv_src/OpenGD77_MDUV380_DM1701_20260130/MDUV380_firmware
make -f Makefile.opengd77 -j$(nproc) 2>&1 | grep -iE "error|undefined" | head -10
arm-none-eabi-size build_ogd77/OpenMDUV380.elf 2>/dev/null | tail -1' 2>&1 | tail -10
```
+105
Просмотреть файл
@@ -0,0 +1,105 @@
#!/usr/bin/env python3
"""Diagnostic build: make the radio show what its SMS assembler actually sees.
Three counters are surfaced on screen, so we can tell where reception stops:
RX - data bursts handed to the assembler by the driver
HDR - data headers parsed (with the announced block count)
ASM - completed assemblies (all blocks collected)
If RX stays at zero the bursts never reach the module and the problem is in the
driver-side capture conditions. If RX counts up but HDR does not, header parsing is
rejecting them. If HDR appears but ASM does not, blocks are being lost.
"""
W = "/home/viktor/ogd77rel/uv_src/OpenGD77_MDUV380_DM1701_20260130/MDUV380_firmware/application"
# ---------------------------------------------------------------- sms.h
p = W + "/include/functions/sms.h"
s = open(p).read()
if "smsGetDiagCounters" not in s:
a = "bool smsHasUnread(void);\n"
assert a in s
s = s.replace(a, a + """
// Diagnostics: how many bursts reached the assembler, how many headers were parsed,
// and how many messages assembled completely.
void smsGetDiagCounters(uint16_t *bursts, uint16_t *headers, uint16_t *assembled, uint8_t *lastBlocks);
""", 1)
open(p, "w").write(s)
print("sms.h: diagnostics declared")
# ---------------------------------------------------------------- sms.c
p = W + "/source/functions/sms.c"
s = open(p).read()
if "diagBursts" in s:
print("sms.c: already instrumented")
else:
a = "static bool unread = false;\n"
assert a in s
s = s.replace(a, a + """
static volatile uint16_t diagBursts = 0;
static uint16_t diagHeaders = 0;
static uint16_t diagAssembled = 0;
static uint8_t diagLastBlocks = 0;
""", 1)
# count every burst the driver hands us
a = "\tmemcpy((uint8_t *)rxQueue[rxQueueWrite], payload, SMS_BLOCK_LENGTH);\n"
assert a in s
s = s.replace(a, "\tdiagBursts++;\n" + a, 1)
# count parsed headers, remembering the announced block count
a = "\tblocksExpected = (uint8_t)(hdr[8] & 0x7FU);\n"
assert a in s
s = s.replace(a, a + "\tdiagHeaders++;\n\tdiagLastBlocks = blocksExpected;\n", 1)
# count completed assemblies
a = "static void finishAssembly(void)\n{\n"
assert a in s
s = s.replace(a, a + "\tdiagAssembled++;\n", 1)
s += """
void smsGetDiagCounters(uint16_t *bursts, uint16_t *headers, uint16_t *assembled, uint8_t *lastBlocks)
{
*bursts = diagBursts;
*headers = diagHeaders;
*assembled = diagAssembled;
*lastBlocks = diagLastBlocks;
}
"""
open(p, "w").write(s)
print("sms.c: counters added")
# ------------------------------------------------------- applicationMain.c
p = W + "/source/applicationMain.c"
s = open(p).read()
if "smsGetDiagCounters" in s:
print("applicationMain.c: already showing diagnostics")
else:
a = "\t\tif (smsHasUnread())\n"
assert a in s, "unread check not found"
s = s.replace(a, """\t\t{
\t\t\t// Diagnostics: show the assembler counters whenever they change, so we can see
\t\t\t// how far a received message gets even when no text is produced.
\t\t\tstatic uint16_t lastShownBursts = 0;
\t\t\tstatic uint16_t lastShownHeaders = 0;
\t\t\tuint16_t dBursts, dHeaders, dAssembled;
\t\t\tuint8_t dBlocks;
\t\t\tsmsGetDiagCounters(&dBursts, &dHeaders, &dAssembled, &dBlocks);
\t\t\tif ((dBursts != lastShownBursts) || (dHeaders != lastShownHeaders))
\t\t\t{
\t\t\t\tchar diagLine[40];
\t\t\t\tlastShownBursts = dBursts;
\t\t\t\tlastShownHeaders = dHeaders;
\t\t\t\tsnprintf(diagLine, sizeof(diagLine), "RX%u HDR%u/%u ASM%u",
\t\t\t\t\t\t(unsigned int)dBursts, (unsigned int)dHeaders,
\t\t\t\t\t\t(unsigned int)dBlocks, (unsigned int)dAssembled);
\t\t\t\tuiNotificationShow(NOTIFICATION_TYPE_MESSAGE, NOTIFICATION_ID_MESSAGE, 8000, diagLine, true);
\t\t\t\tsoundSetMelody(MELODY_KEY_BEEP);
\t\t\t}
\t\t}
""" + a, 1)
open(p, "w").write(s)
print("applicationMain.c: diagnostics displayed")
+72
Просмотреть файл
@@ -0,0 +1,72 @@
#!/usr/bin/env python3
"""Expose the SMS diagnostics through a RAM window that can be read over USB.
Drawing counters on the screen turned out to be unreliable: the notification is
overwritten by the normal screen redraw, so a screenshot usually shows nothing even
when reception is happening. Instead the counters are mirrored into the tail of the
raw audio buffer, which the CPS protocol can read out at any moment (area 7).
Layout at rawBuffer[2376..2399]:
'S' 'M' 'S' 'D' magic, so a stale buffer cannot be mistaken for data
u16 bursts data bursts handed to the assembler
u16 headers data headers parsed
u16 assembled messages assembled completely
u16 lastBlocks block count announced by the last header
u16 collected blocks collected for the message in progress
u16 state 0 = idle, 1 = collecting
u16 textLen length of the last extracted text
"""
W = "/home/viktor/ogd77rel/uv_src/OpenGD77_MDUV380_DM1701_20260130/MDUV380_firmware/application"
p = W + "/source/functions/sms.c"
s = open(p).read()
if "SMS_DIAG_OFFSET" in s:
print("already patched")
raise SystemExit(0)
s = s.replace('#include "functions/ticks.h"\n',
'#include "functions/ticks.h"\n#include "functions/sound.h"\n', 1)
anchor = "void smsInit(void)\n"
assert anchor in s
helper = """// Diagnostics mirror: the tail of the raw audio buffer is readable over USB (CPS area 7),
// which is far more dependable than trying to keep text on the screen.
#define SMS_DIAG_OFFSET 2376
static void smsDiagPublish(void)
{
volatile uint8_t *d = &audioAndHotspotDataBuffer.rawBuffer[SMS_DIAG_OFFSET];
uint16_t values[7];
values[0] = diagBursts;
values[1] = diagHeaders;
values[2] = diagAssembled;
values[3] = diagLastBlocks;
values[4] = blocksCollected;
values[5] = (uint16_t)rxState;
values[6] = diagTextLen;
d[0] = 'S'; d[1] = 'M'; d[2] = 'S'; d[3] = 'D';
for (uint8_t i = 0; i < 7; i++)
{
d[4 + (i * 2)] = (uint8_t)(values[i] & 0xFF);
d[5 + (i * 2)] = (uint8_t)(values[i] >> 8);
}
}
"""
s = s.replace(anchor, helper + anchor, 1)
# track the extracted text length too
s = s.replace("static uint8_t diagLastBlocks = 0;\n",
"static uint8_t diagLastBlocks = 0;\nstatic uint16_t diagTextLen = 0;\n", 1)
s = s.replace("\tlatest.text[t] = '\\0';\n",
"\tlatest.text[t] = '\\0';\n\tdiagTextLen = t;\n", 1)
# publish on every tick, so a read over USB always sees the current state
s = s.replace("void smsTick(void)\n{\n", "void smsTick(void)\n{\n\tsmsDiagPublish();\n\n", 1)
open(p, "w").write(s)
print("sms.c: diagnostics mirrored into the readable RAM window")
+448
Просмотреть файл
@@ -0,0 +1,448 @@
#!/usr/bin/env python3
"""Fix the Messages menus: wrong language string index, and hand-rolled navigation.
Two defects:
- the main menu entry pointed at string 281 while .messages is index 280, so the
entry rendered as "GD77LANG" (it picked up a neighbouring field);
- the screens tracked the selection in their own variables and drew entries at a
fixed offset, instead of using the menu system's list handling. That makes the
cursor behave wrongly (no scrolling, wrong highlight).
Both screens are rewritten to the pattern used by the stock list menus:
menuDataGlobal.currentItemIndex + menuGetMenuOffset() + menuSystemMenuIncrement().
"""
W = "/home/viktor/ogd77rel/uv_src/OpenGD77_MDUV380_DM1701_20260130/MDUV380_firmware/application"
# ---- 1. correct the main menu string index ------------------------------
p = W + "/source/user_interface/menuSystem.c"
s = open(p).read()
if "{ 281, MENU_MESSAGES" in s:
s = s.replace("{ 281, MENU_MESSAGES },", "{ 280, MENU_MESSAGES },", 1)
open(p, "w").write(s)
print("menuSystem.c: string index corrected 281 -> 280")
else:
print("menuSystem.c: index already correct")
# ---- 2. rewrite the screens using the standard list handling ------------
p = W + "/source/user_interface/menuMessages.c"
src = open(p).read()
header_end = src.index("// Entries of the top level Messages menu.")
header = src[:header_end]
body = '''// Entries of the top level Messages menu.
enum
{
MESSAGES_ITEM_NEW = 0,
MESSAGES_ITEM_INBOX,
MESSAGES_ITEM_OUTBOX,
NUM_MESSAGES_ITEMS
};
// Entries of the Inbox / Outbox menus.
enum
{
FOLDER_ITEM_LIST = 0,
FOLDER_ITEM_DELETE_ALL,
NUM_FOLDER_ITEMS
};
static bool showingOutbox = false; // which folder the list screens act on
static bool viewingMessage = false;// a single message is open
static menuStatus_t exitCode = MENU_STATUS_SUCCESS;
static void messagesUpdateScreen(void);
static void messagesHandleEvent(uiEvent_t *ev);
static void folderUpdateScreen(void);
static void folderHandleEvent(uiEvent_t *ev);
static void listUpdateScreen(void);
static void listHandleEvent(uiEvent_t *ev);
// ---------------------------------------------------------------- Messages
menuStatus_t menuMessages(uiEvent_t *ev, bool isFirstRun)
{
if (isFirstRun)
{
menuDataGlobal.numItems = NUM_MESSAGES_ITEMS;
menuDataGlobal.currentItemIndex = 0;
messagesUpdateScreen();
return (MENU_STATUS_LIST_TYPE | MENU_STATUS_SUCCESS);
}
exitCode = MENU_STATUS_SUCCESS;
if (ev->hasEvent)
{
messagesHandleEvent(ev);
}
return exitCode;
}
static void messagesUpdateScreen(void)
{
char buf[SCREEN_LINE_BUFFER_SIZE];
int mNum;
displayClearBuf();
menuDisplayTitle(currentLanguage->messages);
for (int i = MENU_START_ITERATION_VALUE; i <= MENU_END_ITERATION_VALUE; i++)
{
mNum = menuGetMenuOffset(NUM_MESSAGES_ITEMS, i);
if (mNum == MENU_OFFSET_BEFORE_FIRST_ENTRY)
{
continue;
}
else if (mNum == MENU_OFFSET_AFTER_LAST_ENTRY)
{
break;
}
switch (mNum)
{
case MESSAGES_ITEM_NEW:
snprintf(buf, SCREEN_LINE_BUFFER_SIZE, "New msg");
break;
case MESSAGES_ITEM_INBOX:
snprintf(buf, SCREEN_LINE_BUFFER_SIZE, "Inbox (%u)", (unsigned int)smsInboxCount());
break;
case MESSAGES_ITEM_OUTBOX:
snprintf(buf, SCREEN_LINE_BUFFER_SIZE, "Outbox (%u)", (unsigned int)smsOutboxCount());
break;
default:
buf[0] = 0;
break;
}
menuDisplayEntry(i, mNum, buf, 0, THEME_ITEM_FG_MENU_ITEM, THEME_ITEM_COLOUR_NONE, THEME_ITEM_BG);
}
displayRender();
}
static void messagesHandleEvent(uiEvent_t *ev)
{
if (KEYCHECK_PRESS(ev->keys, KEY_DOWN))
{
menuSystemMenuIncrement(&menuDataGlobal.currentItemIndex, NUM_MESSAGES_ITEMS);
messagesUpdateScreen();
exitCode |= MENU_STATUS_LIST_TYPE;
return;
}
if (KEYCHECK_PRESS(ev->keys, KEY_UP))
{
menuSystemMenuDecrement(&menuDataGlobal.currentItemIndex, NUM_MESSAGES_ITEMS);
messagesUpdateScreen();
exitCode |= MENU_STATUS_LIST_TYPE;
return;
}
if (KEYCHECK_SHORTUP(ev->keys, KEY_GREEN))
{
switch (menuDataGlobal.currentItemIndex)
{
case MESSAGES_ITEM_NEW:
// Sending is not implemented yet; say so rather than opening a dead screen.
uiNotificationShow(NOTIFICATION_TYPE_MESSAGE, NOTIFICATION_ID_MESSAGE, 2000, "Not yet", true);
break;
case MESSAGES_ITEM_INBOX:
showingOutbox = false;
menuSystemPushNewMenu(MENU_MESSAGES_FOLDER);
break;
case MESSAGES_ITEM_OUTBOX:
showingOutbox = true;
menuSystemPushNewMenu(MENU_MESSAGES_FOLDER);
break;
}
return;
}
if (KEYCHECK_SHORTUP(ev->keys, KEY_RED))
{
menuSystemPopPreviousMenu();
}
}
// ------------------------------------------------------- Inbox / Outbox menu
menuStatus_t menuMessagesFolder(uiEvent_t *ev, bool isFirstRun)
{
if (isFirstRun)
{
menuDataGlobal.numItems = NUM_FOLDER_ITEMS;
menuDataGlobal.currentItemIndex = 0;
folderUpdateScreen();
return (MENU_STATUS_LIST_TYPE | MENU_STATUS_SUCCESS);
}
exitCode = MENU_STATUS_SUCCESS;
if (ev->hasEvent)
{
folderHandleEvent(ev);
}
return exitCode;
}
static void folderUpdateScreen(void)
{
char buf[SCREEN_LINE_BUFFER_SIZE];
uint8_t count = (showingOutbox ? smsOutboxCount() : smsInboxCount());
int mNum;
displayClearBuf();
menuDisplayTitle(showingOutbox ? "Outbox" : "Inbox");
for (int i = MENU_START_ITERATION_VALUE; i <= MENU_END_ITERATION_VALUE; i++)
{
mNum = menuGetMenuOffset(NUM_FOLDER_ITEMS, i);
if (mNum == MENU_OFFSET_BEFORE_FIRST_ENTRY)
{
continue;
}
else if (mNum == MENU_OFFSET_AFTER_LAST_ENTRY)
{
break;
}
if (mNum == FOLDER_ITEM_LIST)
{
snprintf(buf, SCREEN_LINE_BUFFER_SIZE, "%s (%u)", (showingOutbox ? "Outbox List" : "Inbox List"), (unsigned int)count);
}
else
{
snprintf(buf, SCREEN_LINE_BUFFER_SIZE, "Delete All");
}
menuDisplayEntry(i, mNum, buf, 0, THEME_ITEM_FG_MENU_ITEM, THEME_ITEM_COLOUR_NONE, THEME_ITEM_BG);
}
displayRender();
}
static void folderHandleEvent(uiEvent_t *ev)
{
if (KEYCHECK_PRESS(ev->keys, KEY_DOWN))
{
menuSystemMenuIncrement(&menuDataGlobal.currentItemIndex, NUM_FOLDER_ITEMS);
folderUpdateScreen();
exitCode |= MENU_STATUS_LIST_TYPE;
return;
}
if (KEYCHECK_PRESS(ev->keys, KEY_UP))
{
menuSystemMenuDecrement(&menuDataGlobal.currentItemIndex, NUM_FOLDER_ITEMS);
folderUpdateScreen();
exitCode |= MENU_STATUS_LIST_TYPE;
return;
}
if (KEYCHECK_SHORTUP(ev->keys, KEY_GREEN))
{
if (menuDataGlobal.currentItemIndex == FOLDER_ITEM_LIST)
{
viewingMessage = false;
menuSystemPushNewMenu(MENU_MESSAGES_LIST);
}
else
{
if (showingOutbox)
{
smsOutboxClear();
}
else
{
smsInboxClear();
}
soundSetMelody(MELODY_ACK_BEEP);
folderUpdateScreen();
}
return;
}
if (KEYCHECK_SHORTUP(ev->keys, KEY_RED))
{
menuSystemPopPreviousMenu();
}
}
// ------------------------------------------------------------- message list
menuStatus_t menuMessagesList(uiEvent_t *ev, bool isFirstRun)
{
if (isFirstRun)
{
menuDataGlobal.numItems = (showingOutbox ? smsOutboxCount() : smsInboxCount());
menuDataGlobal.currentItemIndex = 0;
viewingMessage = false;
listUpdateScreen();
return (MENU_STATUS_LIST_TYPE | MENU_STATUS_SUCCESS);
}
exitCode = MENU_STATUS_SUCCESS;
if (ev->hasEvent)
{
listHandleEvent(ev);
}
return exitCode;
}
static void listUpdateScreen(void)
{
char buf[SCREEN_LINE_BUFFER_SIZE];
uint8_t count = (showingOutbox ? smsOutboxCount() : smsInboxCount());
int mNum;
displayClearBuf();
if (count == 0)
{
menuDisplayTitle(showingOutbox ? "Outbox" : "Inbox");
displayPrintCentered(32, "Empty", FONT_SIZE_3);
displayRender();
return;
}
if (viewingMessage)
{
const smsMessage_t *msg = (showingOutbox
? smsOutboxGet((uint8_t)menuDataGlobal.currentItemIndex)
: smsInboxGet((uint8_t)menuDataGlobal.currentItemIndex));
if (msg != NULL)
{
const int charsPerLine = 20;
int len = (int)strlen(msg->text);
snprintf(buf, SCREEN_LINE_BUFFER_SIZE, "%u", (unsigned int)(showingOutbox ? msg->dstId : msg->srcId));
menuDisplayTitle(buf);
// Wrap the text so long messages are readable instead of being cut off.
for (int line = 0; (line < 4) && ((line * charsPerLine) < len); line++)
{
int n = len - (line * charsPerLine);
if (n > charsPerLine)
{
n = charsPerLine;
}
memcpy(buf, &msg->text[line * charsPerLine], n);
buf[n] = 0;
displayPrintCore(0, (16 + (line * 12)), buf, FONT_SIZE_2, TEXT_ALIGN_LEFT, false);
}
}
displayRender();
return;
}
menuDisplayTitle(showingOutbox ? "Outbox" : "Inbox");
for (int i = MENU_START_ITERATION_VALUE; i <= MENU_END_ITERATION_VALUE; i++)
{
mNum = menuGetMenuOffset(count, i);
if (mNum == MENU_OFFSET_BEFORE_FIRST_ENTRY)
{
continue;
}
else if (mNum == MENU_OFFSET_AFTER_LAST_ENTRY)
{
break;
}
const smsMessage_t *m = (showingOutbox ? smsOutboxGet((uint8_t)mNum) : smsInboxGet((uint8_t)mNum));
if (m == NULL)
{
continue;
}
snprintf(buf, SCREEN_LINE_BUFFER_SIZE, "%s%u %.8s",
(m->unread ? "*" : ""),
(unsigned int)(showingOutbox ? m->dstId : m->srcId),
m->text);
menuDisplayEntry(i, mNum, buf, 0, THEME_ITEM_FG_MENU_ITEM, THEME_ITEM_COLOUR_NONE, THEME_ITEM_BG);
}
displayRender();
}
static void listHandleEvent(uiEvent_t *ev)
{
uint8_t count = (showingOutbox ? smsOutboxCount() : smsInboxCount());
if ((viewingMessage == false) && (count > 0))
{
if (KEYCHECK_PRESS(ev->keys, KEY_DOWN))
{
menuSystemMenuIncrement(&menuDataGlobal.currentItemIndex, count);
listUpdateScreen();
exitCode |= MENU_STATUS_LIST_TYPE;
return;
}
if (KEYCHECK_PRESS(ev->keys, KEY_UP))
{
menuSystemMenuDecrement(&menuDataGlobal.currentItemIndex, count);
listUpdateScreen();
exitCode |= MENU_STATUS_LIST_TYPE;
return;
}
}
if (KEYCHECK_SHORTUP(ev->keys, KEY_GREEN) && (count > 0))
{
if (viewingMessage == false)
{
viewingMessage = true;
if (showingOutbox == false)
{
smsMarkRead();
}
listUpdateScreen();
}
return;
}
if (KEYCHECK_SHORTUP(ev->keys, KEY_RED))
{
if (viewingMessage)
{
viewingMessage = false;
listUpdateScreen();
return;
}
menuSystemPopPreviousMenu();
}
}
'''
open(p, "w").write(header + body)
print("menuMessages.c: rewritten with standard list navigation")
+44
Просмотреть файл
@@ -0,0 +1,44 @@
#!/usr/bin/env python3
"""Stop the radio from dropping out of hotspot (modem) mode when the host goes quiet.
Stock behaviour: if the USB host does not talk to the radio for MMDVMHOST_TIMEOUT
(20 s), the firmware calls hotspotExit() and leaves modem mode by itself. That is a
safety net for a crashed host, but it makes the radio unusable as a permanently
attached modem - which is exactly how it is used here.
With the timeout set to 0 the inactivity check is disabled; leaving modem mode is
then only done deliberately (by the user from the radio, or by the host asking the
modem to go idle).
"""
W = "/home/viktor/ogd77rel/uv_src/OpenGD77_MDUV380_DM1701_20260130/MDUV380_firmware/application"
p = W + "/source/functions/hotspot.c"
s = open(p).read()
if "MMDVMHOST_TIMEOUT_DISABLED" in s:
print("already patched")
raise SystemExit(0)
old_const = "static const uint32_t MMDVMHOST_TIMEOUT = 20000; // 20s timeout (MMDVMHost mode only, there is no timeout for BlueDV)\n"
assert old_const in s, "timeout constant not found"
new_const = (
"// 0 disables the host-inactivity timeout, so the radio stays in modem mode until it is\n"
"// told to leave. Set this back to 20000 to restore the stock 20 second auto-exit.\n"
"#define MMDVMHOST_TIMEOUT_DISABLED 0\n"
"static const uint32_t MMDVMHOST_TIMEOUT = MMDVMHOST_TIMEOUT_DISABLED;\n"
)
s = s.replace(old_const, new_const, 1)
# Guard both inactivity checks so a zero timeout means "never time out".
old_check = """ if ((nonVolatileSettings.hotspotType == HOTSPOT_TYPE_MMDVM) &&
((ticksGetMillis() - mmdvmHostLastActiveTime) > MMDVMHOST_TIMEOUT))
"""
new_check = """ if ((MMDVMHOST_TIMEOUT != 0) && (nonVolatileSettings.hotspotType == HOTSPOT_TYPE_MMDVM) &&
((ticksGetMillis() - mmdvmHostLastActiveTime) > MMDVMHOST_TIMEOUT))
"""
count = s.count(old_check)
assert count == 2, "expected 2 inactivity checks, found %d" % count
s = s.replace(old_check, new_check)
open(p, "w").write(s)
print("hotspot.c: host-inactivity auto-exit disabled (%d checks guarded)" % count)
+62
Просмотреть файл
@@ -0,0 +1,62 @@
#!/usr/bin/env python3
"""Patch 1 of RX support: new RX event type + Slot Type encoder."""
W = "/home/viktor/ogd77rel/uv_src/OpenGD77_MDUV380_DM1701_20260130/MDUV380_firmware/application"
# --- 1. new RX event type -------------------------------------------------
p = W + "/include/functions/hotspot.h"
s = open(p).read()
if "HOTSPOT_RX_DATA_FRAME" not in s:
a = "\tHOTSPOT_RX_STOP,\n"
assert a in s, "RX enum anchor missing"
s = s.replace(a, a + "\tHOTSPOT_RX_DATA_FRAME,// a packet data burst (SMS etc) was received off air\n", 1)
open(p, "w").write(s)
print("hotspot.h: RX event added")
else:
print("hotspot.h: already has RX event")
# --- 2. bring the Slot Type encoder over from the older MK22 branch -------
src = open("/home/viktor/OpenGD77/firmware/source/hotspot/DMRSlotType.c").read()
start = src.index("static const uint16_t ENCODING_TABLE_2087[] =")
table = src[start:src.index("};", start) + 2]
p = W + "/source/functions/hotspot.c"
s = open(p).read()
if "ENCODING_TABLE_2087" in s:
print("hotspot.c: encoder already present")
raise SystemExit(0)
anchor = "static void BPTCencode(const uint8_t *inputData, uint8_t *outputData)\n{"
assert anchor in s, "BPTCencode definition not found"
header = (
"// Slot Type encoder, needed to build received data bursts for the host.\n"
"// Golay(20,8) table and logic come from the project's own DMRSlotType implementation\n"
"// (present in the MK22 branch, absent here because stock firmware never sent data).\n"
)
func = """
// Writes colour code + data type (FEC protected) into the Slot Type half-bursts of a frame.
static void DMRSlotTypeEncode(uint32_t colorCode, uint32_t dataType, uint8_t *frame)
{
\tuint8_t slotType[3];
\tslotType[0] = (colorCode << 4) & 0xF0U;
\tslotType[0] |= (dataType << 0) & 0x0FU;
\tuint16_t cksum = ENCODING_TABLE_2087[slotType[0]];
\tslotType[1] = (cksum >> 0) & 0xFFU;
\tslotType[2] = (cksum >> 8) & 0xFFU;
\tframe[12] = (frame[12] & 0xC0U) | ((slotType[0] >> 2) & 0x3FU);
\tframe[13] = (frame[13] & 0x0FU) | ((slotType[0] << 6) & 0xC0U) | ((slotType[1] >> 2) & 0x30U);
\tframe[19] = (frame[19] & 0xF0U) | ((slotType[1] >> 2) & 0x0FU);
\tframe[20] = (frame[20] & 0x03U) | ((slotType[1] << 6) & 0xC0U) | ((slotType[2] >> 2) & 0x3CU);
}
"""
s = s.replace(anchor, header + table + func + anchor, 1)
open(p, "w").write(s)
print("hotspot.c: slot type encoder added (table %d bytes)" % len(table))
+51
Просмотреть файл
@@ -0,0 +1,51 @@
#!/usr/bin/env python3
"""Patch 2 of RX support: hand received data bursts to the MMDVM host."""
W = "/home/viktor/ogd77rel/uv_src/OpenGD77_MDUV380_DM1701_20260130/MDUV380_firmware/application"
p = W + "/source/functions/hotspot.c"
s = open(p).read()
if "sendDataFrameToHost" in s:
print("already patched")
raise SystemExit(0)
# --- 1. MS sourced data sync pattern (frames we originate towards the host) ---
a = "static const uint8_t MS_SOURCED_AUDIO_SYNC[] = { 0x07, 0xF7, 0xD5, 0xDD, 0x57, 0xDF, 0xD0 };\n"
assert a in s, "sync constant anchor missing"
s = s.replace(a, a + "static const uint8_t MS_SOURCED_DATA_SYNC[] = { 0x0D, 0x5D, 0x7F, 0x77, 0xFD, 0x75, 0x70 };\n", 1)
# --- 2. the function itself, placed right before the voice header builder ---
anchor = "static bool sendVoiceHeaderLC_Frame(volatile const uint8_t *receivedDMRDataAndAudio)\n{"
assert anchor in s, "sendVoiceHeaderLC_Frame not found"
func = '''// Rebuild a packet data burst that was received off air and pass it to the MMDVM host.
//
// The HR-C6000 hands us the decoded 96 bit payload (it strips the FEC itself), while the
// host expects a complete 33 byte DMR burst. So we run the payload back through BPTC,
// stamp the data sync pattern and the Slot Type (colour code + data type) into the middle
// of the burst, and send it as a normal DMR data frame.
static void sendDataFrameToHost(volatile const uint8_t *receivedData, uint8_t dataType)
{
\tuint8_t frameData[DMR_FRAME_LENGTH_BYTES + MMDVM_HEADER_LENGTH + 2] = {MMDVM_FRAME_START, (DMR_FRAME_LENGTH_BYTES + MMDVM_HEADER_LENGTH + 2), MMDVM_DMR_DATA2, DMR_SYNC_DATA | dataType};
\tBPTCglobalsInit();
\tBPTCencode((uint8_t *)receivedData, frameData + MMDVM_HEADER_LENGTH);
\t// data sync pattern goes into the middle of the burst
\tfor (uint8_t i = 0; i < 7; i++)
\t{
\t\tframeData[i + EMBEDDED_DATA_OFFSET + MMDVM_HEADER_LENGTH] =
\t\t\t\t(frameData[i + EMBEDDED_DATA_OFFSET + MMDVM_HEADER_LENGTH] & ~SYNC_MASK[i]) | MS_SOURCED_DATA_SYNC[i];
\t}
\tDMRSlotTypeEncode(trxGetDMRColourCode(), dataType, frameData + MMDVM_HEADER_LENGTH);
\tsetRSSIToFrame(frameData);
\tenqueueUSBData(frameData, frameData[1]);
}
'''
s = s.replace(anchor, func + anchor, 1)
open(p, "w").write(s)
print("hotspot.c: sendDataFrameToHost added")
+67
Просмотреть файл
@@ -0,0 +1,67 @@
#!/usr/bin/env python3
"""Patch 3 of RX support: dispatch in the hotspot state machine, and read the
payload out of the HR-C6000 when a data burst arrives off air."""
W = "/home/viktor/ogd77rel/uv_src/OpenGD77_MDUV380_DM1701_20260130/MDUV380_firmware/application"
# ---------------------------------------------------------------- hotspot.c
p = W + "/source/functions/hotspot.c"
s = open(p).read()
if "case HOTSPOT_RX_DATA_FRAME:" in s:
print("hotspot.c: dispatch already present")
else:
a = "\t\t\t\t\t\tcase HOTSPOT_RX_STOP:\n"
assert a in s, "RX_STOP case not found"
case = """\t\t\t\t\t\tcase HOTSPOT_RX_DATA_FRAME:
\t\t\t\t\t\t\t// Packet data burst (SMS, position report...) received off air.
\t\t\t\t\t\t\t// The data type the HR-C6000 reported is stored right after the frame.
\t\t\t\t\t\t\tsendDataFrameToHost(audioAndHotspotDataBuffer.hotspotBuffer[rfFrameBufReadIdx],
\t\t\t\t\t\t\t\t\taudioAndHotspotDataBuffer.hotspotBuffer[rfFrameBufReadIdx][AMBE_AUDIO_LENGTH + LC_DATA_LENGTH + 1]);
\t\t\t\t\t\t\tlastRxState = HOTSPOT_RX_DATA_FRAME;
\t\t\t\t\t\t\trxFrameTime = ticksGetMillis();
\t\t\t\t\t\t\tbreak;
"""
s = s.replace(a, case + a, 1)
open(p, "w").write(s)
print("hotspot.c: RX_DATA_FRAME dispatch added")
# ------------------------------------------------------------- HR-C6000.c
p = W + "/source/hardware/HR-C6000.c"
s = open(p).read()
if "HOTSPOT_RX_DATA_FRAME" in s:
print("HR-C6000.c: already patched")
raise SystemExit(0)
# Hook in right after the terminator handling, which is the existing example of
# "this is a data sync burst" handling in this interrupt.
a = """ if (hrc6000CrcIsValid() && (slotState != DMR_STATE_IDLE) && (hrc.skipCount > 0) && (rxSyncClass != SYNC_CLASS_DATA) && ((rxDataType & 0x07) == 0x01))
{
hrc.skipCount--;
}
"""
assert a in s, "skipCount anchor missing"
block = """ // Packet data bursts (SMS, position reports, CSBK). Stock firmware reads nothing off the
// chip for these, which is why data never reached the host. The HR-C6000 has already
// removed the FEC, so the 96 bit payload can be read from the same register page that
// carries LC data, and handed to the hotspot code to be rebuilt into a full burst.
if ((settingsUsbMode == USB_MODE_HOTSPOT) && hrc6000CrcIsValid() && (rxSyncClass == SYNC_CLASS_DATA) &&
((rxDataType == DT_CSBK) || (rxDataType == DT_DATA_HEADER) || (rxDataType == DT_RATE_12_DATA)) &&
hrc6000CheckColourCodeFilter())
{
if (SPI0ReadPageRegByteArray(0x02, 0x00, (uint8_t *)DMR_frame_buffer, LC_DATA_LENGTH) == kStatus_Success)
{
DMR_frame_buffer[AMBE_AUDIO_LENGTH + LC_DATA_LENGTH] = HOTSPOT_RX_DATA_FRAME;
DMR_frame_buffer[AMBE_AUDIO_LENGTH + LC_DATA_LENGTH + 1] = rxDataType;
hrc.hotspotDMRRxFrameBufferAvailable = true;
}
return;
}
"""
s = s.replace(a, block + a, 1)
open(p, "w").write(s)
print("HR-C6000.c: data burst capture added")
+53
Просмотреть файл
@@ -0,0 +1,53 @@
#!/usr/bin/env python3
"""Show a received SMS on a proper message screen instead of a transient notification.
uiNotificationShow() draws over the current screen, but the normal screen redraw wipes
it again - which is why the text never appeared even though the message was received
and decoded correctly. The message box screen is a real menu: it stays until the user
dismisses it.
"""
W = "/home/viktor/ogd77rel/uv_src/OpenGD77_MDUV380_DM1701_20260130/MDUV380_firmware/application"
p = W + "/source/applicationMain.c"
s = open(p).read()
if "MESSAGEBOX_TYPE_INFO" in s and "smsMarkRead" in s and "MessageBox.message, MESSAGEBOX_MESSAGE_LEN_MAX, \"%u:" in s:
print("already patched")
raise SystemExit(0)
# Replace the notification-based display with a message box screen.
old_start = "\t\tif (smsHasUnread())\n\t\t{\n"
assert old_start in s, "unread block not found"
start = s.index(old_start)
# find the end of that block (the closing brace of the if, at the same indent)
end_marker = "\n\t\t}\n"
end = s.index(end_marker, start) + len(end_marker)
old_block = s[start:end]
assert "smsMarkRead" in old_block, "unexpected block contents: %r" % old_block[:200]
new_block = """ if (smsHasUnread())
{
const smsMessage_t *msg = smsGetLatest();
// Only interrupt the user when they are on a normal operating screen.
int currentMenu = menuSystemGetCurrentMenuNumber();
if ((msg != NULL) && ((currentMenu == UI_VFO_MODE) || (currentMenu == UI_CHANNEL_MODE)))
{
snprintf(uiDataGlobal.MessageBox.message, MESSAGEBOX_MESSAGE_LEN_MAX, "%u\\n%s",
(unsigned int)msg->srcId, msg->text);
uiDataGlobal.MessageBox.type = MESSAGEBOX_TYPE_INFO;
uiDataGlobal.MessageBox.decoration = MESSAGEBOX_DECORATION_FRAME;
uiDataGlobal.MessageBox.buttons = MESSAGEBOX_BUTTONS_OK;
uiDataGlobal.MessageBox.validatorCallback = NULL;
menuSystemPushNewMenu(UI_MESSAGE_BOX);
soundSetMelody(MELODY_ACK_BEEP);
smsMarkRead();
}
}
"""
s = s[:start] + new_block + s[end:]
open(p, "w").write(s)
print("applicationMain.c: incoming SMS now opens a message screen")
+60
Просмотреть файл
@@ -0,0 +1,60 @@
#!/usr/bin/env python3
"""SMS step 1: capture data bursts in normal radio mode too, and route them to an
assembler module. In hotspot mode nothing changes - bursts still go to the host."""
W = "/home/viktor/ogd77rel/uv_src/OpenGD77_MDUV380_DM1701_20260130/MDUV380_firmware/application"
p = W + "/source/hardware/HR-C6000.c"
s = open(p).read()
if "smsRxHandleBurst" in s:
print("HR-C6000.c already routed to the SMS assembler")
else:
old = """ if ((settingsUsbMode == USB_MODE_HOTSPOT) && hrc6000CrcIsValid() && (rxSyncClass == SYNC_CLASS_DATA) &&
((rxDataType == DT_CSBK) || (rxDataType == DT_DATA_HEADER) || (rxDataType == DT_RATE_12_DATA)) &&
hrc6000CheckColourCodeFilter())
{
if (SPI0ReadPageRegByteArray(0x02, 0x00, (uint8_t *)DMR_frame_buffer, LC_DATA_LENGTH) == kStatus_Success)
{
DMR_frame_buffer[AMBE_AUDIO_LENGTH + LC_DATA_LENGTH] = HOTSPOT_RX_DATA_FRAME;
DMR_frame_buffer[AMBE_AUDIO_LENGTH + LC_DATA_LENGTH + 1] = rxDataType;
hrc.hotspotDMRRxFrameBufferAvailable = true;
}
return;
}
"""
assert old in s, "existing capture block not found"
new = """ if (hrc6000CrcIsValid() && (rxSyncClass == SYNC_CLASS_DATA) &&
((rxDataType == DT_CSBK) || (rxDataType == DT_DATA_HEADER) || (rxDataType == DT_RATE_12_DATA)) &&
hrc6000CheckColourCodeFilter())
{
uint8_t dataBurst[LC_DATA_LENGTH];
if (SPI0ReadPageRegByteArray(0x02, 0x00, dataBurst, LC_DATA_LENGTH) == kStatus_Success)
{
if (settingsUsbMode == USB_MODE_HOTSPOT)
{
// Acting as a modem: hand the burst to the host untouched.
memcpy((uint8_t *)DMR_frame_buffer, dataBurst, LC_DATA_LENGTH);
DMR_frame_buffer[AMBE_AUDIO_LENGTH + LC_DATA_LENGTH] = HOTSPOT_RX_DATA_FRAME;
DMR_frame_buffer[AMBE_AUDIO_LENGTH + LC_DATA_LENGTH + 1] = rxDataType;
hrc.hotspotDMRRxFrameBufferAvailable = true;
}
else
{
// Normal radio operation: collect the bursts into a message.
smsRxHandleBurst(dataBurst, rxDataType);
}
}
return;
}
"""
s = s.replace(old, new, 1)
# include the new module
anchor = '#include "functions/hotspot.h"\n'
assert anchor in s, "include anchor missing"
s = s.replace(anchor, anchor + '#include "functions/sms.h"\n', 1)
open(p, "w").write(s)
print("HR-C6000.c: data bursts now captured in normal mode too")
+35
Просмотреть файл
@@ -0,0 +1,35 @@
#!/usr/bin/env python3
"""SMS step 2: run the assembler from the main loop and initialise it at startup."""
W = "/home/viktor/ogd77rel/uv_src/OpenGD77_MDUV380_DM1701_20260130/MDUV380_firmware/application"
p = W + "/source/applicationMain.c"
s = open(p).read()
if "smsTick" in s:
print("already wired into the main loop")
raise SystemExit(0)
# include
anchor = None
for cand in ['#include "functions/voicePrompts.h"\n', '#include "functions/settings.h"\n', '#include "functions/ticks.h"\n']:
if cand in s:
anchor = cand
break
assert anchor, "no suitable include anchor in applicationMain.c"
s = s.replace(anchor, anchor + '#include "functions/sms.h"\n', 1)
# drain the received bursts once per main loop pass, next to the other periodic work
a = "\t\tvoxTick();\n"
assert a in s, "voxTick anchor missing"
s = s.replace(a, a + "\t\tsmsTick();// turn received data bursts into messages\n", 1)
# initialise alongside the other subsystems
init_anchor = None
for cand in ["\tbatteryUpdate();\n", "\tvoxInit();\n"]:
if cand in s:
init_anchor = cand
break
assert init_anchor, "no init anchor found"
s = s.replace(init_anchor, init_anchor + "\tsmsInit();\n", 1)
open(p, "w").write(s)
print("applicationMain.c: smsInit() + smsTick() wired in")
+34
Просмотреть файл
@@ -0,0 +1,34 @@
#!/usr/bin/env python3
"""SMS step 3: show a received message on the screen and beep."""
W = "/home/viktor/ogd77rel/uv_src/OpenGD77_MDUV380_DM1701_20260130/MDUV380_firmware/application"
p = W + "/source/applicationMain.c"
s = open(p).read()
if "smsGetLatest" in s:
print("already showing messages")
raise SystemExit(0)
old = "\t\tsmsTick();// turn received data bursts into messages\n"
assert old in s, "smsTick call not found"
new = """\t\tsmsTick();// turn received data bursts into messages
\t\tif (smsHasUnread())
\t\t{
\t\t\tconst smsMessage_t *msg = smsGetLatest();
\t\t\tif (msg != NULL)
\t\t\t{
\t\t\t\tchar smsLine[SMS_MAX_TEXT_LENGTH + 16];
\t\t\t\tsnprintf(smsLine, sizeof(smsLine), "%u: %s", (unsigned int)msg->srcId, msg->text);
\t\t\t\tuiNotificationShow(NOTIFICATION_TYPE_MESSAGE, NOTIFICATION_ID_MESSAGE, 10000, smsLine, true);
\t\t\t\tsoundSetMelody(MELODY_ACK_BEEP);
\t\t\t\tsmsMarkRead();
\t\t\t}
\t\t}
"""
s = s.replace(old, new, 1)
open(p, "w").write(s)
print("applicationMain.c: incoming SMS is shown and beeps")
+149
Просмотреть файл
@@ -0,0 +1,149 @@
#!/usr/bin/env python3
"""Add Inbox/Outbox storage to the SMS module."""
W = "/home/viktor/ogd77rel/uv_src/OpenGD77_MDUV380_DM1701_20260130/MDUV380_firmware/application"
# ------------------------------------------------------------------ header
p = W + "/include/functions/sms.h"
s = open(p).read()
if "smsInboxCount" in s:
print("sms.h: already has storage API")
else:
s = s.replace(
"// Most recently received message, or NULL when nothing has been received yet.\n",
"""// How many messages each folder keeps. Older ones are dropped when full.
#define SMS_INBOX_SIZE 10
#define SMS_OUTBOX_SIZE 10
// Inbox: messages received off air. Newest first.
uint8_t smsInboxCount(void);
const smsMessage_t *smsInboxGet(uint8_t index);
void smsInboxClear(void);
uint8_t smsInboxUnreadCount(void);
// Outbox: messages we sent. Newest first.
uint8_t smsOutboxCount(void);
const smsMessage_t *smsOutboxGet(uint8_t index);
void smsOutboxClear(void);
void smsOutboxAdd(uint32_t dstId, bool isPrivate, const char *text);
// Most recently received message, or NULL when nothing has been received yet.
""", 1)
open(p, "w").write(s)
print("sms.h: storage API declared")
# ------------------------------------------------------------------ source
p = W + "/source/functions/sms.c"
s = open(p).read()
if "smsInboxCount" in s:
print("sms.c: already has storage")
raise SystemExit(0)
a = "static smsMessage_t latest;\n"
assert a in s
s = s.replace(a, a + """
// Message folders. Index 0 is always the newest; adding shifts the rest down.
static smsMessage_t inbox[SMS_INBOX_SIZE];
static uint8_t inboxCount = 0;
static smsMessage_t outbox[SMS_OUTBOX_SIZE];
static uint8_t outboxCount = 0;
""", 1)
# clear folders on init
s = s.replace("\tmemset(&latest, 0, sizeof(latest));\n",
"\tmemset(&latest, 0, sizeof(latest));\n"
"\tinboxCount = 0;\n"
"\toutboxCount = 0;\n", 1)
# store a completed message into the inbox
a = "\thaveMessage = (t > 0);\n"
assert a in s, "completion point not found"
s = s.replace(a, """ if (t > 0)
{
// Newest first: shift the existing entries down, dropping the oldest if full.
for (int8_t i = (int8_t)((inboxCount < SMS_INBOX_SIZE) ? inboxCount : (SMS_INBOX_SIZE - 1)); i > 0; i--)
{
inbox[i] = inbox[i - 1];
}
inbox[0] = latest;
if (inboxCount < SMS_INBOX_SIZE)
{
inboxCount++;
}
}
""" + a, 1)
s += """
uint8_t smsInboxCount(void)
{
return inboxCount;
}
const smsMessage_t *smsInboxGet(uint8_t index)
{
return ((index < inboxCount) ? &inbox[index] : NULL);
}
void smsInboxClear(void)
{
inboxCount = 0;
unread = false;
haveMessage = false;
}
uint8_t smsInboxUnreadCount(void)
{
uint8_t n = 0;
for (uint8_t i = 0; i < inboxCount; i++)
{
if (inbox[i].unread)
{
n++;
}
}
return n;
}
uint8_t smsOutboxCount(void)
{
return outboxCount;
}
const smsMessage_t *smsOutboxGet(uint8_t index)
{
return ((index < outboxCount) ? &outbox[index] : NULL);
}
void smsOutboxClear(void)
{
outboxCount = 0;
}
void smsOutboxAdd(uint32_t dstId, bool isPrivate, const char *text)
{
for (int8_t i = (int8_t)((outboxCount < SMS_OUTBOX_SIZE) ? outboxCount : (SMS_OUTBOX_SIZE - 1)); i > 0; i--)
{
outbox[i] = outbox[i - 1];
}
memset(&outbox[0], 0, sizeof(smsMessage_t));
outbox[0].dstId = dstId;
outbox[0].isPrivate = isPrivate;
outbox[0].unread = false;
strncpy(outbox[0].text, text, SMS_MAX_TEXT_LENGTH);
outbox[0].text[SMS_MAX_TEXT_LENGTH] = '\\0';
if (outboxCount < SMS_OUTBOX_SIZE)
{
outboxCount++;
}
}
"""
open(p, "w").write(s)
print("sms.c: inbox/outbox storage added")
+194
Просмотреть файл
@@ -0,0 +1,194 @@
#!/usr/bin/env python3
"""Transmit an SMS from the radio itself.
The burst mechanism is the one already proven in hotspot mode: the 12 byte payload goes
to register page 0x02 and reg 0x50 selects the burst type, and the HR-C6000 builds the
FEC and sync around it. What is new here is driving it from normal radio operation,
where the state machine is otherwise built around voice.
A transmission is queued as a list of bursts (preambles, data header, then the rate 1/2
blocks). The timeslot interrupt pulls one burst per active slot until the queue empties,
then ends the transmission.
"""
W = "/home/viktor/ogd77rel/uv_src/OpenGD77_MDUV380_DM1701_20260130/MDUV380_firmware/application"
# --------------------------------------------------------------- sms.h
p = W + "/include/functions/sms.h"
s = open(p).read()
if "smsSendText" in s:
print("sms.h: already declared")
else:
s = s.replace("uint8_t smsGetBlockCount(uint16_t packetLength);", """// Queue a text message for transmission. Returns false if a transmission is already
// in progress or the message does not fit.
bool smsSendText(uint32_t dstId, bool isGroup, const char *text);
// True while bursts are still waiting to go out.
bool smsTxIsActive(void);
// Called from the timeslot interrupt: fetch the next burst to send.
// Returns false when the transmission is finished.
bool smsTxGetNextBurst(uint8_t *payload, uint8_t *dataType);
uint8_t smsGetBlockCount(uint16_t packetLength);""", 1)
open(p, "w").write(s)
print("sms.h: transmit API declared")
# --------------------------------------------------------------- sms.c
p = W + "/source/functions/sms.c"
s = open(p).read()
if "smsSendText" in s:
print("sms.c: already has transmit")
else:
s = s.replace('#include "functions/ticks.h"\n',
'#include "functions/ticks.h"\n#include "functions/trx.h"\n#include "functions/settings.h"\n', 1)
s += """
// ---------------------------------------------------------------- transmit
#define SMS_TX_MAX_BURSTS 24U // 4 preambles + header + up to 19 blocks
#define SMS_PREAMBLE_COUNT 4U
static uint8_t txBursts[SMS_TX_MAX_BURSTS][SMS_BLOCK_LENGTH];
static uint8_t txTypes[SMS_TX_MAX_BURSTS];
static uint8_t txCount = 0;
static uint8_t txIndex = 0;
static uint8_t txSeq = 0x81;
bool smsSendText(uint32_t dstId, bool isGroup, const char *text)
{
uint8_t packet[(SMS_MAX_TEXT_LENGTH * 2) + 64];
uint8_t pad = 0;
uint16_t packetLength;
uint8_t blocks;
uint8_t n = 0;
if (txCount != 0)
{
return false;// a transmission is already queued
}
packetLength = smsBuildPacket(packet, sizeof(packet), trxDMRID, dstId, isGroup, text,
0xA0, txSeq, &pad);// 0xA0: we are not asking for a delivery report yet
if (packetLength == 0)
{
return false;
}
blocks = smsGetBlockCount(packetLength);
if ((uint8_t)(blocks + SMS_PREAMBLE_COUNT + 1U) > SMS_TX_MAX_BURSTS)
{
return false;// message too long for the queue
}
// Preambles first, counting down towards the header, as real radios send them.
for (uint8_t i = 0; i < SMS_PREAMBLE_COUNT; i++)
{
smsBuildPreamble(txBursts[n], trxDMRID, dstId, isGroup, (uint8_t)(SMS_PREAMBLE_COUNT - i));
txTypes[n] = DT_CSBK;
n++;
}
smsBuildDataHeader(txBursts[n], trxDMRID, dstId, isGroup, blocks, pad);
txTypes[n] = DT_DATA_HEADER;
n++;
for (uint8_t i = 0; i < blocks; i++)
{
smsGetBlock(packet, packetLength, i, txBursts[n]);
txTypes[n] = DT_RATE_12_DATA;
n++;
}
txIndex = 0;
txCount = n;
txSeq = (uint8_t)(0x80U | (((txSeq + 1U) & 0x7FU) ? ((txSeq + 1U) & 0x7FU) : 1U));
smsOutboxAdd(dstId, (isGroup == false), text);
// Key the transmitter; the timeslot interrupt will drain the queue.
trxTransmissionEnabled = true;
trxEnableTransmission();
return true;
}
bool smsTxIsActive(void)
{
return (txCount != 0);
}
bool smsTxGetNextBurst(uint8_t *payload, uint8_t *dataType)
{
if ((txCount == 0) || (txIndex >= txCount))
{
txCount = 0;
txIndex = 0;
return false;
}
memcpy(payload, txBursts[txIndex], SMS_BLOCK_LENGTH);
*dataType = txTypes[txIndex];
txIndex++;
return true;
}
"""
open(p, "w").write(s)
print("sms.c: transmit queue added")
# --------------------------------------------------------------- HR-C6000.c
p = W + "/source/hardware/HR-C6000.c"
s = open(p).read()
if "smsTxGetNextBurst" in s:
print("HR-C6000.c: already patched")
else:
# In the ongoing-TX slot, send our data bursts instead of voice when a message is queued.
anchor = """ case DMR_STATE_TX_2: // Ongoing TX (active timeslot)
if (hrc.transmissionEnabled)
{
"""
assert anchor in s, "TX_2 anchor not found"
s = s.replace(anchor, """ case DMR_STATE_TX_2: // Ongoing TX (active timeslot)
if (hrc.transmissionEnabled)
{
// A text message queued by the radio itself takes over the slot: its
// payload goes to the LC page and reg 0x50 picks the burst type, exactly
// as in hotspot mode, while the vocoder path stays idle.
if ((settingsUsbMode != USB_MODE_HOTSPOT) && smsTxIsActive())
{
uint8_t smsPayload[LC_DATA_LENGTH];
uint8_t smsType;
if (smsTxGetNextBurst(smsPayload, &smsType))
{
SPI0WritePageRegByteArray(0x02, 0x00, smsPayload, LC_DATA_LENGTH);
SPI1WritePageRegByteArray(0x03, 0x00, SILENCE_AUDIO, AMBE_AUDIO_LENGTH);
SPI0WritePageRegByte(0x04, 0x41, 0x80);
SPI0WritePageRegByte(0x04, 0x50, (uint8_t)(smsType << 4));
}
else
{
// Queue drained: stop keying and let the normal shutdown run.
hrc.transmissionEnabled = false;
trxTransmissionEnabled = false;
}
slotState = DMR_STATE_TX_1;
break;
}
""", 1)
# And do not announce a voice call in front of the data when we are the sender.
s = s.replace("static inline bool isHotspotDataTx(void)\n{\n\treturn ((settingsUsbMode == USB_MODE_HOTSPOT) && (hrc.txDataType != HOTSPOT_TX_TYPE_VOICE));\n}",
"static inline bool isHotspotDataTx(void)\n{\n"
"\t// Also true while the radio itself is sending a text message, so that the\n"
"\t// Voice LC Header is suppressed in front of the data bursts.\n"
"\treturn (((settingsUsbMode == USB_MODE_HOTSPOT) && (hrc.txDataType != HOTSPOT_TX_TYPE_VOICE))\n"
"\t\t\t|| smsTxIsActive());\n}", 1)
open(p, "w").write(s)
print("HR-C6000.c: radio-originated data transmission wired in")
+63
Просмотреть файл
@@ -0,0 +1,63 @@
#!/usr/bin/env python3
"""Stop emitting voice LC headers in front of a packet data transmission.
TX_START_1/3/5 always sent three Voice LC Headers built from the radio's own DMR ID,
which is correct for a voice call but pollutes the air before an SMS: the receiving
end logs them as "RF user <id> rejected for using TG 0". When the queued frame is a
data burst we send standard Idle data bursts instead - a legitimate filler that
receivers discard silently.
"""
W = "/home/viktor/ogd77rel/uv_src/OpenGD77_MDUV380_DM1701_20260130/MDUV380_firmware/application"
p = W + "/source/hardware/HR-C6000.c"
s = open(p).read()
if "DT_IDLE << 4" in s:
print("already patched")
raise SystemExit(0)
# The three start steps share the same two lines; patch each occurrence.
old_variants = [
# TX_START_1 (preceded by the LED write and header call)
(""" hrc6000SendPcOrTgLCHeader();
SPI0WritePageRegByte(0x04, 0x41, 0x80); // Transmit during next Timeslot
SPI0WritePageRegByte(0x04, 0x50, 0x10); // Set Data Type to 0001 (Voice LC Header), Data, LCSS=00
""",
""" if (hrc.txDataType != HOTSPOT_TX_TYPE_VOICE)
{
// A data burst is queued: don't announce a voice call in front of it.
SPI0WritePageRegByte(0x04, 0x41, 0x80); // Transmit during next Timeslot
SPI0WritePageRegByte(0x04, 0x50, (DT_IDLE << 4)); // Idle data burst as filler
}
else
{
hrc6000SendPcOrTgLCHeader();
SPI0WritePageRegByte(0x04, 0x41, 0x80); // Transmit during next Timeslot
SPI0WritePageRegByte(0x04, 0x50, 0x10); // Set Data Type to 0001 (Voice LC Header), Data, LCSS=00
}
"""),
# TX_START_3 and TX_START_5 (no header call, just the two register writes)
(""" SPI0WritePageRegByte(0x04, 0x41, 0x80); // Transmit during Next Timeslot
SPI0WritePageRegByte(0x04, 0x50, 0x10); // Set Data Type to 0001 (Voice LC Header), Data, LCSS=00
""",
""" SPI0WritePageRegByte(0x04, 0x41, 0x80); // Transmit during Next Timeslot
SPI0WritePageRegByte(0x04, 0x50, ((hrc.txDataType != HOTSPOT_TX_TYPE_VOICE) ? (DT_IDLE << 4) : 0x10));// Voice LC Header, or Idle filler ahead of data
"""),
(""" SPI0WritePageRegByte(0x04, 0x41, 0x80); // Transmit during next Timeslot
SPI0WritePageRegByte(0x04, 0x50, 0x10); // Set Data Type to 0001 (Voice LC Header), Data, LCSS=00
""",
""" SPI0WritePageRegByte(0x04, 0x41, 0x80); // Transmit during next Timeslot
SPI0WritePageRegByte(0x04, 0x50, ((hrc.txDataType != HOTSPOT_TX_TYPE_VOICE) ? (DT_IDLE << 4) : 0x10));// Voice LC Header, or Idle filler ahead of data
"""),
]
count = 0
for old, new in old_variants:
if old in s:
s = s.replace(old, new, 1)
count += 1
if count == 0:
raise SystemExit("no TX_START patterns matched - aborting")
open(p, "w").write(s)
print("patched %d of 3 TX start steps" % count)
+74
Просмотреть файл
@@ -0,0 +1,74 @@
#!/usr/bin/env python3
"""Fix: a stale "this frame is packet data" marker could suppress the Voice LC Header
of an ordinary voice transmission.
hrc.txDataType is only ever set on the hotspot path, and was only cleared after a data
burst was actually sent. If a data transmission was cut short before that, the marker
stayed set - and the next PTT from the radio itself started without a Voice LC Header,
so receivers never recognised the call.
Two guards:
- the start-of-transmission checks now only apply while acting as a hotspot modem;
- the marker is cleared whenever a normal (non-hotspot) transmission begins.
"""
W = "/home/viktor/ogd77rel/uv_src/OpenGD77_MDUV380_DM1701_20260130/MDUV380_firmware/application"
p = W + "/source/hardware/HR-C6000.c"
s = open(p).read()
if "isHotspotDataTx" in s:
print("already patched")
raise SystemExit(0)
# 1. helper right after the struct instance, so all three call sites can use it
anchor = "\t\t.txDataType = HOTSPOT_TX_TYPE_VOICE,\n"
assert anchor in s, "hrc initialiser not found"
# find the end of the initialiser block to place the helper after it
marker = "static void hrc6000SendPcOrTgLCHeader(void);"
assert marker in s, "forward declaration anchor not found"
helper = """// True only when the frame about to be sent is a packet data burst queued by the host.
// Outside hotspot mode there is no such thing, so ordinary voice transmissions are never
// affected by a leftover marker.
static inline bool isHotspotDataTx(void)
{
return ((settingsUsbMode == USB_MODE_HOTSPOT) && (hrc.txDataType != HOTSPOT_TX_TYPE_VOICE));
}
"""
s = s.replace(marker, marker + "\n" + helper, 1)
# 2. use it in the three start-of-transmission checks
a1 = "\t\t\tif (hrc.txDataType != HOTSPOT_TX_TYPE_VOICE)\n\t\t\t{\n\t\t\t\t// A data burst is queued: don't announce a voice call in front of it."
assert a1 in s, "TX_START_1 check not found"
s = s.replace(a1, "\t\t\tif (isHotspotDataTx())\n\t\t\t{\n\t\t\t\t// A data burst is queued: don't announce a voice call in front of it.", 1)
a2 = "((hrc.txDataType != HOTSPOT_TX_TYPE_VOICE) ? (DT_IDLE << 4) : 0x10)"
n2 = s.count(a2)
assert n2 == 2, "expected 2 shared checks, found %d" % n2
s = s.replace(a2, "(isHotspotDataTx() ? (DT_IDLE << 4) : 0x10)")
# 3. and in the ongoing-TX branch, so a stale marker cannot divert voice frames either
a3 = "\t\t\t\t\tif (hrc.txDataType != HOTSPOT_TX_TYPE_VOICE)\n"
assert a3 in s, "TX_2 data branch not found"
s = s.replace(a3, "\t\t\t\t\tif (isHotspotDataTx())\n", 1)
# 4. belt and braces: clear the marker when a non-hotspot transmission starts
a4 = """ if (settingsUsbMode != USB_MODE_HOTSPOT)
{
codecInit(false);
}
else
{
hrc.hotspotPostponedFrameHandling = (HS_NUM_OF_SILENCE_SEQ_ON_STARTUP * 6);"""
assert a4 in s, "TX start branch not found"
s = s.replace(a4, """ if (settingsUsbMode != USB_MODE_HOTSPOT)
{
hrc.txDataType = HOTSPOT_TX_TYPE_VOICE;// plain voice call: never a data burst
codecInit(false);
}
else
{
hrc.hotspotPostponedFrameHandling = (HS_NUM_OF_SILENCE_SEQ_ON_STARTUP * 6);""", 1)
open(p, "w").write(s)
print("HR-C6000.c: voice transmissions can no longer be suppressed by a stale data marker")
+74
Просмотреть файл
@@ -0,0 +1,74 @@
#!/usr/bin/env python3
"""Hook sending into the interface: a Reply button on an incoming message, and a
working New msg entry.
For this first step the text is a fixed quick reply, so the whole transmit path can be
exercised on air before the keypad editor is written.
"""
W = "/home/viktor/ogd77rel/uv_src/OpenGD77_MDUV380_DM1701_20260130/MDUV380_firmware/application"
# ---- Reply button on the incoming message popup -------------------------
p = W + "/source/applicationMain.c"
s = open(p).read()
if "smsReplyValidator" in s:
print("applicationMain.c: reply already wired")
else:
# remember who to answer, and send the quick reply when the user confirms
anchor = "\t\tif (smsHasUnread())\n"
assert anchor in s, "unread block not found"
helper = """static uint32_t smsReplyToId = 0;
static bool smsReplyIsPrivate = true;
// Called when the user confirms the Reply prompt on an incoming message.
static bool smsReplyValidator(void)
{
smsSendText(smsReplyToId, (smsReplyIsPrivate == false), "OK");
return true;
}
"""
# place the helper just before the function that contains the unread block
fn_start = s.rindex("\nvoid ", 0, s.index(anchor))
s = s[:fn_start + 1] + helper + s[fn_start + 1:]
old_show = """ uiDataGlobal.MessageBox.buttons = MESSAGEBOX_BUTTONS_OK;
uiDataGlobal.MessageBox.validatorCallback = NULL;"""
assert old_show in s, "message box setup not found"
s = s.replace(old_show, """ // Yes replies to the sender, No just dismisses the message.
uiDataGlobal.MessageBox.buttons = MESSAGEBOX_BUTTONS_YESNO;
uiDataGlobal.MessageBox.validatorCallback = smsReplyValidator;
smsReplyToId = msg->srcId;
smsReplyIsPrivate = msg->isPrivate;""", 1)
open(p, "w").write(s)
print("applicationMain.c: Reply wired into the incoming message popup")
# ---- New msg actually sends --------------------------------------------
p = W + "/source/user_interface/menuMessages.c"
s = open(p).read()
if "smsSendText" in s:
print("menuMessages.c: already sends")
else:
old = """ case MESSAGES_ITEM_NEW:
// Sending is not implemented yet; say so rather than opening a dead screen.
uiNotificationShow(NOTIFICATION_TYPE_MESSAGE, NOTIFICATION_ID_MESSAGE, 2000, "Not yet", true);
break;"""
assert old in s, "New msg handler not found"
s = s.replace(old, """ case MESSAGES_ITEM_NEW:
// First step: send a fixed text to the current talkgroup, so the transmit
// path can be exercised on air. The keypad editor comes next.
if (smsSendText((trxTalkGroupOrPcId & 0x00FFFFFF), ((trxTalkGroupOrPcId >> 24) == TG_CALL_FLAG), "TEST FROM RADIO"))
{
uiNotificationShow(NOTIFICATION_TYPE_MESSAGE, NOTIFICATION_ID_MESSAGE, 2000, "Sending...", true);
}
else
{
uiNotificationShow(NOTIFICATION_TYPE_MESSAGE, NOTIFICATION_ID_MESSAGE, 2000, "Busy", true);
}
break;""", 1)
s = s.replace('#include "functions/sound.h"',
'#include "functions/sound.h"\n#include "functions/trx.h"\n#include "user_interface/uiUtilities.h"', 1)
open(p, "w").write(s)
print("menuMessages.c: New msg now transmits")
+56
Просмотреть файл
@@ -0,0 +1,56 @@
#!/usr/bin/env python3
"""Leave modem mode when the USB host goes away, instead of after a fixed timeout.
Stock firmware dropped out of hotspot mode if the host was quiet for 20 seconds, which
made the radio unusable as a permanently attached modem - so that timeout was disabled
on request. The side effect was that the radio then stayed a modem forever and had to be
power cycled by hand after every test.
This ties the exit to the actual USB connection state instead: while the host is present
the radio stays a modem no matter how quiet it is, and as soon as the cable is pulled (or
the host stops enumerating it) the radio returns to normal operation by itself.
"""
W = "/home/viktor/ogd77rel/uv_src/OpenGD77_MDUV380_DM1701_20260130/MDUV380_firmware/application"
p = W + "/source/functions/hotspot.c"
s = open(p).read()
if "usbHostIsAttached" in s:
print("already patched")
raise SystemExit(0)
# a small helper next to the other statics
anchor = "static const uint32_t MMDVMHOST_TIMEOUT = MMDVMHOST_TIMEOUT_DISABLED;\n"
assert anchor in s, "timeout constant not found"
helper = anchor + """
// The USB device handle tells us whether a host is actually enumerating us. Losing that
// is what "the computer went away" really means; host silence alone is not.
extern USBD_HandleTypeDef hUsbDeviceFS;
static bool usbHostIsAttached(void)
{
return (hUsbDeviceFS.dev_state == USBD_STATE_CONFIGURED);
}
"""
s = s.replace(anchor, helper, 1)
# make sure the USB device header is included
if '#include "usb_device.h"' not in s:
inc_anchor = '#include "functions/ticks.h"\n'
assert inc_anchor in s, "include anchor missing"
s = s.replace(inc_anchor, inc_anchor + '#include "usb_device.h"\n', 1)
# both inactivity checks now also fire when the host has disconnected
old = """ if ((MMDVMHOST_TIMEOUT != 0) && (nonVolatileSettings.hotspotType == HOTSPOT_TYPE_MMDVM) &&
((ticksGetMillis() - mmdvmHostLastActiveTime) > MMDVMHOST_TIMEOUT))
"""
new = """ if ((nonVolatileSettings.hotspotType == HOTSPOT_TYPE_MMDVM) &&
((usbHostIsAttached() == false) ||
((MMDVMHOST_TIMEOUT != 0) && ((ticksGetMillis() - mmdvmHostLastActiveTime) > MMDVMHOST_TIMEOUT))))
"""
count = s.count(old)
assert count == 2, "expected 2 inactivity checks, found %d" % count
s = s.replace(old, new)
open(p, "w").write(s)
print("hotspot.c: modem mode now ends when the USB host disconnects (%d checks)" % count)
+112
Просмотреть файл
@@ -0,0 +1,112 @@
#!/usr/bin/env python3
"""Listen on the radio's MMDVM port and dump every received DMR burst verbatim.
Prints the MMDVM control byte and the raw 33-byte burst as hex, so the frames can be
decoded and compared byte for byte elsewhere. Read-only towards the air: the only thing
sent to the radio is a periodic GET_STATUS, which is required to put/keep it in modem
mode (the firmware leaves modem mode if the host goes quiet, and only enters it when it
sees an MMDVM frame).
"""
import os, sys, termios, time, argparse
MMDVM_FRAME_START = 0xE0
MMDVM_GET_STATUS = 0x01
MMDVM_DMR_DATA1 = 0x18
MMDVM_DMR_DATA2 = 0x1A
DATA_TYPES = {
0x0: "PI header", 0x1: "voice LC header", 0x2: "terminator with LC",
0x3: "CSBK", 0x4: "MBC header", 0x5: "MBC continuation",
0x6: "data header", 0x7: "rate 1/2 data", 0x8: "rate 3/4 data",
0x9: "idle", 0xA: "rate 1 data",
}
def open_port(path):
fd = os.open(path, os.O_RDWR | os.O_NOCTTY | os.O_NONBLOCK)
a = termios.tcgetattr(fd)
a[0] = 0
a[1] = 0
a[2] = termios.CS8 | termios.CREAD | termios.CLOCAL
a[3] = 0
a[6][termios.VMIN] = 0
a[6][termios.VTIME] = 0
termios.tcsetattr(fd, termios.TCSANOW, a)
termios.tcflush(fd, termios.TCIOFLUSH)
return fd
def main():
ap = argparse.ArgumentParser()
ap.add_argument("--port", default="/dev/ttyACM0")
ap.add_argument("--secs", type=int, default=600)
args = ap.parse_args()
fd = open_port(args.port)
buf = b""
deadline = time.time() + args.secs
next_poll = 0.0
counts = {}
total = 0
print("dumping raw DMR bursts for %d s (GET_STATUS keepalive every 3 s)" % args.secs, flush=True)
try:
while time.time() < deadline:
now = time.time()
if now >= next_poll:
# keepalive: also what puts the radio into modem mode in the first place
try:
os.write(fd, bytes([MMDVM_FRAME_START, 3, MMDVM_GET_STATUS]))
except OSError:
pass
next_poll = now + 3.0
try:
chunk = os.read(fd, 4096)
except (BlockingIOError, OSError):
chunk = b""
if chunk:
buf += chunk
else:
time.sleep(0.002)
# parse MMDVM frames: E0 <len> <type> <payload...>
while len(buf) >= 3:
if buf[0] != MMDVM_FRAME_START:
buf = buf[1:]
continue
length = buf[1]
if length < 3:
buf = buf[1:]
continue
if len(buf) < length:
break
frame, buf = buf[:length], buf[length:]
ftype = frame[2]
if ftype in (MMDVM_DMR_DATA1, MMDVM_DMR_DATA2):
total += 1
control = frame[3] if len(frame) > 3 else 0
burst = frame[4:37]
slot = 1 if ftype == MMDVM_DMR_DATA1 else 2
if control & 0x40: # data sync burst
dt = control & 0x0F
label = "data/%s" % DATA_TYPES.get(dt, "type %d" % dt)
elif control & 0x20:
label = "voice sync"
else:
label = "voice seq %d" % (control & 0x0F)
counts[label] = counts.get(label, 0) + 1
print("%s slot%d ctrl=%02X %-28s %s" % (
time.strftime("%H:%M:%S", time.gmtime()), slot, control, label, burst.hex()),
flush=True)
finally:
os.close(fd)
print("\n--- summary: %d DMR bursts ---" % total, flush=True)
for k in sorted(counts, key=lambda x: -counts[x]):
print(" %-28s %d" % (k, counts[k]), flush=True)
if __name__ == "__main__":
main()
+115
Просмотреть файл
@@ -0,0 +1,115 @@
#!/usr/bin/env python3
"""Grab the radio's screen over USB and save it as a PNG.
Uses the CPS read command (area 6 = display buffer), which the firmware answers only
while the radio is NOT in modem mode - and, importantly, such requests do not switch it
into modem mode either. So the screen can be watched while the radio does its normal job.
Display is 160x128, RGB565, 2 bytes per pixel.
"""
import os, sys, termios, time, zlib, struct, argparse
WIDTH, HEIGHT = 160, 128
CPS_ACCESS_DISPLAY_BUFFER = 6
CHUNK = 1024
def open_port(path):
fd = os.open(path, os.O_RDWR | os.O_NOCTTY)
a = termios.tcgetattr(fd)
a[0] = 0
a[1] = 0
a[2] = termios.CS8 | termios.CREAD | termios.CLOCAL
a[3] = 0
a[6][termios.VMIN] = 0
a[6][termios.VTIME] = 20
termios.tcsetattr(fd, termios.TCSANOW, a)
termios.tcflush(fd, termios.TCIOFLUSH)
return fd
def read_exact(fd, n, timeout=3.0):
buf = b""
deadline = time.time() + timeout
while len(buf) < n and time.time() < deadline:
try:
part = os.read(fd, n - len(buf))
except BlockingIOError:
part = b""
if part:
buf += part
else:
time.sleep(0.003)
return buf
def read_block(fd, area, addr, length):
req = bytes([ord('R'), area,
(addr >> 24) & 0xFF, (addr >> 16) & 0xFF, (addr >> 8) & 0xFF, addr & 0xFF,
(length >> 8) & 0xFF, length & 0xFF])
termios.tcflush(fd, termios.TCIFLUSH)
os.write(fd, req)
head = read_exact(fd, 3)
if len(head) < 3 or head[0:1] != b'R':
return None
got = (head[1] << 8) | head[2]
data = read_exact(fd, got)
return data if len(data) == got else None
def write_png(path, rgb_rows):
"""Minimal PNG writer (stdlib only)."""
raw = b"".join(b"\x00" + row for row in rgb_rows)
def chunk(tag, data):
c = struct.pack(">I", len(data)) + tag + data
return c + struct.pack(">I", zlib.crc32(tag + data) & 0xFFFFFFFF)
png = b"\x89PNG\r\n\x1a\n"
png += chunk(b"IHDR", struct.pack(">IIBBBBB", WIDTH, HEIGHT, 8, 2, 0, 0, 0))
png += chunk(b"IDAT", zlib.compress(raw, 6))
png += chunk(b"IEND", b"")
open(path, "wb").write(png)
def main():
ap = argparse.ArgumentParser()
ap.add_argument("--port", default="/dev/ttyACM0")
ap.add_argument("--out", default="/home/viktor/screen.png")
args = ap.parse_args()
fd = open_port(args.port)
try:
total = WIDTH * HEIGHT * 2
data = b""
while len(data) < total:
n = min(CHUNK, total - len(data))
part = read_block(fd, CPS_ACCESS_DISPLAY_BUFFER, len(data), n)
if part is None:
print("read failed at offset %d - is the radio in modem mode?" % len(data))
return 2
data += part
finally:
os.close(fd)
rows = []
for y in range(HEIGHT):
row = bytearray()
for x in range(WIDTH):
i = (y * WIDTH + x) * 2
# RGB565, little endian as stored by the display driver
v = data[i] | (data[i + 1] << 8)
r = ((v >> 11) & 0x1F) << 3
g = ((v >> 5) & 0x3F) << 2
b = (v & 0x1F) << 3
row += bytes((r, g, b))
rows.append(bytes(row))
write_png(args.out, rows)
nonblack = sum(1 for i in range(0, len(data), 2) if data[i] or data[i + 1])
print("saved %s (%d of %d pixels non-black)" % (args.out, nonblack, WIDTH * HEIGHT))
return 0
if __name__ == "__main__":
sys.exit(main())
+117
Просмотреть файл
@@ -0,0 +1,117 @@
#!/usr/bin/env python3
"""Reference implementation of outgoing SMS packet assembly, mirrored from the spec.
Written on the PC first so the tricky parts (CRC-32 with its byte-pair swap, the IP and
UDP checksums, the block accounting) can be checked against a known-good packet before
any of it goes into firmware. The firmware version will be a literal translation of this.
"""
def crc32_dmr(data: bytes) -> bytes:
"""ETSI/MOTOTRBO CRC-32: swap bytes within each 16-bit pair, then MSB-first
CRC32 (poly 0x04C11DB7, init 0, no final xor). Returned little endian."""
swapped = bytearray(data)
for i in range(0, len(swapped) - 1, 2):
swapped[i], swapped[i + 1] = swapped[i + 1], swapped[i]
crc = 0
for b in swapped:
crc ^= b << 24
for _ in range(8):
crc = ((crc << 1) ^ 0x04C11DB7) & 0xFFFFFFFF if (crc & 0x80000000) else ((crc << 1) & 0xFFFFFFFF)
return crc.to_bytes(4, "little")
def ones_complement(data: bytes) -> int:
if len(data) % 2:
data += b"\x00"
total = 0
for i in range(0, len(data), 2):
total += (data[i] << 8) | data[i + 1]
total = (total & 0xFFFF) + (total >> 16)
return (~total) & 0xFFFF
def crc16_ccitt(data: bytes) -> int:
crc = 0x0000
for b in data:
crc ^= b << 8
for _ in range(8):
crc = ((crc << 1) ^ 0x1021) & 0xFFFF if (crc & 0x8000) else ((crc << 1) & 0xFFFF)
return crc
def build_tms(text: str, flag: int = 0xA0, seq: int = 0x81) -> bytes:
"""[len 2B BE][flag][00][seq][04 0D 00 0A 00][text UTF-16LE]"""
body = bytes([flag, 0x00, seq]) + bytes([0x04, 0x0D, 0x00, 0x0A, 0x00]) + text.encode("utf-16-le")
tms_len = 8 + 2 * len(text)
assert tms_len == len(body), "length mismatch: %d vs %d" % (tms_len, len(body))
return tms_len.to_bytes(2, "big") + body
def build_packet(src_id: int, dst_id: int, is_group: bool, text: str,
flag: int = 0xA0, seq: int = 0x81, ip_id: int = 0x0001) -> bytes:
tms = build_tms(text, flag, seq)
udp_len = 8 + len(tms)
src_ip = (0x0C << 24) | src_id
dst_ip = ((0xE1 if is_group else 0x0C) << 24) | dst_id
ttl = 0x01 if is_group else 0x40
udp = bytearray()
udp += (0x0FA7).to_bytes(2, "big") # source port 4007
udp += (0x0FA7).to_bytes(2, "big") # destination port 4007
udp += udp_len.to_bytes(2, "big")
udp += b"\x00\x00" # checksum placeholder
udp += tms
pseudo = src_ip.to_bytes(4, "big") + dst_ip.to_bytes(4, "big") + b"\x00\x11" + udp_len.to_bytes(2, "big")
udp_csum = ones_complement(pseudo + bytes(udp))
udp[6:8] = udp_csum.to_bytes(2, "big")
ip_len = 20 + udp_len
ip = bytearray()
ip += b"\x45\x00"
ip += ip_len.to_bytes(2, "big")
ip += ip_id.to_bytes(2, "big")
ip += b"\x00\x00"
ip += bytes([ttl, 0x11])
ip += b"\x00\x00" # header checksum placeholder
ip += src_ip.to_bytes(4, "big")
ip += dst_ip.to_bytes(4, "big")
ip[10:12] = ones_complement(bytes(ip)).to_bytes(2, "big")
packet = bytes(ip) + bytes(udp)
# pad to a multiple of 12, then append the CRC-32 as its own tail
pad = (-len(packet)) % 12
packet += b"\x00" * pad
packet += crc32_dmr(packet)
return packet, pad
def build_data_header(src_id: int, dst_id: int, is_group: bool, blocks: int, pad: int) -> bytes:
h = bytearray(12)
h[0] = (0x80 if is_group else 0x00) | 0x02 # DPF=2 unconfirmed
h[1] = 0x40 | (pad & 0x0F) # SAP=4 (IP), pad octets
h[2:5] = dst_id.to_bytes(3, "big")
h[5:8] = src_id.to_bytes(3, "big")
h[8] = 0x80 | (blocks & 0x7F)
h[9] = 0x10
crc = crc16_ccitt(bytes(h[0:10])) ^ 0xCCCC
h[10] = (crc >> 8) & 0xFF
h[11] = crc & 0xFF
return bytes(h)
if __name__ == "__main__":
text = "PRIVET"
packet, pad = build_packet(777777, 6, True, text)
blocks = len(packet) // 12
hdr = build_data_header(777777, 6, True, blocks, pad)
print("text : %r" % text)
print("packet len : %d bytes, pad %d, blocks %d" % (len(packet), pad, blocks))
print("packet hex : %s" % packet.hex(" "))
print("last 4 bytes: %s (reference ends with 92 ba 1c 62)" % packet[-4:].hex(" "))
print("data header : %s" % hdr.hex(" "))