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
Этот коммит содержится в:
2026-07-24 03:10:24 +09:00
co-authored by Claude Opus 4.8
родитель 6d49ba3cba
Коммит c10ab3c6f1
20 изменённых файлов: 1373 добавлений и 1 удалений
+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)).
+8
Просмотреть файл
@@ -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, при передаче рации
+104
Просмотреть файл
@@ -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-модуля.
+20
Просмотреть файл
@@ -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.
+35
Просмотреть файл
@@ -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
+26
Просмотреть файл
@@ -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
+168
Просмотреть файл
@@ -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)
+14
Просмотреть файл
@@ -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)
+34
Просмотреть файл
@@ -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)
+121
Просмотреть файл
@@ -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)
+365
Просмотреть файл
@@ -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)
+42
Просмотреть файл
@@ -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'))
+120
Просмотреть файл
@@ -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 |
+48
Просмотреть файл
@@ -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
+33
Просмотреть файл
@@ -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)
+46
Просмотреть файл
@@ -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))
+41
Просмотреть файл
@@ -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)
+30
Просмотреть файл
@@ -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

+117
Просмотреть файл
@@ -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()