DMR-чип опознан по схеме: HF6853 = AUCTUS A6 (RDA8809), не SCT3258
Опознание БЕЗ разбора рации, по схеме RT4DDLT01 (Radtel v2.1): DMR-модуль «FM100B» = один чип U700 HF6853 — одночиповый DMR-SoC семейства Auctus A6 (внутри RDA8809): CPU + ВЧ-трансивер + DSP + AMBE-вокодер + аудио-кодек в одном кристалле, прошивка во внешнем SPI-flash, кварц 26 МГц. Прежняя версия «Sicomm SCT3258TD» неверна (SCT3258 — baseband-only без ВЧ; и нативный протокол другой). Скрытый ATE/CPS-интерфейс полностью реверснут (jhart99/a6tools, вендорен в research/a6tools). Даёт КОНКРЕТНУЮ починку нашей TX-проблемы: AT+GETFREQERR / AT+DMOFREQERR=N, offset=-2500+10*N, ±2500 Гц — наши ~1100 Гц внутри диапазона. Плюс DMR_ADJTXSYMDEV (девиация), FGU_AFC, DMOSETPOWER, CPS chanInfo, дамп прошивки модуля. - docs/hf6853-auctus-a6.md — идентификация + протокол + план TX-fix - tools/a6_freqfix.py — заготовка тула (probe/read/write, не запускать вслепую) - research/a6tools/ — вендоренный реверс (MIT), atcommands.md/cpecommands.md - docs/dmr-tx-recovery.md, README — путь 0 (AT+DMOFREQERR) как лучший - research/rt4ddlt01-dmr-module-sheet.png — рендер листа схемы DMR-модуля Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01NikMGoqQWWk9wy2ww2vJAr
Этот коммит содержится в:
+1
-1
@@ -18,7 +18,7 @@
|
|||||||
- ✅ **Прошивка разложена на запчасти:** два процессора + внешний SPI = три независимых канала внедрения. Карта — [`docs/firmware-parts.md`](docs/firmware-parts.md).
|
- ✅ **Прошивка разложена на запчасти:** два процессора + внешний SPI = три независимых канала внедрения. Карта — [`docs/firmware-parts.md`](docs/firmware-parts.md).
|
||||||
- ✅ **Найден штатный загрузчик ресурсов** (`Unicode Download.exe`, внутри `FontVoicePicture`) — официальный путь замены **шрифтов, голосовых подсказок и картинок** (в т.ч. загрузочного логотипа) без патча MCU.
|
- ✅ **Найден штатный загрузчик ресурсов** (`Unicode Download.exe`, внутри `FontVoicePicture`) — официальный путь замены **шрифтов, голосовых подсказок и картинок** (в т.ч. загрузочного логотипа) без патча MCU.
|
||||||
- ✅ **Разобран спектроанализатор REFV DualTachyon** — та самая реализация, которую хвалят пользователи. См. [`docs/refw-spectrum.md`](docs/refw-spectrum.md).
|
- ✅ **Разобран спектроанализатор REFV DualTachyon** — та самая реализация, которую хвалят пользователи. См. [`docs/refw-spectrum.md`](docs/refw-spectrum.md).
|
||||||
- 🎯 **DMR-чип опознан: FM100B = Sicomm SCT3258TD** — документированный baseband. Датащит и Packet Interface (126 стр) в [`docs/SCT3258_packet_interface.txt`](docs/SCT3258_packet_interface.txt). Открывает калибровку/AMBE/вокодер по спеке. Родня: UV-4R, китайские клоны (та же связка SCT3258+AT32).
|
- 🎯 **DMR-чип опознан по схеме: FM100B = HF6853 / AUCTUS A6 (внутри RDA8809)** — одночиповый DMR-SoC (CPU+ВЧ-трансивер+DSP+AMBE-вокодер+кодек), прошивка во внешнем SPI-flash. См. [`docs/hf6853-auctus-a6.md`](docs/hf6853-auctus-a6.md). Скрытый ATE-интерфейс полностью реверснут ([`research/a6tools`](research/a6tools)) — **чинит частотную калибровку TX** (`AT+DMOFREQERR`) без Radtel. Родня: COTRE CO01D, GOCOM GD900, UV-4R. *(Прежняя версия «SCT3258» — ошибка по строкам вокодера; SCT3258 это baseband-only, HF6853 содержит ВЧ-тракт сам.)*
|
||||||
- ✅ **Приём Talker Alias** — позывной вызывающего крупно на экране (cmd `0x60`). Сборка [`tools/build_ta.py`](tools/build_ta.py).
|
- ✅ **Приём Talker Alias** — позывной вызывающего крупно на экране (cmd `0x60`). Сборка [`tools/build_ta.py`](tools/build_ta.py).
|
||||||
- ✅ **Интерактивный агент** — управление FM100B через COM без перешивки ([`tools/build_agent.py`](tools/build_agent.py) + [`tools/agent.py`](tools/agent.py)); снят полный [список команд FM100B](docs/fm100b-commands.md).
|
- ✅ **Интерактивный агент** — управление FM100B через COM без перешивки ([`tools/build_agent.py`](tools/build_agent.py) + [`tools/agent.py`](tools/agent.py)); снят полный [список команд FM100B](docs/fm100b-commands.md).
|
||||||
- ✅ **Работа с SPI** — чтение/запись/восстановление codeplug и калибровки из дампа ([`tools/spi_read.py`](tools/spi_read.py), [`tools/spi_wr.py`](tools/spi_wr.py), [`tools/spi_restore.py`](tools/spi_restore.py)).
|
- ✅ **Работа с SPI** — чтение/запись/восстановление codeplug и калибровки из дампа ([`tools/spi_read.py`](tools/spi_read.py), [`tools/spi_wr.py`](tools/spi_wr.py), [`tools/spi_restore.py`](tools/spi_restore.py)).
|
||||||
|
|||||||
@@ -30,6 +30,14 @@ Software «Write» залил ДЕФОЛТНЫЕ** значения смещен
|
|||||||
|
|
||||||
## Пути восстановления (по приоритету)
|
## Пути восстановления (по приоритету)
|
||||||
|
|
||||||
|
0. **⭐ AT+DMOFREQERR (скрытый ATE-интерфейс чипа) — ЛУЧШИЙ.** Опознано (24.07.2026):
|
||||||
|
DMR-чип = **HF6853 / AUCTUS A6 (RDA8809)**, не SCT3258 (см. `docs/hf6853-auctus-a6.md`).
|
||||||
|
У него реверснутый ATE-интерфейс с частотной калибровкой TCXO:
|
||||||
|
`AT+GETFREQERR` / `AT+DMOFREQERR=<N>`, где `offset[Гц] = -2500 + 10*N`, N=0..500, ±2500 Гц.
|
||||||
|
Наше смещение ~1100 Гц — внутри диапазона, чинится одной записью. После записи сменить
|
||||||
|
канал. Инструмент: `tools/a6_freqfix.py` (probe/read/write, требует SDR-измерения TX).
|
||||||
|
Не требует Radtel `.updat` и Radio Tuning Software. Библиотека — `research/a6tools`.
|
||||||
|
|
||||||
1. **Компенсация на хотспоте (RXOffset)** — БЕЗ прибора. Если TX рации смещена на постоянный
|
1. **Компенсация на хотспоте (RXOffset)** — БЕЗ прибора. Если TX рации смещена на постоянный
|
||||||
X Гц, выставить на хотспоте `RXOffset` так, чтобы приёмник поймал смещённую передачу.
|
X Гц, выставить на хотспоте `RXOffset` так, чтобы приёмник поймал смещённую передачу.
|
||||||
Перебор RXOffset в MMDVM.ini (±1000 Гц шагом ~100), рестарт MMDVMHost, при передаче рации
|
Перебор RXOffset в MMDVM.ini (±1000 Гц шагом ~100), рестарт MMDVMHost, при передаче рации
|
||||||
|
|||||||
@@ -0,0 +1,104 @@
|
|||||||
|
# RT-4D DMR-модуль «FM100B» = чип **HF6853** (семейство AUCTUS A6 / RDA8809)
|
||||||
|
|
||||||
|
**Дата: 24.07.2026. Опознание БЕЗ разбора рации — по схеме `RT4DDLT01` (Radtel, v2.1, 2024-06-04).**
|
||||||
|
|
||||||
|
## Что на схеме
|
||||||
|
|
||||||
|
Лист `RT4DDLT01` («DMR daughterboard») содержит РОВНО ОДИН основной чип:
|
||||||
|
|
||||||
|
- **U700 — маркировка `HF6853`**, 41-выводный корпус.
|
||||||
|
- Обвязка: кварц **X700 = 26 МГц (2520)**, LDO **U702 = XC6204E332MR (3.3 В)**,
|
||||||
|
аудио-ОУ **U701**, аналоговый ключ **Q700** (DIG/ANA AF-SW), пассив.
|
||||||
|
- Внешний **SPI-flash** к чипу (выводы `F_SO / F_SI / F_SCK / F_CSN`, они же
|
||||||
|
`MCLK/IO2`, `TCK/IO3`, `IO0`, `IO1`) — там лежит прошивка модуля (`FM100B_V1.2.0.13`).
|
||||||
|
- UART к хосту (AT32): `P28/RX_EN` / `P25/TX_EN` → **DIG-TXD / DIG-RXD** на разъёме.
|
||||||
|
|
||||||
|
### HF6853 — это ВСЁ-в-одном (не только baseband!)
|
||||||
|
|
||||||
|
Выводы U700 однозначно показывают интеграцию всего радиотракта в один кристалл:
|
||||||
|
|
||||||
|
| Группа выводов | Назначение |
|
||||||
|
|---|---|
|
||||||
|
| `VCCRXFE, ANT, VCCPA, VCCPAD, VCCIF, VSYS` | ВЧ-приёмник (RX front-end), PA, ПЧ — **интегрированный трансивер** |
|
||||||
|
| `MICBIAS, MICLP, MICLN, LINEL, LINER` | вход микрофона/линии — **аудио-кодек** |
|
||||||
|
| `AUDLP, AUDLN, VCOM, VDDAUD, GNDAUD` | выход аудио — **ЦАП/кодек** |
|
||||||
|
| `F_SO/F_SI/F_SCK/F_CSN` | внешний SPI-flash с прошивкой |
|
||||||
|
| `P28/RX, P25/TX, P0/TXD2, P12/CTS, P13/RTS` | UART’ы к хосту |
|
||||||
|
| `XI/XO (26 МГц), CLK_26M/PWM0` | опорный генератор |
|
||||||
|
|
||||||
|
То есть DSP + AMBE-вокодер + ВЧ-трансивер + аудио-кодек + CPU — **в одном чипе HF6853**.
|
||||||
|
|
||||||
|
## Идентификация: семейство AUCTUS A6
|
||||||
|
|
||||||
|
Архитектура «CPU + ВЧ-трансивер + DSP + AMBE-вокодер + кодек в одном кристалле,
|
||||||
|
прошивка во внешнем SPI-flash, 26 МГц» — это **AUCTUS A6** ( shenzhen Auctus).
|
||||||
|
Реверс jhart99 показал: **A6 внутри = RDA8809** (дериватив RDA8955, 2G-GSM SoC,
|
||||||
|
перепрофилированный под рацию). Auctus также владеет правами на AT1846.
|
||||||
|
|
||||||
|
Тот же чип/модуль стоит в: COTRE CO01D/CO04D/CO06D, GOCOM GD900/GD100/GD700/GD800,
|
||||||
|
CONNECOM. Прошивочные строки RT-4D (`Vocoder/WebRTC/codec adc/dac/mic`,
|
||||||
|
`F312/F315/DP275/FM100/PM100`) — это общая прошивка Auctus/iRadio/Radtel DMR-модуля.
|
||||||
|
|
||||||
|
> **Коррекция прежней версии:** ранее модуль был опознан как *Sicomm SCT3258TD* по
|
||||||
|
> строкам вокодера. Это неверно: SCT3258 — только baseband (нужен внешний ВЧ SCT3700),
|
||||||
|
> а HF6853 содержит ВЧ-тракт сам. Нативный протокол тоже другой (не `84 A9 61`, а
|
||||||
|
> RDA-debug `AD 00 …` + CPS `aa … bb`, см. ниже). Актуальная идентификация — **HF6853 / Auctus A6 / RDA8809**.
|
||||||
|
|
||||||
|
## Скрытый ATE/CPS-интерфейс (реверс jhart99, репо `a6tools`)
|
||||||
|
|
||||||
|
Полностью реверснут. Копия в [`research/a6tools/`](../research/a6tools). Транспорт: UART **921600 8N1, XON/XOFF**.
|
||||||
|
|
||||||
|
### Два уровня протокола
|
||||||
|
|
||||||
|
1. **RDA-debug** (низкий уровень, доступ к памяти/регистрам):
|
||||||
|
кадр `AD 00 <len16 BE> FF <cmd> <payload> <XOR>`; спецбайты `0x5C/0x11/0x13` экранируются
|
||||||
|
(`5C, 0xFF^x`); ACK = `11 13`.
|
||||||
|
Команды: `0x02` read_word, `0x04` read_reg8, `0x84` write_reg8, `0x83` write_block.
|
||||||
|
2. **ATE/CPS** (высокий уровень): команда пишется `write_block`’ом в спец-регистр
|
||||||
|
`ate_cps_addr` (адрес = read_word(`0x81C00270`)), затем «звонок» — запись в
|
||||||
|
регистр `0x00000005` (h2p): `0x00`=clear, `0xA5`=выполнить, `0xEE`=reboot, `0xFF`=boot handler.
|
||||||
|
- ATE-кадр: строка `AT+…\r`, добита нулями до кратности 4.
|
||||||
|
- CPS-кадр: `aa <len8> <cmd…> <XOR> bb` + null-паддинг до кратности 4.
|
||||||
|
- Ответ читается из памяти: длина по `resp_addr-4`, данные с `resp_addr`
|
||||||
|
(`resp_addr` = read_word(`0x81C00264`), uart_resp = `0x81C0026C`).
|
||||||
|
|
||||||
|
Вход в debug: держать UART в debug-режиме записью регистра `0x00000003 = 0x80`.
|
||||||
|
`rebootandfreeze` (запись рег.`0` = `0x03`) — сброс+останов CPU для чтения ROM.
|
||||||
|
|
||||||
|
## ⭐ Починка частотного смещения TX (наша проблема [[rt4d-dmr-tx-broken]])
|
||||||
|
|
||||||
|
**Ровно наш случай.** У A6-раций заводское смещение TCXO (у jhart99 GOCOM GD900 было
|
||||||
|
+800 Гц «в допуске», но MMDVM не тянул). У нас ~1100 Гц — тоже в пределах ±2500 Гц.
|
||||||
|
|
||||||
|
Команды: `AT+GETFREQERR` (прочитать), `AT+DMOFREQERR=<N>` (записать).
|
||||||
|
**Формула:** `offset[Гц] = -2500 + 10*N` ⇔ `N = (offset + 2500)/10` (N = 0..500, шаг 10 Гц).
|
||||||
|
После записи **сменить канал**, чтобы применилось. Диапазон ±2500 Гц.
|
||||||
|
|
||||||
|
`freqoffset.py`: `delta = target - current; N = ((get_freq_err()+delta)+2500)/10`.
|
||||||
|
Т.е. измеряем реальную TX по SDR, даём (измеренную, номинальную) — тул сам считает N.
|
||||||
|
|
||||||
|
### Как применить на RT-4D
|
||||||
|
1. Ввести модуль в bridge/update-режим («Обновление DMR чипа» — вкл. с зажатой MENU).
|
||||||
|
Проверить, слушает ли он RDA-debug (`AD 00 … FF 04 <03000000> 01 …` → ответ `content=0x80`)
|
||||||
|
на нужной скорости (пробовать 921600, затем скорость нашего моста).
|
||||||
|
2. Если отвечает → адаптировать `tools/a6_freqfix.py` (заготовка в репо) под COM рации через
|
||||||
|
powershell-мост (как в `tools/fm100b.py`).
|
||||||
|
3. Измерить реальную TX-частоту рации по SDR (RTL-SDRv3) относительно номинала (438.800 и т.д.).
|
||||||
|
4. `AT+DMOCONNECT` → `AT+GETFREQERR` → посчитать N → `AT+DMOFREQERR=N` → сменить канал → проверить BER на хотспоте.
|
||||||
|
|
||||||
|
**НЕ выполнять вслепую** (боевая рация, нужен SDR для измерения — см. [[rt4d-dmr-tx-broken]]).
|
||||||
|
Это первый путь, НЕ требующий Radtel `.updat` и не требующий Radio Tuning Software.
|
||||||
|
|
||||||
|
## Что это открывает помимо TX
|
||||||
|
- `AT+DMOSETVAFC/DMOGETVAFC`, `AT+DMOSETPOWER/DMOSETCHPOWER`, `AT+DMORDRSSI` — вся калибровка.
|
||||||
|
- CPS-команды (`0x0012` chanInfo и др.) — прямое чтение/запись каналов в модуле.
|
||||||
|
- `radiodump-ng.py` — дамп прошивки модуля через debug-интерфейс (свой вокодер/reverse).
|
||||||
|
- Полный список — `research/a6tools/atcommands.md`, `cpecommands.md`.
|
||||||
|
|
||||||
|
## Источники
|
||||||
|
- jhart99: [A6 обзор](https://jhart99.com/auctus-a6/), [A6 in depth](https://jhart99.com/auctus-a6-revealed/),
|
||||||
|
[hidden interface](https://jhart99.com/a6-hidden-interface/), [ATE/CPS commands](https://jhart99.com/atecps/),
|
||||||
|
[Frequency Fix (GD900)](https://jhart99.com/frequency-fix/)
|
||||||
|
- Код: [github.com/jhart99/a6tools](https://github.com/jhart99/a6tools) (копия в `research/a6tools/`)
|
||||||
|
- qdmr `AuctusA6Interface` (libdmrconf)
|
||||||
|
- Схема: `RT4DDLT01.pdf` (Radtel), рендеры листа DMR-модуля.
|
||||||
@@ -0,0 +1,20 @@
|
|||||||
|
Copyright 2021 Jonathan Hart
|
||||||
|
|
||||||
|
Permission is hereby granted, free of charge, to any person obtaining
|
||||||
|
a copy of this software and associated documentation files (the
|
||||||
|
"Software"), to deal in the Software without restriction, including
|
||||||
|
without limitation the rights to use, copy, modify, merge, publish,
|
||||||
|
distribute, sublicense, and/or sell copies of the Software, and to
|
||||||
|
permit persons to whom the Software is furnished to do so, subject to
|
||||||
|
the following conditions:
|
||||||
|
|
||||||
|
The above copyright notice and this permission notice shall be
|
||||||
|
included in all copies or substantial portions of the Software.
|
||||||
|
|
||||||
|
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
||||||
|
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
||||||
|
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
||||||
|
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
||||||
|
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
||||||
|
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
||||||
|
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||||
@@ -0,0 +1,35 @@
|
|||||||
|
## A6tools
|
||||||
|
|
||||||
|
A Python library and tools AUCTUS A6 based radios.
|
||||||
|
|
||||||
|
### Radios
|
||||||
|
Tested and working with:
|
||||||
|
* COTRE CO01D
|
||||||
|
* COTRE CO04D
|
||||||
|
* COTRE CO06D
|
||||||
|
* GOCOM GD900
|
||||||
|
|
||||||
|
Probably also works with:
|
||||||
|
* GOCOM GD100
|
||||||
|
* GOCOM GD700
|
||||||
|
* GOCOM GD800
|
||||||
|
* CONNECOM radios as well
|
||||||
|
|
||||||
|
### Usage
|
||||||
|
|
||||||
|
#### atcommander
|
||||||
|
|
||||||
|
atcommander sends AT commands to the radio which can be used to change many of the parameters.
|
||||||
|
|
||||||
|
Usage:
|
||||||
|
```
|
||||||
|
$ python3 atcommander.py AT+DMOCONNECT
|
||||||
|
OnCmd_DMOCONNECT
|
||||||
|
ATE_SendCmdAck: cmd:DMOCONNECT isOk:0x1
|
||||||
|
tx_length:17
|
||||||
|
ATE_SendDataFrame:
|
||||||
|
+DMOCONNECT:0
|
||||||
|
|
||||||
|
ATE pipe. used[HOST]
|
||||||
|
```
|
||||||
|
You must start with AT+DMOCONNECT prior to any other commands
|
||||||
@@ -0,0 +1,26 @@
|
|||||||
|
from .escaper import escaper
|
||||||
|
from .escaper import unescaper
|
||||||
|
from .rdadebug import compute_check
|
||||||
|
from .rdadebug import rda_debug_frame
|
||||||
|
from .rdadebug import read_word
|
||||||
|
from .rdadebug import write_register_int8
|
||||||
|
from .rdadebug import read_register_int8
|
||||||
|
from .rdadebug import write_block
|
||||||
|
from .a6commands import h2p_command
|
||||||
|
from .a6commands import set_uart_to_host
|
||||||
|
from .a6commands import set_uart_to_normal
|
||||||
|
from .a6commands import read_uart_to_host
|
||||||
|
from .a6commands import ate_command
|
||||||
|
from .a6commands import cps_command
|
||||||
|
from .a6commands import reboot_and_freeze
|
||||||
|
from .serialio import send_uart_setup
|
||||||
|
from .serialio import fetch_memory_address
|
||||||
|
from .serialio import send_ate_command
|
||||||
|
from .serialio import send_cps_command
|
||||||
|
from .serialio import atecps_resp_read
|
||||||
|
from .serialio import read_mem_range
|
||||||
|
from .serialio import get_chan_info
|
||||||
|
from .serialio import get_freq_err
|
||||||
|
from .serialio import parse_freq_err_resp
|
||||||
|
from .serialio import set_freq_err
|
||||||
|
from .serialio import SerialIO
|
||||||
@@ -0,0 +1,168 @@
|
|||||||
|
from .rdadebug import write_register_int8
|
||||||
|
from .rdadebug import read_register_int8
|
||||||
|
from .rdadebug import write_block
|
||||||
|
from .rdadebug import compute_check
|
||||||
|
from .eprint import eprint
|
||||||
|
|
||||||
|
def h2p_command(msg):
|
||||||
|
""" Format a frame for an h2p command
|
||||||
|
|
||||||
|
The CPS software sends commands to a special debug register
|
||||||
|
00000005. Writing a value to this register throws an interupt
|
||||||
|
which is picked up by a function on the device.
|
||||||
|
|
||||||
|
0x00 : Command finished, clears semaphore
|
||||||
|
0xA5 : Process command with RxByHostPortCB
|
||||||
|
0xEE : Reboot
|
||||||
|
0xFF : Handle with boot_HstCmdBasicHandler
|
||||||
|
|
||||||
|
@param msg: the message to send
|
||||||
|
@return: the frame to send
|
||||||
|
|
||||||
|
"""
|
||||||
|
return write_register_int8(0x5, msg)
|
||||||
|
|
||||||
|
def set_uart_to_normal():
|
||||||
|
""" Set device uart to host mode
|
||||||
|
|
||||||
|
The CPS software sends repeated requests to set internal register
|
||||||
|
00000003 to 0x80 which has the effect of locking the UART to debug
|
||||||
|
mode
|
||||||
|
|
||||||
|
@return: the frame to send
|
||||||
|
|
||||||
|
"""
|
||||||
|
return write_register_int8(3, 0x00)
|
||||||
|
|
||||||
|
def set_uart_to_host():
|
||||||
|
""" Set device uart to host mode
|
||||||
|
|
||||||
|
The CPS software sends repeated requests to set internal register
|
||||||
|
00000003 to 0x80 which has the effect of locking the UART to debug
|
||||||
|
mode
|
||||||
|
|
||||||
|
@return: the frame to send
|
||||||
|
|
||||||
|
"""
|
||||||
|
return write_register_int8(3, 0x80)
|
||||||
|
|
||||||
|
def reboot_and_freeze():
|
||||||
|
""" Reboot and freeze the processor
|
||||||
|
|
||||||
|
This command comes from coolwatcher and resets the processor and
|
||||||
|
immediately halts it. This is useful for stepping through the
|
||||||
|
boot process, but also allows some areas of ROM to be read without
|
||||||
|
crashing
|
||||||
|
|
||||||
|
@return: the frame to send
|
||||||
|
|
||||||
|
"""
|
||||||
|
return write_register_int8(0, 0x03)
|
||||||
|
|
||||||
|
def read_uart_to_host():
|
||||||
|
""" make a frame containing a knock command
|
||||||
|
|
||||||
|
this function creates a frame that I assume wakes up the device
|
||||||
|
for further commands.
|
||||||
|
|
||||||
|
@return: the frame to send
|
||||||
|
|
||||||
|
"""
|
||||||
|
return read_register_int8(3)
|
||||||
|
|
||||||
|
def ate_command(cmd, p_atecps_write):
|
||||||
|
""" make a frame containing an ATE command
|
||||||
|
|
||||||
|
@param cmd: the command to send
|
||||||
|
@param p_atecps_write: the address of the CPS write register
|
||||||
|
@return: the frame to send
|
||||||
|
|
||||||
|
"""
|
||||||
|
cmd = bytearray(cmd, 'utf-8') + b'\r'
|
||||||
|
cmd += bytes(4 - len(cmd) % 4)
|
||||||
|
return write_block(p_atecps_write, cmd)
|
||||||
|
|
||||||
|
def cps_command(cmd, p_atecps_write):
|
||||||
|
""" make a frame containing an CPS command
|
||||||
|
|
||||||
|
@param cmd: the command to send
|
||||||
|
@param p_atecps_write: the address of the CPS write register
|
||||||
|
@return: the frame to send
|
||||||
|
|
||||||
|
"""
|
||||||
|
length = (len(cmd) + 4).to_bytes(1, 'big')
|
||||||
|
check = compute_check(length + cmd)
|
||||||
|
begin = bytes([0xaa])
|
||||||
|
end = bytes([0xbb])
|
||||||
|
msg = begin + length + cmd + check + end
|
||||||
|
padding = 4 - (len(msg) % 4)
|
||||||
|
return write_block(p_atecps_write, msg + bytes([0x00]) * padding)
|
||||||
|
|
||||||
|
class CPSFrame:
|
||||||
|
""" Received CPS class
|
||||||
|
|
||||||
|
This class decodes CPS frames received from the device.
|
||||||
|
"""
|
||||||
|
check_fail = False
|
||||||
|
length = 0
|
||||||
|
type = 0
|
||||||
|
content = bytes([])
|
||||||
|
def __init__(self, msg):
|
||||||
|
eprint(msg.hex())
|
||||||
|
if (msg[-1].to_bytes(1, 'big') != compute_check(msg[1:-2])):
|
||||||
|
self.check_fail = True
|
||||||
|
eprint('CPS frame check failed')
|
||||||
|
return
|
||||||
|
self.length = msg[1]
|
||||||
|
self.type = int.from_bytes(msg[2:4], 'big')
|
||||||
|
self.is_ok = msg[4] == 0x01
|
||||||
|
self.content = msg[5:-3]
|
||||||
|
def __repr__(self):
|
||||||
|
return 'packet length {} type {} is_ok {} content {}'.format(self.length, self.type, self.is_ok, self.content)
|
||||||
|
|
||||||
|
class ChanInfoFrame(CPSFrame):
|
||||||
|
""" Received ChanInfoFrame class
|
||||||
|
|
||||||
|
This class decodes ChanInfoFrame frames received from the device.
|
||||||
|
|
||||||
|
"\tcpsInst.chanInfo.nChanIndex=%d\n
|
||||||
|
\tcpsInst.chanInfo.nChanType=%d\n
|
||||||
|
\tcpsInst.chanInfo.nVox=%d\n
|
||||||
|
\tcpsInst.chanInfo.nPower=%d\n
|
||||||
|
\tcpsInst.chanInfo.nRxFreq=%d\n
|
||||||
|
\tcpsInst.chanInfo.nTxFreq=%d\n
|
||||||
|
\tcpsInst.chanInfo.nTxContactsIdx=0x%08x\n
|
||||||
|
\tcpsInst.chanInfo.nColorCode=%d\n
|
||||||
|
\tcpsInst.chanInfo.nTimeSlot=%d\n
|
||||||
|
\tcpsInst.chanInfo.bPoliteCall=%d\n"
|
||||||
|
\tcpsInst.chanInfo.nEmrSys=%d\n
|
||||||
|
\tcpsInst.chanInfo.nEncry=%d\n
|
||||||
|
\tcpsInst.chanInfo.nTypeWideNarrow=%d\n
|
||||||
|
\tcpsInst.chanInfo.nRxCtdcs=%d\n
|
||||||
|
\tcpsInst.chanInfo.bRxCtdcsInvert=%d\n
|
||||||
|
\tcpsInst.chanInfo.bTxCtdcsInvert=%d\n
|
||||||
|
\tcpsInst.chanInfo.nTxCtdcs=%d\n
|
||||||
|
\tcpsInst.chanInfo.nRxGrpListIdx=%d\n"
|
||||||
|
"""
|
||||||
|
def __init__(self, msg):
|
||||||
|
super().__init__(msg)
|
||||||
|
self.index = int.from_bytes(self.content[0:2], 'little')
|
||||||
|
self.chantype = self.content[2]
|
||||||
|
self.rxFreq = int.from_bytes(self.content[4:8], 'little')
|
||||||
|
self.txFreq = int.from_bytes(self.content[8:12], 'little')
|
||||||
|
self.txContactIndex = int.from_bytes(self.content[12:16], 'little')
|
||||||
|
self.colorCode = self.content[16]
|
||||||
|
self.timeslot = self.content[17]
|
||||||
|
self.polite = self.content[18]
|
||||||
|
self.emrSys = int.from_bytes(self.content[1:2], 'big')
|
||||||
|
self.encryption = int.from_bytes(self.content[1:2], 'big')
|
||||||
|
self.widenarrow = int.from_bytes(self.content[1:2], 'big')
|
||||||
|
self.rxctdcs = int.from_bytes(self.content[1:2], 'big')
|
||||||
|
self.rxctdcsinvert = int.from_bytes(self.content[1:2], 'big')
|
||||||
|
self.txctdcsinvert = int.from_bytes(self.content[1:2], 'big')
|
||||||
|
self.txctdcs = int.from_bytes(self.content[1:2], 'big')
|
||||||
|
self.rxGroupIdx = int.from_bytes(self.content[1:2], 'big')
|
||||||
|
self.vox = int.from_bytes(self.content[1:2], 'big')
|
||||||
|
def __repr__(self):
|
||||||
|
return 'packet length {} type {} is_ok {} index {} chantype {} rxfreq {} txfreq {}'.format(
|
||||||
|
self.length, self.type, self.is_ok, self.index, self.chantype, self.rxFreq, self.txFreq)
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
import sys
|
||||||
|
|
||||||
|
def eprint(*args, **kwargs):
|
||||||
|
""" print to stderr
|
||||||
|
|
||||||
|
This function takes its arguments just as if it were the normal
|
||||||
|
print function and instead prints to stderr.
|
||||||
|
|
||||||
|
@param args: the arguments to print
|
||||||
|
@param kwargs: the keyword arguments to print
|
||||||
|
|
||||||
|
"""
|
||||||
|
|
||||||
|
print(*args, file=sys.stderr, **kwargs)
|
||||||
@@ -0,0 +1,34 @@
|
|||||||
|
def escaper(msg):
|
||||||
|
""" escape message
|
||||||
|
|
||||||
|
this function escapes special characters in the message. These
|
||||||
|
are 0x5c, 0x11 and 0x13 which are '\' and XON and XOFF characters.
|
||||||
|
|
||||||
|
@param msg: the message to escape
|
||||||
|
@return: the escaped message
|
||||||
|
|
||||||
|
"""
|
||||||
|
|
||||||
|
out = bytes(sum([[0x5c, 0xFF ^ x ] if x in [0x11, 0x13, 0x5c] else [x] for x in msg], []))
|
||||||
|
return out
|
||||||
|
|
||||||
|
def unescaper(msg):
|
||||||
|
""" unescape message
|
||||||
|
|
||||||
|
this function undoes any escape sequences in a received message
|
||||||
|
|
||||||
|
@param msg: the message to unescape
|
||||||
|
@return: the unescaped message
|
||||||
|
"""
|
||||||
|
|
||||||
|
out = []
|
||||||
|
escape = False
|
||||||
|
for x in msg:
|
||||||
|
if x == 0x5c:
|
||||||
|
escape = True
|
||||||
|
continue
|
||||||
|
if escape:
|
||||||
|
x = 0x5c ^ x ^ 0xa3
|
||||||
|
escape = False
|
||||||
|
out.append(x)
|
||||||
|
return bytes(out)
|
||||||
@@ -0,0 +1,121 @@
|
|||||||
|
import functools
|
||||||
|
import operator
|
||||||
|
from .escaper import escaper
|
||||||
|
from .escaper import unescaper
|
||||||
|
from .eprint import eprint
|
||||||
|
|
||||||
|
__author__ = "jhart99"
|
||||||
|
__license__ = "MIT"
|
||||||
|
|
||||||
|
def compute_check(msg):
|
||||||
|
""" Compute the check value for a message
|
||||||
|
|
||||||
|
AUCTUS messages use a check byte which is simply the XOR of all
|
||||||
|
the values of the message.
|
||||||
|
|
||||||
|
"""
|
||||||
|
|
||||||
|
if len(msg) == 0:
|
||||||
|
return int(0).to_bytes(1, 'little')
|
||||||
|
return functools.reduce(operator.xor, msg).to_bytes(1, 'little')
|
||||||
|
|
||||||
|
def rda_debug_frame(flow, cmd, message):
|
||||||
|
""" Format a raw message into a frame
|
||||||
|
|
||||||
|
AUCTUS frames are of the form AD 00 XX FF ...message... YY
|
||||||
|
where XX is the length of the message and YY is the check byte
|
||||||
|
Additionally certain bytes in the message are escaped.
|
||||||
|
|
||||||
|
"""
|
||||||
|
|
||||||
|
header = int(0xad).to_bytes(1, 'big')
|
||||||
|
msg = flow + cmd + message
|
||||||
|
msglen = len(msg).to_bytes(2, 'big')
|
||||||
|
check = compute_check(msg)
|
||||||
|
return escaper(header + msglen + msg + check)
|
||||||
|
|
||||||
|
def read_word(addr, seq = 1):
|
||||||
|
""" make a frame to read a word at a memory address
|
||||||
|
|
||||||
|
this function creates a frame to read the memory from the device
|
||||||
|
suitable for serial transmission.
|
||||||
|
|
||||||
|
"""
|
||||||
|
|
||||||
|
flow = bytes([0xff])
|
||||||
|
command = bytes([0x02])
|
||||||
|
if isinstance(addr, int):
|
||||||
|
addr = addr.to_bytes(4, 'little')
|
||||||
|
msg = addr + seq.to_bytes(1, 'big')
|
||||||
|
return rda_debug_frame(flow, command, msg)
|
||||||
|
|
||||||
|
def write_register_int8(addr, msg):
|
||||||
|
""" write to a byte to an internal register
|
||||||
|
|
||||||
|
this function creates a frame to do some device magic and these
|
||||||
|
frames are used in the preamble and finalizer commands.
|
||||||
|
|
||||||
|
"""
|
||||||
|
|
||||||
|
flow = bytes([0xff])
|
||||||
|
command = bytes([0x84])
|
||||||
|
msg = addr.to_bytes(4, 'little') + msg.to_bytes(1, 'little')
|
||||||
|
return rda_debug_frame(flow, command, msg)
|
||||||
|
|
||||||
|
def read_register_int8(addr, seq=1):
|
||||||
|
""" make a frame containing a knock command
|
||||||
|
|
||||||
|
this function creates a frame that I assume wakes up the device
|
||||||
|
for further commands.
|
||||||
|
|
||||||
|
"""
|
||||||
|
|
||||||
|
flow = bytes([0xff])
|
||||||
|
command = bytes([0x04])
|
||||||
|
msg = addr.to_bytes(4, 'little') + seq.to_bytes(1, 'big')
|
||||||
|
return rda_debug_frame(flow, command, msg)
|
||||||
|
|
||||||
|
def write_block(addr, msg):
|
||||||
|
""" make a frame containing a write command
|
||||||
|
|
||||||
|
this function creates a frame to do write a multiple byte content
|
||||||
|
at a specific memory address. The length need not be a word, but
|
||||||
|
could be 16 bytes or more.
|
||||||
|
|
||||||
|
"""
|
||||||
|
|
||||||
|
flow = bytes([0xff])
|
||||||
|
command = bytes([0x83])
|
||||||
|
if isinstance(addr, int):
|
||||||
|
addr = addr.to_bytes(4, 'little')
|
||||||
|
msg = addr + msg
|
||||||
|
return rda_debug_frame(flow, command, msg)
|
||||||
|
|
||||||
|
|
||||||
|
class RdaFrame:
|
||||||
|
""" Received Frame class
|
||||||
|
|
||||||
|
This class decodes possible received Frames.
|
||||||
|
"""
|
||||||
|
ack = False
|
||||||
|
check_fail = False
|
||||||
|
seq = 0
|
||||||
|
length = 0
|
||||||
|
content = bytes([])
|
||||||
|
def __init__(self, msg):
|
||||||
|
msg = unescaper(msg)
|
||||||
|
if len(msg) <= 4:
|
||||||
|
if(msg == b'\x11\x13'):
|
||||||
|
self.ack = True
|
||||||
|
else:
|
||||||
|
# impossibly short frame something is wrong.
|
||||||
|
self.check_fail = True
|
||||||
|
return
|
||||||
|
if (msg[-1].to_bytes(1, 'big') != compute_check(msg[3:-1])):
|
||||||
|
self.check_fail = True
|
||||||
|
return
|
||||||
|
self.seq = msg[4]
|
||||||
|
self.length = msg[2]
|
||||||
|
self.content = msg[5:-1]
|
||||||
|
def __repr__(self):
|
||||||
|
return 'packet length {} seq {} content {} ack {} check {}'.format(self.length, self.seq, self.content, self.ack, self.check_fail)
|
||||||
@@ -0,0 +1,365 @@
|
|||||||
|
import serial
|
||||||
|
import time
|
||||||
|
import sys
|
||||||
|
import re
|
||||||
|
from .eprint import eprint
|
||||||
|
from .a6commands import CPSFrame, h2p_command
|
||||||
|
from .a6commands import ChanInfoFrame, h2p_command
|
||||||
|
from .a6commands import ate_command
|
||||||
|
from .a6commands import cps_command
|
||||||
|
from .a6commands import read_uart_to_host
|
||||||
|
from .rdadebug import RdaFrame
|
||||||
|
from .rdadebug import read_word
|
||||||
|
|
||||||
|
class Singleton(object):
|
||||||
|
def __new__(cls, *args, **kwargs):
|
||||||
|
""" Singleton class
|
||||||
|
|
||||||
|
@param args: arguments
|
||||||
|
@param kwargs: keyword arguments
|
||||||
|
@return: object
|
||||||
|
"""
|
||||||
|
it = cls.__dict__.get("__it__")
|
||||||
|
if it is not None:
|
||||||
|
return it
|
||||||
|
cls.__it__ = it = object.__new__(cls)
|
||||||
|
it.init(*args, **kwargs)
|
||||||
|
return it
|
||||||
|
|
||||||
|
def init(self, *args, **kwargs):
|
||||||
|
"""
|
||||||
|
"""
|
||||||
|
pass
|
||||||
|
|
||||||
|
class SerialIO(Singleton):
|
||||||
|
def init(self, port, baudrate=921600, verbosity=0, timeout=0.1):
|
||||||
|
""" Initialize the serial port
|
||||||
|
|
||||||
|
@param port: serial port
|
||||||
|
@param baudrate: baud rate
|
||||||
|
@param verbosity: verbosity level
|
||||||
|
"""
|
||||||
|
self.port = port
|
||||||
|
self.sio = serial.Serial(port, baudrate,
|
||||||
|
serial.EIGHTBITS, serial.PARITY_NONE, serial.STOPBITS_ONE,
|
||||||
|
xonxoff=True, rtscts=False, timeout=timeout)
|
||||||
|
self.verbosity = verbosity
|
||||||
|
self._ate_cps_addr = 0
|
||||||
|
self._ate_cps_resp_addr = 0
|
||||||
|
self._ate_cps_resp_length_addr = 0
|
||||||
|
self._uart_resp_addr = 0
|
||||||
|
self.sio.flush()
|
||||||
|
if verbosity > 0:
|
||||||
|
eprint("SerialIO: {} initialized".format(self.port))
|
||||||
|
|
||||||
|
def __del__(self):
|
||||||
|
""" Close the serial port
|
||||||
|
"""
|
||||||
|
self.sio.close()
|
||||||
|
|
||||||
|
def write(self, msg):
|
||||||
|
""" Write a message to the serial port
|
||||||
|
|
||||||
|
@param msg: message
|
||||||
|
"""
|
||||||
|
if self.verbosity > 0:
|
||||||
|
eprint("write : ", msg.hex())
|
||||||
|
self.sio.write(msg)
|
||||||
|
|
||||||
|
def read(self, nbytes):
|
||||||
|
""" Read nbytes from the serial port
|
||||||
|
|
||||||
|
@param nbytes: number of bytes
|
||||||
|
@return: message
|
||||||
|
"""
|
||||||
|
data = self.sio.read(nbytes)
|
||||||
|
if self.verbosity > 0:
|
||||||
|
eprint("read : ", data.hex())
|
||||||
|
return data
|
||||||
|
|
||||||
|
def flush(self):
|
||||||
|
""" Flush the serial port
|
||||||
|
"""
|
||||||
|
self.sio.flush()
|
||||||
|
|
||||||
|
@property
|
||||||
|
def in_waiting(self):
|
||||||
|
""" return the number of bytes in the serial port
|
||||||
|
"""
|
||||||
|
return self.sio.in_waiting
|
||||||
|
|
||||||
|
@property
|
||||||
|
def ate_cps_addr(self):
|
||||||
|
""" return the address of the ate command
|
||||||
|
"""
|
||||||
|
if self._ate_cps_addr == 0:
|
||||||
|
self._ate_cps_addr = fetch_memory_address(0x81c00270)
|
||||||
|
self._ate_cps_addr = int.from_bytes(self._ate_cps_addr, byteorder='little')
|
||||||
|
return self._ate_cps_addr
|
||||||
|
|
||||||
|
@property
|
||||||
|
def ate_cps_resp_addr(self):
|
||||||
|
""" return the address of the ate command response
|
||||||
|
"""
|
||||||
|
if self._ate_cps_resp_addr == 0:
|
||||||
|
self._ate_cps_resp_addr = fetch_memory_address(0x81c00264)
|
||||||
|
self._ate_cps_resp_addr = int.from_bytes(self._ate_cps_resp_addr, byteorder='little')
|
||||||
|
return self._ate_cps_resp_addr
|
||||||
|
|
||||||
|
@property
|
||||||
|
def ate_cps_resp_length_addr(self):
|
||||||
|
""" return the address of the ate command response
|
||||||
|
"""
|
||||||
|
return self.ate_cps_resp_addr - 4
|
||||||
|
|
||||||
|
@property
|
||||||
|
def uart_resp_addr(self):
|
||||||
|
""" return the address of the ate command response
|
||||||
|
"""
|
||||||
|
if self._uart_resp_addr == 0:
|
||||||
|
self._uart_resp_addr = fetch_memory_address(0x81c0026c)
|
||||||
|
self._uart_resp_addr = int.from_bytes(self._uart_resp_addr, byteorder='little')
|
||||||
|
return self._uart_resp_addr
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
def write_flush_pause(msg, sleep = 0.07):
|
||||||
|
""" Write out to serial and wait for the radio to process the command
|
||||||
|
|
||||||
|
@param msg: bytes to write
|
||||||
|
@param sleep: time to sleep after writing in ms
|
||||||
|
|
||||||
|
"""
|
||||||
|
uart = SerialIO()
|
||||||
|
uart.write(msg)
|
||||||
|
uart.flush()
|
||||||
|
time.sleep(0.07)
|
||||||
|
|
||||||
|
|
||||||
|
def send_ate_command(msg):
|
||||||
|
""" Send a command to the ATE/CPS function on the radio
|
||||||
|
|
||||||
|
To send a command to the ATE or CPS software on the radio, it has
|
||||||
|
to be surrounded by these h2p commands which clear the registers
|
||||||
|
and then throw and interupt which causes the command to be
|
||||||
|
executed
|
||||||
|
|
||||||
|
@param msg: bytes to write
|
||||||
|
|
||||||
|
"""
|
||||||
|
uart = SerialIO()
|
||||||
|
write_flush_pause(h2p_command(0))
|
||||||
|
write_flush_pause(ate_command(msg, uart.ate_cps_addr))
|
||||||
|
write_flush_pause(h2p_command(0xa5))
|
||||||
|
|
||||||
|
def send_cps_command(msg):
|
||||||
|
""" Send a command to the ATE/CPS function on the radio
|
||||||
|
|
||||||
|
To send a command to the ATE or CPS software on the radio, it has
|
||||||
|
to be surrounded by these h2p commands which clear the registers
|
||||||
|
and then throw and interupt which causes the command to be
|
||||||
|
executed
|
||||||
|
|
||||||
|
@param msg: bytes to write
|
||||||
|
|
||||||
|
"""
|
||||||
|
|
||||||
|
uart = SerialIO()
|
||||||
|
write_flush_pause(h2p_command(0))
|
||||||
|
write_flush_pause(cps_command(msg, uart.ate_cps_addr))
|
||||||
|
write_flush_pause(h2p_command(0xa5))
|
||||||
|
|
||||||
|
def wait_on_read(retries=256, delay=0):
|
||||||
|
""" Wait until a read happens
|
||||||
|
|
||||||
|
This function waits until something is received from the serial or
|
||||||
|
will abort after a certain number of retries.
|
||||||
|
|
||||||
|
@param retries: number of retries before aborting
|
||||||
|
@param delay: time to sleep between retries
|
||||||
|
"""
|
||||||
|
|
||||||
|
uart = SerialIO()
|
||||||
|
size = uart.in_waiting
|
||||||
|
countdown = retries
|
||||||
|
while size == 0 and countdown > 0:
|
||||||
|
size = uart.in_waiting
|
||||||
|
countdown -= 1
|
||||||
|
if delay: time.sleep(delay)
|
||||||
|
if countdown == 0:
|
||||||
|
# nothing received
|
||||||
|
return b''
|
||||||
|
if size > 0:
|
||||||
|
data = uart.read(size)
|
||||||
|
return data
|
||||||
|
|
||||||
|
def send_uart_setup():
|
||||||
|
""" Replays the initial UART setup sequence
|
||||||
|
|
||||||
|
This sequence and timing is from the CPS software capture.
|
||||||
|
"""
|
||||||
|
uart = SerialIO()
|
||||||
|
knock_worked = False
|
||||||
|
retries = 25
|
||||||
|
while not knock_worked and retries > 0:
|
||||||
|
uart.write(read_uart_to_host())
|
||||||
|
uart.flush()
|
||||||
|
time.sleep(0.001)
|
||||||
|
data = wait_on_read()
|
||||||
|
response = RdaFrame(data)
|
||||||
|
if response.seq == 1 and response.content == b'\x80':
|
||||||
|
knock_worked = True
|
||||||
|
else:
|
||||||
|
time.sleep(0.25)
|
||||||
|
retries -= 1
|
||||||
|
return knock_worked
|
||||||
|
|
||||||
|
def fetch_memory_address(addr, seq=1):
|
||||||
|
""" Attempt to read a memory address and keep trying until it succeeds
|
||||||
|
|
||||||
|
"""
|
||||||
|
uart = SerialIO()
|
||||||
|
read_ok = False
|
||||||
|
retval = b''
|
||||||
|
retries = 25
|
||||||
|
while not read_ok:
|
||||||
|
frame = read_word(addr, seq)
|
||||||
|
uart.write(frame)
|
||||||
|
uart.flush()
|
||||||
|
size = uart.in_waiting
|
||||||
|
i = retries
|
||||||
|
while size == 0 and i > 0:
|
||||||
|
time.sleep(0.001)
|
||||||
|
size = uart.in_waiting
|
||||||
|
i -= 1
|
||||||
|
if retries == 0:
|
||||||
|
continue
|
||||||
|
data = uart.read(size)
|
||||||
|
inbound_frame = RdaFrame(data)
|
||||||
|
read_ok = inbound_frame.seq == seq and not inbound_frame.check_fail
|
||||||
|
retval = inbound_frame.content
|
||||||
|
return retval
|
||||||
|
|
||||||
|
def atecps_resp_read():
|
||||||
|
""" Read the response from an ATECPS command
|
||||||
|
|
||||||
|
@return: response from ATECPS command
|
||||||
|
|
||||||
|
"""
|
||||||
|
uart = SerialIO()
|
||||||
|
length = fetch_memory_address(uart.ate_cps_resp_length_addr)
|
||||||
|
length = int.from_bytes(length, 'little')
|
||||||
|
response = read_mem_range(uart.ate_cps_resp_addr, uart.ate_cps_resp_addr + length)
|
||||||
|
return response
|
||||||
|
|
||||||
|
def uart_resp_read():
|
||||||
|
""" Read the response from an ATECPS command
|
||||||
|
|
||||||
|
@return: response from ATECPS command
|
||||||
|
|
||||||
|
"""
|
||||||
|
uart = SerialIO()
|
||||||
|
length = fetch_memory_address(uart.uart_resp_addr)
|
||||||
|
length = length[1]
|
||||||
|
response = read_mem_range(uart.uart_resp_addr, uart.uart_resp_addr + length)
|
||||||
|
return response
|
||||||
|
|
||||||
|
def read_mem_range(begin, end):
|
||||||
|
""" Read a memory range
|
||||||
|
|
||||||
|
@param begin: start address
|
||||||
|
@param end: end address
|
||||||
|
@return: the data in bytes
|
||||||
|
|
||||||
|
"""
|
||||||
|
addr = begin
|
||||||
|
datalist = []
|
||||||
|
while addr < end:
|
||||||
|
data = fetch_memory_address(addr)
|
||||||
|
if len(data) == 4:
|
||||||
|
datalist.append(data)
|
||||||
|
addr = addr + 4
|
||||||
|
return b''.join(datalist)
|
||||||
|
|
||||||
|
def read_mem_burst(sio, begin, end, offset=0, verbosity=0):
|
||||||
|
""" Read a limited memory range using a burst read
|
||||||
|
|
||||||
|
@param sio: serial object
|
||||||
|
@param begin: start address
|
||||||
|
@param end: end address
|
||||||
|
@param offset: offset of the sequence number
|
||||||
|
@param verbosity: verbosity level
|
||||||
|
@return: the data in bytes
|
||||||
|
"""
|
||||||
|
|
||||||
|
if end - begin > 0x100:
|
||||||
|
raise ValueError('burst read only supports ranges of less than 256 bytes')
|
||||||
|
# the burst is in words of 4 bytes
|
||||||
|
burst = (end - begin) / 4
|
||||||
|
# preallocate the lists
|
||||||
|
recvflags = [False] * burst
|
||||||
|
recvdata = [0] * burst
|
||||||
|
i = 0
|
||||||
|
data = b''
|
||||||
|
while sum(recvflags) != burst:
|
||||||
|
while i < burst:
|
||||||
|
if not recvflags[i]:
|
||||||
|
sio.write(read_word(begin + 4 * i, i + offset + 1))
|
||||||
|
i += 1
|
||||||
|
size = sio.in_waiting
|
||||||
|
if size > 0:
|
||||||
|
data += sio.read(size)
|
||||||
|
i = 0
|
||||||
|
|
||||||
|
return b''.join(recvdata)
|
||||||
|
|
||||||
|
def get_chan_info(channel = 0):
|
||||||
|
""" Get the channel info
|
||||||
|
|
||||||
|
@param channel: channel number
|
||||||
|
@return: the channel info
|
||||||
|
"""
|
||||||
|
cmd = bytes([0, 0x12]) + channel.to_bytes(1, 'little')
|
||||||
|
send_cps_command(cmd)
|
||||||
|
resp = uart_resp_read()
|
||||||
|
print(ChanInfoFrame(resp))
|
||||||
|
# sys.stdout.buffer.write(resp)
|
||||||
|
|
||||||
|
def get_freq_err():
|
||||||
|
""" Get the frequency error from the Radio
|
||||||
|
|
||||||
|
@return: frequency error in Hz
|
||||||
|
"""
|
||||||
|
send_ate_command("AT+DMOCONNECT")
|
||||||
|
send_ate_command("AT+GETFREQERR")
|
||||||
|
resp = atecps_resp_read()
|
||||||
|
resp = resp.split(b'\x00')
|
||||||
|
resp = [x.decode('utf-8') for x in resp]
|
||||||
|
return parse_freq_err_resp(resp[0])
|
||||||
|
|
||||||
|
def parse_freq_err_resp(resp):
|
||||||
|
""" Parse the frequency error response
|
||||||
|
|
||||||
|
@param resp: response from ATECPS
|
||||||
|
@return: frequency error in Hz
|
||||||
|
"""
|
||||||
|
pattern = '\[(.+)\]'
|
||||||
|
freqerr = re.search(pattern, resp)
|
||||||
|
if freqerr:
|
||||||
|
return int(freqerr.group(1))
|
||||||
|
else:
|
||||||
|
return 0
|
||||||
|
|
||||||
|
def set_freq_err(freqerr):
|
||||||
|
""" Set the frequency error on the Radio
|
||||||
|
|
||||||
|
@param freqerr: frequency error parameter which is (-2500 + 10 * freqerr) in Hz
|
||||||
|
|
||||||
|
"""
|
||||||
|
send_ate_command("AT+DMOCONNECT")
|
||||||
|
send_ate_command("AT+DMOFREQERR={}".format(freqerr))
|
||||||
|
resp = atecps_resp_read()
|
||||||
|
resp = resp.split(b'\x00')
|
||||||
|
resp = [x.decode('utf-8') for x in resp]
|
||||||
|
for line in resp:
|
||||||
|
print(line)
|
||||||
@@ -0,0 +1,42 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
""" AT Commander for AUCTUS based radios
|
||||||
|
|
||||||
|
Allow for communication with AUCTUS A6 radios to their serial
|
||||||
|
interface through the debug interface. Commands can either be "AT"
|
||||||
|
commands or "CPS" commands. Both styles will work.
|
||||||
|
|
||||||
|
"""
|
||||||
|
|
||||||
|
import time
|
||||||
|
from a6 import send_ate_command, send_cps_command, atecps_resp_read, SerialIO
|
||||||
|
|
||||||
|
__author__ = "jhart99"
|
||||||
|
__license__ = "MIT"
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
import argparse
|
||||||
|
parser = argparse.ArgumentParser(description='Auctus A6 ATECPS commander')
|
||||||
|
parser.add_argument('-p', '--port', default='/dev/ttyUSB0',
|
||||||
|
type=str, help='serial port')
|
||||||
|
parser.add_argument('-b','--baudrate', default=921600,
|
||||||
|
type=int, help='baud rate')
|
||||||
|
parser.add_argument('-v','--verbosity', default=0, action='count',
|
||||||
|
help='print sent and received frames to stderr for debugging')
|
||||||
|
parser.add_argument('-V', '--version', action='version',
|
||||||
|
version='%(prog)s 0.0.1',
|
||||||
|
help='display version information and exit')
|
||||||
|
parser.add_argument('command')
|
||||||
|
args = parser.parse_args()
|
||||||
|
|
||||||
|
uart = SerialIO(args.port, args.baudrate, args.verbosity)
|
||||||
|
|
||||||
|
if args.command[0:3] == 'AT+':
|
||||||
|
send_ate_command(args.command)
|
||||||
|
else:
|
||||||
|
send_cps_command(bytes.fromhex(args.command))
|
||||||
|
time.sleep(0.1)
|
||||||
|
data = atecps_resp_read()
|
||||||
|
data = data.split(b'\x00')
|
||||||
|
for line in data:
|
||||||
|
print(line.decode('utf-8'))
|
||||||
@@ -0,0 +1,120 @@
|
|||||||
|
Command | function
|
||||||
|
---------------------|----------
|
||||||
|
DMOCONNECT | Required before issuing other commands
|
||||||
|
DMODISCONNECT | Ends the current session
|
||||||
|
DMORLIC | Returns the Device Serial Number
|
||||||
|
DMOVERQ | Returns Ver 1.0.0
|
||||||
|
DMOREVDATA | No response
|
||||||
|
BINARY | No response
|
||||||
|
DMOGETSTARTUP | No response
|
||||||
|
DMOGETSOFTVERSION | Returns the firmware version
|
||||||
|
DMOGETSQSTATUS | No response
|
||||||
|
DMOGETCURRCH | Replies with the current channel parameters +DMOGETCURRCH:0,DIG,438802000,438802000,0,0,1,2,0,0,0 or +DMOGETCURRCH:15,ANA,405000000,405000000,1,1
|
||||||
|
DMOGETCHFREQ | Replies with the channel parameters for the given channel number AT+DMOGETCHFREQ=<channel>
|
||||||
|
DMOGETLNAOFFS | Returns the LNA offset
|
||||||
|
DMOGETRSSIPARAM | Returns the RSSI offset
|
||||||
|
DMOGETCHIPID | A unique ID for the A6 processor
|
||||||
|
DMOGETGRPADDR | Returns the DMR contact for the current channel
|
||||||
|
DMOGETWLKTLKID | Returns the DMR radio ID of the radio
|
||||||
|
DMORDRSSI | Returns the current receive signal strength in dBm
|
||||||
|
DMOTESTMODE |
|
||||||
|
DMOSETGROUP | Sets all of the frequency settings for the given channel
|
||||||
|
DMOSETCTCDCS | Sets the CTCSS or DCS mode for the current channel AT+DMOSETCTCDCS=<mode>,<ctcDcsType>,<ctcDcsNum>,<ctcDcsInvert>
|
||||||
|
DMOSETCTCDCSGAIN | Sets the audio gain for the CTCSS or DCS tone AT+DMOSETCTCDCSGAIN=<ctcDcsType>,<gain>
|
||||||
|
DMOSETTAILFREQ | Sets the tail frequency(550-2000 in tenths of Hz) for the current channel AT+DMOSETTAILFREQ=<tailFreq>
|
||||||
|
DMOAUTOPOWCONTR | Sets the automatic power control mode for the current channel AT+DMOAUTOPOWCONTR=<mode>
|
||||||
|
DMOSETMIC | Sets the microphone gain for the current channel AT+DMOSETMIC=<gain>
|
||||||
|
DMOSETVOLUME | Sets the volume for the current channel AT+DMOSETVOLUME=<volume>
|
||||||
|
DMOSETVOLUMELEVEL | Sets the volume level for the current channel AT+DMOSETVOLUMELEVEL=<level>
|
||||||
|
DMOSETVOX | Sets the VOX level for the current channel AT+DMOSETVOX=<level>
|
||||||
|
DMOSENDDATA | ¯\\_(ツ)_/¯
|
||||||
|
DMOSETAPC |
|
||||||
|
DMOSETSN |
|
||||||
|
DMORESETSN |
|
||||||
|
DMOSETRSSIPARAM |
|
||||||
|
DMOSETLNAOFFS |
|
||||||
|
DMOSAVEPARAM |
|
||||||
|
DMOERASEPARAM |
|
||||||
|
DMOSETSMARTLO |
|
||||||
|
DMOGETRXBER | Returns the received bit error rate
|
||||||
|
DMOCHSWITCH | Changes the current channel AT+DMOCHSWITCH=<channel>
|
||||||
|
DMOSETCHFREQ | Changes the channel parameters for the given channel number AT+DMOSETCHFREQ=<txfreq>,<rxfreq> where the frequencies are like this 438.8001. Must have this number of digits to work
|
||||||
|
DMOSETCHPOWER |
|
||||||
|
DMOSETPOWER | Sets the power level for the current channel AT+DMOSETPOWER=<level> where level is 0-2
|
||||||
|
DMOSETPWRSAVELV | Sets the FM power save level for the current channel if no audio, the radio will stop transmitting AT+DMOSETPWRSAVELV=<level>
|
||||||
|
DMOSETWLKTLKID | Set the DMR Radio ID
|
||||||
|
DMOSETGRPADDR | Set the DMR group address
|
||||||
|
DMOSETFREQBANDNUM | Set the number of frequency bands AT+DMOSETFREQBANDNUM=<number> where number is less than 16
|
||||||
|
DMOGETFREQBANDNUM | Get the current number of frequency bands
|
||||||
|
DMOSETFREQBANDVAL | Set the values of the frequency bands
|
||||||
|
DMOGETFREQBANDVAL | Get the values of the frequency bands
|
||||||
|
DMOAMPCTRLTEST | Sets an amplifier gain and tests the amplifier AT+DMOAMPCTRLTEST=<gain> gain is 0-128
|
||||||
|
DMODIGPWRSAVECTRL |
|
||||||
|
DMOSETLCHEADERCNT | Sets the number of LC headers sent AT+DMOSETLCHEADERCNT=<count>
|
||||||
|
DMOGRPCALLREQ |
|
||||||
|
DMOINDCALLREQ |
|
||||||
|
DMOALLCALLREQ |
|
||||||
|
DMOALERTCSBKREQ | Sends an alert call request to the radio AT+DMOALERTCSBKREQ=<dmrid>
|
||||||
|
DMOCHECKCSBKREQ | Sends a check call request to the radio AT+DMOCHECKCSBKREQ=<dmrid>
|
||||||
|
DMOALARMCSBKREQ | Sends an alarm call request to the radio AT+DMOALARMCSBKREQ=<dmrid>
|
||||||
|
DMORESPCSBKREQ | Sends a response call request to the radio AT+DMORESPCSBKREQ=<dmrid>
|
||||||
|
SETIMAGEREJEMODE |
|
||||||
|
SETIMAGEREJE |
|
||||||
|
SETEQUALIZER |
|
||||||
|
SETCOMPANDER |
|
||||||
|
SETSCRAMBLER |
|
||||||
|
SETTONELIST |
|
||||||
|
SETDTMF |
|
||||||
|
RESTOREFACTORYSETTING | Restores the original settings. The password for this is 778123456
|
||||||
|
BERTEST |
|
||||||
|
SETCOLORCODE | Set the color code for the current channel AT+SETCOLORCODE=<colorCode>
|
||||||
|
GETGPADCVALUE | Get the current value of the GPADC channel(0-1) AT+GETGPADCVALUE=<channel>
|
||||||
|
SETGPADCCHAN | Set the GPADC channel enable AT+SETGPADCCHAN=<channel>,<enable> channel=0,1 enable=0,1
|
||||||
|
GETGPIOVALUE | Get the current value of the GPIO channel(0-32) AT+GETGPIOVALUE=<channel>
|
||||||
|
SETGPIOVALUE | Set the GPIO channel value AT+SETGPIOVALUE=<channel>,<value> channel=0-32 value=0,1
|
||||||
|
SETGPIODIR | Set the direction of the GPIO channel(0-32) AT+SETGPIODIR=<channel>,<direction> channel=0-32 direction=0,1
|
||||||
|
GETPOWERAMPGAIN | Get the power amplifier gain values AT+GETPOWERAMPGAIN=<band> returns <band>,<digLow>,<digMed>,<digHigh>,<anaLow>,<anaMed>,<anaHigh>
|
||||||
|
SETPOWERAMPGAIN | Sets the power amplifier gain
|
||||||
|
GETPOWERAPCGAIN | Get the automatic power control gain values AT+GETPOWERAPCGAIN=<band> returns <band>,<digLow>,<digMed>,<digHigh>,<anaLow>,<anaMed>,<anaHigh>
|
||||||
|
SETPOWERAPCGAIN | Sets the automatic power control gain
|
||||||
|
SETPOWERAPCSW | Sets the automatic power control switch ?
|
||||||
|
GETEFLTRXADJ | Returns the RX filter adjustment for a band AT+GETEFLTRXADJ=<band> where the value is <band>,<dac>,<gain>
|
||||||
|
SETEFLTRXADJ | Sets the RX filter adjustment for a band AT+SETEFLTRXADJ=<band>,<dac>,<gain>
|
||||||
|
DMOGETVAFC | Returns the current Automatic Frequency Correction value
|
||||||
|
DMOSETVAFC | Sets the Automatic Frequency Correction value
|
||||||
|
DMOPTT | Sets the PTT state for the current channel AT+DMOPTT=<state> where state is 0 for off and 1 for on
|
||||||
|
GETFREQERR | Get the TCXO frequency error in 10s of Hz
|
||||||
|
DMOFREQERR | Set the TCXO frequency error in 10s of Hz
|
||||||
|
GETBANDBYFREQ | Returns the band number for the given frequency AT+GETBANDBYFREQ=<freq>
|
||||||
|
DMOGETFREQBANDVAL | Returns the cut offs for the different frequency bands
|
||||||
|
DMOGETFREQBANDNUM | Returns the number of frequency bands in use/defined AT+DMOGETFREQBANDNUM
|
||||||
|
DMOSETFREQBANDNUM | Can be used to change the number of defined frequency bands AT+DMOSETFREQBANDNUM=2
|
||||||
|
DMRSIGNATURE | Unknown command. The signature looks something like this: AT+DMRSIGNATURE/aa,aa,aa,... with a total of 30 bytes worth of numbers Returns something that says AUTHSETKEYS
|
||||||
|
WRITECFS | Does something similar to the DMRSIGNATURE command
|
||||||
|
GETINCALLID | Returns the incoming call ID, <calltype>,<from>,<to> where call type is 0,1,2 corresponding to individual, group and all calls and from and to are DMR IDs
|
||||||
|
SETALERTTONE |
|
||||||
|
SETBATTDETRANGADC |
|
||||||
|
SETBATTADJ |
|
||||||
|
SETPROFILERAMPSSHIFT |
|
||||||
|
SETPROFILERAMPSDWPOS |
|
||||||
|
GETPROFILERAMPS |
|
||||||
|
SETRAMPINGTABLE |
|
||||||
|
GETAPC_FINEVALUE |
|
||||||
|
SETAPC_FINEVALUE |
|
||||||
|
DMR_ADJTXSYMDEV |
|
||||||
|
DMR_GETTXSYMDEV |
|
||||||
|
SETSTANDARD | Sets the testing standard for the radio AT+SETSTANDARD=<standard> where standard is 0-4. Completely resets the radio's program!
|
||||||
|
SETSQLEVEL |
|
||||||
|
SETSQADJUST |
|
||||||
|
SETSQMODE |
|
||||||
|
SETTXIQGAIN |
|
||||||
|
GETSQLEVEL |
|
||||||
|
SETMODULE |
|
||||||
|
SETCHANNELKNOBMODE |
|
||||||
|
FGU_AFC_ADJ |
|
||||||
|
FGU_AFC_GET |
|
||||||
|
FGU_VCON_BIAS |
|
||||||
|
FGU_VCON_BIAS_GET |
|
||||||
|
_FGU_TUNE_GET |
|
||||||
|
FGU_TUNE_GET |
|
||||||
|
|
||||||
@@ -0,0 +1,48 @@
|
|||||||
|
Command | Description
|
||||||
|
--------|------------
|
||||||
|
0000 | SendBaseInfo
|
||||||
|
0001 | Unknown
|
||||||
|
0002 | Unknown Writes to NVRAM!
|
||||||
|
0003 | SetEmbInfo
|
||||||
|
0004 | GetEmbInfoAll
|
||||||
|
000f | SetBaseSetting
|
||||||
|
0010 | SendBaseSettingsBasicSet
|
||||||
|
0011 | SetChanInfo
|
||||||
|
0012 | GetChanInfo
|
||||||
|
0013 | SetChanName
|
||||||
|
0014 | GetChanName
|
||||||
|
0015 | SetTalkieName
|
||||||
|
0016 | Unknown
|
||||||
|
0017 | SetIndividualCallCnt
|
||||||
|
0018 | SetIndividualCallInfo
|
||||||
|
0019 | SendIndividualCallCnt
|
||||||
|
001a | SendIndividualCallInfo
|
||||||
|
001b | SendCmdResponse ?
|
||||||
|
001c | SetGroupCallInfo
|
||||||
|
001d | Unknown
|
||||||
|
001e | GetGroupCallInfo
|
||||||
|
001e | SendCmdResponse ?
|
||||||
|
0020 | Unknown
|
||||||
|
0021 | SetShortcutMsgInfo ?
|
||||||
|
0022 | SetShortcutCnt
|
||||||
|
0023 | SetShortcutMsgInfo ?
|
||||||
|
0024 | GetShortcutMsgInfo
|
||||||
|
0025 | SetKeyFunc
|
||||||
|
0026 | GetKeyFunc
|
||||||
|
0027 | SetScanlist
|
||||||
|
0028 | GetScanlistCnt
|
||||||
|
002a | GetScanlist
|
||||||
|
002b | Checks the programming password
|
||||||
|
0100 | Unknown
|
||||||
|
0101 | Unknown
|
||||||
|
0102 | Unknown
|
||||||
|
0104 | Unknown
|
||||||
|
0a00 | @CpsByHostPort_ConcatenateHdr CPS_Hdr[StartAddr:0x82004810 len:772] 0x88105948
|
||||||
|
0a01 | Unknown
|
||||||
|
0a02 | Unknown
|
||||||
|
0a03 | Unknown
|
||||||
|
0a04 | Unknown but same as 0a07
|
||||||
|
0a05 | Unknown
|
||||||
|
0a06 | Unknown
|
||||||
|
0a07 | Unknown but same as 0a04
|
||||||
|
0a08 | PROT_SendDataFrame Seems to echo the sent packet back
|
||||||
@@ -0,0 +1,33 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
""" AT Commander for AUCTUS based radios
|
||||||
|
|
||||||
|
Allow for communication with AUCTUS A6 radios to their serial
|
||||||
|
interface through the debug interface. Commands can either be "AT"
|
||||||
|
commands or "CPS" commands. Both styles will work.
|
||||||
|
|
||||||
|
"""
|
||||||
|
|
||||||
|
from a6 import get_chan_info, SerialIO
|
||||||
|
|
||||||
|
__author__ = "jhart99"
|
||||||
|
__license__ = "MIT"
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
import argparse
|
||||||
|
parser = argparse.ArgumentParser(description='Auctus A6 ATECPS commander')
|
||||||
|
parser.add_argument('-p', '--port', default='/dev/ttyUSB0',
|
||||||
|
type=str, help='serial port')
|
||||||
|
parser.add_argument('-b','--baudrate', default=921600,
|
||||||
|
type=int, help='baud rate')
|
||||||
|
parser.add_argument('-v','--verbosity', default=0, action='count',
|
||||||
|
help='print sent and received frames to stderr for debugging')
|
||||||
|
parser.add_argument('-V', '--version', action='version',
|
||||||
|
version='%(prog)s 0.0.1',
|
||||||
|
help='display version information and exit')
|
||||||
|
parser.add_argument('channel', type=int, help='channel number')
|
||||||
|
args = parser.parse_args()
|
||||||
|
|
||||||
|
|
||||||
|
uart = SerialIO(args.port, args.baudrate, args.verbosity)
|
||||||
|
get_chan_info(args.channel)
|
||||||
@@ -0,0 +1,46 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
""" Frequency offset fix for AUCTUS A6 based radios
|
||||||
|
|
||||||
|
This program takes a measured frequency and a desired frequency and
|
||||||
|
then reads a TCXO error programmed into the radio and sets a new TCXO
|
||||||
|
offset in the radio. The radio needs to have its channel changed
|
||||||
|
after for the new setting to take effect.
|
||||||
|
|
||||||
|
This fixes the high BER seen on some radios like my GOCOM GD900 which
|
||||||
|
had a 800 Hz offset from the factory which while within spec was
|
||||||
|
outside what my poor MMDVM board could tolerate.
|
||||||
|
|
||||||
|
"""
|
||||||
|
|
||||||
|
from a6 import SerialIO, get_freq_err, set_freq_err
|
||||||
|
|
||||||
|
__author__ = "jhart99"
|
||||||
|
__license__ = "MIT"
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
import argparse
|
||||||
|
parser = argparse.ArgumentParser(description='Auctus A6 Frequency Error Fixer')
|
||||||
|
parser.add_argument('-p', '--port', default='/dev/ttyUSB0',
|
||||||
|
type=str, help='serial port')
|
||||||
|
parser.add_argument('-b','--baudrate', default=921600,
|
||||||
|
type=int, help='baud rate')
|
||||||
|
parser.add_argument('-v','--verbosity', default=0, action='count',
|
||||||
|
help='print sent and received frames to stderr for debugging')
|
||||||
|
parser.add_argument('-V', '--version', action='version',
|
||||||
|
version='%(prog)s 0.0.1',
|
||||||
|
help='display version information and exit')
|
||||||
|
parser.add_argument('current', type=int,
|
||||||
|
help='the measured frequency the radio is currently transmitting in Hz')
|
||||||
|
parser.add_argument('target', type=int,
|
||||||
|
help='the programmed frequency in the radio in Hz')
|
||||||
|
args = parser.parse_args()
|
||||||
|
|
||||||
|
uart = SerialIO(args.port, args.baudrate, args.verbosity)
|
||||||
|
|
||||||
|
delta = args.target - args.current
|
||||||
|
curerr = get_freq_err()
|
||||||
|
target = curerr + delta
|
||||||
|
if abs(target) > 2500:
|
||||||
|
raise ValueError("Desired offset exceeds maximum of 2500 Hz")
|
||||||
|
set_freq_err(int((target + 2500)/10))
|
||||||
@@ -0,0 +1,41 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
""" AT Commander for AUCTUS based radios
|
||||||
|
|
||||||
|
Allow for communication with AUCTUS A6 radios to their serial
|
||||||
|
interface through the debug interface. Commands can either be "AT"
|
||||||
|
commands or "CPS" commands. Both styles will work.
|
||||||
|
|
||||||
|
"""
|
||||||
|
|
||||||
|
import serial
|
||||||
|
import sys
|
||||||
|
from a6 import read_mem_range, SerialIO
|
||||||
|
|
||||||
|
|
||||||
|
__author__ = "jhart99"
|
||||||
|
__license__ = "MIT"
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
import argparse
|
||||||
|
parser = argparse.ArgumentParser(description='Auctus A6 dumper')
|
||||||
|
parser.add_argument('--begin', type=lambda x: int(x,0),
|
||||||
|
help='begin address default 0x82000000',
|
||||||
|
default=0x82000000)
|
||||||
|
parser.add_argument('--end', type=lambda x: int(x,0),
|
||||||
|
help='end address default 0x8200ff00',
|
||||||
|
default=0x8200ff00)
|
||||||
|
parser.add_argument('-p', '--port', default='/dev/ttyUSB0',
|
||||||
|
type=str, help='serial port')
|
||||||
|
parser.add_argument('-b','--baudrate', default=921600,
|
||||||
|
type=int, help='baud rate')
|
||||||
|
parser.add_argument('-v','--verbosity', default=0, action='count',
|
||||||
|
help='print sent and received frames to stderr for debugging')
|
||||||
|
parser.add_argument('-V', '--version', action='version',
|
||||||
|
version='%(prog)s 0.0.1',
|
||||||
|
help='display version information and exit')
|
||||||
|
args = parser.parse_args()
|
||||||
|
|
||||||
|
uart = SerialIO(args.port, args.baudrate, args.verbosity)
|
||||||
|
data = read_mem_range(args.begin, args.end)
|
||||||
|
sys.stdout.buffer.write(data)
|
||||||
@@ -0,0 +1,30 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
""" Reboot and freeze
|
||||||
|
|
||||||
|
Reset the AUCTUS A6 processor and freeze execution. This allows for
|
||||||
|
uninterupted dumping of the normally problematic BCPU rom.
|
||||||
|
|
||||||
|
"""
|
||||||
|
|
||||||
|
from a6 import SerialIO, reboot_and_freeze
|
||||||
|
|
||||||
|
__author__ = "jhart99"
|
||||||
|
__license__ = "MIT"
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
import argparse
|
||||||
|
parser = argparse.ArgumentParser(description='Auctus A6 reboot and freeze')
|
||||||
|
parser.add_argument('-p', '--port', default='/dev/ttyUSB0',
|
||||||
|
type=str, help='serial port')
|
||||||
|
parser.add_argument('-b','--baudrate', default=921600,
|
||||||
|
type=int, help='baud rate')
|
||||||
|
parser.add_argument('-v','--verbosity', default=0, action='count',
|
||||||
|
help='print sent and received frames to stderr for debugging')
|
||||||
|
parser.add_argument('-V', '--version', action='version',
|
||||||
|
version='%(prog)s 0.0.1',
|
||||||
|
help='display version information and exit')
|
||||||
|
args = parser.parse_args()
|
||||||
|
|
||||||
|
uart = SerialIO(args.port, args.baudrate, args.verbosity)
|
||||||
|
uart.write(reboot_and_freeze())
|
||||||
Двоичные данные
Двоичный файл не отображается.
|
После Ширина: | Высота: | Размер: 1.9 MiB |
@@ -0,0 +1,117 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""RT-4D DMR-модуль (HF6853 / AUCTUS A6) — починка частотного смещения TX.
|
||||||
|
|
||||||
|
Модуль RT-4D — это одночиповый Auctus A6 (RDA8809), у которого частотная
|
||||||
|
калибровка TCXO хранится в NVRAM и правится СКРЫТЫМ ATE-интерфейсом:
|
||||||
|
AT+GETFREQERR -> прочитать N (в десятках Гц)
|
||||||
|
AT+DMOFREQERR=<N> -> записать N; offset[Гц] = -2500 + 10*N, N=0..500
|
||||||
|
После записи СМЕНИТЬ КАНАЛ на рации, чтобы применилось.
|
||||||
|
|
||||||
|
Наш случай ([[rt4d-dmr-tx-broken]]): TX смещена ~1100 Гц (в пределах ±2500),
|
||||||
|
поэтому один AT+DMOFREQERR должен вернуть декодирование хотспотом.
|
||||||
|
|
||||||
|
Реверс/библиотека — jhart99/a6tools (копия в research/a6tools). См.
|
||||||
|
docs/hf6853-auctus-a6.md.
|
||||||
|
|
||||||
|
БЕЗОПАСНОСТЬ:
|
||||||
|
* По умолчанию РЕЖИМ probe/read (--probe / --read). Запись только с --write И --yes.
|
||||||
|
* Точное значение требует ИЗМЕРЕНИЯ реальной TX по SDR (RTL-SDR).
|
||||||
|
* Рация боевая — вслепую не писать.
|
||||||
|
|
||||||
|
Транспорт: два варианта COM-доступа —
|
||||||
|
--port /dev/ttyUSBx прямой pyserial (если модуль виден напрямую)
|
||||||
|
--bridge через powershell-мост WSL (как tools/fm100b.py) — TODO,
|
||||||
|
заготовка ниже; сперва проверить probe напрямую.
|
||||||
|
|
||||||
|
Скорость Auctus по умолчанию 921600 8N1 XON/XOFF. Режим рации: «Обновление DMR
|
||||||
|
чипа» (вкл. с зажатой MENH/MENU) — прозрачный мост PC<->модуль.
|
||||||
|
|
||||||
|
Примеры:
|
||||||
|
python3 a6_freqfix.py --port /dev/ttyUSB0 --probe
|
||||||
|
python3 a6_freqfix.py --port /dev/ttyUSB0 --read
|
||||||
|
# измерили: рация реально шлёт 438.799100 вместо 438.800000 (=-900 Гц)
|
||||||
|
python3 a6_freqfix.py --port /dev/ttyUSB0 --measured 438799100 --nominal 438800000 --write --yes
|
||||||
|
"""
|
||||||
|
import sys, os, time, argparse
|
||||||
|
|
||||||
|
# использовать вендоренную библиотеку a6tools
|
||||||
|
HERE = os.path.dirname(os.path.abspath(__file__))
|
||||||
|
sys.path.insert(0, os.path.join(HERE, "..", "research", "a6tools"))
|
||||||
|
|
||||||
|
|
||||||
|
def load_a6(port, baud, verbosity):
|
||||||
|
from a6.serialio import (SerialIO, send_ate_command, atecps_resp_read,
|
||||||
|
send_uart_setup, get_freq_err, set_freq_err)
|
||||||
|
from a6.a6commands import set_uart_to_host
|
||||||
|
from a6.serialio import write_flush_pause
|
||||||
|
SerialIO(port, baud, verbosity) # singleton init
|
||||||
|
return dict(send_ate=send_ate_command, resp=atecps_resp_read,
|
||||||
|
setup=send_uart_setup, geterr=get_freq_err, seterr=set_freq_err,
|
||||||
|
to_host=set_uart_to_host, wfp=write_flush_pause)
|
||||||
|
|
||||||
|
|
||||||
|
def probe(a6):
|
||||||
|
"""Проверить, отвечает ли модуль на RDA-debug (knock). content==0x80 => ок."""
|
||||||
|
print("[probe] knock RDA-debug (read reg 0x03)…")
|
||||||
|
ok = a6["setup"]()
|
||||||
|
print(" ответ:", "МОДУЛЬ ОТВЕЧАЕТ (debug активен)" if ok else "нет ответа")
|
||||||
|
if not ok:
|
||||||
|
print(" -> модуль не в RDA-debug. Проверь режим рации / скорость (пробуй --baud 115200).")
|
||||||
|
return ok
|
||||||
|
|
||||||
|
|
||||||
|
def read_err(a6):
|
||||||
|
a6["send_ate"]("AT+DMOCONNECT")
|
||||||
|
n = a6["geterr"]()
|
||||||
|
print(f"[read] GETFREQERR N (в получаемом виде) -> offset={n} Гц")
|
||||||
|
return n
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
ap = argparse.ArgumentParser(description="HF6853/A6 TX freq-offset fix")
|
||||||
|
ap.add_argument("--port", required=True, help="COM/tty (напр. /dev/ttyUSB0)")
|
||||||
|
ap.add_argument("--baud", type=int, default=921600)
|
||||||
|
ap.add_argument("-v", "--verbose", action="count", default=1)
|
||||||
|
ap.add_argument("--probe", action="store_true", help="только проверить связь")
|
||||||
|
ap.add_argument("--read", action="store_true", help="прочитать текущий freqerr")
|
||||||
|
ap.add_argument("--measured", type=int, help="реально измеренная TX-частота, Гц (по SDR)")
|
||||||
|
ap.add_argument("--nominal", type=int, help="номинальная (запрограммированная) частота, Гц")
|
||||||
|
ap.add_argument("--write", action="store_true", help="применить новое значение")
|
||||||
|
ap.add_argument("--yes", action="store_true", help="подтвердить запись (обязателен с --write)")
|
||||||
|
a = ap.parse_args()
|
||||||
|
|
||||||
|
a6 = load_a6(a.port, a.baud, a.verbose)
|
||||||
|
|
||||||
|
if not probe(a6):
|
||||||
|
if a.probe:
|
||||||
|
return
|
||||||
|
print("Прерываю: без связи писать нельзя.")
|
||||||
|
return
|
||||||
|
if a.probe:
|
||||||
|
return
|
||||||
|
|
||||||
|
cur = read_err(a6)
|
||||||
|
if a.read and not a.write:
|
||||||
|
return
|
||||||
|
|
||||||
|
if a.write:
|
||||||
|
if a.measured is None or a.nominal is None:
|
||||||
|
print("Для --write нужны --measured и --nominal (по SDR).")
|
||||||
|
return
|
||||||
|
delta = a.nominal - a.measured # сколько добавить к текущему offset
|
||||||
|
target = cur + delta
|
||||||
|
if abs(target) > 2500:
|
||||||
|
print(f"Целевой offset {target} Гц вне ±2500 — так фиксить нельзя (нужна другая калибровка).")
|
||||||
|
return
|
||||||
|
N = int((target + 2500) / 10)
|
||||||
|
print(f"[plan] cur={cur} Гц, delta={delta} Гц, target={target} Гц -> N={N}")
|
||||||
|
if not a.yes:
|
||||||
|
print("Добавь --yes чтобы записать. (ничего не записано)")
|
||||||
|
return
|
||||||
|
a6["send_ate"]("AT+DMOCONNECT")
|
||||||
|
a6["seterr"](N)
|
||||||
|
print(f"[write] AT+DMOFREQERR={N} отправлено. ТЕПЕРЬ СМЕНИ КАНАЛ на рации и проверь BER.")
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
Ссылка в новой задаче
Block a user