Created
September 7, 2022 04:44
-
-
Save yeyus/476306a2dc12061e0dd2f49376234b89 to your computer and use it in GitHub Desktop.
Codeplug/Firmware upgrade tool for Retevis RT73 / Kydera CDR-300UV / Radioddity DB25-D)
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
""" Codeplug/Firmware upgrade tool for Retevis RT73 / Kydera CDR-300UV / Radioddity DB25-D) | |
This program is designed to allow you to use your radio (create/modify/save codeplug data) | |
as well as to upgrade the firmware (as supplied by the manufacturer) | |
The main purpose is to allow you to enjoy your radio without the need for manufacturer-produced, | |
buggy, windows-only software. | |
Encryption settings are (intentionally) not supported, as these are not permitted for amateur radio use. | |
Any feedback welcome! | |
73, David, M0DMP | |
This program is free software: you can redistribute it and/or modify it under | |
the terms of the GNU General Public License as published by the Free Software | |
Foundation, either version 3 of the License, or (at your option) any later | |
version. | |
This program is distributed in the hope that it will be useful, but WITHOUT | |
ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS | |
FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. | |
You should have received a copy of the GNU General Public License along with | |
this program. If not, see <http://www.gnu.org/licenses/>. | |
""" | |
__author__ = 'David Pye' | |
__contact__ = 'davidmpye@gmail.com' | |
__copyright__ = 'Copyright 2020-2021' | |
__deprecated__ = False | |
__email__ = 'davidmpye@gmail.com' | |
__license__ = 'GPLv3' | |
__maintainer__ = 'David Pye' | |
__status__ = 'Beta' | |
__version__ = '0.0.2' | |
__contributions__ = 'Dave MM7DBT' | |
import time, struct, sys | |
from enum import Enum | |
import csv, json, serial, argparse, platform, math | |
if platform.system() == 'Windows': | |
import ctypes | |
ctypes.windll.kernel32.SetConsoleTitleW('Codeplug/Firmware Tool by David M0DMP') | |
import functools | |
print = functools.partial(print, flush=True) | |
else: | |
channel_record_size = 32 | |
zone_record_size = 32 | |
contact_record_size = 16 | |
message_record_size = 40 | |
rx_group_record_size = 210 | |
scan_list_record_size = 216 | |
channel_count_address = 5009 | |
channel_count_len = 2 | |
contact_count_address = 5011 | |
contact_count_len = 2 | |
zone_count_address = 5007 | |
zone_count_len = 2 | |
contact_start_address = 111616 | |
rx_group_start_address = 59071 | |
scan_list_start_address = 5071 | |
message_block_1_start_addr = 4014 | |
message_block_1_count = 16 | |
message_block_2_start_addr = 299 | |
message_block_2_count = 84 | |
aprs_dmr_channel_start_address = 3659 | |
CTCSS_Tones = [ | |
62.5, 67.0, 69.3, 71.9, 74.4, 77.0, 79.7, 82.5, 85.4, 88.5, 91.5, 94.8, 97.4, 100.0, 103.5, 107.2, 110.9, 114.8, 118.8, | |
123.0, 127.3, 131.8, 136.5, 141.3, 146.2, 151.4, 156.7, 159.8, 162.2, 165.5, 167.9, 171.3, 173.8, 177.3, 179.9, 183.5, 186.2, | |
189.9, 192.8, 196.6, 199.5, 203.5, 206.5, 210.7, 218.1, 225.7, 229.1, 233.6, 241.8, 250.3, 254.1] | |
DCS_Codes = [ | |
17, 23, 25, 26, 31, 32, 36, 43, 47, 50, 51, 53, 54, 65, 71, 72, 73, 74, 114, | |
115, 116, 122, 125, 131, 132, 134, 143, 145, 152, 155, 156, 162, 165, 172, 174, 205, 212, 223, | |
225, 226, 243, 244, 245, 246, 251, 252, 255, 261, 263, 265, 266, 271, 274, 306, 311, 315, 325, | |
331, 332, 343, 346, 351, 356, 364, 365, 371, 411, 412, 413, 423, 431, 432, 445, 446, 452, 454, | |
455, 462, 464, 465, 466, 503, 506, 516, 523, 526, 532, 546, 565, 606, 612, 624, 627, 631, 632, | |
645, 646, 654, 662, 664, 703, 712, 723, 731, 732, 734, 743, 754] | |
Button_IDs = {0:'UNDEFINED', | |
1:'HI_LO_POWER', | |
2:'BACKLIGHT_TOGGLE', | |
3:'KEYLOCK_TOGGLE', | |
4:'VOX', | |
5:'ZONE_SWITCH', | |
6:'SCAN', | |
7:'SCAN_MODE_TOGGLE', | |
8:'RPTR_TALKAROUND', | |
9:'EMERGENCY_ALARM', | |
10:'ENCRYPTION_TOGGLE', | |
11:'CONTACTS', | |
12:'SMS', | |
13:'RADIO_REVIVE', | |
14:'RADIO_DETECTION', | |
15:'RADIO_KILL', | |
16:'REMOTE_MONITOR', | |
17:'MONITOR', | |
18:'PERMANENT_MONITOR', | |
19:'TONEBURST_1750HZ', | |
49:'DTMF_TOGGLE', | |
52:'ROAM_TOGGLE', | |
27:'GPS_TOGGLE', | |
44:'RECORD_TOGGLE', | |
38:'MANDOWN_TOGGLE', | |
53:'RELAY_TOGGLE', | |
54:'RELAY_MONITOR_TOGGLE', | |
62:'PROMISCUOUS_MODE', | |
255:'NOT_USED', | |
40:'MENU', | |
55:'UP', | |
56:'DOWN', | |
57:'BACK', | |
58:'DQT_QT', | |
59:'A_B_TOGGLE', | |
60:'VOL', | |
61:'VFO', | |
62:'PROMISCUOUS_MODE', | |
63:'DUAL_WATCH_TOGGLE'} | |
Frequency_Ranges = {0:'COMMERCIAL_EUROPE_0', | |
1:'COMMERCIAL_US_1', | |
2:'AMATEUR_EUROPE_3', | |
3:'AMATEUR_US_7', | |
4:'COMMERCIAL_8', | |
5:'AMATEUR_AUS_CAN_10', | |
6:'COMMERCIAL_13', | |
7:'AMATEUR_THAILAND_16', | |
8:'COMMERCIAL_THAILAND_17'} | |
class Parser: | |
def __init__(self): | |
pass | |
def fromBytes(self, definitions, bytes): | |
data = {} | |
for key in definitions: | |
definition = definitions[key] | |
if definition[0] == 'Bitmask': | |
b = bytes[definition[1]] & definition[2] | |
try: | |
val = definition[3][b] | |
except: | |
val = definition[4] | |
else: | |
if definition[0] == 'String': | |
val = bytes[definition[1]:definition[1] + definition[2]].decode('ascii', 'ignore').rstrip('\x00').replace('\x00', '') | |
else: | |
if definition[0] == 'Number': | |
val = int.from_bytes((bytes[definition[1]:definition[1] + definition[2]]), byteorder='little') | |
else: | |
if definition[0] == 'MaskNum': | |
val = bytes[definition[1]] & definition[2] | |
if len(definition) > 3: | |
val = definition[3](val) | |
data[key] = val | |
else: | |
return data | |
def toBytes(self, data, definitions, item): | |
for key in definitions: | |
definition = definitions[key] | |
if definition[0] == 'Bitmask': | |
for k, v in definition[3].items(): | |
if v == item[key]: | |
data[definition[1]] |= k | |
if definition[0] == 'String': | |
encoded_str = item[key].encode('ascii', 'ignore') | |
for p in range(len(item[key])): | |
data[definition[1] + p] = encoded_str[p] | |
elif definition[0] == 'Number': | |
data[definition[1]:definition[1] + definition[2]] = item[key].to_bytes(length=(definition[2]), byteorder='little') | |
elif definition[0] == 'MaskNum': | |
if len(definition) > 3: | |
data[definition[1]] |= int(definition[4](item[key])) & definition[2] | |
else: | |
data[definition[1]] |= int(item[key]) & definition[2] | |
dev_info = {} | |
dev_info['Factory Number'] = [ | |
'String', 0, 16] | |
dev_info['Serial Number'] = ['String', 16, 16] | |
dev_info['Model Number'] = ['String', 32, 16] | |
dev_info['FW Version'] = ['String', 48, 31] | |
dev_info['Frequency range'] = ['String', 80, 8] | |
dev_info['Update date'] = ['String', 96, 16] | |
dev_info['Firmware ID'] = ['String', 112, 16] | |
basic_parameters = {} | |
basic_parameters['Radio name'] = [ | |
'String', 128, 10] | |
basic_parameters['DMR ID'] = ['Number', 144, 3] | |
basic_parameters['Language'] = [ | |
'Bitmask', 149, 16, {0:'Chinese', 16:'English'}, 'English'] | |
basic_parameters['TimeoutTimer'] = ['Number', 4943, 1] | |
basic_parameters['Busy channel lockout'] = ['Bitmask', 166, 128, {0:'Off', 128:'On'}, 'Off'] | |
basic_parameters['VOX'] = ['Bitmask', 166, 64, {0:'Off', 64:'On'}, 'Off'] | |
basic_parameters['VOX sensitivity'] = ['MaskNum', 166, 15, lambda x: x + 1, lambda x: x - 1] | |
basic_parameters['Scan mode'] = ['Bitmask', 4989, 3, {0:'CO', 1:'TO', 2:'SE'}, 'CO'] | |
basic_parameters['End tone types'] = ['Bitmask', 4993, 3, {0:'55Hz', 1:"120'", 2:'180', 3:'240'}, '55Hz'] | |
basic_parameters['Squelch A level'] = ['MaskNum', 147, 15] | |
basic_parameters['Squelch B level'] = ['MaskNum', 147, 240, lambda x: x >> 4, lambda x: x << 4] | |
basic_parameters['Backlight'] = [ | |
'Bitmask', 149, 40, {0:'Off', 8:'On', 32:'Auto'}, 'On'] | |
basic_parameters['Keylock'] = ['Bitmask', 149, 68, {0:'Off', 4:'Auto', 64:'Manual', 68:'Manual & Auto'}, 'Off'] | |
basic_parameters['Roaming'] = ['Bitmask', 4987, 1, {0:'Off', 1:'On'}, 'Off'] | |
basic_parameters['Roaming mode'] = ['Bitmask', 4981, 3, {0:'Auto', 1:'Manual', 2:'Strong RSSI Priority'}, 'Auto'] | |
basic_parameters['RSSI set'] = ['MaskNum', 4982, 255, lambda x: -90 - x, lambda x: -1 * x - 90] | |
basic_parameters['Connect check timer'] = ['MaskNum', 4983, 255] | |
basic_parameters['Repeater check timer'] = ['MaskNum', 4984, 255] | |
basic_parameters['Connect timer'] = ['MaskNum', 4985, 9, lambda x: x + 1, lambda x: x - 1] | |
basic_parameters['Record set'] = ['Bitmask', 4992, 3, {0:'None', 1:'TX', 2:'RX', 3:'TX/RX'}, 'None'] | |
basic_parameters['Power Saving'] = [ | |
'Bitmask', 150, 4, {0:'Off', 4:'On'}, 'On'] | |
basic_parameters['Power Saving Ratio'] = ['Bitmask', 150, 24, {0:'1:1', 8:'1:2', 16:'1:4'}, 'def'] | |
basic_parameters['Power Saving Delay'] = ['MaskNum', 163, 255] | |
common_menu_parameters = {} | |
common_menu_parameters['Contact list'] = [ | |
'Bitmask', 174, 1, {0:'Off', 1:'On'}, 'On'] | |
common_menu_parameters['New contact'] = ['Bitmask', 174, 2, {0:'Off', 2:'On'}, 'On'] | |
common_menu_parameters['Manual dial'] = ['Bitmask', 174, 4, {0:'Off', 4:'On'}, 'On'] | |
common_menu_parameters['Ham contacts'] = ['Bitmask', 174, 8, {0:'Off', 8:'On'}, 'On'] | |
common_menu_parameters['Ham groups'] = ['Bitmask', 174, 16, {0:'Off', 16:'On'}, 'On'] | |
common_menu_parameters['Radio check'] = [ | |
'Bitmask', 186, 1, {0:'Off', 1:'On'}, 'On'] | |
common_menu_parameters['Call alert'] = ['Bitmask', 186, 2, {0:'Off', 2:'On'}, 'On'] | |
common_menu_parameters['Radio monitor'] = ['Bitmask', 186, 4, {0:'Off', 4:'On'}, 'On'] | |
common_menu_parameters['Radio disable'] = ['Bitmask', 186, 8, {0:'Off', 8:'On'}, 'On'] | |
common_menu_parameters['Radio enable'] = ['Bitmask', 186, 16, {0:'Off', 16:'On'}, 'On'] | |
common_menu_parameters['SMS write'] = [ | |
'Bitmask', 175, 1, {0:'Off', 1:'On'}, 'On'] | |
common_menu_parameters['SMS quick msg'] = ['Bitmask', 175, 2, {0:'Off', 2:'On'}, 'On'] | |
common_menu_parameters['SMS inbox'] = ['Bitmask', 175, 4, {0:'Off', 4:'On'}, 'On'] | |
common_menu_parameters['SMS outbox'] = ['Bitmask', 175, 8, {0:'Off', 8:'On'}, 'On'] | |
common_menu_parameters['SMS drafts'] = ['Bitmask', 175, 16, {0:'Off', 16:'On'}, 'On'] | |
common_menu_parameters['Call log outgoing'] = [ | |
'Bitmask', 176, 1, {0:'Off', 1:'On'}, 'On'] | |
common_menu_parameters['Call log received'] = ['Bitmask', 176, 2, {0:'Off', 2:'On'}, 'On'] | |
common_menu_parameters['Call log missed'] = ['Bitmask', 176, 4, {0:'Off', 4:'On'}, 'On'] | |
common_menu_parameters['Scan on/off'] = [ | |
'Bitmask', 177, 1, {0:'Off', 1:'On'}, 'On'] | |
common_menu_parameters['Scan list'] = ['Bitmask', 177, 2, {0:'Off', 2:'On'}, 'On'] | |
common_menu_parameters['Scan mode'] = ['Bitmask', 177, 4, {0:'Off', 4:'On'}, 'On'] | |
common_menu_parameters['Roam on/off'] = ['Bitmask', 177, 8, {0:'Off', 8:'On'}, 'On'] | |
common_menu_parameters['Scan running on/off'] = ['Bitmask', 16, 16, {0:'Off', 16:'On'}, 'On'] | |
common_menu_parameters['Zone list on/off'] = [ | |
'Bitmask', 178, 1, {0:'Off', 1:'On'}, 'On'] | |
common_menu_parameters['Language'] = [ | |
'Bitmask', 179, 1, {0:'Off', 1:'On'}, 'On'] | |
common_menu_parameters['Keylock'] = ['Bitmask', 179, 2, {0:'Off', 2:'On'}, 'On'] | |
common_menu_parameters['Backlight'] = ['Bitmask', 179, 4, {0:'Off', 4:'On'}, 'On'] | |
common_menu_parameters['LEDs'] = ['Bitmask', 179, 8, {0:'Off', 8:'On'}, 'On'] | |
common_menu_parameters['Display mode'] = ['Bitmask', 179, 16, {0:'Off', 16:'On'}, 'On'] | |
common_menu_parameters['Vox'] = ['Bitmask', 179, 32, {0:'Off', 32:'On'}, 'On'] | |
common_menu_parameters['Channel sw'] = ['Bitmask', 179, 64, {0:'Off', 64:'On'}, 'On'] | |
common_menu_parameters['Factory reset'] = ['Bitmask', 179, 128, {0:'Off', 128:'On'}, 'On'] | |
common_menu_parameters['Local repeat'] = [ | |
'Bitmask', 188, 1, {0:'Off', 1:'On'}, 'On'] | |
common_menu_parameters['ToT'] = [ | |
'Bitmask', 181, 1, {0:'Off', 1:'On'}, 'On'] | |
common_menu_parameters['Power set'] = ['Bitmask', 181, 2, {0:'Off', 2:'On'}, 'On'] | |
common_menu_parameters['Repeat set'] = ['Bitmask', 181, 4, {0:'Off', 4:'On'}, 'On'] | |
common_menu_parameters['Sleep mode'] = ['Bitmask', 181, 8, {0:'Off', 8:'On'}, 'On'] | |
common_menu_parameters['Squelch level'] = ['Bitmask', 181, 16, {0:'Off', 16:'On'}, 'On'] | |
common_menu_parameters['Wide/Narrow band'] = ['Bitmask', 181, 32, {0:'Off', 32:'On'}, 'On'] | |
common_menu_parameters['Busy channel lockout'] = ['Bitmask', 181, 64, {0:'Off', 64:'On'}, 'On'] | |
common_menu_parameters['Signalling'] = ['Bitmask', 181, 128, {0:'Off', 128:'On'}, 'On'] | |
common_menu_parameters['End tone types'] = [ | |
'Bitmask', 189, 1, {0:'Off', 1:'On'}, 'On'] | |
common_menu_parameters['Enc level'] = [ | |
'Bitmask', 180, 1, {0:'Off', 1:'On'}, 'On'] | |
common_menu_parameters['Profiles'] = [ | |
'Bitmask', 182, 1, {0:'Off', 1:'On'}, 'On'] | |
common_menu_parameters['Keytone'] = ['Bitmask', 182, 2, {0:'Off', 2:'On'}, 'On'] | |
common_menu_parameters['Power tone'] = ['Bitmask', 182, 4, {0:'Off', 4:'On'}, 'On'] | |
common_menu_parameters['Msg tone'] = ['Bitmask', 182, 8, {0:'Off', 8:'On'}, 'On'] | |
common_menu_parameters['Private call tone'] = ['Bitmask', 182, 16, {0:'Off', 16:'On'}, 'On'] | |
common_menu_parameters['Group call tone'] = ['Bitmask', 182, 32, {0:'Off', 32:'On'}, 'On'] | |
common_menu_parameters['Call tone'] = ['Bitmask', 182, 64, {0:'Off', 64:'On'}, 'On'] | |
common_menu_parameters['Power on tone'] = ['Bitmask', 182, 128, {0:'Off', 128:'On'}, 'On'] | |
common_menu_parameters['GPS'] = [ | |
'Bitmask', 183, 1, {0:'Off', 1:'On'}, 'On'] | |
common_menu_parameters['Torch'] = ['Bitmask', 183, 2, {0:'Off', 2:'On'}, 'On'] | |
common_menu_parameters['FM radio'] = ['Bitmask', 183, 4, {0:'Off', 4:'On'}, 'On'] | |
common_menu_parameters['Time'] = ['Bitmask', 183, 8, {0:'Off', 8:'On'}, 'On'] | |
common_menu_parameters['DTMF'] = ['Bitmask', 183, 16, {0:'Off', 16:'On'}, 'On'] | |
common_menu_parameters['Speaker handmic'] = ['Bitmask', 183, 32, {0:'Off', 32:'On'}, 'On'] | |
common_menu_parameters['APRS'] = ['Bitmask', 183, 64, {0:'Off', 64:'On'}, 'On'] | |
common_menu_parameters['Record set'] = [ | |
'Bitmask', 184, 1, {0:'Off', 1:'On'}, 'On'] | |
common_menu_parameters['Record list'] = ['Bitmask', 184, 2, {0:'Off', 2:'On'}, 'On'] | |
common_menu_parameters['Record clear'] = ['Bitmask', 184, 4, {0:'Off', 4:'On'}, 'On'] | |
common_menu_parameters['Record space'] = ['Bitmask', 184, 8, {0:'Off', 8:'On'}, 'On'] | |
common_menu_parameters['Radio ID'] = [ | |
'Bitmask', 185, 1, {0:'Off', 1:'On'}, 'On'] | |
common_menu_parameters['RX group list'] = ['Bitmask', 185, 2, {0:'Off', 2:'On'}, 'On'] | |
common_menu_parameters['Channel contact'] = ['Bitmask', 185, 4, {0:'Off', 4:'On'}, 'On'] | |
common_menu_parameters['Version'] = ['Bitmask', 185, 8, {0:'Off', 8:'On'}, 'On'] | |
common_menu_parameters['VFO'] = [ | |
'Bitmask', 187, 1, {0:'Off', 1:'On'}, 'On'] | |
prompt_tone_parameters = {} | |
prompt_tone_parameters['Profiles'] = [ | |
'Bitmask', 167, 1, {0:'Standard', 1:'Silent'}, 'Standard'] | |
prompt_tone_parameters['SMS Prompt'] = ['MaskNum', 168, 15] | |
prompt_tone_parameters['Private call Tone'] = ['MaskNum', 169, 5] | |
prompt_tone_parameters['Group call Tone'] = ['MaskNum', 170, 5] | |
prompt_tone_parameters['Key tone'] = ['Bitmask', 171, 128, {0:'Off', 128:'On'}, 'On'] | |
prompt_tone_parameters['Key tone vol'] = ['MaskNum', 171, 15] | |
prompt_tone_parameters['Low bat alert tone'] = ['Bitmask', 172, 128, {0:'Off', 128:'On'}, 'On'] | |
prompt_tone_parameters['Low bat alert vol'] = ['MaskNum', 172, 15] | |
prompt_tone_parameters['Call hang up'] = ['Bitmask', 4824, 1, {0:'Off', 1:'On'}, 'On'] | |
prompt_tone_parameters['Boot ringtone'] = ['Bitmask', 149, 2, {0:'Off', 2:'On'}, 'On'] | |
prompt_tone_parameters['Roaming restart prompt'] = ['MaskNum', 4995, 15] | |
prompt_tone_parameters['Repeater selected prompt'] = ['MaskNum', 4995, 15] | |
indicator_parameters = {} | |
indicator_parameters['All'] = [ | |
'Bitmask', 173, 16, {16:'On', 0:'Off'}, 'On'] | |
indicator_parameters['Tx'] = ['Bitmask', 173, 8, {8:'On', 0:'Off'}, 'On'] | |
indicator_parameters['Rx'] = ['Bitmask', 173, 4, {4:'On', 0:'Off'}, 'On'] | |
indicator_parameters['Scanning'] = ['Bitmask', 173, 2, {2:'On', 0:'Off'}, 'On'] | |
indicator_parameters['Low battery'] = ['Bitmask', 173, 1, {1:'On', 0:'Off'}, 'On'] | |
button_preset_parameters = {} | |
button_preset_parameters['LongPressDuration'] = [ | |
'Bitmask', 193, 255, {0:'0.5', 1:'1.0', 2:'1.5', 3:'2.0', 4:'2.5', 5:'3.0', 6:'3.5', 7:'4.0', 8:'4.5', 9:'5.0'}, '2.0'] | |
button_preset_parameters['P1 LongPress'] = ['Bitmask', 194, 255, Button_IDs, 'UNDEFINED'] | |
button_preset_parameters['P1 ShortPress'] = ['Bitmask', 195, 255, Button_IDs, 'UNDEFINED'] | |
button_preset_parameters['P2 LongPress'] = ['Bitmask', 196, 255, Button_IDs, 'UNDEFINED'] | |
button_preset_parameters['P2 ShortPress'] = ['Bitmask', 197, 255, Button_IDs, 'UNDEFINED'] | |
button_preset_parameters['P3 LongPress'] = ['Bitmask', 198, 255, Button_IDs, 'UNDEFINED'] | |
button_preset_parameters['P3 ShortPress'] = ['Bitmask', 199, 255, Button_IDs, 'UNDEFINED'] | |
button_preset_parameters['P4 LongPress'] = ['Bitmask', 200, 255, Button_IDs, 'UNDEFINED'] | |
button_preset_parameters['P4 ShortPress'] = ['Bitmask', 201, 255, Button_IDs, 'UNDEFINED'] | |
button_preset_parameters['P5 LongPress'] = ['Bitmask', 202, 255, Button_IDs, 'UNDEFINED'] | |
button_preset_parameters['P5 ShortPress'] = ['Bitmask', 203, 255, Button_IDs, 'UNDEFINED'] | |
button_preset_parameters['P6 LongPress'] = ['Bitmask', 204, 255, Button_IDs, 'UNDEFINED'] | |
button_preset_parameters['P6 ShortPress'] = ['Bitmask', 205, 255, Button_IDs, 'UNDEFINED'] | |
button_preset_parameters['P7 LongPress'] = ['Bitmask', 4997, 255, Button_IDs, 'UNDEFINED'] | |
button_preset_parameters['P7 ShortPress'] = ['Bitmask', 4998, 255, Button_IDs, 'UNDEFINED'] | |
mic_gain_parameters = {} | |
mic_gain_parameters['Mic gain 1'] = [ | |
'Bitmask', 164, 128, {0:'Off', 128:'On'}, 'On'] | |
mic_gain_parameters['Mic gain 1 setting'] = ['MaskNum', 164, 7, lambda x: x * 4, lambda x: int(x / 4)] | |
mic_gain_parameters['Mic gain 2'] = ['Bitmask', 165, 128, {0:'Off', 128:'On'}, 'On'] | |
mic_gain_parameters['Mic gain 2 setting'] = ['MaskNum', 165, 31] | |
dmr_parameters = {} | |
dmr_parameters['Remote monitor duration'] = [ | |
'MaskNum', 4012, 255, lambda x: 10 + x * 10, lambda x: x / 10 - 1] | |
dmr_parameters['Remote monitor decode'] = ['Bitmask', 4013, 128, {0:'Off', 128:'On'}, 'Off'] | |
dmr_parameters['Remote kill decode'] = ['Bitmask', 4013, 64, {0:'Off', 64:'On'}, 'Off'] | |
dmr_parameters['Radio detection decode'] = ['Bitmask', 4013, 32, {0:'Off', 32:'On'}, 'Off'] | |
dmr_parameters['Radio revive decode'] = ['Bitmask', 4013, 16, {0:'Off', 16:'On'}, 'On'] | |
dmr_parameters['Call alert'] = ['Bitmask', 4013, 8, {0:'Off', 8:'On'}, 'Off'] | |
dmr_parameters['Group call hang time'] = ['MaskNum', 4944, 255, lambda x: x * 500, lambda x: int(x / 500)] | |
dmr_parameters['Private call hang time'] = ['MaskNum', 4945, 15, lambda x: x * 500, lambda x: int(x / 500)] | |
dmr_parameters['Import delay'] = ['MaskNum', 4976, 255, lambda x: x * 10, lambda x: x / 10] | |
dmr_parameters['DTMF duration (on-time)'] = ['MaskNum', 4977, 255, lambda x: x * 10, lambda x: int(x / 10)] | |
dmr_parameters['DTMF duration (off-time)'] = ['MaskNum', 4978, 255, lambda x: x * 10, lambda x: int(x / 10)] | |
dmr_parameters['DTMF volume (local)'] = ['MaskNum', 4979, 15] | |
dmr_parameters['DTMF On/off'] = ['Bitmask', 4963, 1, {0:'Off', 1:'On'}, 'Off'] | |
dmr_parameters['DTMF Code'] = ['String', 4963, 11] | |
dmr_parameters['GPS On/off'] = [ | |
'Bitmask', 4986, 1, {0:'Off', 1:'On'}, 'Off'] | |
dmr_parameters['GPS Interval'] = ['MaskNum', 4947, 255] | |
dmr_parameters['GPS Channel group ID'] = [ | |
'Number', 4948, 2] | |
dmr_parameters['GPS Channel channel ID'] = ['Number', 4950, 2] | |
dmr_parameters['Mandown On/Off'] = [ | |
'Bitmask', 4996, 1, {0:'Off', 1:'On'}, 'Off'] | |
dmr_parameters['Mandown Interval'] = ['MaskNum', 4946, 255] | |
dmr_parameters['Mandown Angle'] = ['MaskNum', 4952, 255] | |
dmr_parameters['Mandown Duration'] = ['MaskNum', 4953, 255] | |
aprs_parameters = {} | |
aprs_parameters['Manual TX interval'] = [ | |
'Number', 3723, 1] | |
aprs_parameters['Auto TX interval'] = [ | |
'MaskNum', 3724, 255, lambda x: x * 30, lambda x: int(x / 30)] | |
aprs_parameters['Beacon'] = ['Bitmask', 3726, 1, {0:'FIXED_LOCATION', 1:'GPS_LOCATION'}, 'GPS_LOCATION'] | |
aprs_parameters['LatNS'] = [ | |
'Bitmask', 3727, 1, {0:'North', 1:'South'}, 'North'] | |
aprs_parameters['Lat Degrees'] = ['Number', 3729, 4] | |
aprs_parameters['LongEW'] = [ | |
'Bitmask', 3728, 1, {0:'East', 1:'West'}, 'East'] | |
aprs_parameters['Long Degrees'] = ['Number', 3733, 4] | |
aprs_parameters['AX25 TX Freq'] = [ | |
'Number', 3737, 4] | |
aprs_parameters['AX25 TX Power'] = ['Bitmask', 3745, 1, {0:'LOW', 1:'HIGH'}, 'LOW'] | |
aprs_parameters['AX25 QT/DQT'] = [ | |
'Number', 3741, 2] | |
aprs_parameters['AX25 APRS Tone'] = [ | |
'Bitmask', 3746, 1, {0:'OFF', 1:'ON'}, 'OFF'] | |
aprs_parameters['AX25 TX Delay'] = [ | |
'MaskNum', 3743, 255, lambda x: x * 20, lambda x: int(x / 20)] | |
aprs_parameters['AX25 Prewave time'] = [ | |
'MaskNum', 3744, 255, lambda x: x * 10, lambda x: int(x / 10)] | |
aprs_parameters['AX25 Your Callsign'] = [ | |
'String', 3755, 6] | |
aprs_parameters['AX25 Your SSID'] = ['Number', 3748, 1] | |
aprs_parameters['AX25 Dest Callsign'] = ['String', 3749, 6] | |
aprs_parameters['AX25 Dest SSID'] = ['Number', 3747, 1] | |
aprs_parameters['AX25 APRS Symbol Table'] = [ | |
'Number', 3761, 1] | |
aprs_parameters['AX25 APRS Map Icon'] = ['Number', 3762, 1] | |
aprs_parameters['AX25 APRS Signal Path'] = [ | |
'String', 3763, 20] | |
aprs_parameters['AX25 Your Sending Text'] = ['String', 3783, 61] | |
aprs_dmr_record_parameters = {} | |
aprs_dmr_record_parameters['Zone ID'] = [ | |
'Number', 0, 2] | |
aprs_dmr_record_parameters['Channel ID'] = ['Number', 2, 2] | |
aprs_dmr_record_parameters['Call Type'] = ['Bitmask', 7, 4, {0:'PRIVATE', 4:'GROUP'}, 'PRIVATE'] | |
aprs_dmr_record_parameters['PTT'] = ['Bitmask', 7, 8, {0:'OFF', 8:'ON'}, 'OFF'] | |
aprs_dmr_record_parameters['Report Slot'] = ['Bitmask', 7, 3, {0:'CURRENT', 1:'TS1', 2:'TS2', 3:'TS2'}, 'CURRENT'] | |
aprs_dmr_record_parameters['APRS TG'] = ['Number', 4, 3] | |
contact_parameters = {} | |
contact_parameters['ID'] = [ | |
'Number', 0, 2] | |
contact_parameters['Name'] = ['String', 3, 10] | |
contact_parameters['DMR ID'] = ['Number', 13, 3] | |
contact_parameters['Type'] = ['Bitmask', 2, 255, {4:'Group', 5:'Private', 6:'All Call', 7:'No-Address Call', 8:'RawData', 9:'Define Data', 10:'SPDATA'}, 'Group'] | |
scan_list_info = {} | |
scan_list_info['Name'] = [ | |
'String', 0, 10] | |
scan_list_info['Talkback'] = ['Bitmask', 11, 32, {0:'Off', 32:'On'}, 'Off'] | |
scan_list_info['Scan TX Mode'] = ['Bitmask', 11, 15, {0:'Current Channel', 4:'Last Operated Channel', 8:'Appointed Channel'}, 'Current Channel'] | |
scan_list_info['Appointed channel group ID'] = ['Number', 12, 2] | |
scan_list_info['Appointed channel channel ID'] = ['Number', 14, 2] | |
rx_group_info = {} | |
rx_group_info['Name'] = [ | |
'String', 0, 10] | |
zone_info = {} | |
zone_info['ID'] = [ | |
'Number', 0, 2] | |
zone_info['Name'] = ['String', 3, 10] | |
channel_info = {} | |
channel_info['ID'] = [ | |
'Number', 0, 2] | |
channel_info['Type'] = ['Bitmask', 20, 192, {0:'ANALOG', 64:'DIGITAL', 128:'D_A_TX_A', 192:'D_A_TX_D'}, 'ANALOG'] | |
channel_info['Name'] = ['String', 2, 10] | |
channel_info['Rx Freq'] = ['Number', 12, 4] | |
channel_info['Tx Freq'] = ['Number', 16, 4] | |
channel_info['Tx Power'] = ['Bitmask', 20, 32, {0:'LOW', 32:'HIGH'}, 'LOW'] | |
channel_info['Rx only'] = ['Bitmask', 25, 16, {0:'OFF', 16:'ON'}, 'OFF'] | |
channel_info['Alarm'] = ['Bitmask', 20, 8, {0:'OFF', 8:'ON'}, 'OFF'] | |
channel_info['Prompt'] = ['Bitmask', 20, 4, {0:'OFF', 4:'ON'}, 'OFF'] | |
channel_info['PCT'] = ['Bitmask', 20, 2, {0:'PATCS', 2:'OACSU'}, 'PATCS'] | |
channel_info['TS Rx'] = ['Bitmask', 20, 1, {0:'TS1', 1:'TS2'}, 'TS1'] | |
channel_info['TS Tx'] = ['Bitmask', 29, 2, {0:'TS1', 2:'TS2'}, 'TS1'] | |
channel_info['RX CC'] = ['MaskNum', 21, 15] | |
channel_info['TX CC'] = ['MaskNum', 29, 240, lambda x: x >> 4, lambda x: x << 4] | |
channel_info['MSG Type'] = ['Bitmask', 21, 16, {0:'UNCONFIRMED', 16:'CONFIRMED'}, 'UNCONFIRMED'] | |
channel_info['TX Policy'] = ['Bitmask', 21, 192, {0:'IMPOLITE', 64:'POLITE_TO_CC', 96:'POLITE_TO_ALL'}, 'IMPOLITE'] | |
channel_info['Group call list'] = ['Number', 23, 1] | |
channel_info['Scan List ID'] = [ | |
'Number', 24, 1] | |
channel_info['Default Contact ID'] = [ | |
'Number', 30, 1] | |
channel_info['EAS'] = ['Bitmask', 25, 15, {0:'OFF', 1:'A1', 2:'A2', 3:'A3', 4:'A4'}, 'OFF'] | |
channel_info['Bandwidth'] = ['Bitmask', 20, 16, {16:'25KHz', 0:'12.5KHz'}, '12.5KHz'] | |
channel_info['Tone Type Tx'] = [ | |
'Bitmask', 26, 12, {0:'OFF', 4:'CTCSS', 8:'DCS', 12:'DCS Invert'}, 'OFF'] | |
channel_info['Tone Tx'] = ['Number', 28, 1] | |
channel_info['Tone Type Rx'] = ['Bitmask', 26, 3, {0:'OFF', 1:'CTCSS', 2:'DCS', 3:'DCS Invert'}, 'OFF'] | |
channel_info['Tone Rx'] = ['Number', 27, 1] | |
channel_info['APRS Channel'] = [ | |
'MaskNum', 31, 240, lambda x: x >> 4, lambda x: x << 4] | |
channel_info['Relay Monitor'] = [ | |
'Bitmask', 21, 128, {0:'OFF', 128:'ON'}, 'OFF'] | |
channel_info['Relay Mode'] = ['Bitmask', 25, 192, {0:'OFF', 64:'RX ONLY', 128:'TX ONLY', 192:'RX AND TX'}, 'OFF'] | |
tson_info = {} | |
tson_info['TS Rx ON'] = [ | |
'Bitmask', 29, 1, {0:'ON', 1:'OFF'}, 'OFF'] | |
tson_info['TS Tx ON'] = ['Bitmask', 29, 4, {0:'ON', 4:'OFF'}, 'OFF'] | |
def decompileCodeplug(data): | |
codeplug = {} | |
debugMsg(2, 'Parsing Binary Codeplug To JSON') | |
num_channels = int.from_bytes((data[channel_count_address:channel_count_address + channel_count_len]), byteorder='little') | |
num_zones = int.from_bytes((data[zone_count_address:zone_count_address + zone_count_len]), byteorder='little') | |
num_contacts = int.from_bytes((data[contact_count_address:contact_count_address + contact_count_len]), byteorder='little') | |
contact_block_size = num_contacts * contact_record_size | |
debugMsg(3, 'Contacts Block Size ' + str(contact_block_size)) | |
if contact_block_size % 1024 != 0: | |
contact_block_size += 1024 - contact_block_size % 1024 | |
debugMsg(3, 'Contacts Block Padded ' + str(contact_block_size)) | |
if contact_block_size // 1024 % 2 == 0: | |
debugMsg(3, 'Contacts Block Count Was Even - Adding another block') | |
contact_block_size = contact_block_size + 1024 | |
debugMsg(3, 'Contact Block Size ' + str(contact_block_size)) | |
zone_start_address = contact_start_address + contact_block_size | |
if zone_start_address % 1024 != 0: | |
zone_start_address += 1024 - zone_start_address % 1024 | |
debugMsg(3, 'Zones:' + str(num_zones) + ', start address ' + hex(zone_start_address)) | |
debugMsg(3, 'Channels:' + str(num_channels)) | |
debugMsg(3, 'Contacts:' + str(num_contacts) + ', start address: ' + hex(contact_start_address)) | |
p = Parser() | |
debugMsg(2, 'Parsing Device info') | |
codeplug['Device info'] = p.fromBytes(dev_info, data) | |
debugMsg(2, 'Parsing Basic parameters') | |
codeplug['Basic parameters'] = p.fromBytes(basic_parameters, data) | |
debugMsg(2, 'Parsing Common menu parameters') | |
codeplug['Common menu parameters'] = p.fromBytes(common_menu_parameters, data) | |
debugMsg(2, 'Parsing Prompt Tone') | |
codeplug['Prompt Tone'] = p.fromBytes(prompt_tone_parameters, data) | |
debugMsg(2, 'Parsing Indicators') | |
codeplug['Indicators'] = p.fromBytes(indicator_parameters, data) | |
debugMsg(2, 'Parsing Buttons') | |
codeplug['Preset buttons'] = p.fromBytes(button_preset_parameters, data) | |
debugMsg(2, 'Parsing Mic gain') | |
codeplug['Mic gain'] = p.fromBytes(mic_gain_parameters, data) | |
debugMsg(2, 'Parsing APRS settings') | |
codeplug['APRS'] = p.fromBytes(aprs_parameters, data) | |
if 159 <= codeplug['APRS']['AX25 QT/DQT'] <= 267: | |
codeplug['APRS']['AX25 QT/DQT'] = 'D' + str(DCS_Codes[(codeplug['APRS']['AX25 QT/DQT'] - 160)]).zfill(3) + 'I' | |
else: | |
if 52 <= codeplug['APRS']['AX25 QT/DQT'] <= 158: | |
codeplug['APRS']['AX25 QT/DQT'] = 'D' + str(DCS_Codes[(codeplug['APRS']['AX25 QT/DQT'] - 53)]).zfill(3) + 'N' | |
else: | |
if 1 <= codeplug['APRS']['AX25 QT/DQT'] <= 51: | |
codeplug['APRS']['AX25 QT/DQT'] = str(CTCSS_Tones[codeplug['APRS']['AX25 QT/DQT']]) | |
else: | |
codeplug['APRS']['AX25 QT/DQT'] = 'Off' | |
if codeplug['APRS']['Lat Degrees'] > 8999999: | |
codeplug['APRS']['Lat Degrees'] = 0 | |
if codeplug['APRS']['Long Degrees'] > 17999999: | |
codeplug['APRS']['Long Degrees'] = 0 | |
debugMsg(2, 'Parsing APRS DMR channels') | |
codeplug['APRS']['DMR channels'] = [] | |
for i in range(8): | |
codeplug['APRS']['DMR channels'].append(p.fromBytes(aprs_dmr_record_parameters, data[aprs_dmr_channel_start_address + i * 8:aprs_dmr_channel_start_address + (i + 1) * 8])) | |
else: | |
debugMsg(2, 'Parsing DMR Service') | |
codeplug['DMR Service'] = p.fromBytes(dmr_parameters, data) | |
debugMsg(2, 'Parsing Quick messages') | |
msg_str = '' | |
codeplug['Quick messages'] = [] | |
for i in range(message_block_1_count + message_block_2_count): | |
if i < message_block_1_count: | |
start_of_message = message_block_1_start_addr + i * message_record_size | |
else: | |
start_of_message = message_block_2_start_addr + (i - message_block_1_count) * message_record_size | |
try: | |
msg_str = data[start_of_message:start_of_message + message_record_size].decode('ascii', 'ignore').rstrip('\x00') | |
except: | |
debugMsg(1, ' Skipped message number ' + str(i) + ' containing non-ascii text') | |
else: | |
debugMsg(3, 'Adding message ' + str(i + 1) + ' - ' + msg_str) | |
codeplug['Quick messages'].append(msg_str) | |
else: | |
debugMsg(2, 'Parsing Contacts') | |
debugMsg(4, 'Contacts ' + str(num_contacts)) | |
codeplug['Contacts'] = [] | |
for i in range(num_contacts): | |
contact_data = data[contact_start_address + i * contact_record_size:contact_start_address + (i + 1) * contact_record_size] | |
parsed_contact = p.fromBytes(contact_parameters, contact_data) | |
debugMsg(3, 'Parsed contact - ' + str(parsed_contact)) | |
codeplug['Contacts'].append(parsed_contact) | |
else: | |
debugMsg(2, 'Parsing Scan lists') | |
codeplug['Scan lists'] = [] | |
for i in range(250): | |
record_start_addr = scan_list_start_address + i * scan_list_record_size | |
record = p.fromBytes(scan_list_info, data[record_start_addr:record_start_addr + scan_list_record_size]) | |
if record['Name'] != '': | |
record['Selected channels'] = [] | |
for j in range(50): | |
group = int.from_bytes((data[record_start_addr + 16 + j * 4:record_start_addr + 16 + j * 4 + 2]), byteorder='little') | |
channel = int.from_bytes((data[record_start_addr + 16 + j * 4 + 2:record_start_addr + 16 + j * 4 + 4]), byteorder='little') | |
if group != 0: | |
if channel != 0: | |
pair = {} | |
pair['Group'] = group | |
pair['Channel'] = channel | |
record['Selected channels'].append(pair) | |
debugMsg(3, 'Parsed scan list ' + str(record)) | |
codeplug['Scan lists'].append(record) | |
else: | |
debugMsg(2, 'Parsing Rx groups') | |
codeplug['RX groups'] = [] | |
for i in range(250): | |
record = p.fromBytes(rx_group_info, data[rx_group_start_address + i * rx_group_record_size:rx_group_start_address + (i + 1) * rx_group_record_size]) | |
record['Contacts'] = [] | |
for j in range(100): | |
id = int.from_bytes((data[rx_group_start_address + i * rx_group_record_size + 10 + j * 2:rx_group_start_address + i * rx_group_record_size + 10 + j * 2 + 2]), byteorder='little') | |
if id != 0: | |
record['Contacts'].append(id) | |
if not record['Name'] != '': | |
if len(record['Contacts']) != 0: | |
pass | |
debugMsg(3, 'Parsed rx group - ' + str(record)) | |
codeplug['RX groups'].append(record) | |
debugMsg(2, 'Parsing Zones') | |
codeplug['Zones'] = [] | |
for i in range(num_zones): | |
zone_data = data[zone_start_address + 32 * i:zone_start_address + 32 * (i + 1)] | |
parsed_zone = p.fromBytes(zone_info, zone_data) | |
num_channels = int.from_bytes((zone_data[15:17]), byteorder='little') | |
channels_offset = int.from_bytes((zone_data[13:15]), byteorder='little') | |
debugMsg(3, 'Zone ' + parsed_zone['Name'] + ' - channel offset address - ' + hex(channels_offset)) | |
channels_offset = (channels_offset - 1) * 32 | |
parsed_zone['Channels'] = [] | |
debugMsg(2, 'Parsing Channels') | |
for i in range(num_channels): | |
channel_data = data[zone_start_address + channels_offset + i * 32:zone_start_address + channels_offset + (i + 1) * 32] | |
channel = p.fromBytes(channel_info, channel_data) | |
tempTSON_INFO = p.fromBytes(tson_info, channel_data) | |
if channel['Rx Freq'] != channel['Tx Freq']: | |
tempTSON_INFO['TS Rx ON'] = 'OFF' | |
tempTSON_INFO['TS Tx ON'] = 'OFF' | |
if tempTSON_INFO['TS Rx ON'] == 'ON': | |
channel['TS Rx'] = 'ON' | |
if tempTSON_INFO['TS Tx ON'] == 'ON': | |
channel['TS Tx'] = 'ON' | |
if channel['Tone Type Tx'] == 'CTCSS': | |
channel['Tone Tx'] = CTCSS_Tones[channel['Tone Tx']] | |
else: | |
if channel['Tone Type Tx'] != 'OFF': | |
channel['Tone Tx'] = DCS_Codes[channel['Tone Tx']] | |
elif channel['Tone Type Rx'] == 'CTCSS': | |
channel['Tone Rx'] = CTCSS_Tones[channel['Tone Rx']] | |
else: | |
if channel['Tone Type Rx'] != 'OFF': | |
channel['Tone Rx'] = DCS_Codes[channel['Tone Rx']] | |
parsed_zone['Channels'].append(channel) | |
debugMsg(3, 'Parsed channel - ' + str(channel)) | |
else: | |
codeplug['Zones'].append(parsed_zone) | |
else: | |
debugMsg(2, 'Success!') | |
return json.dumps(codeplug, indent=2) | |
def compileCodeplug(data): | |
codeplug = json.loads(data) | |
num_contacts = len(codeplug['Contacts']) | |
debugMsg(4, 'Num Contacts ' + str(num_contacts)) | |
num_zones = len(codeplug['Zones']) | |
num_channels = 0 | |
for i in codeplug['Zones']: | |
num_channels += len(i['Channels']) | |
else: | |
contact_block_size = num_contacts * contact_record_size | |
debugMsg(2, 'Contacts Block Size ' + str(contact_block_size)) | |
if contact_block_size % 1024 != 0: | |
contact_block_size += 1024 - contact_block_size % 1024 | |
debugMsg(2, 'Contacts Block Padded ' + str(contact_block_size)) | |
if contact_block_size // 1024 % 2 == 0: | |
debugMsg(2, 'Contacts Block Count Was Even') | |
contact_block_size = contact_block_size + 1024 | |
zone_start_address = contact_start_address + contact_block_size | |
debugMsg(2, 'Contact Start Address ' + str(contact_start_address)) | |
debugMsg(2, 'Zone Start Address ' + str(zone_start_address)) | |
channel_start_address = zone_start_address + 32 * num_zones | |
debugMsg(2, 'Channel Start Address ' + str(channel_start_address)) | |
codeplug_size = channel_start_address + num_channels * channel_record_size | |
if codeplug_size % 2048 != 0: | |
codeplug_size += 2048 - codeplug_size % 2048 | |
debugMsg(2, 'Compiled codeplug of size ' + str(codeplug_size)) | |
template = bytearray(b'\x00' * codeplug_size) | |
template[zone_count_address:zone_count_address + 2] = num_zones.to_bytes(length=2, byteorder='little') | |
template[contact_count_address:contact_count_address + 2] = num_contacts.to_bytes(length=2, byteorder='little') | |
template[channel_count_address:channel_count_address + 2] = num_channels.to_bytes(length=2, byteorder='little') | |
try: | |
exists = codeplug['DMR Service']['Mandown On/Off'] | |
except: | |
codeplug['DMR Service']['Mandown On/Off'] = 'Off' | |
else: | |
try: | |
exists = codeplug['DMR Service']['Mandown Interval'] | |
except: | |
codeplug['DMR Service']['Mandown Interval'] = 0 | |
try: | |
exists = codeplug['DMR Service']['Mandown Angle'] | |
except: | |
codeplug['DMR Service']['Mandown Angle'] = 0 | |
else: | |
try: | |
exists = codeplug['DMR Service']['Mandown Duration'] | |
except: | |
codeplug['DMR Service']['Mandown Duration'] = 0 | |
else: | |
p = Parser() | |
p.toBytes(template, dev_info, codeplug['Device info']) | |
p.toBytes(template, basic_parameters, codeplug['Basic parameters']) | |
p.toBytes(template, common_menu_parameters, codeplug['Common menu parameters']) | |
p.toBytes(template, prompt_tone_parameters, codeplug['Prompt Tone']) | |
p.toBytes(template, indicator_parameters, codeplug['Indicators']) | |
p.toBytes(template, button_preset_parameters, codeplug['Preset buttons']) | |
p.toBytes(template, mic_gain_parameters, codeplug['Mic gain']) | |
p.toBytes(template, dmr_parameters, codeplug['DMR Service']) | |
if codeplug['APRS']['AX25 QT/DQT'] == 'Off': | |
codeplug['APRS']['AX25 QT/DQT'] = 0 | |
else: | |
if codeplug['APRS']['AX25 QT/DQT'].endswith('I'): | |
codeplug['APRS']['AX25 QT/DQT'] = DCS_Codes.index(int(str(codeplug['APRS']['AX25 QT/DQT']).replace('D', '').replace('I', ''))) + 160 | |
else: | |
if codeplug['APRS']['AX25 QT/DQT'].endswith('N'): | |
codeplug['APRS']['AX25 QT/DQT'] = DCS_Codes.index(int(str(codeplug['APRS']['AX25 QT/DQT']).replace('D', '').replace('N', ''))) + 53 | |
else: | |
codeplug['APRS']['AX25 QT/DQT'] = CTCSS_Tones.index(codeplug['APRS']['AX25 QT/DQT']) | |
p.toBytes(template, aprs_parameters, codeplug['APRS']) | |
aprs_dmr_record_count = 0 | |
for record in codeplug['APRS']['DMR channels']: | |
debugMsg(4, 'Writing APRS DMR channel record ' + str(aprs_dmr_record_count)) | |
debugMsg(5, 'Record : ' + str(record)) | |
aprs_dmr_record = bytearray(8) | |
p.toBytes(aprs_dmr_record, aprs_dmr_record_parameters, record) | |
template[aprs_dmr_channel_start_address + 8 * aprs_dmr_record_count:aprs_dmr_channel_start_address + 8 * (aprs_dmr_record_count + 1)] = aprs_dmr_record | |
aprs_dmr_record_count = aprs_dmr_record_count + 1 | |
else: | |
for i in range(len(codeplug['Quick messages'])): | |
debugMsg(4, 'Parsing message ' + codeplug['Quick messages'][i]) | |
if i < message_block_1_count: | |
start_address = message_block_1_start_addr + i * message_record_size | |
else: | |
start_address = message_block_2_start_addr + (i - message_block_1_count) * message_record_size | |
encoded_str = codeplug['Quick messages'][i][0:message_record_size].encode('ascii', 'ignore') | |
template[start_address:start_address + len(encoded_str)] = encoded_str | |
else: | |
count = 0 | |
for contact in codeplug['Contacts']: | |
contact_record = bytearray(contact_record_size) | |
try: | |
p.toBytes(contact_record, contact_parameters, contact) | |
except: | |
raise SystemExit('Contact Error:\n' + str(contact)) | |
else: | |
debugMsg(4, 'Contact ' + str(contact)) | |
template[contact_start_address + contact_record_size * count:contact_start_address + contact_record_size * (count + 1)] = contact_record | |
count = count + 1 | |
else: | |
count = 0 | |
for scan_list in codeplug['Scan lists']: | |
scan_list_record = bytearray(scan_list_record_size) | |
try: | |
p.toBytes(scan_list_record, scan_list_info, scan_list) | |
except: | |
raise SystemExit('Scan List Error:\n' + str(scan_list)) | |
else: | |
for i in range(len(scan_list['Selected channels'])): | |
scan_list_record[16 + i * 4:16 + i * 4 + 2] = scan_list['Selected channels'][i]['Group'].to_bytes(byteorder='little', length=2) | |
scan_list_record[16 + i * 4 + 2:16 + i * 4 + 4] = scan_list['Selected channels'][i]['Channel'].to_bytes(byteorder='little', length=2) | |
else: | |
template[scan_list_start_address + scan_list_record_size * count:scan_list_start_address + scan_list_record_size * (count + 1)] = scan_list_record | |
count = count + 1 | |
else: | |
count = 0 | |
for group in codeplug['RX groups']: | |
group_record = bytearray(rx_group_record_size) | |
try: | |
p.toBytes(group_record, rx_group_info, group) | |
except: | |
raise SystemExit('RX Group Error:\n' + str(group)) | |
else: | |
for i in range(len(group['Contacts'])): | |
group_record[10 + i * 2:10 + (i + 1) * 2] = group['Contacts'][i].to_bytes(byteorder='little', length=2) | |
else: | |
template[rx_group_start_address + rx_group_record_size * count:rx_group_start_address + rx_group_record_size * (count + 1)] = group_record | |
count = count + 1 | |
else: | |
count = 0 | |
for i in codeplug['Zones']: | |
channel_offset = len(codeplug['Zones']) + 1 | |
for j in range(count): | |
channel_offset += len(codeplug['Zones'][j]['Channels']) | |
else: | |
debugMsg(4, 'Channel offset for ' + i['Name'] + ' was ' + hex(channel_offset)) | |
zone_record = bytearray(zone_record_size) | |
try: | |
p.toBytes(zone_record, zone_info, i) | |
except: | |
raise SystemExit('Zone Error:\n' + i['Name']) | |
else: | |
zone_record[13:15] = channel_offset.to_bytes(length=2, byteorder='little') | |
zone_record[15:17] = len(i['Channels']).to_bytes(length=2, byteorder='little') | |
template[zone_start_address + zone_record_size * count:zone_start_address + zone_record_size * (count + 1)] = zone_record | |
count = count + 1 | |
else: | |
count = 0 | |
for i in codeplug['Zones']: | |
for channel in i['Channels']: | |
debugMsg(4, 'Writing channel ' + str(channel['ID']) + ' - ' + channel['Name'] + ' to address : ' + hex(channel_start_address + channel_record_size * count)) | |
if channel['Tone Type Tx'] != 'OFF': | |
if channel['Tone Tx'] == 0: | |
channel['Tone Type Tx'] = 'OFF' | |
if channel['Tone Type Rx'] != 'OFF': | |
if channel['Tone Rx'] == 0: | |
channel['Tone Type Rx'] = 'OFF' | |
if channel['Tone Type Tx'] == 'CTCSS': | |
channel['Tone Tx'] = CTCSS_Tones.index(channel['Tone Tx']) | |
elif channel['Tone Type Tx'] != 'OFF': | |
channel['Tone Tx'] = DCS_Codes.index(channel['Tone Tx']) | |
if channel['Tone Type Rx'] == 'CTCSS': | |
channel['Tone Rx'] = CTCSS_Tones.index(channel['Tone Rx']) | |
else: | |
if channel['Tone Type Rx'] != 'OFF': | |
channel['Tone Rx'] = DCS_Codes.index(channel['Tone Rx']) | |
tempTSON_INFO = {} | |
tempTSON_INFO['TS Rx ON'] = 'OFF' | |
tempTSON_INFO['TS Tx ON'] = 'OFF' | |
if channel['Rx Freq'] != channel['Tx Freq']: | |
tempTSON_INFO['TS Rx ON'] = 'OFF' | |
tempTSON_INFO['TS Tx ON'] = 'OFF' | |
if channel['TS Rx'] == 'ON': | |
tempTSON_INFO['TS Rx ON'] = 'ON' | |
if channel['TS Tx'] == 'ON': | |
tempTSON_INFO['TS Tx ON'] = 'ON' | |
channel_record = bytearray(channel_record_size) | |
try: | |
p.toBytes(channel_record, channel_info, channel) | |
except: | |
raise SystemExit('Channel Error:\n' + str(channel).replace(', ', '\n')) | |
else: | |
p.toBytes(channel_record, tson_info, tempTSON_INFO) | |
template[channel_start_address + channel_record_size * count:channel_start_address + channel_record_size * (count + 1)] = channel_record | |
count = count + 1 | |
else: | |
if len(template) != codeplug_size: | |
raise SystemExit('Codeplug Size Should Be ' + str(codeplug_size) + ' But Was ' + len(template)) | |
return template | |
def downloadCodeplug(serialdevice): | |
plug = bytearray() | |
with serial.Serial(serialdevice) as (port): | |
port.baudrate = 115200 | |
port.timeout = 5 | |
print('Establishing Connection To Radio...') | |
if port.isOpen() == False: | |
port.Open() | |
port.reset_input_buffer() | |
port.reset_output_buffer() | |
port.write('Flash Read '.encode('ascii')) | |
port.write(b'\x00<\x00\x00\x00\x00\x007\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00') | |
response = port.read(2) | |
if len(response) < 2: | |
raise SystemExit('No Response From Radio!') | |
else: | |
port.timeout = 2 | |
response = port.read(105) | |
if 'EARSAB' in response.decode('ascii', 'ignore'): | |
print('DR300UV: Begin Download...') | |
else: | |
if 'EHRSAB' in response.decode('ascii', 'ignore'): | |
print('DR880UV: Begin Download...') | |
else: | |
raise SystemExit('Unexpected Response From Radio!') | |
port.timeout = 5 | |
port.reset_input_buffer() | |
port.reset_output_buffer() | |
if debug_level == 4: | |
print('Message rx from plug download handshake:') | |
print(response) | |
for i in range(len(response)): | |
print((hex(response[i]) + ' '), end='') | |
else: | |
num_pages = response[16] + response[18] | |
if num_pages < 1: | |
raise SystemExit('Unexpected Response From Radio! - ' + response) | |
else: | |
for i in range(num_pages): | |
print('Reading Block ' + str(i + 1) + ' of ' + str(num_pages)) | |
port.write('Read'.encode('ascii')) | |
plug += port.read(2048) | |
print('Download Complete...') | |
return plug | |
def uploadCodeplug(serialdevice, data): | |
size = len(data) | |
if size % 2048 != 0: | |
data += b'\x00' * (2048 - size % 2048) | |
block_count = int(len(data) / 2048) | |
if block_count > 255: | |
raise SystemExit('Codeplug Too Large!') | |
response = bytearray('Flash Write'.encode('ascii')) + b'\x00<\x00\x00\x00\x00\x00\xff\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00' | |
response[18] = block_count | |
with serial.Serial(serialdevice) as (port): | |
port.baudrate = 115200 | |
port.timeout = 5 | |
print('Establishing Connection To Radio...') | |
if port.isOpen() == False: | |
port.Open() | |
else: | |
port.reset_input_buffer() | |
port.reset_output_buffer() | |
port.write(response) | |
bytes = port.read(2) | |
if len(bytes) < 2: | |
raise SystemExit('No Response From Radio!') | |
else: | |
port.timeout = 2 | |
bytes = port.read(100) | |
if 'EARSAB' in bytes.decode('ascii', 'ignore'): | |
print('DR300UV: Begin Upload...') | |
else: | |
if 'EHRSAB' in bytes.decode('ascii', 'ignore'): | |
print('DR880UV: Begin Upload...') | |
else: | |
raise SystemExit('Unexpected Response From Radio!') | |
port.timeout = 5 | |
port.reset_input_buffer() | |
port.reset_output_buffer() | |
if 'Write' not in bytes.decode('ascii', 'ignore'): | |
raise SystemExit('Unexpected Response From Radio! - ' + bytes.decode('ascii', 'ignore')) | |
for i in range(block_count): | |
print('Writing Block ' + str(i + 1) + ' of ' + str(block_count)) | |
port.write(data[2048 * i:2048 * (i + 1)]) | |
bytes = port.read(5) | |
if 'Write' in bytes.decode('ascii', 'ignore'): | |
pass | |
elif i == block_count - 1 and 'Check' in bytes.decode('ascii', 'ignore'): | |
pass | |
else: | |
raise SystemExit('Unexpected Response From Radio! - ' + bytes.decode('ascii', 'ignore')) | |
print('Upload Complete...') | |
def downloadFactoryCodeplug(serialdevice): | |
plug = bytearray() | |
with serial.Serial(serialdevice) as (port): | |
port.baudrate = 115200 | |
port.timeout = 5 | |
print('Establishing Connection To Radio...') | |
if port.isOpen() == False: | |
port.Open() | |
port.reset_input_buffer() | |
port.reset_output_buffer() | |
port.write('Flash Read '.encode('ascii')) | |
port.write(b'\x00<\x00\x00\x00\x00\x007\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00GHOST') | |
response = port.read(2) | |
if len(response) < 2: | |
raise SystemExit('No Response From Radio!') | |
else: | |
port.timeout = 2 | |
response = port.read(105) | |
if 'EARSAB' in response.decode('ascii', 'ignore'): | |
print('DR300UV: Begin Download...') | |
else: | |
if 'EHRSAB' in response.decode('ascii', 'ignore'): | |
print('DR880UV: Begin Download...') | |
else: | |
raise SystemExit('Unexpected Response From Radio!') | |
port.timeout = 5 | |
port.reset_input_buffer() | |
port.reset_output_buffer() | |
if debug_level == 4: | |
print('Message rx from plug download handshake:') | |
print(response) | |
for i in range(len(response)): | |
print((hex(response[i]) + ' '), end='') | |
else: | |
num_pages = response[18] + response[20] | |
if num_pages < 1: | |
raise SystemExit('Unexpected Response From Radio!') | |
else: | |
for i in range(num_pages): | |
print('Reading Block ' + str(i + 1) + ' of ' + str(num_pages)) | |
port.write('Read'.encode('ascii')) | |
plug += port.read(2048) | |
print('Download Complete...') | |
return plug | |
def uploadFactoryCodeplug(serialdevice, data): | |
size = len(data) | |
if size % 2048 != 0: | |
data += b'\x00' * (2048 - size % 2048) | |
block_count = int(len(data) / 2048) | |
if block_count > 255: | |
raise SystemExit('Codeplug Too Large!') | |
response = bytearray('Flash Write'.encode('ascii')) + b'\x00<\x00\x00\x00\x00\x00\xff\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00GHOST' | |
response[18] = block_count | |
with serial.Serial(serialdevice) as (port): | |
port.baudrate = 115200 | |
port.timeout = 5 | |
print('Establishing Connection To Radio...') | |
if port.isOpen() == False: | |
port.Open() | |
else: | |
port.reset_input_buffer() | |
port.reset_output_buffer() | |
port.write(response) | |
bytes = port.read(2) | |
if len(bytes) < 2: | |
raise SystemExit('No Response From Radio!') | |
else: | |
port.timeout = 2 | |
bytes = port.read(100) | |
if 'EARSAB' in bytes.decode('ascii', 'ignore'): | |
print('DR300UV: Begin Upload...') | |
else: | |
if 'EHRSAB' in bytes.decode('ascii', 'ignore'): | |
print('DR880UV: Begin Upload...') | |
else: | |
raise SystemExit('Unexpected Response From Radio!') | |
port.timeout = 5 | |
port.reset_input_buffer() | |
port.reset_output_buffer() | |
if 'Write' not in bytes.decode('ascii', 'ignore'): | |
raise SystemExit('Unexpected Response From Radio! - ' + bytes.decode('ascii', 'ignore')) | |
for i in range(block_count): | |
print('Writing Block ' + str(i + 1) + ' of ' + str(block_count)) | |
port.write(data[2048 * i:2048 * (i + 1)]) | |
bytes = port.read(5) | |
if 'Write' in bytes.decode('ascii', 'ignore'): | |
pass | |
elif i == block_count - 1 and 'Check' in bytes.decode('ascii', 'ignore'): | |
pass | |
else: | |
raise SystemExit('Unexpected Response From Radio! - ' + bytes.decode('ascii', 'ignore')) | |
print('Upload Complete...') | |
def uploadHamContacts(serialdevice, csvfile, contactbytes): | |
RADIO_ID = [] | |
CONTACT_INFO = [] | |
with open(csvfile, 'r', encoding='ascii', errors='surrogateescape') as (csv_file): | |
csv_reader = csv.DictReader(csv_file, delimiter=',') | |
for row in csv_reader: | |
RADIO_ID.append(int(row['RADIO_ID'])) | |
if len(row['LAST_NAME']) > 0 and row['LAST_NAME'] != ' ': | |
CONTACT_INFO.append(row['CALLSIGN'] + ',' + row['FIRST_NAME'] + ' ' + row['LAST_NAME'] + ',' + row['CITY'] + ',' + row['STATE'] + ',' + row['COUNTRY']) | |
else: | |
CONTACT_INFO.append(row['CALLSIGN'] + ',' + row['FIRST_NAME'] + ',' + row['CITY'] + ',' + row['STATE'] + ',' + row['COUNTRY']) | |
contactcount = len(RADIO_ID) | |
hamcontacts = bytearray(b'\x00' * contactbytes) | |
for i in range(contactcount): | |
hamcontacts[contactbytes * i:contactbytes * i + 2] = RADIO_ID[i].to_bytes(length=3, byteorder='little') | |
hamcontacts[contactbytes * i + 3:] = CONTACT_INFO[i].encode('ascii', 'ignore') | |
size = len(hamcontacts[contactbytes * i:contactbytes * i + contactbytes]) | |
if size % contactbytes != 0: | |
hamcontacts[contactbytes * i:contactbytes * i + contactbytes] += b'\x00' * (contactbytes - size % contactbytes) | |
else: | |
if len(hamcontacts) % 2048 != 0: | |
hamcontacts[len(hamcontacts):] = b'\x00' * (2048 - len(hamcontacts) % 2048) | |
print('Uploading ' + str(contactcount) + ' Contacts (' + str(contactbytes) + 'bytes)') | |
block_count = int(len(hamcontacts) / 2048) | |
if block_count > 18750: | |
raise SystemExit('Too Many Ham Contacts - 300,000 max!') | |
writecommand = b'' | |
writecommand += bytearray('Flash Write'.encode('ascii')) | |
writecommand += b'\x81\x10\x00\x00\x00\x00' | |
writecommand += bytearray(block_count.to_bytes(2, 'big')) | |
writecommand += b'\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00+' | |
writecommand += bytearray(contactbytes.to_bytes(1, 'little')) | |
writecommand += b'+\x00' | |
writecommand += bytearray(contactcount.to_bytes(3, 'big')) | |
with serial.Serial(serialdevice) as (port): | |
port.baudrate = 115200 | |
port.timeout = 5 | |
print('Establishing Connection To Radio...') | |
if port.isOpen() == False: | |
port.Open() | |
else: | |
port.reset_input_buffer() | |
port.reset_output_buffer() | |
port.write(writecommand) | |
bytes = port.read(2) | |
if len(bytes) < 2: | |
raise SystemExit('No Response From Radio!') | |
else: | |
port.timeout = 2 | |
bytes = port.read(100) | |
if 'EARSAB' in bytes.decode('ascii', 'ignore'): | |
print('DR300UV: Begin Upload...') | |
else: | |
if 'EHRSAB' in bytes.decode('ascii', 'ignore'): | |
print('DR880UV: Begin Upload...') | |
else: | |
raise SystemExit('Unexpected Response From Radio!') | |
port.timeout = 5 | |
port.reset_input_buffer() | |
port.reset_output_buffer() | |
if 'Write' not in bytes.decode('ascii', 'ignore'): | |
raise SystemExit('Unexpected Response From Radio! - ' + bytes.decode('ascii', 'ignore')) | |
for i in range(block_count): | |
print('Writing Block ' + str(i + 1) + ' of ' + str(block_count)) | |
port.write(hamcontacts[2048 * i:2048 * (i + 1)]) | |
bytes = port.read(5) | |
if 'Write' in bytes.decode('ascii', 'ignore'): | |
pass | |
elif 'Check' in bytes.decode('ascii', 'ignore'): | |
pass | |
else: | |
raise SystemExit('Unexpected Response From Radio! - ' + bytes.decode('ascii', 'ignore')) | |
print('Upload Complete...') | |
def uploadHamGroups(serialdevice, csvfile): | |
GROUP_ID = [] | |
GROUP_NAME = [] | |
with open(csvfile, 'r', encoding='ascii', errors='surrogateescape') as (csv_file): | |
csv_reader = csv.DictReader(csv_file, delimiter=',') | |
for row in csv_reader: | |
GROUP_ID.append(int(row['GROUP_ID'])) | |
GROUP_NAME.append(row['GROUP_NAME']) | |
groupcount = len(GROUP_ID) | |
hamgroups = bytearray(b'\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00') | |
for i in range(groupcount): | |
hamgroups[16 * i:16 * i + 2] = GROUP_ID[i].to_bytes(length=3, byteorder='little') | |
hamgroups[16 * i + 3:] = GROUP_NAME[i].encode('ascii', 'ignore') | |
size = len(hamgroups[16 * i:16 * i + 16]) | |
if size % 16 != 0: | |
hamgroups[16 * i:16 * i + 16] += b'\x00' * (16 - size % 16) | |
else: | |
if len(hamgroups) % 2048 != 0: | |
hamgroups[len(hamgroups):] = b'\x00' * (2048 - len(hamgroups) % 2048) | |
print('Uploading ' + str(groupcount) + ' Ham Groups') | |
block_count = int(len(hamgroups) / 2048) | |
if block_count > 235: | |
raise SystemExit('Too Many Ham Groups - 30,000 max!') | |
writecommand = bytearray('Flash Write'.encode('ascii')) | |
writecommand += b'\x82\x98\x00\x00\x00\x00' | |
writecommand += bytearray(block_count.to_bytes(2, 'big')) | |
writecommand += b'\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00+\x10+\x00' | |
writecommand += bytearray(groupcount.to_bytes(3, 'big')) | |
with serial.Serial(serialdevice) as (port): | |
port.baudrate = 115200 | |
port.timeout = 5 | |
print('Establishing Connection To Radio...') | |
if port.isOpen() == False: | |
port.Open() | |
else: | |
port.reset_input_buffer() | |
port.reset_output_buffer() | |
port.write(writecommand) | |
bytes = port.read(2) | |
if len(bytes) < 2: | |
raise SystemExit('No Response From Radio!') | |
else: | |
port.timeout = 2 | |
bytes = port.read(100) | |
if 'EARSAB' in bytes.decode('ascii', 'ignore'): | |
print('DR300UV: Begin Upload...') | |
else: | |
if 'EHRSAB' in bytes.decode('ascii', 'ignore'): | |
print('DR880UV: Begin Upload...') | |
else: | |
raise SystemExit('Unexpected Response From Radio!') | |
port.timeout = 5 | |
port.reset_input_buffer() | |
port.reset_output_buffer() | |
if 'Write' not in bytes.decode('ascii', 'ignore'): | |
raise SystemExit('Unexpected Response From Radio! - ' + bytes.decode('ascii', 'ignore')) | |
for i in range(block_count): | |
print('Writing Block ' + str(i + 1) + ' of ' + str(block_count)) | |
port.write(hamgroups[2048 * i:2048 * (i + 1)]) | |
bytes = port.read(5) | |
if 'Write' in bytes.decode('ascii', 'ignore'): | |
pass | |
elif 'Check' in bytes.decode('ascii', 'ignore'): | |
pass | |
else: | |
raise SystemExit('Unexpected Response From Radio!') | |
print('Upload Complete...') | |
def uploadFirmware(serialdevice, data): | |
with serial.Serial(serialdevice) as (port): | |
port.baudrate = 115200 | |
port.timeout = 2 | |
if port.isOpen() == False: | |
port.Open() | |
else: | |
port.reset_input_buffer() | |
port.reset_output_buffer() | |
print('Starting Firmware Upload Process') | |
size = len(data) | |
if size % 2048 != 0: | |
data += b'\x00' * (2048 - size % 2048) | |
else: | |
num_blocks = int(len(data) / 2048) | |
writecommand = bytearray('Erase'.encode('ascii')) | |
writecommand += b' \x00\x00\x00\x00\x00\x00' | |
writecommand += bytearray((num_blocks - 1).to_bytes(length=2, byteorder='big')) | |
port.write(writecommand) | |
bytes = port.read(2) | |
if len(bytes) < 2: | |
raise SystemExit('No Response From Radio!') | |
port.reset_input_buffer() | |
port.reset_output_buffer() | |
bytes = port.read_until(b'Erase ok') | |
print(bytes) | |
if 'IAP' not in bytes.decode('ascii', 'ignore'): | |
raise SystemExit('Invalid Response From Radio - Is The Radio In DFU Mode?') | |
else: | |
if 'Erase ok' in bytes.decode('ascii', 'ignore'): | |
print('Success: Begin Upload...') | |
for i in range(num_blocks): | |
block = data[2048 * i:2048 * (i + 1)] | |
print('Writing Block ' + str(i + 1) + ' of ' + str(num_blocks)) | |
port.write(block) | |
bytes = port.read(3) | |
if bytes.decode('ascii', 'ignore') == 'kyd': | |
pass | |
else: | |
raise SystemExit('Unexpected Response From Radio!') | |
else: | |
bytes = port.read(13) | |
if 'Check' in bytes.decode('ascii', 'ignore'): | |
print('Firmware Upload Complete') | |
else: | |
raise SystemExit('Checksum Not Received But May Still Have Completed Successfully.') | |
def DownloadBootLogo(serialdevice): | |
bootlogo = bytearray() | |
with serial.Serial(serialdevice) as (port): | |
port.baudrate = 115200 | |
port.timeout = 2 | |
print('Establishing Connection To Radio...') | |
if port.isOpen() == False: | |
port.Open() | |
else: | |
port.reset_input_buffer() | |
port.reset_output_buffer() | |
port.write('Flash Read '.encode('ascii')) | |
port.write(b'\x80\x04\x00\x00\x00\x00\x01~\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00') | |
bytes = port.read(2) | |
if len(bytes) < 2: | |
raise SystemExit('No Response From Radio!') | |
else: | |
bytes = port.read(36) | |
if len(bytes) < 36: | |
raise SystemExit('Invalid Response From Radio - Is The Radio In DFU Mode?') | |
else: | |
if 'Read_2M_V' in bytes.decode('ascii', 'ignore'): | |
print('Success: Begin Upload...') | |
else: | |
raise SystemExit('Invalid Response From Radio - Is The Radio In DFU Mode?') | |
port.reset_input_buffer() | |
port.reset_output_buffer() | |
num_pages = 20 | |
if num_pages < 1: | |
raise SystemExit('Unexpected Response From Radio!') | |
else: | |
for i in range(num_pages): | |
print('Reading Block ' + str(i + 1) + ' of ' + str(num_pages)) | |
port.write('Read'.encode('ascii')) | |
bootlogo += port.read(2048) | |
print('Download Complete...') | |
return bootlogo | |
def UploadBootLogo(serialdevice, data): | |
fileheader = b'\x10\x10\x00' | |
if data[0:3] != fileheader: | |
raise SystemExit('Incorrect Or Corrupt Boot Image Binary File!') | |
with serial.Serial(serialdevice) as (port): | |
port.baudrate = 115200 | |
port.timeout = 2 | |
print('Establishing Connection To Radio...') | |
if port.isOpen() == False: | |
port.Open() | |
else: | |
port.reset_input_buffer() | |
port.reset_output_buffer() | |
size = len(data) | |
if size % 2048 != 0: | |
data += b'\x00' * (2048 - size % 2048) | |
else: | |
num_blocks = int(len(data) / 2048) | |
response = bytearray('Flash Write'.encode('ascii')) + b'\x80\x04\x00\x00\x00\x00\x00\xff' | |
response[18] = num_blocks | |
port.write(response) | |
bytes = port.read(2) | |
if len(bytes) < 2: | |
raise SystemExit('No Response From Radio - Is The Radio In DFU Mode?') | |
bytes = port.read(37) | |
if len(bytes) < 37: | |
raise SystemExit('Invalid Response From Radio - Is The Radio In DFU Mode?') | |
else: | |
if 'Write_2M_V' in bytes.decode('ascii', 'ignore'): | |
print('Success: Begin Upload...') | |
else: | |
raise SystemExit('Invalid Response From Radio - Is The Radio In DFU Mode?') | |
port.reset_input_buffer() | |
port.reset_output_buffer() | |
bytes = port.read(5) | |
for i in range(num_blocks): | |
block = data[2048 * i:2048 * (i + 1)] | |
print('Writing Block ' + str(i + 1) + ' of ' + str(num_blocks)) | |
port.write(block) | |
bytes = port.read(5) | |
if 'Write' in bytes.decode('ascii', 'ignore'): | |
pass | |
else: | |
raise SystemExit('Unexpected Response From Radio!') | |
else: | |
bytes = port.read(13) | |
if 'Check' in bytes.decode('ascii', 'ignore'): | |
print('Boot Logo Upload Complete') | |
else: | |
raise SystemExit('Unexpected Response From Radio') | |
def downloadRecord(serialdevice): | |
raise SystemExit('Not Yet Implemented Correctly!') | |
record_data = bytearray() | |
with serial.Serial(serialdevice) as (port): | |
port.baudrate = 115200 | |
port.timeout = 5 | |
print('Establishing Connection To Radio...') | |
if port.isOpen() == False: | |
port.Open() | |
else: | |
port.write('Flash Read '.encode('ascii')) | |
port.write(b'\x00<\x00\x00\x00\x00\x00P\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00') | |
response = port.read(103) | |
print(response) | |
if len(response) < 11: | |
raise SystemExit('No Response From Radio!') | |
else: | |
print('Success: Begin Download...') | |
if debug_level == 4: | |
print('Message rx from record_data download handshake:') | |
print(response) | |
num_pages = response[18] + response[20] | |
if num_pages < 1: | |
raise SystemExit('Unexpected Response From Radio!') | |
else: | |
for i in range(num_pages): | |
print('Reading Block ' + str(i + 1) + ' of ' + str(num_pages)) | |
port.write('Read'.encode('ascii')) | |
record_data += port.read(2048) | |
print('Download Complete...') | |
return record_data | |
def debugMsg(level, message): | |
if level <= debug_level: | |
print(message) | |
debug_level = 0 | |
default_serial_device = '' | |
if platform.system() == 'Linux': | |
default_serial_device = '/dev/ttyUSB0' | |
else: | |
if platform.system() == 'Windows': | |
default_serial_device = 'COM1' | |
else: | |
parser = argparse.ArgumentParser(description='Codeplug/firmware upgrade tool, GNU GPL v3 or later, (C) 2020-21 David Pye davidmpye@gmail.com') | |
parser.add_argument('action', type=str, choices=['upload', 'upload_factory', 'download_bootlogo', 'upload_bootlogo', 'download', 'download_factory', 'flash_fw', 'download_bin', 'upload_bin', 'decompile_bin', 'upload_hamcontacts', 'upload_hamgroups', 'download_record'], help="upload - Compile and upload a JSON-formatted codeplug to the radio,upload_factory - Compile and upload a JSON-formatted codeplug to the radio as the 'factory reset' codeplug,download_bootlogo - Read the boot logo of the radio, file format is RGB565 binary with an 8 byte header,upload_bootlogo - Replace the boot logo of the radio, file format is RGB565 binary with an 8 byte header,download - Download the codeplug from the radio and convert into a JSON-formatted file,download_factory - Download the 'factory codeplug' from the radio and convert into a JSON-formatted file,flash_fw - Upgrade the radio's firmware (radio must be powered on while pressing P1, displaying a grey screen before upload),upload_hamcontacts - Upload Ham Contacts to the radio, file must be in RadioID.net CSV format and specify type with --contactbytes {16 or 128}upload_hamgroups - Upload Ham Groups to the radio, file must be in CSV format with headers 'GROUP_NAME' & 'GROUP_ID'") | |
parser.add_argument('filename', type=str, help='Filename to upload, or to save') | |
parser.add_argument('--contactbytes', default=0, type=int, choices=[16, 128], help='Ham Contacts Bytes (16 or 128)') | |
parser.add_argument('--device', default=default_serial_device, help='Specify device to use (default COM1 on Windows, default /dev/ttyUSB0 on Linux') | |
parser.add_argument('--debuglevel', default=[0], type=int, nargs=1, help='Debug level (0 = default, 4 = max)') | |
args = parser.parse_args() | |
debug_level = args.debuglevel[0] | |
if args.action == 'download': | |
data = downloadCodeplug(args.device) | |
json = decompileCodeplug(data) | |
f = open(args.filename, 'w') | |
f.write(json) | |
f.close() | |
else: | |
if args.action == 'download_factory': | |
data = downloadFactoryCodeplug(args.device) | |
json = decompileCodeplug(data) | |
f = open(args.filename, 'w') | |
f.write(json) | |
f.close() | |
else: | |
if args.action == 'upload': | |
f = open(args.filename, 'r') | |
input = f.read() | |
data = compileCodeplug(input) | |
f.close() | |
uploadCodeplug(args.device, data) | |
else: | |
if args.action == 'upload_factory': | |
f = open(args.filename, 'r') | |
input = f.read() | |
data = compileCodeplug(input) | |
f.close() | |
uploadFactoryCodeplug(args.device, data) | |
else: | |
if args.action == 'flash_fw': | |
f = open(args.filename, 'rb') | |
fw = f.read() | |
uploadFirmware(args.device, fw) | |
else: | |
if args.action == 'download_bin': | |
f = open(args.filename, 'wb') | |
f.write(downloadCodeplug(args.device)) | |
else: | |
if args.action == 'upload_bin': | |
f = open(args.filename, 'rb') | |
data = f.read() | |
uploadCodeplug(args.device, data) | |
else: | |
if args.action == 'decompile_bin': | |
f = open(args.filename, 'rb') | |
input = f.read() | |
data = decompileCodeplug(input) | |
f.close() | |
ff = open(args.filename[:-4] + '.json', 'w') | |
ff.write(data) | |
else: | |
if args.action == 'upload_hamcontacts': | |
if args.contactbytes == 0: | |
raise SystemExit("Missing Argument '--contactbytes {16 or 128}") | |
uploadHamContacts(args.device, args.filename, args.contactbytes) | |
else: | |
if args.action == 'upload_hamgroups': | |
uploadHamGroups(args.device, args.filename) | |
else: | |
if args.action == 'download_record': | |
f = open(args.filename, 'wb') | |
f.write(downloadRecord(args.device)) | |
else: | |
if args.action == 'download_bootlogo': | |
f = open(args.filename, 'wb') | |
f.write(DownloadBootLogo(args.device)) | |
else: | |
if args.action == 'upload_bootlogo': | |
f = open(args.filename, 'rb') | |
fw = f.read() | |
UploadBootLogo(args.device, fw) |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment