From 7fd2751e2d4a1f239e10473d0c000c468bfcdc65 Mon Sep 17 00:00:00 2001 From: Hannes Matuschek Date: Tue, 29 Jun 2021 10:04:44 +0200 Subject: [PATCH 01/16] Added D578UV branch. --- .../anytone/d578uv/at_d578uv_emulator.py | 186 ++++++++++++++++++ 1 file changed, 186 insertions(+) create mode 100644 doc/reveng/anytone/d578uv/at_d578uv_emulator.py diff --git a/doc/reveng/anytone/d578uv/at_d578uv_emulator.py b/doc/reveng/anytone/d578uv/at_d578uv_emulator.py new file mode 100644 index 00000000..58615792 --- /dev/null +++ b/doc/reveng/anytone/d578uv/at_d578uv_emulator.py @@ -0,0 +1,186 @@ +#!/usr/bin/env python3 +# +# Emulate anytone d878uv radio to customer programming software. +# Send intercepted data stream over network to server script for further investigation. +# +# This script connects to a virtual com port COM26 which is connected via a virtual +# null modem cable to the virtual com port COM18 which is used by the programming software. +# This virtual ports and cable can be provided by the COM0COM tool. +# +# Linux users can use +# socat -d -d pty,raw,echo=0,b4000000 pty,raw,echo=0,b4000000 +# for emulating a virtual null modem cable. + +import serial +import time +import sys +import struct + + +# config +filebase = 'codeplug' +filecount = 0 +comport = 'COM6' # connected to COM18 with com0com. use COM18 in CPS + + +def hexDump(s): + h = " ".join(map("{:02x}".format, s)) + t = "" + for i in range(len(s)): + c = s[i] + if c>=0x20 and c<0x7f: + t += chr(c) + else: + t += "." + return( h + " | " + t) + +# parameters? +if len(sys.argv) == 3: + filebase = sys.argv[1] + comport = sys.argv[2] +elif len(sys.argv) == 2: + filebase = sys.argv[1] +elif len(sys.argv) >3: + print("Usage: " + sys.argv[0] + ' filebase [comport]') + exit() + + + +# open serial port +serialPort = None + +try: + print("Trying comport " + comport) + serialPort = serial.Serial(port = comport, baudrate=4000000, bytesize=8, timeout=1, stopbits=serial.STOPBITS_ONE) # 115200 921600 4000000 +except (err): + print('ERR: Could not open port ' + comport) + print("Usage: " + sys.argv[0] + ' servername [comport]: ' + err) + exit() + + +out = None +nextaddr = None + +# wait for data + +try: + + while 1: + + command = '' + + command = serialPort.read() + while serialPort.in_waiting > 0: + command += serialPort.read() + + # respond to command on com port + if ( len(command) == 0 ): + pass + + elif ( command == b'PROGRAM'): + print("Program session requested.") + resp = b'QX\x06' + serialPort.write(resp) + filename = "{}_{:04}.hex".format(filebase, filecount) + out = open(filename, "w") + nextaddr = None + + elif ( command == b'\x02' ): + print("Device info requested.") + resp = b'ID578UV\x00\x00V100\x00\x00\x06' + serialPort.write(resp) + + elif ( command == b'R\x02\xfa\x00\x20\x10' ): + print("Read special memory request.") + resp = b'W\x02\xfa\x00\x20\x10\xff\xff\xff\xff\xff\xff\xff\xff\x00\x00\x00\x00\x00\x00\x00\x00\x24\x06' + serialPort.write(resp) + + elif ( command[0:4] == b'R\x02\xfa\x00' and command[5] == 16 ): + # 0x02fa00.. + print("Read local information.") + + resp = b'W\x02\xfa\x00' + bytes([command[4]]) + b'\x10' + + if ( command[4] == 0x00 ): + resp += b'\x00\x00\x00\x03\x01\x01\x01\x00\x00\x01\x01\x20\x20\x20\x20\xff' + + elif ( command[4] == 0x10 ): # Radio Type + resp += b'\x44\x38\x37\x38\x55\x56\x00\x01\x00\xff\xff\xff\xff\xff\xff\xff' + + elif ( command[4] == 0x30 ): # Serial Number + resp += b'\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00' + elif ( command[4] == 0x40 ): # Production Date + resp += b'\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00' + elif ( command[4] == 0x50 ): # Manucfacture Code + resp += b'\x31\x32\x33\x34\x35\x36\x37\x38\xff\xff\xff\xff\xff\xff\xff\xff' + elif ( command[4] == 0x60 ): # Maintained Date + resp += b'\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00' + elif ( command[4] == 0x70 ): # Dealer Code + resp += b'\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00' + elif ( command[4] == 0x80 ): # Stock Date + resp += b'\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00' + elif ( command[4] == 0x90 ): # Sell Date + resp += b'\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00' + elif ( command[4] == 0xa0 ): # Seller + resp += b'\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00' + + elif ( command[4] == 0xb0 ): # Maintained Description + resp += b'\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00' + elif ( command[4] == 0xc0 ): + resp += b'\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00' + elif ( command[4] == 0xd0 ): + resp += b'\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00' + elif ( command[4] == 0xe0 ): + resp += b'\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00' + elif ( command[4] == 0xf0 ): + resp += b'\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00' + + else: + resp += b'\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00' + + resp = resp + bytes( [sum(resp[1:]) & 0xff] ) + b'\x06' + #print(resp.hex()) + serialPort.write(resp) + + elif ( command[0] == ord('W') ) : + resp = b'\x06' # just ack + serialPort.write(resp) + res = struct.unpack(">cIB16sBB", command) + addr = res[1] + if nextaddr != addr: + out.write((8+3+16*3+16+2)*"-" + "\n") + out.write("{:08X} : {}\n".format(addr, hexDump(res[3]))) + nextaddr = addr+16 + + elif ( command == b'END' ): + print("End session.") + resp = b'\x06' # just ack + serialPort.write(resp) + out.close() + filecount += 1 + + elif ( command == b'UPDATE' ): + # for firmware update the device has to be switched on while pressing PF3 (blue button on top) and PTT keys + print("Start Firmware Update. Only useful if device is in update receiving mode. (Switch on while pressing PF3 (blue button on top) and PTT keys)") + resp = b'\x06' # just ack + serialPort.write(resp) + + elif ( command == b'\x18' ): + print("Firmware Update Send Complete. Switch device on while pressing PF2 (top left side) and PTT keys to start installer.") + resp = b'\x06' # just ack + serialPort.write(resp) + + elif ( command[0] == 0x01 ): + print("Firmware data.") + resp = b'\x06' # just ack + serialPort.write(resp) + else: + #print("> " + str(command)) + pass + + + +finally: + print('QRT') + serialPort.close() + From e94e910d8c0245ed4d2da4add1b01d305f95dd72 Mon Sep 17 00:00:00 2001 From: Hannes Matuschek Date: Wed, 30 Jun 2021 14:29:11 +0200 Subject: [PATCH 02/16] Fixed emulation for FW version 1.13 --- doc/reveng/anytone/d578uv/at_d578uv_emulator.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/reveng/anytone/d578uv/at_d578uv_emulator.py b/doc/reveng/anytone/d578uv/at_d578uv_emulator.py index 58615792..e1014cab 100644 --- a/doc/reveng/anytone/d578uv/at_d578uv_emulator.py +++ b/doc/reveng/anytone/d578uv/at_d578uv_emulator.py @@ -87,7 +87,7 @@ def hexDump(s): elif ( command == b'\x02' ): print("Device info requested.") - resp = b'ID578UV\x00\x00V100\x00\x00\x06' + resp = b'ID578UV\x00\x00V110\x00\x00\x06' serialPort.write(resp) elif ( command == b'R\x02\xfa\x00\x20\x10' ): From ab6f1da223154dd679b60f853b97d826a56413c2 Mon Sep 17 00:00:00 2001 From: Hannes Matuschek Date: Wed, 30 Jun 2021 15:29:11 +0200 Subject: [PATCH 03/16] Added AT-D578UV classes. --- cli/decodecodeplug.cc | 54 +++++++++- cli/encodecallsigndb.cc | 9 ++ cli/encodecodeplug.cc | 41 ++++++++ doc/fig/d578uv.jpg | Bin 0 -> 88483 bytes lib/CMakeLists.txt | 12 ++- lib/d578uv.cc | 149 ++++++++++++++++++++++++++ lib/d578uv.hh | 33 ++++++ lib/d578uv_codeplug.cc | 116 +++++++++++++++++++++ lib/d578uv_codeplug.hh | 224 ++++++++++++++++++++++++++++++++++++++++ lib/d878uv2.cc | 3 +- lib/radio.cc | 3 + 11 files changed, 638 insertions(+), 6 deletions(-) create mode 100644 doc/fig/d578uv.jpg create mode 100644 lib/d578uv.cc create mode 100644 lib/d578uv.hh create mode 100644 lib/d578uv_codeplug.cc create mode 100644 lib/d578uv_codeplug.hh diff --git a/cli/decodecodeplug.cc b/cli/decodecodeplug.cc index 59f050f6..0e159daa 100644 --- a/cli/decodecodeplug.cc +++ b/cli/decodecodeplug.cc @@ -11,8 +11,10 @@ #include "rd5r_codeplug.hh" #include "gd77_codeplug.hh" #include "opengd77_codeplug.hh" +#include "d868uv_codeplug.hh" #include "d878uv_codeplug.hh" - +#include "d878uv2_codeplug.hh" +#include "d578uv_codeplug.hh" int decodeCodeplug(QCommandLineParser &parser, QCoreApplication &app) { Q_UNUSED(app); @@ -146,6 +148,31 @@ int decodeCodeplug(QCommandLineParser &parser, QCoreApplication &app) { return -1; } + if (3 <= parser.positionalArguments().size()) { + QFile outfile(parser.positionalArguments().at(2)); + if (! outfile.open(QIODevice::WriteOnly)) { + logError() << "Cannot write CSV codeplug file '" << outfile.fileName() << "':" << outfile.errorString(); + return -1; + } + QTextStream stream(&outfile); + config.writeCSV(stream, errorMessage); + outfile.close(); + } else { + QTextStream stream(stdout); + config.writeCSV(stream, errorMessage); + } + } else if ("d878uv2"==parser.value("radio").toLower()) { + D878UV2Codeplug codeplug; + if (! codeplug.read(filename)) { + logError() << "Cannot decode binary codeplug file '" << filename << "':" << codeplug.errorMessage(); + return -1; + } + Config config; + if (! codeplug.decode(&config)) { + logError() << "Cannot decode binary codeplug file '" << filename << "':" << codeplug.errorMessage(); + return -1; + } + if (3 <= parser.positionalArguments().size()) { QFile outfile(parser.positionalArguments().at(2)); if (! outfile.open(QIODevice::WriteOnly)) { @@ -171,6 +198,31 @@ int decodeCodeplug(QCommandLineParser &parser, QCoreApplication &app) { return -1; } + if (3 <= parser.positionalArguments().size()) { + QFile outfile(parser.positionalArguments().at(2)); + if (! outfile.open(QIODevice::WriteOnly)) { + logError() << "Cannot write CSV codeplug file '" << outfile.fileName() << "':" << outfile.errorString(); + return -1; + } + QTextStream stream(&outfile); + config.writeCSV(stream, errorMessage); + outfile.close(); + } else { + QTextStream stream(stdout); + config.writeCSV(stream, errorMessage); + } + } else if ("d578uv"==parser.value("radio").toLower()) { + D578UVCodeplug codeplug; + if (! codeplug.read(filename)) { + logError() << "Cannot decode binary codeplug file '" << filename << "':" << codeplug.errorMessage(); + return -1; + } + Config config; + if (! codeplug.decode(&config)) { + logError() << "Cannot decode binary codeplug file '" << filename << "':" << codeplug.errorMessage(); + return -1; + } + if (3 <= parser.positionalArguments().size()) { QFile outfile(parser.positionalArguments().at(2)); if (! outfile.open(QIODevice::WriteOnly)) { diff --git a/cli/encodecallsigndb.cc b/cli/encodecallsigndb.cc index 49318c3d..e0498ae4 100644 --- a/cli/encodecallsigndb.cc +++ b/cli/encodecallsigndb.cc @@ -9,6 +9,7 @@ #include "uv390_callsigndb.hh" #include "opengd77_callsigndb.hh" #include "d868uv_callsigndb.hh" +#include "d878uv2_callsigndb.hh" #include "crc32.hh" @@ -89,6 +90,14 @@ int encodeCallsignDB(QCommandLineParser &parser, QCoreApplication &app) { << "': " << db.errorMessage(); return -1; } + } else if (("d878uv2"==parser.value("radio").toLower()) || ("d578uv"==parser.value("radio").toLower()) ){ + D878UV2CallsignDB db; + db.encode(&userdb, selection); + if (! db.write(parser.positionalArguments().at(1))) { + logError() << "Cannot write output call-sign DB file '" << parser.positionalArguments().at(1) + << "': " << db.errorMessage(); + return -1; + } } else { logError() << "Cannot encode calls-sign DB: Unknown radio '" << parser.value("radio") << "'."; return -1; diff --git a/cli/encodecodeplug.cc b/cli/encodecodeplug.cc index 2e00be63..2d71e90b 100644 --- a/cli/encodecodeplug.cc +++ b/cli/encodecodeplug.cc @@ -10,7 +10,10 @@ #include "rd5r_codeplug.hh" #include "gd77_codeplug.hh" #include "opengd77_codeplug.hh" +#include "d868uv_codeplug.hh" #include "d878uv_codeplug.hh" +#include "d878uv2_codeplug.hh" +#include "d578uv_codeplug.hh" #include "crc32.hh" @@ -119,6 +122,25 @@ int encodeCodeplug(QCommandLineParser &parser, QCoreApplication &app) { << "': " << codeplug.errorMessage(); return -1; } + } else if ("d878uv2"==parser.value("radio").toLower()) { + Config config; + QString errorMessage; + QTextStream stream(&infile); + if (! config.readCSV(stream, errorMessage)) { + logError() << "Cannot parse CSV codeplug '" << infile.fileName() << "': " << errorMessage; + return -1; + } + D878UV2Codeplug codeplug; + codeplug.setBitmaps(&config); + codeplug.allocateUpdated(); + codeplug.allocateForEncoding(); + codeplug.encode(&config, flags); + codeplug.image(0).sort(); + if (! codeplug.write(parser.positionalArguments().at(2))) { + logError() << "Cannot write output codeplug file '" << parser.positionalArguments().at(1) + << "': " << codeplug.errorMessage(); + return -1; + } } else if ("d868uv"==parser.value("radio").toLower()) { Config config; QString errorMessage; @@ -138,6 +160,25 @@ int encodeCodeplug(QCommandLineParser &parser, QCoreApplication &app) { << "': " << codeplug.errorMessage(); return -1; } + } else if ("d578uv"==parser.value("radio").toLower()) { + Config config; + QString errorMessage; + QTextStream stream(&infile); + if (! config.readCSV(stream, errorMessage)) { + logError() << "Cannot parse CSV codeplug '" << infile.fileName() << "': " << errorMessage; + return -1; + } + D578UVCodeplug codeplug; + codeplug.setBitmaps(&config); + codeplug.allocateUpdated(); + codeplug.allocateForEncoding(); + codeplug.encode(&config, flags); + codeplug.image(0).sort(); + if (! codeplug.write(parser.positionalArguments().at(2))) { + logError() << "Cannot write output codeplug file '" << parser.positionalArguments().at(1) + << "': " << codeplug.errorMessage(); + return -1; + } } else { logError() << "Cannot encode codeplug: Unknown radio '" << parser.value("radio") << "'."; return -1; diff --git a/doc/fig/d578uv.jpg b/doc/fig/d578uv.jpg new file mode 100644 index 0000000000000000000000000000000000000000..64350a5e2328388620c6768f5fa0ec6623931d85 GIT binary patch literal 88483 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

*fHISK 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 Ui?*MNG1c(O{fqp+{!l;v*;bC_Q2+n{ literal 0 HcmV?d00001 diff --git a/lib/CMakeLists.txt b/lib/CMakeLists.txt index 2fca1237..b186d997 100644 --- a/lib/CMakeLists.txt +++ b/lib/CMakeLists.txt @@ -12,10 +12,14 @@ SET(libdmrconf_SOURCES csvreader.cc dfufile.cc repeaterdatabase.cc userdatabase.cc logger.cc config.cc contact.cc rxgrouplist.cc channel.cc zone.cc scanlist.cc gpssystem.cc codeplug.cc roaming.cc callsigndb.cc talkgroupdatabase.cc radioid.cc - rd5r.cc rd5r_codeplug.cc uv390.cc uv390_codeplug.cc uv390_callsigndb.cc gd77.cc gd77_codeplug.cc + rd5r.cc rd5r_codeplug.cc + uv390.cc uv390_codeplug.cc uv390_callsigndb.cc + gd77.cc gd77_codeplug.cc opengd77.cc opengd77_interface.cc opengd77_codeplug.cc opengd77_callsigndb.cc anytone_interface.cc anytone_radio.cc anytone_codeplug.cc - d868uv.cc d868uv_codeplug.cc d868uv_callsigndb.cc d878uv.cc d878uv_codeplug.cc + d868uv.cc d868uv_codeplug.cc d868uv_callsigndb.cc + d878uv.cc d878uv_codeplug.cc + d578uv.cc d578uv_codeplug.cc d878uv2.cc d878uv2_codeplug.cc d878uv2_callsigndb.cc) SET(libdmrconf_MOC_HEADERS radio.hh ${hid_HEADERS} hid_interface.hh dfu_libusb.hh usbserial.hh @@ -25,7 +29,9 @@ SET(libdmrconf_MOC_HEADERS rd5r.hh rd5r_codeplug.hh uv390.hh uv390_codeplug.hh uv390_callsigndb.hh gd77.hh gd77_codeplug.hh opengd77.hh opengd77_interface.hh opengd77_codeplug.hh opengd77_callsigndb.hh anytone_interface.hh anytone_radio.hh anytone_codeplug.hh - d868uv.hh d868uv_codeplug.hh d868uv_callsigndb.hh d878uv.hh d878uv_codeplug.hh + d868uv.hh d868uv_codeplug.hh d868uv_callsigndb.hh + d878uv.hh d878uv_codeplug.hh + d578uv.hh d578uv_codeplug.hh d878uv2.hh d878uv2_codeplug.hh d878uv2_callsigndb.hh) SET(libdmrconf_HEADERS libdmrconf.hh radiointerface.hh utils.hh crc32.hh csvwriter.hh signaling.hh codeplugcontext.hh addressmap.hh) diff --git a/lib/d578uv.cc b/lib/d578uv.cc new file mode 100644 index 00000000..4d966abc --- /dev/null +++ b/lib/d578uv.cc @@ -0,0 +1,149 @@ +#include "d578uv.hh" +#include "config.hh" +#include "logger.hh" + +#include "d578uv_codeplug.hh" +// uses same callsign db as 878 +#include "d878uv2_callsigndb.hh" + +#define RBSIZE 16 +#define WBSIZE 16 + + +static Radio::Features _d578uv_features = +{ + // show beta-warning + .betaWarning = true, + + // general capabilities + .hasDigital = true, + .hasAnalog = true, + + .frequencyLimits = QVector{ {136., 174.}, {220., 225.}, {400., 480.} }, + + // general limits + .maxRadioIDs = 250, + .maxNameLength = 16, + .maxIntroLineLength = 14, + + // channel limits + .maxChannels = 4000, + .maxChannelNameLength = 16, + .allowChannelNoDefaultContact = false, + + // zone limits + .maxZones = 250, + .maxZoneNameLength = 16, + .maxChannelsInZone = 250, + .hasABZone = false, + + // scanlist limits + .hasScanlists = true, + .maxScanlists = 250, + .maxScanlistNameLength = 16, + .maxChannelsInScanlist = 31, + .scanListNeedsPriority = false, + + // contact list limits + .maxContacts = 10000, + .maxContactNameLength = 16, + + // rx group list limits + .maxGrouplists = 250, + .maxGrouplistNameLength = 16, + .maxContactsInGrouplist = 64, + + .hasGPS = true, + .maxGPSSystems = 8, + + .hasAPRS = true, + .maxAPRSSystems = 1, + + .hasRoaming = true, + .maxRoamingChannels = 250, + .maxRoamingZones = 64, + .maxChannelsInRoamingZone = 64, + + // call-sign database limits + .hasCallsignDB = true, // hasCallsignDB + .callsignDBImplemented = true, // callsignDBImplemented + .maxCallsignsInDB = 500000 // maxCallsignsInDB +}; + + +D578UV::D578UV(AnytoneInterface *device, QObject *parent) + : AnytoneRadio("Anytone AT-D578UV", device, parent), _features(_d578uv_features) +{ + _codeplug = new D578UVCodeplug(this); + _callsigns = new D878UV2CallsignDB(this); + + // Get device info and determine supported TX frequency bands + AnytoneInterface::RadioInfo info; _dev->getInfo(info); + switch (info.bands) { + case 0x00: + case 0x01: + case 0x04: + _features.frequencyLimits = QVector{ {136., 174.}, {400., 480.} }; + break; + case 0x02: + _features.frequencyLimits = QVector{ {136., 174.}, {430., 440.} }; + break; + case 0x03: + case 0x05: + _features.frequencyLimits = QVector{ {144., 146.}, {430., 440.} }; + break; + case 0x06: + _features.frequencyLimits = QVector{ {136., 174.}, {446., 447.} }; + break; + case 0x07: + _features.frequencyLimits = QVector{ {144., 148.}, {420., 450.} }; + break; + case 0x08: + _features.frequencyLimits = QVector{ {136., 174.}, {400., 470.} }; + break; + case 0x09: + _features.frequencyLimits = QVector{ {144., 146.}, {430., 432.} }; + break; + case 0x0a: + _features.frequencyLimits = QVector{ {144., 148.}, {430., 450.} }; + break; + case 0x0b: + _features.frequencyLimits = QVector{ {136., 174.}, {400., 520.} }; + break; + case 0x0c: + _features.frequencyLimits = QVector{ {136., 174.}, {400., 490.} }; + break; + case 0x0d: + _features.frequencyLimits = QVector{ {136., 174.}, {403., 470.} }; + break; + case 0x0e: + _features.frequencyLimits = QVector{ {136., 174.}, {220.,225.}, {400., 520.} }; + break; + case 0x0f: + _features.frequencyLimits = QVector{ {144., 148.}, {400., 520.} }; + break; + case 0x10: + _features.frequencyLimits = QVector{ {144., 147.}, {430., 440.} }; + break; + case 0x11: + _features.frequencyLimits = QVector{ {136., 174.} }; + break; + default: + logInfo() << "Unknown band-code" << QString::number(int(info.bands), 16) + << ": Set freq range to 136-174MHz and 400-480MHz."; + _features.frequencyLimits = QVector{ {136., 174.}, {400., 480.} }; + break; + } + + QStringList bands; + foreach(Radio::Features::FrequencyRange r, _features.frequencyLimits.ranges) { + bands.append(tr("%1-%2MHz").arg(r.min).arg(r.max)); + } + logDebug() << "Got band-code " << QString::number(int(info.bands), 16) + << ": Limit TX frequencies to " << bands.join(", ") << "."; +} + +const Radio::Features & +D578UV::features() const { + return _features; +} diff --git a/lib/d578uv.hh b/lib/d578uv.hh new file mode 100644 index 00000000..b2975eae --- /dev/null +++ b/lib/d578uv.hh @@ -0,0 +1,33 @@ +/** @defgroup d578uv Anytone AT-D578UV + * Device specific classes for Anytone AT-D578UV. + * + * \image html d578uv.jpg "AT-D578UV" width=200px + * \image latex d578uv.jpg "AT-D578UV" width=200px + * + * @ingroup anytone */ +#ifndef __D578UV_HH__ +#define __D578UV_HH__ + +#include "anytone_radio.hh" +#include "anytone_interface.hh" +#include "d878uv2_callsigndb.hh" + +/** Implements an interface to Anytone AT-D578UV VHF/UHF 50W DMR (Tier I & II) radios. + * + * @ingroup d578uv */ +class D578UV: public AnytoneRadio +{ + Q_OBJECT + +public: + /** Do not construct this class directly, rather use @c Radio::detect. */ + explicit D578UV(AnytoneInterface *device=nullptr, QObject *parent=nullptr); + + const Radio::Features &features() const; + +protected: + /** Holds a copy of the specific radio features. */ + Radio::Features _features; +}; + +#endif // __D878UV_HH__ diff --git a/lib/d578uv_codeplug.cc b/lib/d578uv_codeplug.cc new file mode 100644 index 00000000..1666673d --- /dev/null +++ b/lib/d578uv_codeplug.cc @@ -0,0 +1,116 @@ +#include "d578uv_codeplug.hh" +#include "config.hh" +#include "utils.hh" +#include "channel.hh" +#include "gpssystem.hh" +#include "userdatabase.hh" +#include "config.h" +#include "logger.hh" + +#include +#include + +#define NUM_CONTACTS 10000 // Total number of contacts +#define NUM_CONTACT_BANKS 2500 // Number of contact banks +#define CONTACTS_PER_BANK 4 +#define CONTACT_BANK_0 0x02680000 // First bank of 4 contacts +#define CONTACT_BANK_SIZE 0x00000190 // Size of 4 contacts +#define CONTACT_INDEX_LIST 0x02600000 // Address of contact index list +#define CONTACTS_BITMAP 0x02640000 // Address of contact bitmap +#define CONTACTS_BITMAP_SIZE 0x00000500 // Size of contact bitmap +#define CONTACT_ID_MAP 0x04800000 // Address of ID->Contact index map +#define CONTACT_ID_ENTRY_SIZE sizeof(contact_map_t) // Size of each map entry + +#define NUM_APRS_RX_ENTRY 32 +#define ADDR_APRS_RX_ENTRY 0x02501800 // Address of APRS RX list +#define APRS_RX_ENTRY_SIZE 0x00000008 // Size of each APRS RX entry +static_assert( + APRS_RX_ENTRY_SIZE == sizeof(D578UVCodeplug::aprs_rx_entry_t), + "D578UVCodeplug::aprs_rx_entry_t size check failed."); + +#define ADDR_APRS_SET_EXT 0x025010A0 // Address of APRS settings extension +#define APRS_SET_EXT_SIZE 0x00000060 // Size of APRS settings extension +static_assert( + APRS_SET_EXT_SIZE == sizeof(D578UVCodeplug::aprs_setting_ext_t), + "D578UVCodeplug::aprs_setting_ext_t size check failed."); + +#define ADDR_UNKNOWN_SETTING 0x02500600 // Address of unknown settings +#define UNKNOWN_SETTING_SIZE 0x00000030 // Size of unknown settings. + + + +/* ******************************************************************************************** * + * Implementation of D878UV2Codeplug + * ******************************************************************************************** */ +D578UVCodeplug::D578UVCodeplug(QObject *parent) + : D878UVCodeplug(parent) +{ + // pass... +} + +void +D578UVCodeplug::allocateUpdated() { + // allocate everything from D878UV codeplug + D878UVCodeplug::allocateUpdated(); + + // allocate unknown settings + image(0).addElement(ADDR_UNKNOWN_SETTING, UNKNOWN_SETTING_SIZE); + + // allocate APRS RX list + image(0).addElement(ADDR_APRS_RX_ENTRY, NUM_APRS_RX_ENTRY*APRS_RX_ENTRY_SIZE); + // allocate APRS settings extension + image(0).addElement(ADDR_APRS_SET_EXT, APRS_SET_EXT_SIZE); +} + + +void +D578UVCodeplug::allocateContacts() { + /* Allocate contacts */ + uint8_t *contact_bitmap = data(CONTACTS_BITMAP); + uint contactCount=0; + for (uint16_t i=0; i>(i%8)) & 0x01)) + continue; + contactCount++; + uint32_t addr = CONTACT_BANK_0+(i/CONTACTS_PER_BANK)*CONTACT_BANK_SIZE; + if (nullptr == data(addr, 0)) { + image(0).addElement(addr, CONTACT_BANK_SIZE); + memset(data(addr), 0x00, CONTACT_BANK_SIZE); + } + } + if (contactCount) { + image(0).addElement(CONTACT_INDEX_LIST, align_size(4*contactCount, 16)); + memset(data(CONTACT_INDEX_LIST), 0xff, align_size(4*contactCount, 16)); + image(0).addElement(CONTACT_ID_MAP, align_size(CONTACT_ID_ENTRY_SIZE*(1+contactCount), 16)); + memset(data(CONTACT_ID_MAP), 0xff, align_size(CONTACT_ID_ENTRY_SIZE*(1+contactCount), 16)); + } +} + +bool +D578UVCodeplug::encodeContacts(Config *config, const Flags &flags) { + // Encode contacts + QVector contact_id_map; + contact_id_map.reserve(config->contacts()->digitalCount()); + for (int i=0; icontacts()->digitalCount(); i++) { + contact_t *con = (contact_t *)data(CONTACT_BANK_0+i*sizeof(contact_t)); + con->fromContactObj(config->contacts()->digitalContact(i)); + ((uint32_t *)data(CONTACT_INDEX_LIST))[i] = qToLittleEndian(i); + contact_map_t entry; + entry.setID(config->contacts()->digitalContact(i)->number(), + DigitalContact::GroupCall == config->contacts()->digitalContact(i)->type()); + entry.setIndex(i); + contact_id_map.append(entry); + } + // encode index map for contacts + std::sort(contact_id_map.begin(), contact_id_map.end(), + [](const contact_map_t &a, const contact_map_t &b) { + return a.ID() < b.ID(); + }); + for (int i=0; i + +#include "d878uv_codeplug.hh" +#include "signaling.hh" +#include "codeplugcontext.hh" + +class Channel; +class DigitalContact; +class Zone; +class RXGroupList; +class ScanList; +class GPSSystem; + + +/** Represents the device specific binary codeplug for Anytone AT-D578UV radios. + * + * @section d578uvcpl 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 upto 128 channels, see @c D878UVCodeplug::channel_t 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 upto 128 channels.
00842000 max. 002000 Unknown data, related to CH bank 1?
... ... ...
00FC0000 max. 000800 Channel bank 32, upto 32 channels.
00FC2000 max. 000800 Unknown data, realted to CH bank 32.
00FC0800 000040 VFO A settings, see @c channel_t.
00FC0840 000040 VFO B settings, see @c channel_t.
00FC2800 000080 Unknonw 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.
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 upto 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::roaming_channel_t 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::roaming_zone_t 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 D868UVCodeplug::contact_t. + * 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".
04800000 max. 013880 DMR ID to contact index map, see @c D868UVCodeplug::contact_map_t. + * Sorted by ID, empty entries set to 0xffffffffffffffff.
Analog Contacts
Start Size Content
02900000 000080 Index list of valid ananlog contacts.
02900100 000080 Bytemap for 128 analog contacts.
02940000 max. 000180 128 analog contacts. See @c D868UVCodeplug::analog_contact_t. + * 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 D868UVCodeplug::grouplist_t.
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 D868UVCodeplug::scanlist_t.
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 D868UVCodeplug::radioid_t.
GPS/APRS
Start Size Content
02501000 000040 APRS settings, see @c D878UVCodeplug::aprs_setting_t.
02501040 000060 APRS settings, see @c D878UVCodeplug::gps_systems_t.
025010A0 000060 Extended APRS settings, see @c aprs_setting_ext_t.
02501200 000040 APRS Text, upto 60 chars ASCII, 0-padded.
02501800 000100 APRS-RX settings list up to 32 entries, 8b each. + * See @c aprs_rx_entry_t.
General Settings
Start Size Content
02500000 000100 General settings, see @c D878UVCodeplug::general_settings_base_t.
02500100 000400 Zone A & B channel list.
02500500 000100 DTMF list
02501280 000030 General settings extension 1, see @c D878UVCodeplug::general_settings_ext1_t.
02501400 000100 General settings extension 2, see @c D878UVCodeplug::general_settings_ext2_t.
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 message_list_t. 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 D868UVCodeplug::message_t.
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 D868UVCodeplug::analog_quick_call_t.
025C0B00 000010 Status message bitmap.
025C0100 000400 Upto 32 status messages. + * Length unknown, offset 0x20. ASCII 0x00 terminated and padded.
025C0500 000360 18 hot-key settings, see @c D868UVCodeplug::hotkey_t
Encryption keys
Start Size Content
024C4000 004000 Upto 256 AES encryption keys. + * See @c D878UVCodeplug::encryption_key_t.
Misc
Start Size Content
024C1400 000020 Alarm setting, see @c D868UVCodeplug::analog_alarm_setting_t.
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.
Still unknown
Start Size Content
024C1440 000030 Unknown data.
024C1700 000040 Unknown, 8bit indices.
024C1800 000500 Empty, set to 0x00?
02500600 000030 Unknown, set to 0x00.
+ * + * @ingroup d578uv */ +class D578UVCodeplug : public D878UVCodeplug +{ + Q_OBJECT + +public: + /** Represents an APRS RX entry. + */ + struct __attribute__((packed)) aprs_rx_entry_t { + uint8_t enabled; ///< Enabled entry 0x01=on, 0x00=off. + char call[6]; ///< Callsign, 6x ASCII, 0-terminated. + uint8_t ssid; ///< SSID [0,15], 16=off. + }; + + /** Represents an extension to the APRS settings. */ + struct __attribute__((packed)) aprs_setting_ext_t { + uint8_t _unknown0000[8]; ///< Unknown settings block. + uint8_t rep_position : 1, ///< Report position flag. + rep_mic_e : 1, ///< Report MIC-E flag. + rep_object : 1, ///< Report object flag. + rep_item : 1, ///< Report item flag. + rep_message : 1, ///< Report message flag. + rep_wx : 1, ///< WX report flag. + rep_nmea : 1, ///< NMEA report flag. + rep_status : 1; ///< Report status flag. + uint8_t rep_other : 1, ///< Report "other" flag. + _unused0009_1 :7; ///< Unused set to 0. + uint8_t _unknown000a[6]; ///< Unknown settings block. + + uint8_t _unknown0010[16]; ///< Unknown settings block. + uint8_t _unknown0020[16]; ///< Unknown settings block. + uint8_t _unknown0030[16]; ///< Unknown settings block. + uint8_t _unknown0040[16]; ///< Unknown settings block. + uint8_t _unknown0050[16]; ///< Unknown settings block. + }; + +public: + /** Empty constructor. */ + explicit D578UVCodeplug(QObject *parent = nullptr); + + void allocateUpdated(); + void allocateContacts(); + bool encodeContacts(Config *config, const Flags &flags); +}; + +#endif // D578UV_CODEPLUG_HH diff --git a/lib/d878uv2.cc b/lib/d878uv2.cc index 47c7da59..3c563f7a 100644 --- a/lib/d878uv2.cc +++ b/lib/d878uv2.cc @@ -3,8 +3,7 @@ #include "logger.hh" #include "d878uv2_codeplug.hh" -// uses same callsign db as 878 -#include "d868uv_callsigndb.hh" +#include "d878uv2_callsigndb.hh" #define RBSIZE 16 #define WBSIZE 16 diff --git a/lib/radio.cc b/lib/radio.cc index c9915b90..b401097a 100644 --- a/lib/radio.cc +++ b/lib/radio.cc @@ -8,6 +8,7 @@ #include "d868uv.hh" #include "d878uv.hh" #include "d878uv2.hh" +#include "d578uv.hh" #include "config.hh" #include "logger.hh" #include @@ -422,6 +423,8 @@ Radio::detect(QString &errorMessage, const QString &force) { return new D878UV(anytone); } else if (("D878UV2" == id) || ("D878UV2" == force.toUpper())) { return new D878UV2(anytone); + } else if (("D578UV" == id) || ("D578UV" == force.toUpper())) { + return new D578UV(anytone); } else { anytone->close(); anytone->deleteLater(); From e6e6f0a35cd161c2290196ee2c35346c08c96cdf Mon Sep 17 00:00:00 2001 From: Hannes Matuschek Date: Wed, 30 Jun 2021 16:22:09 +0200 Subject: [PATCH 04/16] Fixed AT-D578UV codeplug docs. --- lib/d578uv_codeplug.hh | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/lib/d578uv_codeplug.hh b/lib/d578uv_codeplug.hh index 0445ad67..ae8944bf 100644 --- a/lib/d578uv_codeplug.hh +++ b/lib/d578uv_codeplug.hh @@ -135,7 +135,7 @@ class GPSSystem; * 025C0B00 000010 Status message bitmap. * 025C0100 000400 Upto 32 status messages. * Length unknown, offset 0x20. ASCII 0x00 terminated and padded. - * 025C0500 000360 18 hot-key settings, see @c D868UVCodeplug::hotkey_t + * 025C0500 000470 24 hot-key settings, see @c D868UVCodeplug::hotkey_t * * Encryption keys * Start Size Content @@ -169,11 +169,14 @@ class GPSSystem; * * Still unknown * Start Size Content + * 024C1090 000040 Unknown, set to 0xff * 024C1440 000030 Unknown data. * 024C1700 000040 Unknown, 8bit indices. * 024C1800 000500 Empty, set to 0x00? - * * 02500600 000030 Unknown, set to 0x00. + * 02BC0000 000020 Unknown, set to 0x00. + * 02BC0C60 000020 Unknown, set to 0x00. + * 02BC1000 000060 Unknown, set to 0x00. * * * @ingroup d578uv */ From 4160b734fb465262600051bb2495a74685bd24ac Mon Sep 17 00:00:00 2001 From: Hannes Matuschek Date: Wed, 30 Jun 2021 17:38:41 +0200 Subject: [PATCH 05/16] Fixed channel encoding for AT-D578UV. --- lib/d578uv_codeplug.cc | 419 +++++++++++++++++++++++++++++++++++++- lib/d578uv_codeplug.hh | 231 +++++++++++++++++++++ lib/d868uv_callsigndb.hh | 2 +- lib/d878uv2_callsigndb.hh | 2 +- lib/d878uv_codeplug.hh | 34 ++-- 5 files changed, 668 insertions(+), 20 deletions(-) diff --git a/lib/d578uv_codeplug.cc b/lib/d578uv_codeplug.cc index 1666673d..1697fdd7 100644 --- a/lib/d578uv_codeplug.cc +++ b/lib/d578uv_codeplug.cc @@ -10,6 +10,17 @@ #include #include +#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_BITMAP 0x024c1500 +#define CHANNEL_BITMAP_SIZE 0x00000200 + + #define NUM_CONTACTS 10000 // Total number of contacts #define NUM_CONTACT_BANKS 2500 // Number of contact banks #define CONTACTS_PER_BANK 4 @@ -37,10 +48,400 @@ static_assert( #define ADDR_UNKNOWN_SETTING 0x02500600 // Address of unknown settings #define UNKNOWN_SETTING_SIZE 0x00000030 // Size of unknown settings. +#define ADDR_UNKNOWN_SETTING_2 0x02BC0000 // Address of unknown settings +#define UNKNOWN_SETTING_2_SIZE 0x00000020 // Size of unknown settings. + +#define ADDR_UNKNOWN_SETTING_3 0x02BC0C60 // Address of unknown settings +#define UNKNOWN_SETTING_3_SIZE 0x00000020 // Size of unknown settings. + +#define ADDR_UNKNOWN_SETTING_4 0x02BC1000 // Address of unknown settings +#define UNKNOWN_SETTING_4_SIZE 0x00000060 // Size of unknown settings. + + +/* ******************************************************************************************** * + * Implementation of D578UVCodeplug::channel_t + * ******************************************************************************************** */ +D578UVCodeplug::channel_t::channel_t() { + clear(); +} + +void +D578UVCodeplug::channel_t::clear() { + memset(this, 0, sizeof(D578UVCodeplug::channel_t)); + custom_ctcss = qToLittleEndian(0x09cf); // some value + scan_list_index = 0xff; // None + group_list_index = 0xff; // None + id_index = 0; + squelch_mode = SQ_CARRIER; + tx_permit = ADMIT_ALWAYS; + +} + +bool +D578UVCodeplug::channel_t::isValid() const { + return (0 != name[0]) && (0xff != name[0]); +} + +double +D578UVCodeplug::channel_t::getRXFrequency() const { + return decode_frequency(qFromBigEndian(rx_frequency)); +} + +void +D578UVCodeplug::channel_t::setRXFrequency(double f) { + rx_frequency = qToBigEndian(encode_frequency(f)); +} + +double +D578UVCodeplug::channel_t::getTXFrequency() const { + double f = decode_frequency(qFromBigEndian(rx_frequency)); + switch ((RepeaterMode) repeater_mode) { + case RM_SIMPLEX: + break; + case RM_TXNEG: + f -= decode_frequency(qFromBigEndian(tx_offset)); + break; + case RM_TXPOS: + f += decode_frequency(qFromBigEndian(tx_offset)); + break; + } + return f; +} + +void +D578UVCodeplug::channel_t::setTXFrequency(double f) { + if (getRXFrequency() == f) { + tx_offset = encode_frequency(0); + repeater_mode = RM_SIMPLEX; + } else if (getRXFrequency() > f) { + tx_offset = qToBigEndian(encode_frequency(getRXFrequency()-f)); + repeater_mode = RM_TXNEG; + } else { + tx_offset = qToBigEndian(encode_frequency(f-getRXFrequency())); + repeater_mode = RM_TXPOS; + } +} + +QString +D578UVCodeplug::channel_t::getName() const { + return decode_ascii(name, 16, 0); +} + +void +D578UVCodeplug::channel_t::setName(const QString &name) { + encode_ascii(this->name, name, 16, 0); +} + +Signaling::Code +D578UVCodeplug::channel_t::getRXTone() const { + // If squelch is not SQ_TONE -> RX tone is ignored + if (SQ_TONE != squelch_mode) + return Signaling::SIGNALING_NONE; + + if (rx_ctcss && (ctcss_receive < 52)) + return ctcss_num2code(ctcss_receive); + else if (rx_dcs && (qFromLittleEndian(dcs_receive) < 512)) + return Signaling::fromDCSNumber(dec_to_oct(qFromLittleEndian(dcs_receive)), false); + else if (rx_dcs && (qFromLittleEndian(dcs_receive) >= 512)) + return Signaling::fromDCSNumber(dec_to_oct(qFromLittleEndian(dcs_receive)-512), true); + return Signaling::SIGNALING_NONE; +} + +void +D578UVCodeplug::channel_t::setRXTone(Signaling::Code code) { + if (Signaling::SIGNALING_NONE == code) { + squelch_mode = SQ_CARRIER; + rx_ctcss = rx_dcs = 0; + ctcss_receive = dcs_receive = 0; + } else if (Signaling::isCTCSS(code)) { + squelch_mode = SQ_TONE; + rx_ctcss = 1; + rx_dcs = 0; + ctcss_receive = ctcss_code2num(code); + dcs_receive = 0; + } else if (Signaling::isDCSNormal(code)) { + squelch_mode = SQ_TONE; + rx_ctcss = 0; + rx_dcs = 1; + ctcss_receive = 0; + dcs_receive = qToLittleEndian(oct_to_dec(Signaling::toDCSNumber(code))); + } else if (Signaling::isDCSInverted(code)) { + squelch_mode = SQ_TONE; + rx_ctcss = 0; + rx_dcs = 1; + ctcss_receive = 0; + dcs_receive = qToLittleEndian(oct_to_dec(Signaling::toDCSNumber(code))+512); + } +} + +Signaling::Code +D578UVCodeplug::channel_t::getTXTone() const { + if (tx_ctcss && (ctcss_transmit < 52)) + return ctcss_num2code(ctcss_transmit); + else if (tx_dcs && (qFromLittleEndian(dcs_transmit) < 512)) + return Signaling::fromDCSNumber(dec_to_oct(qFromLittleEndian(dcs_transmit)), false); + else if (tx_dcs && (qFromLittleEndian(dcs_transmit) >= 512)) + return Signaling::fromDCSNumber(dec_to_oct(qFromLittleEndian(dcs_transmit)-512), true); + return Signaling::SIGNALING_NONE; +} + +void +D578UVCodeplug::channel_t::setTXTone(Signaling::Code code) { + if (Signaling::SIGNALING_NONE == code) { + tx_ctcss = tx_dcs = 0; + ctcss_transmit = dcs_transmit = 0; + } else if (Signaling::isCTCSS(code)) { + tx_ctcss = 1; + tx_dcs = 0; + ctcss_transmit = ctcss_code2num(code); + dcs_transmit = 0; + } else if (Signaling::isDCSNormal(code)) { + tx_ctcss = 0; + tx_dcs = 1; + ctcss_transmit = 0; + dcs_transmit = qToLittleEndian(oct_to_dec(Signaling::toDCSNumber(code))); + } else if (Signaling::isDCSInverted(code)) { + tx_ctcss = 0; + tx_dcs = 1; + ctcss_transmit = 0; + dcs_transmit = qToLittleEndian(oct_to_dec(Signaling::toDCSNumber(code))+512); + } +} + +Channel * +D578UVCodeplug::channel_t::toChannelObj() const { + // Decode power setting + Channel::Power power = Channel::LowPower; + switch ((channel_t::Power) this->power) { + case POWER_LOW: + power = Channel::LowPower; + break; + case POWER_MIDDLE: + power = Channel::MidPower; + break; + case POWER_HIGH: + power = Channel::HighPower; + break; + case POWER_TURBO: + power = Channel::MaxPower; + break; + } + bool rxOnly = (1 == this->rx_only); + + Channel *ch; + if ((MODE_ANALOG == channel_mode) || (MODE_MIXED_A_D == channel_mode)) { + if (MODE_MIXED_A_D == channel_mode) + logWarn() << "Mixed mode channels are not supported (for now). Treat ch '" + << getName() <<"' as analog channel."; + AnalogChannel::Admit admit = AnalogChannel::AdmitNone; + switch ((channel_t::Admit) tx_permit) { + case ADMIT_ALWAYS: + admit = AnalogChannel::AdmitNone; + break; + case ADMIT_CH_FREE: + admit = AnalogChannel::AdmitFree; + break; + default: + break; + } + AnalogChannel::Bandwidth bw = AnalogChannel::BWNarrow; + if (BW_12_5_KHZ == bandwidth) + bw = AnalogChannel::BWNarrow; + else + bw = AnalogChannel::BWWide; + ch = new AnalogChannel( + getName(), getRXFrequency(), getTXFrequency(), power, 0.0, rxOnly, admit, + 1, getRXTone(), getTXTone(), bw, nullptr); + } else if ((MODE_DIGITAL == channel_mode) || (MODE_MIXED_D_A == channel_mode)) { + if (MODE_MIXED_D_A == channel_mode) + logWarn() << "Mixed mode channels are not supported (for now). Treat ch '" + << getName() <<"' as digital channel."; + DigitalChannel::Admit admit = DigitalChannel::AdmitNone; + switch ((channel_t::Admit) tx_permit) { + case ADMIT_ALWAYS: + admit = DigitalChannel::AdmitNone; + break; + case ADMIT_CH_FREE: + admit = DigitalChannel::AdmitFree; + break; + case ADMIT_CC_SAME: + case ADMIT_CC_DIFF: + admit = DigitalChannel::AdmitColorCode; + break; + } + DigitalChannel::TimeSlot ts = (slot2 ? DigitalChannel::TimeSlot2 : DigitalChannel::TimeSlot1); + ch = new DigitalChannel( + getName(), getRXFrequency(), getTXFrequency(), power, 0.0, rxOnly, admit, + color_code, ts, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr); + } else { + logError() << "Cannot create channel '" << getName() + << "': Channel type " << channel_mode << "not supported."; + return nullptr; + } + + return ch; +} + +bool +D578UVCodeplug::channel_t::linkChannelObj(Channel *c, const CodeplugContext &ctx) const { + if (MODE_DIGITAL == channel_mode) { + // If channel is digital + DigitalChannel *dc = c->as(); + if (nullptr == dc) + return false; + + // Check if default contact is set, in fact a valid contact index is mandatory. + uint32_t conIdx = qFromLittleEndian(contact_index); + if ((0xffffffff != conIdx) && ctx.hasDigitalContact(conIdx)) + dc->setTXContact(ctx.getDigitalContact(conIdx)); + + // Set if RX group list is set + if ((0xff != group_list_index) && ctx.hasGroupList(group_list_index)) + dc->setRXGroupList(ctx.getGroupList(group_list_index)); + + // Link to GPS system + if ((APRS_REPORT_DIGITAL == aprs_report) && ctx.hasGPSSystem(gps_system)) + dc->setPosSystem(ctx.getGPSSystem(gps_system)); + // Link APRS system if one is defined + // There can only be one active APRS system, hence the index is fixed to one. + if ((APRS_REPORT_ANALOG == aprs_report) && ctx.hasAPRSSystem(0)) + dc->setPosSystem(ctx.getAPRSSystem(0)); + + // If roaming is not disabled -> link to default roaming zone + if (0 == excl_from_roaming) + dc->setRoaming(DefaultRoamingZone::get()); + + // Link radio ID + dc->setRadioId(ctx.getRadioId(id_index)); + + } else if (MODE_ANALOG == channel_mode) { + // If channel is analog + AnalogChannel *ac = c->as(); + if (nullptr == ac) + return false; + + // Link APRS system if one is defined + // There can only be one active APRS system, hence the index is fixed to one. + if ((APRS_REPORT_ANALOG == aprs_report) && ctx.hasAPRSSystem(0)) + ac->setAPRSSystem(ctx.getAPRSSystem(0)); + } + + // For both, analog and digital channels: + + // If channel has scan list + if ((0xff != scan_list_index) && ctx.hasScanList(scan_list_index)) + c->setScanList(ctx.getScanList(scan_list_index)); + + return true; +} + +void +D578UVCodeplug::channel_t::fromChannelObj(const Channel *c, const Config *conf) { + clear(); + + // set channel name + setName(c->name()); + + // set rx and tx frequencies + setRXFrequency(c->rxFrequency()); + setTXFrequency(c->txFrequency()); + + // encode power setting + switch (c->power()) { + case Channel::MaxPower: + power = POWER_TURBO; + break; + case Channel::HighPower: + power = POWER_HIGH; + break; + case Channel::MidPower: + power = POWER_MIDDLE; + break; + case Channel::LowPower: + case Channel::MinPower: + power = POWER_LOW; + break; + } + + // set tx-enable + rx_only = c->rxOnly() ? 1 : 0; + + // Link scan list if set + if (nullptr == c->scanList()) + scan_list_index = 0xff; + else + scan_list_index = conf->scanlists()->indexOf(c->scanList()); + + // Dispatch by channel type + if (c->is()) { + const AnalogChannel *ac = c->as(); + channel_mode = MODE_ANALOG; + // pack analog channel config + // set admit criterion + switch (ac->admit()) { + case AnalogChannel::AdmitNone: tx_permit = ADMIT_ALWAYS; break; + case AnalogChannel::AdmitFree: tx_permit = ADMIT_CH_FREE; break; + case AnalogChannel::AdmitTone: tx_permit = ADMIT_ALWAYS; break; + } + // squelch mode + squelch_mode = (Signaling::SIGNALING_NONE == ac->rxTone()) ? SQ_CARRIER : SQ_TONE; + setRXTone(ac->rxTone()); + setTXTone(ac->txTone()); + // set bandwidth + bandwidth = (AnalogChannel::BWNarrow == ac->bandwidth()) ? BW_12_5_KHZ : BW_25_KHZ; + // Set APRS system + rx_gps = 0; + if (nullptr != ac->aprsSystem()) { + aprs_report = APRS_REPORT_ANALOG; + rx_gps = 1; + } + } else if (c->is()) { + const DigitalChannel *dc = c->as(); + // pack digital channel config. + channel_mode = MODE_DIGITAL; + // set admit criterion + switch(dc->admit()) { + case DigitalChannel::AdmitNone: tx_permit = ADMIT_ALWAYS; break; + case DigitalChannel::AdmitFree: tx_permit = ADMIT_CH_FREE; break; + case DigitalChannel::AdmitColorCode: tx_permit = ADMIT_CC_SAME; break; + } + // set color code + color_code = dc->colorCode(); + // set time-slot + slot2 = (DigitalChannel::TimeSlot2 == dc->timeslot()) ? 1 : 0; + // link transmit contact + if (nullptr == dc->txContact()) { + contact_index = 0; + } else { + contact_index = qToLittleEndian( + uint32_t(conf->contacts()->indexOfDigital(dc->txContact()))); + } + // link RX group list + if (nullptr == dc->rxGroupList()) + group_list_index = 0xff; + else + group_list_index = conf->rxGroupLists()->indexOf(dc->rxGroupList()); + // Set GPS system index + rx_gps = 0; + if (dc->posSystem() && dc->posSystem()->is()) { + aprs_report = APRS_REPORT_DIGITAL; + gps_system = conf->posSystems()->indexOfGPSSys(dc->posSystem()->as()); + rx_gps = 1; + } else if (dc->posSystem() && dc->posSystem()->is()) { + aprs_report = APRS_REPORT_ANALOG; + rx_gps = 1; + } + // Set radio ID + if (nullptr != dc->radioId()) + id_index = conf->radioIDs()->indexOf(dc->radioId()); + else + id_index = 0; + } +} /* ******************************************************************************************** * - * Implementation of D878UV2Codeplug + * Implementation of D578UVCodeplug * ******************************************************************************************** */ D578UVCodeplug::D578UVCodeplug(QObject *parent) : D878UVCodeplug(parent) @@ -63,6 +464,21 @@ D578UVCodeplug::allocateUpdated() { } +bool +D578UVCodeplug::encodeChannels(Config *config, const Flags &flags) { + // Encode channels + for (int i=0; ichannelList()->count(); i++) { + // enable channel + uint16_t bank = i/128, idx = i%128; + channel_t *ch = (channel_t *)data(CHANNEL_BANK_0 + + bank*CHANNEL_BANK_OFFSET + + idx*sizeof(channel_t)); + ch->fromChannelObj(config->channelList()->channel(i), config); + } + return true; +} + + void D578UVCodeplug::allocateContacts() { /* Allocate contacts */ @@ -87,6 +503,7 @@ D578UVCodeplug::allocateContacts() { } } + bool D578UVCodeplug::encodeContacts(Config *config, const Flags &flags) { // Encode contacts diff --git a/lib/d578uv_codeplug.hh b/lib/d578uv_codeplug.hh index ae8944bf..68f9df22 100644 --- a/lib/d578uv_codeplug.hh +++ b/lib/d578uv_codeplug.hh @@ -185,6 +185,234 @@ class D578UVCodeplug : public D878UVCodeplug Q_OBJECT public: + /** Represents the actual channel encoded within the binary code-plug. + * + * Memmory layout of encoded channel (64byte): + * @verbinclude d578uvchannel.txt + */ + struct __attribute__((packed)) channel_t { + /** Defines all possible channel modes, see @c channel_mode. */ + typedef enum { + MODE_ANALOG = 0, ///< Analog channel. + MODE_DIGITAL = 1, ///< Digital (DMR) channel. + MODE_MIXED_A_D = 2, ///< Mixed, analog channel with digital RX. + MODE_MIXED_D_A = 3 ///< Mixed, digital channel with analog RX. + } Mode; + + /** Defines all possible power settings.*/ + typedef enum { + POWER_LOW = 0, ///< Low power, usually 1W. + POWER_MIDDLE = 1, ///< Medium power, usually 2.5W. + POWER_HIGH = 2, ///< High power, usually 5W. + POWER_TURBO = 3 ///< Higher power, usually 7W on VHF and 6W on UHF. + } Power; + + /** Defines all band-width settings for analog channel.*/ + typedef enum { + BW_12_5_KHZ = 0, ///< Narrow band-width (12.5kHz). + BW_25_KHZ = 1 ///< High band-width (25kHz). + } Bandwidth; + + /** Defines all possible repeater modes. */ + typedef enum { + RM_SIMPLEX = 0, ///< Simplex mode, that is TX frequency = RX frequency. @c tx_offset is ignored. + RM_TXPOS = 1, ///< Repeater mode with positive @c tx_offset. + RM_TXNEG = 2 ///< Repeater mode with negative @c tx_offset. + } RepeaterMode; + + /** Defines all possible PTT-ID settings. */ + typedef enum { + PTTID_OFF = 0, ///< Never send PTT-ID. + PTTID_START = 1, ///< Send PTT-ID at start. + PTTID_END = 2, ///< Send PTT-ID at end. + PTTID_START_END = 3 ///< Send PTT-ID at start and end. + } PTTId; + + /** Defines all possible squelch settings. */ + typedef enum { + SQ_CARRIER = 0, ///< Open squelch on carrier. + SQ_TONE = 1 ///< Open squelch on matching CTCSS tone or DCS code. + } SquelchMode; + + /** Defines all possible admit criteria. */ + typedef enum { + ADMIT_ALWAYS = 0, ///< Admit TX always. + ADMIT_CH_FREE = 1, ///< Admit TX on channel free. + ADMIT_CC_DIFF = 2, ///< Admit TX on mismatching color-code. + ADMIT_CC_SAME = 3 ///< Admit TX on matching color-code. + } Admit; + + /** Defines all possible optional signalling settings. */ + typedef enum { + OPTSIG_OFF = 0, ///< None. + OPTSIG_DTMF = 1, ///< Use DTMF. + OPTSIG_2TONE = 2, ///< Use 2-tone. + OPTSIG_5TONE = 3 ///< Use 5-tone. + } OptSignaling; + + /** Defines all possible APRS reporting modes. */ + typedef enum { + APRS_REPORT_OFF = 0, ///< No APRS (GPS) reporting at all. + APRS_REPORT_ANALOG = 1, ///< Use analog, actual APRS reporting. + APRS_REPORT_DIGITAL = 2 ///< Use digital reporting. + } APRSReport; + + /** Defines all possible APRS PTT settings. */ + typedef enum { + APRS_PTT_OFF = 0, ///< Do not send APRS on PTT. + APRS_PTT_START = 1, ///< Send APRS at start of transmission. + APRS_PTT_END = 2 ///< Send APRS at end of transmission. + } APRSPTT; + + + // Bytes 00 + uint32_t rx_frequency; ///< RX Frequency, 8 digits BCD, big-endian. + uint32_t tx_offset; ///< TX Offset, 8 digits BCD, big-endian, sign in repeater_mode. + + // Byte 08 + uint8_t channel_mode : 2, ///< Mode: Analog or Digital, see @c Mode. + power : 2, ///< Power: Low, Middle, High, Turbo, see @c Power. + bandwidth : 1, ///< Bandwidth: 12.5 or 25 kHz, see @c Bandwidth. + _unused8 : 1, ///< Unused, set to 0. + repeater_mode : 2; ///< Sign of TX frequency offset, see @c RepeaterMode. + + // Byte 09 + uint8_t rx_ctcss : 1, ///< CTCSS decode enable. + rx_dcs : 1, ///< DCS decode enable. + tx_ctcss : 1, ///< CTCSS encode enable. + tx_dcs : 1, ///< DCS encode enable + reverse : 1, ///< CTCSS phase-reversal. + rx_only : 1, ///< TX prohibit. + call_confirm : 1, ///< Call confirmation enable. + talkaround : 1; ///< Talk-around enable. + + // Bytes 0a + uint8_t ctcss_transmit; ///< TX CTCSS tone, 0=62.5, 50=254.1, 51=custom CTCSS tone. + uint8_t ctcss_receive; ///< RX CTCSS tone: 0=62.5, 50=254.1, 51=custom CTCSS tone. + uint16_t dcs_transmit; ///< TX DCS code: 0=D000N, 511=D777N, 512=D000I, 1023=D777I, DCS code-number in octal, little-endian. + uint16_t dcs_receive; ///< RX DCS code: 0=D000N, 511=D777N, 512=D000I, 1023=D777I, DCS code-number in octal, little-endian. + + // Bytes 10 + uint16_t custom_ctcss; ///< Custom CTCSS tone frequency: 0x09cf=251.1, 0x0a28=260, big-endian. + uint8_t tone2_decode; ///< 2-Tone decode: 0x00=1, 0x0f=16 + uint8_t _unused19; ///< Unused, set to 0. + + // Bytes 14 + uint32_t contact_index; ///< Contact index, zero-based, little-endian. + + // Byte 18 + uint8_t id_index; ///< Index to radio ID table. + + // Byte 19 + uint8_t ptt_id : 2, ///< PTT ID, see PTTId, unused in U868UV. + _unused19_1 : 2, ///< Unused, set to 0. + squelch_mode : 1, ///< Squelch mode, see @c SquelchMode. + _unused19_2 : 3; ///< Unused, set to 0. + + // Byte 1a + uint8_t tx_permit : 2, ///< TX permit, see @c Admit. + _unused1a_1 : 2, ///< Unused, set to 0. + opt_signal : 2, ///< Optional signaling, see @c OptSignaling. + _unused1a_2 : 2; ///< Unused, set to 0. + + // Bytes 1b + uint8_t scan_list_index; ///< Scan list index, 0xff=None, 0-based. + uint8_t group_list_index; ///< RX group-list, 0xff=None, 0-based. + uint8_t id_2tone; ///< 2-Tone ID, 0=1, 0x17=24. + uint8_t id_5tone; ///< 5-Tone ID, 0=1, 0x63=100. + uint8_t id_dtmf; ///< DTMF ID, 0=1, 0x0f=16. + + // Byte 20 + uint8_t color_code; ///< Color code, 0-15 + + // Byte 21 + uint8_t slot2 : 1, ///< Timeslot, 0=TS1, 1=TS2. + sms_confirm : 1, ///< Send SMS confirmation, 0=off, 1=on. + simplex_tdma : 1, ///< Simplex TDMA enabled. + _unused21_2 : 1, ///< Unused, set to 0. + tdma_adaptive : 1, ///< TDMA adaptive enable. + rx_gps : 1, ///< Receive digital GPS messages. + enh_encryption : 1, ///< Enable enhanced encryption. + work_alone : 1; ///< Work alone, 0=off, 1=on. + + // Byte 22 + uint8_t aes_encryption; ///< Digital AES encryption, 1-32, 0=off. + + // Bytes 23 + uint8_t name[16]; ///< Channel name, ASCII, zero filled. + uint8_t _pad33; ///< Pad byte, set to 0. + + // Byte 34 + uint8_t ranging : 1, ///< Ranging enabled. + through_mode : 1, ///< Through-mode enabled. + bt_hands_free : 1, ///< Bluetooth hands free enabled. + excl_from_roaming : 1, ///< Exclude channel from roaming, data ACK forbit in D868UV. + _unused34_4 : 4; ///< Unused, set to 0. + + // Byte 35 + uint8_t aprs_report : 2, ///< Enable APRS report, see @c APRSReport. + _unused35 : 6; ///< Unused, set to 0. + + // Bytes 36 + uint8_t analog_aprs_ptt; ///< Enable analog APRS PTT, see @c APRSPTT, not used in D868UV. + uint8_t digi_aprs_ptt; ///< Enable digital APRS PTT, 0=off, 1=on. + uint8_t gps_system; ///< Index of DMR GPS report system, 0-7; + int8_t freq_correction; ///< Signed int in 10Hz. + uint8_t scambler; ///< Analog scambler, 0=off. + uint8_t multiple_keys : 1, ///< Enable multiple keys. + random_key : 1, ///< Enable random key. + sms_forbid : 1, ///< Forbit SMS tramsission. + _unused3b_3 : 5; ///< Unused, set to 0. + uint8_t _unused3c; ///< Unused, set to 0. + uint8_t _unused3d_0 : 3, ///< Unused, set to 0. + data_ack_disable : 1, ///< Disable data ACK. + _unused3d_4 : 4; ///< Unused, set to 0. + uint8_t _unused3e; ///< Unused, set to 0. + uint8_t _unused3f; ///< Unused, set to 0. + + /** Constructor, also clears the struct. */ + channel_t(); + + /** Clears and invalidates the channel. */ + void clear(); + + /** Returns @c true if the channel is valid. */ + bool isValid() const; + + /** Returns the RX frequency in MHz. */ + double getRXFrequency() const; + /** Sets the RX frequency in MHz. */ + void setRXFrequency(double f); + + /** Returns the TX frequency in MHz. */ + double getTXFrequency() const; + /** Sets the TX frequency in MHz. + * @note As the TX frequency is stored as difference to the RX frequency, the RX frequency + * should be set first. */ + void setTXFrequency(double f); + + /** Returns the name of the radio. */ + QString getName() const; + /** Sets the name of the radio. */ + void setName(const QString &name); + + /** Returns the RX CTCSS/DCS tone. */ + Signaling::Code getRXTone() const; + /** Sets the RX CTCSS/DCS tone. */ + void setRXTone(Signaling::Code code); + /** Returns the TX CTCSS/DCS tone. */ + Signaling::Code getTXTone() const; + /** Sets the TX CTCSS/DCS tone. */ + void setTXTone(Signaling::Code code); + + /** Constructs a generic @c Channel object from the codeplug channel. */ + Channel *toChannelObj() const; + /** Links a previously constructed channel to the rest of the configuration. */ + bool linkChannelObj(Channel *c, const CodeplugContext &ctx) const; + /** Initializes this codeplug channel from the given generic configuration. */ + void fromChannelObj(const Channel *c, const Config *conf); + }; + /** Represents an APRS RX entry. */ struct __attribute__((packed)) aprs_rx_entry_t { @@ -220,6 +448,9 @@ public: explicit D578UVCodeplug(QObject *parent = nullptr); void allocateUpdated(); + + bool encodeChannels(Config *config, const Flags &flags); + void allocateContacts(); bool encodeContacts(Config *config, const Flags &flags); }; diff --git a/lib/d868uv_callsigndb.hh b/lib/d868uv_callsigndb.hh index 68d1483a..674a3b1e 100644 --- a/lib/d868uv_callsigndb.hh +++ b/lib/d868uv_callsigndb.hh @@ -10,7 +10,7 @@ * Callsign database * Start Size Content * 04000000 max. 186a00 Index of callsign entries. Follows the same - * weird format as @c D878UVCodeplug::contact_map_t. Sorted by ID. Empty entries set to + * weird format as @c D868UVCodeplug::contact_map_t. Sorted by ID. Empty entries set to * 0xffffffffffffffff. * 044c0000 unknown Database limits, see @c limits_t. * 04500000 unknown The actual DB entries, each entry is of diff --git a/lib/d878uv2_callsigndb.hh b/lib/d878uv2_callsigndb.hh index 41b5a484..8fa1ac63 100644 --- a/lib/d878uv2_callsigndb.hh +++ b/lib/d878uv2_callsigndb.hh @@ -9,7 +9,7 @@ * Callsign database * Start Size Content * 04000000 variable Index of callsign entries. Follows the same - * weird format as @c D878UVCodeplug::contact_map_t. Sorted by ID. Empty entries set to + * weird format as @c D868UVCodeplug::contact_map_t. Sorted by ID. Empty entries set to * 0xffffffffffffffff. * 04840000 000010 Database limits, see @c limits_t. * 05500000 variable The actual DB entries, each entry is of diff --git a/lib/d878uv_codeplug.hh b/lib/d878uv_codeplug.hh index 908b7b56..5c1a2155 100644 --- a/lib/d878uv_codeplug.hh +++ b/lib/d878uv_codeplug.hh @@ -278,18 +278,18 @@ public: } APRSPTT; - // Bytes 0-7 + // Bytes 00 uint32_t rx_frequency; ///< RX Frequency, 8 digits BCD, big-endian. uint32_t tx_offset; ///< TX Offset, 8 digits BCD, big-endian, sign in repeater_mode. - // Byte 8 + // Byte 08 uint8_t channel_mode : 2, ///< Mode: Analog or Digital, see @c Mode. power : 2, ///< Power: Low, Middle, High, Turbo, see @c Power. bandwidth : 1, ///< Bandwidth: 12.5 or 25 kHz, see @c Bandwidth. _unused8 : 1, ///< Unused, set to 0. repeater_mode : 2; ///< Sign of TX frequency offset, see @c RepeaterMode. - // Byte 9 + // Byte 09 uint8_t rx_ctcss : 1, ///< CTCSS decode enable. rx_dcs : 1, ///< DCS decode enable. tx_ctcss : 1, ///< CTCSS encode enable. @@ -299,46 +299,46 @@ public: call_confirm : 1, ///< Call confirmation enable. talkaround : 1; ///< Talk-around enable. - // Bytes 10-15 + // Bytes 0a uint8_t ctcss_transmit; ///< TX CTCSS tone, 0=62.5, 50=254.1, 51=custom CTCSS tone. uint8_t ctcss_receive; ///< RX CTCSS tone: 0=62.5, 50=254.1, 51=custom CTCSS tone. uint16_t dcs_transmit; ///< TX DCS code: 0=D000N, 511=D777N, 512=D000I, 1023=D777I, DCS code-number in octal, little-endian. uint16_t dcs_receive; ///< RX DCS code: 0=D000N, 511=D777N, 512=D000I, 1023=D777I, DCS code-number in octal, little-endian. - // Bytes 16-19 + // Bytes 10 uint16_t custom_ctcss; ///< Custom CTCSS tone frequency: 0x09cf=251.1, 0x0a28=260, big-endian. uint8_t tone2_decode; ///< 2-Tone decode: 0x00=1, 0x0f=16 uint8_t _unused19; ///< Unused, set to 0. - // Bytes 20-23 + // Bytes 14 uint32_t contact_index; ///< Contact index, zero-based, little-endian. - // Byte 24 + // Byte 18 uint8_t id_index; ///< Index to radio ID table. - // Byte 25 + // Byte 19 uint8_t ptt_id : 2, ///< PTT ID, see PTTId, unused in U868UV. _unused25_1 : 2, ///< Unused, set to 0. squelch_mode : 1, ///< Squelch mode, see @c SquelchMode. _unused25_2 : 3; ///< Unused, set to 0. - // Byte 26 + // Byte 1a uint8_t tx_permit : 2, ///< TX permit, see @c Admit. _unused26_1 : 2, ///< Unused, set to 0. opt_signal : 2, ///< Optional signaling, see @c OptSignaling. _unused26_2 : 2; ///< Unused, set to 0. - // Bytes 27-31 + // Bytes 1b uint8_t scan_list_index; ///< Scan list index, 0xff=None, 0-based. uint8_t group_list_index; ///< RX group-list, 0xff=None, 0-based. uint8_t id_2tone; ///< 2-Tone ID, 0=1, 0x17=24. uint8_t id_5tone; ///< 5-Tone ID, 0=1, 0x63=100. uint8_t id_dtmf; ///< DTMF ID, 0=1, 0x0f=16. - // Byte 32 + // Byte 20 uint8_t color_code; ///< Color code, 0-15 - // Byte 33 + // Byte 21 uint8_t slot2 : 1, ///< Timeslot, 0=TS1, 1=TS2. sms_confirm : 1, ///< Send SMS confirmation, 0=off, 1=on. simplex_tdma : 1, ///< Simplex TDMA enabled. @@ -348,25 +348,25 @@ public: enh_encryption : 1, ///< Enable enhanced encryption. work_alone : 1; ///< Work alone, 0=off, 1=on. - // Byte 34 + // Byte 22 uint8_t aes_encryption; ///< Digital AES encryption, 1-32, 0=off. - // Bytes 35-51 + // Bytes 23 uint8_t name[16]; ///< Channel name, ASCII, zero filled. uint8_t _pad51; ///< Pad byte, set to 0. - // Byte 52 + // Byte 34 uint8_t ranging : 1, ///< Ranging enabled. through_mode : 1, ///< Through-mode enabled. excl_from_roaming : 1, ///< Exclude channel from roaming, data ACK forbit in D868UV. data_ack_disable : 1, ///< Data ACK disable. _unused52_4 : 4; ///< Unused, set to 0. - // Byte 53 + // Byte 35 uint8_t aprs_report : 2, ///< Enable APRS report, see @c APRSReport. _unused53 : 6; ///< Unused, set to 0. - // Bytes 54-63 + // Bytes 36 uint8_t analog_aprs_ptt; ///< Enable analog APRS PTT, see @c APRSPTT, not used in D868UV. uint8_t digi_aprs_ptt; ///< Enable digital APRS PTT, 0=off, 1=on. uint8_t gps_system; ///< Index of DMR GPS report system, 0-7; From 53f6062cda501f239d06d2db613586084040424e Mon Sep 17 00:00:00 2001 From: Hannes Matuschek Date: Wed, 30 Jun 2021 17:41:34 +0200 Subject: [PATCH 06/16] Fixed hot-key encoding for AT-D578UV. --- lib/d578uv_codeplug.cc | 7 +++++++ lib/d578uv_codeplug.hh | 1 + 2 files changed, 8 insertions(+) diff --git a/lib/d578uv_codeplug.cc b/lib/d578uv_codeplug.cc index 1697fdd7..b5a50a00 100644 --- a/lib/d578uv_codeplug.cc +++ b/lib/d578uv_codeplug.cc @@ -45,6 +45,9 @@ static_assert( APRS_SET_EXT_SIZE == sizeof(D578UVCodeplug::aprs_setting_ext_t), "D578UVCodeplug::aprs_setting_ext_t size check failed."); +#define ADDR_HOTKEY 0x025C0000 +#define HOTKEY_SIZE 0x00000970 + #define ADDR_UNKNOWN_SETTING 0x02500600 // Address of unknown settings #define UNKNOWN_SETTING_SIZE 0x00000030 // Size of unknown settings. @@ -463,6 +466,10 @@ D578UVCodeplug::allocateUpdated() { image(0).addElement(ADDR_APRS_SET_EXT, APRS_SET_EXT_SIZE); } +void +D578UVCodeplug::allocateHotKeySettings() { + image(0).addElement(ADDR_HOTKEY, HOTKEY_SIZE); +} bool D578UVCodeplug::encodeChannels(Config *config, const Flags &flags) { diff --git a/lib/d578uv_codeplug.hh b/lib/d578uv_codeplug.hh index 68f9df22..f23ab56f 100644 --- a/lib/d578uv_codeplug.hh +++ b/lib/d578uv_codeplug.hh @@ -448,6 +448,7 @@ public: explicit D578UVCodeplug(QObject *parent = nullptr); void allocateUpdated(); + void allocateHotKeySettings(); bool encodeChannels(Config *config, const Flags &flags); From d4df63593bae0dc919b9badae97b4bd8f6bbcab9 Mon Sep 17 00:00:00 2001 From: Hannes Matuschek Date: Wed, 30 Jun 2021 18:07:36 +0200 Subject: [PATCH 07/16] Enable Bluetooth hands free by default for D578UV. --- lib/d578uv_codeplug.cc | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/lib/d578uv_codeplug.cc b/lib/d578uv_codeplug.cc index b5a50a00..dfbf2c88 100644 --- a/lib/d578uv_codeplug.cc +++ b/lib/d578uv_codeplug.cc @@ -77,7 +77,8 @@ D578UVCodeplug::channel_t::clear() { id_index = 0; squelch_mode = SQ_CARRIER; tx_permit = ADMIT_ALWAYS; - + // Enable BT hands free by default. + bt_hands_free = 1; } bool From 5838a0c50b18d1a5026f07525d7d40d9348e1188 Mon Sep 17 00:00:00 2001 From: Hannes Matuschek Date: Wed, 30 Jun 2021 18:27:22 +0200 Subject: [PATCH 08/16] Implemented firmware version check. --- lib/anytone_radio.hh | 2 ++ lib/d578uv.cc | 21 +++++++++++++++++++++ lib/d578uv.hh | 3 +++ lib/d868uv.cc | 23 +++++++++++++++++++++++ lib/d868uv.hh | 3 +++ lib/d878uv.cc | 21 +++++++++++++++++++++ lib/d878uv.hh | 3 +++ lib/d878uv2.cc | 20 ++++++++++++++++++++ lib/d878uv2.hh | 3 +++ lib/radio.hh | 6 +++--- 10 files changed, 102 insertions(+), 3 deletions(-) diff --git a/lib/anytone_radio.hh b/lib/anytone_radio.hh index 002a9e8a..1ec46ad0 100644 --- a/lib/anytone_radio.hh +++ b/lib/anytone_radio.hh @@ -87,6 +87,8 @@ protected: AnytoneCodeplug *_codeplug; /** The actual binary callsign database representation. */ CallsignDB *_callsigns; + /** Holds the hardware version of the radio. Used for codeplug compatibility. */ + QString _version; }; #endif // __D868UV_HH__ diff --git a/lib/d578uv.cc b/lib/d578uv.cc index 4d966abc..943f030e 100644 --- a/lib/d578uv.cc +++ b/lib/d578uv.cc @@ -147,3 +147,24 @@ const Radio::Features & D578UV::features() const { return _features; } + +VerifyIssue::Type +D578UV::verifyConfig(Config *config, QList &issues, const VerifyFlags &flags) { + QString supported = "V110"; + VerifyIssue::Type issue = AnytoneRadio::verifyConfig(config, issues, flags); + if (supported < _version) { + issues.append(VerifyIssue( + VerifyIssue::WARNING, + tr("You are likely running a newer firmware version (%1) than supported (%2) by qdmr. " + "Notify the developers of qdmr about the new firmware version.").arg(_version, supported))); + issue = std::max(issue, VerifyIssue::WARNING); + } else if (supported > _version) { + issues.append(VerifyIssue( + VerifyIssue::WARNING, + tr("You are likely running an older firmware version (%1) than supported (%2) by qdmr. " + "Condsider updating your firmware.").arg(_version, supported))); + issue = std::max(issue, VerifyIssue::WARNING); + } + return issue; +} + diff --git a/lib/d578uv.hh b/lib/d578uv.hh index b2975eae..e9e3000b 100644 --- a/lib/d578uv.hh +++ b/lib/d578uv.hh @@ -25,6 +25,9 @@ public: const Radio::Features &features() const; + VerifyIssue::Type verifyConfig(Config *config, QList &issues, + const VerifyFlags &flags=VerifyFlags()); + protected: /** Holds a copy of the specific radio features. */ Radio::Features _features; diff --git a/lib/d868uv.cc b/lib/d868uv.cc index ca029951..d527be7b 100644 --- a/lib/d868uv.cc +++ b/lib/d868uv.cc @@ -131,9 +131,32 @@ D868UV::D868UV(AnytoneInterface *device, QObject *parent) } logDebug() << "Got band-code " << QString::number(int(info.bands), 16) << ": Limit TX frequencies to " << bands.join(", ") << "."; + + // Store HW version + _version = info.version; } const Radio::Features & D868UV::features() const { return _features; } + +VerifyIssue::Type +D868UV::verifyConfig(Config *config, QList &issues, const VerifyFlags &flags) { + QString supported = "V102"; + VerifyIssue::Type issue = AnytoneRadio::verifyConfig(config, issues, flags); + if (supported < _version) { + issues.append(VerifyIssue( + VerifyIssue::WARNING, + tr("You are likely running a newer firmware version (%1) than supported (%2) by qdmr. " + "Notify the developers of qdmr about the new firmware version.").arg(_version, supported))); + issue = std::max(issue, VerifyIssue::WARNING); + } else if (supported > _version) { + issues.append(VerifyIssue( + VerifyIssue::WARNING, + tr("You are likely running an older firmware version (%1) than supported (%2) by qdmr. " + "Condsider updating your firmware.").arg(_version, supported))); + issue = std::max(issue, VerifyIssue::WARNING); + } + return issue; +} diff --git a/lib/d868uv.hh b/lib/d868uv.hh index 2f08748c..2622a099 100644 --- a/lib/d868uv.hh +++ b/lib/d868uv.hh @@ -42,6 +42,9 @@ public: const Radio::Features &features() const; + VerifyIssue::Type verifyConfig(Config *config, QList &issues, + const VerifyFlags &flags=VerifyFlags()); + protected: /** Features of detected radio, variant and mode. */ Radio::Features _features; diff --git a/lib/d878uv.cc b/lib/d878uv.cc index df214261..fd5a1597 100644 --- a/lib/d878uv.cc +++ b/lib/d878uv.cc @@ -147,3 +147,24 @@ const Radio::Features & D878UV::features() const { return _features; } + + +VerifyIssue::Type +D878UV::verifyConfig(Config *config, QList &issues, const VerifyFlags &flags) { + QString supported = "V100"; + VerifyIssue::Type issue = AnytoneRadio::verifyConfig(config, issues, flags); + if (supported < _version) { + issues.append(VerifyIssue( + VerifyIssue::WARNING, + tr("You are likely running a newer firmware version (%1) than supported (%2) by qdmr. " + "Notify the developers of qdmr about the new firmware version.").arg(_version, supported))); + issue = std::max(issue, VerifyIssue::WARNING); + } else if (supported > _version) { + issues.append(VerifyIssue( + VerifyIssue::WARNING, + tr("You are likely running an older firmware version (%1) than supported (%2) by qdmr. " + "Condsider updating your firmware.").arg(_version, supported))); + issue = std::max(issue, VerifyIssue::WARNING); + } + return issue; +} diff --git a/lib/d878uv.hh b/lib/d878uv.hh index 5af4ad12..9048cc13 100644 --- a/lib/d878uv.hh +++ b/lib/d878uv.hh @@ -42,6 +42,9 @@ public: const Radio::Features &features() const; + VerifyIssue::Type verifyConfig(Config *config, QList &issues, + const VerifyFlags &flags=VerifyFlags()); + protected: /** Holds a copy of the specific radio features. */ Radio::Features _features; diff --git a/lib/d878uv2.cc b/lib/d878uv2.cc index 3c563f7a..d4ea3537 100644 --- a/lib/d878uv2.cc +++ b/lib/d878uv2.cc @@ -146,3 +146,23 @@ const Radio::Features & D878UV2::features() const { return _features; } + +VerifyIssue::Type +D878UV2::verifyConfig(Config *config, QList &issues, const VerifyFlags &flags) { + QString supported = "V100"; + VerifyIssue::Type issue = AnytoneRadio::verifyConfig(config, issues, flags); + if (supported < _version) { + issues.append(VerifyIssue( + VerifyIssue::WARNING, + tr("You are likely running a newer firmware version (%1) than supported (%2) by qdmr. " + "Notify the developers of qdmr about the new firmware version.").arg(_version, supported))); + issue = std::max(issue, VerifyIssue::WARNING); + } else if (supported > _version) { + issues.append(VerifyIssue( + VerifyIssue::WARNING, + tr("You are likely running an older firmware version (%1) than supported (%2) by qdmr. " + "Condsider updating your firmware.").arg(_version, supported))); + issue = std::max(issue, VerifyIssue::WARNING); + } + return issue; +} diff --git a/lib/d878uv2.hh b/lib/d878uv2.hh index db351edd..150b5bc2 100644 --- a/lib/d878uv2.hh +++ b/lib/d878uv2.hh @@ -42,6 +42,9 @@ public: const Radio::Features &features() const; + VerifyIssue::Type verifyConfig(Config *config, QList &issues, + const VerifyFlags &flags=VerifyFlags()); + protected: /** Holds a copy of the specific radio features. */ Radio::Features _features; diff --git a/lib/radio.hh b/lib/radio.hh index 2f110da8..634dfdaa 100644 --- a/lib/radio.hh +++ b/lib/radio.hh @@ -27,7 +27,7 @@ class VerifyIssue { public: /** Issue type. */ typedef enum { - NONE, ///< All ok. + NONE = 0, ///< All ok. NOTIFICATION, ///< Inform user about changes made to the config to fit radio. WARNING, ///< Verification warning, some configured fature is just ignored for the particular radio. ERROR ///< Verification error, a consistent device specific configutation cannot be derived from the generic config. @@ -220,8 +220,8 @@ public: /** Verifies the configuration against the radio features. * On exit, @c issues will contain the issues found and the maximum severity is returned. */ - VerifyIssue::Type verifyConfig(Config *config, QList &issues, - const VerifyFlags &flags=VerifyFlags()); + virtual VerifyIssue::Type verifyConfig(Config *config, QList &issues, + const VerifyFlags &flags=VerifyFlags()); /** Returns the current status. */ Status status() const; From 86efb7150ac4997dd8534fa3e4f32e85ce1e15d9 Mon Sep 17 00:00:00 2001 From: Hannes Matuschek Date: Fri, 2 Jul 2021 11:32:36 +0200 Subject: [PATCH 09/16] Update of D878UV codeplug to CPS 1.23, cleanup of CP for D878UVII, D578UV. --- lib/d578uv_codeplug.cc | 47 +++++++++++------------------------------ lib/d578uv_codeplug.hh | 33 ++--------------------------- lib/d878uv2_codeplug.cc | 30 -------------------------- lib/d878uv2_codeplug.hh | 35 +----------------------------- lib/d878uv_codeplug.cc | 22 ++++++++++++++++++- lib/d878uv_codeplug.hh | 42 +++++++++++++++++++++++++++++++++++- 6 files changed, 77 insertions(+), 132 deletions(-) diff --git a/lib/d578uv_codeplug.cc b/lib/d578uv_codeplug.cc index dfbf2c88..2107c9e4 100644 --- a/lib/d578uv_codeplug.cc +++ b/lib/d578uv_codeplug.cc @@ -32,33 +32,15 @@ #define CONTACT_ID_MAP 0x04800000 // Address of ID->Contact index map #define CONTACT_ID_ENTRY_SIZE sizeof(contact_map_t) // Size of each map entry -#define NUM_APRS_RX_ENTRY 32 -#define ADDR_APRS_RX_ENTRY 0x02501800 // Address of APRS RX list -#define APRS_RX_ENTRY_SIZE 0x00000008 // Size of each APRS RX entry -static_assert( - APRS_RX_ENTRY_SIZE == sizeof(D578UVCodeplug::aprs_rx_entry_t), - "D578UVCodeplug::aprs_rx_entry_t size check failed."); - -#define ADDR_APRS_SET_EXT 0x025010A0 // Address of APRS settings extension -#define APRS_SET_EXT_SIZE 0x00000060 // Size of APRS settings extension -static_assert( - APRS_SET_EXT_SIZE == sizeof(D578UVCodeplug::aprs_setting_ext_t), - "D578UVCodeplug::aprs_setting_ext_t size check failed."); - -#define ADDR_HOTKEY 0x025C0000 -#define HOTKEY_SIZE 0x00000970 - -#define ADDR_UNKNOWN_SETTING 0x02500600 // Address of unknown settings -#define UNKNOWN_SETTING_SIZE 0x00000030 // Size of unknown settings. - -#define ADDR_UNKNOWN_SETTING_2 0x02BC0000 // Address of unknown settings -#define UNKNOWN_SETTING_2_SIZE 0x00000020 // Size of unknown settings. - -#define ADDR_UNKNOWN_SETTING_3 0x02BC0C60 // Address of unknown settings -#define UNKNOWN_SETTING_3_SIZE 0x00000020 // Size of unknown settings. +#define ADDR_HOTKEY 0x025C0000 // Same address as D868UV::hotkey_settings_t +#define HOTKEY_SIZE 0x00000970 // Different size. -#define ADDR_UNKNOWN_SETTING_4 0x02BC1000 // Address of unknown settings -#define UNKNOWN_SETTING_4_SIZE 0x00000060 // Size of unknown settings. +#define ADDR_UNKNOWN_SETTING_1 0x02BC0000 // Address of unknown settings +#define UNKNOWN_SETTING_1_SIZE 0x00000020 // Size of unknown settings. +#define ADDR_UNKNOWN_SETTING_2 0x02BC0C60 // Address of unknown settings +#define UNKNOWN_SETTING_2_SIZE 0x00000020 // Size of unknown settings. +#define ADDR_UNKNOWN_SETTING_3 0x02BC1000 // Address of unknown settings +#define UNKNOWN_SETTING_3_SIZE 0x00000060 // Size of unknown settings. /* ******************************************************************************************** * @@ -455,16 +437,11 @@ D578UVCodeplug::D578UVCodeplug(QObject *parent) void D578UVCodeplug::allocateUpdated() { - // allocate everything from D878UV codeplug - D878UVCodeplug::allocateUpdated(); - - // allocate unknown settings - image(0).addElement(ADDR_UNKNOWN_SETTING, UNKNOWN_SETTING_SIZE); + D868UVCodeplug::allocateUpdated(); - // allocate APRS RX list - image(0).addElement(ADDR_APRS_RX_ENTRY, NUM_APRS_RX_ENTRY*APRS_RX_ENTRY_SIZE); - // allocate APRS settings extension - image(0).addElement(ADDR_APRS_SET_EXT, APRS_SET_EXT_SIZE); + image(0).addElement(ADDR_UNKNOWN_SETTING_1, UNKNOWN_SETTING_1_SIZE); + image(0).addElement(ADDR_UNKNOWN_SETTING_2, UNKNOWN_SETTING_2_SIZE); + image(0).addElement(ADDR_UNKNOWN_SETTING_3, UNKNOWN_SETTING_3_SIZE); } void diff --git a/lib/d578uv_codeplug.hh b/lib/d578uv_codeplug.hh index f23ab56f..3efc0a6b 100644 --- a/lib/d578uv_codeplug.hh +++ b/lib/d578uv_codeplug.hh @@ -112,6 +112,7 @@ class GPSSystem; * 02500000 000100 General settings, see @c D878UVCodeplug::general_settings_base_t. * 02500100 000400 Zone A & B channel list. * 02500500 000100 DTMF list + * 02500600 000030 Power on settings * 02501280 000030 General settings extension 1, see @c D878UVCodeplug::general_settings_ext1_t. * 02501400 000100 General settings extension 2, see @c D878UVCodeplug::general_settings_ext2_t. * 024C2000 0003F0 List of 250 auto-repeater offset frequencies. @@ -173,7 +174,6 @@ class GPSSystem; * 024C1440 000030 Unknown data. * 024C1700 000040 Unknown, 8bit indices. * 024C1800 000500 Empty, set to 0x00? - * 02500600 000030 Unknown, set to 0x00. * 02BC0000 000020 Unknown, set to 0x00. * 02BC0C60 000020 Unknown, set to 0x00. * 02BC1000 000060 Unknown, set to 0x00. @@ -413,41 +413,12 @@ public: void fromChannelObj(const Channel *c, const Config *conf); }; - /** Represents an APRS RX entry. - */ - struct __attribute__((packed)) aprs_rx_entry_t { - uint8_t enabled; ///< Enabled entry 0x01=on, 0x00=off. - char call[6]; ///< Callsign, 6x ASCII, 0-terminated. - uint8_t ssid; ///< SSID [0,15], 16=off. - }; - - /** Represents an extension to the APRS settings. */ - struct __attribute__((packed)) aprs_setting_ext_t { - uint8_t _unknown0000[8]; ///< Unknown settings block. - uint8_t rep_position : 1, ///< Report position flag. - rep_mic_e : 1, ///< Report MIC-E flag. - rep_object : 1, ///< Report object flag. - rep_item : 1, ///< Report item flag. - rep_message : 1, ///< Report message flag. - rep_wx : 1, ///< WX report flag. - rep_nmea : 1, ///< NMEA report flag. - rep_status : 1; ///< Report status flag. - uint8_t rep_other : 1, ///< Report "other" flag. - _unused0009_1 :7; ///< Unused set to 0. - uint8_t _unknown000a[6]; ///< Unknown settings block. - - uint8_t _unknown0010[16]; ///< Unknown settings block. - uint8_t _unknown0020[16]; ///< Unknown settings block. - uint8_t _unknown0030[16]; ///< Unknown settings block. - uint8_t _unknown0040[16]; ///< Unknown settings block. - uint8_t _unknown0050[16]; ///< Unknown settings block. - }; - public: /** Empty constructor. */ explicit D578UVCodeplug(QObject *parent = nullptr); void allocateUpdated(); + void allocateHotKeySettings(); bool encodeChannels(Config *config, const Flags &flags); diff --git a/lib/d878uv2_codeplug.cc b/lib/d878uv2_codeplug.cc index 4a49d561..f19cf97b 100644 --- a/lib/d878uv2_codeplug.cc +++ b/lib/d878uv2_codeplug.cc @@ -21,22 +21,6 @@ #define CONTACT_ID_MAP 0x04800000 // Address of ID->Contact index map #define CONTACT_ID_ENTRY_SIZE sizeof(contact_map_t) // Size of each map entry -#define NUM_APRS_RX_ENTRY 32 -#define ADDR_APRS_RX_ENTRY 0x02501800 // Address of APRS RX list -#define APRS_RX_ENTRY_SIZE 0x00000008 // Size of each APRS RX entry -static_assert( - APRS_RX_ENTRY_SIZE == sizeof(D878UV2Codeplug::aprs_rx_entry_t), - "D878UV2Codeplug::aprs_rx_entry_t size check failed."); - -#define ADDR_APRS_SET_EXT 0x025010A0 // Address of APRS settings extension -#define APRS_SET_EXT_SIZE 0x00000060 // Size of APRS settings extension -static_assert( - APRS_SET_EXT_SIZE == sizeof(D878UV2Codeplug::aprs_setting_ext_t), - "D878UV2Codeplug::aprs_setting_ext_t size check failed."); - -#define ADDR_UNKNOWN_SETTING 0x02500600 // Address of unknown settings -#define UNKNOWN_SETTING_SIZE 0x00000030 // Size of unknown settings. - /* ******************************************************************************************** * @@ -48,20 +32,6 @@ D878UV2Codeplug::D878UV2Codeplug(QObject *parent) // pass... } -void -D878UV2Codeplug::allocateUpdated() { - // allocate everything from D878UV codeplug - D878UVCodeplug::allocateUpdated(); - - // allocate unknown settings - image(0).addElement(ADDR_UNKNOWN_SETTING, UNKNOWN_SETTING_SIZE); - - // allocate APRS RX list - image(0).addElement(ADDR_APRS_RX_ENTRY, NUM_APRS_RX_ENTRY*APRS_RX_ENTRY_SIZE); - // allocate APRS settings extension - image(0).addElement(ADDR_APRS_SET_EXT, APRS_SET_EXT_SIZE); -} - void D878UV2Codeplug::allocateContacts() { diff --git a/lib/d878uv2_codeplug.hh b/lib/d878uv2_codeplug.hh index a54f4f56..03beffc2 100644 --- a/lib/d878uv2_codeplug.hh +++ b/lib/d878uv2_codeplug.hh @@ -112,6 +112,7 @@ class GPSSystem; * 02500000 000100 General settings, see @c D878UVCodeplug::general_settings_base_t. * 02500100 000400 Zone A & B channel list. * 02500500 000100 DTMF list + * 02500600 000030 Power on settings * 02501280 000030 General settings extension 1, see @c D878UVCodeplug::general_settings_ext1_t. * 02501400 000100 General settings extension 2, see @c D878UVCodeplug::general_settings_ext2_t. * 024C2000 0003F0 List of 250 auto-repeater offset frequencies. @@ -172,8 +173,6 @@ class GPSSystem; * 024C1440 000030 Unknown data. * 024C1700 000040 Unknown, 8bit indices. * 024C1800 000500 Empty, set to 0x00? - * - * 02500600 000030 Unknown, set to 0x00. * * * @ingroup d878uv2 */ @@ -181,42 +180,10 @@ class D878UV2Codeplug : public D878UVCodeplug { Q_OBJECT -public: - /** Represents an APRS RX entry. - */ - struct __attribute__((packed)) aprs_rx_entry_t { - uint8_t enabled; ///< Enabled entry 0x01=on, 0x00=off. - char call[6]; ///< Callsign, 6x ASCII, 0-terminated. - uint8_t ssid; ///< SSID [0,15], 16=off. - }; - - /** Represents an extension to the APRS settings. */ - struct __attribute__((packed)) aprs_setting_ext_t { - uint8_t _unknown0000[8]; ///< Unknown settings block. - uint8_t rep_position : 1, ///< Report position flag. - rep_mic_e : 1, ///< Report MIC-E flag. - rep_object : 1, ///< Report object flag. - rep_item : 1, ///< Report item flag. - rep_message : 1, ///< Report message flag. - rep_wx : 1, ///< WX report flag. - rep_nmea : 1, ///< NMEA report flag. - rep_status : 1; ///< Report status flag. - uint8_t rep_other : 1, ///< Report "other" flag. - _unused0009_1 :7; ///< Unused set to 0. - uint8_t _unknown000a[6]; ///< Unknown settings block. - - uint8_t _unknown0010[16]; ///< Unknown settings block. - uint8_t _unknown0020[16]; ///< Unknown settings block. - uint8_t _unknown0030[16]; ///< Unknown settings block. - uint8_t _unknown0040[16]; ///< Unknown settings block. - uint8_t _unknown0050[16]; ///< Unknown settings block. - }; - public: /** Empty constructor. */ explicit D878UV2Codeplug(QObject *parent = nullptr); - void allocateUpdated(); void allocateContacts(); bool encodeContacts(Config *config, const Flags &flags); }; diff --git a/lib/d878uv_codeplug.cc b/lib/d878uv_codeplug.cc index 4be5b3f6..ff5fc395 100644 --- a/lib/d878uv_codeplug.cc +++ b/lib/d878uv_codeplug.cc @@ -40,12 +40,26 @@ static_assert( #define ADDR_APRS_SETTING 0x02501000 // Address of APRS settings #define APRS_SETTING_SIZE 0x00000040 // Size of the APRS settings + +#define ADDR_APRS_SET_EXT 0x025010A0 // Address of APRS settings extension +#define APRS_SET_EXT_SIZE 0x00000060 // Size of APRS settings extension +static_assert( + APRS_SET_EXT_SIZE == sizeof(D878UVCodeplug::aprs_setting_ext_t), + "D878UVCodeplug::aprs_setting_ext_t size check failed."); + #define ADDR_APRS_MESSAGE 0x02501200 // Address of APRS messages #define APRS_MESSAGE_SIZE 0x00000040 // Size of APRS messages static_assert( APRS_SETTING_SIZE == sizeof(D878UVCodeplug::aprs_setting_t), "D878UVCodeplug::aprs_setting_t size check failed."); +#define NUM_APRS_RX_ENTRY 32 +#define ADDR_APRS_RX_ENTRY 0x02501800 // Address of APRS RX list +#define APRS_RX_ENTRY_SIZE 0x00000008 // Size of each APRS RX entry +static_assert( + APRS_RX_ENTRY_SIZE == sizeof(D878UVCodeplug::aprs_rx_entry_t), + "D878UVCodeplug::aprs_rx_entry_t size check failed."); + #define NUM_GPS_SYSTEMS 8 #define ADDR_GPS_SETTING 0x02501040 // Address of GPS settings #define GPS_SETTING_SIZE 0x00000060 // Size of the GPS settings @@ -74,7 +88,6 @@ static_assert( #define ENCRYPTION_KEYS_SIZE 0x00004000 - /* ******************************************************************************************** * * Implementation of D878UVCodeplug::channel_t * ******************************************************************************************** */ @@ -1004,8 +1017,15 @@ void D878UVCodeplug::allocateUpdated() { // First allocate everything common between D868UV and D878UV codeplugs. D868UVCodeplug::allocateUpdated(); + // Encryption keys image(0).addElement(ADDR_ENCRYPTION_KEYS, ENCRYPTION_KEYS_SIZE); + + // allocate APRS settings extension + image(0).addElement(ADDR_APRS_SET_EXT, APRS_SET_EXT_SIZE); + + // allocate APRS RX list + image(0).addElement(ADDR_APRS_RX_ENTRY, NUM_APRS_RX_ENTRY*APRS_RX_ENTRY_SIZE); } void diff --git a/lib/d878uv_codeplug.hh b/lib/d878uv_codeplug.hh index 5c1a2155..a0747c37 100644 --- a/lib/d878uv_codeplug.hh +++ b/lib/d878uv_codeplug.hh @@ -123,13 +123,17 @@ class GPSSystem; * Start Size Content * 02501000 000040 APRS settings, see @c aprs_setting_t. * 02501040 000060 APRS settings, see @c gps_systems_t. + * 025010A0 000060 Extended APRS settings, see @c aprs_setting_ext_t. * 02501200 000040 APRS Text, upto 60 chars ASCII, 0-padded. + * 02501800 000100 APRS-RX settings list up to 32 entries, 8b each. + * See @c aprs_rx_entry_t. * * General Settings * Start Size Content * 02500000 000100 General settings, see @c general_settings_base_t. * 02500100 000400 Zone A & B channel list. * 02500500 000100 DTMF list + * 02500600 000030 Power on settings * 02501280 000030 General settings extension 1, see @c general_settings_ext1_t. * 02501400 000100 General settings extension 2, see @c general_settings_ext2_t. * 024C2000 0003F0 List of 250 auto-repeater offset frequencies. @@ -190,6 +194,7 @@ class GPSSystem; * 024C1440 000030 Unknown data. * 024C1700 000040 Unknown, 8bit indices. * 024C1800 000500 Empty, set to 0x00? + * * * * @ingroup d878uv */ @@ -886,7 +891,7 @@ public: } Hemisphere; // byte 0x00 - uint8_t _unknown0; ///< Unknown, set to 0xff. + uint8_t _unknown0; ///< Unknown, set to 0x00. uint32_t frequency; ///< TX frequency, BCD encoded, little endian in 10Hz. uint8_t tx_delay; ///< TX delay, multiples of 20ms, default=1200ms. uint8_t sig_type; ///< Signalling type, 0=off, 1=ctcss, 2=dcs, default=off. @@ -987,6 +992,41 @@ public: }; + /** Represents an extension to the APRS settings. + * + * Memmory layout of APRS settings (0x60byte): + * @verbinclude d878uvaprssettingext.txt */ + struct __attribute__((packed)) aprs_setting_ext_t { + uint8_t _unknown0000[8]; ///< Unknown settings block. + uint8_t rep_position : 1, ///< Report position flag. + rep_mic_e : 1, ///< Report MIC-E flag. + rep_object : 1, ///< Report object flag. + rep_item : 1, ///< Report item flag. + rep_message : 1, ///< Report message flag. + rep_wx : 1, ///< WX report flag. + rep_nmea : 1, ///< NMEA report flag. + rep_status : 1; ///< Report status flag. + uint8_t rep_other : 1, ///< Report "other" flag. + _unused0009_1 :7; ///< Unused set to 0. + uint8_t _unknown000a[6]; ///< Unknown settings block. + + uint8_t _unknown0010[16]; ///< Unknown settings block. + uint8_t _unknown0020[16]; ///< Unknown settings block. + uint8_t _unknown0030[16]; ///< Unknown settings block. + uint8_t _unknown0040[16]; ///< Unknown settings block. + uint8_t _unknown0050[16]; ///< Unknown settings block. + }; + + /** Represents an APRS RX entry. + * + * Memmory layout of APRS-RX entry (0x100byte): + * @verbinclude d878uvaprsexentry.txt */ + struct __attribute__((packed)) aprs_rx_entry_t { + uint8_t enabled; ///< Enabled entry 0x01=on, 0x00=off. + char call[6]; ///< Callsign, 6x ASCII, 0-terminated. + uint8_t ssid; ///< SSID [0,15], 16=off. + }; + /** Represents the 8 GPS systems within the binary codeplug. * * Memmory layout of GPS systems (0x60byte): From bf2f83b37678e351f9835d6cda18b79513c81bb3 Mon Sep 17 00:00:00 2001 From: Hannes Matuschek Date: Fri, 2 Jul 2021 11:43:54 +0200 Subject: [PATCH 10/16] Cleanup radio hardware version detection for AnyTone radios. --- lib/anytone_radio.cc | 29 ++++++++++++++++++++++++++++- lib/anytone_radio.hh | 5 +++++ lib/d578uv.cc | 24 +++--------------------- lib/d578uv.hh | 3 --- lib/d868uv.cc | 21 +-------------------- lib/d868uv.hh | 3 --- lib/d878uv.cc | 24 +++--------------------- lib/d878uv.hh | 3 --- lib/d878uv2.cc | 23 +++-------------------- lib/d878uv2.hh | 3 --- 10 files changed, 43 insertions(+), 95 deletions(-) diff --git a/lib/anytone_radio.cc b/lib/anytone_radio.cc index afd6f4b5..786ea141 100644 --- a/lib/anytone_radio.cc +++ b/lib/anytone_radio.cc @@ -10,7 +10,7 @@ AnytoneRadio::AnytoneRadio(const QString &name, AnytoneInterface *device, QObject *parent) : Radio(parent), _name(name), _dev(device), _codeplugFlags(), _config(nullptr), - _codeplug(nullptr), _callsigns(nullptr) + _codeplug(nullptr), _callsigns(nullptr), _supported_version(), _version() { // Open device to radio if not already present if (! connect()) { @@ -45,6 +45,33 @@ AnytoneRadio::codeplug() { return *_codeplug; } + +VerifyIssue::Type +AnytoneRadio::verifyConfig(Config *config, QList &issues, const VerifyFlags &flags) { + VerifyIssue::Type issue = Radio::verifyConfig(config, issues, flags); + + if (_supported_version.isEmpty() || _version.isEmpty()) + return issue; + + if (_supported_version < _version) { + issues.append(VerifyIssue( + VerifyIssue::WARNING, + tr("You are likely using a newer radio reversion (%1) than supported (%2) by qdmr. " + "The codeplug might be incompatible. " + "Notify the developers of qdmr about the new reversion.").arg(_version, _supported_version))); + issue = std::max(issue, VerifyIssue::WARNING); + } else if (_supported_version > _version) { + issues.append(VerifyIssue( + VerifyIssue::WARNING, + tr("You are likely using an older hardware reversion (%1) than supported (%2) by qdmr. " + "The codeplug might be incompatible.").arg(_version, _supported_version))); + issue = std::max(issue, VerifyIssue::WARNING); + } + return issue; +} + + + bool AnytoneRadio::startDownload(bool blocking) { if (StatusIdle != _task) diff --git a/lib/anytone_radio.hh b/lib/anytone_radio.hh index 1ec46ad0..6f8f6d21 100644 --- a/lib/anytone_radio.hh +++ b/lib/anytone_radio.hh @@ -46,6 +46,9 @@ public: const CodePlug &codeplug() const; CodePlug &codeplug(); + VerifyIssue::Type verifyConfig(Config *config, QList &issues, + const VerifyFlags &flags=VerifyFlags()); + protected: /** Thread main routine, performs all blocking IO operations for codeplug up- and download. */ void run(); @@ -87,6 +90,8 @@ protected: AnytoneCodeplug *_codeplug; /** The actual binary callsign database representation. */ CallsignDB *_callsigns; + /** Holds the hardware version supported by qdmr. Used for codeplug compatibility. */ + QString _supported_version; /** Holds the hardware version of the radio. Used for codeplug compatibility. */ QString _version; }; diff --git a/lib/d578uv.cc b/lib/d578uv.cc index 943f030e..21d4a612 100644 --- a/lib/d578uv.cc +++ b/lib/d578uv.cc @@ -76,6 +76,7 @@ D578UV::D578UV(AnytoneInterface *device, QObject *parent) { _codeplug = new D578UVCodeplug(this); _callsigns = new D878UV2CallsignDB(this); + _supported_version = "V110"; // Get device info and determine supported TX frequency bands AnytoneInterface::RadioInfo info; _dev->getInfo(info); @@ -141,30 +142,11 @@ D578UV::D578UV(AnytoneInterface *device, QObject *parent) } logDebug() << "Got band-code " << QString::number(int(info.bands), 16) << ": Limit TX frequencies to " << bands.join(", ") << "."; + + _version = info.version; } const Radio::Features & D578UV::features() const { return _features; } - -VerifyIssue::Type -D578UV::verifyConfig(Config *config, QList &issues, const VerifyFlags &flags) { - QString supported = "V110"; - VerifyIssue::Type issue = AnytoneRadio::verifyConfig(config, issues, flags); - if (supported < _version) { - issues.append(VerifyIssue( - VerifyIssue::WARNING, - tr("You are likely running a newer firmware version (%1) than supported (%2) by qdmr. " - "Notify the developers of qdmr about the new firmware version.").arg(_version, supported))); - issue = std::max(issue, VerifyIssue::WARNING); - } else if (supported > _version) { - issues.append(VerifyIssue( - VerifyIssue::WARNING, - tr("You are likely running an older firmware version (%1) than supported (%2) by qdmr. " - "Condsider updating your firmware.").arg(_version, supported))); - issue = std::max(issue, VerifyIssue::WARNING); - } - return issue; -} - diff --git a/lib/d578uv.hh b/lib/d578uv.hh index e9e3000b..b2975eae 100644 --- a/lib/d578uv.hh +++ b/lib/d578uv.hh @@ -25,9 +25,6 @@ public: const Radio::Features &features() const; - VerifyIssue::Type verifyConfig(Config *config, QList &issues, - const VerifyFlags &flags=VerifyFlags()); - protected: /** Holds a copy of the specific radio features. */ Radio::Features _features; diff --git a/lib/d868uv.cc b/lib/d868uv.cc index d527be7b..a9aae10d 100644 --- a/lib/d868uv.cc +++ b/lib/d868uv.cc @@ -74,6 +74,7 @@ D868UV::D868UV(AnytoneInterface *device, QObject *parent) { _codeplug = new D868UVCodeplug(this); _callsigns = new D868UVCallsignDB(this); + _supported_version = "V102"; // Get device info and determine supported TX frequency bands AnytoneInterface::RadioInfo info; _dev->getInfo(info); @@ -140,23 +141,3 @@ const Radio::Features & D868UV::features() const { return _features; } - -VerifyIssue::Type -D868UV::verifyConfig(Config *config, QList &issues, const VerifyFlags &flags) { - QString supported = "V102"; - VerifyIssue::Type issue = AnytoneRadio::verifyConfig(config, issues, flags); - if (supported < _version) { - issues.append(VerifyIssue( - VerifyIssue::WARNING, - tr("You are likely running a newer firmware version (%1) than supported (%2) by qdmr. " - "Notify the developers of qdmr about the new firmware version.").arg(_version, supported))); - issue = std::max(issue, VerifyIssue::WARNING); - } else if (supported > _version) { - issues.append(VerifyIssue( - VerifyIssue::WARNING, - tr("You are likely running an older firmware version (%1) than supported (%2) by qdmr. " - "Condsider updating your firmware.").arg(_version, supported))); - issue = std::max(issue, VerifyIssue::WARNING); - } - return issue; -} diff --git a/lib/d868uv.hh b/lib/d868uv.hh index 2622a099..2f08748c 100644 --- a/lib/d868uv.hh +++ b/lib/d868uv.hh @@ -42,9 +42,6 @@ public: const Radio::Features &features() const; - VerifyIssue::Type verifyConfig(Config *config, QList &issues, - const VerifyFlags &flags=VerifyFlags()); - protected: /** Features of detected radio, variant and mode. */ Radio::Features _features; diff --git a/lib/d878uv.cc b/lib/d878uv.cc index fd5a1597..55ea4fee 100644 --- a/lib/d878uv.cc +++ b/lib/d878uv.cc @@ -76,6 +76,7 @@ D878UV::D878UV(AnytoneInterface *device, QObject *parent) { _codeplug = new D878UVCodeplug(this); _callsigns = new D868UVCallsignDB(this); + _supported_version = "V100"; // Get device info and determine supported TX frequency bands AnytoneInterface::RadioInfo info; _dev->getInfo(info); @@ -141,30 +142,11 @@ D878UV::D878UV(AnytoneInterface *device, QObject *parent) } logDebug() << "Got band-code " << QString::number(int(info.bands), 16) << ": Limit TX frequencies to " << bands.join(", ") << "."; + + _version = info.version; } const Radio::Features & D878UV::features() const { return _features; } - - -VerifyIssue::Type -D878UV::verifyConfig(Config *config, QList &issues, const VerifyFlags &flags) { - QString supported = "V100"; - VerifyIssue::Type issue = AnytoneRadio::verifyConfig(config, issues, flags); - if (supported < _version) { - issues.append(VerifyIssue( - VerifyIssue::WARNING, - tr("You are likely running a newer firmware version (%1) than supported (%2) by qdmr. " - "Notify the developers of qdmr about the new firmware version.").arg(_version, supported))); - issue = std::max(issue, VerifyIssue::WARNING); - } else if (supported > _version) { - issues.append(VerifyIssue( - VerifyIssue::WARNING, - tr("You are likely running an older firmware version (%1) than supported (%2) by qdmr. " - "Condsider updating your firmware.").arg(_version, supported))); - issue = std::max(issue, VerifyIssue::WARNING); - } - return issue; -} diff --git a/lib/d878uv.hh b/lib/d878uv.hh index 9048cc13..5af4ad12 100644 --- a/lib/d878uv.hh +++ b/lib/d878uv.hh @@ -42,9 +42,6 @@ public: const Radio::Features &features() const; - VerifyIssue::Type verifyConfig(Config *config, QList &issues, - const VerifyFlags &flags=VerifyFlags()); - protected: /** Holds a copy of the specific radio features. */ Radio::Features _features; diff --git a/lib/d878uv2.cc b/lib/d878uv2.cc index d4ea3537..b5f7c529 100644 --- a/lib/d878uv2.cc +++ b/lib/d878uv2.cc @@ -75,6 +75,7 @@ D878UV2::D878UV2(AnytoneInterface *device, QObject *parent) { _codeplug = new D878UV2Codeplug(this); _callsigns = new D878UV2CallsignDB(this); + _supported_version = "V100"; // Get device info and determine supported TX frequency bands AnytoneInterface::RadioInfo info; _dev->getInfo(info); @@ -140,29 +141,11 @@ D878UV2::D878UV2(AnytoneInterface *device, QObject *parent) } logDebug() << "Got band-code " << QString::number(int(info.bands), 16) << ": Limit TX frequencies to " << bands.join(", ") << "."; + + _version = info.version; } const Radio::Features & D878UV2::features() const { return _features; } - -VerifyIssue::Type -D878UV2::verifyConfig(Config *config, QList &issues, const VerifyFlags &flags) { - QString supported = "V100"; - VerifyIssue::Type issue = AnytoneRadio::verifyConfig(config, issues, flags); - if (supported < _version) { - issues.append(VerifyIssue( - VerifyIssue::WARNING, - tr("You are likely running a newer firmware version (%1) than supported (%2) by qdmr. " - "Notify the developers of qdmr about the new firmware version.").arg(_version, supported))); - issue = std::max(issue, VerifyIssue::WARNING); - } else if (supported > _version) { - issues.append(VerifyIssue( - VerifyIssue::WARNING, - tr("You are likely running an older firmware version (%1) than supported (%2) by qdmr. " - "Condsider updating your firmware.").arg(_version, supported))); - issue = std::max(issue, VerifyIssue::WARNING); - } - return issue; -} diff --git a/lib/d878uv2.hh b/lib/d878uv2.hh index 150b5bc2..db351edd 100644 --- a/lib/d878uv2.hh +++ b/lib/d878uv2.hh @@ -42,9 +42,6 @@ public: const Radio::Features &features() const; - VerifyIssue::Type verifyConfig(Config *config, QList &issues, - const VerifyFlags &flags=VerifyFlags()); - protected: /** Holds a copy of the specific radio features. */ Radio::Features _features; From 7c620b80a24ca434dfafe6d52587de71ccd3c140 Mon Sep 17 00:00:00 2001 From: Hannes Matuschek Date: Fri, 2 Jul 2021 13:10:47 +0200 Subject: [PATCH 11/16] Cleanup --- lib/d578uv_codeplug.hh | 6 ++-- lib/d868uv_codeplug.hh | 6 ++-- lib/d878uv2_codeplug.hh | 19 +++++++----- lib/d878uv_codeplug.cc | 9 ++++-- lib/d878uv_codeplug.hh | 64 +++++++++++++++++++++++++---------------- 5 files changed, 64 insertions(+), 40 deletions(-) diff --git a/lib/d578uv_codeplug.hh b/lib/d578uv_codeplug.hh index 3efc0a6b..643ca4c7 100644 --- a/lib/d578uv_codeplug.hh +++ b/lib/d578uv_codeplug.hh @@ -102,17 +102,17 @@ class GPSSystem; * Start Size Content * 02501000 000040 APRS settings, see @c D878UVCodeplug::aprs_setting_t. * 02501040 000060 APRS settings, see @c D878UVCodeplug::gps_systems_t. - * 025010A0 000060 Extended APRS settings, see @c aprs_setting_ext_t. + * 025010A0 000060 Extended APRS settings, see @c D878UVCodeplug::aprs_setting_ext_t. * 02501200 000040 APRS Text, upto 60 chars ASCII, 0-padded. * 02501800 000100 APRS-RX settings list up to 32 entries, 8b each. - * See @c aprs_rx_entry_t. + * See @c D878UVCodeplug::aprs_rx_entry_t. * * General Settings * Start Size Content * 02500000 000100 General settings, see @c D878UVCodeplug::general_settings_base_t. * 02500100 000400 Zone A & B channel list. * 02500500 000100 DTMF list - * 02500600 000030 Power on settings + * 02500600 000030 Power on settings, see @c D868UVCodeplug::boot_settings_t. * 02501280 000030 General settings extension 1, see @c D878UVCodeplug::general_settings_ext1_t. * 02501400 000100 General settings extension 2, see @c D878UVCodeplug::general_settings_ext2_t. * 024C2000 0003F0 List of 250 auto-repeater offset frequencies. diff --git a/lib/d868uv_codeplug.hh b/lib/d868uv_codeplug.hh index eb528df5..9602d7eb 100644 --- a/lib/d868uv_codeplug.hh +++ b/lib/d868uv_codeplug.hh @@ -100,15 +100,15 @@ class GPSSystem; * * GPS * Start Size Content - * 02501000 000030 GPS settings, see @c gps_setting_t. + * 02501000 000030 GPS settings, see @c gps_settings_t. * 02501100 000030 GPS message. * * General Settings * Start Size Content - * 02500000 0000D0 General settings, see @c general_settings_t. + * 02500000 0000D0 General settings, see @c general_settings_base_t. * 02500100 000400 Zone A & B channel list. * 02500500 000100 DTMF list - * 02500600 000030 Power on settings + * 02500600 000030 Power on settings, see @c boot_settings_t. * 024C2000 0003F0 List of 250 auto-repeater offset frequencies. * 32bit little endian frequency in 10Hz. I.e., 600kHz = 60000. Default 0x00000000, 0x00 padded. * diff --git a/lib/d878uv2_codeplug.hh b/lib/d878uv2_codeplug.hh index 03beffc2..0f2eb51a 100644 --- a/lib/d878uv2_codeplug.hh +++ b/lib/d878uv2_codeplug.hh @@ -102,26 +102,31 @@ class GPSSystem; * Start Size Content * 02501000 000040 APRS settings, see @c D878UVCodeplug::aprs_setting_t. * 02501040 000060 APRS settings, see @c D878UVCodeplug::gps_systems_t. - * 025010A0 000060 Extended APRS settings, see @c aprs_setting_ext_t. + * 025010A0 000060 Extended APRS settings, + * see @c D878UVCodeplug::aprs_setting_ext_t. * 02501200 000040 APRS Text, upto 60 chars ASCII, 0-padded. * 02501800 000100 APRS-RX settings list up to 32 entries, 8b each. - * See @c aprs_rx_entry_t. + * See @c D878UVCodeplug::aprs_rx_entry_t. * * General Settings * Start Size Content - * 02500000 000100 General settings, see @c D878UVCodeplug::general_settings_base_t. + * 02500000 000100 General settings, + * see @c D878UVCodeplug::general_settings_base_t. * 02500100 000400 Zone A & B channel list. * 02500500 000100 DTMF list - * 02500600 000030 Power on settings - * 02501280 000030 General settings extension 1, see @c D878UVCodeplug::general_settings_ext1_t. - * 02501400 000100 General settings extension 2, see @c D878UVCodeplug::general_settings_ext2_t. + * 02500600 000030 Power on settings, + * see @c D868UVCodeplug::boot_settings_t. + * 02501280 000030 General settings extension 1, + * see @c D878UVCodeplug::general_settings_ext1_t. + * 02501400 000100 General settings extension 2, + * see @c D878UVCodeplug::general_settings_ext2_t. * 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 message_list_t. Each entry has a size of 0x10. + * See @c D868UVCodeplug::message_list_t. 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. diff --git a/lib/d878uv_codeplug.cc b/lib/d878uv_codeplug.cc index ff5fc395..d3c9a8ee 100644 --- a/lib/d878uv_codeplug.cc +++ b/lib/d878uv_codeplug.cc @@ -595,8 +595,12 @@ D878UVCodeplug::aprs_setting_t::getAutoTXInterval() const { } void D878UVCodeplug::aprs_setting_t::setAutoTxInterval(int sec) { - // round up to multiples of 30 - auto_tx_interval = (sec+29)/30; + if (0 == sec) + auto_tx_interval = 0; + else if (30 >= sec) + auto_tx_interval = 1; + else + auto_tx_interval = (sec-16)/15; } int @@ -605,7 +609,6 @@ D878UVCodeplug::aprs_setting_t::getManualTXInterval() const { } void D878UVCodeplug::aprs_setting_t::setManualTxInterval(int sec) { - // round up to multiples of 30 manual_tx_interval = sec; } diff --git a/lib/d878uv_codeplug.hh b/lib/d878uv_codeplug.hh index a0747c37..c9be96d0 100644 --- a/lib/d878uv_codeplug.hh +++ b/lib/d878uv_codeplug.hh @@ -67,10 +67,12 @@ class GPSSystem; * * Roaming * Start Size Content - * 01042000 000020 Roaming channel bitmask, up to 250 bits, 0-padded, default 0. + * 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 roaming_channel_t for details. - * 01042080 000010 Roaming zone bitmask, up to 64 bits, 0-padded, default 0. + * 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 roaming_zone_t for details. * @@ -78,25 +80,29 @@ class GPSSystem; * 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 contact_t. + * 02640000 000500 Contact bitmap, 10000 bit, inverted, + * default 0xff, 0x00 padded. + * 02680000 max. 0f4240 10000 contacts, see @c D868UVCodeplug::contact_t. * 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 contact_map_t. - * Sorted by ID, empty entries set to 0xffffffffffffffff. + * 04340000 max. 013880 DMR ID to contact index map, + * see @c D868UVCodeplug::contact_map_t. Sorted by ID, empty entries set to + * 0xffffffffffffffff. * * Analog Contacts * Start Size Content * 02900000 000080 Index list of valid ananlog contacts. * 02900100 000080 Bytemap for 128 analog contacts. - * 02940000 max. 000180 128 analog contacts. See @c analog_contact_t. - * 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. + * 02940000 max. 000180 128 analog contacts. + * See @c D868UVCodeplug::analog_contact_t. 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 grouplist_t. + * 025C0B10 000020 Bitmap of 250 RX group lists, + * default/padding 0x00. + * 02980000 max. 000120 Grouplist 0, + * see @c D868UVCodeplug::grouplist_t. * 02980200 max. 000120 Grouplist 1 * ... ... ... * 0299f200 max. 000120 Grouplist 250 @@ -104,7 +110,8 @@ class GPSSystem; * Scan lists * Start Size Content * 024C1340 000020 Bitmap of 250 scan lists. - * 01080000 000090 Bank 0, Scanlist 1, see @c scanlist_t. + * 01080000 000090 Bank 0, Scanlist 1, + * see @c D868UVCodeplug::scanlist_t. * 01080200 000090 Bank 0, Scanlist 2 * ... ... ... * 01081E00 000090 Bank 0, Scanlist 16 @@ -117,12 +124,13 @@ class GPSSystem; * Radio IDs * Start Size Content * 024C1320 000020 Bitmap of 250 radio IDs. - * 02580000 max. 001f40 250 Radio IDs. See @c radioid_t. + * 02580000 max. 001f40 250 Radio IDs. + * See @c D868UVCodeplug::radioid_t. * * GPS/APRS * Start Size Content * 02501000 000040 APRS settings, see @c aprs_setting_t. - * 02501040 000060 APRS settings, see @c gps_systems_t. + * 02501040 000060 APRS settings, see @c D868UVCodeplug::gps_settings_t. * 025010A0 000060 Extended APRS settings, see @c aprs_setting_ext_t. * 02501200 000040 APRS Text, upto 60 chars ASCII, 0-padded. * 02501800 000100 APRS-RX settings list up to 32 entries, 8b each. @@ -133,31 +141,37 @@ class GPSSystem; * 02500000 000100 General settings, see @c general_settings_base_t. * 02500100 000400 Zone A & B channel list. * 02500500 000100 DTMF list - * 02500600 000030 Power on settings - * 02501280 000030 General settings extension 1, see @c general_settings_ext1_t. - * 02501400 000100 General settings extension 2, see @c general_settings_ext2_t. + * 02500600 000030 Power on settings, + * see @c D868UVCodeplug::boot_settings_t. + * 02501280 000030 General settings extension 1, + * see @c general_settings_ext1_t. + * 02501400 000100 General settings extension 2, + * see @c general_settings_ext2_t. * 024C2000 0003F0 List of 250 auto-repeater offset frequencies. - * 32bit little endian frequency in 10Hz. I.e., 600kHz = 60000. Default 0x00000000, 0x00 padded. + * 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 message_list_t. Each entry has a size of 0x10. + * See @c D868UVCodeplug::message_list_t. 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 message_t. + * Each message consumes 0x100b. See @c D868UVCodeplug::message_t. * 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 analog_quick_call_t. + * 025C0000 000100 4 analog quick-call settings. + * See @c D868UVCodeplug::analog_quick_call_t. * 025C0B00 000010 Status message bitmap. * 025C0100 000400 Upto 32 status messages. * Length unknown, offset 0x20. ASCII 0x00 terminated and padded. - * 025C0500 000360 18 hot-key settings, see @c hotkey_t + * 025C0500 000360 18 hot-key settings, see + * @c D868UVCodeplug::hotkey_t * * Encryption keys * Start Size Content @@ -166,7 +180,8 @@ class GPSSystem; * * Misc * Start Size Content - * 024C1400 000020 Alarm setting, see @c analog_alarm_setting_t. + * 024C1400 000020 Alarm setting, see + * @c D868UVCodeplug::analog_alarm_setting_t. * * FM Broadcast * Start Size Content @@ -898,7 +913,8 @@ public: uint8_t ctcss; ///< CTCSS tone-code, default=0. uint16_t dcs; ///< DCS code, little endian, default=0x0013. uint8_t manual_tx_interval; ///< Global manual TX intervals in seconds. - uint8_t auto_tx_interval; ///< Global auto TX interval in multiples of 30s. + uint8_t auto_tx_interval; ///< Global auto TX interval in multiples of 15s. That is + /// 0 = Off, 1 = 30s, n = 45s + (n-1) *15s. uint8_t tx_tone_enable; ///< TX tone enable, 0=off, 1=on. uint8_t fixed_location; ///< Fixed location data, 0=off, 1=on. From 59585d932d5de51f75f2b1d132174d2002f552b2 Mon Sep 17 00:00:00 2001 From: Hannes Matuschek Date: Fri, 2 Jul 2021 13:45:04 +0200 Subject: [PATCH 12/16] Fixed APRS settings for D878UV. --- lib/d878uv_codeplug.hh | 23 ++++++++++++----------- 1 file changed, 12 insertions(+), 11 deletions(-) diff --git a/lib/d878uv_codeplug.hh b/lib/d878uv_codeplug.hh index c9be96d0..98c79524 100644 --- a/lib/d878uv_codeplug.hh +++ b/lib/d878uv_codeplug.hh @@ -883,33 +883,33 @@ public: */ struct __attribute__((packed)) aprs_setting_t { /** Possible signalling for APRS repeater.*/ - typedef enum { + enum SignalingType: uint8_t { SIG_OFF = 0, ///< No signalling. SIG_CTCSS = 1, ///< CTCSS signalling. SIG_DCS = 2 ///< DCS signalling. - } SignalingType; + }; /** Power setting for the APRS/GPS channel. */ - typedef enum { + enum Power: uint8_t { POWER_LOW = 0, ///< Low power (usually about 1W). POWER_MID = 1, ///< Medium power (usually about 2W). POWER_HIGH = 2, ///< High power (usually about 5W). POWER_TURBO = 3 ///< Highest power (upto 7W). - } Power; + }; /** Hemisphere settings for the fixed location beacon. */ - typedef enum { + enum Hemisphere: uint8_t { NORTH = 0, SOUTH = 1, EAST = 0, WEST = 1 - } Hemisphere; + }; // byte 0x00 uint8_t _unknown0; ///< Unknown, set to 0x00. uint32_t frequency; ///< TX frequency, BCD encoded, little endian in 10Hz. uint8_t tx_delay; ///< TX delay, multiples of 20ms, default=1200ms. - uint8_t sig_type; ///< Signalling type, 0=off, 1=ctcss, 2=dcs, default=off. + SignalingType sig_type; ///< Signalling type, 0=off, 1=ctcss, 2=dcs, default=off. uint8_t ctcss; ///< CTCSS tone-code, default=0. uint16_t dcs; ///< DCS code, little endian, default=0x0013. uint8_t manual_tx_interval; ///< Global manual TX intervals in seconds. @@ -921,11 +921,11 @@ public: uint8_t lat_deg; ///< Latitude in degree. uint8_t lat_min; ///< Latitude minutes. uint8_t lat_sec; ///< Latitude seconds (1/100th of a minute). - uint8_t north_south; ///< North or south flag, north=0, south=1. + Hemisphere north_south; ///< North or south flag, north=0, south=1. uint8_t lon_deg; ///< Longitude in degree. uint8_t lon_min; ///< Longitude in minutes. uint8_t lon_sec; ///< Longitude in seconds (1/100th of a minute). - uint8_t east_west; ///< East or west flag, east=0, west=1. + Hemisphere east_west; ///< East or west flag, east=0, west=1. uint8_t to_call[6]; ///< Destination call, 6 x ASCII, 0x20-padded. uint8_t to_ssid; ///< Destination SSID, 0xff=None. @@ -941,7 +941,7 @@ public: char table; ///< ASCII-char for APRS icon table, ie. '/' or '\' for primary /// and alternate icon table respectively. char icon; ///< ASCII-char of APRS map icon. - uint8_t power; ///< Transmit power. + Power power; ///< Transmit power. uint8_t prewave_delay; ///< Prewave delay in 10ms steps. // bytes 0x3d @@ -1013,7 +1013,8 @@ public: * Memmory layout of APRS settings (0x60byte): * @verbinclude d878uvaprssettingext.txt */ struct __attribute__((packed)) aprs_setting_ext_t { - uint8_t _unknown0000[8]; ///< Unknown settings block. + uint8_t _unknown0000[6]; ///< Unknown settings block. + uint16_t fixed_altitude; ///< Fixed altitude in feet, little endian. uint8_t rep_position : 1, ///< Report position flag. rep_mic_e : 1, ///< Report MIC-E flag. rep_object : 1, ///< Report object flag. From bcbbbb092d074d2f2224edaf2989748673a3331a Mon Sep 17 00:00:00 2001 From: Hannes Matuschek Date: Fri, 2 Jul 2021 15:26:07 +0200 Subject: [PATCH 13/16] Fixed alarm settings for anytone devices. --- lib/d578uv_codeplug.hh | 11 +++- lib/d868uv_codeplug.cc | 20 +++++-- lib/d868uv_codeplug.hh | 128 +++++++++++++++++++++++++++++++--------- lib/d878uv2_codeplug.hh | 15 ++--- lib/d878uv_codeplug.hh | 18 +++--- 5 files changed, 141 insertions(+), 51 deletions(-) diff --git a/lib/d578uv_codeplug.hh b/lib/d578uv_codeplug.hh index 643ca4c7..15fbc6f6 100644 --- a/lib/d578uv_codeplug.hh +++ b/lib/d578uv_codeplug.hh @@ -140,12 +140,19 @@ class GPSSystem; * * Encryption keys * 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 Upto 256 AES encryption keys. * See @c D878UVCodeplug::encryption_key_t. * * Misc * Start Size Content - * 024C1400 000020 Alarm setting, see @c D868UVCodeplug::analog_alarm_setting_t. + * 024C1400 000020 Alarm setting, + * see @c D868UVCodeplug::alarm_setting_t. + * 024C1440 000030 Digital alarm settings extension, + * see @c D868UVCodeplug::digital_alarm_settings_ext_t. * * FM Broadcast * Start Size Content @@ -172,8 +179,6 @@ class GPSSystem; * Start Size Content * 024C1090 000040 Unknown, set to 0xff * 024C1440 000030 Unknown data. - * 024C1700 000040 Unknown, 8bit indices. - * 024C1800 000500 Empty, set to 0x00? * 02BC0000 000020 Unknown, set to 0x00. * 02BC0C60 000020 Unknown, set to 0x00. * 02BC1000 000060 Unknown, set to 0x00. diff --git a/lib/d868uv_codeplug.cc b/lib/d868uv_codeplug.cc index 8da51d31..9d608f40 100644 --- a/lib/d868uv_codeplug.cc +++ b/lib/d868uv_codeplug.cc @@ -151,6 +151,15 @@ static_assert( #define ADDR_ALARM_SETTING 0x024C1400 #define ALARM_SETTING_SIZE 0x00000020 +static_assert( + ALARM_SETTING_SIZE == sizeof(D868UVCodeplug::alarm_settings_t), + "D868UVCodeplug::alarm_settings_t size check failed."); + +#define ADDR_ALARM_SETTING_EXT 0x024c1440 +#define ALARM_SETTING_EXT_SIZE 0x00000030 +static_assert( + ALARM_SETTING_EXT_SIZE == sizeof(D868UVCodeplug::digital_alarm_settings_ext_t), + "D868UVCodeplug::digital_alarm_settings_ext_t size check failed."); #define FMBC_BITMAP 0x02480210 #define FMBC_BITMAP_SIZE 0x00000020 @@ -189,6 +198,10 @@ static_assert( #define ADDR_TWO_TONE_SETTINGS 0x024C1290 #define TWO_TONE_SETTINGS_SIZE 0x00000010 +#define ADDR_DMR_ENCRYPTION_LIST 0x024C1700 +#define DMR_ENCRYPTION_LIST_SIZE 0x00000040 +#define ADDR_DMR_ENCRYPTION_KEYS 0x024C1800 +#define DMR_ENCRYPTION_KEYS_SIZE 0x00000500 using namespace Signaling; @@ -1340,10 +1353,8 @@ D868UVCodeplug::allocateUpdated() { image(0).addElement(ADDR_DTMF_SETTINGS, DTMF_SETTINGS_SIZE); image(0).addElement(ADDR_TWO_TONE_SETTINGS, TWO_TONE_SETTINGS_SIZE); - // Unknown memory region - image(0).addElement(0x024C1440, 0x030); - image(0).addElement(0x024C1700, 0x040); - image(0).addElement(0x024C1800, 0x500); + image(0).addElement(ADDR_DMR_ENCRYPTION_LIST, DMR_ENCRYPTION_LIST_SIZE); + image(0).addElement(ADDR_DMR_ENCRYPTION_KEYS, DMR_ENCRYPTION_KEYS_SIZE); } void @@ -2141,6 +2152,7 @@ void D868UVCodeplug::allocateAlarmSettings() { // Alarm settings image(0).addElement(ADDR_ALARM_SETTING, ALARM_SETTING_SIZE); + image(0).addElement(ADDR_ALARM_SETTING_EXT, ALARM_SETTING_EXT_SIZE); } void diff --git a/lib/d868uv_codeplug.hh b/lib/d868uv_codeplug.hh index 9602d7eb..e273d586 100644 --- a/lib/d868uv_codeplug.hh +++ b/lib/d868uv_codeplug.hh @@ -134,7 +134,9 @@ class GPSSystem; * * Misc * Start Size Content - * 024C1400 000020 Alarm setting, see @c analog_alarm_setting_t. + * 024C1400 000020 Alarm setting, see @c alarm_setting_t. + * 024C1440 000030 Digital alarm settings extension, + * see @c digital_alarm_settings_ext_t. * * FM Broadcast * Start Size Content @@ -157,11 +159,11 @@ class GPSSystem; * 024C2600 000010 2-tone decoding bitmap. * 024C2400 000030 2-tone decoding. * - * Still unknown + * Encryption * Start Size Content - * 024C1440 000030 Unknown data. - * 024C1700 000040 Unknown, 8bit indices. - * 024C1800 000500 Empty, set to 0x00? + * 024C1700 000040 32 Encryption IDs, 0-based, 16bit big-endian. + * 024C1800 000500 32 DMR-Encryption keys, see @c dmr_encryption_key_t, + * 40b each. * * * @ingroup d868uv */ @@ -1133,31 +1135,101 @@ public: uint8_t _unused9[39]; ///< Unused, set to 0x00. }; + /** Alarm settings. */ + struct __attribute__((packed)) alarm_settings_t { + /** Possible alarm channel selections. */ + enum ChannelSelect : uint8_t { + ASSIGNED_CHANNEL = 0, ///< The assigned channel. + CURRENT_CHANNEL = 1 ///< The current channel. + }; - /** Binary representation of the analog alarm settings. - * Size 0x6 bytes. */ - struct __attribute__((packed)) analog_alarm_setting_t { - /** Possible alarm types. */ - typedef enum { - ALARM_AA_NONE = 0, ///< No alarm at all. - ALARM_AA_TX_AND_BG = 1, ///< Transmit and background. - ALARM_AA_TX_AND_ALARM = 2, ///< Transmit and alarm - ALARM_AA_BOTH = 3, ///< Both? - } Action; + /** Binary representation of the analog alarm settings. + * Size 0x6 bytes. */ + struct __attribute__((packed)) analog_alarm_setting_t { + /** Possible alarm types. */ + enum Action : uint8_t { + ALARM_AA_NONE = 0, ///< No alarm at all. + ALARM_AA_TX_AND_BG = 1, ///< Transmit and background. + ALARM_AA_TX_AND_ALARM = 2, ///< Transmit and alarm + ALARM_AA_BOTH = 3, ///< Both? + }; + + /** Possible alarm signalling types. */ + enum ENIType : uint8_t { + ALARM_ENI_NONE = 0, ///< No alarm code signalling. + ALARM_ENI_DTMF = 1, ///< Send alarm code as DTMF. + ALARM_ENI_5TONE = 2 ///< Send alarm code as 5-tone. + }; + + Action action; ///< Action to take, see @c Action. + ENIType eni_type; ///< ENI type, see @c ENIType. + uint8_t emergency_id_idx; ///< Emergency ID index, 0-based. + uint8_t time; ///< Alarm time in seconds, default 10. + uint8_t tx_dur; ///< TX duration in seconds, default 10. + uint8_t rx_dur; ///< RX duration in seconds, default 60. + uint16_t channel; ///< Emergency channel index, 16bit little-endian, analog channels only. + ChannelSelect channel_select; ///< ENI Channel select, 0=assigned, 1=current. + uint8_t alarm_repeat; ///< Alarm repeat 0=Continuous, 1..255. + }; - /** Possible alarm signalling types. */ - typedef enum { - ALARM_ENI_NONE = 0, ///< No alarm code signalling. - ALARM_ENI_DTMF = 1, ///< Send alarm code as DTMF. - ALARM_ENI_5TONE = 2 ///< Send alarm code as 5-tone. - } ENIType; - - uint8_t action; ///< Action to take, see @c Action. - uint8_t eni_type; ///< ENI type, see @c ENIType. - uint8_t emergency_id_idx; ///< Emergency ID index, 0-based. - uint8_t time; ///< Alarm time in seconds, default 10. - uint8_t tx_dur; ///< TX duration in seconds, default 10. - uint8_t rx_dur; ///< RX duration in seconds, default 60. + /** Encodes digital alarm settings. + * Size 12b. */ + struct __attribute__((packed)) digital_alarm_settings_t { + /** Possible alarm types. */ + enum Action : uint8_t { + ALARM_DA_NONE = 0, ///< No alarm at all. + ALARM_DA_TX_AND_BG = 1, ///< Transmit and background. + ALARM_DA_TX_AND_NLOC = 2, ///< Transmit and non-local alarm. + ALARM_DA_TX_AND_LOC = 3, ///< Transmit and local alarm. + }; + + /** Possible alarm PTT methods. */ + enum MicBroadcast : uint8_t { + MIC_BC_PTT = 0, + MIC_BC_VOX = 1 + }; + + Action action; ///< Action to take, see @c Action. + uint8_t time; ///< Alarm time in seconds, default 10. + uint8_t tx_dur; ///< TX duration in seconds, default 10. + uint8_t rx_dur; ///< RX duration in seconds, default 60. + uint16_t channel; ///< Emergency channel index, 16bit little-endian, digital channels only. + ChannelSelect channel_select; ///< ENI Channel select, 0=assigned, 1=current. + uint8_t alarm_repeat; ///< Alarm repeat 0=Continuous, 1..255. + uint8_t voice_sw_bc; ///< Voice switch broadcast, 0=1min, ..., 255=256min. + uint8_t area_sw_bc; ///< Area switch broadcast, 0=1min, ..., 255=256min. + MicBroadcast mic_bc; ///< Mic broadcast. + uint8_t rx_alarm; ///< Receive alarm broadcasts, 0=off, 1=on. + }; + + analog_alarm_setting_t analog; ///< Analog alarm settings. + digital_alarm_settings_t digital; ///< Digital alarm settings. + uint8_t _unused16[10]; ///< Unused, set to 0x00. + }; + + /** Some extension to the digital alarm settings. */ + struct __attribute__((packed)) digital_alarm_settings_ext_t { + /** Represents the digital alarm call type. */ + enum CallType : uint8_t { + PRIVATE_CALL = 0, ///< A private call. + GROUP_CALL = 1, ///< A group call. + ALL_CALL = 2 ///< An all call. + }; + + CallType call_type; /// contact map within the binary code-plug. */ diff --git a/lib/d878uv2_codeplug.hh b/lib/d878uv2_codeplug.hh index 0f2eb51a..c1e0208d 100644 --- a/lib/d878uv2_codeplug.hh +++ b/lib/d878uv2_codeplug.hh @@ -145,12 +145,19 @@ class GPSSystem; * * Encryption keys * 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 Upto 256 AES encryption keys. * See @c D878UVCodeplug::encryption_key_t. * * Misc * Start Size Content - * 024C1400 000020 Alarm setting, see @c D868UVCodeplug::analog_alarm_setting_t. + * 024C1400 000020 Alarm setting, + * see @c D868UVCodeplug::alarm_setting_t. + * 024C1440 000030 Digital alarm settings extension, + * see @c D868UVCodeplug::digital_alarm_settings_ext_t. * * FM Broadcast * Start Size Content @@ -172,12 +179,6 @@ class GPSSystem; * 024C1290 000010 2-tone settings. * 024C2600 000010 2-tone decoding bitmap. * 024C2400 000030 2-tone decoding. - * - * Still unknown - * Start Size Content - * 024C1440 000030 Unknown data. - * 024C1700 000040 Unknown, 8bit indices. - * 024C1800 000500 Empty, set to 0x00? * * * @ingroup d878uv2 */ diff --git a/lib/d878uv_codeplug.hh b/lib/d878uv_codeplug.hh index 98c79524..fef8e92c 100644 --- a/lib/d878uv_codeplug.hh +++ b/lib/d878uv_codeplug.hh @@ -173,15 +173,21 @@ class GPSSystem; * 025C0500 000360 18 hot-key settings, see * @c D868UVCodeplug::hotkey_t * - * Encryption keys + * 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 Upto 256 AES encryption keys. * See @c encryption_key_t. * * Misc * Start Size Content - * 024C1400 000020 Alarm setting, see - * @c D868UVCodeplug::analog_alarm_setting_t. + * 024C1400 000020 Alarm setting, + * see @c D868UVCodeplug::alarm_setting_t. + * 024C1440 000030 Digital alarm settings extension, + * see @c D868UVCodeplug::digital_alarm_settings_ext_t. * * FM Broadcast * Start Size Content @@ -204,12 +210,6 @@ class GPSSystem; * 024C2600 000010 2-tone decoding bitmap. * 024C2400 000030 2-tone decoding. * - * Still unknown - * Start Size Content - * 024C1440 000030 Unknown data. - * 024C1700 000040 Unknown, 8bit indices. - * 024C1800 000500 Empty, set to 0x00? - * * * * @ingroup d878uv */ From c92539bf9ed42abf3d07d08fa4fd2e0fd3198714 Mon Sep 17 00:00:00 2001 From: Hannes Matuschek Date: Fri, 2 Jul 2021 16:45:11 +0200 Subject: [PATCH 14/16] Fixed 5-tone settings. --- lib/d868uv_codeplug.cc | 67 +++++++++++++++++-------- lib/d868uv_codeplug.hh | 111 ++++++++++++++++++++++++++++++++++++++--- 2 files changed, 152 insertions(+), 26 deletions(-) diff --git a/lib/d868uv_codeplug.cc b/lib/d868uv_codeplug.cc index 9d608f40..4eac8920 100644 --- a/lib/d868uv_codeplug.cc +++ b/lib/d868uv_codeplug.cc @@ -168,19 +168,34 @@ static_assert( #define ADDR_FMBC_VFO 0x02480200 #define FMBC_VFO_SIZE 0x00000010 -#define FIVE_TONE_BITMAP 0x024C0C80 -#define FIVE_TONE_BITMAP_SIZE 0x00000010 -#define NUM_FIVE_TONE_FUNCTIONS 100 -#define ADDR_FIVE_TONE_FUNCTIONS 0x024C0000 -#define FIVE_TONE_FUNCTION_SIZE 0x00000020 - -#define NUM_FIVE_TONE_IDS 16 -#define ADDR_FIVE_TONE_ID_LIST 0x024C0D00 +#define FIVE_TONE_ID_BITMAP 0x024C0C80 +#define FIVE_TONE_ID_BITMAP_SIZE 0x00000010 +#define NUM_FIVE_TONE_IDS 100 +#define ADDR_FIVE_TONE_ID_LIST 0x024C0000 #define FIVE_TONE_ID_SIZE 0x00000020 -#define FIVE_TONE_ID_LIST_SIZE 0x00000200 - +#define FIVE_TONE_ID_LIST_SIZE 0x00000c80 +static_assert( + FIVE_TONE_ID_SIZE == sizeof(D868UVCodeplug::five_tone_id_t), + "D868UVCodeplug::five_tone_id_t size check failed."); +static_assert( + FIVE_TONE_ID_LIST_SIZE == (NUM_FIVE_TONE_IDS*sizeof(D868UVCodeplug::five_tone_id_t)), + "D868UVCodeplug::five_tone_function_t list size check failed."); +#define NUM_FIVE_TONE_FUNCTIONS 16 +#define ADDR_FIVE_TONE_FUNCTIONS 0x024C0D00 +#define FIVE_TONE_FUNCTION_SIZE 0x00000020 +#define FIVE_TONE_FUNCTIONS_SIZE 0x00000200 +static_assert( + FIVE_TONE_FUNCTION_SIZE == sizeof(D868UVCodeplug::five_tone_function_t), + "D868UVCodeplug::five_tone_function_t size check failed."); +static_assert( + FIVE_TONE_FUNCTIONS_SIZE == (NUM_FIVE_TONE_FUNCTIONS*sizeof(D868UVCodeplug::five_tone_function_t)), + "D868UVCodeplug::five_tone_function_t list size check failed."); #define ADDR_FIVE_TONE_SETTINGS 0x024C1000 #define FIVE_TONE_SETTINGS_SIZE 0x00000080 +static_assert( + FIVE_TONE_SETTINGS_SIZE == sizeof(D868UVCodeplug::five_tone_settings_t), + "D868UVCodeplug::five_tone_settings_t size check failed."); + #define ADDR_DTMF_SETTINGS 0x024C1080 #define DTMF_SETTINGS_SIZE 0x00000050 @@ -1317,7 +1332,7 @@ D868UVCodeplug::D868UVCodeplug(QObject *parent) // FM Broadcast bitmaps image(0).addElement(FMBC_BITMAP, FMBC_BITMAP_SIZE); // 5-Tone function bitmaps - image(0).addElement(FIVE_TONE_BITMAP, FIVE_TONE_BITMAP_SIZE); + image(0).addElement(FIVE_TONE_ID_BITMAP, FIVE_TONE_ID_BITMAP_SIZE); // 2-Tone function bitmaps image(0).addElement(TWO_TONE_ENC_BITMAP, TWO_TONE_ENC_BITMAP_SIZE); image(0).addElement(TWO_TONE_DEC_BITMAP, TWO_TONE_DEC_BITMAP_SIZE); @@ -1345,14 +1360,16 @@ D868UVCodeplug::allocateUpdated() { this->allocateRepeaterOffsetSettings(); this->allocateAlarmSettings(); this->allocateFMBroadcastSettings(); + + this->allocate5ToneIDs(); this->allocate5ToneFunctions(); - this->allocate2ToneFunctions(); + this->allocate5ToneSettings(); - image(0).addElement(ADDR_FIVE_TONE_ID_LIST, FIVE_TONE_ID_LIST_SIZE); - image(0).addElement(ADDR_FIVE_TONE_SETTINGS, FIVE_TONE_SETTINGS_SIZE); - image(0).addElement(ADDR_DTMF_SETTINGS, DTMF_SETTINGS_SIZE); + this->allocate2ToneIDs(); image(0).addElement(ADDR_TWO_TONE_SETTINGS, TWO_TONE_SETTINGS_SIZE); + image(0).addElement(ADDR_DTMF_SETTINGS, DTMF_SETTINGS_SIZE); + image(0).addElement(ADDR_DMR_ENCRYPTION_LIST, DMR_ENCRYPTION_LIST_SIZE); image(0).addElement(ADDR_DMR_ENCRYPTION_KEYS, DMR_ENCRYPTION_KEYS_SIZE); } @@ -2162,19 +2179,29 @@ D868UVCodeplug::allocateFMBroadcastSettings() { } void -D868UVCodeplug::allocate5ToneFunctions() { +D868UVCodeplug::allocate5ToneIDs() { // Allocate 5-tone functions - uint8_t *bitmap = data(FIVE_TONE_BITMAP); - for (uint8_t i=0; iDTMF, 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. + * 024C0C80 000010 5-tone encoding bitmap for 100 IDs. + * 024C0000 000020 List of 100 5-tone IDs, + * see @c five_tone_id_t + * 024C0D00 000200 List of 16 5-tone functions, + * see @c five_tone_function_t. + * 024C1000 000080 5-tone settings, + * see @c five_tone_settings_t. * 024C1080 000050 DTMF settings. * 024C1280 000010 2-tone encoding bitmap. * 024C1100 000010 2-tone encoding. @@ -1232,6 +1235,96 @@ public: uint8_t _unused12[14]; ///< Unused bytes, set to 0x00. }; + /** Binary representation of a 5-tone id. + * Size 0x20 bytes. */ + struct __attribute__((packed)) five_tone_id_t { + /** Possible 5-tone encoding standards. */ + enum EncStandard : uint8_t { + ZVEI1 = 0, ZVEI2, ZVEI3, PZVEI, DZVEI, PDZVEI, CCIR1, CCIR2, PCCIR, EEA, EURO_SIGNAL, NATEL, + MODAT, CCITT, EIA + }; + + uint8_t _unused00; ///< Unused, set to 0x00. + EncStandard standard; ///< Specifies the encoding standard. + uint8_t id_length; ///< Length of ID in bytes. + uint8_t tone_dur; ///< Tone duration in ms. + uint8_t id[20]; ///< ID, 40 BCD values (20 bytes). + uint8_t name[7]; ///< Name, 7 chars ASCII. + uint8_t _pad1f; ///< Pad byte, set to 0. + }; + + /** Binary representation of a 5-tone function. + * Size 0x20 bytes. */ + struct __attribute__((packed)) five_tone_function_t { + enum Function : uint8_t { + OPEN_SQUELCH=0, CALL_ALL, EMERGENCY_ALARM, REMOTE_KILL, REMOTE_STUN, REMOTE_WAKEUP, + GROUP_CALL, + }; + + enum Response : uint8_t { + RESP_NONE=0, RESP_TONE, RESP_TONE_RESPOND + }; + + Function function; ///< The function to perform. + Response response; ///< Response type. + uint8_t id_length; ///< Length of ID. + uint8_t id[12]; ///< The ID, 1 byte per char ([0-9A-F]). + char name[7]; ///< Function name, max 7 bytes ASCII. + uint8_t _pad17; ///< Pad byte, set to 0. + uint8_t _unused18[9]; ///< Unused, set to 0x00. + }; + + /** Binary representation of the 5-tone settings. + * Size 0x80 bytes. */ + struct __attribute__((packed)) five_tone_settings_t { + enum Response : uint8_t { + RESP_NONE = 0, RESP_TONE, RESP_TONE_RESPOND + }; + + enum Standard : uint8_t { + ZVEI1 = 0, ZVEI2, ZVEI3, PZVEI, DZVEI, PDZVEI, CCIR1, CCIR2, PCCIR, EEA, EURO_SIGNAL, NATEL, + MODAT, CCITT, EIA + }; + + uint8_t _unknown00[32]; ///< Unknown data. + + uint8_t _unused20; ///< Unused, set to 0x00. + Response response; ///< The decoding response. + Standard decoding_standard; ///< The decoding standard. + uint8_t self_id_length; ///< Length of self ID. + uint8_t dec_tone_duration; ///< Decoding tone duration in ms. + uint8_t self_id[7]; ///< Self ID, max 7 chars ([0-9A-F]). + uint8_t post_encode_delay; ///< Post encoding delay in multiple of 10ms. + uint8_t ptt_id; ///< PTT ID, 0=off, [5,75]. + uint8_t auto_reset_time; ///< Auto-reset time in multiples of 10s. + uint8_t first_delay; ///< First delay in multiple of 10ms; + + uint8_t sidetone_enable; ///< Side-tone enable. + uint8_t _unknown31; ///< Unknown byte. + uint8_t stop_code; ///< Stop code [0x0, 0xf]. + uint8_t stop_time; ///< Stop time in multiple of 10ms. + uint8_t decode_time; ///< Decode time in multiple of 10ms. + uint8_t first_delay_after_stop;///< First delay after stop, in multiple of 10ms. + uint8_t pre_time; ///< Pre-time, in multiple of 10ms. + uint8_t _unused37[9]; ///< Unused, set to 0x00. + + uint8_t _unused40; ///< Unused, set to 0x00. + Standard bot_stardard; ///< BOT encoding standard. + uint8_t bot_id_length; ///< BOT PTT ID length. + uint8_t bot_tone_duration; ///< BOT encoding tone duration in ms. + uint8_t bot_id[12]; ///< BOT PTT ID, up to 24 BCD chars. + + uint8_t _unused50[16]; ///< Unused, set to 0x00. + + uint8_t _unused60; ///< Unused, set to 0x00. + Standard eot_stardard; ///< EOT encoding standard. + uint8_t eot_id_length; ///< EOT PTT ID length. + uint8_t eot_tone_duration; ///< EOT encoding tone duration in ms. + uint8_t eot_id[12]; ///< EOT PTT ID, up to 24 BCD chars. + + uint8_t _unused70[16]; ///< Unused, set to 0x00. + }; + /** Represents an entry in the DMR ID -> contact map within the binary code-plug. */ struct __attribute__((packed)) contact_map_t { uint32_t id_group; ///< Combined ID and group-call flag. The ID is encoded in @@ -1386,10 +1479,16 @@ protected: virtual void allocateAlarmSettings(); /** Allocates FM broadcast settings memory section. */ virtual void allocateFMBroadcastSettings(); - /** Allocates all 5-Tone functions used. */ + + /** Allocates all 5-Tone IDs used. */ + virtual void allocate5ToneIDs(); + /** Allocates 5-Tone functions. */ virtual void allocate5ToneFunctions(); + /** Allocates 5-Tone settings. */ + virtual void allocate5ToneSettings(); + /** Allocates all 2-Tone functions used. */ - virtual void allocate2ToneFunctions(); + virtual void allocate2ToneIDs(); /** Internal used function to encode CTCSS frequencies. */ static uint8_t ctcss_code2num(Signaling::Code code); From 61c0781814dfdde7c36b6156e51b3f60708f0fdc Mon Sep 17 00:00:00 2001 From: Hannes Matuschek Date: Fri, 2 Jul 2021 22:18:26 +0200 Subject: [PATCH 15/16] Implemented 2-tone & 5-tone settings representation. --- lib/d868uv_codeplug.cc | 72 ++++++++++++++++++++++++----------- lib/d868uv_codeplug.hh | 86 +++++++++++++++++++++++++++++++++++++++--- 2 files changed, 131 insertions(+), 27 deletions(-) diff --git a/lib/d868uv_codeplug.cc b/lib/d868uv_codeplug.cc index 4eac8920..c6a981cb 100644 --- a/lib/d868uv_codeplug.cc +++ b/lib/d868uv_codeplug.cc @@ -198,20 +198,33 @@ static_assert( #define ADDR_DTMF_SETTINGS 0x024C1080 #define DTMF_SETTINGS_SIZE 0x00000050 - -#define NUM_TWO_TONE_ENC_FUNC 24 -#define TWO_TONE_ENC_BITMAP 0x024C1280 -#define TWO_TONE_ENC_BITMAP_SIZE 0x00000010 -#define ADDR_TWO_TONE_ENC_FUNC 0x024C1100 -#define TWO_TONE_ENC_FUNC_SIZE 0x00000010 -#define NUM_TWO_TONE_DEC_FUNC 24 -#define TWO_TONE_DEC_BITMAP 0x024c2600 -#define TWO_TONE_DEC_BITMAP_SIZE 0x00000010 -#define ADDR_TWO_TONE_DEC_FUNC 0x024c2400 -#define TWO_TONE_DEC_FUNC_SIZE 0x00000020 +static_assert( + DTMF_SETTINGS_SIZE == sizeof(D868UVCodeplug::dtmf_settings_t), + "D868UVCodeplug::dtmf_settings_t size check failed."); + +#define NUM_TWO_TONE_IDS 24 +#define TWO_TONE_IDS_BITMAP 0x024C1280 +#define TWO_TONE_IDS_BITMAP_SIZE 0x00000010 +#define ADDR_TWO_TONE_IDS 0x024C1100 +#define TWO_TONE_ID_SIZE 0x00000010 +static_assert( + TWO_TONE_ID_SIZE == sizeof(D868UVCodeplug::two_tone_id_t), + "D868UVCodeplug::two_tone_settings_t size check failed."); + +#define NUM_TWO_TONE_FUNCTIONS 16 +#define TWO_TONE_FUNCTIONS_BITMAP 0x024c2600 +#define TWO_TONE_FUNC_BITMAP_SIZE 0x00000010 +#define ADDR_TWO_TONE_FUNCTIONS 0x024c2400 +#define TWO_TONE_FUNCTION_SIZE 0x00000020 +static_assert( + TWO_TONE_FUNCTION_SIZE == sizeof(D868UVCodeplug::two_tone_function_t), + "D868UVCodeplug::two_tone_settings_t size check failed."); #define ADDR_TWO_TONE_SETTINGS 0x024C1290 #define TWO_TONE_SETTINGS_SIZE 0x00000010 +static_assert( + TWO_TONE_SETTINGS_SIZE == sizeof(D868UVCodeplug::two_tone_settings_t), + "D868UVCodeplug::two_tone_settings_t size check failed."); #define ADDR_DMR_ENCRYPTION_LIST 0x024C1700 #define DMR_ENCRYPTION_LIST_SIZE 0x00000040 @@ -1334,8 +1347,8 @@ D868UVCodeplug::D868UVCodeplug(QObject *parent) // 5-Tone function bitmaps image(0).addElement(FIVE_TONE_ID_BITMAP, FIVE_TONE_ID_BITMAP_SIZE); // 2-Tone function bitmaps - image(0).addElement(TWO_TONE_ENC_BITMAP, TWO_TONE_ENC_BITMAP_SIZE); - image(0).addElement(TWO_TONE_DEC_BITMAP, TWO_TONE_DEC_BITMAP_SIZE); + image(0).addElement(TWO_TONE_IDS_BITMAP, TWO_TONE_IDS_BITMAP_SIZE); + image(0).addElement(TWO_TONE_FUNCTIONS_BITMAP, TWO_TONE_FUNC_BITMAP_SIZE); } void @@ -1366,9 +1379,10 @@ D868UVCodeplug::allocateUpdated() { this->allocate5ToneSettings(); this->allocate2ToneIDs(); - image(0).addElement(ADDR_TWO_TONE_SETTINGS, TWO_TONE_SETTINGS_SIZE); + this->allocate2ToneFunctions(); + this->allocate2ToneSettings(); - image(0).addElement(ADDR_DTMF_SETTINGS, DTMF_SETTINGS_SIZE); + this->allocateDTMFSettings(); image(0).addElement(ADDR_DMR_ENCRYPTION_LIST, DMR_ENCRYPTION_LIST_SIZE); image(0).addElement(ADDR_DMR_ENCRYPTION_KEYS, DMR_ENCRYPTION_KEYS_SIZE); @@ -2203,19 +2217,35 @@ D868UVCodeplug::allocate5ToneSettings() { void D868UVCodeplug::allocate2ToneIDs() { // Allocate 2-tone encoding - uint8_t *enc_bitmap = data(TWO_TONE_ENC_BITMAP); - for (uint8_t i=0; i * 024C1000 000080 5-tone settings, * see @c five_tone_settings_t. - * 024C1080 000050 DTMF settings. + * 024C1080 000050 DTMF settings, see @c dtmf_settings_t. * 024C1280 000010 2-tone encoding bitmap. * 024C1100 000010 2-tone encoding. * 024C1290 000010 2-tone settings. @@ -1235,7 +1235,8 @@ public: uint8_t _unused12[14]; ///< Unused bytes, set to 0x00. }; - /** Binary representation of a 5-tone id. + /** Binary representation of a 5-tone id, that is 5-tone codes being send. + * * Size 0x20 bytes. */ struct __attribute__((packed)) five_tone_id_t { /** Possible 5-tone encoding standards. */ @@ -1253,14 +1254,17 @@ public: uint8_t _pad1f; ///< Pad byte, set to 0. }; - /** Binary representation of a 5-tone function. + /** Binary representation of a 5-tone function, that is actions being performed on decoding of + * 5-tone codes. + * * Size 0x20 bytes. */ struct __attribute__((packed)) five_tone_function_t { + /** Possible function being performed on 5-tone decoding. */ enum Function : uint8_t { OPEN_SQUELCH=0, CALL_ALL, EMERGENCY_ALARM, REMOTE_KILL, REMOTE_STUN, REMOTE_WAKEUP, GROUP_CALL, }; - + /** Possible responses to 5-tone decoding. */ enum Response : uint8_t { RESP_NONE=0, RESP_TONE, RESP_TONE_RESPOND }; @@ -1277,10 +1281,11 @@ public: /** Binary representation of the 5-tone settings. * Size 0x80 bytes. */ struct __attribute__((packed)) five_tone_settings_t { + /** Possible responses to decoded 5-tone codes. */ enum Response : uint8_t { RESP_NONE = 0, RESP_TONE, RESP_TONE_RESPOND }; - + /** Possible 5-tone standards. */ enum Standard : uint8_t { ZVEI1 = 0, ZVEI2, ZVEI3, PZVEI, DZVEI, PDZVEI, CCIR1, CCIR2, PCCIR, EEA, EURO_SIGNAL, NATEL, MODAT, CCITT, EIA @@ -1325,6 +1330,68 @@ public: uint8_t _unused70[16]; ///< Unused, set to 0x00. }; + /** Digital representation of the 2-tone ID, that is 2-tone codes being send. */ + struct __attribute__((packed)) two_tone_id_t { + uint16_t frequencies[2]; ///< Tone frequencies in 0.1Hz, little endian. + uint32_t _unused04; ///< Unused, set to 0. + char name[7]; ///< Name, upto 7 ASCII chars, 0-pad. + uint8_t _pad0f; ///< Pad byte, set to 0x00. + }; + + /** Binary encoding of the 2-tone function, that is actions being performed on a particular + * 2-tone code. */ + struct __attribute__((packed)) two_tone_function_t { + /** Possible responses to a DTMF decode. */ + enum Response : uint8_t { + RESP_NONE = 0, RESP_TONE, RESP_TONE_RESPOND + }; + + uint16_t frequencies[2]; ///< Tone frequencies in 0.1Hz, little endian. + Response response; ///< Decoding response. + char name[7]; ///< Name, upto 7 ASCII chars, 0-padded. + uint32_t _unused0c; ///< Unused, set to 0. + uint8_t _unused10[16]; ///< Unused, set to 0x00. + }; + + /** Binary representation of the 2-tone settings. */ + struct __attribute__((packed)) two_tone_settings_t { + uint8_t _unused00[9]; ///< Unused, set to 0x00. + uint8_t tone1_dur; ///< First tone duration in multiples of 100ms. + uint8_t tone2_dur; ///< Second tone duration in multiples of 100ms. + uint8_t long_tone_dur; ///< Long tone duration in multiples of 100ms. + uint8_t gap_time; ///< Gap time in multiples of 10ms. + uint8_t auto_reset_time; ///< Auto-reset time in multiples of 10s. + uint8_t sidetone_enable; ///< Side-tone enable. + uint8_t _unused0f; ///< Unused, set to 0x00. + }; + + /** Binary representation of the DTMF settings. */ + struct __attribute__((packed)) dtmf_settings_t { + /** Possible responses to a DTMF decode. */ + enum Response : uint8_t { + RESP_NONE=0, RESP_TONE, RESP_TONE_RESPOND + }; + + uint8_t interval_symb; ///< Interval charater [0x0,0xf]. + uint8_t group_code; ///< Group code [0x0,0xf]. + Response response; ///< Decoding response. + uint8_t pre_time; ///< Pretime in multiple of 10ms. + uint8_t first_digit_time; ///< First digit duration in multiple of 10ms. + uint8_t auto_reset_time; ///< Auto-reset time in multiple of 10s. + uint8_t my_id[3]; ///< Self-ID 3 chars [0x0,0xf]. + uint8_t post_enc_delay; ///< Post encoding delay in multiple of 10ms. + uint8_t ptt_id_pause; ///< PTT ID pause time in seconds. + uint8_t ptt_id_enable; ///< PTT ID enable. + uint8_t d_code_pause; ///< D-code pause in seconds. + uint8_t sidetone_enable; ///< Side-tone enable. + uint16_t _unused0e; ///< Unused set 0. + + uint8_t bot_id[16]; ///< BOT PTT ID, max 16 chars [0x0-0xf], pad=0xff. + uint8_t eot_id[16]; ///< EOT PTT ID, max 16 chars [0x0-0xf], pad=0xff. + uint8_t remote_kill_id[16]; ///< Remote kill id, max 16 chars [0x0-0xf], pad=0xff. + uint8_t remote_stun_id[16]; ///< Remote stun id, max 16 chars [0x0-0xf], pad=0xff. + }; + /** Represents an entry in the DMR ID -> contact map within the binary code-plug. */ struct __attribute__((packed)) contact_map_t { uint32_t id_group; ///< Combined ID and group-call flag. The ID is encoded in @@ -1487,8 +1554,15 @@ protected: /** Allocates 5-Tone settings. */ virtual void allocate5ToneSettings(); - /** Allocates all 2-Tone functions used. */ + /** Allocates all 2-Tone IDs used. */ virtual void allocate2ToneIDs(); + /** Allocates 2-Tone functions. */ + virtual void allocate2ToneFunctions(); + /** Allocates 2-Tone settings. */ + virtual void allocate2ToneSettings(); + + /** Allocates DTMF settings. */ + virtual void allocateDTMFSettings(); /** Internal used function to encode CTCSS frequencies. */ static uint8_t ctcss_code2num(Signaling::Code code); From db0fdfd08dab44585eb372741e03ca7e9d517bf1 Mon Sep 17 00:00:00 2001 From: Hannes Matuschek Date: Fri, 2 Jul 2021 22:42:45 +0200 Subject: [PATCH 16/16] Added friend flag for user-db. Fixed #82. --- lib/d868uv_callsigndb.hh | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/d868uv_callsigndb.hh b/lib/d868uv_callsigndb.hh index 674a3b1e..030b4007 100644 --- a/lib/d868uv_callsigndb.hh +++ b/lib/d868uv_callsigndb.hh @@ -52,8 +52,8 @@ public: uint8_t call_type; ///< Call type, see @c CallType. uint32_t id; ///< DMR ID, BCD encoded, big endian. - uint8_t ring; ///< Ring tone, see @c RingTone. - + uint8_t ring : 4, ///< Ring tone, see @c RingTone. + is_friend : 4; ///< If 0x1, entry is marked as a "friend". uint8_t body[94]; ///< Up to 94 bytes name, city, callsign, state, country and comment. /** Constructs a database entry from the given user.