DMR-данные (SMS) на TYT MD-UV380 под OpenGD77
Восстановлено из транскрипта сессии после потери рабочего дерева на сервере сборки. Что внутри: - четыре новых модуля прошивки (приём, сборка исходящего, меню Messages); - правки существующих файлов в виде скриптов, с порядком применения; - инструменты стенда: снятие экрана рации по USB, дамп кадров, эталонная сборка пакета; - NOTES.md: что проверено в эфире, что не доделано, где грабли. Работало в эфире в августе 2026: данные ходят в обе стороны в режиме хотспота, рация принимает SMS и показывает на экране. Отправка собрана и сверена с эталоном сервера побайтово, но в эфире не проверялась. Claude-Session: https://claude.ai/code/session_01GJtvG5WwqYNe1b7s9LLtgy
Этот коммит содержится в:
@@ -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 прямо запрещает коммерческое использование и
|
||||
включение в проекты, допускающие коммерческое использование). Эти правки —
|
||||
производная работа и наследуют те же условия.
|
||||
@@ -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 прямо запрещает коммерческое использование и включение в проекты, которые
|
||||
такое использование допускают. Эти правки - производная работа с теми же условиями.
|
||||
@@ -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 эталонный приёмник.
|
||||
@@ -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]]
|
||||
@@ -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]]
|
||||
@@ -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]]
|
||||
@@ -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
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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
|
||||
```
|
||||
|
||||
@@ -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")
|
||||
@@ -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")
|
||||
@@ -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")
|
||||
@@ -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)
|
||||
@@ -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))
|
||||
@@ -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")
|
||||
@@ -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")
|
||||
@@ -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")
|
||||
@@ -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")
|
||||
@@ -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")
|
||||
@@ -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")
|
||||
@@ -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")
|
||||
@@ -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")
|
||||
@@ -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)
|
||||
@@ -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")
|
||||
@@ -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")
|
||||
@@ -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)
|
||||
@@ -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()
|
||||
@@ -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())
|
||||
@@ -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(" "))
|
||||
Ссылка в новой задаче
Block a user