From 01939be3e1cffc3e08ce3b9e7ee7b1db885dfeee Mon Sep 17 00:00:00 2001 From: Hannes Matuschek Date: Thu, 15 Dec 2022 11:30:01 +0100 Subject: [PATCH 01/16] Added BTECH DMR-6X2UV device. --- doc/code/dmr6x2uv_generalsettings.txt | 134 +++++++++++++++ doc/fig/dmr6x2uv.jpg | Bin 0 -> 110747 bytes lib/CMakeLists.txt | 6 +- lib/anytone_interface.cc | 2 +- lib/d868uv.cc | 1 - lib/dmr6x2uv.cc | 104 ++++++++++++ lib/dmr6x2uv.hh | 38 +++++ lib/dmr6x2uv_codeplug.cc | 9 ++ lib/dmr6x2uv_codeplug.hh | 225 ++++++++++++++++++++++++++ lib/dmr6x2uv_limits.cc | 169 +++++++++++++++++++ lib/dmr6x2uv_limits.hh | 19 +++ lib/radioinfo.cc | 6 +- lib/radioinfo.hh | 2 +- 13 files changed, 708 insertions(+), 7 deletions(-) create mode 100644 doc/code/dmr6x2uv_generalsettings.txt create mode 100644 doc/fig/dmr6x2uv.jpg create mode 100644 lib/dmr6x2uv.cc create mode 100644 lib/dmr6x2uv.hh create mode 100644 lib/dmr6x2uv_codeplug.cc create mode 100644 lib/dmr6x2uv_codeplug.hh create mode 100644 lib/dmr6x2uv_limits.cc create mode 100644 lib/dmr6x2uv_limits.hh diff --git a/doc/code/dmr6x2uv_generalsettings.txt b/doc/code/dmr6x2uv_generalsettings.txt new file mode 100644 index 00000000..ab92df0d --- /dev/null +++ b/doc/code/dmr6x2uv_generalsettings.txt @@ -0,0 +1,134 @@ + 7 6 5 4 3 2 1 0 7 6 5 4 3 2 1 0 7 6 5 4 3 2 1 0 7 6 5 4 3 2 1 0 + +---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+ +00 | Enable key tone | Display mode | Enable automatic key lock | Automatic shutdown time | + +---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+ +04 | Transmit timeout in 30s | Language | Boot display | Enable boot password | + +---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+ +08 | VFO frequency step | Squelch level VFO A | Squelch level VFO B | Power save mode | + +---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+ +0c | VOX sensitivity | VOX delay in 100+500*n ms | VFO scan type | MIC gain | + +---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+ +10 | PF1 short press function | PF2 short press function | PF3 short press function | P1 short press function | + +---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+ +14 | P2 short press function | Work mode A | Work mode B | STE Type | + +---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+ +18 | STE freq. no signal | Group call hang time in sec | Private call hang time in sec | Prewave time in 20ms | + +---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+ +1c | Wake head period in 20ms | WFM channel index | WFM VFO Mode enable | Work mode MEM zone A | + +---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+ +20 | Work mode MEM zone B | Unused set to 0x00 | Enable recording | DTMF duration | + +---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+ +24 | Man down enable | Unused set to 0x00 | Display brightness | Backlight duration | + +---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+ +28 | Enable GPS | Enable SMS alert | Unused set to 0x00 | WFM monitor enable | + +---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+ +2c | Work mode main channel set | Enable sub channel | TBST frquency | Enable call alert | + +---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+ +30 | GPS Time zone | Talk permit tone | Digital call reset tone | VOX source | + +---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+ +34 | Enable pro mode | Unused set to 0x00 | Idle channel tone | Menu exit time | + +---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+ +38 | Filter own ID enable | Startup tone | Enable call end prompt | Max speaker volume | + +---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+ +3c | Remote stun/kill enable | Unused set to 0x00 | Remote monitor enable | GPS RX positions | + +---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+ +40 | Unknown | PF1 long press function | PF2 long press function | PF3 long press function | + +---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+ +44 | P1 long press function | P2 long press function | Long press duration | Enable Volume change prompt | + +---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+ +48 | Auto repeater A direction | Digital monitor slot | Digital monitor color code | Digital monitor match ID | + +---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+ +4c | Digital monitor hold slot | Display later caller | Unknown | Man down delay in sec | + +---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+ +50 | Analog call hold in seconds | Display clock | Max head phone volume | Enable GPS range message | + +---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+ +54 | Unknown settings | Enable enhanced audio | + +---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+ +58 | VFO Scan UHF minimum frequency in 10Hz, 8-digit BCD, little endian | + +---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+ +5c | VFO Scan UHF maximum frequency in 10Hz, 8-digit BCD, little endian | + +---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+ +60 | VFO Scan VHF minimum frequency in 10Hz, 8-digit BCD, little endian | + +---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+ +64 | VFO Scan VHF maximum frequency in 10Hz, 8-digit BCD, little endian | + +---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+ +68 | Auto rep. offset index UHF | Auto rep. offset index VHF | Unknown ... + +---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+ +6c | | Maintain call channel | Priority zone A index | + +---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+ +70 | Priority zone B index | Unused set to 0x00 | Call tone frequency 1 in Hz, little endian | + +---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+ +74 | Call tone frequency 2 in Hz, little endian | Call tone frequency 3 in Hz, little endian | + +---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+ +78 | Call tone frequency 4 in Hz, little endian | Call tone frequency 5 in Hz, little endian | + +---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+ +7c | Call tone duration 1 in ms, little endian | Call tone duration 2 in ms, little endian | + +---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+ +80 | Call tone duration 3 in ms, little endian | Call tone duration 4 in ms, little endian | + +---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+ +84 | Call tone duration 5 in ms, little endian | Idle tone frequency 1 in Hz, little endian | + +---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+ +88 | Idle tone frequency 2 in Hz, little endian | Idle tone frequency 3 in Hz, little endian | + +---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+ +8c | Idle tone frequency 4 in Hz, little endian | Idle tone frequency 5 in Hz, little endian | + +---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+ +90 | Idle tone duration 1 in ms, little endian | Idle tone duration 2 in ms, little endian | + +---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+ +94 | Idle tone duration 3 in ms, little endian | Idle tone duration 4 in ms, little endian | + +---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+ +98 | Idle tone duration 5 in ms, little endian | Reset tone frequency 1 in Hz, little endian | + +---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+ +9c | Reset tone frequency 2 in Hz, little endian | Reset tone frequency 3 in Hz, little endian | + +---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+ +a0 | Reset tone frequency 4 in Hz, little endian | Reset tone frequency 5 in Hz, little endian | + +---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+ +a4 | Reset tone duration 1 in ms, little endian | Reset tone duration 2 in ms, little endian | + +---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+ +a8 | Reset tone duration 3 in ms, little endian | Reset tone duration 4 in ms, little endian | + +---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+ +ac | Reset tone duration 5 in ms, little endian | Record delay in 200ms | Call display mode | + +---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+ +b0 | Unknown, set to 0x01 | Simplex Repeater 0=Off, 1=On | Unknown set to 0x05 | Sp. dur. RX. Simpl. Rep. | + +---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+ +b4 | Unknown, set to 0x00 | GPS ranging interval in sec | Unknown set to 0x0f | SimplRepSlot 0=TS1 1=TS2 2=CH | + +---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+ +b8 | Display channel number | Display contact | Auto roaming period in min | Key tone level | + +---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+ +bc | Callsign color | GPS unit | 0 0 0 |KLF|LSK| 0 |LKB|LKN| Auto roam delay in seconds | + +---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+ +c0 | Standby text color | Standby image color | Show last heard | SMS format | + +---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+ +c4 | Auto repeater VHF min frequency in 10Hz, 8-digit BCD, big endian | + +---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+ +c8 | Auto repeater VHF max frequency in 10Hz, 8-digit BCD, big endian | + +---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+ +cc | Auto repeater UHF min frequency in 10Hz, 8-digit BCD, big endian | + +---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+ +d0 | Auto repeater UHF max frequency in 10Hz, 8-digit BCD, big endian | + +---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+ +d4 | Auto rep. VFO B direction | Unknown | Unused set to 0x00 | Boot channel enable | + +---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+ +d8 | VFO A default zone index | VFO B default zone index | VFO A default channel index | VFO B default channel index | + +---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+ +dc | Default roaming zone index | Repeater range check enable | Repeater check interval in 5s | RepCheck reconnect count | + +---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+ +e0 | Roam start condition | Backlight duration TX in sec | Separate display | Keep last caller | + +---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+ +e4 | Channel name color | RepCheck notification | Backlight duration RX in sec | Roaming enable | + +---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+ +e8 | Unused set to 0x00 | Mute delay in minutes-1 | RepCheck num notification | Startup GPS test enable | + +---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+ +ec | Startup reset enable | Unused set to 0x00 | + +---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+ +f0 | Unknown settings ... + +---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+ +fc ... | + +---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+ + +Field description: + - SRP: Simplex repeater + - KLF: Key lock forced enable + - LSK: Lock side key + - LKB: Keyboard lock + - LKN: Knob lock + diff --git a/doc/fig/dmr6x2uv.jpg b/doc/fig/dmr6x2uv.jpg new file mode 100644 index 0000000000000000000000000000000000000000..ce173f6af57eb0e722ae4103c313fffceb9b60f2 GIT binary patch literal 110747 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

_?Vo9Hy5H6yuM(73C6 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 - RadioInfo(RadioInfo::DMR6X2, "dmr6x2", "DMR-6X2", "BTech", AnytoneInterface::interfaceInfo()), RadioInfo(RadioInfo::D868UV, "d868uv", "AT-D868UV", "AnyTone", AnytoneInterface::interfaceInfo()) }); } diff --git a/lib/dmr6x2uv.cc b/lib/dmr6x2uv.cc new file mode 100644 index 00000000..e946bb75 --- /dev/null +++ b/lib/dmr6x2uv.cc @@ -0,0 +1,104 @@ +#include "dmr6x2uv.hh" +#include "dmr6x2uv_codeplug.hh" +#include "dmr6x2uv_limits.hh" +#include "d868uv_callsigndb.hh" +#include "logger.hh" + +DMR6X2UV::DMR6X2UV(AnytoneInterface *device, QObject *parent) + : AnytoneRadio("BTECH DMR-6X2UV", device, parent), _limits(nullptr) +{ + _codeplug = new DMR6X2UVCodeplug(this); + _codeplug->clear(); + _callsigns = new D868UVCallsignDB(this); + + // Get device info and determine supported TX frequency bands + AnytoneInterface::RadioVariant info; _dev->getInfo(info); + switch (info.bands) { + case 0x00: + case 0x01: + _limits = new DMR6X2UVLimits({ {136., 174.}, {400., 480.} }, + { {136., 174.}, {400., 480.} }, info.version, this); + break; + case 0x02: + _limits = new DMR6X2UVLimits({ {136., 174.}, {430., 440.} }, + { {136., 174.}, {430., 440.} }, info.version, this); + break; + case 0x03: + _limits = new DMR6X2UVLimits({ {136., 174.}, {400., 480.} }, + { {144., 146.}, {430., 440.} }, info.version, this); + break; + case 0x04: + _limits = new DMR6X2UVLimits({ {144., 146.}, {434., 438.} }, + { {144., 146.}, {434., 438.} }, info.version, this); + break; + case 0x05: + _limits = new DMR6X2UVLimits({ {144., 146.}, {434., 437.} }, + { {144., 146.}, {434., 437.} }, info.version, this); + break; + case 0x06: + _limits = new DMR6X2UVLimits({ {136., 174.}, {446., 447.} }, + { {136., 174.}, {446., 447.} }, info.version, this); + break; + case 0x07: + _limits = new DMR6X2UVLimits({ {136., 174.}, {400., 480.} }, + { {144., 148.}, {420., 450.} }, info.version, this); + break; + case 0x08: + _limits = new DMR6X2UVLimits({ {136., 174.}, {400., 470.} }, + { {136., 174.}, {400., 470.} }, info.version, this); + break; + case 0x09: + _limits = new DMR6X2UVLimits({ {144., 146.}, {430., 432.} }, + { {144., 146.}, {430., 432.} }, info.version, this); + break; + case 0x0a: + _limits = new DMR6X2UVLimits({ {136., 174.}, {400., 480.} }, + { {144., 148.}, {430., 450.} }, info.version, this); + break; + case 0x0b: + _limits = new DMR6X2UVLimits({ {136., 174.}, {400., 520.} }, + { {136., 174.}, {400., 520.} }, info.version, this); + break; + case 0x0c: + _limits = new DMR6X2UVLimits({ {136., 174.}, {400., 490.} }, + { {136., 174.}, {400., 490.} }, info.version, this); + break; + case 0x0d: + _limits = new DMR6X2UVLimits({ {136., 174.}, {400., 480.} }, + { {136., 174.}, {403., 470.} }, info.version, this); + break; + case 0x0e: + _limits = new DMR6X2UVLimits({ {136., 174.}, {220.,225.}, {400., 520.} }, + { {136., 174.}, {220.,225.}, {400., 520.} }, info.version, this); + break; + case 0x0f: + _limits = new DMR6X2UVLimits({ {144., 148.}, {420., 520.} }, + { {144., 148.}, {420., 520.} }, info.version, this); + break; + case 0x10: + _limits = new DMR6X2UVLimits({ {144., 147.}, {430., 440.} }, + { {144., 147.}, {430., 440.} }, info.version, this); + break; + case 0x11: + _limits = new DMR6X2UVLimits({ {136., 174.}, {430., 440.} }, + { {136., 174.} }, info.version, this); + break; + default: + logInfo() << "Unknown band-code" << QString::number(int(info.bands), 16) + << ": Do not check frequency range."; + _limits = new DMR6X2UVLimits({}, {}, info.version, this); + break; + } +} + +const RadioLimits & +DMR6X2UV::limits() const { + return *_limits; +} + +RadioInfo +DMR6X2UV::defaultRadioInfo() { + return RadioInfo( + RadioInfo::DMR6X2UV, "dmr6x2uv", "DMR-6X2UV", "BTECH", AnytoneInterface::interfaceInfo()); +} + diff --git a/lib/dmr6x2uv.hh b/lib/dmr6x2uv.hh new file mode 100644 index 00000000..6059bc88 --- /dev/null +++ b/lib/dmr6x2uv.hh @@ -0,0 +1,38 @@ +/** @defgroup d6x2uv BTECH DMR-6X2UV + * Device specific classes for BTECH DMR-6X2UV. + * + * Athough labeled BTECH (Baofeng USA), this device is basically a relabled AnyTone AT-D878UV. + * However, there are some minor differences in the codeplug format, hence it needs a separate + * implementation. + * + * \image html dmr6x2uv.jpg "DMR-6X2UV" width=200px + * \image latex dmr6x2uv.jpg "DMR-6X2UV" width=200px + * + * @ingroup anytone */ + +#ifndef DMR6X2UV_HH +#define DMR6X2UV_HH + +#include "anytone_radio.hh" +#include "anytone_interface.hh" + +class DMR6X2UV: public AnytoneRadio +{ + Q_OBJECT + +public: + /** Do not construct this class directly, rather use @c Radio::detect. */ + explicit DMR6X2UV(AnytoneInterface *device=nullptr, QObject *parent=nullptr); + + const RadioLimits &limits() const; + + /** Returns the default radio information. The actual instance may have different properties + * due to variants of the same radio. */ + static RadioInfo defaultRadioInfo(); + +private: + RadioLimits *_limits; +}; + + +#endif // DMR6X2UV_HH diff --git a/lib/dmr6x2uv_codeplug.cc b/lib/dmr6x2uv_codeplug.cc new file mode 100644 index 00000000..3997b19d --- /dev/null +++ b/lib/dmr6x2uv_codeplug.cc @@ -0,0 +1,9 @@ +#include "dmr6x2uv_codeplug.hh" + +DMR6X2UVCodeplug::DMR6X2UVCodeplug(QObject *parent) + : D878UVCodeplug(parent) +{ + // pass... +} + + diff --git a/lib/dmr6x2uv_codeplug.hh b/lib/dmr6x2uv_codeplug.hh new file mode 100644 index 00000000..70b0fba7 --- /dev/null +++ b/lib/dmr6x2uv_codeplug.hh @@ -0,0 +1,225 @@ +#ifndef DMR6X2UVCODEPLUG_HH +#define DMR6X2UVCODEPLUG_HH + +#include "d878uv_codeplug.hh" + +/** Represents the device specific binary codeplug for BTECH DMR-6X2UV radios. + * + * In contrast to many other code-plugs, the code-plug for Anytone radios are spread over a large + * memory area. In principle, this is a good idea, as it allows one to upload only the portion of the + * codeplug that is actually configured. For example, if only a small portion of the available + * contacts and channels are used, the amount of data that is written to the device can be + * reduced. + * + * However, the implementation of this idea in this device is utter shit. The amount of + * fragmentation of the codeplug is overwhelming. For example, while channels are organized more or + * less nicely in continuous banks, zones are distributed throughout the entire code-plug. That is, + * the names of zones are located in a different memory section that the channel lists. Some lists + * are defined though bit-masks others by byte-masks. All bit-masks are positive, that is 1 + * indicates an enabled item while the bit-mask for contacts is inverted. + * + * In general the code-plug is huge and complex. Moreover, the radio provides a huge amount of + * options and features. To this end, reverse-engineering this code-plug was a nightmare. + * + * More over, the binary code-plug file generate by the windows CPS does not directly relate to + * the data being written to the radio. To this end the code-plug has been reverse-engineered + * using wireshark to monitor the USB communication between the windows CPS (running in a virtual + * box) and the device. The latter makes the reverse-engineering particularly cumbersome. + * + * @section dmr6x2uvcpl Codeplug structure within radio + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + *
Channels
Start Size Content
024C1500 000200 Bitmap of 4000 channels, default 0x00, + * 0x00 padded.
00800000 max. 002000 Channel bank 0 of up to 128 channels, + * see @c D878UVCodeplug::ChannelElement 64 b each.
00802000 max, 002000 Unknown data, Maybe extended channel information + * for channel bank 0? It is of exactly the same size as the channel bank 0. Mostly 0x00, a + * few 0xff.
00840000 max. 002000 Channel bank 1 of up to 128 channels.
00842000 max. 002000 Unknown data, related to CH bank 1?
... ... ...
00FC0000 max. 000800 Channel bank 32, up to 32 channels.
00FC2000 max. 000800 Unknown data, related to CH bank 32.
00FC0800 000040 VFO A settings, + * see @c D878UVCodeplug::ChannelElement.
00FC0840 000040 VFO B settings, + * see @c D878UVCodeplug::ChannelElement.
00FC2800 000080 Unknown data, related to VFO A+B? + * It is of exactly the same size as the two VFO channels. Mostly 0x00, a few 0xff. Same pattern + * as the unknown data associated with channel banks.
Zones
Start Size Content
024C1300 000020 Bitmap of 250 zones.
024C1360 000020 Hidden zone bitmap of 250 zones.
01000000 max. 01f400 250 zones channel lists of 250 16bit indices each. + * 0-based, little endian, default/padded=0xffff. Offset between channel lists 0x200, + * size of each list 0x1f4.
02540000 max. 001f40 250 Zone names. + * Each zone name is up to 16 ASCII chars long and gets 0-padded to 32b.
Roaming
Start Size Content
01042000 000020 Roaming channel bitmask, up to 250 bits, + * 0-padded, default 0.
01040000 max. 0x1f40 Optional up to 250 roaming channels, of 32b each. + * See @c D878UVCodeplug::RoamingChannelElement for details.
01042080 000010 Roaming zone bitmask, up to 64 bits, 0-padded, + * default 0.
01043000 max. 0x2000 Optional up to 64 roaming zones, of 128b each. + * See @c D878UVCodeplug::RoamingZoneElement for details.
Contacts
Start Size Content
02600000 max. 009C40 Index list of valid contacts. + * 10000 32bit indices, little endian, default 0xffffffff
02640000 000500 Contact bitmap, 10000 bit, inverted, + * default 0xff, 0x00 padded.
02680000 max. 0f4240 10000 contacts, + * see @c AnytoneCodeplug::ContactElement. As each contact is 100b, they do not align with the + * 16b blocks being transferred to the device. Hence contacts are organized internally in groups + * of 4 contacts forming a "bank".
04340000 max. 013880 DMR ID to contact index map, + * see @c AnytoneCodeplug::ContactMapElement. Sorted by ID, empty entries set to + * @c 0xffffffffffffffff.
Analog Contacts
Start Size Content
02900000 000080 Index list of valid analog contacts.
02900100 000080 Bytemap for 128 analog contacts.
02940000 max. 000180 128 analog contacts. + * See @c AnytoneCodeplug::DTMFContactElement. As each analog contact is 24b, they do not align with + * the 16b transfer block-size. Hence analog contacts are internally organized in groups of 2.
RX Group Lists
Start Size Content
025C0B10 000020 Bitmap of 250 RX group lists, + * default/padding 0x00.
02980000 max. 000120 Grouplist 0, + * see @c AnytoneCodeplug::GroupListElement.
02980200 max. 000120 Grouplist 1
... ... ...
0299f200 max. 000120 Grouplist 250
Scan lists
Start Size Content
024C1340 000020 Bitmap of 250 scan lists.
01080000 000090 Bank 0, Scanlist 1, + * see @c AnytoneCodeplug::ScanListElement.
01080200 000090 Bank 0, Scanlist 2
... ... ...
01081E00 000090 Bank 0, Scanlist 16
010C0000 000090 Bank 1, Scanlist 17
... ... ...
01440000 000090 Bank 15, Scanlist 241
... ... ...
01441400 000090 Bank 15, Scanlist 250
Radio IDs
Start Size Content
024C1320 000020 Bitmap of 250 radio IDs.
02580000 max. 001f40 250 Radio IDs. + * See @c AnytoneCodeplug::RadioIDElement.
GPS/APRS
Start Size Content
02501000 000040 APRS settings, + * see @c D878UVCodeplug::AnalogAPRSSettingsElement.
02501040 000060 APRS settings, + * see @c D878UVCodeplug::DMRAPRSSystemsElement.
025010A0 000060 Extended APRS settings, + * see @c D878UVCodeplug::AnalogAPRSSettingsExtensionElement.
02501200 000040 APRS Text, up to 60 chars ASCII, 0-padded.
02501800 000100 APRS-RX settings list up to 32 entries, 8b each. + * See @c D878UVCodeplug::AnalogAPRSRXEntryElement.
General Settings
Start Size Content
02500000 000100 General settings, + * see @c D878UVCodeplug::GeneralSettingsElement.
02500100 000400 Zone A & B channel list.
02500500 000100 DTMF list
02500600 000030 Power on settings, + * see @c AnytoneCodeplug::BootSettingsElement.
02501280 000030 General settings extension 1, + * see @c D878UVCodeplug::AnalogAPRSRXEntryElement.
02501400 000100 General settings extension 2, + * see @c D878UVCodeplug::AnalogAPRSSettingsExtensionElement.
024C2000 0003F0 List of 250 auto-repeater offset frequencies. + * 32bit little endian frequency in 10Hz. I.e., 600kHz = 60000. + * Default 0x00000000, 0x00 padded.
Messages
Start Size Content
01640000 max. 000100 Some kind of linked list of messages. + * See @c AnytoneCodeplug::MessageListElement. Each entry has a size of 0x10.
01640800 000090 Bytemap of up to 100 valid messages. + * 0x00=valid, 0xff=invalid, remaining 46b set to 0x00.
02140000 max. 000800 Bank 0, Messages 1-8. + * Each message consumes 0x100b. See @c AnytoneCodeplug::MessageElement.
02180000 max. 000800 Bank 1, Messages 9-16
... ... ...
02440000 max. 000800 Bank 12, Messages 97-100
Hot Keys
Start Size Content
025C0000 000100 4 analog quick-call settings. + * See @c AnytoneCodeplug::AnalogQuickCallElement.
025C0B00 000010 Status message bitmap.
025C0100 000400 Up to 32 status messages. + * Length unknown, offset 0x20. ASCII 0x00 terminated and padded.
025C0500 000360 18 hot-key settings, see + * @c AnytoneCodeplug::HotKeyElement
Encryption
Start Size Content
024C1700 000040 32 Encryption IDs, 0-based, 16bit big-endian.
024C1800 000500 32 DMR-Encryption keys, + * see @c D868UVCodeplug::dmr_encryption_key_t, + * 40b each.
024C4000 004000 Up to 256 AES encryption keys. + * See @c D878UVCodeplug::AESEncryptionKeyElement.
Misc
Start Size Content
024C1400 000020 Alarm setting, + * see @c AnytoneCodeplug::AlarmSettingElement.
024C1440 000030 Digital alarm settings extension, + * see @c AnytoneCodeplug::DigitalAlarmExtensionElement.
FM Broadcast
Start Size Content
02480210 000020 Bitmap of 100 FM broadcast channels.
02480000 max. 000200 100 FM broadcast channels. Encoded + * as 8-digit BCD little-endian in 100Hz. Filled with 0x00.
02480200 000010 FM broadcast VFO frequency. Encoded + * as 8-digit BCD little-endian in 100Hz. Filled with 0x00.
DTMF, 2-tone & 5-tone signaling.
Start Size Content
024C0C80 000010 5-tone encoding bitmap.
024C0000 000020 5-tone encoding.
024C0D00 000200 5-tone ID list.
024C1000 000080 5-tone settings.
024C1080 000050 DTMF settings.
024C1280 000010 2-tone encoding bitmap.
024C1100 000010 2-tone encoding.
024C1290 000010 2-tone settings.
024C2600 000010 2-tone decoding bitmap.
024C2400 000030 2-tone decoding.
+ * + * @ingroup dmr6x2uv */ +class DMR6X2UVCodeplug : public D878UVCodeplug +{ + Q_OBJECT + +public: + /** Empty constructor. */ + explicit DMR6X2UVCodeplug(QObject *parent=nullptr); +}; + +#endif // DMR6X2UVCODEPLUG_HH diff --git a/lib/dmr6x2uv_limits.cc b/lib/dmr6x2uv_limits.cc new file mode 100644 index 00000000..e3b4e8ae --- /dev/null +++ b/lib/dmr6x2uv_limits.cc @@ -0,0 +1,169 @@ +#include "dmr6x2uv_limits.hh" +#include "channel.hh" +#include "radioid.hh" +#include "contact.hh" +#include "rxgrouplist.hh" +#include "zone.hh" +#include "scanlist.hh" +#include "gpssystem.hh" +#include "roaming.hh" + + +DMR6X2UVLimits::DMR6X2UVLimits(const std::initializer_list > &rxFreqRanges, + const std::initializer_list > &txFreqRanges, + const QString &hardwareRevision, QObject *parent) + : AnytoneLimits(hardwareRevision, "V102", true, parent) +{ + // Define limits for call-sign DB + _hasCallSignDB = true; + _callSignDBImplemented = true; + _numCallSignDBEntries = 200000; + + /* Define limits for the general settings. */ + add("settings", + new RadioLimitItem{ + { "introLine1", new RadioLimitString(-1, 14, RadioLimitString::ASCII) }, + { "introLine2", new RadioLimitString(-1, 14, RadioLimitString::ASCII) }, + { "micLevel", new RadioLimitUInt(1, 10) }, + { "speech", new RadioLimitIgnoredBool() }, + { "power", new RadioLimitEnum({unsigned(Channel::Power::Low), unsigned(Channel::Power::High)}) }, + { "squlech", new RadioLimitUInt(0, 10) }, + { "vox", new RadioLimitUInt(0, 10) }, + { "tot", new RadioLimitUInt(0, -1) } + }); + + /* Define limits for radio IDs. */ + add("radioIDs", + new RadioLimitList { + { DMRRadioID::staticMetaObject, 1, 250, new RadioLimitObject { + {"name", new RadioLimitString(1,8, RadioLimitString::ASCII) }, + {"id", new RadioLimitUInt(0, 16777215)} + } } + }); + + /* Define limits for contacts. */ + add("contacts", + new RadioLimitList{ + { DMRContact::staticMetaObject, 1, 10000, new RadioLimitObject { + { "name", new RadioLimitString(1, 16, RadioLimitString::ASCII) }, + { "ring", new RadioLimitBool() }, + { "type", new RadioLimitEnum{ + (unsigned)DMRContact::PrivateCall, + (unsigned)DMRContact::GroupCall, + (unsigned)DMRContact::AllCall + }}, + { "number", new RadioLimitUInt(0, 16777215) } + } }, + { DTMFContact::staticMetaObject, -1, -1, new RadioLimitIgnored() } + }); + + /* Define limits for group lists. */ + add("groupLists", + new RadioLimitList( + RXGroupList::staticMetaObject, 1, 250, new RadioLimitObject { + { "name", new RadioLimitString(1, 16, RadioLimitString::ASCII) }, + { "contacts", new RadioLimitGroupCallRefList(1, 64) } + })); + + /* Define limits for channel list. */ + add("channels", + new RadioLimitList( + Channel::staticMetaObject, 1, 4000, + new RadioLimitObjects { + { FMChannel::staticMetaObject, + new RadioLimitObject { + {"name", new RadioLimitString(1, 16, RadioLimitString::ASCII)}, + {"rxFrequency", new RadioLimitFrequencies(rxFreqRanges, true)}, + {"txFrequency", new RadioLimitTransmitFrequencies(txFreqRanges)}, + {"power", new RadioLimitEnum{unsigned(Channel::Power::Low), unsigned(Channel::Power::High)}}, + {"timeout", new RadioLimitUInt(0, -1, std::numeric_limits::max())}, + {"scanlist", new RadioLimitObjRef(ScanList::staticMetaObject)}, + {"vox", new RadioLimitUInt(0, 10, std::numeric_limits::max())}, + {"rxOnly", new RadioLimitBool()}, + {"admit", new RadioLimitEnum{ + (unsigned)FMChannel::Admit::Always, + (unsigned)FMChannel::Admit::Free, + (unsigned)FMChannel::Admit::Tone + } }, + {"squelch", new RadioLimitUInt(0, 10, std::numeric_limits::max())}, + {"bandwidth", new RadioLimitEnum{ + (unsigned)FMChannel::Bandwidth::Narrow, + (unsigned)FMChannel::Bandwidth::Wide + }}, + {"aprs", new RadioLimitObjRef(APRSSystem::staticMetaObject)}, + {"openGD77", new RadioLimitIgnored(RadioLimitIssue::Hint)}, + {"tyt", new RadioLimitIgnored(RadioLimitIssue::Hint)} + } }, + { DMRChannel::staticMetaObject, + new RadioLimitObject { + {"name", new RadioLimitString(1,16, RadioLimitString::ASCII)}, + {"rxFrequency", new RadioLimitFrequencies(rxFreqRanges, true)}, + {"txFrequency", new RadioLimitTransmitFrequencies(txFreqRanges)}, + {"power", new RadioLimitEnum{unsigned(Channel::Power::Low), unsigned(Channel::Power::High)}}, + {"timeout", new RadioLimitUInt(0, -1, std::numeric_limits::max())}, + {"scanlist", new RadioLimitObjRef(ScanList::staticMetaObject)}, + {"vox", new RadioLimitUInt(0, 10, std::numeric_limits::max())}, + {"rxOnly", new RadioLimitBool()}, + {"admit", new RadioLimitEnum { + unsigned(DMRChannel::Admit::Always), + unsigned(DMRChannel::Admit::Free), + unsigned(DMRChannel::Admit::ColorCode) } }, + {"colorCode", new RadioLimitUInt(0,16)}, + {"timeSlot", new RadioLimitEnum { + unsigned(DMRChannel::TimeSlot::TS1), + unsigned(DMRChannel::TimeSlot::TS2) } }, + {"radioID", new RadioLimitObjRef(RadioID::staticMetaObject, true)}, + {"groupList", new RadioLimitObjRef(RXGroupList::staticMetaObject, false)}, + {"contact", new RadioLimitObjRef(DMRContact::staticMetaObject, false)}, + {"aprs", new RadioLimitObjRef(PositioningSystem::staticMetaObject, true)}, + {"roaming", new RadioLimitObjRef(RoamingZone::staticMetaObject, true) }, + {"openGD77", new RadioLimitIgnored(RadioLimitIssue::Hint)}, + {"tyt", new RadioLimitIgnored(RadioLimitIssue::Hint)} + } } + } )); + + /* Define limits for zone list. */ + add("zones", + new RadioLimitList( + Zone::staticMetaObject, 1, 250, new RadioLimitSingleZone( + 250, { + { "name", new RadioLimitString(1, 16, RadioLimitString::ASCII) }, // 16 ASCII chars in name + { "anytone", new RadioLimitIgnored(RadioLimitIssue::Hint) } // ignore AnyTone extensions + }) + ) ); + + /* Define limits for scan lists. */ + add("scanlists", + new RadioLimitList( + ScanList::staticMetaObject, 1, 250, new RadioLimitObject{ + { "name", new RadioLimitString(1, 16, RadioLimitString::ASCII) }, + { "primary", new RadioLimitObjRef(Channel::staticMetaObject, false) }, + { "secondary", new RadioLimitObjRef(Channel::staticMetaObject, true) }, + { "revert", new RadioLimitObjRef(Channel::staticMetaObject, true) }, + { "channels", new RadioLimitRefList(0, 31, Channel::staticMetaObject) } + })); + + /* Handle positioning systems. */ + add("positioning", new RadioLimitList{ + { GPSSystem::staticMetaObject, 0, 8, new RadioLimitObject { + { "name", new RadioLimitStringIgnored() }, + { "period", new RadioLimitUInt(0, 7650) }, + { "contact", new RadioLimitObjRef(DMRContact::staticMetaObject, false) }, + { "revert", new RadioLimitObjRef(DMRChannel::staticMetaObject, true) } } }, + { APRSSystem::staticMetaObject, 0, 1, new RadioLimitObject { + { "name", new RadioLimitStringIgnored() }, + { "period", new RadioLimitUInt(0, 7650) }, + { "revert", new RadioLimitObjRef(FMChannel::staticMetaObject, false) }, + { "icon", new RadioLimitEnum{} }, + { "message", new RadioLimitString(0, 60, RadioLimitString::ASCII) } + ///@todo extend APRSSystem to expose other settings as properties. + }} } ); + + /* Handle roaming zones. */ + add("roaming", + new RadioLimitList(RoamingZone::staticMetaObject, 0, 64, + new RadioLimitObject { + { "name", new RadioLimitStringIgnored() }, + { "channels", new RadioLimitRefList(0, 64, DMRChannel::staticMetaObject) } + } ) ); +} diff --git a/lib/dmr6x2uv_limits.hh b/lib/dmr6x2uv_limits.hh new file mode 100644 index 00000000..3db3de23 --- /dev/null +++ b/lib/dmr6x2uv_limits.hh @@ -0,0 +1,19 @@ +#ifndef DMR6X2UVLIMITS_HH +#define DMR6X2UVLIMITS_HH + +#include "anytone_limits.hh" + +/** Implements the limits for the BTECH DMR-6X2UV. + * @ingroup dmr6x2uv */ +class DMR6X2UVLimits: public AnytoneLimits +{ + Q_OBJECT + +public: + /** Constructor. */ + DMR6X2UVLimits(const std::initializer_list> &rxFreqRanges, + const std::initializer_list> &txFreqRanges, + const QString &hardwareRevision, QObject *parent=nullptr); +}; + +#endif // D878UVLIMITS_HH diff --git a/lib/radioinfo.cc b/lib/radioinfo.cc index 8c17645a..ecf9f1cc 100644 --- a/lib/radioinfo.cc +++ b/lib/radioinfo.cc @@ -11,6 +11,7 @@ #include "d878uv.hh" #include "d878uv2.hh" #include "d578uv.hh" +#include "dmr6x2uv.hh" #include "dm1701.hh" @@ -32,7 +33,7 @@ RadioInfo::_radiosByName = QHash{ {"rt84", RadioInfo::RT84}, {"d868uv", RadioInfo::D868UV}, {"d868uve", RadioInfo::D868UVE}, - {"dmr6x2", RadioInfo::DMR6X2}, + {"dmr6x2uv", RadioInfo::DMR6X2UV}, {"d878uv", RadioInfo::D878UV}, {"d878uv2", RadioInfo::D878UVII}, {"d578uv", RadioInfo::D578UV} @@ -51,7 +52,8 @@ RadioInfo::_radiosById = QHash{ {RadioInfo::D868UVE, D868UV::defaultRadioInfo()}, {RadioInfo::D878UV, D878UV::defaultRadioInfo()}, {RadioInfo::D878UVII, D878UV2::defaultRadioInfo()}, - {RadioInfo::D578UV, D578UV::defaultRadioInfo()} + {RadioInfo::D578UV, D578UV::defaultRadioInfo()}, + {RadioInfo::DMR6X2UV, DMR6X2UV::defaultRadioInfo()} }; diff --git a/lib/radioinfo.hh b/lib/radioinfo.hh index ebe5f33a..c1fa314e 100644 --- a/lib/radioinfo.hh +++ b/lib/radioinfo.hh @@ -30,7 +30,7 @@ public: // Anytone devices D868UVE, D868UV = D868UVE, // Actually a different device. Implement! - DMR6X2 = D868UVE, // Actually a D868UV, implement! + DMR6X2UV, D878UV, D878UVII, D578UV, From c1a09599996ffb746cc2ea11a99de8f83e106bba Mon Sep 17 00:00:00 2001 From: Hannes Matuschek Date: Thu, 15 Dec 2022 11:59:17 +0100 Subject: [PATCH 02/16] Fixed general settings for BTECH DMR-6X2UV. --- lib/dmr6x2uv.hh | 4 ++- lib/dmr6x2uv_codeplug.cc | 76 ++++++++++++++++++++++++++++++++++++++++ lib/dmr6x2uv_codeplug.hh | 57 ++++++++++++++++++++++++++---- 3 files changed, 129 insertions(+), 8 deletions(-) diff --git a/lib/dmr6x2uv.hh b/lib/dmr6x2uv.hh index 6059bc88..9730738a 100644 --- a/lib/dmr6x2uv.hh +++ b/lib/dmr6x2uv.hh @@ -1,4 +1,4 @@ -/** @defgroup d6x2uv BTECH DMR-6X2UV +/** @defgroup dmr6x2uv BTECH DMR-6X2UV * Device specific classes for BTECH DMR-6X2UV. * * Athough labeled BTECH (Baofeng USA), this device is basically a relabled AnyTone AT-D878UV. @@ -16,6 +16,8 @@ #include "anytone_radio.hh" #include "anytone_interface.hh" +/** Represents a BTECH DMR-6X2UV. + * @ingroup dmr6x2uv */ class DMR6X2UV: public AnytoneRadio { Q_OBJECT diff --git a/lib/dmr6x2uv_codeplug.cc b/lib/dmr6x2uv_codeplug.cc index 3997b19d..2916523a 100644 --- a/lib/dmr6x2uv_codeplug.cc +++ b/lib/dmr6x2uv_codeplug.cc @@ -1,5 +1,81 @@ #include "dmr6x2uv_codeplug.hh" +/* ********************************************************************************************* * + * Implementation of DMR6X2UVCodeplug::GeneralSettingsElement + * ********************************************************************************************* */ +DMR6X2UVCodeplug::GeneralSettingsElement::GeneralSettingsElement(uint8_t *ptr, unsigned size) + : D878UVCodeplug::GeneralSettingsElement(ptr, size) +{ + // pass... +} + +DMR6X2UVCodeplug::GeneralSettingsElement::GeneralSettingsElement(uint8_t *ptr) + : D878UVCodeplug::GeneralSettingsElement(ptr, 0x0100) +{ + // pass... +} + +bool +DMR6X2UVCodeplug::GeneralSettingsElement::simplexRepeaterEnabled() const { + return 0x01 == getUInt8(0x00b2); +} +void +DMR6X2UVCodeplug::GeneralSettingsElement::enableSimplexRepeater(bool enable) { + setUInt8(0x00b2, enable ? 0x01 : 0x00); +} + +bool +DMR6X2UVCodeplug::GeneralSettingsElement::monitorSimplexRepeaterEnabled() const { + return 0x01 == getUInt8(0x00b3); +} +void +DMR6X2UVCodeplug::GeneralSettingsElement::enableMonitorSimplexRepeater(bool enable) { + setUInt8(0x00b3, enable ? 0x01 : 0x00); +} + +DMR6X2UVCodeplug::GeneralSettingsElement::SimplexRepeaterSlot +DMR6X2UVCodeplug::GeneralSettingsElement::simplexRepeaterTimeslot() const { + switch (getUInt8(0x00b7)) { + case 0x00: + return SimplexRepeaterSlot::TS1; + case 0x01: + return SimplexRepeaterSlot::TS2; + case 0x02: + default: + return SimplexRepeaterSlot::Channel; + } +} + +void +DMR6X2UVCodeplug::GeneralSettingsElement::setSimplexRepeaterTimeslot(SimplexRepeaterSlot slot) { + switch (slot) { + case SimplexRepeaterSlot::TS1: setUInt8(0x00b7, 0x00); break; + case SimplexRepeaterSlot::TS2: setUInt8(0x00b7, 0x01); break; + case SimplexRepeaterSlot::Channel: setUInt8(0x00b7, 0x02); break; + } +} + +bool +DMR6X2UVCodeplug::GeneralSettingsElement::fromConfig(const Flags &flags, Context &ctx) +{ + if (! D878UVCodeplug::GeneralSettingsElement::fromConfig(flags, ctx)) + return false; + // apply DMR-6X2UV specific settings. + return true; +} + +bool +DMR6X2UVCodeplug::GeneralSettingsElement::updateConfig(Context &ctx) { + if (! D878UVCodeplug::GeneralSettingsElement::updateConfig(ctx)) + return false; + // Extract DMR-6X2UV specific settings. + return true; +} + + +/* ********************************************************************************************* * + * Implementation of DMR6X2UVCodeplug + * ********************************************************************************************* */ DMR6X2UVCodeplug::DMR6X2UVCodeplug(QObject *parent) : D878UVCodeplug(parent) { diff --git a/lib/dmr6x2uv_codeplug.hh b/lib/dmr6x2uv_codeplug.hh index 70b0fba7..9608ec24 100644 --- a/lib/dmr6x2uv_codeplug.hh +++ b/lib/dmr6x2uv_codeplug.hh @@ -5,7 +5,9 @@ /** Represents the device specific binary codeplug for BTECH DMR-6X2UV radios. * - * In contrast to many other code-plugs, the code-plug for Anytone radios are spread over a large + * This codeplug implementation is compatible with firmware revision 2.04. + * + * In contrast to many other codeplugs, the codeplug for Anytone radios are spread over a large * memory area. In principle, this is a good idea, as it allows one to upload only the portion of the * codeplug that is actually configured. For example, if only a small portion of the available * contacts and channels are used, the amount of data that is written to the device can be @@ -13,18 +15,18 @@ * * However, the implementation of this idea in this device is utter shit. The amount of * fragmentation of the codeplug is overwhelming. For example, while channels are organized more or - * less nicely in continuous banks, zones are distributed throughout the entire code-plug. That is, + * less nicely in continuous banks, zones are distributed throughout the entire codeplug. That is, * the names of zones are located in a different memory section that the channel lists. Some lists * are defined though bit-masks others by byte-masks. All bit-masks are positive, that is 1 * indicates an enabled item while the bit-mask for contacts is inverted. * - * In general the code-plug is huge and complex. Moreover, the radio provides a huge amount of - * options and features. To this end, reverse-engineering this code-plug was a nightmare. + * In general the codeplug is huge and complex. Moreover, the radio provides a huge amount of + * options and features. To this end, reverse engineering this codeplug was a nightmare. * - * More over, the binary code-plug file generate by the windows CPS does not directly relate to - * the data being written to the radio. To this end the code-plug has been reverse-engineered + * More over, the binary codeplug file generate by the windows CPS does not directly relate to + * the data being written to the radio. To this end the codeplug has been reverse-engineered * using wireshark to monitor the USB communication between the windows CPS (running in a virtual - * box) and the device. The latter makes the reverse-engineering particularly cumbersome. + * box) and the device. The latter makes the reverse engineering particularly cumbersome. * * @section dmr6x2uvcpl Codeplug structure within radio * @@ -217,6 +219,47 @@ class DMR6X2UVCodeplug : public D878UVCodeplug { Q_OBJECT +public: + /** General settings element for the DMR-6X2UV. + * + * Memory representation of the encoded settings element (size 0x100 bytes): + * @verbinclude dmr6x2uv_generalsettings.txt */ + class GeneralSettingsElement: public D878UVCodeplug::GeneralSettingsElement + { + public: + /** Possible GPS modes. */ + enum class SimplexRepeaterSlot { + TS1 = 0, TS2 = 1, Channel = 2 + }; + + protected: + /** Hidden Constructor. */ + GeneralSettingsElement(uint8_t *ptr, unsigned size); + + public: + /** Constructor. */ + explicit GeneralSettingsElement(uint8_t *ptr); + + /** Returns @c true if the simplex repeater feature is enabled. */ + virtual bool simplexRepeaterEnabled() const; + /** Enables disables the simplex repeater feature. */ + virtual void enableSimplexRepeater(bool enable); + /** Returns @c true if the speaker is switched on during RX in simplex repeater mode, + * see @c simplexRepeaterEnabled. */ + virtual bool monitorSimplexRepeaterEnabled() const; + /** Enables/disables the speaker during RX in simplex repeater mode. */ + virtual void enableMonitorSimplexRepeater(bool enable); + /** Returns the time-slot in simplex repeater mode. */ + virtual SimplexRepeaterSlot simplexRepeaterTimeslot() const; + /** Sets the time-slot in simplex repeater mode. */ + virtual void setSimplexRepeaterTimeslot(SimplexRepeaterSlot slot); + + /** Encodes the settings from the config. */ + virtual bool fromConfig(const Flags &flags, Context &ctx); + /** Update config from settings. */ + virtual bool updateConfig(Context &ctx); + }; + public: /** Empty constructor. */ explicit DMR6X2UVCodeplug(QObject *parent=nullptr); From 98e98ad6bb8433bd69bff590b39f9bbfcb33d07b Mon Sep 17 00:00:00 2001 From: Hannes Matuschek Date: Thu, 15 Dec 2022 13:19:35 +0100 Subject: [PATCH 03/16] Fixed memory layout for DMR-6X2UV codeplug. --- doc/code/dmr6x2uv_generalsettings.txt | 12 ---------- lib/dmr6x2uv_codeplug.cc | 25 ++++++++++++++++---- lib/dmr6x2uv_codeplug.hh | 34 +++++++++++---------------- 3 files changed, 35 insertions(+), 36 deletions(-) diff --git a/doc/code/dmr6x2uv_generalsettings.txt b/doc/code/dmr6x2uv_generalsettings.txt index ab92df0d..b4e0fe62 100644 --- a/doc/code/dmr6x2uv_generalsettings.txt +++ b/doc/code/dmr6x2uv_generalsettings.txt @@ -112,18 +112,6 @@ d8 | VFO A default zone index | VFO B default zone index | VFO A defau +---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+ dc | Default roaming zone index | Repeater range check enable | Repeater check interval in 5s | RepCheck reconnect count | +---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+ -e0 | Roam start condition | Backlight duration TX in sec | Separate display | Keep last caller | - +---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+ -e4 | Channel name color | RepCheck notification | Backlight duration RX in sec | Roaming enable | - +---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+ -e8 | Unused set to 0x00 | Mute delay in minutes-1 | RepCheck num notification | Startup GPS test enable | - +---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+ -ec | Startup reset enable | Unused set to 0x00 | - +---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+ -f0 | Unknown settings ... - +---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+ -fc ... | - +---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+ Field description: - SRP: Simplex repeater diff --git a/lib/dmr6x2uv_codeplug.cc b/lib/dmr6x2uv_codeplug.cc index 2916523a..3a1e2d61 100644 --- a/lib/dmr6x2uv_codeplug.cc +++ b/lib/dmr6x2uv_codeplug.cc @@ -1,5 +1,24 @@ #include "dmr6x2uv_codeplug.hh" +#define NUM_CHANNELS 4000 +#define NUM_CHANNEL_BANKS 32 +#define CHANNEL_BANK_0 0x00800000 +#define CHANNEL_BANK_SIZE 0x00002000 +#define CHANNEL_BANK_31 0x00fc0000 +#define CHANNEL_BANK_31_SIZE 0x00000800 +#define CHANNEL_BANK_OFFSET 0x00040000 +#define CHANNEL_SIZE 0x00000040 +#define CHANNEL_BITMAP 0x024c1500 +#define CHANNEL_BITMAP_SIZE 0x00000200 + +#define ADDR_GENERAL_CONFIG 0x02500000 +#define GENERAL_CONFIG_SIZE 0x00000100 +#define ADDR_GENERAL_CONFIG_EXT1 0x02501280 +#define GENERAL_CONFIG_EXT1_SIZE 0x00000030 +#define ADDR_GENERAL_CONFIG_EXT2 0x02501400 +#define GENERAL_CONFIG_EXT2_SIZE 0x00000100 + + /* ********************************************************************************************* * * Implementation of DMR6X2UVCodeplug::GeneralSettingsElement * ********************************************************************************************* */ @@ -10,7 +29,7 @@ DMR6X2UVCodeplug::GeneralSettingsElement::GeneralSettingsElement(uint8_t *ptr, u } DMR6X2UVCodeplug::GeneralSettingsElement::GeneralSettingsElement(uint8_t *ptr) - : D878UVCodeplug::GeneralSettingsElement(ptr, 0x0100) + : D878UVCodeplug::GeneralSettingsElement(ptr, 0x00e0) { // pass... } @@ -77,9 +96,7 @@ DMR6X2UVCodeplug::GeneralSettingsElement::updateConfig(Context &ctx) { * Implementation of DMR6X2UVCodeplug * ********************************************************************************************* */ DMR6X2UVCodeplug::DMR6X2UVCodeplug(QObject *parent) - : D878UVCodeplug(parent) + : D868UVCodeplug(parent) { // pass... } - - diff --git a/lib/dmr6x2uv_codeplug.hh b/lib/dmr6x2uv_codeplug.hh index 9608ec24..db4e8ae1 100644 --- a/lib/dmr6x2uv_codeplug.hh +++ b/lib/dmr6x2uv_codeplug.hh @@ -1,6 +1,7 @@ #ifndef DMR6X2UVCODEPLUG_HH #define DMR6X2UVCODEPLUG_HH +#include "d868uv_codeplug.hh" #include "d878uv_codeplug.hh" /** Represents the device specific binary codeplug for BTECH DMR-6X2UV radios. @@ -36,21 +37,12 @@ * 0x00 padded. * - * * - * - * * - * * * - * * * * @@ -131,24 +123,16 @@ * see @c D878UVCodeplug::AnalogAPRSSettingsElement. * - * * - * * * * - * * * * - * - * * @@ -212,17 +196,26 @@ * * * + * + * + * + * + * + * + * + * + * *
00800000 max. 002000 Channel bank 0 of up to 128 channels, * see @c D878UVCodeplug::ChannelElement 64 b each.
00802000 max, 002000 Unknown data, Maybe extended channel information - * for channel bank 0? It is of exactly the same size as the channel bank 0. Mostly 0x00, a - * few 0xff.
00840000 max. 002000 Channel bank 1 of up to 128 channels.
00842000 max. 002000 Unknown data, related to CH bank 1?
... ... ...
00FC0000 max. 000800 Channel bank 32, up to 32 channels.
00FC2000 max. 000800 Unknown data, related to CH bank 32.
00FC0800 000040 VFO A settings, * see @c D878UVCodeplug::ChannelElement.
00FC0840 000040 VFO B settings, * see @c D878UVCodeplug::ChannelElement.
00FC2800 000080 Unknown data, related to VFO A+B? - * It is of exactly the same size as the two VFO channels. Mostly 0x00, a few 0xff. Same pattern - * as the unknown data associated with channel banks.
Zones
Start Size Content
02501040 000060 APRS settings, * see @c D878UVCodeplug::DMRAPRSSystemsElement.
025010A0 000060 Extended APRS settings, - * see @c D878UVCodeplug::AnalogAPRSSettingsExtensionElement.
02501200 000040 APRS Text, up to 60 chars ASCII, 0-padded.
02501800 000100 APRS-RX settings list up to 32 entries, 8b each. - * See @c D878UVCodeplug::AnalogAPRSRXEntryElement.
General Settings
Start Size Content
02500000 000100 General settings, + *
02500000 0000e0 General settings, * see @c D878UVCodeplug::GeneralSettingsElement.
02500100 000400 Zone A & B channel list.
02500500 000100 DTMF list
02500600 000030 Power on settings, * see @c AnytoneCodeplug::BootSettingsElement.
02501280 000030 General settings extension 1, - * see @c D878UVCodeplug::AnalogAPRSRXEntryElement.
02501400 000100 General settings extension 2, - * see @c D878UVCodeplug::AnalogAPRSSettingsExtensionElement.
024C2000 0003F0 List of 250 auto-repeater offset frequencies. * 32bit little endian frequency in 10Hz. I.e., 600kHz = 60000. * Default 0x00000000, 0x00 padded.
024C2600 000010 2-tone decoding bitmap.
024C2400 000030 2-tone decoding.
Unknown settings.
Start Size Content
024C2610 000020 Unknown bitmap.
024C2630 000020 Unknown bitmap.
024C3000 000020 Unknown settings.
024C5000 000020 Unknown settings.
02501280 000030 Unknown settings.
02501400 000030 Uknonwn settings.
025C1000 005000 Unknown settings.
* * @ingroup dmr6x2uv */ -class DMR6X2UVCodeplug : public D878UVCodeplug +class DMR6X2UVCodeplug : public D868UVCodeplug { Q_OBJECT public: /** General settings element for the DMR-6X2UV. * - * Memory representation of the encoded settings element (size 0x100 bytes): + * Memory representation of the encoded settings element (size 0x0e0 bytes): * @verbinclude dmr6x2uv_generalsettings.txt */ class GeneralSettingsElement: public D878UVCodeplug::GeneralSettingsElement { @@ -263,6 +256,7 @@ public: public: /** Empty constructor. */ explicit DMR6X2UVCodeplug(QObject *parent=nullptr); + }; #endif // DMR6X2UVCODEPLUG_HH From 3f3b8462073b6953930b6b29e3a35dc7bc038bd8 Mon Sep 17 00:00:00 2001 From: Hannes Matuschek Date: Thu, 15 Dec 2022 13:35:16 +0100 Subject: [PATCH 04/16] Cleanup. --- lib/CMakeLists.txt | 4 +- lib/anytone_filereader.hh | 3 - lib/codeplugcontext.cc | 264 -------------------------------------- lib/codeplugcontext.hh | 120 ----------------- lib/d578uv_codeplug.hh | 1 - lib/d868uv_codeplug.hh | 1 - lib/d878uv2_codeplug.hh | 1 - lib/d878uv_codeplug.hh | 1 - lib/dm1701_codeplug.cc | 1 - lib/gd77_codeplug.hh | 1 - lib/md2017_codeplug.cc | 1 - lib/tyt_codeplug.cc | 1 - lib/uv390_codeplug.cc | 1 - 13 files changed, 2 insertions(+), 398 deletions(-) delete mode 100644 lib/codeplugcontext.cc delete mode 100644 lib/codeplugcontext.hh diff --git a/lib/CMakeLists.txt b/lib/CMakeLists.txt index fc2bde3a..a3896e71 100644 --- a/lib/CMakeLists.txt +++ b/lib/CMakeLists.txt @@ -9,7 +9,7 @@ ELSE (APPLE) ENDIF(APPLE) SET(libdmrconf_SOURCES - utils.cc crc32.cc signaling.cc codeplugcontext.cc addressmap.cc radiointerface.cc errorstack.cc + utils.cc crc32.cc signaling.cc addressmap.cc radiointerface.cc errorstack.cc radio.cc ${hid_SOURCES} dfu_libusb.cc usbserial.cc radioinfo.cc usbdevice.cc radiolimits.cc csvreader.cc dfufile.cc userdatabase.cc logger.cc configobject.cc configreference.cc config.cc radiosettings.cc contact.cc rxgrouplist.cc @@ -58,7 +58,7 @@ SET(libdmrconf_MOC_HEADERS SET(libdmrconf_HEADERS libdmrconf.hh radiointerface.hh radioinfo.hh usbdevice.hh gd77_filereader.hh rd5r_filereader.hh uv390_filereader.hh md2017_filereader.hh md390_filereader.hh - utils.hh crc32.hh signaling.hh codeplugcontext.hh addressmap.hh errorstack.hh) + utils.hh crc32.hh signaling.hh addressmap.hh errorstack.hh) configure_file(config.h.in ${PROJECT_BINARY_DIR}/lib/config.h) diff --git a/lib/anytone_filereader.hh b/lib/anytone_filereader.hh index c67a5a3b..89e79ae4 100644 --- a/lib/anytone_filereader.hh +++ b/lib/anytone_filereader.hh @@ -3,7 +3,6 @@ #include #include "config.hh" -#include "codeplugcontext.hh" /** This class implements a reader of AnyTone codeplug files. @@ -72,8 +71,6 @@ public: static bool read(const QString &filename, Config *config, QString &message); protected: - /** Offset-element map. */ - CodeplugContext _context; /** Pointer to the start. */ const uint8_t * const _start; /** Pointer to the entire data. */ diff --git a/lib/codeplugcontext.cc b/lib/codeplugcontext.cc deleted file mode 100644 index a9e61b38..00000000 --- a/lib/codeplugcontext.cc +++ /dev/null @@ -1,264 +0,0 @@ -#include "codeplugcontext.hh" -#include "logger.hh" - - -CodeplugContext::CodeplugContext(Config *config) - : _config(config), _radioIDTable(), _channelTable(), _digitalContactTable(), _analogContactTable(), - _groupListTable(), _scanListTable(), _gpsSystemTable(), _aprsSystemTable() -{ - // pass... -} - -Config * -CodeplugContext::config() const { - return _config; -} - - -bool -CodeplugContext::hasRadioId(int index) const { - return _radioIDTable.contains(index); -} - -bool -CodeplugContext::setDefaultRadioId(uint32_t id, int index) { - if (_radioIDTable.contains(0)) - return false; - _config->radioIDs()->getId(0)->setNumber(id); - _radioIDTable[index] = 0; - return true; -} - -bool -CodeplugContext::addRadioId(uint32_t id, int index, const QString &name) { - if (_radioIDTable.contains(index)) - return false; - int cidx = _config->radioIDs()->addId(name, id); - if (cidx < 0) - return false; - _radioIDTable[index] = cidx; - return true; -} - -DMRRadioID * -CodeplugContext::getRadioId(int idx) const { - if (! _radioIDTable.contains(idx)) - return nullptr; - return _config->radioIDs()->getId(_radioIDTable[idx]); -} - - -bool -CodeplugContext::hasChannel(int index) const { - return _channelTable.contains(index); -} - -bool -CodeplugContext::addChannel(Channel *ch, int index) { - if (_channelTable.contains(index)) - return false; - //logDebug() << "Register channel '" << ch->name() << "' under idx=" << index << "."; - int cidx = _config->channelList()->add(ch); - if (0 > cidx) - return false; - _channelTable[index] = cidx; - return true; -} - -Channel * -CodeplugContext::getChannel(int index) const { - if (! _channelTable.contains(index)) - return nullptr; - return _config->channelList()->channel(_channelTable[index]); -} - - -bool -CodeplugContext::hasDigitalContact(int index) const { - return _digitalContactTable.contains(index); -} - -bool -CodeplugContext::addDigitalContact(DMRContact *con, int index) { - if (_digitalContactTable.contains(index)) - return false; - //logDebug() << "Register contact '" << con->name() << "' under idx=" << index << "."; - int cidx = _config->contacts()->add(con); - if (0 > cidx) - return false; - _digitalContactTable[index] = cidx; - return true; -} - -DMRContact * -CodeplugContext::getDigitalContact(int index) const { - if (! _digitalContactTable.contains(index)) - return nullptr; - return _config->contacts()->digitalContact(_digitalContactTable[index]); -} - - -bool -CodeplugContext::hasAnalogContact(int index) const { - return _analogContactTable.contains(index); -} - -bool -CodeplugContext::addAnalogContact(DTMFContact *con, int index) { - if (_analogContactTable.contains(index)) - return false; - int cidx = _config->contacts()->add(con); - if (0 > cidx) - return false; - _analogContactTable[index] = cidx; - return true; -} - -DTMFContact * -CodeplugContext::getAnalogContact(int index) const { - if (! _analogContactTable.contains(index)) - return nullptr; - return _config->contacts()->dtmfContact(_analogContactTable[index]); -} - - -bool -CodeplugContext::hasGroupList(int index) const { - return _groupListTable.contains(index); -} - -bool -CodeplugContext::addGroupList(RXGroupList *grp, int index) { - if (_groupListTable.contains(index)) - return false; - int cidx = _config->rxGroupLists()->add(grp); - if (0 > cidx) - return false; - _groupListTable[index] = cidx; - return true; -} - -RXGroupList * -CodeplugContext::getGroupList(int index) const { - if (! _groupListTable.contains(index)) - return nullptr; - return _config->rxGroupLists()->list(_groupListTable[index]); -} - - -bool -CodeplugContext::hasScanList(int index) const { - return _scanListTable.contains(index); -} - -ScanList * -CodeplugContext::getScanList(int index) const { - if (! _scanListTable.contains(index)) - return nullptr; - return _config->scanlists()->scanlist(_scanListTable[index]); -} - -bool -CodeplugContext::addScanList(ScanList *lst, int index) { - if (_scanListTable.contains(index)) - return false; - int sidx = _config->scanlists()->count(); - if (0 > _config->scanlists()->add(lst)) - return false; - _scanListTable[index] = sidx; - return true; -} - - -bool -CodeplugContext::hasGPSSystem(int index) const { - return _gpsSystemTable.contains(index); -} - -GPSSystem * -CodeplugContext::getGPSSystem(int index) const { - if (! _gpsSystemTable.contains(index)) - return nullptr; - return _config->posSystems()->gpsSystem(_gpsSystemTable[index]); -} - -bool -CodeplugContext::addGPSSystem(GPSSystem *sys, int index) { - if (_gpsSystemTable.contains(index)) - return false; - int sidx = _config->posSystems()->gpsCount(); - if (0 > _config->posSystems()->add(sys)) - return false; - _gpsSystemTable[index] = sidx; - return true; -} - - -bool -CodeplugContext::hasAPRSSystem(int index) const { - return _aprsSystemTable.contains(index); -} - -APRSSystem * -CodeplugContext::getAPRSSystem(int index) const { - if (! _aprsSystemTable.contains(index)) - return nullptr; - return _config->posSystems()->aprsSystem(_aprsSystemTable[index]); -} - -bool -CodeplugContext::addAPRSSystem(APRSSystem *sys, int index) { - if (_aprsSystemTable.contains(index)) - return false; - int sidx = _config->posSystems()->aprsCount(); - if (0 > _config->posSystems()->add(sys)) - return false; - _aprsSystemTable[index] = sidx; - return true; -} - - -bool -CodeplugContext::hasRoamingZone(int index) const { - return _roamingZoneTable.contains(index); -} - -RoamingZone * -CodeplugContext::getRoamingZone(int index) const { - if (! _roamingZoneTable.contains(index)) - return nullptr; - return _config->roaming()->zone(_roamingZoneTable[index]); -} - -bool -CodeplugContext::addRoamingZone(RoamingZone *zone, int index) { - if (_roamingZoneTable.contains(index)) - return false; - int sidx = _config->roaming()->count(); - if (! _config->roaming()->add(zone)) - return false; - _roamingZoneTable[index] = sidx; - return true; -} - - -bool -CodeplugContext::hasRoamingChannel(int index) const { - return _roamingChannelTable.contains(index); -} - -DMRChannel * -CodeplugContext::getRoamingChannel(int index) const { - if (! _roamingChannelTable.contains(index)) - return nullptr; - return _config->channelList()->channel(_roamingChannelTable[index])->as(); -} - -bool -CodeplugContext::addRoamingChannel(DMRChannel *ch, int index) { - if (_roamingChannelTable.contains(index)) - return false; - int sidx = _config->channelList()->indexOf(ch); - _roamingChannelTable[index] = sidx; - return true; -} diff --git a/lib/codeplugcontext.hh b/lib/codeplugcontext.hh deleted file mode 100644 index 531e747f..00000000 --- a/lib/codeplugcontext.hh +++ /dev/null @@ -1,120 +0,0 @@ -#ifndef CODEPLUGCONTEXT_HH -#define CODEPLUGCONTEXT_HH - -#include -#include "config.hh" - -/** Helper class to assemble radio config from binary codeplugs that use index-tables and bitmaps - * to specify which elements of the codeplug are valid. This may lead to sparse specification - * of channels etc. Hence an codeplug index to config object mapping is needed. This class - * provides this mapping. - * - * @deprecated With version 0.9.0, there is an extensible codeplug context @c CodePlug::Context. - * This class will be phased out and replaced with the new one. - * @ingroup util */ -class CodeplugContext -{ -public: - /** Wraps the given device config. */ - CodeplugContext(Config *config); - - /** Returns the wrapped config. */ - Config *config() const; - - /** Returns @c true if the radio DMR ID of given index is set.*/ - bool hasRadioId(int index) const; - /** Stores the given ID at the specified index as the default radio ID. */ - bool setDefaultRadioId(uint32_t id, int index); - /** Adds the given radio ID at the given index to the list. */ - bool addRadioId(uint32_t id, int index, const QString &name=""); - /** Maps the given index to the associated radio ID. */ - DMRRadioID *getRadioId(int idx) const; - - /** Returns @c true, if the given channel index has been defined before. */ - bool hasChannel(int index) const; - /** Adds a channel to the config and maps the given index to that channel. */ - bool addChannel(Channel *ch, int index); - /** Gets a channel for the specified index or @c nullptr if not defined. */ - Channel *getChannel(int index) const; - - /** Returns @c true, if the given digital contact index has been defined before. */ - bool hasDigitalContact(int index) const; - /** Adds a digital contact to the config and maps the given index to that contact. */ - bool addDigitalContact(DMRContact *con, int index); - /** Gets a digital contact for the specified index or @c nullptr if not defined. */ - DMRContact *getDigitalContact(int index) const; - - /** Returns @c true, if the given analog contact index has been defined before. */ - bool hasAnalogContact(int index) const; - /** Adds a analog contact to the config and maps the given index to that contact. */ - bool addAnalogContact(DTMFContact *con, int index); - /** Gets a analog contact for the specified index or @c nullptr if not defined. */ - DTMFContact *getAnalogContact(int index) const; - - /** Returns @c true, if the given RX group list index has been defined before. */ - bool hasGroupList(int index) const; - /** Adds a RX group list to the config and maps the given index to that group list. */ - bool addGroupList(RXGroupList *grp, int index); - /** Gets a RX group list for the specified index or @c nullptr if not defined. */ - RXGroupList *getGroupList(int index) const; - - /** Returns @c true, if the given scan list index has been defined before. */ - bool hasScanList(int index) const; - /** Adds a scan list to the config and maps the given index to that scan list. */ - bool addScanList(ScanList *lst, int index); - /** Gets a scan list for the specified index or @c nullptr if not defined. */ - ScanList *getScanList(int index) const; - - /** Returns @c true, if the given GPS system index has been defined before. */ - bool hasGPSSystem(int index) const; - /** Adds a GPS system to the config and maps the given index to that GPS system. */ - bool addGPSSystem(GPSSystem *sys, int index); - /** Gets a GPS system for the specified index or @c nullptr if not defined. */ - GPSSystem *getGPSSystem(int index) const; - - /** Returns @c true, if the given APRS system index has been defined before. */ - bool hasAPRSSystem(int index) const; - /** Adds an APRS system to the config and maps the given index to that APRS system. */ - bool addAPRSSystem(APRSSystem *sys, int index); - /** Gets an APRS system for the specified index or @c nullptr if not defined. */ - APRSSystem *getAPRSSystem(int index) const; - - /** Returns @c true if the given roaming zone index is defined. */ - bool hasRoamingZone(int index) const; - /** Associates the given roaming zone with the given index. */ - bool addRoamingZone(RoamingZone *zone, int index); - /** Returns the roaming zone associated with the given index. */ - RoamingZone *getRoamingZone(int index) const; - /** Returns @c true if a roaming channel is defined for the given index. */ - bool hasRoamingChannel(int index) const; - /** Associates the given digital channel as the roaming channel with the given index. */ - bool addRoamingChannel(DMRChannel *ch, int index); - /** Returns the roaming channel associated with the given index. */ - DMRChannel *getRoamingChannel(int index) const; - -protected: - /** The wrapped radio config (aka abstract code-plug). */ - Config *_config; - /** Maps a code-plug radio ID index to the radio ID index within the wrapped radio config. */ - QHash _radioIDTable; - /** Maps a code-plug channel index to the channel index within the wrapped radio config. */ - QHash _channelTable; - /** Maps a code-plug digital-contact index to the contact index within the wrapped radio config. */ - QHash _digitalContactTable; - /** Maps a code-plug analog-contact index to the contact index within the wrapped radio config. */ - QHash _analogContactTable; - /** Maps a code-plug RX group-list index to the group-list index within the wrapped radio config. */ - QHash _groupListTable; - /** Maps a code-plug scan-list index to the scan-list index within the wrapped radio config. */ - QHash _scanListTable; - /** Maps a code-plug GPS system index to the GPS system index within the wrapped radio config. */ - QHash _gpsSystemTable; - /** Maps a code-plug APRS system index to the APRS system index within the wrapped radio config. */ - QHash _aprsSystemTable; - /** Maps a code-plug roaming zone index to the roaming zone index within the wrapped radio config. */ - QHash _roamingZoneTable; - /** Maps a code-plug roaming channel index to a digital channel index within the wrapped radio config. */ - QHash _roamingChannelTable; -}; - -#endif // CODEPLUGCONTEXT_HH diff --git a/lib/d578uv_codeplug.hh b/lib/d578uv_codeplug.hh index 2be73c36..3c190aba 100644 --- a/lib/d578uv_codeplug.hh +++ b/lib/d578uv_codeplug.hh @@ -5,7 +5,6 @@ #include "d878uv_codeplug.hh" #include "signaling.hh" -#include "codeplugcontext.hh" class Channel; class DMRContact; diff --git a/lib/d868uv_codeplug.hh b/lib/d868uv_codeplug.hh index dd733fb7..451f6c06 100644 --- a/lib/d868uv_codeplug.hh +++ b/lib/d868uv_codeplug.hh @@ -5,7 +5,6 @@ #include "anytone_codeplug.hh" #include "signaling.hh" -#include "codeplugcontext.hh" class Channel; class DMRContact; diff --git a/lib/d878uv2_codeplug.hh b/lib/d878uv2_codeplug.hh index ae4b6866..85b84e0d 100644 --- a/lib/d878uv2_codeplug.hh +++ b/lib/d878uv2_codeplug.hh @@ -5,7 +5,6 @@ #include "d878uv_codeplug.hh" #include "signaling.hh" -#include "codeplugcontext.hh" class Channel; class DMRContact; diff --git a/lib/d878uv_codeplug.hh b/lib/d878uv_codeplug.hh index 87fdb451..18f3b54c 100644 --- a/lib/d878uv_codeplug.hh +++ b/lib/d878uv_codeplug.hh @@ -5,7 +5,6 @@ #include "d868uv_codeplug.hh" #include "signaling.hh" -#include "codeplugcontext.hh" class Channel; class DMRContact; diff --git a/lib/dm1701_codeplug.cc b/lib/dm1701_codeplug.cc index 0c8b0632..8ca4d5e3 100644 --- a/lib/dm1701_codeplug.cc +++ b/lib/dm1701_codeplug.cc @@ -1,5 +1,4 @@ #include "dm1701_codeplug.hh" -#include "codeplugcontext.hh" #include "logger.hh" #include diff --git a/lib/gd77_codeplug.hh b/lib/gd77_codeplug.hh index 8e9fd2da..ea7405b8 100644 --- a/lib/gd77_codeplug.hh +++ b/lib/gd77_codeplug.hh @@ -3,7 +3,6 @@ #include "radioddity_codeplug.hh" #include "signaling.hh" -#include "codeplugcontext.hh" /** Represents, encodes and decodes the device specific codeplug for a Radioddity GD-77. diff --git a/lib/md2017_codeplug.cc b/lib/md2017_codeplug.cc index d9c84933..f1e46f88 100644 --- a/lib/md2017_codeplug.cc +++ b/lib/md2017_codeplug.cc @@ -1,5 +1,4 @@ #include "md2017_codeplug.hh" -#include "codeplugcontext.hh" #include "logger.hh" #define NUM_CHANNELS 3000 diff --git a/lib/tyt_codeplug.cc b/lib/tyt_codeplug.cc index 88ea3877..238067a9 100644 --- a/lib/tyt_codeplug.cc +++ b/lib/tyt_codeplug.cc @@ -1,5 +1,4 @@ #include "tyt_codeplug.hh" -#include "codeplugcontext.hh" #include "config.hh" #include "utils.hh" #include "channel.hh" diff --git a/lib/uv390_codeplug.cc b/lib/uv390_codeplug.cc index 285446fa..a86d1263 100644 --- a/lib/uv390_codeplug.cc +++ b/lib/uv390_codeplug.cc @@ -1,5 +1,4 @@ #include "uv390_codeplug.hh" -#include "codeplugcontext.hh" #include "logger.hh" #include "tyt_extensions.hh" #include From 8a01a24b0849b078c744aa2f9eca74253cbc53a4 Mon Sep 17 00:00:00 2001 From: Hannes Matuschek Date: Fri, 16 Dec 2022 13:26:10 +0100 Subject: [PATCH 05/16] Fixed frequency limits for BTECH DMR-6X2UV. --- lib/dmr6x2uv.cc | 45 ++++++++++++++------------------------------- lib/dmr6x2uv.hh | 2 +- 2 files changed, 15 insertions(+), 32 deletions(-) diff --git a/lib/dmr6x2uv.cc b/lib/dmr6x2uv.cc index e946bb75..a1aea995 100644 --- a/lib/dmr6x2uv.cc +++ b/lib/dmr6x2uv.cc @@ -15,33 +15,36 @@ DMR6X2UV::DMR6X2UV(AnytoneInterface *device, QObject *parent) AnytoneInterface::RadioVariant info; _dev->getInfo(info); switch (info.bands) { case 0x00: - case 0x01: _limits = new DMR6X2UVLimits({ {136., 174.}, {400., 480.} }, { {136., 174.}, {400., 480.} }, info.version, this); break; + case 0x01: + _limits = new DMR6X2UVLimits({ {144., 146.}, {400., 480.} }, + { {144., 146.}, {420., 450.} }, info.version, this); + break; case 0x02: _limits = new DMR6X2UVLimits({ {136., 174.}, {430., 440.} }, { {136., 174.}, {430., 440.} }, info.version, this); break; case 0x03: - _limits = new DMR6X2UVLimits({ {136., 174.}, {400., 480.} }, + _limits = new DMR6X2UVLimits({ {144., 146.}, {430., 440.} }, { {144., 146.}, {430., 440.} }, info.version, this); break; case 0x04: - _limits = new DMR6X2UVLimits({ {144., 146.}, {434., 438.} }, - { {144., 146.}, {434., 438.} }, info.version, this); + _limits = new DMR6X2UVLimits({ {136., 174.}, {440., 480.} }, + { {136., 174.}, {440., 480.} }, info.version, this); break; case 0x05: - _limits = new DMR6X2UVLimits({ {144., 146.}, {434., 437.} }, - { {144., 146.}, {434., 437.} }, info.version, this); + _limits = new DMR6X2UVLimits({ {144., 146.}, {440., 480.} }, + { {144., 146.}, {440., 480.} }, info.version, this); break; case 0x06: _limits = new DMR6X2UVLimits({ {136., 174.}, {446., 447.} }, { {136., 174.}, {446., 447.} }, info.version, this); break; case 0x07: - _limits = new DMR6X2UVLimits({ {136., 174.}, {400., 480.} }, - { {144., 148.}, {420., 450.} }, info.version, this); + _limits = new DMR6X2UVLimits({ {144., 146.}, {446., 447.} }, + { {144., 146.}, {446., 447.} }, info.version, this); break; case 0x08: _limits = new DMR6X2UVLimits({ {136., 174.}, {400., 470.} }, @@ -56,33 +59,13 @@ DMR6X2UV::DMR6X2UV(AnytoneInterface *device, QObject *parent) { {144., 148.}, {430., 450.} }, info.version, this); break; case 0x0b: - _limits = new DMR6X2UVLimits({ {136., 174.}, {400., 520.} }, - { {136., 174.}, {400., 520.} }, info.version, this); + _limits = new DMR6X2UVLimits({ {136., 174.}, {400., 480.} }, + { {144., 146.}, {430., 440.} }, info.version, this); break; case 0x0c: - _limits = new DMR6X2UVLimits({ {136., 174.}, {400., 490.} }, - { {136., 174.}, {400., 490.} }, info.version, this); - break; - case 0x0d: - _limits = new DMR6X2UVLimits({ {136., 174.}, {400., 480.} }, + _limits = new DMR6X2UVLimits({ {136., 174.}, {403., 470.} }, { {136., 174.}, {403., 470.} }, info.version, this); break; - case 0x0e: - _limits = new DMR6X2UVLimits({ {136., 174.}, {220.,225.}, {400., 520.} }, - { {136., 174.}, {220.,225.}, {400., 520.} }, info.version, this); - break; - case 0x0f: - _limits = new DMR6X2UVLimits({ {144., 148.}, {420., 520.} }, - { {144., 148.}, {420., 520.} }, info.version, this); - break; - case 0x10: - _limits = new DMR6X2UVLimits({ {144., 147.}, {430., 440.} }, - { {144., 147.}, {430., 440.} }, info.version, this); - break; - case 0x11: - _limits = new DMR6X2UVLimits({ {136., 174.}, {430., 440.} }, - { {136., 174.} }, info.version, this); - break; default: logInfo() << "Unknown band-code" << QString::number(int(info.bands), 16) << ": Do not check frequency range."; diff --git a/lib/dmr6x2uv.hh b/lib/dmr6x2uv.hh index 9730738a..87fcad47 100644 --- a/lib/dmr6x2uv.hh +++ b/lib/dmr6x2uv.hh @@ -1,7 +1,7 @@ /** @defgroup dmr6x2uv BTECH DMR-6X2UV * Device specific classes for BTECH DMR-6X2UV. * - * Athough labeled BTECH (Baofeng USA), this device is basically a relabled AnyTone AT-D878UV. + * Athough labeled BTECH (Baofeng USA), this device is basically a relabled AnyTone AT-D868UV. * However, there are some minor differences in the codeplug format, hence it needs a separate * implementation. * From 6f7b1afcbd9874d2f418e10c1d3b1f0ad1b99439 Mon Sep 17 00:00:00 2001 From: Hannes Matuschek Date: Mon, 19 Dec 2022 13:33:55 +0100 Subject: [PATCH 06/16] Some more reverse engineering of DMR6X2UV. --- doc/code/dmr6x2uv_generalsettings.txt | 65 ++++++++------- doc/code/dmr6x2uv_settingsextension.txt | 39 +++++++++ lib/dmr6x2uv_codeplug.hh | 63 +++++++-------- lib/libdmrconf.hh | 100 ++++++++++++++++++++++-- 4 files changed, 198 insertions(+), 69 deletions(-) create mode 100644 doc/code/dmr6x2uv_settingsextension.txt diff --git a/doc/code/dmr6x2uv_generalsettings.txt b/doc/code/dmr6x2uv_generalsettings.txt index b4e0fe62..3fb5c977 100644 --- a/doc/code/dmr6x2uv_generalsettings.txt +++ b/doc/code/dmr6x2uv_generalsettings.txt @@ -1,6 +1,6 @@ 7 6 5 4 3 2 1 0 7 6 5 4 3 2 1 0 7 6 5 4 3 2 1 0 7 6 5 4 3 2 1 0 +---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+ -00 | Enable key tone | Display mode | Enable automatic key lock | Automatic shutdown time | +00 | Idle channel tone | Display mode | Enable automatic key lock | Automatic shutdown time | +---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+ 04 | Transmit timeout in 30s | Language | Boot display | Enable boot password | +---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+ @@ -26,7 +26,7 @@ +---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+ 30 | GPS Time zone | Talk permit tone | Digital call reset tone | VOX source | +---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+ -34 | Enable pro mode | Unused set to 0x00 | Idle channel tone | Menu exit time | +34 | Enable pro mode | Unused set to 0x00 | Enable key tone | Menu exit time | +---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+ 38 | Filter own ID enable | Startup tone | Enable call end prompt | Max speaker volume | +---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+ @@ -38,23 +38,23 @@ +---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+ 48 | Auto repeater A direction | Digital monitor slot | Digital monitor color code | Digital monitor match ID | +---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+ -4c | Digital monitor hold slot | Display later caller | Unknown | Man down delay in sec | +4c | Digital monitor hold slot | Display last caller | Unknown | Man down delay in sec | +---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+ -50 | Analog call hold in seconds | Display clock | Max head phone volume | Enable GPS range message | +50 | Analog call hold in seconds | Enable display clock | Max head phone volume | Enable GPS range message | +---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+ 54 | Unknown settings | Enable enhanced audio | +---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+ -58 | VFO Scan UHF minimum frequency in 10Hz, 8-digit BCD, little endian | +58 | VFO Scan UHF minimum frequency in 10Hz, little endian | +---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+ -5c | VFO Scan UHF maximum frequency in 10Hz, 8-digit BCD, little endian | +5c | VFO Scan UHF maximum frequency in 10Hz, little endian | +---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+ -60 | VFO Scan VHF minimum frequency in 10Hz, 8-digit BCD, little endian | +60 | VFO Scan VHF minimum frequency in 10Hz, little endian | +---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+ -64 | VFO Scan VHF maximum frequency in 10Hz, 8-digit BCD, little endian | +64 | VFO Scan VHF maximum frequency in 10Hz, little endian | +---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+ -68 | Auto rep. offset index UHF | Auto rep. offset index VHF | Unknown ... +68 | Autorep. off. idx UHF, ffh=off| Autorep. off. idx VHF, ffh=off| Unknown ... +---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+ -6c | | Maintain call channel | Priority zone A index | +6c ... | Maintain call channel | Priority zone A index | +---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+ 70 | Priority zone B index | Unused set to 0x00 | Call tone frequency 1 in Hz, little endian | +---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+ @@ -88,35 +88,44 @@ a8 | Reset tone duration 3 in ms, little endian | Reset tone +---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+ ac | Reset tone duration 5 in ms, little endian | Record delay in 200ms | Call display mode | +---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+ -b0 | Unknown, set to 0x01 | Simplex Repeater 0=Off, 1=On | Unknown set to 0x05 | Sp. dur. RX. Simpl. Rep. | +b0 | Call-sign display color | Enable simplex repeater | GPS ranging interv. in sec. | Enable speaker in simpl. rep. | +---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+ -b4 | Unknown, set to 0x00 | GPS ranging interval in sec | Unknown set to 0x0f | SimplRepSlot 0=TS1 1=TS2 2=CH | +b4 | Show current contact | Key sound adjustable | | SimplRepSlot 0=TS1 1=TS2 2=CH | +---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+ -b8 | Display channel number | Display contact | Auto roaming period in min | Key tone level | +b8 | | GPS units | | +---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+ -bc | Callsign color | GPS unit | 0 0 0 |KLF|LSK| 0 |LKB|LKN| Auto roam delay in seconds | +bc | Auto repeater lower VHF frequency, in 10Hz, little endian | +---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+ -c0 | Standby text color | Standby image color | Show last heard | SMS format | +c0 | Auto repeater upper VHF frequency, in 10Hz, little endian | +---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+ -c4 | Auto repeater VHF min frequency in 10Hz, 8-digit BCD, big endian | +c4 | Auto repeater lower UHF frequency, in 10Hz, little endian | +---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+ -c8 | Auto repeater VHF max frequency in 10Hz, 8-digit BCD, big endian | +c8 | Auto repeater upper UHF frequency, in 10Hz, little endian | +---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+ -cc | Auto repeater UHF min frequency in 10Hz, 8-digit BCD, big endian | +cc | Auto repeater B direction | | +---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+ -d0 | Auto repeater UHF max frequency in 10Hz, 8-digit BCD, big endian | +d0 | | Enable keep last caller | +---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+ -d4 | Auto rep. VFO B direction | Unknown | Unused set to 0x00 | Boot channel enable | +d4 | RX Backlight delay | Display mode | | +---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+ -d8 | VFO A default zone index | VFO B default zone index | VFO A default channel index | VFO B default channel index | +d8 | | +---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+ -dc | Default roaming zone index | Repeater range check enable | Repeater check interval in 5s | RepCheck reconnect count | +dc | | +---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+ Field description: - - SRP: Simplex repeater - - KLF: Key lock forced enable - - LSK: Lock side key - - LKB: Keyboard lock - - LKN: Knob lock - + - VFO scan type: 0x00=TO, 0x01=CO, 0x02=SE + - Talk permit tone: 0x00=Off, 0x01=Digital, 0x02=Analog, 0x03=Both + - Display brightness: 0x00=1, ..., 0x04=5 + - Backlight duration: 0x00=Always, 0x01=5s, ..., 0x06=30s, 0x07=1m, ..., 0x0a=5m + - Menu exit time: (n+1)*5s + - Auto repeater A/B direction: 0x00=Off, 0x01=positive, 0x02=negative + - Display last caller: 0x00=Off, 0x01=Show ID, 0x02=Show name, 0x03=Both + - Call display mode: 0x00=Name based, 0x01=Call-sign based + - Call-sign display color: 0x00=orange, 0x01=red, 0x02=yellow, 0x03=green, 0x04=turquoise, 0x05=blue, 0x06=white, 0x07=black + - Enable keep last caller: A channel switch keeps laster caller. + - RX Backlight delay: 0x00=Always, 0x01=1s, ..., 0x1e=30s. + - Display mode: 0x00=black background, 0x01=blue background + - Key sound adjustable: 0x00=Adjustable, 0x01=1, ..., 0x0f=15 + - GPS Time zone: 0x00=GMT-12, ..., 0x0c=GMT, 0x19=GMT+13 + - GPS units: 0x00=metric, 0x01=imperial diff --git a/doc/code/dmr6x2uv_settingsextension.txt b/doc/code/dmr6x2uv_settingsextension.txt new file mode 100644 index 00000000..7579e3e8 --- /dev/null +++ b/doc/code/dmr6x2uv_settingsextension.txt @@ -0,0 +1,39 @@ + 7 6 5 4 3 2 1 0 7 6 5 4 3 2 1 0 7 6 5 4 3 2 1 0 7 6 5 4 3 2 1 0 + +---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+ +00 | | Font Color | + +---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+ +04 | Enable custom ch. background | Roaming zone index 0-based | Enable auto roaming | Enable repeater check | + +---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+ +08 | Out of repeater range alert | Rep. out of range reminder | Repeater check intervall | Repeater reconnections | + +---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+ +0c | Start roaming condition | Auto roaming intervall | Roaming eff. waiting time | Roaming return condition | + +---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+ +10 | | Zone A name color | Zone B name color | + +---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+ +14 | Channel A name color | Channel B name color | | + +---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+ +18 | | + +---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+ +1c | | + +---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+ +20 | | + +---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+ +24 | | + +---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+ +28 | | + +---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+ +2c | | + +---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+ + +Field description: + - Font Color: 0x00=White, 0x01=Black, 0x02=Orange, 0x03=Red, 0x04=Yellow, 0x05=Green, 0x06=Turquoise, 0x07=Blue + - Zone/Channel A/B name color: 0x00=Orange, 0x01=Red, 0x02=Yellow, 0x03=Green, 0x04=Turquoise, 0x05=Blue, 0x06=White, 0x07=Black + - Out of repeater range alert: 0x00=Off, 0x01=Bell, 0x02=Voice + - Rep. out of range reminder: Number of times, the reminder is shown (n-1), 0x00=1, 0x01=2, ..., 0x09=10 + - Repeater check intervall (n+1)*5 seconds: 0x00=5s, ..., 0x09=50s + - Repeater reconnections: 0x00=3, 0x01=4, 0x03=5 + - Start roaming condition: 0x00=fixed time, 0x01=out of range + - Auto roaming intervall: 0x00=1min, ..., 0xff=256min + - Roaming eff. waiting time: 0x00=off, 0x01=1s, ..., 0x1e=30s + - Roaming return condition: 0x00=fixed time, 0x01=out of range + diff --git a/lib/dmr6x2uv_codeplug.hh b/lib/dmr6x2uv_codeplug.hh index db4e8ae1..fc5d5097 100644 --- a/lib/dmr6x2uv_codeplug.hh +++ b/lib/dmr6x2uv_codeplug.hh @@ -1,34 +1,12 @@ #ifndef DMR6X2UVCODEPLUG_HH #define DMR6X2UVCODEPLUG_HH -#include "d868uv_codeplug.hh" #include "d878uv_codeplug.hh" /** Represents the device specific binary codeplug for BTECH DMR-6X2UV radios. * * This codeplug implementation is compatible with firmware revision 2.04. * - * In contrast to many other codeplugs, the codeplug for Anytone radios are spread over a large - * memory area. In principle, this is a good idea, as it allows one to upload only the portion of the - * codeplug that is actually configured. For example, if only a small portion of the available - * contacts and channels are used, the amount of data that is written to the device can be - * reduced. - * - * However, the implementation of this idea in this device is utter shit. The amount of - * fragmentation of the codeplug is overwhelming. For example, while channels are organized more or - * less nicely in continuous banks, zones are distributed throughout the entire codeplug. That is, - * the names of zones are located in a different memory section that the channel lists. Some lists - * are defined though bit-masks others by byte-masks. All bit-masks are positive, that is 1 - * indicates an enabled item while the bit-mask for contacts is inverted. - * - * In general the codeplug is huge and complex. Moreover, the radio provides a huge amount of - * options and features. To this end, reverse engineering this codeplug was a nightmare. - * - * More over, the binary codeplug file generate by the windows CPS does not directly relate to - * the data being written to the radio. To this end the codeplug has been reverse-engineered - * using wireshark to monitor the USB communication between the windows CPS (running in a virtual - * box) and the device. The latter makes the reverse engineering particularly cumbersome. - * * @section dmr6x2uvcpl Codeplug structure within radio * * @@ -36,13 +14,13 @@ * * + * see @c AnytoneCodeplug::ChannelElement 64 b each. * * * + * see @c AnytoneCodeplug::ChannelElement. * + * see @c AnytoneCodeplug::ChannelElement. * * * @@ -59,11 +37,11 @@ * * + * See @c D868UVCodeplug::RoamingChannelElement for details. * * + * See @c D868UVCodeplug::RoamingZoneElement for details. * * * @@ -120,19 +98,21 @@ * * * + * see @c D868UVCodeplug::AnalogAPRSSettingsElement. * + * see @c D868UVCodeplug::DMRAPRSSystemsElement. * + * * * * * + * see @c DMR6X2UVCodeplug::GeneralSettingsElement. * * * + * * @@ -166,7 +146,7 @@ * see @c D868UVCodeplug::dmr_encryption_key_t, * 40b each. * + * See @c D868UVCodeplug::AESEncryptionKeyElement. * * * @@ -202,8 +182,6 @@ * * * - * - * * *
Channels
024C1500 000200 Bitmap of 4000 channels, default 0x00, * 0x00 padded.
00800000 max. 002000 Channel bank 0 of up to 128 channels, - * see @c D878UVCodeplug::ChannelElement 64 b each.
00840000 max. 002000 Channel bank 1 of up to 128 channels.
00FC0000 max. 000800 Channel bank 32, up to 32 channels.
00FC0800 000040 VFO A settings, - * see @c D878UVCodeplug::ChannelElement.
00FC0840 000040 VFO B settings, - * see @c D878UVCodeplug::ChannelElement.
Zones
Start Size Content
01042000 000020 Roaming channel bitmask, up to 250 bits, * 0-padded, default 0.
01040000 max. 0x1f40 Optional up to 250 roaming channels, of 32b each. - * See @c D878UVCodeplug::RoamingChannelElement for details.
01042080 000010 Roaming zone bitmask, up to 64 bits, 0-padded, * default 0.
01043000 max. 0x2000 Optional up to 64 roaming zones, of 128b each. - * See @c D878UVCodeplug::RoamingZoneElement for details.
Contacts
Start Size Content
GPS/APRS
Start Size Content
02501000 000040 APRS settings, - * see @c D878UVCodeplug::AnalogAPRSSettingsElement.
02501040 000060 APRS settings, - * see @c D878UVCodeplug::DMRAPRSSystemsElement.
02501200 000040 APRS Text, up to 60 chars ASCII, 0-padded.
02501280 000030 GPS template message, ASCII, 0-padded.
General Settings
Start Size Content
02500000 0000e0 General settings, - * see @c D878UVCodeplug::GeneralSettingsElement.
02500100 000400 Zone A & B channel list.
02500500 000100 DTMF list
02500600 000030 Power on settings, * see @c AnytoneCodeplug::BootSettingsElement.
02501400 000030 Settings extension, see @c DMR6X2UVCodeplug::ExtendedSettingsElement.
024C2000 0003F0 List of 250 auto-repeater offset frequencies. * 32bit little endian frequency in 10Hz. I.e., 600kHz = 60000. * Default 0x00000000, 0x00 padded.
024C4000 004000 Up to 256 AES encryption keys. - * See @c D878UVCodeplug::AESEncryptionKeyElement.
Misc
Start Size Content
024C2630 000020 Unknown bitmap.
024C3000 000020 Unknown settings.
024C5000 000020 Unknown settings.
02501280 000030 Unknown settings.
02501400 000030 Uknonwn settings.
025C1000 005000 Unknown settings.
* @@ -253,6 +231,25 @@ public: virtual bool updateConfig(Context &ctx); }; + /** Implements some settings extension for the BTECH DMR-6X2UV. + * + * Memory representation of the encoded settings element (size 0x0e0 bytes): + * @verbinclude dmr6x2uv_settingsextension.txt */ + class ExtendedSettingsElement: public Codeplug::Element + { + protected: + /** Hidden Constructor. */ + ExtendedSettingsElement(uint8_t *ptr, unsigned size); + + public: + /** Constructor. */ + explicit ExtendedSettingsElement(uint8_t *ptr); + + /** Resets the general settings. */ + void clear(); + }; + + public: /** Empty constructor. */ explicit DMR6X2UVCodeplug(QObject *parent=nullptr); diff --git a/lib/libdmrconf.hh b/lib/libdmrconf.hh index 4954cd6b..d825844a 100644 --- a/lib/libdmrconf.hh +++ b/lib/libdmrconf.hh @@ -1,8 +1,7 @@ /** @mainpage libdmrconf -- A Library to program DMR radios. * Libdmrconf is a Qt based library, providing methods to assemble, read and program codeplugs for * cheap Chinese DMR radios. To this end, it aims at providing a common way to describe codeplugs - * irrespective of the actual radio being used. This project relies heavily on the work by - * Serge Vakulenko (https://github.com/sergev/dmrconfig). + * irrespective of the actual radio being used. * * The aim of this project is, to define a minimal but sufficient generic configuration for all * radios, such that all relevant ham-radio features of the radios can be used. This is a challenge, @@ -31,6 +30,92 @@ * If a radio does not support a particular feature within the configuration, it simply gets * ignored. The configuration language as well as classes for reading and writing config files are * documented in the manual. + * + * @section usage Using libdmrconf + * - Using cmake to find libdmrconf + * - setting paths + * - linking + * - include files + * + * @section interface Interfacing radios + * In a first step, detect all USB devices that could be connected radios. Some radios use generic + * USB CDC-AMC (USB-to-serial) chips. In these cases be careful, that you do not confuse them with + * other hardware connected to the host. So, to detect USB interfaces use the device specific + * interfaces, e.g. @c AnytoneInterface::detect(), @c RadioddityInterface::detect() or + * @c TyTInterface::detect(). For example + * @code + * #include + * + * int main(void) { + * // Find any supported devices connected + * QList devices = AnytoneInterface::detect() + + * TyTInterface::detect() + + * RadioddityInterface::detect() + + * OpenGD77Interface::detect(); + * if (0 == devices.size()) { + * std::cerr << "No matching device found."; + * return -1; + * } + * + * // As the connected radio may use a generic USB CDC-ACM chip, check if the interface is safe. + * if (! devices.first().isSave()) { + * std::cerr << "Sorry, the detected device is not save to talk to automatically, " + * "check if the chosen device is the correct one."; + * return -1; + * } + * + * // Then try to connect to the first found. + * // The radio interface is common to all devices. So the remaining code is device independent. + * Radio *radio = Radio::detect(devices.first(), RadioInfo(), err); + * if (nullptr == radio) { + * std::cerr << "Opps:" << err.format(); + * return -1; + * } + * + * // Download codeplug + * if (! radio->startDownload(true, err)) { + * std::cerr << "Error while downloading codeplug:" << err.format(); + * delete radio; + * return -1; + * } + * + * // Decode codeplug to device independent config + * Config config; + * if (! radio->codeplug().decode(&config, err)) { + * std::cerr << "Cannot decode codeplug:" << err.format(); + * delete radio; + * return -1; + * } + * + * // Now, manipulate the generic codeplug in config. + * + * // Verify codeplug with radio + * RadioLimitContext issues; + * if (! radio->limits().verifyConfig(config, issues)) { + * // Dump issues... + * delete radio; + * return -1; + * } + * + * // Encode and upload codeplug back to the device + * if (! radio->startUpload(config, true, Codeplug::Flags(), err)) { + * std::err << "Cannot write codeplug: " << err.format(); + * delete radio; + * return -1; + * } + * + * // Done. + * delete radio; + * return 0; + * } + * @endcode + * + * @section codeplug Reading and writing codeplug files + * - Using YAML for reading and writing codeplug files + * + * @section config Manipulating codeplugs + * - Some examples for basic manipulations of the @c Config object. + * */ #ifndef __LIBDMRCONF_HH__ @@ -38,14 +123,13 @@ #include "config.h" #include "config.hh" -#include "codeplug.hh" #include "csvreader.hh" #include "radio.hh" -#include "uv390.hh" -#include "rd5r.hh" -#include "opengd77.hh" -#include "gd77.hh" -#include "d878uv.hh" + +#include "anytone_interface.hh" +#include "tyt_interface.hh" +#include "opengd77_interface.hh" +#include "radioddity_interface.hh" #endif // __LIBDMRCONF_HH__ From e954645ec4654360b3cda4867fd7a385c6de2479 Mon Sep 17 00:00:00 2001 From: Hannes Matuschek Date: Mon, 19 Dec 2022 16:03:20 +0100 Subject: [PATCH 07/16] Checked general settings for DMR-6X2UV. --- doc/code/dmr6x2uv_generalsettings.txt | 54 +++++++++++++++++++------ doc/code/dmr6x2uv_settingsextension.txt | 3 +- lib/dmr6x2uv_codeplug.cc | 9 ++++- lib/dmr6x2uv_codeplug.hh | 12 +++++- 4 files changed, 61 insertions(+), 17 deletions(-) diff --git a/doc/code/dmr6x2uv_generalsettings.txt b/doc/code/dmr6x2uv_generalsettings.txt index 3fb5c977..15a4ba87 100644 --- a/doc/code/dmr6x2uv_generalsettings.txt +++ b/doc/code/dmr6x2uv_generalsettings.txt @@ -10,11 +10,11 @@ +---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+ 10 | PF1 short press function | PF2 short press function | PF3 short press function | P1 short press function | +---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+ -14 | P2 short press function | Work mode A | Work mode B | STE Type | +14 | P2 short press function | Work mode B | Work mode A | STE Type | +---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+ 18 | STE freq. no signal | Group call hang time in sec | Private call hang time in sec | Prewave time in 20ms | +---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+ -1c | Wake head period in 20ms | WFM channel index | WFM VFO Mode enable | Work mode MEM zone A | +1c | Wake head period in 20ms | WFM channel index | WFM VFO Mode enable | Work mode MEM zone A idx | +---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+ 20 | Work mode MEM zone B | Unused set to 0x00 | Enable recording | DTMF duration | +---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+ @@ -22,7 +22,7 @@ +---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+ 28 | Enable GPS | Enable SMS alert | Unused set to 0x00 | WFM monitor enable | +---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+ -2c | Work mode main channel set | Enable sub channel | TBST frquency | Enable call alert | +2c | Work mode main channel | Enable sub channel | TBST frquency | Enable call alert | +---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+ 30 | GPS Time zone | Talk permit tone | Digital call reset tone | VOX source | +---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+ @@ -32,7 +32,7 @@ +---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+ 3c | Remote stun/kill enable | Unused set to 0x00 | Remote monitor enable | GPS RX positions | +---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+ -40 | Unknown | PF1 long press function | PF2 long press function | PF3 long press function | +40 | Enable select TX contact | PF1 long press function | PF2 long press function | PF3 long press function | +---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+ 44 | P1 long press function | P2 long press function | Long press duration | Enable Volume change prompt | +---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+ @@ -40,7 +40,7 @@ +---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+ 4c | Digital monitor hold slot | Display last caller | Unknown | Man down delay in sec | +---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+ -50 | Analog call hold in seconds | Enable display clock | Max head phone volume | Enable GPS range message | +50 | Analog call hold in seconds | Enable display clock | Max headphone volume | Enable GPS range message | +---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+ 54 | Unknown settings | Enable enhanced audio | +---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+ @@ -54,9 +54,9 @@ +---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+ 68 | Autorep. off. idx UHF, ffh=off| Autorep. off. idx VHF, ffh=off| Unknown ... +---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+ -6c ... | Maintain call channel | Priority zone A index | +6c ... | Enable maintain call channel | Priority zone A idx, 0xff=off | +---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+ -70 | Priority zone B index | Unused set to 0x00 | Call tone frequency 1 in Hz, little endian | +70 | Priority zone B idx, 0xff=off | Enable SMS confirmation | Call tone frequency 1 in Hz, little endian | +---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+ 74 | Call tone frequency 2 in Hz, little endian | Call tone frequency 3 in Hz, little endian | +---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+ @@ -90,9 +90,9 @@ ac | Reset tone duration 5 in ms, little endian | Record dela +---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+ b0 | Call-sign display color | Enable simplex repeater | GPS ranging interv. in sec. | Enable speaker in simpl. rep. | +---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+ -b4 | Show current contact | Key sound adjustable | | SimplRepSlot 0=TS1 1=TS2 2=CH | +b4 | Show current contact | Key sound adjustable | 0 0 0 0 |KNL|KBL|SKL|PKL| SimplRepSlot 0=TS1 1=TS2 2=CH | +---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+ -b8 | | GPS units | | +b8 | Show last call on startup | SMS format | GPS units | | +---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+ bc | Auto repeater lower VHF frequency, in 10Hz, little endian | +---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+ @@ -102,11 +102,11 @@ c4 | Auto repeater lower UHF frequency, in 10Hz, little endian +---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+ c8 | Auto repeater upper UHF frequency, in 10Hz, little endian | +---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+ -cc | Auto repeater B direction | | +cc | Auto repeater B direction | AddrBk is send with own code | Enable default boot channel | Boot zone A idx | +---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+ -d0 | | Enable keep last caller | +d0 | Boot zone B idx | Boot ch A idx, 0xff=VFO | Boot ch B idx, 0xff=VFO | Enable keep last caller | +---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+ -d4 | RX Backlight delay | Display mode | | +d4 | RX Backlight delay | Display mode | ManDial GrpCall hgn time sec. | ManDial PrvCall hgn time sec. | +---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+ d8 | | +---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+ @@ -114,12 +114,36 @@ dc | +---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+ Field description: + - Display mode: 0x00=Channel, 0x01=Frequency + - VFO frequency step: 0x00=2.5kHz, 0x01=5kHz, 0x02=6.25kHz, 0x03=10kHz, 0x04=12.5kHz, 0x05=20kHz, 0x06=25kHz, 0x07=30kHz, 0x08=50kHz + - Squelch level: 0x00=Off, 0x01=1, ..., 0x05=5 + - Automatic shutdown time: 0x00=Off, 0x01=10min, 0x02=30min, 0x03=60min, 0x04=120min + - Power save mode: 0x00=Off, 0x01=1:1, 0x02=2:1 + - VOX sensitivity: 0x00=Off, 0x01=1, ..., 0x03=3 + - Work mode A/B: 0x00=VFO, 0x01=MEM + - Work mode main channel: 0x00=A, 0x01=B + - TBST frquency: 0x00=1000Hz, 0x01=1450Hz, 0x02=1750Hz, 0x03=2100Hz + - GPS Time zone: 0x00=GMT-12, ..., 0x0c=GMT, 0x19=GMT+13 + - Boot display: 0x00=Default, 0x01=Custom Text, 0x02=Custom Image - VFO scan type: 0x00=TO, 0x01=CO, 0x02=SE + - Key press function: 0x00=Off, 0x01=Voltage, 0x02=Power, 0x03=Repeater, 0x04=Reverse, 0x05=Digital Encryption, 0x06=Call, 0x07=VOX, + 0x08=V/M, 0x09=Sub PTT, 0x0a=Scan, 0x0b=FM, 0x0c=Alarm, 0x0d=Record Switch, 0x0e=Record, 0x0f=SMS, 0x10=Dial, + 0x11=GPS Information, 0x12=Monitor, 0x13=Main Ch. switch, 0x14=HotKey 1, ..., 0x19=HotKey 6, 0x1a=Work Alone, + 0x1b=Nuisance Delete, 0x1c=Digital Monitor, 0x1d=Sub Ch Switch, 0x1e=Priority Zone, 0x1f=Programming Scan, + 0x20=MIC sound quality, 0x21=Last call reply, 0x22=Channel type switch, 0x23=Simplex repeater, 0x24=Max. volume, + 0x25=Ranging, 0x26=Channel ranging, 0x27=Slot switch, 0x28=Analog squelch, 0x29=Roaming, 0x2a=Zone select, + 0x2b=Roaming settings, 0x2c=FixTime Mute, 0x2d=CTCSS/DCS settings, 0x2e=APRS type, 0x2f=APRS settings - Talk permit tone: 0x00=Off, 0x01=Digital, 0x02=Analog, 0x03=Both + - STE Type: 0x00=Off, 0x01=Silent, 0x02=120deg, 0x03=180deg, 0x04=240deg + - STE freq. no signal: 0x00=Off, 0x01=55.2Hz, 0x02=259.2Hz + - VOX source: 0x00=Internal, 0x01=External, 0x02=Both - Display brightness: 0x00=1, ..., 0x04=5 - Backlight duration: 0x00=Always, 0x01=5s, ..., 0x06=30s, 0x07=1m, ..., 0x0a=5m + - MIC gain: 0x00=1, ..., 0x04=5 + - Max speaker/headphone volume: 0x00=Indoors, 0x01=1, ..., 0x08=8 - Menu exit time: (n+1)*5s - Auto repeater A/B direction: 0x00=Off, 0x01=positive, 0x02=negative + - Digital monitor slot: 0x00=Off, 0x01=single, 0x02=both´ - Display last caller: 0x00=Off, 0x01=Show ID, 0x02=Show name, 0x03=Both - Call display mode: 0x00=Name based, 0x01=Call-sign based - Call-sign display color: 0x00=orange, 0x01=red, 0x02=yellow, 0x03=green, 0x04=turquoise, 0x05=blue, 0x06=white, 0x07=black @@ -127,5 +151,9 @@ Field description: - RX Backlight delay: 0x00=Always, 0x01=1s, ..., 0x1e=30s. - Display mode: 0x00=black background, 0x01=blue background - Key sound adjustable: 0x00=Adjustable, 0x01=1, ..., 0x0f=15 - - GPS Time zone: 0x00=GMT-12, ..., 0x0c=GMT, 0x19=GMT+13 - GPS units: 0x00=metric, 0x01=imperial + - KNL: Knob lock enable + - KBL: Keyboard lock enable + - SKL: Side key lock enable + - PKL: Professional key lock enable + - SMS format: 0x00=M-format, 0x01=H-format, 0x02=DMR standard diff --git a/doc/code/dmr6x2uv_settingsextension.txt b/doc/code/dmr6x2uv_settingsextension.txt index 7579e3e8..27f051ca 100644 --- a/doc/code/dmr6x2uv_settingsextension.txt +++ b/doc/code/dmr6x2uv_settingsextension.txt @@ -8,7 +8,7 @@ +---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+ 0c | Start roaming condition | Auto roaming intervall | Roaming eff. waiting time | Roaming return condition | +---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+ -10 | | Zone A name color | Zone B name color | +10 | Mute timer (n+1) min | Encryption type | Zone A name color | Zone B name color | +---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+ 14 | Channel A name color | Channel B name color | | +---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+ @@ -27,6 +27,7 @@ Field description: - Font Color: 0x00=White, 0x01=Black, 0x02=Orange, 0x03=Red, 0x04=Yellow, 0x05=Green, 0x06=Turquoise, 0x07=Blue + - Encryption type: 0x00=Common(Basic), 0x01=AES - Zone/Channel A/B name color: 0x00=Orange, 0x01=Red, 0x02=Yellow, 0x03=Green, 0x04=Turquoise, 0x05=Blue, 0x06=White, 0x07=Black - Out of repeater range alert: 0x00=Off, 0x01=Bell, 0x02=Voice - Rep. out of range reminder: Number of times, the reminder is shown (n-1), 0x00=1, 0x01=2, ..., 0x09=10 diff --git a/lib/dmr6x2uv_codeplug.cc b/lib/dmr6x2uv_codeplug.cc index 3a1e2d61..5ff58242 100644 --- a/lib/dmr6x2uv_codeplug.cc +++ b/lib/dmr6x2uv_codeplug.cc @@ -23,17 +23,22 @@ * Implementation of DMR6X2UVCodeplug::GeneralSettingsElement * ********************************************************************************************* */ DMR6X2UVCodeplug::GeneralSettingsElement::GeneralSettingsElement(uint8_t *ptr, unsigned size) - : D878UVCodeplug::GeneralSettingsElement(ptr, size) + : AnytoneCodeplug::GeneralSettingsElement(ptr, size) { // pass... } DMR6X2UVCodeplug::GeneralSettingsElement::GeneralSettingsElement(uint8_t *ptr) - : D878UVCodeplug::GeneralSettingsElement(ptr, 0x00e0) + : AnytoneCodeplug::GeneralSettingsElement(ptr, 0x00e0) { // pass... } +DMR6X2UVCodeplug::GeneralSettingsElement::DisplayColor +DMR6X2UVCodeplug::GeneralSettingsElement::callsignDisplayColor() const { + return (DisplayColor)getUInt8(0x00b0); +} + bool DMR6X2UVCodeplug::GeneralSettingsElement::simplexRepeaterEnabled() const { return 0x01 == getUInt8(0x00b2); diff --git a/lib/dmr6x2uv_codeplug.hh b/lib/dmr6x2uv_codeplug.hh index fc5d5097..8b59b54d 100644 --- a/lib/dmr6x2uv_codeplug.hh +++ b/lib/dmr6x2uv_codeplug.hh @@ -195,9 +195,14 @@ public: * * Memory representation of the encoded settings element (size 0x0e0 bytes): * @verbinclude dmr6x2uv_generalsettings.txt */ - class GeneralSettingsElement: public D878UVCodeplug::GeneralSettingsElement + class GeneralSettingsElement: public AnytoneCodeplug::GeneralSettingsElement { public: + /** Possible colors for the callsign etc. */ + enum class DisplayColor { + Orange=0x00, Red=0x01, Yellow=0x02, Green=0x03, Turquoise=0x04, Blue=0x05, White=0x06, Black=0x07 + }; + /** Possible GPS modes. */ enum class SimplexRepeaterSlot { TS1 = 0, TS2 = 1, Channel = 2 @@ -211,6 +216,11 @@ public: /** Constructor. */ explicit GeneralSettingsElement(uint8_t *ptr); + /** Retunrs the color for call-signs. */ + virtual DisplayColor callsignDisplayColor() const; + /** Sets the color for call-signs. */ + void setCallsignDisplayColor(DisplayColor color); + /** Returns @c true if the simplex repeater feature is enabled. */ virtual bool simplexRepeaterEnabled() const; /** Enables disables the simplex repeater feature. */ From 0077171f9c6cf824246b3ba1a46b6408593c8f13 Mon Sep 17 00:00:00 2001 From: Hannes Matuschek Date: Wed, 21 Dec 2022 08:57:32 +0100 Subject: [PATCH 08/16] Implemented GeneralSettingsElement and ExtendedSettingsElement for BTECH DMR-6X2UV. --- lib/dmr6x2uv_codeplug.cc | 451 ++++++++++++++++++++++++++++++++++++++- lib/dmr6x2uv_codeplug.hh | 261 ++++++++++++++++++++++ 2 files changed, 710 insertions(+), 2 deletions(-) diff --git a/lib/dmr6x2uv_codeplug.cc b/lib/dmr6x2uv_codeplug.cc index 5ff58242..a6303604 100644 --- a/lib/dmr6x2uv_codeplug.cc +++ b/lib/dmr6x2uv_codeplug.cc @@ -79,10 +79,253 @@ DMR6X2UVCodeplug::GeneralSettingsElement::setSimplexRepeaterTimeslot(SimplexRepe } } +unsigned int +DMR6X2UVCodeplug::GeneralSettingsElement::gpsRangingIntervall() const { + return getUInt8(0x00b1); +} +void +DMR6X2UVCodeplug::GeneralSettingsElement::setGPSRangingIntervall(unsigned int sec) { + setUInt8(0x00b1, sec); +} + +bool +DMR6X2UVCodeplug::GeneralSettingsElement::currentContactShown() const { + return 0x01 == getUInt8(0x00b4); +} +void +DMR6X2UVCodeplug::GeneralSettingsElement::showCurrentContact(bool show) { + setUInt8(0x00b4, show ? 0x01 : 0x00); +} + +unsigned int +DMR6X2UVCodeplug::GeneralSettingsElement::keySoundVolume() const { + return getUInt8(0x00b5); +} +void +DMR6X2UVCodeplug::GeneralSettingsElement::setKeySoundVolume(unsigned int vol) { + vol = std::min(15U, vol); + setUInt8(0x00b5, vol); +} + +bool +DMR6X2UVCodeplug::GeneralSettingsElement::professionalKeyLock() const { + return getBit(0x00b6, 0); +} +void +DMR6X2UVCodeplug::GeneralSettingsElement::enableProfessionalKeyLock(bool enable) { + setBit(0x00b6, 0, enable); +} + +bool +DMR6X2UVCodeplug::GeneralSettingsElement::sideKeyLock() const { + return getBit(0x00b6, 1); +} +void +DMR6X2UVCodeplug::GeneralSettingsElement::enableSideKeyLock(bool enable) { + setBit(0x00b6, 1, enable); +} + +bool +DMR6X2UVCodeplug::GeneralSettingsElement::keyboadLock() const { + return getBit(0x00b6, 2); +} +void +DMR6X2UVCodeplug::GeneralSettingsElement::enableKeyboradLock(bool enable) { + setBit(0x00b6, 2, enable); +} + +bool +DMR6X2UVCodeplug::GeneralSettingsElement::knobLock() const { + return getBit(0x00b6, 3); +} +void +DMR6X2UVCodeplug::GeneralSettingsElement::enableKnobLock(bool enable) { + setBit(0x00b6, 3, enable); +} + +bool +DMR6X2UVCodeplug::GeneralSettingsElement::lastCallShownOnStartup() const { + return 0x01 == getUInt8(0x00b8); +} +void +DMR6X2UVCodeplug::GeneralSettingsElement::enableShowLastCallOnStartup(bool enable) { + setUInt8(0x00b8, enable ? 0x01 : 0x00); +} + +DMR6X2UVCodeplug::GeneralSettingsElement::SMSFormat +DMR6X2UVCodeplug::GeneralSettingsElement::smsFormat() const { + return (SMSFormat) getUInt8(0x00b9); +} +void +DMR6X2UVCodeplug::GeneralSettingsElement::setSMSFormat(SMSFormat format) { + setUInt8(0x00b8, (uint8_t)format); +} + +DMR6X2UVCodeplug::GeneralSettingsElement::GPSUnits +DMR6X2UVCodeplug::GeneralSettingsElement::gpsUnits() const { + return (GPSUnits) getUInt8(0x00ba); +} +void +DMR6X2UVCodeplug::GeneralSettingsElement::setGPSUnits(GPSUnits units) { + setUInt8(0x00ba, (uint8_t) units); +} + +QPair +DMR6X2UVCodeplug::GeneralSettingsElement::vhfAutoRepeaterFrequencyRange() const { + double min = double(getUInt32_le(0x00bc))/1.0e5; // Stored in 10Hz + double max = double(getUInt32_le(0x00c0))/1.0e5; + return QPair(min,max); +} +void +DMR6X2UVCodeplug::GeneralSettingsElement::setVHFAutoRepeaterFrequencyRange(double lower, double upper) { + setUInt32_le(0x00bc, uint32_t(lower*1e5)); + setUInt32_le(0x00c0, uint32_t(upper*1e5)); +} +QPair +DMR6X2UVCodeplug::GeneralSettingsElement::uhfAutoRepeaterFrequencyRange() const { + double min = double(getUInt32_le(0x00c4))/1.0e5; // Stored in 10Hz + double max = double(getUInt32_le(0x00c8))/1.0e5; + return QPair(min,max); +} +void +DMR6X2UVCodeplug::GeneralSettingsElement::setUHFAutoRepeaterFrequencyRange(double lower, double upper) { + setUInt32_le(0x00c4, uint32_t(lower*1e5)); + setUInt32_le(0x00c8, uint32_t(upper*1e5)); +} +AnytoneCodeplug::GeneralSettingsElement::AutoRepDir +DMR6X2UVCodeplug::GeneralSettingsElement::autoRepeaterDirectionB() const { + return (AutoRepDir)getUInt8(0x00cc); +} +void +DMR6X2UVCodeplug::GeneralSettingsElement::setAutoRepeaterDirectionB(AutoRepDir dir) { + setUInt8(0x00cc, (uint8_t)dir); +} + +bool +DMR6X2UVCodeplug::GeneralSettingsElement::addressBookIsSendWithOwnCode() const { + return 0x01 == getUInt8(0x00cd); +} +void +DMR6X2UVCodeplug::GeneralSettingsElement::enableSendAddressbookWithOwnCode(bool enable) { + setUInt8(0x00cd, enable ? 0x01 : 0x00); +} + +bool +DMR6X2UVCodeplug::GeneralSettingsElement::defaultBootChannelEnabled() const { + return 0x01 == getUInt8(0x00ce); +} +void +DMR6X2UVCodeplug::GeneralSettingsElement::enableDefaultBootChannel(bool enable) { + setUInt8(0x00ce, enable ? 0x01 : 0x00); +} + +unsigned int +DMR6X2UVCodeplug::GeneralSettingsElement::bootZoneAIndex() const { + return getUInt8(0x00cf); +} +void +DMR6X2UVCodeplug::GeneralSettingsElement::setBootZoneAIndex(unsigned int index) { + setUInt8(0x00cf, index); +} +bool +DMR6X2UVCodeplug::GeneralSettingsElement::bootChannelAIsVFO() const { + return 0xff == bootChannelAIndex(); +} +unsigned int +DMR6X2UVCodeplug::GeneralSettingsElement::bootChannelAIndex() const { + return getUInt8(0x00d1); +} +void +DMR6X2UVCodeplug::GeneralSettingsElement::setBootChannelAIndex(unsigned int index) { + return setUInt8(0x00d1, index); +} +void +DMR6X2UVCodeplug::GeneralSettingsElement::setBootChannelAIsVFO() { + setBootChannelAIndex(0xff); +} + +unsigned int +DMR6X2UVCodeplug::GeneralSettingsElement::bootZoneBIndex() const { + return getUInt8(0x00d0); +} +void +DMR6X2UVCodeplug::GeneralSettingsElement::setBootZoneBIndex(unsigned int index) { + setUInt8(0x00d0, index); +} +bool +DMR6X2UVCodeplug::GeneralSettingsElement::bootChannelBIsVFO() const { + return 0xff == bootChannelBIndex(); +} +unsigned int +DMR6X2UVCodeplug::GeneralSettingsElement::bootChannelBIndex() const { + return getUInt8(0x00d2); +} +void +DMR6X2UVCodeplug::GeneralSettingsElement::setBootChannelBIndex(unsigned int index) { + return setUInt8(0x00d2, index); +} +void +DMR6X2UVCodeplug::GeneralSettingsElement::setBootChannelBIsVFO() { + setBootChannelBIndex(0xff); +} + +bool +DMR6X2UVCodeplug::GeneralSettingsElement::lastCallerIsKept() const { + return 0x01 == getUInt8(0x00d3); +} +void +DMR6X2UVCodeplug::GeneralSettingsElement::enableKeepLastCaller(bool enable) { + setUInt8(0x00d3, enable ? 0x01 : 0x00); +} + +bool +DMR6X2UVCodeplug::GeneralSettingsElement::rxBacklightIsAlwaysOn() const { + return 0x00 == rxBacklightDuration(); +} +unsigned int +DMR6X2UVCodeplug::GeneralSettingsElement::rxBacklightDuration() const { + return getUInt8(0x00d4); +} +void +DMR6X2UVCodeplug::GeneralSettingsElement::setRXBacklightDuration(unsigned int dur) { + dur = std::max(30U, dur); + setUInt8(0x00d4, dur); +} +void +DMR6X2UVCodeplug::GeneralSettingsElement::setRXBacklightAlwaysOn() { + setRXBacklightDuration(0); +} + +DMR6X2UVCodeplug::GeneralSettingsElement::BackgroundColor +DMR6X2UVCodeplug::GeneralSettingsElement::backgroundColor() const { + return (BackgroundColor) getUInt8(0x00d5); +} +void +DMR6X2UVCodeplug::GeneralSettingsElement::setBackgroundColor(BackgroundColor color) { + setUInt8(0x00d5, (uint8_t)color); +} + +unsigned int +DMR6X2UVCodeplug::GeneralSettingsElement::manualDialedGroupCallHangTime() const { + return getUInt8(0x00d6); +} +void +DMR6X2UVCodeplug::GeneralSettingsElement::setManualDialedGroupCallHangTime(unsigned int dur) { + setUInt8(0x00d6, dur); +} +unsigned int +DMR6X2UVCodeplug::GeneralSettingsElement::manualDialedPrivateCallHangTime() const { + return getUInt8(0x00d7); +} +void +DMR6X2UVCodeplug::GeneralSettingsElement::setManualDialedPrivateCallHangTime(unsigned int dur) { + setUInt8(0x00d7, dur); +} + bool DMR6X2UVCodeplug::GeneralSettingsElement::fromConfig(const Flags &flags, Context &ctx) { - if (! D878UVCodeplug::GeneralSettingsElement::fromConfig(flags, ctx)) + if (! AnytoneCodeplug::GeneralSettingsElement::fromConfig(flags, ctx)) return false; // apply DMR-6X2UV specific settings. return true; @@ -90,13 +333,217 @@ DMR6X2UVCodeplug::GeneralSettingsElement::fromConfig(const Flags &flags, Context bool DMR6X2UVCodeplug::GeneralSettingsElement::updateConfig(Context &ctx) { - if (! D878UVCodeplug::GeneralSettingsElement::updateConfig(ctx)) + if (! AnytoneCodeplug::GeneralSettingsElement::updateConfig(ctx)) return false; // Extract DMR-6X2UV specific settings. return true; } +/* ********************************************************************************************* * + * Implementation of DMR6X2UVCodeplug::ExtendedSettingsElement + * ********************************************************************************************* */ +DMR6X2UVCodeplug::ExtendedSettingsElement::ExtendedSettingsElement(uint8_t *ptr, unsigned size) + : Codeplug::Element(ptr, size) +{ + // pass... +} + +DMR6X2UVCodeplug::ExtendedSettingsElement::ExtendedSettingsElement(uint8_t *ptr) + : Codeplug::Element(ptr, 0x0030) +{ + // pass... +} + +void +DMR6X2UVCodeplug::ExtendedSettingsElement::clear() { + Codeplug::Element::clear(); +} + +DMR6X2UVCodeplug::ExtendedSettingsElement::FontColor +DMR6X2UVCodeplug::ExtendedSettingsElement::fontColor() const { + return (FontColor) getUInt8(0x0003); +} +void +DMR6X2UVCodeplug::ExtendedSettingsElement::setFontColor(FontColor color) { + setUInt8(0x0003, (uint8_t)color); +} + +bool +DMR6X2UVCodeplug::ExtendedSettingsElement::customChannelBackgroundEnabled() const { + return 0x01 == getUInt8(0x0004); +} +void +DMR6X2UVCodeplug::ExtendedSettingsElement::enableCustomChannelBackground(bool enable) { + setUInt8(0x0004, enable ? 0x01 : 0x00); +} + +unsigned int +DMR6X2UVCodeplug::ExtendedSettingsElement::roamingZoneIndex() const { + return getUInt8(0x0005); +} +void +DMR6X2UVCodeplug::ExtendedSettingsElement::setRoamingZoneIndex(unsigned int index) { + setUInt8(0x0005, index); +} +bool +DMR6X2UVCodeplug::ExtendedSettingsElement::autoRoamingEnabled() const { + return 0x01 == getUInt8(0x0006); +} +void +DMR6X2UVCodeplug::ExtendedSettingsElement::enableAutoRoaming(bool enable) { + setUInt8(0x006, enable ? 0x01 : 0x00); +} +bool +DMR6X2UVCodeplug::ExtendedSettingsElement::repeaterCheckEnabled() const { + return 0x01 == getUInt8(0x0007); +} +void +DMR6X2UVCodeplug::ExtendedSettingsElement::enableRepeaterCheck(bool enable) { + setUInt8(0x0007, enable ? 0x01 : 0x00); +} +DMR6X2UVCodeplug::ExtendedSettingsElement::OutOfRangeAlert +DMR6X2UVCodeplug::ExtendedSettingsElement::repeaterOutOfRangeAlert() const { + return (OutOfRangeAlert) getUInt8(0x0008); +} +void +DMR6X2UVCodeplug::ExtendedSettingsElement::setRepeaterOutOfRangeAlert(OutOfRangeAlert alert) { + setUInt8(0x0008, (uint8_t)alert); +} +unsigned int +DMR6X2UVCodeplug::ExtendedSettingsElement::repeaterOutOfRangeReminder() const { + return getUInt8(0x0009)+1; +} +void +DMR6X2UVCodeplug::ExtendedSettingsElement::setRepeaterOutOfRangeReminder(unsigned int n) { + n = std::max(1U, std::min(10U, n)); + setUInt8(0x0009, n-1); +} +unsigned int +DMR6X2UVCodeplug::ExtendedSettingsElement::repeaterCheckIntervall() const { + return (1+getUInt8(0x000a))*5; +} +void +DMR6X2UVCodeplug::ExtendedSettingsElement::setRepeaterCheckIntervall(unsigned int intervall) { + intervall = std::min(50U, std::max(5U, intervall)); + setUInt8(0x000a, intervall/5-1); +} +unsigned int +DMR6X2UVCodeplug::ExtendedSettingsElement::repeaterReconnections() const { + return getUInt8(0x000b)+3; +} +void +DMR6X2UVCodeplug::ExtendedSettingsElement::setRepeaterReconnections(unsigned int n) { + n = std::max(3U, std::min(5U, n)); + setUInt8(0x000b, n); +} + +DMR6X2UVCodeplug::ExtendedSettingsElement::RoamingCondition +DMR6X2UVCodeplug::ExtendedSettingsElement::startRoamingCondition() const { + return (RoamingCondition) getUInt8(0x000c); +} +void +DMR6X2UVCodeplug::ExtendedSettingsElement::setStartRoamingCondition(RoamingCondition cond) { + setUInt8(0x000c, (uint8_t)cond); +} + +unsigned int +DMR6X2UVCodeplug::ExtendedSettingsElement::autoRoamingIntervall() const { + return ((unsigned int) getUInt8(0x000d)) + 1; +} +void +DMR6X2UVCodeplug::ExtendedSettingsElement::setAutoRoamingIntervall(unsigned int minutes) { + minutes = std::max(1U, std::min(256U, minutes)); + setUInt8(0x000d, minutes-1); +} + +unsigned int +DMR6X2UVCodeplug::ExtendedSettingsElement::effectiveRoamingWaitingTime() const { + return getUInt8(0x000e); +} +void +DMR6X2UVCodeplug::ExtendedSettingsElement::setEffectiveRoamingWaitingTime(unsigned int sec) { + sec = std::min(30U, sec); + setUInt8(0x000e, sec); +} + +DMR6X2UVCodeplug::ExtendedSettingsElement::RoamingCondition +DMR6X2UVCodeplug::ExtendedSettingsElement::roamingReturnCondition() const { + return (RoamingCondition) getUInt8(0x000f); +} +void +DMR6X2UVCodeplug::ExtendedSettingsElement::setRoamingReturnCondition(RoamingCondition cond) { + setUInt8(0x000f, (uint8_t)cond); +} + +unsigned int +DMR6X2UVCodeplug::ExtendedSettingsElement::muteTimer() const { + return ((unsigned int)getUInt8(0x0010)) + 1; +} +void +DMR6X2UVCodeplug::ExtendedSettingsElement::setMuteTimer(unsigned int minutes) { + minutes = std::max(1U, minutes); + setUInt8(0x0010, minutes-1); +} + +DMR6X2UVCodeplug::ExtendedSettingsElement::EncryptionType +DMR6X2UVCodeplug::ExtendedSettingsElement::encryptionType() const { + return (EncryptionType) getUInt8(0x0011); +} +void +DMR6X2UVCodeplug::ExtendedSettingsElement::setEncryptionType(EncryptionType type) { + setUInt8(0x0011, (uint8_t)type); +} + +DMR6X2UVCodeplug::ExtendedSettingsElement::NameColor +DMR6X2UVCodeplug::ExtendedSettingsElement::zoneANameColor() const { + return (NameColor) getUInt8(0x0012); +} +void +DMR6X2UVCodeplug::ExtendedSettingsElement::setZoneANameColor(NameColor color) { + setUInt8(0x0012, (uint8_t) color); +} +DMR6X2UVCodeplug::ExtendedSettingsElement::NameColor +DMR6X2UVCodeplug::ExtendedSettingsElement::zoneBNameColor() const { + return (NameColor) getUInt8(0x0013); +} +void +DMR6X2UVCodeplug::ExtendedSettingsElement::setZoneBNameColor(NameColor color) { + setUInt8(0x0013, (uint8_t) color); +} + +DMR6X2UVCodeplug::ExtendedSettingsElement::NameColor +DMR6X2UVCodeplug::ExtendedSettingsElement::channelANameColor() const { + return (NameColor) getUInt8(0x0014); +} +void +DMR6X2UVCodeplug::ExtendedSettingsElement::setChannelANameColor(NameColor color) { + setUInt8(0x0014, (uint8_t) color); +} +DMR6X2UVCodeplug::ExtendedSettingsElement::NameColor +DMR6X2UVCodeplug::ExtendedSettingsElement::channelBNameColor() const { + return (NameColor) getUInt8(0x0015); +} +void +DMR6X2UVCodeplug::ExtendedSettingsElement::setChannelBNameColor(NameColor color) { + setUInt8(0x0015, (uint8_t) color); +} + +bool +DMR6X2UVCodeplug::ExtendedSettingsElement::fromConfig(const Flags &flags, Context &ctx) +{ + Q_UNUSED(flags); Q_UNUSED(ctx); + return true; +} + +bool +DMR6X2UVCodeplug::ExtendedSettingsElement::updateConfig(Context &ctx) { + Q_UNUSED(ctx); + return true; +} + + + /* ********************************************************************************************* * * Implementation of DMR6X2UVCodeplug * ********************************************************************************************* */ diff --git a/lib/dmr6x2uv_codeplug.hh b/lib/dmr6x2uv_codeplug.hh index 8b59b54d..aed46129 100644 --- a/lib/dmr6x2uv_codeplug.hh +++ b/lib/dmr6x2uv_codeplug.hh @@ -192,6 +192,9 @@ class DMR6X2UVCodeplug : public D868UVCodeplug public: /** General settings element for the DMR-6X2UV. + * + * Extends the @c AnytoneCodeplug::GeneralSettingsElement by the device specific settings for + * the BTECH DMR-6X2UV. * * Memory representation of the encoded settings element (size 0x0e0 bytes): * @verbinclude dmr6x2uv_generalsettings.txt */ @@ -208,6 +211,25 @@ public: TS1 = 0, TS2 = 1, Channel = 2 }; + /** Possible SMS formats. */ + enum class SMSFormat { + MFormat = 0x00, + HFormat = 0x01, + DMRStandard = 0x02 + }; + + /** Possible GPS units. */ + enum class GPSUnits { + Metric = 0x00, + Imperial = 0x01 + }; + + /** Possible background colors. */ + enum class BackgroundColor { + Black = 0x00, + Blue = 0x01 + }; + protected: /** Hidden Constructor. */ GeneralSettingsElement(uint8_t *ptr, unsigned size); @@ -235,6 +257,130 @@ public: /** Sets the time-slot in simplex repeater mode. */ virtual void setSimplexRepeaterTimeslot(SimplexRepeaterSlot slot); + /** Returns the GPS ranging intervall in seconds. */ + virtual unsigned int gpsRangingIntervall() const; + /** Sets the GPS ranging intervall in seconds. */ + virtual void setGPSRangingIntervall(unsigned int sec); + /** Returns @c true if the current contact is shown. */ + virtual bool currentContactShown() const; + /** Shows/hides the current contact. */ + virtual void showCurrentContact(bool show); + /** Key sound volume. Any value from 1-15, 0 means adjustable. */ + virtual unsigned int keySoundVolume() const; + /** Sets the key sound volume. Any value from 1-15, 0 means adjustable. */ + virtual void setKeySoundVolume(unsigned int vol); + + /** Returns @c true if the "professional key" gets locked too. */ + virtual bool professionalKeyLock() const; + /** Enables/disables professional key lock. */ + virtual void enableProfessionalKeyLock(bool enable); + /** Returns @c true if the side keys gets locked too. */ + virtual bool sideKeyLock() const; + /** Enables/disables side key lock. */ + virtual void enableSideKeyLock(bool enable); + /** Returns @c true if the keyboard gets locked too. */ + virtual bool keyboadLock() const; + /** Enables/disables keyboard lock. */ + virtual void enableKeyboradLock(bool enable); + /** Returns @c true if the knob gets locked too. */ + virtual bool knobLock() const; + /** Enables/disables knob lock. */ + virtual void enableKnobLock(bool enable); + + /** Returns @c true if the last call is shown on startup. */ + virtual bool lastCallShownOnStartup() const; + /** Enables/disables showing last call on startup. */ + virtual void enableShowLastCallOnStartup(bool enable); + + /** Returns the SMS format. */ + virtual SMSFormat smsFormat() const; + /** Sets the SMS format. */ + virtual void setSMSFormat(SMSFormat format); + + /** Retuns the units used to display distances. */ + virtual GPSUnits gpsUnits() const; + /** Sets the GPS units. */ + virtual void setGPSUnits(GPSUnits units); + + /** Returns the VHF frequency range in MHz for the auto-repeater feature. + * If a frequency within this range is chosen, the radio will transmit at the specified + * offset. */ + virtual QPair vhfAutoRepeaterFrequencyRange() const; + /** Sets the VHF auto-repeater frequency range in MHz. */ + virtual void setVHFAutoRepeaterFrequencyRange(double lower, double upper); + /** Returns the UHF frequency range in MHz for the auto-repeater feature. + * If a frequency within this range is chosen, the radio will transmit at the specified + * offset. */ + virtual QPair uhfAutoRepeaterFrequencyRange() const; + /** Sets the UHF auto-repeater frequency range in MHz. */ + virtual void setUHFAutoRepeaterFrequencyRange(double lower, double upper); + /** Returns auto-repeater direction for VFO B. */ + virtual AutoRepDir autoRepeaterDirectionB() const; + /** Sets the auto-repeater direction for VFO B. */ + virtual void setAutoRepeaterDirectionB(AutoRepDir dir); + + /** Returns @c true if the address book is send with its own code (WTF?). */ + virtual bool addressBookIsSendWithOwnCode() const; + /** Enables/disables sending address book with own code. */ + virtual void enableSendAddressbookWithOwnCode(bool enable); + + /** Returns @c true if a default boot channel is enabled. */ + virtual bool defaultBootChannelEnabled() const; + /** Enables/disables default boot channel. */ + virtual void enableDefaultBootChannel(bool enable); + /** Returns the zone of the boot channel for VFO A. */ + virtual unsigned int bootZoneAIndex() const; + /** Sets the index of the zone of the boot channel for VFO A. */ + virtual void setBootZoneAIndex(unsigned int index); + /** Returns @c true if the boot channel for VFO A is the VFO. */ + virtual bool bootChannelAIsVFO() const; + /** Returns the index of the boot channel for VFO A. */ + virtual unsigned int bootChannelAIndex() const; + /** Sets the boot channel index for VFO A. */ + virtual void setBootChannelAIndex(unsigned int index); + /** Sets the boot channel for VFO A to VFO. */ + virtual void setBootChannelAIsVFO(); + /** Returns the zone of the boot channel for VFO B. */ + virtual unsigned int bootZoneBIndex() const; + /** Sets the index of the zone of the boot channel for VFO B. */ + virtual void setBootZoneBIndex(unsigned int index); + /** Returns @c true if the boot channel for VFO B is the VFO. */ + virtual bool bootChannelBIsVFO() const; + /** Returns the index of the boot channel for VFO B. */ + virtual unsigned int bootChannelBIndex() const; + /** Sets the boot channel index for VFO B. */ + virtual void setBootChannelBIndex(unsigned int index); + /** Sets the boot channel for VFO B to VFO. */ + virtual void setBootChannelBIsVFO(); + + /** Returns @c true if the last caller is kept. */ + virtual bool lastCallerIsKept() const; + /** Enables/disables if last caller is kept. */ + virtual void enableKeepLastCaller(bool enable); + + /** Returns true if the RX backlight stays on infinitely. */ + virtual bool rxBacklightIsAlwaysOn() const; + /** Returns the duration of the RX backlight in seconds (max 30s). */ + virtual unsigned int rxBacklightDuration() const; + /** Sets the RX backlight duration in seconds (max 30s). */ + virtual void setRXBacklightDuration(unsigned int dur); + /** Enables the RX backlight to stay on infinitely. */ + virtual void setRXBacklightAlwaysOn(); + + /** Returns the background color. */ + virtual BackgroundColor backgroundColor() const; + /** Sets the background color. */ + virtual void setBackgroundColor(BackgroundColor color); + + /** Returns the group-call hang time, if group call was dialed manually. */ + virtual unsigned int manualDialedGroupCallHangTime() const; + /** Sets the group-call hang time, if the group call was dialed maually. */ + virtual void setManualDialedGroupCallHangTime(unsigned int dur); + /** Returns the private-call hang time, if private call was dialed manually. */ + virtual unsigned int manualDialedPrivateCallHangTime() const; + /** Sets the private-call hang time, if the private call was dialed maually. */ + virtual void setManualDialedPrivateCallHangTime(unsigned int dur); + /** Encodes the settings from the config. */ virtual bool fromConfig(const Flags &flags, Context &ctx); /** Update config from settings. */ @@ -247,6 +393,32 @@ public: * @verbinclude dmr6x2uv_settingsextension.txt */ class ExtendedSettingsElement: public Codeplug::Element { + public: + /** Possible font colors. */ + enum class FontColor { + White=0x00, Black=0x01, Orange=0x02, Red=0x03, Yellow=0x04, Green=0x05, Turquoise=0x06, Blue=0x07 + }; + + /** Possible name colors. */ + enum class NameColor { + Orange=0x00, Red=0x01, Yellow=0x02, Green=0x03, Turquoise=0x04, Blue=0x05, White=0x06, Black=0x07 + }; + + /** Possible repeater out-of-range alerts. */ + enum class OutOfRangeAlert { + None = 0x00, Bell = 0x01, Voice = 0x02 + }; + + /** Possible roaming conditions. */ + enum class RoamingCondition { + FixedTime=0x00, OutOfRange=0x01 + }; + + /** Possible encryption types. */ + enum class EncryptionType { + Basic = 0x00, AES = 0x01 + }; + protected: /** Hidden Constructor. */ ExtendedSettingsElement(uint8_t *ptr, unsigned size); @@ -257,6 +429,95 @@ public: /** Resets the general settings. */ void clear(); + + /** Returns the font color. */ + virtual FontColor fontColor() const; + /** Sets the font color. */ + virtual void setFontColor(FontColor color); + + /** Returns @c true if the custom channel background is enabled. */ + virtual bool customChannelBackgroundEnabled() const; + /** Enables/disables the custom channel background. */ + virtual void enableCustomChannelBackground(bool enable); + + /** Returns @c true if auto roaming is enabled. */ + virtual bool autoRoamingEnabled() const; + /** Enables/disables auto roaming. */ + virtual void enableAutoRoaming(bool enable); + + /** Returns @c true if repeater check is enabled. */ + virtual bool repeaterCheckEnabled() const; + /** Enables/disables repeater check. */ + virtual void enableRepeaterCheck(bool enable); + /** Returns the number of times, the repeater out-of-range reminder is shown (1-10). */ + virtual unsigned int repeaterOutOfRangeReminder() const; + /** Sets the number of times, the repeater out-of-range reminder is shown (1-10). */ + virtual void setRepeaterOutOfRangeReminder(unsigned int n); + /** Returns the repeater check intervall in seconds (5-50s). */ + virtual unsigned int repeaterCheckIntervall() const; + /** Sets the repeater check intervall in seconds (5-50s). */ + virtual void setRepeaterCheckIntervall(unsigned int intervall); + /** Returns the repeater out-of-range alert type. */ + virtual OutOfRangeAlert repeaterOutOfRangeAlert() const; + /** Sets the repeater out-of-range alert type. */ + virtual void setRepeaterOutOfRangeAlert(OutOfRangeAlert alert); + /** Returns the number of times, a repeater reconnection is tried (3-5). */ + virtual unsigned int repeaterReconnections() const; + /** Sets the number of times, a repeater reconnection is tried (3-5). */ + virtual void setRepeaterReconnections(unsigned int n); + + /** Retunrs the roaming zone index. */ + virtual unsigned int roamingZoneIndex() const; + /** Sets the roaming zone index. */ + virtual void setRoamingZoneIndex(unsigned int index); + /** Returns the condition to start roaming. */ + virtual RoamingCondition startRoamingCondition() const; + /** Sets the condition to start roaming. */ + virtual void setStartRoamingCondition(RoamingCondition cond); + /** Returns the auto-roaming intervall in minutes (1-256). */ + virtual unsigned int autoRoamingIntervall() const; + /** Sets the auto-roaming intervall in minutes (1-256). */ + virtual void setAutoRoamingIntervall(unsigned int minutes); + /** Returns the effective roaming waiting time in seconds (0-30s). */ + virtual unsigned int effectiveRoamingWaitingTime() const; + /** Sets the effective roaming waiting time in seconds (0-30s). */ + virtual void setEffectiveRoamingWaitingTime(unsigned int sec); + /** Returns the roaming return condition. */ + virtual RoamingCondition roamingReturnCondition() const; + /** Sets the roaming return condition. */ + virtual void setRoamingReturnCondition(RoamingCondition cond); + + /** Returns the mute timer in minutes. */ + virtual unsigned int muteTimer() const; + /** Sets the mute timer in minutes. */ + virtual void setMuteTimer(unsigned int minutes); + + /** Returns the encryption type. */ + virtual EncryptionType encryptionType() const; + /** Sets the encryption type. */ + virtual void setEncryptionType(EncryptionType type); + + /** Returns the name color for zone A. */ + virtual NameColor zoneANameColor() const; + /** Sets the name color for zone A. */ + virtual void setZoneANameColor(NameColor color); + /** Returns the name color for zone B. */ + virtual NameColor zoneBNameColor() const; + /** Sets the name color for zone B. */ + virtual void setZoneBNameColor(NameColor color); + /** Returns the name color for channel A. */ + virtual NameColor channelANameColor() const; + /** Sets the name color for channel A. */ + virtual void setChannelANameColor(NameColor color); + /** Returns the name color for channel B. */ + virtual NameColor channelBNameColor() const; + /** Sets the name color for channel B. */ + virtual void setChannelBNameColor(NameColor color); + + /** Encodes the settings from the config. */ + virtual bool fromConfig(const Flags &flags, Context &ctx); + /** Update config from settings. */ + virtual bool updateConfig(Context &ctx); }; From 05f20b1e7e682058165160546f29d32093c8d3b0 Mon Sep 17 00:00:00 2001 From: Hannes Matuschek Date: Wed, 21 Dec 2022 13:42:37 +0100 Subject: [PATCH 09/16] Fixed settings for DMR-6X2UV. --- doc/code/dmr6x2uv_settingsextension.txt | 4 +- lib/d868uv_codeplug.hh | 2 +- lib/dmr6x2uv_codeplug.cc | 179 +++++++++++++++++++++++- lib/dmr6x2uv_codeplug.hh | 47 +++++-- lib/dmr6x2uv_limits.cc | 4 +- lib/radiolimits.cc | 12 +- lib/radiolimits.hh | 14 ++ 7 files changed, 242 insertions(+), 20 deletions(-) diff --git a/doc/code/dmr6x2uv_settingsextension.txt b/doc/code/dmr6x2uv_settingsextension.txt index 27f051ca..e7fd404b 100644 --- a/doc/code/dmr6x2uv_settingsextension.txt +++ b/doc/code/dmr6x2uv_settingsextension.txt @@ -1,6 +1,6 @@ 7 6 5 4 3 2 1 0 7 6 5 4 3 2 1 0 7 6 5 4 3 2 1 0 7 6 5 4 3 2 1 0 +---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+ -00 | | Font Color | +00 | Enable send talker alias | Talker alias display priority | Talker alias encoding | Font Color | +---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+ 04 | Enable custom ch. background | Roaming zone index 0-based | Enable auto roaming | Enable repeater check | +---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+ @@ -26,6 +26,8 @@ +---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+ Field description: + - Talker alias display priority: 0x00=None, 0x01=Contact alias, 0x02=Over-the-air alias + - Talker alias encoding: 0x00=ISO-8, 0x01=ISO-7, 0x02=Unicode - Font Color: 0x00=White, 0x01=Black, 0x02=Orange, 0x03=Red, 0x04=Yellow, 0x05=Green, 0x06=Turquoise, 0x07=Blue - Encryption type: 0x00=Common(Basic), 0x01=AES - Zone/Channel A/B name color: 0x00=Orange, 0x01=Red, 0x02=Yellow, 0x03=Green, 0x04=Turquoise, 0x05=Blue, 0x06=White, 0x07=Black diff --git a/lib/d868uv_codeplug.hh b/lib/d868uv_codeplug.hh index 451f6c06..9c214603 100644 --- a/lib/d868uv_codeplug.hh +++ b/lib/d868uv_codeplug.hh @@ -441,7 +441,7 @@ protected: /** Link GPS systems. */ virtual bool linkGPSSystems(Context &ctx, const ErrorStack &err=ErrorStack()); - /** Allocate refab SMS messages. */ + /** Allocate prefab SMS messages. */ virtual void allocateSMSMessages(); /** Allocates hot key settings memory section. */ virtual void allocateHotKeySettings(); diff --git a/lib/dmr6x2uv_codeplug.cc b/lib/dmr6x2uv_codeplug.cc index a6303604..b611e5cc 100644 --- a/lib/dmr6x2uv_codeplug.cc +++ b/lib/dmr6x2uv_codeplug.cc @@ -1,4 +1,6 @@ #include "dmr6x2uv_codeplug.hh" +#include "utils.hh" + #define NUM_CHANNELS 4000 #define NUM_CHANNEL_BANKS 32 @@ -12,11 +14,18 @@ #define CHANNEL_BITMAP_SIZE 0x00000200 #define ADDR_GENERAL_CONFIG 0x02500000 -#define GENERAL_CONFIG_SIZE 0x00000100 -#define ADDR_GENERAL_CONFIG_EXT1 0x02501280 -#define GENERAL_CONFIG_EXT1_SIZE 0x00000030 -#define ADDR_GENERAL_CONFIG_EXT2 0x02501400 -#define GENERAL_CONFIG_EXT2_SIZE 0x00000100 +#define GENERAL_CONFIG_SIZE 0x000000e0 +#define ADDR_EXTENDED_SETTINGS 0x02501400 +#define EXTENDED_SETTINGS_SIZE 0x00000030 + +#define ADDR_APRS_SETTINGS 0x02501000 +#define APRS_SETTINGS_SIZE 0x00000040 +#define NUM_DMRAPRS_SYSTEMS 8 +#define ADDR_DMRAPRS_SETTINGS 0x02501040 +#define DMRAPRS_SETTINGS_SIZE 0x00000060 + +#define ADDR_APRS_MESSAGE 0x02501200 // Address of APRS messages +#define APRS_MESSAGE_SIZE 0x00000040 // Size of APRS messages /* ********************************************************************************************* * @@ -29,7 +38,7 @@ DMR6X2UVCodeplug::GeneralSettingsElement::GeneralSettingsElement(uint8_t *ptr, u } DMR6X2UVCodeplug::GeneralSettingsElement::GeneralSettingsElement(uint8_t *ptr) - : AnytoneCodeplug::GeneralSettingsElement(ptr, 0x00e0) + : AnytoneCodeplug::GeneralSettingsElement(ptr, GENERAL_CONFIG_SIZE) { // pass... } @@ -350,7 +359,7 @@ DMR6X2UVCodeplug::ExtendedSettingsElement::ExtendedSettingsElement(uint8_t *ptr, } DMR6X2UVCodeplug::ExtendedSettingsElement::ExtendedSettingsElement(uint8_t *ptr) - : Codeplug::Element(ptr, 0x0030) + : Codeplug::Element(ptr, EXTENDED_SETTINGS_SIZE) { // pass... } @@ -360,6 +369,33 @@ DMR6X2UVCodeplug::ExtendedSettingsElement::clear() { Codeplug::Element::clear(); } +bool +DMR6X2UVCodeplug::ExtendedSettingsElement::talkerAliasIsSend() const { + return 0x01 == getUInt8(0x0000); +} +void +DMR6X2UVCodeplug::ExtendedSettingsElement::enableSendTalkerAlias(bool enable) { + setUInt8(0x0000, enable ? 0x01 : 0x00); +} + +DMR6X2UVCodeplug::ExtendedSettingsElement::TalkerAliasSource +DMR6X2UVCodeplug::ExtendedSettingsElement::talkerAliasSource() const { + return (TalkerAliasSource) getUInt8(0x0001); +} +void +DMR6X2UVCodeplug::ExtendedSettingsElement::setTalkerAliasSource(TalkerAliasSource source) { + setUInt8(0x0001, (uint8_t)source); +} + +DMR6X2UVCodeplug::ExtendedSettingsElement::TalkerAliasEncoding +DMR6X2UVCodeplug::ExtendedSettingsElement::talkerAliasEncoding() const { + return (TalkerAliasEncoding) getUInt8(0x0002); +} +void +DMR6X2UVCodeplug::ExtendedSettingsElement::setTalkerAliasEncoding(TalkerAliasEncoding encoding) { + setUInt8(0x0002, (uint8_t)encoding); +} + DMR6X2UVCodeplug::ExtendedSettingsElement::FontColor DMR6X2UVCodeplug::ExtendedSettingsElement::fontColor() const { return (FontColor) getUInt8(0x0003); @@ -552,3 +588,132 @@ DMR6X2UVCodeplug::DMR6X2UVCodeplug(QObject *parent) { // pass... } + +void +DMR6X2UVCodeplug::allocateGeneralSettings() { + image(0).addElement(ADDR_GENERAL_CONFIG, GENERAL_CONFIG_SIZE); + image(0).addElement(ADDR_EXTENDED_SETTINGS, EXTENDED_SETTINGS_SIZE); +} + +bool +DMR6X2UVCodeplug::encodeGeneralSettings(const Flags &flags, Context &ctx, const ErrorStack &err) { + if (! GeneralSettingsElement(data(ADDR_GENERAL_CONFIG)).fromConfig(flags, ctx)) { + errMsg(err) << "Cannot encode general settings element."; + return false; + } + + if (! ExtendedSettingsElement(data(ADDR_EXTENDED_SETTINGS)).fromConfig(flags, ctx)) { + errMsg(err) << "Cannot encode extended settings element."; + return false; + } + + return true; +} + +bool +DMR6X2UVCodeplug::decodeGeneralSettings(Context &ctx, const ErrorStack &err) { + if (! GeneralSettingsElement(data(ADDR_GENERAL_CONFIG)).updateConfig(ctx)) { + errMsg(err) << "Cannot decode general settings element."; + return false; + } + + if (! ExtendedSettingsElement(data(ADDR_EXTENDED_SETTINGS)).updateConfig(ctx)) { + errMsg(err) << "Cannot decode extended settings element."; + return false; + } + + return true; +} + +void +DMR6X2UVCodeplug::allocateGPSSystems() { + // replaces D868UVCodeplug::allocateGPSSystems + + // APRS settings + image(0).addElement(ADDR_APRS_SETTINGS, APRS_SETTINGS_SIZE); + image(0).addElement(ADDR_APRS_MESSAGE, APRS_MESSAGE_SIZE); + image(0).addElement(ADDR_DMRAPRS_SETTINGS, DMRAPRS_SETTINGS_SIZE); +} + +bool +DMR6X2UVCodeplug::encodeGPSSystems(const Flags &flags, Context &ctx, const ErrorStack &err) { + Q_UNUSED(flags); Q_UNUSED(err) + // replaces D868UVCodeplug::encodeGPSSystems + + // Encode APRS system (there can only be one) + if (0 < ctx.config()->posSystems()->aprsCount()) { + D878UVCodeplug::AnalogAPRSSettingsElement(data(ADDR_APRS_SETTINGS)) + .fromAPRSSystem(ctx.config()->posSystems()->aprsSystem(0), ctx); + uint8_t *aprsmsg = (uint8_t *)data(ADDR_APRS_MESSAGE); + encode_ascii(aprsmsg, ctx.config()->posSystems()->aprsSystem(0)->message(), 60, 0x00); + } + + // Encode GPS systems + D878UVCodeplug::DMRAPRSSystemsElement gps(data(ADDR_DMRAPRS_SETTINGS)); + if (! gps.fromGPSSystems(ctx)) + return false; + if (0 < ctx.config()->posSystems()->gpsCount()) { + // If there is at least one GPS system defined -> set auto TX interval. + // This setting might be overridden by any analog APRS system below + D878UVCodeplug::AnalogAPRSSettingsElement aprs(data(ADDR_APRS_SETTINGS)); + aprs.setAutoTXInterval(ctx.config()->posSystems()->gpsSystem(0)->period()); + aprs.setManualTXInterval(ctx.config()->posSystems()->gpsSystem(0)->period()); + } + return true; +} + +bool +DMR6X2UVCodeplug::createGPSSystems(Context &ctx, const ErrorStack &err) { + Q_UNUSED(err) + + // replaces D868UVCodeplug::createGPSSystems + + // Before creating any GPS/APRS systems, get global auto TX interval + D878UVCodeplug::AnalogAPRSSettingsElement aprs(data(ADDR_APRS_SETTINGS)); + unsigned pos_intervall = aprs.autoTXInterval(); + + // Create APRS system (if enabled) + uint8_t *aprsmsg = (uint8_t *)data(ADDR_APRS_MESSAGE); + if (aprs.isValid()) { + APRSSystem *sys = aprs.toAPRSSystem(); + sys->setPeriod(pos_intervall); + sys->setMessage(decode_ascii(aprsmsg, 60, 0x00)); + ctx.config()->posSystems()->add(sys); ctx.add(sys,0); + } + + // Create GPS systems + D878UVCodeplug::DMRAPRSSystemsElement gps_systems(data(ADDR_DMRAPRS_SETTINGS)); + for (int i=0; isetPeriod(pos_intervall); + ctx.config()->posSystems()->add(sys); ctx.add(sys, i); + } else { + return false; + } + } + return true; +} + +bool +DMR6X2UVCodeplug::linkGPSSystems(Context &ctx, const ErrorStack &err) { + Q_UNUSED(err); + // replaces D868UVCodeplug::linkGPSSystems + + // Link APRS system + D878UVCodeplug::AnalogAPRSSettingsElement aprs(data(ADDR_APRS_SETTINGS)); + if (aprs.isValid()) { + aprs.linkAPRSSystem(ctx.config()->posSystems()->aprsSystem(0), ctx); + } + + // Link GPS systems + D878UVCodeplug::DMRAPRSSystemsElement gps_systems(data(ADDR_DMRAPRS_SETTINGS)); + for (int i=0; i(i), ctx); + } + + return true; +} diff --git a/lib/dmr6x2uv_codeplug.hh b/lib/dmr6x2uv_codeplug.hh index aed46129..6c27556f 100644 --- a/lib/dmr6x2uv_codeplug.hh +++ b/lib/dmr6x2uv_codeplug.hh @@ -25,7 +25,6 @@ * Zones * Start Size Content * 024C1300 000020 Bitmap of 250 zones. - * 024C1360 000020 Hidden zone bitmap of 250 zones. * 01000000 max. 01f400 250 zones channel lists of 250 16bit indices each. * 0-based, little endian, default/padded=0xffff. Offset between channel lists 0x200, * size of each list 0x1f4. @@ -98,9 +97,9 @@ * GPS/APRS * Start Size Content * 02501000 000040 APRS settings, - * see @c D868UVCodeplug::AnalogAPRSSettingsElement. - * 02501040 000060 APRS settings, - * see @c D868UVCodeplug::DMRAPRSSystemsElement. + * see @c D878UVCodeplug::AnalogAPRSSettingsElement. + * 02501040 000060 DMR-APRS systems, + * see @c D878UVCodeplug::DMRAPRSSystemsElement. * 02501200 000040 APRS Text, up to 60 chars ASCII, 0-padded. * 02501280 000030 GPS template message, ASCII, 0-padded. * @@ -145,8 +144,8 @@ * 024C1800 000500 32 DMR-Encryption keys, * see @c D868UVCodeplug::dmr_encryption_key_t, * 40b each. - * 024C4000 004000 Up to 256 AES encryption keys. - * See @c D868UVCodeplug::AESEncryptionKeyElement. + * 025C1000 004000 Up to 256 AES encryption keys. + * See @c D878UVCodeplug::AESEncryptionKeyElement. * * Misc * Start Size Content @@ -182,7 +181,6 @@ * 024C2630 000020 Unknown bitmap. * 024C3000 000020 Unknown settings. * 024C5000 000020 Unknown settings. - * 025C1000 005000 Unknown settings. * * * @ingroup dmr6x2uv */ @@ -394,6 +392,16 @@ public: class ExtendedSettingsElement: public Codeplug::Element { public: + /** Possible talker alias encodings. */ + enum class TalkerAliasEncoding { + ISO8=0x00, ISO7=0x01, Unicode=0x02 + }; + + /** Possible display priorities for the talker alias. */ + enum class TalkerAliasSource { + None=0x00, Database=0x01, OverTheAir=0x02 + }; + /** Possible font colors. */ enum class FontColor { White=0x00, Black=0x01, Orange=0x02, Red=0x03, Yellow=0x04, Green=0x05, Turquoise=0x06, Blue=0x07 @@ -430,6 +438,19 @@ public: /** Resets the general settings. */ void clear(); + /** Returns @c true, if a talker alias is send. */ + virtual bool talkerAliasIsSend() const; + /** Enables/disables sending the talker alias. */ + virtual void enableSendTalkerAlias(bool enable); + /** Retunrs the talker alias source. */ + virtual TalkerAliasSource talkerAliasSource() const; + /** Sets the talker alias source. */ + virtual void setTalkerAliasSource(TalkerAliasSource source); + /** Retunrs the talker alias encoding. */ + virtual TalkerAliasEncoding talkerAliasEncoding() const; + /** Sets the talker alias encoding. */ + virtual void setTalkerAliasEncoding(TalkerAliasEncoding encoding); + /** Returns the font color. */ virtual FontColor fontColor() const; /** Sets the font color. */ @@ -520,11 +541,21 @@ public: virtual bool updateConfig(Context &ctx); }; - public: /** Empty constructor. */ explicit DMR6X2UVCodeplug(QObject *parent=nullptr); + /** Allocates general settings memory section. */ + virtual void allocateGeneralSettings(); + /** Encodes the general settings section. */ + virtual bool encodeGeneralSettings(const Flags &flags, Context &ctx, const ErrorStack &err=ErrorStack()); + /** Decodes the general settings section. */ + virtual bool decodeGeneralSettings(Context &ctx, const ErrorStack &err=ErrorStack()); + + void allocateGPSSystems(); + bool encodeGPSSystems(const Flags &flags, Context &ctx, const ErrorStack &err=ErrorStack()); + bool createGPSSystems(Context &ctx, const ErrorStack &err=ErrorStack()); + bool linkGPSSystems(Context &ctx, const ErrorStack &err=ErrorStack()); }; #endif // DMR6X2UVCODEPLUG_HH diff --git a/lib/dmr6x2uv_limits.cc b/lib/dmr6x2uv_limits.cc index e3b4e8ae..dbefbe5f 100644 --- a/lib/dmr6x2uv_limits.cc +++ b/lib/dmr6x2uv_limits.cc @@ -37,7 +37,7 @@ DMR6X2UVLimits::DMR6X2UVLimits(const std::initializer_list &values, QObject *parent) : RadioLimitValue(parent), _values(values) diff --git a/lib/radiolimits.hh b/lib/radiolimits.hh index 216fd37a..5e673724 100644 --- a/lib/radiolimits.hh +++ b/lib/radiolimits.hh @@ -304,6 +304,20 @@ protected: }; +/** Represents a DMR ID. + * That is an uint between 1 and 16777215 without any default value. + * @ingroup limits */ +class RadioLimitDMRId: public RadioLimitUInt +{ + Q_OBJECT + +public: + /** Constructor. + * @param parent Specifies the QObject parent. */ + explicit RadioLimitDMRId(QObject *parent=nullptr); +}; + + /** Represents a limit for a set of enum values. * @ingroup limits */ class RadioLimitEnum: public RadioLimitValue From 6ec812c85ab7a6634a6387dbb7dcd8a3ec4436ad Mon Sep 17 00:00:00 2001 From: Hannes Matuschek Date: Mon, 2 Jan 2023 09:06:06 +0100 Subject: [PATCH 10/16] Fixed channel element for DMR-6X2UV. --- doc/code/anytone_channel.txt | 16 +--- doc/code/d868uv_channel.txt | 61 +++++++++++++ doc/code/dmr6x2uv_channel.txt | 58 +++++++++++++ lib/addressmap.hh | 2 +- lib/anytone_codeplug.cc | 98 --------------------- lib/anytone_codeplug.hh | 39 --------- lib/d578uv.cc | 2 + lib/d868uv_codeplug.cc | 157 ++++++++++++++++++++++++++++++++++ lib/d868uv_codeplug.hh | 63 ++++++++++++++ lib/d878uv_codeplug.cc | 10 +-- lib/d878uv_codeplug.hh | 2 +- lib/dmr6x2uv_codeplug.cc | 72 ++++++++++++++++ lib/dmr6x2uv_codeplug.hh | 46 ++++++++++ 13 files changed, 469 insertions(+), 157 deletions(-) create mode 100644 doc/code/d868uv_channel.txt create mode 100644 doc/code/dmr6x2uv_channel.txt diff --git a/doc/code/anytone_channel.txt b/doc/code/anytone_channel.txt index b3be7005..780038ba 100644 --- a/doc/code/anytone_channel.txt +++ b/doc/code/anytone_channel.txt @@ -18,13 +18,11 @@ +---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+ 20 | Color code |LWK|EEE|RGP|EAT| 0 |EST|SMC|TSL| AES Encryption key | Name 16 x ASCII 0-padded ... +---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+ -30 ... | Unused set to 0x00 | +30 ... | Pad byte set to 0x00 | +---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+ -34 | 0 0 0 0 0 |DAD|ETM|RNG| 0 0 0 0 0 0 0 |TAP| DMR APRS IDX (0-based) | Unused set to 0x00 | +34 | Device specific settings ... +---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+ -38 | Unused set to 0x00 | Unused set to 0x00 | DMR encryption idx +1, 0=off | 0 0 0 0 0 |SMF|RnK|Muk| - +---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+ -3c | Unused set to 0x00000000 | +3c ... | +---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+ Field description: @@ -51,11 +49,3 @@ Field description: - EST: Enable simplex TDMA, - SMC: SMS confirmation, - TSL: Time slot where 0=TS1, 1=TS2 - - DAD: Data ACK disable (inverted!) - - ETM: Enable through mode - - RNG: Ranging - - APRSRep: Enable APRS/GPS report, - - TAP: TX APRS enable - - MuK: Multiple keys - - RnK: Random key - - SMF: SMS forbid. diff --git a/doc/code/d868uv_channel.txt b/doc/code/d868uv_channel.txt new file mode 100644 index 00000000..b3be7005 --- /dev/null +++ b/doc/code/d868uv_channel.txt @@ -0,0 +1,61 @@ + 7 6 5 4 3 2 1 0 7 6 5 4 3 2 1 0 7 6 5 4 3 2 1 0 7 6 5 4 3 2 1 0 + +---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+ +00 | RX Frequency 32bit BCD encoded in big-endian as MMMkkkhh | + +---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+ +04 | Tx Frequency Offset 32bit BCD encoded in big-endian as MMMkkkhh | + +---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+ +08 | RMode | 0 |BWd| PWR | CMode |TAr|CaC|RXO|CTR|TDC|TCT|RDC|RCT| CTCSS transmit | CTCSS receive | + +---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+ +0c | DCS transmit code, little endian | DCS receive code, little endian | + +---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+ +10 | Custom CTCSS frequency in 0.1Hz, little endian | 2-tone decode index, 0-based, little endian | + +---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+ +14 | Contact index 0-based, little endian | + +---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+ +18 | Radio ID table index. | 0 |SquelchMode| 0 0 0 0 | 0 0 |OptSig | 0 0 | TxPer | Scan list index | + +---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+ +1c | RX Group list index | 2-tone ID | 5-tone ID | DTMF ID | + +---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+ +20 | Color code |LWK|EEE|RGP|EAT| 0 |EST|SMC|TSL| AES Encryption key | Name 16 x ASCII 0-padded ... + +---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+ +30 ... | Unused set to 0x00 | + +---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+ +34 | 0 0 0 0 0 |DAD|ETM|RNG| 0 0 0 0 0 0 0 |TAP| DMR APRS IDX (0-based) | Unused set to 0x00 | + +---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+ +38 | Unused set to 0x00 | Unused set to 0x00 | DMR encryption idx +1, 0=off | 0 0 0 0 0 |SMF|RnK|Muk| + +---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+ +3c | Unused set to 0x00000000 | + +---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+ + +Field description: + - RMode: Repater mode where 0=simplex, 1=positive TX offset, 2=negative TX offset. + - BWd: Band width where 0=narrow (12.5kHz), 1=wide (25kHz). + - PWR: Power where 0=low, 1=mid, 2=high, 3=turbo. + - CMode: Channel mode, 0=analog, 1=digital, 2=analog + digi RX, 3=digital + analog RX. + - TAr: Enable talkaround. + - CaC: Enable call confirm. + - RXO: Enable RX only. + - CTR: Enable CTCSS phase reversal. + - TDC: Enable TX DCS code. + - TCT: Enable TX CTCSS tone. + - RDC: Enable RX DCS code. + - RCT: Enable RX CTCSS tone. + - SquelchMode: Squelch mode 0=Carrier, 1=CTCSS/DCS, 2=Optional Signaling, 3=CTCSS/DCS and Optional Signaling, + 4 = CTCSS/DCS or Optional Signaling + - OptSig: Optional signalling where 0=off, 1=DTMF, 2=2-tone, 3=5-tone + - TxPer: TX permit/admit criterion, 0=always, 1=colorcode, 2=channel free. + - LWK: Enable lone worker. + - EEE: Enable enhanced encryption + - RGP: Enable RX GPS: + - EAT: Enable adative TDMA + - EST: Enable simplex TDMA, + - SMC: SMS confirmation, + - TSL: Time slot where 0=TS1, 1=TS2 + - DAD: Data ACK disable (inverted!) + - ETM: Enable through mode + - RNG: Ranging + - APRSRep: Enable APRS/GPS report, + - TAP: TX APRS enable + - MuK: Multiple keys + - RnK: Random key + - SMF: SMS forbid. diff --git a/doc/code/dmr6x2uv_channel.txt b/doc/code/dmr6x2uv_channel.txt new file mode 100644 index 00000000..aa1138bc --- /dev/null +++ b/doc/code/dmr6x2uv_channel.txt @@ -0,0 +1,58 @@ + 7 6 5 4 3 2 1 0 7 6 5 4 3 2 1 0 7 6 5 4 3 2 1 0 7 6 5 4 3 2 1 0 + +---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+ +00 | RX Frequency 32bit BCD encoded in big-endian as MMMkkkhh | + +---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+ +04 | Tx Frequency Offset 32bit BCD encoded in big-endian as MMMkkkhh | + +---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+ +08 | RMode | 0 |BWd| PWR | CMode |TAr|CaC|RXO|CTR|TDC|TCT|RDC|RCT| CTCSS transmit | CTCSS receive | + +---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+ +0c | DCS transmit code, little endian | DCS receive code, little endian | + +---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+ +10 | Custom CTCSS frequency in 0.1Hz, little endian | 2-tone decode index, 0-based, little endian | + +---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+ +14 | Contact index 0-based, little endian | + +---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+ +18 | Radio ID table index. | 0 |SquelchMode| 0 0 0 0 | 0 0 |OptSig | 0 0 | TxPer | 0 0 0 0 0 |XFR| 0 |RNG| + +---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+ +1c | RX Group list index | 2-tone ID | 5-tone ID | DTMF ID | + +---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+ +20 | Color code |LWK|EEE|RGP|EAT| 0 |EST|SMC|TSL| AES Encryption key | Name 16 x ASCII 0-padded ... + +---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+ +30 ... | Unused set to 0x00 | + +---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+ +34 | 0 0 0 0 0 |DAD|ETM| 0 | Unused, set to 0x00 | Scanlist idx 0, 0xff=unset | Scanlist idx 1, 0xff=unset | + +---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+ +38 | Scanlist idx 2, 0xff=unset | Scanlist idx 3, 0xff=unset | Scanlist idx 4, 0xff=unset | Scanlist idx 5, 0xff=unset | + +---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+ +3c | Scanlist idx 6, 0xff=unset | Scanlist idx 7, 0xff=unset | ARPS Report Channel Index | 0 0 0 0 0 0 |RGP|ATy| + +---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+ + +Field description: + - RMode: Repater mode where 0=simplex, 1=positive TX offset, 2=negative TX offset. + - BWd: Band width where 0=narrow (12.5kHz), 1=wide (25kHz). + - PWR: Power where 0=low, 1=mid, 2=high, 3=turbo. + - CMode: Channel mode, 0=analog, 1=digital, 2=analog + digi RX, 3=digital + analog RX. + - TAr: Enable talkaround. + - CaC: Enable call confirm. + - RXO: Enable RX only. + - CTR: Enable CTCSS phase reversal. + - TDC: Enable TX DCS code. + - TCT: Enable TX CTCSS tone. + - RDC: Enable RX DCS code. + - RCT: Enable RX CTCSS tone. + - SquelchMode: Squelch mode 0=Carrier, 1=CTCSS/DCS, 2=Optional Signaling, 3=CTCSS/DCS and Optional Signaling, + 4 = CTCSS/DCS or Optional Signaling + - OptSig: Optional signalling where 0=off, 1=DTMF, 2=2-tone, 3=5-tone + - TxPer: TX permit/admit criterion, 0=always, 1=colorcode, 2=channel free. + - XFR: Exlcude channel from roaming + - LWK: Enable lone worker. + - EEE: Enable enhanced encryption + - RGP: Enable DMR-APRS RX + - EAT: Enable adaptive TDMA + - EST: Enable simplex TDMA + - SMC: SMS confirmation, + - TSL: Time slot where 0=TS1, 1=TS2 + - DAD: Data ACK disable (inverted!) + - ETM: Enable through mode + - RNG: Ranging + - ATy: APRS Type: 0=DMR-APRS, 1=Analog APRS diff --git a/lib/addressmap.hh b/lib/addressmap.hh index 369859b7..515d439d 100644 --- a/lib/addressmap.hh +++ b/lib/addressmap.hh @@ -1,7 +1,7 @@ #ifndef ADDRESSMAP_HH #define ADDRESSMAP_HH -#include +#include #include /** This class represents a memory map. diff --git a/lib/anytone_codeplug.cc b/lib/anytone_codeplug.cc index 70bb4a3f..964daf57 100644 --- a/lib/anytone_codeplug.cc +++ b/lib/anytone_codeplug.cc @@ -572,83 +572,6 @@ AnytoneCodeplug::ChannelElement::setName(const QString &name) { writeASCII(0x0023, name, 16, 0x00); } -bool -AnytoneCodeplug::ChannelElement::ranging() const { - return getBit(0x0034, 0); -} -void -AnytoneCodeplug::ChannelElement::enableRanging(bool enable) { - setBit(0x0034, 0, enable); -} -bool -AnytoneCodeplug::ChannelElement::throughMode() const { - return getBit(0x0034, 1); -} -void -AnytoneCodeplug::ChannelElement::enableThroughMode(bool enable) { - setBit(0x0034, 1, enable); -} -bool -AnytoneCodeplug::ChannelElement::dataACK() const { - return !getBit(0x0034, 2); -} -void -AnytoneCodeplug::ChannelElement::enableDataACK(bool enable) { - setBit(0x0034, 2, !enable); -} - -bool -AnytoneCodeplug::ChannelElement::txDigitalAPRS() const { - return getBit(0x0035, 0); -} -void -AnytoneCodeplug::ChannelElement::enableTXDigitalAPRS(bool enable) { - setBit(0x0035, 0, enable); -} -unsigned -AnytoneCodeplug::ChannelElement::digitalAPRSSystemIndex() const { - return getUInt8(0x0036); -} -void -AnytoneCodeplug::ChannelElement::setDigitalAPRSSystemIndex(unsigned idx) { - setUInt8(0x0036, idx); -} - -unsigned -AnytoneCodeplug::ChannelElement::dmrEncryptionKeyIndex() const { - return getUInt8(0x003a); -} -void -AnytoneCodeplug::ChannelElement::setDMREncryptionKeyIndex(unsigned idx) { - setUInt8(0x003a, idx); -} - -bool -AnytoneCodeplug::ChannelElement::multipleKeyEncryption() const { - return getBit(0x003b, 0); -} -void -AnytoneCodeplug::ChannelElement::enableMultipleKeyEncryption(bool enable) { - setBit(0x003b, 0, enable); -} - -bool -AnytoneCodeplug::ChannelElement::randomKey() const { - return getBit(0x003b, 1); -} -void -AnytoneCodeplug::ChannelElement::enableRandomKey(bool enable) { - setBit(0x003b, 1, enable); -} -bool -AnytoneCodeplug::ChannelElement::sms() const { - return !getBit(0x003b, 2); -} -void -AnytoneCodeplug::ChannelElement::enableSMS(bool enable) { - setBit(0x003b, 0, !enable); -} - Channel * AnytoneCodeplug::ChannelElement::toChannelObj(Context &ctx) const { @@ -706,13 +629,10 @@ AnytoneCodeplug::ChannelElement::toChannelObj(Context &ctx) const { AnytoneDMRChannelExtension *ext = new AnytoneDMRChannelExtension(); ch_ext = ext; dch->setAnytoneChannelExtension(ext); ext->enableCallConfirm(callConfirm()); - ext->enableSMS(sms()); ext->enableSMSConfirm(smsConfirm()); - ext->enableDataACK(dataACK()); ext->enableSimplexTDMA(simplexTDMA()); ext->enableAdaptiveTDMA(adaptiveTDMA()); ext->enableLoneWorker(loneWorker()); - ext->enableThroughMode(throughMode()); // Done ch = dch; } else { @@ -759,12 +679,6 @@ AnytoneCodeplug::ChannelElement::linkChannelObj(Channel *c, Context &ctx) const if (hasGroupListIndex() && ctx.has(groupListIndex())) dc->setGroupListObj(ctx.get(groupListIndex())); - // Link to GPS system - if (txDigitalAPRS() && (! ctx.has(digitalAPRSSystemIndex()))) - logWarn() << "Cannot link to DMR APRS system index " << digitalAPRSSystemIndex() << ": undefined DMR APRS system."; - else if (ctx.has(digitalAPRSSystemIndex())) - dc->setAPRSObj(ctx.get(digitalAPRSSystemIndex())); - // Link radio ID DMRRadioID *rid = ctx.get(radioIDIndex()); if (rid == ctx.config()->radioIDs()->defaultId()) @@ -865,15 +779,6 @@ AnytoneCodeplug::ChannelElement::fromChannelObj(const Channel *c, Context &ctx) clearGroupListIndex(); else setGroupListIndex(ctx.index(dc->groupListObj())); - // Set GPS system index - if (dc->aprsObj() && dc->aprsObj()->is()) { - setDigitalAPRSSystemIndex(ctx.index(dc->aprsObj()->as())); - enableTXDigitalAPRS(true); - enableRXAPRS(false); - } else { - enableTXDigitalAPRS(false); - enableRXAPRS(false); - } // Set radio ID if ((nullptr == dc->radioIdObj()) || (DefaultRadioID::get() == dc->radioIdObj())) { if (nullptr == ctx.config()->radioIDs()->defaultId()) { @@ -891,13 +796,10 @@ AnytoneCodeplug::ChannelElement::fromChannelObj(const Channel *c, Context &ctx) enableTalkaround(ext->talkaround()); // Apply DMR settings enableCallConfirm(ext->callConfirm()); - enableSMS(ext->sms()); enableSMSConfirm(ext->smsConfirm()); - enableDataACK(ext->dataACK()); enableSimplexTDMA(ext->simplexTDMA()); enableAdaptiveTDMA(ext->adaptiveTDMA()); enableLoneWorker(ext->loneWorker()); - enableThroughMode(ext->throughMode()); } } diff --git a/lib/anytone_codeplug.hh b/lib/anytone_codeplug.hh index c346ed9e..7943d6c2 100644 --- a/lib/anytone_codeplug.hh +++ b/lib/anytone_codeplug.hh @@ -301,45 +301,6 @@ public: /** Sets the channel name. */ virtual void setName(const QString &name); - /** Returns @c true if ranging is enabled. */ - virtual bool ranging() const; - /** Enables/disables ranging. */ - virtual void enableRanging(bool enable); - /** Returns @c true if through mode is enabled. */ - virtual bool throughMode() const; - /** Enables/disables though mode. */ - virtual void enableThroughMode(bool enable); - /** Returns @c true if data ACK is enabled. */ - virtual bool dataACK() const; - /** Enables/disables data ACK. */ - virtual void enableDataACK(bool enable); - - /** Returns @c true if TX APRS is enabled. */ - virtual bool txDigitalAPRS() const; - /** Enables/disables TX APRS. */ - virtual void enableTXDigitalAPRS(bool enable); - /** Returns the DMR APRS system index. */ - virtual unsigned digitalAPRSSystemIndex() const; - /** Sets the DMR APRS system index. */ - virtual void setDigitalAPRSSystemIndex(unsigned idx); - - /** Returns the DMR encryption key index (+1), 0=Off. */ - virtual unsigned dmrEncryptionKeyIndex() const; - /** Sets the DMR encryption key index (+1), 0=Off. */ - virtual void setDMREncryptionKeyIndex(unsigned idx); - /** Returns @c true if multiple key encryption is enabled. */ - virtual bool multipleKeyEncryption() const; - /** Enables/disables multiple key encryption. */ - virtual void enableMultipleKeyEncryption(bool enable); - /** Returns @c true if random key is enabled. */ - virtual bool randomKey() const; - /** Enables/disables random key. */ - virtual void enableRandomKey(bool enable); - /** Returns @c true if SMS is enabled. */ - virtual bool sms() const; - /** Enables/disables SMS. */ - virtual void enableSMS(bool enable); - /** Constructs a generic @c Channel object from the codeplug channel. */ virtual Channel *toChannelObj(Context &ctx) const; /** Links a previously constructed channel to the rest of the configuration. */ diff --git a/lib/d578uv.cc b/lib/d578uv.cc index 968e6d44..1fe51db4 100644 --- a/lib/d578uv.cc +++ b/lib/d578uv.cc @@ -25,6 +25,7 @@ D578UV::D578UV(AnytoneInterface *device, QObject *parent) case 0x01: _limits = new D578UVLimits({ {136., 174.}, {400., 480.} }, { {136., 174.}, {400., 480.} }, info.version, this); + break; case 0x02: _limits = new D578UVLimits({ {136., 174.}, {430., 440.} }, { {136., 174.}, {430., 440.} }, info.version, this); @@ -32,6 +33,7 @@ D578UV::D578UV(AnytoneInterface *device, QObject *parent) case 0x03: _limits = new D578UVLimits({ {136., 174.}, {400., 480.} }, { {144., 146.}, {430., 440.} }, info.version, this); + break; case 0x04: _limits = new D578UVLimits({ {144., 146.}, {434., 438.} }, { {144., 146.}, {434., 438.} }, info.version, this); diff --git a/lib/d868uv_codeplug.cc b/lib/d868uv_codeplug.cc index 675ab8dd..86c9a118 100644 --- a/lib/d868uv_codeplug.cc +++ b/lib/d868uv_codeplug.cc @@ -220,6 +220,163 @@ D868UVCodeplug::ctcss_num2code(uint8_t num) { } +/* ******************************************************************************************** * + * Implementation of D868UVCodeplug::ChannelElement + * ******************************************************************************************** */ +D868UVCodeplug::ChannelElement::ChannelElement(uint8_t *ptr, unsigned size) + : AnytoneCodeplug::ChannelElement(ptr, size) +{ + // pass... +} + +D868UVCodeplug::ChannelElement::ChannelElement(uint8_t *ptr) + : AnytoneCodeplug::ChannelElement(ptr) +{ + // pass... +} + +bool +D868UVCodeplug::ChannelElement::ranging() const { + return getBit(0x0034, 0); +} +void +D868UVCodeplug::ChannelElement::enableRanging(bool enable) { + setBit(0x0034, 0, enable); +} + +bool +D868UVCodeplug::ChannelElement::throughMode() const { + return getBit(0x0034, 1); +} +void +D868UVCodeplug::ChannelElement::enableThroughMode(bool enable) { + setBit(0x0034, 1, enable); +} + +bool +D868UVCodeplug::ChannelElement::dataACK() const { + return !getBit(0x0034, 2); +} +void +D868UVCodeplug::ChannelElement::enableDataACK(bool enable) { + setBit(0x0034, 2, !enable); +} + +bool +D868UVCodeplug::ChannelElement::txDigitalAPRS() const { + return getBit(0x0035, 0); +} +void +D868UVCodeplug::ChannelElement::enableTXDigitalAPRS(bool enable) { + setBit(0x0035, 0, enable); +} +unsigned +D868UVCodeplug::ChannelElement::digitalAPRSSystemIndex() const { + return getUInt8(0x0036); +} +void +D868UVCodeplug::ChannelElement::setDigitalAPRSSystemIndex(unsigned idx) { + setUInt8(0x0036, idx); +} + +unsigned +D868UVCodeplug::ChannelElement::dmrEncryptionKeyIndex() const { + return getUInt8(0x003a); +} +void +D868UVCodeplug::ChannelElement::setDMREncryptionKeyIndex(unsigned idx) { + setUInt8(0x003a, idx); +} + +bool +D868UVCodeplug::ChannelElement::multipleKeyEncryption() const { + return getBit(0x003b, 0); +} +void +D868UVCodeplug::ChannelElement::enableMultipleKeyEncryption(bool enable) { + setBit(0x003b, 0, enable); +} + +bool +D868UVCodeplug::ChannelElement::randomKey() const { + return getBit(0x003b, 1); +} +void +D868UVCodeplug::ChannelElement::enableRandomKey(bool enable) { + setBit(0x003b, 1, enable); +} +bool +D868UVCodeplug::ChannelElement::sms() const { + return !getBit(0x003b, 2); +} +void +D868UVCodeplug::ChannelElement::enableSMS(bool enable) { + setBit(0x003b, 0, !enable); +} + +Channel * +D868UVCodeplug::ChannelElement::toChannelObj(Context &ctx) const { + Channel *ch = AnytoneCodeplug::ChannelElement::toChannelObj(ctx); + if (nullptr == ch) + return nullptr; + + if (ch->is()) { + DMRChannel *dch = ch->as(); + if (AnytoneDMRChannelExtension *ext = dch->anytoneChannelExtension()) { + ext->enableSMS(sms()); + ext->enableDataACK(dataACK()); + ext->enableThroughMode(throughMode()); + } + } + + return ch; +} + +bool +D868UVCodeplug::ChannelElement::linkChannelObj(Channel *c, Context &ctx) const { + if (! AnytoneCodeplug::ChannelElement::linkChannelObj(c, ctx)) + return false; + + if (c->is()) { + DMRChannel *dc = c->as(); + // Link to GPS system + if (txDigitalAPRS() && (! ctx.has(digitalAPRSSystemIndex()))) + logWarn() << "Cannot link to DMR APRS system index " << digitalAPRSSystemIndex() << ": undefined DMR APRS system."; + else if (ctx.has(digitalAPRSSystemIndex())) + dc->setAPRSObj(ctx.get(digitalAPRSSystemIndex())); + } + + return true; +} + +bool +D868UVCodeplug::ChannelElement::fromChannelObj(const Channel *c, Context &ctx) { + if (! AnytoneCodeplug::ChannelElement::fromChannelObj(c, ctx)) + return false; + + if (c->is()) { + const DMRChannel *dc = c->as(); + // Set GPS system index + if (dc->aprsObj() && dc->aprsObj()->is()) { + setDigitalAPRSSystemIndex(ctx.index(dc->aprsObj()->as())); + enableTXDigitalAPRS(true); + enableRXAPRS(false); + } else { + enableTXDigitalAPRS(false); + enableRXAPRS(false); + } + + // Handle extension + if (AnytoneDMRChannelExtension *ext = dc->anytoneChannelExtension()) { + enableSMS(ext->sms()); + enableDataACK(ext->dataACK()); + enableThroughMode(ext->throughMode()); + } + } + + return true; +} + /* ******************************************************************************************** * * Implementation of D868UVCodeplug::GeneralSettingsElement * ******************************************************************************************** */ diff --git a/lib/d868uv_codeplug.hh b/lib/d868uv_codeplug.hh index 9c214603..14f55df8 100644 --- a/lib/d868uv_codeplug.hh +++ b/lib/d868uv_codeplug.hh @@ -190,6 +190,69 @@ class D868UVCodeplug : public AnytoneCodeplug Q_OBJECT public: + /** Represents the channel element for AnyTone D868UV devices. + * This class derives from @c AnytoneCodeplug::ChannelElement and implements the device-specific + * encoding of channels for the AnyTone D868UV. + * + * Memory layout of the encoded channel element (size 0x0040 bytes): + * @verbinclude d868uv_channel.txt */ + class ChannelElement: public AnytoneCodeplug::ChannelElement + { + protected: + /** Hidden constructor. */ + ChannelElement(uint8_t *ptr, unsigned size); + + public: + /** Constructor. */ + ChannelElement(uint8_t *ptr); + + /** Returns @c true if ranging is enabled. */ + virtual bool ranging() const; + /** Enables/disables ranging. */ + virtual void enableRanging(bool enable); + /** Returns @c true if through mode is enabled. */ + virtual bool throughMode() const; + /** Enables/disables though mode. */ + virtual void enableThroughMode(bool enable); + /** Returns @c true if data ACK is enabled. */ + virtual bool dataACK() const; + /** Enables/disables data ACK. */ + virtual void enableDataACK(bool enable); + + /** Returns @c true if TX APRS is enabled. */ + virtual bool txDigitalAPRS() const; + /** Enables/disables TX APRS. */ + virtual void enableTXDigitalAPRS(bool enable); + /** Returns the DMR APRS system index. */ + virtual unsigned digitalAPRSSystemIndex() const; + /** Sets the DMR APRS system index. */ + virtual void setDigitalAPRSSystemIndex(unsigned idx); + + /** Returns the DMR encryption key index (+1), 0=Off. */ + virtual unsigned dmrEncryptionKeyIndex() const; + /** Sets the DMR encryption key index (+1), 0=Off. */ + virtual void setDMREncryptionKeyIndex(unsigned idx); + /** Returns @c true if multiple key encryption is enabled. */ + virtual bool multipleKeyEncryption() const; + /** Enables/disables multiple key encryption. */ + virtual void enableMultipleKeyEncryption(bool enable); + /** Returns @c true if random key is enabled. */ + virtual bool randomKey() const; + /** Enables/disables random key. */ + virtual void enableRandomKey(bool enable); + /** Returns @c true if SMS is enabled. */ + virtual bool sms() const; + /** Enables/disables SMS. */ + virtual void enableSMS(bool enable); + + /** Constructs a generic @c Channel object from the codeplug channel. */ + virtual Channel *toChannelObj(Context &ctx) const; + /** Links a previously constructed channel to the rest of the configuration. */ + virtual bool linkChannelObj(Channel *c, Context &ctx) const; + /** Initializes this codeplug channel from the given generic configuration. */ + virtual bool fromChannelObj(const Channel *c, Context &ctx); + }; + /** Represents the general config of the radio within the D868UV binary codeplug. * * This class only implements the differences to the generic diff --git a/lib/d878uv_codeplug.cc b/lib/d878uv_codeplug.cc index f9ad7af0..da6d8044 100644 --- a/lib/d878uv_codeplug.cc +++ b/lib/d878uv_codeplug.cc @@ -75,13 +75,13 @@ * Implementation of D878UVCodeplug::ChannelElement * ******************************************************************************************** */ D878UVCodeplug::ChannelElement::ChannelElement(uint8_t *ptr, unsigned size) - : AnytoneCodeplug::ChannelElement(ptr, size) + : D868UVCodeplug::ChannelElement(ptr, size) { // pass... } D878UVCodeplug::ChannelElement::ChannelElement(uint8_t *ptr) - : AnytoneCodeplug::ChannelElement(ptr, 0x0040) + : D868UVCodeplug::ChannelElement(ptr, 0x0040) { // pass... } @@ -177,7 +177,7 @@ D878UVCodeplug::ChannelElement::setFrequencyCorrection(int corr) { Channel * D878UVCodeplug::ChannelElement::toChannelObj(Context &ctx) const { - Channel *ch = AnytoneCodeplug::ChannelElement::toChannelObj(ctx); + Channel *ch = D868UVCodeplug::ChannelElement::toChannelObj(ctx); if (nullptr == ch) return nullptr; @@ -200,7 +200,7 @@ D878UVCodeplug::ChannelElement::toChannelObj(Context &ctx) const { bool D878UVCodeplug::ChannelElement::linkChannelObj(Channel *c, Context &ctx) const { - if (! AnytoneCodeplug::ChannelElement::linkChannelObj(c, ctx)) + if (! D868UVCodeplug::ChannelElement::linkChannelObj(c, ctx)) return false; if (c->is()) { @@ -228,7 +228,7 @@ D878UVCodeplug::ChannelElement::linkChannelObj(Channel *c, Context &ctx) const { bool D878UVCodeplug::ChannelElement::fromChannelObj(const Channel *c, Context &ctx) { - if (! AnytoneCodeplug::ChannelElement::fromChannelObj(c, ctx)) + if (! D868UVCodeplug::ChannelElement::fromChannelObj(c, ctx)) return false; if (const DMRChannel *dc = c->as()) { diff --git a/lib/d878uv_codeplug.hh b/lib/d878uv_codeplug.hh index 18f3b54c..d195953f 100644 --- a/lib/d878uv_codeplug.hh +++ b/lib/d878uv_codeplug.hh @@ -237,7 +237,7 @@ public: * Memory layout of encoded channel (size 0x40 bytes): * @verbinclude d878uv_channel.txt */ - class ChannelElement: public AnytoneCodeplug::ChannelElement + class ChannelElement: public D868UVCodeplug::ChannelElement { public: /** Possible PTT ID settings. */ diff --git a/lib/dmr6x2uv_codeplug.cc b/lib/dmr6x2uv_codeplug.cc index b611e5cc..799d68b5 100644 --- a/lib/dmr6x2uv_codeplug.cc +++ b/lib/dmr6x2uv_codeplug.cc @@ -580,6 +580,78 @@ DMR6X2UVCodeplug::ExtendedSettingsElement::updateConfig(Context &ctx) { +/* ********************************************************************************************* * + * Implementation of DMR6X2UVCodeplug::ChannelElement + * ********************************************************************************************* */ +DMR6X2UVCodeplug::ChannelElement::ChannelElement(uint8_t *ptr, unsigned size) + : AnytoneCodeplug::ChannelElement(ptr, size) +{ + // pass... +} + +DMR6X2UVCodeplug::ChannelElement::ChannelElement(uint8_t *ptr) + : AnytoneCodeplug::ChannelElement(ptr) +{ + // pass... +} + +bool +DMR6X2UVCodeplug::ChannelElement::hasScanListIndex() const { + return hasScanListIndex(0); +} +unsigned +DMR6X2UVCodeplug::ChannelElement::scanListIndex() const { + return scanListIndex(0); +} +void +DMR6X2UVCodeplug::ChannelElement::setScanListIndex(unsigned idx) { + setScanListIndex(0, idx); +} +void +DMR6X2UVCodeplug::ChannelElement::clearScanListIndex() { + clearScanListIndex(0); +} + +bool +DMR6X2UVCodeplug::ChannelElement::hasScanListIndex(unsigned int n) const { + return 0xff != scanListIndex(n); +} +unsigned int +DMR6X2UVCodeplug::ChannelElement::scanListIndex(unsigned int n) const { + if (n > 7) + return 0xff; + return getUInt8(0x0036+n); +} +void +DMR6X2UVCodeplug::ChannelElement::setScanListIndex(unsigned int n, unsigned idx) { + if (n > 7) + return; + setUInt8(0x0036+n, idx); +} +void +DMR6X2UVCodeplug::ChannelElement::clearScanListIndex(unsigned int n) { + setScanListIndex(n, 0xff); +} + +bool +DMR6X2UVCodeplug::ChannelElement::roamingEnabled() const { + return ! getBit(0x001b, 2); +} +void +DMR6X2UVCodeplug::ChannelElement::enableRoaming(bool enable) { + setBit(0x001b, 2, !enable); +} + +bool +DMR6X2UVCodeplug::ChannelElement::ranging() const { + return getBit(0x001b, 0); +} +void +DMR6X2UVCodeplug::ChannelElement::enableRanging(bool enable) { + return setBit(0x001b, 0, enable); +} + + /* ********************************************************************************************* * * Implementation of DMR6X2UVCodeplug * ********************************************************************************************* */ diff --git a/lib/dmr6x2uv_codeplug.hh b/lib/dmr6x2uv_codeplug.hh index 6c27556f..44600159 100644 --- a/lib/dmr6x2uv_codeplug.hh +++ b/lib/dmr6x2uv_codeplug.hh @@ -541,6 +541,52 @@ public: virtual bool updateConfig(Context &ctx); }; + /** Implements the channel element for the BTECH DMR-6X2UV. + * Extends the AnytoneCodeplug::ChannelElement by the device specific features, like multiple + * scan lists associated with the channel. + * + * Memory representation of the encoded channel element (size 0x040 bytes): + * @verbinclude dmr6x2uv_channel.txt */ + class ChannelElement: public AnytoneCodeplug::ChannelElement + { + protected: + /** Hidden constructor. */ + ChannelElement(uint8_t *ptr, unsigned size); + + public: + /** Constructor. */ + ChannelElement(uint8_t *ptr); + + /** Returns @c true, if the first scan list index is set. */ + bool hasScanListIndex() const; + /** Returns the first scan list index (0-based). */ + unsigned scanListIndex() const; + /** Sets the first scan list index (0-based). */ + void setScanListIndex(unsigned idx); + /** Clears the first scan list index. */ + void clearScanListIndex(); + + /** Returns @c true, if the n-th scan list index is set (n=0,...,7). */ + virtual bool hasScanListIndex(unsigned int n) const; + /** Returns the n-th scan list index (0-based, n=0,...,7). */ + virtual unsigned scanListIndex(unsigned int n) const; + /** Sets the n-th scan list index (0-based, n=0,...,7). */ + virtual void setScanListIndex(unsigned int n, unsigned idx); + /** Clears the n-th scan list index (n=0,...,7). */ + virtual void clearScanListIndex(unsigned int n); + + /** Returns @c true if roaming is enabled for this channel. */ + virtual bool roamingEnabled() const; + /** Enables/disables roaming. */ + virtual void enableRoaming(bool enable); + + /** Returns @c true, if ranging is enabled. */ + bool ranging() const; + /** Enables/disables ranging. */ + void enableRanging(bool enable); + + }; + public: /** Empty constructor. */ explicit DMR6X2UVCodeplug(QObject *parent=nullptr); From 22c8be2cae94af0f390dffbf664e8ae5f9b4e400 Mon Sep 17 00:00:00 2001 From: Hannes Matuschek Date: Mon, 2 Jan 2023 14:00:00 +0100 Subject: [PATCH 11/16] Implemented comparison of config-items and lists. --- CMakeLists.txt | 4 + lib/configobject.cc | 83 ++++++ lib/configobject.hh | 5 + lib/configreference.cc | 11 + lib/configreference.hh | 3 + test/CMakeLists.txt | 11 +- test/configtest.cc | 233 +---------------- test/configtest.hh | 13 +- test/data/config_test.yaml | 95 +++++++ test/rd5rtest.cc | 491 ---------------------------------- test/rd5rtest.hh | 38 --- test/resources.qrc | 2 +- test/testconfig.conf | 110 -------- test/uv390test.cc | 524 ------------------------------------- test/uv390test.hh | 38 --- 15 files changed, 211 insertions(+), 1450 deletions(-) create mode 100644 test/data/config_test.yaml delete mode 100644 test/rd5rtest.cc delete mode 100644 test/rd5rtest.hh delete mode 100644 test/testconfig.conf delete mode 100644 test/uv390test.cc delete mode 100644 test/uv390test.hh diff --git a/CMakeLists.txt b/CMakeLists.txt index f8e1ec52..94e22c7d 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -58,6 +58,10 @@ LINK_DIRECTORIES(${PROJECT_BINARY_DIR}/src) set(CORE_LIBS ${Qt5Core_LIBRARIES} ${Qt5Core_QTMAIN_LIBRARIES} ${Qt5Network_LIBRARIES} ${Qt5Positioning_LIBRARIES} ${Qt5SerialPort_LIBRARIES} ${LIBUSB_1_LIBRARIES} ${YAMLCPP_LIBRARIES}) +if (${BUILD_TESTS}) + set(CORE_LIBS ${CORE_LIBS} ${Qt5Test_LIBRARIES}) +endif(${BUILD_TESTS}) + set(LIBS ${CORE_LIBS} ${Qt5Widgets_LIBRARIES} ${Qt5UiTools_LIBRARIES}) IF (UNIX AND APPLE) diff --git a/lib/configobject.cc b/lib/configobject.cc index 878ec08b..ffb901c0 100644 --- a/lib/configobject.cc +++ b/lib/configobject.cc @@ -229,6 +229,76 @@ ConfigItem::copy(const ConfigItem &other) { return true; } +int +ConfigItem::compare(const ConfigItem &other) const { + // Check if other has the same type + if (strcmp(other.metaObject()->className(), metaObject()->className())) + return strcmp(metaObject()->className(), other.metaObject()->className()); + + // Compare by properties + const QMetaObject *meta = metaObject(); + for (int p=QObject::staticMetaObject.propertyCount(); ppropertyCount(); p++) { + // This property + QMetaProperty prop = meta->property(p); + // the same property over at other + QMetaProperty oprop = other.metaObject()->property(p); + + // Should never happen + if (! prop.isValid()) + continue; + + // Handle comparison of basic types + if ((prop.isEnumType()) || (QVariant::Bool == prop.type()) || (QVariant::Int == prop.type()) || (QVariant::UInt == prop.type())) { + int a=prop.read(this).toInt(), b=oprop.read(&other).toInt(); + if (ab) return 1; + continue; + } + + if (QVariant::Double == prop.type()) { + double a=prop.read(this).toDouble(), b=oprop.read(&other).toDouble(); + if (ab) return 1; + continue; + } + + if (QVariant::String == prop.type()) { + int cmp = QString::compare(prop.read(this).toString(), oprop.read(&other).toString()); + if (cmp) return cmp; + } + + if (ConfigObjectReference *ref = prop.read(this).value()) { + int cmp = ref->compare(*oprop.read(&other).value()); + if (cmp) return cmp; + } + + if (ConfigObjectList *lst = prop.read(this).value()) { + int cmp = lst->compare(*oprop.read(&other).value()); + if (cmp) return cmp; + } + + if (ConfigObjectRefList *lst = prop.read(this).value()) { + int cmp = lst->copy(*oprop.read(&other).value()); + if (cmp) return cmp; + } + + if (propIsInstance(prop)) { + // If the owned item is writeable -> clone if set in other + if (prop.read(&other).isNull() && !oprop.read(&other).isNull()) + return -1; + if (!prop.read(&other).isNull() && oprop.read(&other).isNull()) + return 1; + if (prop.read(&other).isNull() && oprop.read(&other).isNull()) + continue; + int cmp = prop.read(&other).value()->compare(*oprop.read(&other).value()); + if (cmp) return cmp; + } + } + + return 0; +} + + bool ConfigItem::label(ConfigObject::Context &context, const ErrorStack &err) { // Label properties owning config objects, that is of type ConfigObject or ConfigObjectList @@ -1264,6 +1334,19 @@ ConfigObjectList::copy(const AbstractConfigObjectList &other) { return true; } +int +ConfigObjectList::compare(const ConfigObjectList &other) const { + if (count() < other.count()) + return -1; + if (count() > other.count()) + return 1; + for (int i=0; iget(i)->compare(*other.get(i)); + if (cmp) return cmp; + } + + return 0; +} /* ********************************************************************************************* * * Implementation of ConfigObjectRefList diff --git a/lib/configobject.hh b/lib/configobject.hh index ab27f094..47ee94bc 100644 --- a/lib/configobject.hh +++ b/lib/configobject.hh @@ -104,6 +104,8 @@ public: virtual bool copy(const ConfigItem &other); /** Clones this item. */ virtual ConfigItem *clone() const = 0; + /** Compares two items. */ + virtual int compare(const ConfigItem &other) const; public: /** Recursively labels the config object. @@ -338,6 +340,9 @@ public: void clear(); bool copy(const AbstractConfigObjectList &other); + /** Compares the elements within the list. */ + virtual int compare(const ConfigObjectList &other) const; + /** Allocates a member objects for the given YAML node. */ virtual ConfigItem *allocateChild(const YAML::Node &node, ConfigItem::Context &ctx, const ErrorStack &err=ErrorStack()) = 0; /** Parses the list from the YAML node. */ diff --git a/lib/configreference.cc b/lib/configreference.cc index ba2727e2..22e8dc83 100644 --- a/lib/configreference.cc +++ b/lib/configreference.cc @@ -73,6 +73,17 @@ ConfigObjectReference::copy(const ConfigObjectReference *ref) { return set(ref->_object); } +int +ConfigObjectReference::compare(const ConfigObjectReference &other) const { + if (isNull() && other.isNull()) + return 0; + if (!isNull() && other.isNull()) + return 1; + if (isNull() && !other.isNull()) + return -1; + return this->_object->compare(*other._object); +} + bool ConfigObjectReference::allow(const QMetaObject *elementType) { if (! _elementTypes.contains(elementType->className())) diff --git a/lib/configreference.hh b/lib/configreference.hh index 65b66324..2fb8e398 100644 --- a/lib/configreference.hh +++ b/lib/configreference.hh @@ -56,6 +56,9 @@ public: return _object->is(); } + /** Compares the references. */ + int compare(const ConfigObjectReference &other) const; + signals: /** Gets emitted if the reference is changed. * This signal is not emitted if the referenced object is modified. */ diff --git a/test/CMakeLists.txt b/test/CMakeLists.txt index 0a4bcd82..f4f1ab6f 100644 --- a/test/CMakeLists.txt +++ b/test/CMakeLists.txt @@ -12,16 +12,7 @@ qt5_wrap_cpp(utilstest_MOC_SOURCES utilstest.hh) add_executable(utilstest utilstest.cc ${utilstest_MOC_SOURCES}) target_link_libraries(utilstest ${LIBS} libdmrconf) -qt5_wrap_cpp(rd5rtest_MOC_SOURCES rd5rtest.hh) -add_executable(rd5rtest rd5rtest.cc ${rd5rtest_MOC_SOURCES} ${testlib_RCC_SOURCES}) -target_link_libraries(rd5rtest ${LIBS} libdmrconf) - -qt5_wrap_cpp(uv390test_MOC_SOURCES uv390test.hh) -add_executable(uv390test uv390test.cc ${uv390test_MOC_SOURCES} ${testlib_RCC_SOURCES}) -target_link_libraries(uv390test ${LIBS} libdmrconf) - add_test(NAME Config COMMAND configtest) add_test(NAME CRC32 COMMAND crc32test) add_test(NAME Utils COMMAND utilstest) -add_test(NAME RD5R COMMAND rd5rtest) -add_test(NAME UV390 COMMAND uv390test) + diff --git a/test/configtest.cc b/test/configtest.cc index 638636d9..d8b5345a 100644 --- a/test/configtest.cc +++ b/test/configtest.cc @@ -11,241 +11,22 @@ ConfigTest::ConfigTest(QObject *parent) : QObject(parent) void ConfigTest::initTestCase() { - // Read simple configuration file - QString errMessage; - QVERIFY(_config.readCSV("://testconfig.conf", errMessage)); + _config.readYAML(":/config_test.yaml"); } void ConfigTest::cleanupTestCase() { // clear codeplug - _config.reset(); + _config.clear(); } void -ConfigTest::testRadioName() { - QCOMPARE(_config.name(), QString("DM3MAT")); -} - -void -ConfigTest::testDMRID() { - QCOMPARE(_config.id(), 1234567U); -} - -void -ConfigTest::testIntroLines() { - QCOMPARE(_config.introLine1(), QString("ABC")); - QCOMPARE(_config.introLine2(), QString("DEF")); -} - -void -ConfigTest::testMicLevel() { - QCOMPARE(_config.micLevel(), 3u); -} - -void -ConfigTest::testSpeechSynthesis() { - QCOMPARE(_config.speech(), false); -} - -void -ConfigTest::testDigitalContacts() { - QCOMPARE(_config.contacts()->digitalCount(), 4); // Number of digital contacts - /* - * Test Contact 01 - */ - QCOMPARE(_config.contacts()->digitalContact(0)->name(), QString("Local")); - QCOMPARE(_config.contacts()->digitalContact(0)->number(), 9U); - QCOMPARE(_config.contacts()->digitalContact(0)->type(), DigitalContact::GroupCall); - QCOMPARE(_config.contacts()->digitalContact(0)->rxTone(), false); // receive tone (off) - - /* - * Test Contact 02 - */ - QCOMPARE(_config.contacts()->digitalContact(1)->name(), QString("Bln/Brb")); - QCOMPARE(_config.contacts()->digitalContact(1)->number(), 2621U); - QCOMPARE(_config.contacts()->digitalContact(1)->type(), DigitalContact::GroupCall); // group call - QCOMPARE(_config.contacts()->digitalContact(1)->rxTone(), false); // receive tone (off) - - /* - * Test Contact 03 - */ - QCOMPARE(_config.contacts()->digitalContact(2)->name(), QString("All Call")); - QCOMPARE(_config.contacts()->digitalContact(2)->number(), 16777215U); - QCOMPARE(_config.contacts()->digitalContact(2)->type(), DigitalContact::AllCall); // all call - QCOMPARE(_config.contacts()->digitalContact(2)->rxTone(), false); // receive tone (off) - - /* - * Test Contact 04 - */ - QCOMPARE(_config.contacts()->digitalContact(3)->name(), QString("BM APRS")); - QCOMPARE(_config.contacts()->digitalContact(3)->number(), 262999U); - QCOMPARE(_config.contacts()->digitalContact(3)->type(), DigitalContact::PrivateCall); // private call - QCOMPARE(_config.contacts()->digitalContact(3)->rxTone(), false); // receive tone (off) -} - -void -ConfigTest::testRXGroups() { - /* - * Test RX Group List table - */ - QCOMPARE(_config.rxGroupLists()->count(), 1); // Number group lists - - /* - * Test RX Group List 01 - */ - QCOMPARE(_config.rxGroupLists()->list(0)->count(), 2); // Number group lists - QCOMPARE(_config.rxGroupLists()->list(0)->name(), QString("Berlin/Brand")); // Check name - QCOMPARE(_config.contacts()->indexOfDigital(_config.rxGroupLists()->list(0)->contact(0)), 0x00); // 1st member index +1 - QCOMPARE(_config.contacts()->indexOfDigital(_config.rxGroupLists()->list(0)->contact(1)), 0x01); // 2nd member index +1 -} - -void -ConfigTest::testDigitalChannels() { - /* - * Test Channel bank 0 - */ - QCOMPARE(_config.channelList()->count(), 0x5); // First 5 channels enabled - - /* - * Test channel 01 - */ - QCOMPARE(_config.channelList()->channel(0)->name(), QString("BB DB0LDS TS2")); // Check name - QCOMPARE(_config.channelList()->channel(0)->rxFrequency(), 439.563); // check RX Frequency - QCOMPARE(_config.channelList()->channel(0)->txFrequency(), 431.963); // check TX Frequency - QCOMPARE(_config.channelList()->channel(0)->power(), Channel::HighPower); // check power - QCOMPARE(_config.channelList()->channel(0)->scanList(), nullptr); // Scanlist - QCOMPARE(_config.channelList()->channel(0)->txTimeout(), 45U); - QCOMPARE(_config.channelList()->channel(0)->rxOnly(), false); - QCOMPARE(_config.channelList()->channel(0)->as()->admit(), DigitalChannel::AdmitColorCode); - QCOMPARE(_config.channelList()->channel(0)->as()->colorCode(), 1u); - QCOMPARE(_config.channelList()->channel(0)->as()->timeslot(), DigitalChannel::TimeSlot2); - QCOMPARE(_config.rxGroupLists()->indexOf( - _config.channelList()->channel(0)->as()->rxGroupList()), 0); // RX Group list index - QCOMPARE(_config.contacts()->indexOfDigital( - _config.channelList()->channel(0)->as()->txContact()), 1); // TX contact index - QCOMPARE(_config.gpsSystems()->indexOf( - _config.channelList()->channel(0)->as()->gpsSystem()), 0); // GPS system index - - /* - * Test channel 02 - */ - QCOMPARE(_config.channelList()->channel(1)->name(), QString("BB DM0TT TS2")); // Check name - QCOMPARE(_config.channelList()->channel(1)->rxFrequency(), 439.0870); // check RX Frequency - QCOMPARE(_config.channelList()->channel(1)->txFrequency(), 431.4870); // check TX Frequency - QCOMPARE(_config.channelList()->channel(1)->power(), Channel::HighPower); // check power - QCOMPARE(_config.channelList()->channel(1)->scanList(), nullptr); // Scanlist - QCOMPARE(_config.channelList()->channel(1)->txTimeout(), 45U); - QCOMPARE(_config.channelList()->channel(1)->rxOnly(), false); - QCOMPARE(_config.channelList()->channel(1)->as()->admit(), DigitalChannel::AdmitColorCode); - QCOMPARE(_config.channelList()->channel(1)->as()->colorCode(), 1u); - QCOMPARE(_config.channelList()->channel(1)->as()->timeslot(), DigitalChannel::TimeSlot2); - QCOMPARE(_config.rxGroupLists()->indexOf( - _config.channelList()->channel(1)->as()->rxGroupList()), 0); // RX Group list index - QCOMPARE(_config.contacts()->indexOfDigital( - _config.channelList()->channel(1)->as()->txContact()), 1); // TX contact index - QCOMPARE(_config.channelList()->channel(1)->as()->gpsSystem(), nullptr); // GPS system none - - /* - * Test channel 03 - */ - QCOMPARE(_config.channelList()->channel(2)->name(), QString("TG9 DB0KK TS1")); // Check name - QCOMPARE(_config.channelList()->channel(2)->rxFrequency(), 439.5380); // check RX Frequency - QCOMPARE(_config.channelList()->channel(2)->txFrequency(), 431.9380); // check TX Frequency - QCOMPARE(_config.channelList()->channel(2)->power(), Channel::HighPower); // check power - QCOMPARE(_config.channelList()->channel(2)->scanList(), nullptr); // Scanlist - QCOMPARE(_config.channelList()->channel(2)->txTimeout(), 45U); - QCOMPARE(_config.channelList()->channel(2)->rxOnly(), false); - QCOMPARE(_config.channelList()->channel(2)->as()->admit(), DigitalChannel::AdmitColorCode); - QCOMPARE(_config.channelList()->channel(2)->as()->colorCode(), 1u); - QCOMPARE(_config.channelList()->channel(2)->as()->timeslot(), DigitalChannel::TimeSlot1); - QCOMPARE(_config.rxGroupLists()->indexOf( - _config.channelList()->channel(2)->as()->rxGroupList()), 0); // RX Group list index - QCOMPARE(_config.contacts()->indexOfDigital( - _config.channelList()->channel(2)->as()->txContact()), 0); // TX contact index - QCOMPARE(_config.channelList()->channel(2)->as()->gpsSystem(), nullptr); // GPS system index -} - -void -ConfigTest::testAnalogChannels() { - /* - * Test Channel bank 0 - */ - QCOMPARE(_config.channelList()->count(), 0x5); // First 5 channels enabled - - /* - * Test channel 04 - */ - QCOMPARE(_config.channelList()->channel(3)->name(), QString("DB0LDS")); // Check name - QCOMPARE(_config.channelList()->channel(3)->rxFrequency(), 439.5625); // check RX Frequency - QCOMPARE(_config.channelList()->channel(3)->txFrequency(), 431.9625); // check TX Frequency - QCOMPARE(_config.channelList()->channel(3)->power(), Channel::HighPower); // check power - QCOMPARE(_config.channelList()->channel(3)->scanList(), nullptr); // Scanlist - QCOMPARE(_config.channelList()->channel(3)->txTimeout(), 45U); - QCOMPARE(_config.channelList()->channel(3)->rxOnly(), false); - QCOMPARE(_config.channelList()->channel(3)->as()->admit(), AnalogChannel::AdmitTone); // admit - QCOMPARE(_config.channelList()->channel(3)->as()->squelch(), 1u); // squelch - QCOMPARE(_config.channelList()->channel(3)->as()->rxTone(), Signaling::CTCSS_67_0Hz); // RX CTCSS tone - QCOMPARE(_config.channelList()->channel(3)->as()->txTone(), Signaling::CTCSS_67_0Hz); // TX CTCSS tone - - /* - * Test channel 05 - */ - QCOMPARE(_config.channelList()->channel(4)->name(), QString("DB0SP-2")); // Check name - QCOMPARE(_config.channelList()->channel(4)->rxFrequency(), 145.6000); // check RX Frequency - QCOMPARE(_config.channelList()->channel(4)->txFrequency(), 145.0000); // check TX Frequency - QCOMPARE(_config.channelList()->channel(4)->power(), Channel::HighPower); // check power - QCOMPARE(_config.channelList()->channel(4)->scanList(), nullptr); // Scanlist - QCOMPARE(_config.channelList()->channel(4)->txTimeout(), 45U); - QCOMPARE(_config.channelList()->channel(4)->rxOnly(), false); - QCOMPARE(_config.channelList()->channel(4)->as()->admit(), AnalogChannel::AdmitFree); // admit - QCOMPARE(_config.channelList()->channel(4)->as()->squelch(), 1u); // squelch - QCOMPARE(_config.channelList()->channel(4)->as()->rxTone(), Signaling::SIGNALING_NONE); // RX CTCSS tone - QCOMPARE(_config.channelList()->channel(4)->as()->txTone(), Signaling::SIGNALING_NONE); // TX CTCSS tone -} - -void -ConfigTest::testZones() { - // Check number of zones (1) - QCOMPARE(_config.zones()->count(), 1); - - /* - * Check zone 01 - */ - QCOMPARE(_config.zones()->zone(0)->name(), QString("KW")); - QCOMPARE(_config.zones()->zone(0)->A()->count(), 3); - QCOMPARE(_config.zones()->zone(0)->B()->count(), 2); - QCOMPARE(_config.channelList()->indexOf( - _config.zones()->zone(0)->A()->channel(0)), 0); - QCOMPARE(_config.channelList()->indexOf( - _config.zones()->zone(0)->A()->channel(1)), 2); - QCOMPARE(_config.channelList()->indexOf( - _config.zones()->zone(0)->A()->channel(2)), 4); - QCOMPARE(_config.channelList()->indexOf( - _config.zones()->zone(0)->B()->channel(0)), 1); - QCOMPARE(_config.channelList()->indexOf( - _config.zones()->zone(0)->B()->channel(1)), 3); -} - -void -ConfigTest::testScanLists() { - // pass... -} - -void -ConfigTest::testGPSSystems() { - // Check number of GPS systems (1) - QCOMPARE(_config.gpsSystems()->count(), 1); - - /* - * Check GPS system 01 - */ - QCOMPARE(_config.gpsSystems()->gpsSystem(0)->name(), QString("BM APRS")); - QCOMPARE(_config.contacts()->indexOfDigital( - _config.gpsSystems()->gpsSystem(0)->contact()), 3); - QCOMPARE(_config.gpsSystems()->gpsSystem(0)->period(), 300u); - QCOMPARE(_config.gpsSystems()->gpsSystem(0)->revertChannel(), nullptr); +ConfigTest::testCloneChannelBasic() { + // Check if a channel can be cloned + Channel *clone = _config.channelList()->channel(0)->clone()->as(); + QTEST_ASSERT(0 == clone->compare(*_config.channelList()->channel(0))); } QTEST_GUILESS_MAIN(ConfigTest) + diff --git a/test/configtest.hh b/test/configtest.hh index 27a8858f..b8aae98e 100644 --- a/test/configtest.hh +++ b/test/configtest.hh @@ -16,18 +16,7 @@ private slots: void initTestCase(); void cleanupTestCase(); - void testDMRID(); - void testRadioName(); - void testIntroLines(); - void testMicLevel(); - void testSpeechSynthesis(); - void testDigitalContacts(); - void testRXGroups(); - void testDigitalChannels(); - void testAnalogChannels(); - void testZones(); - void testScanLists(); - void testGPSSystems(); + void testCloneChannelBasic(); protected: Config _config; diff --git a/test/data/config_test.yaml b/test/data/config_test.yaml new file mode 100644 index 00000000..8c4f0d28 --- /dev/null +++ b/test/data/config_test.yaml @@ -0,0 +1,95 @@ +--- +version: 0.9.0 +settings: + introLine1: DM3MAT + introLine2: qDMR + micLevel: 3 + speech: false + power: High + squelch: 1 + vox: 0 + tot: 0 + defaultID: id1 +radioIDs: + - dmr: {id: id1, name: DM3MAT, number: 2621370} +contacts: + - dmr: {id: cont1, name: Local, ring: false, type: GroupCall, number: 9} + - dmr: {id: cont2, name: Regional, ring: false, type: GroupCall, number: 8} + - dmr: {id: cont3, name: BB, ring: false, type: GroupCall, number: 2621} + - dmr: {id: cont4, name: DL, ring: false, type: GroupCall, number: 262} + - dmr: {id: cont5, name: BM APRS, ring: false, type: PrivateCall, number: 262999} + - dmr: {id: cont6, name: HamRadioVillage, ring: false, type: GroupCall, number: 3177826} +groupLists: + - {id: grp1, name: Local, contacts: [cont1, cont2, cont3]} + - {id: grp2, name: DL, contacts: [cont4]} + - {id: grp3, name: HamRadioVillage, contacts: [cont6]} +channels: + - digital: + id: ch1 + name: L9 DB0LDS + rxFrequency: 439.5625 + txFrequency: 431.9625 + rxOnly: false + admit: Always + colorCode: 1 + timeSlot: TS2 + groupList: grp1 + contact: cont1 + power: High + timeout: 0 + vox: 0 + - digital: + id: ch2 + name: BB DB0LDS + rxFrequency: 439.5625 + txFrequency: 431.9625 + rxOnly: false + admit: Always + colorCode: 1 + timeSlot: TS2 + groupList: grp1 + contact: cont3 + aprs: aprs1 + power: High + timeout: 0 + vox: 0 + - digital: + id: ch3 + name: DL DB0LDS + rxFrequency: 439.5625 + txFrequency: 431.9625 + rxOnly: false + admit: Always + colorCode: 1 + timeSlot: TS1 + groupList: grp2 + contact: cont4 + power: High + timeout: 0 + vox: 0 + - digital: + id: ch4 + name: HRV DB0LDS + rxFrequency: 439.5625 + txFrequency: 431.9625 + rxOnly: false + admit: Always + colorCode: 1 + timeSlot: TS1 + groupList: grp3 + contact: cont6 + power: High + timeout: 0 + vox: 0 +zones: + - id: zone1 + name: Zu Hause + A: [ch1, ch2, ch4] + B: [ch3] +positioning: + - dmr: + id: aprs1 + name: GPS System + period: 300 + contact: cont5 +... diff --git a/test/rd5rtest.cc b/test/rd5rtest.cc deleted file mode 100644 index c16d225e..00000000 --- a/test/rd5rtest.cc +++ /dev/null @@ -1,491 +0,0 @@ -#include "rd5rtest.hh" -#include "config.hh" -#include -#include "utils.hh" -#include - -RD5RTest::RD5RTest(QObject *parent) : QObject(parent) -{ - // pass... -} - -void -RD5RTest::initTestCase() { - // Read simple configuration file - QString errMessage; - QVERIFY(_config.readCSV("://testconfig.conf", errMessage)); - // Encode config as code-plug - QVERIFY(_codeplug.encode(&_config)); -} - -void -RD5RTest::cleanupTestCase() { - // clear codeplug - _codeplug.clear(); -} - -void -RD5RTest::testRadioName() { - QCOMPARE(decode_ascii(_codeplug.data(0x000e0+0x00), 8, 0xff), QString("DM3MAT")); -} - -void -RD5RTest::testDMRID() { - QCOMPARE(decode_dmr_id_bcd(_codeplug.data(0x000e0+0x08)), 1234567U); -} - -void -RD5RTest::testIntroLines() { - QCOMPARE(decode_ascii(_codeplug.data(0x07540+0x00), 16, 0xff), QString("ABC")); - QCOMPARE(decode_ascii(_codeplug.data(0x07540+0x10), 16, 0xff), QString("DEF")); -} - -void -RD5RTest::testGeneralDefaults() { - // Unused 0x00 - QCOMPARE((int)*(uint32_t *)_codeplug.data(0x000e0+0x0c), 0); - QCOMPARE((int)*(uint8_t *)_codeplug.data(0x000e0+0x10), 0); - // TX preamble default=0x06 - QCOMPARE((int)*(uint8_t *)_codeplug.data(0x000e0+0x11), 6); - // Monitor type 0=open SQ - QCOMPARE((int)*(uint8_t *)_codeplug.data(0x000e0+0x12), 0); - // VOX sensitivity default=3 - QCOMPARE((int)*(uint8_t *)_codeplug.data(0x000e0+0x13), 3); - // low batt interval default=6 - QCOMPARE((int)*(uint8_t *)_codeplug.data(0x000e0+0x14), 6); - // Call alert dur default=0x18 - QCOMPARE((int)*(uint8_t *)_codeplug.data(0x000e0+0x15), 0x18); - // Lone worker response default=1 - QCOMPARE((int)*(uint8_t *)_codeplug.data(0x000e0+0x16), 1); - // Lone worker reminder default=10 - QCOMPARE((int)*(uint8_t *)_codeplug.data(0x000e0+0x17), 10); - // Group call hang time default=6 - QCOMPARE((int)*(uint8_t *)_codeplug.data(0x000e0+0x18), 6); - // Private call hang time default=6 - QCOMPARE((int)*(uint8_t *)_codeplug.data(0x000e0+0x19), 6); - // Up/Down-Ch mode, RST tone, unk. numb. tone, ARTS tone, default=0x40 - QCOMPARE((int)*(uint8_t *)_codeplug.data(0x000e0+0x1a), 0x40); - // perm. tone analog/digi, self-test tone, CH freq. ind. tone, dis. all tones, save bat, default=0xc4 - QCOMPARE((int)*(uint8_t *)_codeplug.data(0x000e0+0x1b), 0xc4); - // dis. LEDs, quick-key override, default=0x80 - QCOMPARE((int)*(uint8_t *)_codeplug.data(0x000e0+0x1c), 0x80); - // TX exit tone, TX on act. CH, animation, scan mode, default=0x10 - QCOMPARE((int)*(uint8_t *)_codeplug.data(0x000e0+0x1d), 0x10); - // repeat. delay & STE, default=0 - QCOMPARE((int)*(uint8_t *)_codeplug.data(0x000e0+0x1e), 0); - // unused 0x00 - QCOMPARE((int)*(uint8_t *)_codeplug.data(0x000e0+0x1f), 0); - // prog passwd., disabled=0xffffffffffffffff - QCOMPARE(*(uint64_t *)_codeplug.data(0x000e0+0x20), 0xffffffffffffffffUL); -} - -void -RD5RTest::testDigitalContacts() { - /* - * Test Contact 01 - */ - QCOMPARE(decode_ascii(_codeplug.data(0x01788+0x00), 16, 0xff), QString("Local")); - QCOMPARE(decode_dmr_id_bcd(_codeplug.data(0x01788+0x10)), 9U); - QCOMPARE((int)*((uint8_t *)_codeplug.data(0x01788+0x14)), 0x00); // group call - QCOMPARE((int)*((uint8_t *)_codeplug.data(0x01788+0x15)), 0x00); // receive tone (off) - QCOMPARE((int)*((uint8_t *)_codeplug.data(0x01788+0x16)), 0x00); // ring style (off) - QCOMPARE((int)*((uint8_t *)_codeplug.data(0x01788+0x17)), 0x00); // unused byte 0x00 - - /* - * Test Contact 02 - */ - QCOMPARE(decode_ascii(_codeplug.data(0x01788+0x18), 16, 0xff), QString("Bln/Brb")); - QCOMPARE(decode_dmr_id_bcd(_codeplug.data(0x01788+0x28)), 2621U); - QCOMPARE((int)*((uint8_t *)_codeplug.data(0x01788+0x2c)), 0x00); // group call - QCOMPARE((int)*((uint8_t *)_codeplug.data(0x01788+0x2d)), 0x00); // receive tone (off) - QCOMPARE((int)*((uint8_t *)_codeplug.data(0x01788+0x2e)), 0x00); // ring style (off) - QCOMPARE((int)*((uint8_t *)_codeplug.data(0x01788+0x2f)), 0x00); // unused byte 0x00 - - /* - * Test Contact 03 - */ - QCOMPARE(decode_ascii(_codeplug.data(0x01788+0x30), 16, 0xff), QString("All Call")); - QCOMPARE(decode_dmr_id_bcd(_codeplug.data(0x01788+0x40)), 16777215U); - QCOMPARE((int)*((uint8_t *)_codeplug.data(0x01788+0x44)), 0x02); // all call - QCOMPARE((int)*((uint8_t *)_codeplug.data(0x01788+0x45)), 0x00); // receive tone (off) - QCOMPARE((int)*((uint8_t *)_codeplug.data(0x01788+0x46)), 0x00); // ring style (off) - QCOMPARE((int)*((uint8_t *)_codeplug.data(0x01788+0x47)), 0x00); // unused byte 0x00 - - /* - * Test Contact 04 - */ - QCOMPARE(decode_ascii(_codeplug.data(0x01788+0x48), 16, 0xff), QString("BM APRS")); - QCOMPARE(decode_dmr_id_bcd(_codeplug.data(0x01788+0x58)), 262999U); - QCOMPARE((int)*((uint8_t *)_codeplug.data(0x01788+0x5c)), 0x01); // private call - QCOMPARE((int)*((uint8_t *)_codeplug.data(0x01788+0x5d)), 0x00); // receive tone (off) - QCOMPARE((int)*((uint8_t *)_codeplug.data(0x01788+0x5e)), 0x00); // ring style (off) - QCOMPARE((int)*((uint8_t *)_codeplug.data(0x01788+0x5f)), 0x00); // unused byte 0x00 -} - -void -RD5RTest::testRXGroups() { - /* - * Test RX Group List table - */ - QCOMPARE((int)*((uint8_t *)_codeplug.data(0x1d620+0x00)), 0x03); // Number of contacts in first list (+1) - QCOMPARE((int)*((uint8_t *)_codeplug.data(0x1d620+0x01)), 0x00); // Second list disabled == 0. - - /* - * Test RX Group List 01 - */ - QCOMPARE(decode_ascii(_codeplug.data(0x1d6a0+0x00), 16, 0xff), QString("Berlin/Brand")); // Check name - QCOMPARE((int)*((uint16_t *)_codeplug.data(0x1d6a0+0x10)), 0x01); // 1st member index +1 - QCOMPARE((int)*((uint16_t *)_codeplug.data(0x1d6a0+0x12)), 0x02); // 2nd member index +1 - QCOMPARE((int)*((uint16_t *)_codeplug.data(0x1d6a0+0x14)), 0x00); // 3rd member index == 0 (EOL) -} - -void -RD5RTest::testDigitalChannels() { - /* - * Test Channel bank 0 - */ - QCOMPARE((int)*((uint8_t *)_codeplug.data(0x03780+0x00)), 0x1f); // First 5 channels enabled - QCOMPARE((int)*((uint8_t *)_codeplug.data(0x03780+0x01)), 0x00); // rest is disabled - - /* - * Test channel 01 - */ - QCOMPARE(decode_ascii(_codeplug.data(0x03790+0x00), 16, 0xff), QString("BB DB0LDS TS2")); // Check name - QCOMPARE(decode_frequency(*(uint32_t *)_codeplug.data(0x03790+0x10)), 439.563); // check RX Frequency - QCOMPARE(decode_frequency(*(uint32_t *)_codeplug.data(0x03790+0x14)), 431.963); // check TX Frequency - QCOMPARE((int)*(uint8_t *)_codeplug.data(0x03790+0x18), 1); // check mode 1 == Digital - QCOMPARE((int)*(uint16_t *)_codeplug.data(0x03790+0x19), 0); // unused 0x00 - QCOMPARE((int)*(uint8_t *)_codeplug.data(0x03790+0x1b), 3); // tot 3 == 45s - QCOMPARE((int)*(uint8_t *)_codeplug.data(0x03790+0x1c), 0); // tot re-key delay 0 - QCOMPARE((int)*(uint8_t *)_codeplug.data(0x03790+0x1d), 2); // admit color code - QCOMPARE((int)*(uint8_t *)_codeplug.data(0x03790+0x1e), 0x50); // unused 0x50 - QCOMPARE((int)*(uint8_t *)_codeplug.data(0x03790+0x1f), 0); // scanlist index +1 (0=none) - QCOMPARE((int)*(uint16_t *)_codeplug.data(0x03790+0x20), 0xffff); // rx ctcss 0xffff = none - QCOMPARE((int)*(uint16_t *)_codeplug.data(0x03790+0x22), 0xffff); // tx ctcss 0xffff = none - QCOMPARE((int)*(uint8_t *)_codeplug.data(0x03790+0x24), 0); // unused 0x00 - QCOMPARE((int)*(uint8_t *)_codeplug.data(0x03790+0x25), 0); // TX sig. 0=off - QCOMPARE((int)*(uint8_t *)_codeplug.data(0x03790+0x26), 0); // unused 0x00 - QCOMPARE((int)*(uint8_t *)_codeplug.data(0x03790+0x27), 0); // RX sig. 0=off - QCOMPARE((int)*(uint8_t *)_codeplug.data(0x03790+0x28), 0x16); // unused 0x16 - QCOMPARE((int)*(uint8_t *)_codeplug.data(0x03790+0x29), 0); // priv group 0=none - QCOMPARE((int)*(uint8_t *)_codeplug.data(0x03790+0x2a), 1); // CC TX - QCOMPARE((int)*(uint8_t *)_codeplug.data(0x03790+0x2b), 1); // GprListIdx +1 - QCOMPARE((int)*(uint8_t *)_codeplug.data(0x03790+0x2c), 1); // CC RX - QCOMPARE((int)*(uint8_t *)_codeplug.data(0x03790+0x2d), 0); // Emergency Sys 0=None - QCOMPARE((int)*(uint16_t *)_codeplug.data(0x03790+0x2e), 2); // TX Contact idx +1 - QCOMPARE((int)*(uint8_t *)_codeplug.data(0x03790+0x30), 0); // Em ACK + DataCallConf. - QCOMPARE((int)*(uint8_t *)_codeplug.data(0x03790+0x31), 0x40); // PrivateCallConf, Priv., TS2 - QCOMPARE((int)*(uint8_t *)_codeplug.data(0x03790+0x32), 0); // DCDM, ... - QCOMPARE((int)*(uint8_t *)_codeplug.data(0x03790+0x33), 0x80); // Power, ... - QCOMPARE((int)*(uint8_t *)_codeplug.data(0x03790+0x34), 0); // unused 0x00 - QCOMPARE((int)*(uint8_t *)_codeplug.data(0x03790+0x35), 0); // unused 0x00 - QCOMPARE((int)*(uint8_t *)_codeplug.data(0x03790+0x36), 0); // unused 0x00 - QCOMPARE((int)*(uint8_t *)_codeplug.data(0x03790+0x37), 0); // Squelch = 0 - - /* - * Test channel 02 - */ - QCOMPARE(decode_ascii(_codeplug.data(0x037c8+0x00), 16, 0xff), QString("BB DM0TT TS2")); // Check name - QCOMPARE(decode_frequency(*(uint32_t *)_codeplug.data(0x037c8+0x10)), 439.087); // check RX Frequency - QCOMPARE(decode_frequency(*(uint32_t *)_codeplug.data(0x037c8+0x14)), 431.487); // check TX Frequency - QCOMPARE((int)*(uint8_t *)_codeplug.data(0x037c8+0x18), 1); // check mode 1 == Digital - QCOMPARE((int)*(uint16_t *)_codeplug.data(0x037c8+0x19), 0); // unused 0x00 - QCOMPARE((int)*(uint8_t *)_codeplug.data(0x037c8+0x1b), 3); // tot 3 == 45s - QCOMPARE((int)*(uint8_t *)_codeplug.data(0x037c8+0x1c), 0); // tot re-key delay 0 - QCOMPARE((int)*(uint8_t *)_codeplug.data(0x037c8+0x1d), 2); // admit color code - QCOMPARE((int)*(uint8_t *)_codeplug.data(0x037c8+0x1e), 0x50); // unused 0x50 - QCOMPARE((int)*(uint8_t *)_codeplug.data(0x037c8+0x1f), 0); // scanlist index +1 (0=none) - QCOMPARE((int)*(uint16_t *)_codeplug.data(0x037c8+0x20), 0xffff); // rx ctcss 0xffff = none - QCOMPARE((int)*(uint16_t *)_codeplug.data(0x037c8+0x22), 0xffff); // tx ctcss 0xffff = none - QCOMPARE((int)*(uint8_t *)_codeplug.data(0x037c8+0x24), 0); // unused 0x00 - QCOMPARE((int)*(uint8_t *)_codeplug.data(0x037c8+0x25), 0); // TX sig. 0=off - QCOMPARE((int)*(uint8_t *)_codeplug.data(0x037c8+0x26), 0); // unused 0x00 - QCOMPARE((int)*(uint8_t *)_codeplug.data(0x037c8+0x27), 0); // RX sig. 0=off - QCOMPARE((int)*(uint8_t *)_codeplug.data(0x037c8+0x28), 0x16); // unused 0x16 - QCOMPARE((int)*(uint8_t *)_codeplug.data(0x037c8+0x29), 0); // priv group 0=none - QCOMPARE((int)*(uint8_t *)_codeplug.data(0x037c8+0x2a), 1); // CC TX - QCOMPARE((int)*(uint8_t *)_codeplug.data(0x037c8+0x2b), 1); // GprListIdx +1 - QCOMPARE((int)*(uint8_t *)_codeplug.data(0x037c8+0x2c), 1); // CC RX - QCOMPARE((int)*(uint8_t *)_codeplug.data(0x037c8+0x2d), 0); // Emergency Sys 0=None - QCOMPARE((int)*(uint16_t *)_codeplug.data(0x037c8+0x2e), 2); // TX Contact idx +1 - QCOMPARE((int)*(uint8_t *)_codeplug.data(0x037c8+0x30), 0); // Em ACK + DataCallConf. - QCOMPARE((int)*(uint8_t *)_codeplug.data(0x037c8+0x31), 0x40); // PrivateCallConf, Priv., TS2 - QCOMPARE((int)*(uint8_t *)_codeplug.data(0x037c8+0x32), 0); // DCDM, ... - QCOMPARE((int)*(uint8_t *)_codeplug.data(0x037c8+0x33), 0x80); // Power, ... - QCOMPARE((int)*(uint8_t *)_codeplug.data(0x037c8+0x34), 0); // unused 0x00 - QCOMPARE((int)*(uint8_t *)_codeplug.data(0x037c8+0x35), 0); // unused 0x00 - QCOMPARE((int)*(uint8_t *)_codeplug.data(0x037c8+0x36), 0); // unused 0x00 - QCOMPARE((int)*(uint8_t *)_codeplug.data(0x037c8+0x37), 0); // Squelch = 0 - - /* - * Test channel 03 - */ - QCOMPARE(decode_ascii(_codeplug.data(0x03800+0x00), 16, 0xff), QString("TG9 DB0KK TS1")); // Check name - QCOMPARE(decode_frequency(*(uint32_t *)_codeplug.data(0x03800+0x10)), 439.538); // check RX Frequency - QCOMPARE(decode_frequency(*(uint32_t *)_codeplug.data(0x03800+0x14)), 431.938); // check TX Frequency - QCOMPARE((int)*(uint8_t *)_codeplug.data(0x03800+0x18), 1); // check mode 1 == Digital - QCOMPARE((int)*(uint16_t *)_codeplug.data(0x03800+0x19), 0); // unused 0x00 - QCOMPARE((int)*(uint8_t *)_codeplug.data(0x03800+0x1b), 3); // tot 3 == 45s - QCOMPARE((int)*(uint8_t *)_codeplug.data(0x03800+0x1c), 0); // tot re-key delay 0 - QCOMPARE((int)*(uint8_t *)_codeplug.data(0x03800+0x1d), 2); // admit color code - QCOMPARE((int)*(uint8_t *)_codeplug.data(0x03800+0x1e), 0x50); // unused 0x50 - QCOMPARE((int)*(uint8_t *)_codeplug.data(0x03800+0x1f), 0); // scanlist index +1 (0=none) - QCOMPARE((int)*(uint16_t *)_codeplug.data(0x03800+0x20), 0xffff); // rx ctcss 0xffff = none - QCOMPARE((int)*(uint16_t *)_codeplug.data(0x03800+0x22), 0xffff); // tx ctcss 0xffff = none - QCOMPARE((int)*(uint8_t *)_codeplug.data(0x03800+0x24), 0); // unused 0x00 - QCOMPARE((int)*(uint8_t *)_codeplug.data(0x03800+0x25), 0); // TX sig. 0=off - QCOMPARE((int)*(uint8_t *)_codeplug.data(0x03800+0x26), 0); // unused 0x00 - QCOMPARE((int)*(uint8_t *)_codeplug.data(0x03800+0x27), 0); // RX sig. 0=off - QCOMPARE((int)*(uint8_t *)_codeplug.data(0x03800+0x28), 0x16); // unused 0x16 - QCOMPARE((int)*(uint8_t *)_codeplug.data(0x03800+0x29), 0); // priv group 0=none - QCOMPARE((int)*(uint8_t *)_codeplug.data(0x03800+0x2a), 1); // CC TX - QCOMPARE((int)*(uint8_t *)_codeplug.data(0x03800+0x2b), 1); // GprListIdx +1 - QCOMPARE((int)*(uint8_t *)_codeplug.data(0x03800+0x2c), 1); // CC RX - QCOMPARE((int)*(uint8_t *)_codeplug.data(0x03800+0x2d), 0); // Emergency Sys 0=None - QCOMPARE((int)*(uint16_t *)_codeplug.data(0x03800+0x2e), 1); // TX Contact idx +1 - QCOMPARE((int)*(uint8_t *)_codeplug.data(0x03800+0x30), 0); // Em ACK + DataCallConf. - QCOMPARE((int)*(uint8_t *)_codeplug.data(0x03800+0x31), 0x00); // PrivateCallConf, Priv., TS2 - QCOMPARE((int)*(uint8_t *)_codeplug.data(0x03800+0x32), 0); // DCDM, ... - QCOMPARE((int)*(uint8_t *)_codeplug.data(0x03800+0x33), 0x80); // Power, ... - QCOMPARE((int)*(uint8_t *)_codeplug.data(0x03800+0x34), 0); // unused 0x00 - QCOMPARE((int)*(uint8_t *)_codeplug.data(0x03800+0x35), 0); // unused 0x00 - QCOMPARE((int)*(uint8_t *)_codeplug.data(0x03800+0x36), 0); // unused 0x00 - QCOMPARE((int)*(uint8_t *)_codeplug.data(0x03800+0x37), 0); // Squelch = 0 -} - - -void -RD5RTest::testAnalogChannels() { - /* - * Test Channel bank 0 - */ - QCOMPARE((int)*((uint8_t *)_codeplug.data(0x03780+0x00)), 0x1f); // First 5 channels enabled - QCOMPARE((int)*((uint8_t *)_codeplug.data(0x03780+0x01)), 0x00); // rest is disabled - - /* - * Test channel 04 - */ - QCOMPARE(decode_ascii(_codeplug.data(0x3838+0x00), 16, 0xff), QString("DB0LDS")); // Check name - QCOMPARE(decode_frequency(*(uint32_t *)_codeplug.data(0x3838+0x10)), 439.5625); // check RX Frequency - QCOMPARE(decode_frequency(*(uint32_t *)_codeplug.data(0x3838+0x14)), 431.9625); // check TX Frequency - QCOMPARE((int)*(uint8_t *)_codeplug.data(0x3838+0x18), 0); // check mode 0 == Analog - QCOMPARE((int)*(uint16_t *)_codeplug.data(0x3838+0x19), 0); // unused 0x00 - QCOMPARE((int)*(uint8_t *)_codeplug.data(0x3838+0x1b), 3); // tot 3 == 45s - QCOMPARE((int)*(uint8_t *)_codeplug.data(0x3838+0x1c), 0); // tot re-key delay 0 - QCOMPARE((int)*(uint8_t *)_codeplug.data(0x3838+0x1d), 1); // admit 1 = free - QCOMPARE((int)*(uint8_t *)_codeplug.data(0x3838+0x1e), 0x50); // unused 0x50 - QCOMPARE((int)*(uint8_t *)_codeplug.data(0x3838+0x1f), 0); // scanlist index +1 (0=none) - QCOMPARE((uint16_t)*(uint16_t *)_codeplug.data(0x3838+0x20), encode_ctcss_tone_table(Signaling::CTCSS_67_0Hz)); // rx ctcss 0xffff = none - QCOMPARE((uint16_t)*(uint16_t *)_codeplug.data(0x3838+0x22), encode_ctcss_tone_table(Signaling::CTCSS_67_0Hz)); // tx ctcss 0xffff = none - QCOMPARE((int)*(uint8_t *)_codeplug.data(0x3838+0x24), 0); // unused 0x00 - QCOMPARE((int)*(uint8_t *)_codeplug.data(0x3838+0x25), 0); // TX sig. 0=off - QCOMPARE((int)*(uint8_t *)_codeplug.data(0x3838+0x26), 0); // unused 0x00 - QCOMPARE((int)*(uint8_t *)_codeplug.data(0x3838+0x27), 0); // RX sig. 0=off - QCOMPARE((int)*(uint8_t *)_codeplug.data(0x3838+0x28), 0x16); // unused 0x16 - QCOMPARE((int)*(uint8_t *)_codeplug.data(0x3838+0x29), 0); // priv group 0=none - QCOMPARE((int)*(uint8_t *)_codeplug.data(0x3838+0x2a), 0); // CC TX - QCOMPARE((int)*(uint8_t *)_codeplug.data(0x3838+0x2b), 0); // GprListIdx +1 - QCOMPARE((int)*(uint8_t *)_codeplug.data(0x3838+0x2c), 0); // CC RX - QCOMPARE((int)*(uint8_t *)_codeplug.data(0x3838+0x2d), 0); // Emergency Sys 0=None - QCOMPARE((int)*(uint16_t *)_codeplug.data(0x3838+0x2e), 0); // TX Contact idx +1 - QCOMPARE((int)*(uint8_t *)_codeplug.data(0x3838+0x30), 0); // Em ACK + DataCallConf. - QCOMPARE((int)*(uint8_t *)_codeplug.data(0x3838+0x31), 0); // PrivateCallConf, Priv., TS2 - QCOMPARE((int)*(uint8_t *)_codeplug.data(0x3838+0x32), 0); // DCDM, ... - QCOMPARE((int)*(uint8_t *)_codeplug.data(0x3838+0x33), 0x80); // Power, ... - QCOMPARE((int)*(uint8_t *)_codeplug.data(0x3838+0x34), 0); // unused 0x00 - QCOMPARE((int)*(uint8_t *)_codeplug.data(0x3838+0x35), 0); // unused 0x00 - QCOMPARE((int)*(uint8_t *)_codeplug.data(0x3838+0x36), 0); // unused 0x00 - QCOMPARE((int)*(uint8_t *)_codeplug.data(0x3838+0x37), 1); // Squelch = 1 - - /* - * Test channel 05 - */ - QCOMPARE(decode_ascii(_codeplug.data(0x3870+0x00), 16, 0xff), QString("DB0SP-2")); // Check name - QCOMPARE(decode_frequency(*(uint32_t *)_codeplug.data(0x3870+0x10)), 145.6); // check RX Frequency - QCOMPARE(decode_frequency(*(uint32_t *)_codeplug.data(0x3870+0x14)), 145.0); // check TX Frequency - QCOMPARE((int)*(uint8_t *)_codeplug.data(0x3870+0x18), 0); // check mode 0 == Analog - QCOMPARE((int)*(uint16_t *)_codeplug.data(0x3870+0x19), 0); // unused 0x00 - QCOMPARE((int)*(uint8_t *)_codeplug.data(0x3870+0x1b), 3); // tot 3 == 45s - QCOMPARE((int)*(uint8_t *)_codeplug.data(0x3870+0x1c), 0); // tot re-key delay 0 - QCOMPARE((int)*(uint8_t *)_codeplug.data(0x3870+0x1d), 1); // admit free - QCOMPARE((int)*(uint8_t *)_codeplug.data(0x3870+0x1e), 0x50); // unused 0x50 - QCOMPARE((int)*(uint8_t *)_codeplug.data(0x3870+0x1f), 0); // scanlist index +1 (0=none) - QCOMPARE((int)*(uint16_t *)_codeplug.data(0x3870+0x20), 0xffff); // rx ctcss 0xffff = none - QCOMPARE((int)*(uint16_t *)_codeplug.data(0x3870+0x22), 0xffff); // tx ctcss 0xffff = none - QCOMPARE((int)*(uint8_t *)_codeplug.data(0x3870+0x24), 0); // unused 0x00 - QCOMPARE((int)*(uint8_t *)_codeplug.data(0x3870+0x25), 0); // TX sig. 0=off - QCOMPARE((int)*(uint8_t *)_codeplug.data(0x3870+0x26), 0); // unused 0x00 - QCOMPARE((int)*(uint8_t *)_codeplug.data(0x3870+0x27), 0); // RX sig. 0=off - QCOMPARE((int)*(uint8_t *)_codeplug.data(0x3870+0x28), 0x16); // unused 0x16 - QCOMPARE((int)*(uint8_t *)_codeplug.data(0x3870+0x29), 0); // priv group 0=none - QCOMPARE((int)*(uint8_t *)_codeplug.data(0x3870+0x2a), 0); // CC TX - QCOMPARE((int)*(uint8_t *)_codeplug.data(0x3870+0x2b), 0); // GprListIdx +1 - QCOMPARE((int)*(uint8_t *)_codeplug.data(0x3870+0x2c), 0); // CC RX - QCOMPARE((int)*(uint8_t *)_codeplug.data(0x3870+0x2d), 0); // Emergency Sys 0=None - QCOMPARE((int)*(uint16_t *)_codeplug.data(0x3870+0x2e), 0); // TX Contact idx +1 - QCOMPARE((int)*(uint8_t *)_codeplug.data(0x3870+0x30), 0); // Em ACK + DataCallConf. - QCOMPARE((int)*(uint8_t *)_codeplug.data(0x3870+0x31), 0); // PrivateCallConf, Priv., TS2 - QCOMPARE((int)*(uint8_t *)_codeplug.data(0x3870+0x32), 0); // DCDM, ... - QCOMPARE((int)*(uint8_t *)_codeplug.data(0x3870+0x33), 0x80); // Power, ... - QCOMPARE((int)*(uint8_t *)_codeplug.data(0x3870+0x34), 0); // unused 0x00 - QCOMPARE((int)*(uint8_t *)_codeplug.data(0x3870+0x35), 0); // unused 0x00 - QCOMPARE((int)*(uint8_t *)_codeplug.data(0x3870+0x36), 0); // unused 0x00 - QCOMPARE((int)*(uint8_t *)_codeplug.data(0x3870+0x37), 1); // Squelch = 0 -} - -void -RD5RTest::testZones() { - /* - * Test zone bank - */ - QCOMPARE((int)*((uint8_t *)_codeplug.data(0x08010+0x00)), 0x03); // First 2 zones enabled - QCOMPARE((int)*((uint8_t *)_codeplug.data(0x08010+0x01)), 0x00); // rest is disabled - - /* - * Test zone 01 - */ - QCOMPARE(decode_ascii(_codeplug.data(0x08030+0x00), 16, 0xff), QString("KW A")); // Check name - QCOMPARE((int)*(uint16_t *)_codeplug.data(0x08030+0x10), 1); // channel 1 - QCOMPARE((int)*(uint16_t *)_codeplug.data(0x08030+0x12), 3); // channel 3 - QCOMPARE((int)*(uint16_t *)_codeplug.data(0x08030+0x14), 5); // channel 5 - QCOMPARE((int)*(uint16_t *)_codeplug.data(0x08030+0x16), 0); // not set - /* - * Test zone 02 - */ - QCOMPARE(decode_ascii(_codeplug.data(0x08060+0x00), 16, 0xff), QString("KW B")); // Check name - QCOMPARE((int)*(uint16_t *)_codeplug.data(0x08060+0x10), 2); // channel 2 - QCOMPARE((int)*(uint16_t *)_codeplug.data(0x08060+0x12), 4); // channel 4 - QCOMPARE((int)*(uint16_t *)_codeplug.data(0x08060+0x14), 0); // not set -} - -void -RD5RTest::testScanLists() { - /* - * Test scan list bank - */ - QCOMPARE((int)*((uint8_t *)_codeplug.data(0x17620+0x00)), 0x00); // No scanlist enabled. - QCOMPARE((int)*((uint8_t *)_codeplug.data(0x17620+0x01)), 0x00); // No scanlist enabled. -} - -void -RD5RTest::testDecode() { - /* Decode config */ - Config decoded; - QVERIFY(_codeplug.decode(&decoded)); - - // Compare basic config - QCOMPARE(decoded.name(), _config.name()); - QCOMPARE(decoded.id(), _config.id()); - QCOMPARE(decoded.introLine1(), _config.introLine1()); - QCOMPARE(decoded.introLine2(), _config.introLine2()); - // Do not compare MIC level, RD5R has not MIC level settings - // Do not compare speech settings, RD5R has no speech synthesis - - // Compare contacts - QCOMPARE(decoded.contacts()->count(), _config.contacts()->count()); - for (int i=0; i<_config.contacts()->count(); i++) { - // Compare name - QCOMPARE(decoded.contacts()->contact(i)->name(), _config.contacts()->contact(i)->name()); - // Compare number - QCOMPARE(decoded.contacts()->contact(i)->as()->number(), - _config.contacts()->contact(i)->as()->number()); - // Compare type - QCOMPARE(decoded.contacts()->contact(i)->as()->type(), - _config.contacts()->contact(i)->as()->type()); - // Compare tone - QCOMPARE(decoded.contacts()->contact(i)->rxTone(), - _config.contacts()->contact(i)->rxTone()); - } - - // Compare RX Groups - QCOMPARE(decoded.rxGroupLists()->count(), _config.rxGroupLists()->count()); - for (int i=0; i<_config.rxGroupLists()->count(); i++) { - // Compare name - QCOMPARE(decoded.rxGroupLists()->list(i)->name(), _config.rxGroupLists()->list(i)->name()); - // Compare number of entries - QCOMPARE(decoded.rxGroupLists()->list(i)->count(), _config.rxGroupLists()->list(i)->count()); - // Compare entries - for (int j=0; j<_config.rxGroupLists()->list(i)->count(); j++) { - QCOMPARE(decoded.contacts()->indexOf(decoded.rxGroupLists()->list(i)->contact(j)), - _config.contacts()->indexOf(_config.rxGroupLists()->list(i)->contact(j))); - } - } - - // Compare Channels - QCOMPARE(decoded.channelList()->count(), _config.channelList()->count()); - for (int i=0; i<_config.channelList()->count(); i++) { - // Compare name - QCOMPARE(decoded.channelList()->channel(i)->name(), _config.channelList()->channel(i)->name()); - // RX Frequency - QCOMPARE(decoded.channelList()->channel(i)->rxFrequency(), _config.channelList()->channel(i)->rxFrequency()); - // TX Frequency - QCOMPARE(decoded.channelList()->channel(i)->txFrequency(), _config.channelList()->channel(i)->txFrequency()); - // Power - QCOMPARE(decoded.channelList()->channel(i)->power(), _config.channelList()->channel(i)->power()); - // TOT - QCOMPARE(decoded.channelList()->channel(i)->txTimeout(), _config.channelList()->channel(i)->txTimeout()); - // RX only flag - QCOMPARE(decoded.channelList()->channel(i)->rxOnly(), _config.channelList()->channel(i)->rxOnly()); - // Scanlist - QCOMPARE(decoded.scanlists()->indexOf(decoded.channelList()->channel(i)->scanList()), - _config.scanlists()->indexOf(_config.channelList()->channel(i)->scanList())); - // Check type - QCOMPARE(decoded.channelList()->channel(i)->is(), - _config.channelList()->channel(i)->is()); - // Dispatch by type - if (_config.channelList()->channel(i)->is()) { - DigitalChannel *dec = decoded.channelList()->channel(i)->as(); - DigitalChannel *ori = _config.channelList()->channel(i)->as(); - // Compare admit criterion - QCOMPARE(dec->admit(), ori->admit()); - // color code - QCOMPARE(dec->colorCode(), ori->colorCode()); - // time slot - QCOMPARE(dec->timeslot(), ori->timeslot()); - // RX group list - QCOMPARE(decoded.rxGroupLists()->indexOf(dec->rxGroupList()), - _config.rxGroupLists()->indexOf(ori->rxGroupList())); - // TX contact - QCOMPARE(decoded.contacts()->indexOf(dec->txContact()), - _config.contacts()->indexOf(ori->txContact())); - // do not check GSP system (RD5R has no GPS). - QCOMPARE(dec->gpsSystem(), nullptr); - } else { - AnalogChannel *dec = decoded.channelList()->channel(i)->as(); - AnalogChannel *ori = _config.channelList()->channel(i)->as(); - // Do not compare admit criterion, RD5R does not support Admit: CTCSS, mapped -> CH FREE - QCOMPARE(dec->admit(), AnalogChannel::AdmitFree); - // squelch - QCOMPARE(dec->squelch(), ori->squelch()); - // RX Tone - QCOMPARE(dec->rxTone(), ori->rxTone()); - // TX Tone - QCOMPARE(dec->txTone(), ori->txTone()); - // Bandwidth - QCOMPARE(dec->bandwidth(), ori->bandwidth()); - } - } - - // Compare Zones. Note Zones with VFO A/B are split into individual zones for RD5R - QCOMPARE(decoded.zones()->count(), 2*_config.zones()->count()); - QCOMPARE(decoded.zones()->zone(0)->name(), _config.zones()->zone(0)->name()+" A"); - QCOMPARE(decoded.zones()->zone(0)->A()->count(), _config.zones()->zone(0)->A()->count()); - for (int i=0; i<_config.zones()->zone(0)->A()->count(); i++) { - QCOMPARE(decoded.channelList()->indexOf(decoded.zones()->zone(0)->A()->channel(i)), - _config.channelList()->indexOf(_config.zones()->zone(0)->A()->channel(i))); - } - QCOMPARE(decoded.zones()->zone(1)->name(), _config.zones()->zone(0)->name()+" B"); - QCOMPARE(decoded.zones()->zone(1)->A()->count(), _config.zones()->zone(0)->B()->count()); - for (int i=0; i<_config.zones()->zone(0)->B()->count(); i++) { - QCOMPARE(decoded.channelList()->indexOf(decoded.zones()->zone(1)->A()->channel(i)), - _config.channelList()->indexOf(_config.zones()->zone(0)->B()->channel(i))); - } - - // Compare scanlist - QCOMPARE(decoded.scanlists()->count(), _config.scanlists()->count()); - - // Do not compare GPS systems (RD5R has no GPS). - QCOMPARE(decoded.gpsSystems()->count(), 0); -} - -QTEST_GUILESS_MAIN(RD5RTest) diff --git a/test/rd5rtest.hh b/test/rd5rtest.hh deleted file mode 100644 index 1018140f..00000000 --- a/test/rd5rtest.hh +++ /dev/null @@ -1,38 +0,0 @@ -#ifndef RD5RTEST_HH -#define RD5RTEST_HH - -#include "config.hh" -#include "rd5r_codeplug.hh" - -#include - - -class RD5RTest : public QObject -{ - Q_OBJECT - -public: - explicit RD5RTest(QObject *parent = nullptr); - -private slots: - void initTestCase(); - void cleanupTestCase(); - - void testDMRID(); - void testRadioName(); - void testIntroLines(); - void testGeneralDefaults(); - void testDigitalContacts(); - void testRXGroups(); - void testDigitalChannels(); - void testAnalogChannels(); - void testZones(); - void testScanLists(); - void testDecode(); - -protected: - Config _config; - RD5RCodeplug _codeplug; -}; - -#endif // RD5RTEST_HH diff --git a/test/resources.qrc b/test/resources.qrc index 5696ad03..3202c009 100644 --- a/test/resources.qrc +++ b/test/resources.qrc @@ -1,5 +1,5 @@ - testconfig.conf + data/config_test.yaml diff --git a/test/testconfig.conf b/test/testconfig.conf deleted file mode 100644 index f6db2d8e..00000000 --- a/test/testconfig.conf +++ /dev/null @@ -1,110 +0,0 @@ -# -# Configuration generated So. Dez. 22 12:56:19 2019 by qdrm, version 0.2.2 -# see https://dm3mat.darc.de/qdmr for details. -# - -# Unique DMR ID and name (quoted) of this radio. -ID: 1234567 -Name: "DM3MAT" - -# Text displayed when the radio powers up (quoted). -IntroLine1: "ABC" -IntroLine2: "DEF" - -# Microphone amplification, value 1..10: -MICLevel: 3 - -# Speech-synthesis ('On' or 'Off'): -Speech: Off - -# Table of digital channels. -# 1) Channel number: 1-1024 -# 2) Name in quotes. E.g., "NAME" -# 3) Receive frequency in MHz -# 4) Transmit frequency or +/- offset in MHz -# 5) Transmit power: High, Low -# 6) Scan list: - or index in Scanlist table -# 7) Transmit timeout timer in seconds: 0, 15, 30, 45... 555 -# 8) Receive only: -, + -# 9) Admit criteria: -, Free, Color -# 10) Color code: 0, 1, 2, 3... 15 -# 11) Time slot: 1 or 2 -# 12) Receive group list: - or index in Grouplist table -# 13) Contact for transmit: - or index in Contacts table -# 14) GPS System: - or index in GPS table. -# -Digital Name Receive Transmit Power Scan TOT RO Admit CC TS RxGL TxC GPS -1 "BB DB0LDS TS2" 439.5630 -7.6000 High - 45 - Color 1 2 1 2 1 # Bln/Brb -2 "BB DM0TT TS2" 439.0870 -7.6000 High - 45 - Color 1 2 1 2 - # Bln/Brb -3 "TG9 DB0KK TS1" 439.5380 -7.6000 High - 45 - Color 1 1 1 1 - # Local - -# Table of analog channels. -# 1) Channel number: 1-1024 -# 2) Name in quotes. -# 3) Receive frequency in MHz -# 4) Transmit frequency or +/- offset in MHz -# 5) Transmit power: High, Low -# 6) Scan list: - or index -# 7) Transmit timeout timer in seconds: 0, 15, 30, 45... 555 -# 8) Receive only: -, + -# 9) Admit criteria: -, Free, Tone -# 10) Squelch level: 0, 1, 2, 3, 4, 5, 6, 7, 8, 9 -# 11) Guard tone for receive, or '-' to disable -# 12) Guard tone for transmit, or '-' to disable -# 13) Bandwidth in kHz: 12.5, 25 -# -Analog Name Receive Transmit Power Scan TOT RO Admit Squelch RxTone TxTone Width -4 "DB0LDS" 439.5625 -7.6000 High - 45 - Tone 1 67 67 12.5 -5 "DB0SP-2" 145.6000 -0.6000 High - 45 - Free 1 - - 12.5 - -# Table of channel zones. -# 1) Zone number -# 2) Name in quotes. -# 3) VFO: Either A or B. -# 4) List of channels: numbers and ranges (N-M) separated by comma -# -Zone Name VFO Channels -1 "KW" A 1,3,5 -1 "KW" B 2,4 - -# Table of scan lists. -# 1) Scan list number: 1-250 -# 2) Name in quotes. -# 3) Priority channel 1 (50% of scans): -, Sel or index -# 4) Priority channel 2 (25% of scans): -, Sel or index -# 5) Designated transmit channel: Last, Sel or index -# 6) List of channels: numbers and ranges (N-M) separated by comma -# -Scanlist Name PCh1 PCh2 TxCh Channels - -# Table of GPS systems. -# 1) GPS system ID -# 2) Name in quotes. -# 3) Destination contact ID. -# 4) Update period: period in ms -# 5) Revert channel ID or '-'. -# -GPS Name Dest Period Revert -1 "BM APRS" 4 300 - - - # Table of contacts. -# 1) Contact number: 1-256 -# 2) Name in quotes. -# 3) Call type: Group, Private, All or DTMF -# 4) Call ID: 1...16777215 or string with DTMF number -# 5) Call receive tone: -, + -# -Contact Name Type ID RxTone -1 "Local" Group 9 - -2 "Bln/Brb" Group 2621 - -3 "All Call" All 16777215 - -4 "BM APRS" Private 262999 - - -# Table of group lists. -# 1) Group list number: 1-64 -# 2) Name in quotes. -# 3) List of contacts: numbers and ranges (N-M) separated by comma -# -Grouplist Name Contacts -1 "Berlin/Brand" 1,2 - diff --git a/test/uv390test.cc b/test/uv390test.cc deleted file mode 100644 index 9b0e96f6..00000000 --- a/test/uv390test.cc +++ /dev/null @@ -1,524 +0,0 @@ -#include "uv390test.hh" -#include "config.hh" -#include -#include "utils.hh" -#include - -UV390Test::UV390Test(QObject *parent) : QObject(parent) -{ - // pass... -} - -void -UV390Test::initTestCase() { - // Read simple configuration file - QString errMessage; - QVERIFY(_config.readCSV("://testconfig.conf", errMessage)); - // Encode config as code-plug - QVERIFY(_codeplug.encode(&_config)); -} - -void -UV390Test::cleanupTestCase() { - // clear codeplug - _codeplug.clear(); -} - -void -UV390Test::testRadioName() { - QCOMPARE(decode_unicode((uint16_t *)_codeplug.data(0x002840+0x70), 16, 0), QString("DM3MAT")); -} - -void -UV390Test::testDMRID() { - QCOMPARE(decode_dmr_id_bin(_codeplug.data(0x002840+0x44)), 1234567U); -} - -void -UV390Test::testIntroLines() { - QCOMPARE(decode_unicode((uint16_t *)_codeplug.data(0x002840+0x00), 10, 0), QString("ABC")); - QCOMPARE(decode_unicode((uint16_t *)_codeplug.data(0x002840+0x14), 10, 0), QString("DEF")); -} - -void -UV390Test::testGeneralDefaults() { - // Check voice announce (off) - QCOMPARE((int)*(uint8_t *)_codeplug.data(0x002840+0x42), 0xd8); - // Check MIC amplification: 3 in [1..10] => 1 in [0..5] - QCOMPARE((int)*(uint8_t *)_codeplug.data(0x002840+0xa0), 0x8f); -} - -void -UV390Test::testDigitalContacts() { - /* - * Test contact 01 (L9) - */ - // Test ID - QCOMPARE(decode_dmr_id_bin(_codeplug.data(0x140800+0x00)), 9U); - // Type group call, rx tone off - QCOMPARE((int)*(uint8_t *)_codeplug.data(0x140800+0x03), 0xc1); - // Compare name - QCOMPARE(decode_unicode((uint16_t *)_codeplug.data(0x140800+0x04), 16, 0), QString("Local")); - - /* - * Test contact 02 (BB) - */ - // Test ID - QCOMPARE(decode_dmr_id_bin(_codeplug.data(0x140824+0x00)), 2621U); - // Type group call, rx tone off - QCOMPARE((int)*(uint8_t *)_codeplug.data(0x140824+0x03), 0xc1); - // Compare name - QCOMPARE(decode_unicode((uint16_t *)_codeplug.data(0x140824+0x04), 16, 0), QString("Bln/Brb")); - - /* - * Test contact 03 (ALL) - */ - // Test ID - QCOMPARE(decode_dmr_id_bin(_codeplug.data(0x140848+0x00)), 16777215U); - // Type all call, rx tone off - QCOMPARE((int)*(uint8_t *)_codeplug.data(0x140848+0x03), 0xc3); - // Compare name - QCOMPARE(decode_unicode((uint16_t *)_codeplug.data(0x140848+0x04), 16, 0), QString("All Call")); - - /* - * Test contact 04 (APRS) - */ - // Test ID - QCOMPARE(decode_dmr_id_bin(_codeplug.data(0x14086c+0x00)), 262999U); - // Type private call, rx tone off - QCOMPARE((int)*(uint8_t *)_codeplug.data(0x14086c+0x03), 0xc2); - // Compare name - QCOMPARE(decode_unicode((uint16_t *)_codeplug.data(0x14086c+0x04), 16, 0), QString("BM APRS")); -} - -void -UV390Test::testRXGroups() { - // Check name - QCOMPARE(decode_unicode((uint16_t *)_codeplug.data(0x00f420+0x00), 16, 0), QString("Berlin/Brand")); - // Check members - QCOMPARE((int)*(uint16_t *)_codeplug.data(0x00f420+0x20), 1); - QCOMPARE((int)*(uint16_t *)_codeplug.data(0x00f420+0x22), 2); - QCOMPARE((int)*(uint16_t *)_codeplug.data(0x00f420+0x24), 0); -} - -void -UV390Test::testDigitalChannels() { - /* - * Test Channel 01 - */ - // Mode digi, BW 12.5kHz, Autoscan off, lone worker off - QCOMPARE((int)*(uint8_t *)_codeplug.data(0x110800+0x00), 0x62); - // Talkaround off, RX only off, TS 2, CC 1 - QCOMPARE((int)*(uint8_t *)_codeplug.data(0x110800+0x01), 0x19); - // Priv #0, priv none, prv. call conf. off, data call conf. off - QCOMPARE((int)*(uint8_t *)_codeplug.data(0x110800+0x02), 0x00); - // rx ref freq low, emrg. ack off, PTT id off (inv) - QCOMPARE((int)*(uint8_t *)_codeplug.data(0x110800+0x03), 0xe0); - // tx ref freq low, VOX off, admit none - QCOMPARE((int)*(uint8_t *)_codeplug.data(0x110800+0x04), 0xe4); - // in-call always, turn-off freq off. - QCOMPARE((int)*(uint8_t *)_codeplug.data(0x110800+0x05), 0xc0); - // TX contact 2 - QCOMPARE((int)*(uint16_t *)_codeplug.data(0x110800+0x06), 0x02); - // TOT 45s <-> 0x03 - QCOMPARE((int)*(uint8_t *)_codeplug.data(0x110800+0x08), 0x03); - // TOT re-key delay 0 - QCOMPARE((int)*(uint8_t *)_codeplug.data(0x110800+0x09), 0x00); - // Emerg. sys none - QCOMPARE((int)*(uint8_t *)_codeplug.data(0x110800+0x0a), 0x00); - // Scanlist none - QCOMPARE((int)*(uint8_t *)_codeplug.data(0x110800+0x0b), 0x00); - // RX group list 1 - QCOMPARE((int)*(uint8_t *)_codeplug.data(0x110800+0x0c), 0x01); - // GPS system 1 - QCOMPARE((int)*(uint8_t *)_codeplug.data(0x110800+0x0d), 0x01); - // DTMF decode none - QCOMPARE((int)*(uint8_t *)_codeplug.data(0x110800+0x0e), 0x00); - // Squelch none - QCOMPARE((int)*(uint8_t *)_codeplug.data(0x110800+0x0f), 0x00); - // RX frequency - QCOMPARE(decode_frequency(*(uint32_t *)_codeplug.data(0x110800+0x10)), 439.5630); - // TX frequency - QCOMPARE(decode_frequency(*(uint32_t *)_codeplug.data(0x110800+0x14)), 431.9630); - // RX CTCSS none - QCOMPARE(decode_ctcss_tone_table(*(uint16_t *)_codeplug.data(0x110800+0x18)), Signaling::SIGNALING_NONE); - // TX CTCSS none - QCOMPARE(decode_ctcss_tone_table(*(uint16_t *)_codeplug.data(0x110800+0x1a)), Signaling::SIGNALING_NONE); - // RX DTMF sig. off - QCOMPARE((int)*(uint8_t *)_codeplug.data(0x110800+0x1c), 0x00); - // TX DTMF sig. off - QCOMPARE((int)*(uint8_t *)_codeplug.data(0x110800+0x1d), 0x00); - // Power high - QCOMPARE((int)*(uint8_t *)_codeplug.data(0x110800+0x1e), 0xff); - // GPS on (inv), allow interrupt off (inv), DCDM off (inv), Leader/MS off (inv) - QCOMPARE((int)*(uint8_t *)_codeplug.data(0x110800+0x1f), 0xfc); - // Name - QCOMPARE(decode_unicode((uint16_t *)_codeplug.data(0x110800+0x20), 16, 0), QString("BB DB0LDS TS2")); - - /* - * Test Channel 02 - */ - // Mode digi, BW 12.5kHz, Autoscan off, lone worker off - QCOMPARE((int)*(uint8_t *)_codeplug.data(0x110840+0x00), 0x62); - // Talkaround off, RX only off, TS 2, CC 1 - QCOMPARE((int)*(uint8_t *)_codeplug.data(0x110840+0x01), 0x19); - // Priv #0, priv none, prv. call conf. off, data call conf. off - QCOMPARE((int)*(uint8_t *)_codeplug.data(0x110840+0x02), 0x00); - // rx ref freq low, emrg. ack off, PTT id off (inv) - QCOMPARE((int)*(uint8_t *)_codeplug.data(0x110840+0x03), 0xe0); - // tx ref freq low, VOX off, admit none - QCOMPARE((int)*(uint8_t *)_codeplug.data(0x110840+0x04), 0xe4); - // in-call always, turn-off freq off. - QCOMPARE((int)*(uint8_t *)_codeplug.data(0x110840+0x05), 0xc0); - // TX contact 2 - QCOMPARE((int)*(uint16_t *)_codeplug.data(0x110840+0x06), 0x02); - // TOT 45s <-> 0x03 - QCOMPARE((int)*(uint8_t *)_codeplug.data(0x110840+0x08), 0x03); - // TOT re-key delay 0 - QCOMPARE((int)*(uint8_t *)_codeplug.data(0x110840+0x09), 0x00); - // Emerg. sys none - QCOMPARE((int)*(uint8_t *)_codeplug.data(0x110840+0x0a), 0x00); - // Scanlist none - QCOMPARE((int)*(uint8_t *)_codeplug.data(0x110840+0x0b), 0x00); - // RX group list 1 - QCOMPARE((int)*(uint8_t *)_codeplug.data(0x110840+0x0c), 0x01); - // GPS system 0 - QCOMPARE((int)*(uint8_t *)_codeplug.data(0x110840+0x0d), 0x00); - // DTMF decode none - QCOMPARE((int)*(uint8_t *)_codeplug.data(0x110840+0x0e), 0x00); - // Squelch none - QCOMPARE((int)*(uint8_t *)_codeplug.data(0x110840+0x0f), 0x00); - // RX frequency - QCOMPARE(decode_frequency(*(uint32_t *)_codeplug.data(0x110840+0x10)), 439.0870); - // TX frequency - QCOMPARE(decode_frequency(*(uint32_t *)_codeplug.data(0x110840+0x14)), 431.4870); - // RX CTCSS none - QCOMPARE(decode_ctcss_tone_table(*(uint16_t *)_codeplug.data(0x110840+0x18)), Signaling::SIGNALING_NONE); - // TX CTCSS none - QCOMPARE(decode_ctcss_tone_table(*(uint16_t *)_codeplug.data(0x110840+0x1a)), Signaling::SIGNALING_NONE); - // RX DTMF sig. off - QCOMPARE((int)*(uint8_t *)_codeplug.data(0x110840+0x1c), 0x00); - // TX DTMF sig. off - QCOMPARE((int)*(uint8_t *)_codeplug.data(0x110840+0x1d), 0x00); - // Power high - QCOMPARE((int)*(uint8_t *)_codeplug.data(0x110840+0x1e), 0xff); - // GPS off (inv), allow interrupt off (inv), DCDM off (inv), Leader/MS off (inv) - QCOMPARE((int)*(uint8_t *)_codeplug.data(0x110840+0x1f), 0xff); - // Name - QCOMPARE(decode_unicode((uint16_t *)_codeplug.data(0x110840+0x20), 16, 0), QString("BB DM0TT TS2")); - - /* - * Test Channel 03 - */ - // Mode digi, BW 12.5kHz, Autoscan off, lone worker off - QCOMPARE((int)*(uint8_t *)_codeplug.data(0x110880+0x00), 0x62); - // Talkaround off, RX only off, TS 1, CC 1 - QCOMPARE((int)*(uint8_t *)_codeplug.data(0x110880+0x01), 0x15); - // Priv #0, priv none, prv. call conf. off, data call conf. off - QCOMPARE((int)*(uint8_t *)_codeplug.data(0x110880+0x02), 0x00); - // rx ref freq low, emrg. ack off, PTT id off (inv) - QCOMPARE((int)*(uint8_t *)_codeplug.data(0x110880+0x03), 0xe0); - // tx ref freq low, VOX off, admit none - QCOMPARE((int)*(uint8_t *)_codeplug.data(0x110880+0x04), 0xe4); - // in-call always, turn-off freq off. - QCOMPARE((int)*(uint8_t *)_codeplug.data(0x110880+0x05), 0xc0); - // TX contact 2 - QCOMPARE((int)*(uint16_t *)_codeplug.data(0x110880+0x06), 0x01); - // TOT 45s <-> 0x03 - QCOMPARE((int)*(uint8_t *)_codeplug.data(0x110880+0x08), 0x03); - // TOT re-key delay 0 - QCOMPARE((int)*(uint8_t *)_codeplug.data(0x110880+0x09), 0x00); - // Emerg. sys none - QCOMPARE((int)*(uint8_t *)_codeplug.data(0x110880+0x0a), 0x00); - // Scanlist none - QCOMPARE((int)*(uint8_t *)_codeplug.data(0x110880+0x0b), 0x00); - // RX group list 1 - QCOMPARE((int)*(uint8_t *)_codeplug.data(0x110880+0x0c), 0x01); - // GPS system 0 - QCOMPARE((int)*(uint8_t *)_codeplug.data(0x110880+0x0d), 0x00); - // DTMF decode none - QCOMPARE((int)*(uint8_t *)_codeplug.data(0x110880+0x0e), 0x00); - // Squelch none - QCOMPARE((int)*(uint8_t *)_codeplug.data(0x110880+0x0f), 0x00); - // RX frequency - QCOMPARE(decode_frequency(*(uint32_t *)_codeplug.data(0x110880+0x10)), 439.5380); - // TX frequency - QCOMPARE(decode_frequency(*(uint32_t *)_codeplug.data(0x110880+0x14)), 431.9380); - // RX CTCSS none - QCOMPARE(decode_ctcss_tone_table(*(uint16_t *)_codeplug.data(0x110880+0x18)), Signaling::SIGNALING_NONE); - // TX CTCSS none - QCOMPARE(decode_ctcss_tone_table(*(uint16_t *)_codeplug.data(0x110880+0x1a)), Signaling::SIGNALING_NONE); - // RX DTMF sig. off - QCOMPARE((int)*(uint8_t *)_codeplug.data(0x110880+0x1c), 0x00); - // TX DTMF sig. off - QCOMPARE((int)*(uint8_t *)_codeplug.data(0x110880+0x1d), 0x00); - // Power high - QCOMPARE((int)*(uint8_t *)_codeplug.data(0x110880+0x1e), 0xff); - // GPS off (inv), allow interrupt off (inv), DCDM off (inv), Leader/MS off (inv) - QCOMPARE((int)*(uint8_t *)_codeplug.data(0x110880+0x1f), 0xff); - // Name - QCOMPARE(decode_unicode((uint16_t *)_codeplug.data(0x110880+0x20), 16, 0), QString("TG9 DB0KK TS1")); -} - - -void -UV390Test::testAnalogChannels() { - /* - * Test Channel 04 - */ - // Mode analog, BW 12.5kHz, Autoscan off, lone worker off - QCOMPARE((int)*(uint8_t *)_codeplug.data(0x1108c0+0x00), 0x61); - // Talkaround off, RX only off, TS 1, CC 1 - QCOMPARE((int)*(uint8_t *)_codeplug.data(0x1108c0+0x01), 0x15); - // Priv #0, priv none, prv. call conf. off, data call conf. off - QCOMPARE((int)*(uint8_t *)_codeplug.data(0x1108c0+0x02), 0x00); - // rx ref freq low, emrg. ack off, PTT id off (inv) - QCOMPARE((int)*(uint8_t *)_codeplug.data(0x1108c0+0x03), 0xe0); - // tx ref freq low, VOX off, admit CH admit tone - QCOMPARE((int)*(uint8_t *)_codeplug.data(0x1108c0+0x04), 0xa4); - // in-call always, turn-off freq off. - QCOMPARE((int)*(uint8_t *)_codeplug.data(0x1108c0+0x05), 0xc0); - // TX contact none - QCOMPARE((int)*(uint16_t *)_codeplug.data(0x1108c0+0x06), 0x00); - // TOT 45s <-> 0x03 - QCOMPARE((int)*(uint8_t *)_codeplug.data(0x1108c0+0x08), 0x03); - // TOT re-key delay 0 - QCOMPARE((int)*(uint8_t *)_codeplug.data(0x1108c0+0x09), 0x00); - // Emerg. sys none - QCOMPARE((int)*(uint8_t *)_codeplug.data(0x1108c0+0x0a), 0x00); - // Scanlist none - QCOMPARE((int)*(uint8_t *)_codeplug.data(0x1108c0+0x0b), 0x00); - // RX group list 0 - QCOMPARE((int)*(uint8_t *)_codeplug.data(0x1108c0+0x0c), 0x00); - // GPS system none - QCOMPARE((int)*(uint8_t *)_codeplug.data(0x1108c0+0x0d), 0x00); - // DTMF decode none - QCOMPARE((int)*(uint8_t *)_codeplug.data(0x1108c0+0x0e), 0x00); - // Squelch 1 - QCOMPARE((int)*(uint8_t *)_codeplug.data(0x1108c0+0x0f), 0x01); - // RX frequency - QCOMPARE(decode_frequency(*(uint32_t *)_codeplug.data(0x1108c0+0x10)), 439.5625); - // TX frequency - QCOMPARE(decode_frequency(*(uint32_t *)_codeplug.data(0x1108c0+0x14)), 431.9625); - // RX CTCSS none - QCOMPARE(decode_ctcss_tone_table(*(uint16_t *)_codeplug.data(0x1108c0+0x18)), Signaling::CTCSS_67_0Hz); - // TX CTCSS none - QCOMPARE(decode_ctcss_tone_table(*(uint16_t *)_codeplug.data(0x1108c0+0x1a)), Signaling::CTCSS_67_0Hz); - // RX DTMF sig. off - QCOMPARE((int)*(uint8_t *)_codeplug.data(0x1108c0+0x1c), 0x00); - // TX DTMF sig. off - QCOMPARE((int)*(uint8_t *)_codeplug.data(0x1108c0+0x1d), 0x00); - // Power high - QCOMPARE((int)*(uint8_t *)_codeplug.data(0x1108c0+0x1e), 0xff); - // GPS off (inv), allow interrupt off (inv), DCDM off (inv), Leader/MS off (inv) - QCOMPARE((int)*(uint8_t *)_codeplug.data(0x1108c0+0x1f), 0xff); - // Name - QCOMPARE(decode_unicode((uint16_t *)_codeplug.data(0x1108c0+0x20), 16, 0), QString("DB0LDS")); - - /* - * Test Channel 05 - */ - // Mode analog, BW 12.5kHz, Autoscan off, lone worker off - QCOMPARE((int)*(uint8_t *)_codeplug.data(0x110900+0x00), 0x61); - // Talkaround off, RX only off, TS 1, CC 1 - QCOMPARE((int)*(uint8_t *)_codeplug.data(0x110900+0x01), 0x15); - // Priv #0, priv none, prv. call conf. off, data call conf. off - QCOMPARE((int)*(uint8_t *)_codeplug.data(0x110900+0x02), 0x00); - // rx ref freq low, emrg. ack off, PTT id off (inv) - QCOMPARE((int)*(uint8_t *)_codeplug.data(0x110900+0x03), 0xe0); - // tx ref freq low, VOX off, admit CH free - QCOMPARE((int)*(uint8_t *)_codeplug.data(0x110900+0x04), 0x64); - // in-call always, turn-off freq off. - QCOMPARE((int)*(uint8_t *)_codeplug.data(0x110900+0x05), 0xc0); - // TX contact none - QCOMPARE((int)*(uint16_t *)_codeplug.data(0x110900+0x06), 0x00); - // TOT 45s <-> 0x03 - QCOMPARE((int)*(uint8_t *)_codeplug.data(0x110900+0x08), 0x03); - // TOT re-key delay 0 - QCOMPARE((int)*(uint8_t *)_codeplug.data(0x110900+0x09), 0x00); - // Emerg. sys none - QCOMPARE((int)*(uint8_t *)_codeplug.data(0x110900+0x0a), 0x00); - // Scanlist none - QCOMPARE((int)*(uint8_t *)_codeplug.data(0x110900+0x0b), 0x00); - // RX group list none - QCOMPARE((int)*(uint8_t *)_codeplug.data(0x110900+0x0c), 0x00); - // GPS system none - QCOMPARE((int)*(uint8_t *)_codeplug.data(0x110900+0x0d), 0x00); - // DTMF decode none - QCOMPARE((int)*(uint8_t *)_codeplug.data(0x110900+0x0e), 0x00); - // Squelch 1 - QCOMPARE((int)*(uint8_t *)_codeplug.data(0x110900+0x0f), 0x01); - // RX frequency - QCOMPARE(decode_frequency(*(uint32_t *)_codeplug.data(0x110900+0x10)), 145.6000); - // TX frequency - QCOMPARE(decode_frequency(*(uint32_t *)_codeplug.data(0x110900+0x14)), 145.0000); - // RX CTCSS none - QCOMPARE(decode_ctcss_tone_table(*(uint16_t *)_codeplug.data(0x110900+0x18)), Signaling::SIGNALING_NONE); - // TX CTCSS none - QCOMPARE(decode_ctcss_tone_table(*(uint16_t *)_codeplug.data(0x110900+0x1a)), Signaling::SIGNALING_NONE); - // RX DTMF sig. off - QCOMPARE((int)*(uint8_t *)_codeplug.data(0x110900+0x1c), 0x00); - // TX DTMF sig. off - QCOMPARE((int)*(uint8_t *)_codeplug.data(0x110900+0x1d), 0x00); - // Power high - QCOMPARE((int)*(uint8_t *)_codeplug.data(0x110900+0x1e), 0xff); - // GPS off (inv), allow interrupt off (inv), DCDM off (inv), Leader/MS off (inv) - QCOMPARE((int)*(uint8_t *)_codeplug.data(0x110900+0x1f), 0xff); - // Name - QCOMPARE(decode_unicode((uint16_t *)_codeplug.data(0x110900+0x20), 16, 0), QString("DB0SP-2")); -} - -void -UV390Test::testZones() { - // Check name - QCOMPARE(decode_unicode((uint16_t *)_codeplug.data(0x0151e0+0x00), 16, 0), QString("KW")); - // Check members A - QCOMPARE((int)*(uint16_t *)_codeplug.data(0x0151e0+0x20), 1); - QCOMPARE((int)*(uint16_t *)_codeplug.data(0x0151e0+0x22), 3); - QCOMPARE((int)*(uint16_t *)_codeplug.data(0x0151e0+0x24), 5); - QCOMPARE((int)*(uint16_t *)_codeplug.data(0x0151e0+0x26), 0); - // Check members B - QCOMPARE((int)*(uint16_t *)_codeplug.data(0x031800+0x60), 2); - QCOMPARE((int)*(uint16_t *)_codeplug.data(0x031800+0x62), 4); -} - -void -UV390Test::testScanLists() { - // There is none -> check if first scan list is invalid - QCOMPARE((int)*(uint16_t *)_codeplug.data(0x019060+0x00), 0x00); -} - -void -UV390Test::testDecode() { - /* Decode config */ - Config decoded; - QVERIFY(_codeplug.decode(&decoded)); - - // Compare basic config - QCOMPARE(decoded.name(), _config.name()); - QCOMPARE(decoded.id(), _config.id()); - QCOMPARE(decoded.introLine1(), _config.introLine1()); - QCOMPARE(decoded.introLine2(), _config.introLine2()); - QCOMPARE(decoded.micLevel(), _config.micLevel()); - QCOMPARE(decoded.speech(), _config.speech()); - - // Compare contacts - QCOMPARE(decoded.contacts()->count(), _config.contacts()->count()); - for (int i=0; i<_config.contacts()->count(); i++) { - // Compare name - QCOMPARE(decoded.contacts()->contact(i)->name(), _config.contacts()->contact(i)->name()); - // Compare number - QCOMPARE(decoded.contacts()->contact(i)->as()->number(), - _config.contacts()->contact(i)->as()->number()); - // Compare type - QCOMPARE(decoded.contacts()->contact(i)->as()->type(), - _config.contacts()->contact(i)->as()->type()); - // Compare tone - QCOMPARE(decoded.contacts()->contact(i)->rxTone(), - _config.contacts()->contact(i)->rxTone()); - } - - // Compare RX Groups - QCOMPARE(decoded.rxGroupLists()->count(), _config.rxGroupLists()->count()); - for (int i=0; i<_config.rxGroupLists()->count(); i++) { - // Compare name - QCOMPARE(decoded.rxGroupLists()->list(i)->name(), _config.rxGroupLists()->list(i)->name()); - // Compare number of entries - QCOMPARE(decoded.rxGroupLists()->list(i)->count(), _config.rxGroupLists()->list(i)->count()); - // Compare entries - for (int j=0; j<_config.rxGroupLists()->list(i)->count(); j++) { - QCOMPARE(decoded.contacts()->indexOf(decoded.rxGroupLists()->list(i)->contact(j)), - _config.contacts()->indexOf(_config.rxGroupLists()->list(i)->contact(j))); - } - } - - // Compare Channels - QCOMPARE(decoded.channelList()->count(), _config.channelList()->count()); - for (int i=0; i<_config.channelList()->count(); i++) { - // Compare name - QCOMPARE(decoded.channelList()->channel(i)->name(), _config.channelList()->channel(i)->name()); - // RX Frequency - QCOMPARE(decoded.channelList()->channel(i)->rxFrequency(), _config.channelList()->channel(i)->rxFrequency()); - // TX Frequency - QCOMPARE(decoded.channelList()->channel(i)->txFrequency(), _config.channelList()->channel(i)->txFrequency()); - // Power - QCOMPARE(decoded.channelList()->channel(i)->power(), _config.channelList()->channel(i)->power()); - // TOT - QCOMPARE(decoded.channelList()->channel(i)->txTimeout(), _config.channelList()->channel(i)->txTimeout()); - // RX only flag - QCOMPARE(decoded.channelList()->channel(i)->rxOnly(), _config.channelList()->channel(i)->rxOnly()); - // Scanlist - QCOMPARE(decoded.scanlists()->indexOf(decoded.channelList()->channel(i)->scanList()), - _config.scanlists()->indexOf(_config.channelList()->channel(i)->scanList())); - // Check type - QCOMPARE(decoded.channelList()->channel(i)->is(), - _config.channelList()->channel(i)->is()); - // Dispatch by type - if (_config.channelList()->channel(i)->is()) { - DigitalChannel *dec = decoded.channelList()->channel(i)->as(); - DigitalChannel *ori = _config.channelList()->channel(i)->as(); - // Compare admit criterion - QCOMPARE(dec->admit(), ori->admit()); - // color code - QCOMPARE(dec->colorCode(), ori->colorCode()); - // time slot - QCOMPARE(dec->timeslot(), ori->timeslot()); - // RX group list - QCOMPARE(decoded.rxGroupLists()->indexOf(dec->rxGroupList()), - _config.rxGroupLists()->indexOf(ori->rxGroupList())); - // TX contact - QCOMPARE(decoded.contacts()->indexOf(dec->txContact()), - _config.contacts()->indexOf(ori->txContact())); - // do not check GSP system (RD5R has no GPS). - QCOMPARE(nullptr != dec->gpsSystem(), nullptr != ori->gpsSystem()); - QCOMPARE(decoded.gpsSystems()->indexOf(dec->gpsSystem()), - _config.gpsSystems()->indexOf(ori->gpsSystem())); - } else { - AnalogChannel *dec = decoded.channelList()->channel(i)->as(); - AnalogChannel *ori = _config.channelList()->channel(i)->as(); - // Compare admit criterion - QCOMPARE(dec->admit(), ori->admit()); - // squelch - QCOMPARE(dec->squelch(), ori->squelch()); - // RX Tone - QCOMPARE(dec->rxTone(), ori->rxTone()); - // TX Tone - QCOMPARE(dec->txTone(), ori->txTone()); - // Bandwidth - QCOMPARE(dec->bandwidth(), ori->bandwidth()); - } - } - - // Compare Zones. - QCOMPARE(decoded.zones()->count(), _config.zones()->count()); - for (int i=0; i<_config.zones()->count(); i++) { - QCOMPARE(decoded.zones()->zone(i)->name(), _config.zones()->zone(i)->name()); - QCOMPARE(decoded.zones()->zone(i)->A()->count(), _config.zones()->zone(i)->A()->count()); - for (int j=0; j<_config.zones()->zone(0)->A()->count(); j++) { - QCOMPARE(decoded.channelList()->indexOf(decoded.zones()->zone(i)->A()->channel(j)), - _config.channelList()->indexOf(_config.zones()->zone(i)->A()->channel(j))); - } - QCOMPARE(decoded.zones()->zone(i)->B()->count(), _config.zones()->zone(i)->B()->count()); - for (int j=0; j<_config.zones()->zone(0)->B()->count(); j++) { - QCOMPARE(decoded.channelList()->indexOf(decoded.zones()->zone(i)->B()->channel(j)), - _config.channelList()->indexOf(_config.zones()->zone(i)->B()->channel(j))); - } - } - - // Compare scanlist - QCOMPARE(decoded.scanlists()->count(), _config.scanlists()->count()); - - // Compare GPS systems - QCOMPARE(decoded.gpsSystems()->count(), _config.gpsSystems()->count()); - for (int i=0; i<_config.gpsSystems()->count(); i++) { - // Do not compare GPS system name UV390 cannot save GPS system name. - QCOMPARE(decoded.gpsSystems()->gpsSystem(i)->name(), QString("GPS System")); - QCOMPARE(decoded.gpsSystems()->gpsSystem(i)->period(), _config.gpsSystems()->gpsSystem(i)->period()); - QCOMPARE(decoded.contacts()->indexOf(decoded.gpsSystems()->gpsSystem(i)->contact()), - _config.contacts()->indexOf(_config.gpsSystems()->gpsSystem(i)->contact())); - QCOMPARE(decoded.channelList()->indexOf(decoded.gpsSystems()->gpsSystem(i)->revertChannel()), - _config.channelList()->indexOf(_config.gpsSystems()->gpsSystem(i)->revertChannel())); - } -} - -QTEST_GUILESS_MAIN(UV390Test) diff --git a/test/uv390test.hh b/test/uv390test.hh deleted file mode 100644 index 74f2882d..00000000 --- a/test/uv390test.hh +++ /dev/null @@ -1,38 +0,0 @@ -#ifndef UV390TEST_HH -#define UV390TEST_HH - -#include "config.hh" -#include "uv390_codeplug.hh" - -#include - - -class UV390Test : public QObject -{ - Q_OBJECT - -public: - explicit UV390Test(QObject *parent = nullptr); - -private slots: - void initTestCase(); - void cleanupTestCase(); - - void testDMRID(); - void testRadioName(); - void testIntroLines(); - void testGeneralDefaults(); - void testDigitalContacts(); - void testRXGroups(); - void testDigitalChannels(); - void testAnalogChannels(); - void testZones(); - void testScanLists(); - void testDecode(); - -protected: - Config _config; - UV390Codeplug _codeplug; -}; - -#endif // UV390TEST_HH From 55c80cbc821cdccb59bbadce4238d95688776999 Mon Sep 17 00:00:00 2001 From: Hannes Matuschek Date: Mon, 2 Jan 2023 20:34:16 +0100 Subject: [PATCH 12/16] Fixed compiler warnings. --- src/configitemwrapper.cc | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/configitemwrapper.cc b/src/configitemwrapper.cc index 91330136..fdc8739e 100644 --- a/src/configitemwrapper.cc +++ b/src/configitemwrapper.cc @@ -528,6 +528,7 @@ ContactListWrapper::data(const QModelIndex &index, int role) const { case DMRContact::GroupCall: return tr("Group Call"); case DMRContact::AllCall: return tr("All Call"); } + break; case 1: return digi->name(); case 2: @@ -628,6 +629,7 @@ PositioningSystemListWrapper::data(const QModelIndex &index, int role) const { } else if (sys->is()) return tr("%1-%2").arg(sys->as()->destination()) .arg(sys->as()->destSSID()); + break; case 3: return sys->period(); case 4: @@ -638,6 +640,7 @@ PositioningSystemListWrapper::data(const QModelIndex &index, int role) const { } else if (sys->is()) return ((nullptr != sys->as()->revertChannel()) ? sys->as()->revertChannel()->name() : tr("OOPS!")); + break; case 5: if (sys->is()) return tr("[None]"); From d6523179ebff3cedf33167f5684b3df4e65836f6 Mon Sep 17 00:00:00 2001 From: Hannes Matuschek Date: Tue, 3 Jan 2023 08:46:49 +0100 Subject: [PATCH 13/16] Fixed unit test. --- lib/config.cc | 9 ++++++++- lib/configobject.cc | 18 +++++++++++++++++- lib/configobject.hh | 2 ++ src/configitemwrapper.cc | 1 + test/configtest.cc | 10 ++++++++-- 5 files changed, 36 insertions(+), 4 deletions(-) diff --git a/lib/config.cc b/lib/config.cc index b66d1162..dcea2b5e 100644 --- a/lib/config.cc +++ b/lib/config.cc @@ -310,7 +310,14 @@ bool Config::readYAML(const QString &filename, const ErrorStack &err) { YAML::Node node; try { - node = YAML::LoadFile(filename.toStdString()); + QFile file(filename); + if (! file.open(QIODevice::ReadOnly)) { + errMsg(err) << "Cannot open file '" << filename << "': " << file.errorString() << "."; + errMsg(err) << "Cannot read YAML codeplug from file '" << filename << "'."; + return false; + } + QByteArray content = file.readAll(); + node = YAML::Load(content.constData()); } catch (const YAML::Exception &exc) { errMsg(err) << "Cannot read YAML codeplug from file '"<< filename << "': " << QString::fromStdString(exc.msg) << "."; diff --git a/lib/configobject.cc b/lib/configobject.cc index ffb901c0..a6d526a9 100644 --- a/lib/configobject.cc +++ b/lib/configobject.cc @@ -265,21 +265,25 @@ ConfigItem::compare(const ConfigItem &other) const { if (QVariant::String == prop.type()) { int cmp = QString::compare(prop.read(this).toString(), oprop.read(&other).toString()); if (cmp) return cmp; + continue; } if (ConfigObjectReference *ref = prop.read(this).value()) { int cmp = ref->compare(*oprop.read(&other).value()); if (cmp) return cmp; + continue; } if (ConfigObjectList *lst = prop.read(this).value()) { int cmp = lst->compare(*oprop.read(&other).value()); if (cmp) return cmp; + continue; } if (ConfigObjectRefList *lst = prop.read(this).value()) { - int cmp = lst->copy(*oprop.read(&other).value()); + int cmp = lst->compare(*oprop.read(&other).value()); if (cmp) return cmp; + continue; } if (propIsInstance(prop)) { @@ -292,6 +296,7 @@ ConfigItem::compare(const ConfigItem &other) const { continue; int cmp = prop.read(&other).value()->compare(*oprop.read(&other).value()); if (cmp) return cmp; + continue; } } @@ -1383,3 +1388,14 @@ ConfigObjectRefList::serialize(const ConfigItem::Context &context, const ErrorSt return list; } +int +ConfigObjectRefList::compare(const ConfigObjectRefList &other) const { + if (count() < other.count()) return -1; + if (count() > other.count()) return 1; + for (int i=0; icompare(*other.get(i)); + if (cmp) return cmp; + } + return 0; +} + diff --git a/lib/configobject.hh b/lib/configobject.hh index 47ee94bc..4c753866 100644 --- a/lib/configobject.hh +++ b/lib/configobject.hh @@ -372,6 +372,8 @@ protected: public: bool label(ConfigItem::Context &context, const ErrorStack &err=ErrorStack()); YAML::Node serialize(const ConfigItem::Context &context, const ErrorStack &err=ErrorStack()); + + virtual int compare(const ConfigObjectRefList &other) const; }; diff --git a/src/configitemwrapper.cc b/src/configitemwrapper.cc index fdc8739e..75a2ef94 100644 --- a/src/configitemwrapper.cc +++ b/src/configitemwrapper.cc @@ -278,6 +278,7 @@ ChannelListWrapper::data(const QModelIndex &index, int role) const { case Channel::Power::Low: return tr("Low"); break; case Channel::Power::Min: return tr("Min"); break; } + break; case 5: if (channel->defaultTimeout()) return tr("[Default]"); diff --git a/test/configtest.cc b/test/configtest.cc index d8b5345a..c8000b96 100644 --- a/test/configtest.cc +++ b/test/configtest.cc @@ -1,5 +1,7 @@ #include "configtest.hh" #include "config.hh" +#include "errorstack.hh" +#include #include @@ -11,7 +13,10 @@ ConfigTest::ConfigTest(QObject *parent) : QObject(parent) void ConfigTest::initTestCase() { - _config.readYAML(":/config_test.yaml"); + ErrorStack err; + if (! _config.readYAML(":/data/config_test.yaml", err)) { + QFAIL(QString("Cannot open codeplug file: %1").arg(err.format()).toStdString().c_str()); + } } void @@ -24,7 +29,8 @@ void ConfigTest::testCloneChannelBasic() { // Check if a channel can be cloned Channel *clone = _config.channelList()->channel(0)->clone()->as(); - QTEST_ASSERT(0 == clone->compare(*_config.channelList()->channel(0))); + // Check if channels are the same + QCOMPARE(clone->compare(*_config.channelList()->channel(0)), 0); } From a9095719fc253d501216f04372b23e4a9da33ca2 Mon Sep 17 00:00:00 2001 From: Hannes Matuschek Date: Tue, 3 Jan 2023 12:36:41 +0100 Subject: [PATCH 14/16] Fixed encoding of channels for DMR-6X2UV --- lib/configobject.hh | 26 +++++++++++++++++--- lib/dmr6x2uv_codeplug.cc | 51 ++++++++++++++++++++++++++++++++++++++++ lib/dmr6x2uv_codeplug.hh | 7 +++--- 3 files changed, 78 insertions(+), 6 deletions(-) diff --git a/lib/configobject.hh b/lib/configobject.hh index 4c753866..56094066 100644 --- a/lib/configobject.hh +++ b/lib/configobject.hh @@ -100,11 +100,20 @@ protected: explicit ConfigItem(QObject *parent = nullptr); public: - /** Copies the given item into this. */ + /** Copies the given item into this one. + * @returns @c true if copying was successful and false otherwise. The two items must be of the + * same type (obviously). */ virtual bool copy(const ConfigItem &other); + /** Clones this item. */ virtual ConfigItem *clone() const = 0; - /** Compares two items. */ + + /** Compares the items. + * + * This method returns 0 if the two items are equivalent and -1, 1 otherwise. The established + * order is somewhat arbitrary. + * + * @returns 0 if the two items are equivalent, -1 or 1 otherwise.*/ virtual int compare(const ConfigItem &other) const; public: @@ -340,7 +349,12 @@ public: void clear(); bool copy(const AbstractConfigObjectList &other); - /** Compares the elements within the list. */ + /** Compares the object lists. + * + * This method returns 0 if the two lists are equivalent and -1, 1 otherwise. The established + * order is somewhat arbitrary. + * + * @returns 0 if the two lists are equivalent, -1 or 1 otherwise.*/ virtual int compare(const ConfigObjectList &other) const; /** Allocates a member objects for the given YAML node. */ @@ -373,6 +387,12 @@ public: bool label(ConfigItem::Context &context, const ErrorStack &err=ErrorStack()); YAML::Node serialize(const ConfigItem::Context &context, const ErrorStack &err=ErrorStack()); + /** Compares the object ref lists. + * + * This method returns 0 if the two lists are equivalent and -1, 1 otherwise. The established + * order is somewhat arbitrary. + * + * @returns 0 if the two lists are equivalent, -1 or 1 otherwise.*/ virtual int compare(const ConfigObjectRefList &other) const; }; diff --git a/lib/dmr6x2uv_codeplug.cc b/lib/dmr6x2uv_codeplug.cc index 799d68b5..e8a4a2e7 100644 --- a/lib/dmr6x2uv_codeplug.cc +++ b/lib/dmr6x2uv_codeplug.cc @@ -697,6 +697,57 @@ DMR6X2UVCodeplug::decodeGeneralSettings(Context &ctx, const ErrorStack &err) { return true; } + +bool +DMR6X2UVCodeplug::encodeChannels(const Flags &flags, Context &ctx, const ErrorStack &err) { + Q_UNUSED(flags); Q_UNUSED(err) + + // Encode channels + for (int i=0; ichannelList()->count(); i++) { + // enable channel + uint16_t bank = i/128, idx = i%128; + ChannelElement ch(data(CHANNEL_BANK_0 + bank*CHANNEL_BANK_OFFSET + idx*CHANNEL_SIZE)); + if (! ch.fromChannelObj(ctx.config()->channelList()->channel(i), ctx)) + return false; + } + return true; +} + +bool +DMR6X2UVCodeplug::createChannels(Context &ctx, const ErrorStack &err) { + Q_UNUSED(err) + // Create channels + uint8_t *channel_bitmap = data(CHANNEL_BITMAP); + for (uint16_t i=0; i>bit) & 0x01)) + continue; + ChannelElement ch(data(CHANNEL_BANK_0 + bank*CHANNEL_BANK_OFFSET + idx*CHANNEL_SIZE)); + if (Channel *obj = ch.toChannelObj(ctx)) { + ctx.config()->channelList()->add(obj); ctx.add(obj, i); + } + } + return true; +} + +bool +DMR6X2UVCodeplug::linkChannels(Context &ctx, const ErrorStack &err) { + Q_UNUSED(err) + + // Link channel objects + for (uint16_t i=0; i>bit) & 0x01)) + continue; + ChannelElement ch(data(CHANNEL_BANK_0 + bank*CHANNEL_BANK_OFFSET + idx*CHANNEL_SIZE)); + if (ctx.has(i)) + ch.linkChannelObj(ctx.get(i), ctx); + } + return true; +} + void DMR6X2UVCodeplug::allocateGPSSystems() { // replaces D868UVCodeplug::allocateGPSSystems diff --git a/lib/dmr6x2uv_codeplug.hh b/lib/dmr6x2uv_codeplug.hh index 44600159..d2aeb44c 100644 --- a/lib/dmr6x2uv_codeplug.hh +++ b/lib/dmr6x2uv_codeplug.hh @@ -591,13 +591,14 @@ public: /** Empty constructor. */ explicit DMR6X2UVCodeplug(QObject *parent=nullptr); - /** Allocates general settings memory section. */ virtual void allocateGeneralSettings(); - /** Encodes the general settings section. */ virtual bool encodeGeneralSettings(const Flags &flags, Context &ctx, const ErrorStack &err=ErrorStack()); - /** Decodes the general settings section. */ virtual bool decodeGeneralSettings(Context &ctx, const ErrorStack &err=ErrorStack()); + virtual bool encodeChannels(const Flags &flags, Context &ctx, const ErrorStack &err=ErrorStack()); + virtual bool createChannels(Context &ctx, const ErrorStack &err=ErrorStack()); + virtual bool linkChannels(Context &ctx, const ErrorStack &err=ErrorStack()); + void allocateGPSSystems(); bool encodeGPSSystems(const Flags &flags, Context &ctx, const ErrorStack &err=ErrorStack()); bool createGPSSystems(Context &ctx, const ErrorStack &err=ErrorStack()); From 4d8b962a43fc87a3316f7ac9028d9f88eaa10067 Mon Sep 17 00:00:00 2001 From: Hannes Matuschek Date: Tue, 3 Jan 2023 15:35:44 +0100 Subject: [PATCH 15/16] Fixed DMR-6X2UV docs. --- lib/dmr6x2uv_codeplug.hh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/dmr6x2uv_codeplug.hh b/lib/dmr6x2uv_codeplug.hh index d2aeb44c..c3b6bcdd 100644 --- a/lib/dmr6x2uv_codeplug.hh +++ b/lib/dmr6x2uv_codeplug.hh @@ -30,6 +30,7 @@ * size of each list 0x1f4. * 02540000 max. 001f40 250 Zone names. * Each zone name is up to 16 ASCII chars long and gets 0-padded to 32b. + * 02500100 000400 Zone A & B channel list. * * Roaming * Start Size Content @@ -107,7 +108,6 @@ * Start Size Content * 02500000 0000e0 General settings, * see @c DMR6X2UVCodeplug::GeneralSettingsElement. - * 02500100 000400 Zone A & B channel list. * 02500500 000100 DTMF list * 02500600 000030 Power on settings, * see @c AnytoneCodeplug::BootSettingsElement. From d8c5f377a94615eec3e3b900ada233ef121e83db Mon Sep 17 00:00:00 2001 From: Hannes Matuschek Date: Wed, 4 Jan 2023 09:28:07 +0100 Subject: [PATCH 16/16] Implemented friend flag for call-signd db. --- doc/code/d868uv_callsigndbentry.txt | 3 ++- lib/d868uv_callsigndb.cc | 7 ++++++- lib/d868uv_callsigndb.hh | 2 ++ 3 files changed, 10 insertions(+), 2 deletions(-) diff --git a/doc/code/d868uv_callsigndbentry.txt b/doc/code/d868uv_callsigndbentry.txt index 30bc3236..6da22049 100644 --- a/doc/code/d868uv_callsigndbentry.txt +++ b/doc/code/d868uv_callsigndbentry.txt @@ -2,13 +2,14 @@ +---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+ 00 | Call type | DMR ID, 8-digit BCD, big-endian ... +---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+ -04 ... | Ring | Body, up to 94 bytes, string list, 0-terminated ... +04 ... | 0 0 0 |FRD| 0 0 | Ring | Body, up to 94 bytes, string list, 0-terminated ... +---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+ ... | +---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+ Field description: - Call type: The call type of the entry, 0=Private, 1=Group and 2=All call. + - FRD: Friend flag. - Ring: Ring tone settings, 0=Off, 1=Tone, 2=Online(?). - Body: String list variable size. Each string in list 0-terminated. List is [Name, City, Call, State, Country, Comment]. Maximum length is 96 including 0 bytes. diff --git a/lib/d868uv_callsigndb.cc b/lib/d868uv_callsigndb.cc index 935eb9f5..e2692b55 100644 --- a/lib/d868uv_callsigndb.cc +++ b/lib/d868uv_callsigndb.cc @@ -44,9 +44,14 @@ D868UVCallsignDB::EntryElement::setNumber(unsigned num) { setBCD8_be(0x0001, num); } +void +D868UVCallsignDB::EntryElement::setFriendFlag(bool set) { + setBit(0x0005, 3, set); +} + void D868UVCallsignDB::EntryElement::setRingTone(RingTone tone) { - setUInt8(0x0005, (unsigned)tone); + setUInt2(0x0005, 0, (unsigned)tone); } void diff --git a/lib/d868uv_callsigndb.hh b/lib/d868uv_callsigndb.hh index 39b60831..1e6db25c 100644 --- a/lib/d868uv_callsigndb.hh +++ b/lib/d868uv_callsigndb.hh @@ -58,6 +58,8 @@ public: virtual void setCallType(DMRContact::Type type); /** Sets the DMR ID number. */ virtual void setNumber(unsigned num); + /** Set/clear friend flag. */ + virtual void setFriendFlag(bool set); /** Sets the ring tone. */ virtual void setRingTone(RingTone tone); /** Sets the entry content. */