|
'''
|
|
RA25 Channel Merge v1.0 - CowboyPilot
|
|
License: Use it however you want!
|
|
|
|
Usage: This script looks for two files - settings.dat and channels.csv - it then merges the header
|
|
from settings.dat and the channels in channels.csv.
|
|
|
|
settings.dat should be a file you save into the directory from the stock RA25 software. Make sure that
|
|
it is configured for the correct model of radio along with the radio specific settings you want
|
|
(Display options, alarm options, etc).
|
|
|
|
channels.csv should be a chirp standard format CSV file. It expects that Channel 1 is Channel 1 (Not zero)
|
|
|
|
The script searchs the settings.dat file for the signature of \x00\x01\x05 which immediately proceeds the
|
|
number of channels that RA25 software will read-in. You can actually set this much higher than how many
|
|
channels are actually in the file and it will work although it will give you a blank popup when you load
|
|
the file if you do that.
|
|
|
|
After that it writes the number of channels (in little endian) and continues to seek along the header file
|
|
until it finds the next signature that indicates the channel information is starting. All the settings for
|
|
the radio itself are in the header then it just lists channels until the last channel and the EOF is the
|
|
stop marker. It will export a file called RA25-Import.dat that you can open in the RA25 software and
|
|
send to your radio.
|
|
|
|
Memory Map is in the comments at the end of the file as a comma delimited blob. It starts at the first 01 01 after the header information. First column is 00 based hex offset
|
|
and second column is a 1 based index.
|
|
|
|
To-Do:
|
|
-All channels currently write as high power
|
|
-DTCS RX Support
|
|
'''
|
|
|
|
import csv, os
|
|
keys_array = ['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']
|
|
|
|
def read_csv_to_array(file_path):
|
|
data_array = []
|
|
|
|
with open(file_path, 'r') as csvfile:
|
|
csvreader = csv.reader(csvfile)
|
|
next(csvreader)
|
|
for row in csvreader:
|
|
data_array.append(row)
|
|
#Remove any empty rows
|
|
data_array = [row for row in data_array if row[1]]
|
|
return data_array
|
|
|
|
def check_and_process_row(value):
|
|
if int(value[0]) >= 500:
|
|
return('Skip')
|
|
chst = b''
|
|
DTCSPositive = False # Added initialization
|
|
#Write Channel
|
|
channel = int(value[0])
|
|
chst += channel.to_bytes(2,'little')
|
|
chst += b'\x01\x00'
|
|
|
|
if value[14] == 'S':
|
|
chst += b'\x01'
|
|
else:
|
|
chst += b'\x00'
|
|
|
|
chst += b'\x00\x30'
|
|
|
|
#Write Frequency
|
|
without_period = value[2].replace('.', '')[:8]
|
|
padded_string = without_period.ljust(8, '0')
|
|
hex_representation = padded_string.encode('UTF-8')
|
|
chst += hex_representation
|
|
chst +=b'\x30'
|
|
|
|
#Write Offset
|
|
if(value[4]):
|
|
# Split the string around the period
|
|
left, right = value[4].split('.')
|
|
|
|
# Pad the left side to three characters
|
|
left_padded = left.rjust(3, '0')
|
|
|
|
# Pad the right side to four characters
|
|
right_padded = right[:5].ljust(5, '0')
|
|
|
|
# Concatenate the padded parts
|
|
offset = left_padded + right_padded
|
|
hex_representation = offset.encode('UTF-8')
|
|
chst += hex_representation
|
|
else:
|
|
chst += b'\x00\x00\x00\x00\x00\x00\x00\x00'
|
|
|
|
#Write Channel Name
|
|
# Convert item to Unicode string
|
|
unicode_string = str(value[1])
|
|
|
|
# Truncate or pad to exactly 8 Unicode characters
|
|
truncated_padded_unicode = unicode_string[:8].ljust(8, '\x20')
|
|
|
|
# Convert Unicode string to bytes and write out channel name.
|
|
byte_data = truncated_padded_unicode.encode('utf-8')
|
|
chst += byte_data
|
|
chst += b'\x00\x00'
|
|
#TX Off Flag
|
|
if value[3] == 'off':
|
|
chst += b'\x01'
|
|
else:
|
|
chst += b'\x00'
|
|
chst += b'\x00\x00\x00'
|
|
|
|
if value[12] == "NFM":
|
|
chst += b'\x01'
|
|
else:
|
|
chst += b'\x00'
|
|
chst += b'\x00\x00\x00\x00\x00\x00\x00'
|
|
|
|
if value[3] == '+':
|
|
chst += b'\x02'
|
|
elif value[3] == '-':
|
|
chst += b'\x01'
|
|
else:
|
|
chst += b'\x00'
|
|
|
|
chst += b'\x00\x02'
|
|
chst += b'\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00'
|
|
|
|
SetCTtx = False
|
|
SetCTrx = False
|
|
SetDTtx = False
|
|
SetDTrx = False
|
|
sqString = ''
|
|
if value[5] == "Tone":
|
|
SetCTtx = True
|
|
SetCTrx = False
|
|
elif value[5] == "TSQL":
|
|
SetCTtx = True
|
|
SetCTrx = True
|
|
elif value[5] == "DTCS":
|
|
SetDTtx = True
|
|
SetDTrx = False
|
|
elif value[5] == "Cross":
|
|
print(value[11][-5:])
|
|
if value[11][-5:] == ">DTCS":
|
|
print("Found RX DTCS")
|
|
SetDTrx = True
|
|
if value[11][:4] == "DTCS":
|
|
SetDTtx = True
|
|
if value[11][-5:] == ">Tone":
|
|
SetCTrx = True
|
|
if value[11][:4] == "Tone":
|
|
SetCTtx = True
|
|
|
|
if value[9] == "RR" or "RN":
|
|
DTCSPolTX = '2'
|
|
else:
|
|
DTCSPolTX = '3'
|
|
if value[9] == "RR" or "NR":
|
|
DTCSPolRX = '2'
|
|
else:
|
|
DTCSPolRX = '3'
|
|
|
|
if SetCTtx:
|
|
sqString += '1'
|
|
# Find the index of the search_value in the keys_array
|
|
index_of_search_value = keys_array.index(value[6])
|
|
# Convert the index to hex and format as three digits
|
|
hex_index = format(index_of_search_value, '03X')
|
|
sqString += hex_index
|
|
|
|
elif SetDTtx:
|
|
sqString += DTCSPolTX
|
|
#Convert to hex and format as three digits
|
|
decimal_value = int(value[8], 8)
|
|
# Convert to RA25 index format.
|
|
hex_value = format(decimal_value, '03x')
|
|
sqString += hex_value
|
|
|
|
else:
|
|
sqString="FFFF"
|
|
|
|
if SetCTrx:
|
|
sqString += '1'
|
|
# Find the index of the search_value in the keys_array
|
|
index_of_search_value = keys_array.index(value[7])
|
|
|
|
# Convert the index to hex and format as three digits
|
|
hex_index = format(index_of_search_value, '03X')
|
|
sqString += hex_index
|
|
|
|
elif SetDTrx:
|
|
sqString += DTCSPolRX
|
|
#Convert to hex and format as three digits
|
|
decimal_value = int(value[10], 8)
|
|
# Convert to RA25 index format.
|
|
hex_value = format(decimal_value, '03x')
|
|
sqString += hex_value
|
|
else:
|
|
sqString+="FFFF"
|
|
|
|
chst += sqString.encode('utf-8')
|
|
#end Encode Decode
|
|
|
|
#Pad to the end of the line
|
|
chst += b'\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00'
|
|
chst += b'\xe7\x05'
|
|
chst += b'\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x30\x30\x30\x30\x30\x00\x00\x00\x00'
|
|
return chst
|
|
|
|
|
|
|
|
def process_array(input_array):
|
|
rowcount=0
|
|
channel_bin=b''
|
|
byte_sequence = b''
|
|
|
|
#with open(header_file, 'rb') as file1:
|
|
# header_bin = file1.read()
|
|
|
|
for row in input_array:
|
|
if rowcount >= 500:
|
|
break
|
|
rowcount=rowcount+1
|
|
byte_sequence = check_and_process_row(row)
|
|
if byte_sequence != 'Skip':
|
|
channel_bin += byte_sequence
|
|
return channel_bin, rowcount
|
|
|
|
|
|
def write_to_binary_file(header_file, output_file, result_string):
|
|
with open(header_file, 'rb') as file1:
|
|
header_bin = file1.read()
|
|
result_string = header_bin + result_string
|
|
with open(output_file, 'wb') as binary_file:
|
|
binary_file.write(result_string)
|
|
|
|
def generate_donor_header(donor_file, rowcount):
|
|
#This 0105 is the indicator of where the total number of channels to be read is, a data file may contain more channels than are actually displayed
|
|
pattern_to_search = b'\x00\x01\x05'
|
|
replacement_bytes = rowcount.to_bytes(2,'little') +b'\x00'
|
|
#This is where the header cuts off and the channel entries start.
|
|
second_pattern_to_search = b'\x56\x32\x30\x30\x04\x00'
|
|
|
|
input_file_path = donor_file
|
|
output_file_path = 'header.bin'
|
|
|
|
|
|
with open(input_file_path, 'rb') as file:
|
|
file_content = file.read()
|
|
|
|
# Find the index of the first pattern
|
|
index_of_pattern = file_content.find(pattern_to_search)
|
|
|
|
if index_of_pattern != -1:
|
|
# Calculate the index of the bytes to replace
|
|
index_to_replace = index_of_pattern + len(pattern_to_search)
|
|
|
|
# Replace the bytes in the content
|
|
modified_content = (
|
|
file_content[:index_to_replace] +
|
|
replacement_bytes +
|
|
file_content[index_to_replace + len(replacement_bytes):]
|
|
)
|
|
|
|
# Find the index of the second pattern in the modified content
|
|
index_of_second_pattern = modified_content.find(second_pattern_to_search)
|
|
|
|
if index_of_second_pattern != -1:
|
|
# Truncate the content after the second pattern
|
|
modified_content = modified_content[:index_of_second_pattern + len(second_pattern_to_search)]
|
|
|
|
# Write the modified content to a new file
|
|
with open(output_file_path, 'wb') as output_file:
|
|
output_file.write(modified_content)
|
|
else:
|
|
print("Second pattern not found in the modified content.")
|
|
else:
|
|
print("First pattern not found in the file.")
|
|
|
|
|
|
# Example usage:
|
|
donor_file = 'settings.dat'
|
|
header_file = 'header.bin'
|
|
chirp_csv = 'channels.csv'
|
|
output_file = 'RA25-Import.dat'
|
|
csv_data = read_csv_to_array(chirp_csv)
|
|
channel_string = process_array(csv_data)[0]
|
|
generate_donor_header(donor_file, process_array(csv_data)[1])
|
|
write_to_binary_file(header_file, output_file, channel_string)
|
|
os.remove('header.bin')
|
|
print("Complete!")
|
|
|
|
'''
|
|
0,01,Memory Location 01-255 (00-FF)
|
|
1,02,Memory Location (01 - Add 255) Little endian
|
|
2,03,Always 01
|
|
3,04,Always 00
|
|
4,05,"Scan Off Flag (01 - On, 00 - Off)"
|
|
5,06,Always 00
|
|
6,07,Begin Frequency Segment (always 30) XYZ.ABCDE
|
|
7,08,X (31 = 1)
|
|
8,09,Y (33=3)
|
|
9,10,Z (36 = 6)
|
|
A,11,A
|
|
B,12,B
|
|
C,13,C
|
|
D,14,D
|
|
E,15,E
|
|
F,16,End of Freq - Start Offset Delta (XYZ.ABCDE) (30)
|
|
10,17,X
|
|
11,18,Y
|
|
12,19,Z
|
|
13,20,A
|
|
14,21,B
|
|
15,22,C
|
|
16,23,D
|
|
17,24,E
|
|
18,25,Channel Name Char 1
|
|
19,26,Char 2
|
|
1A,27,Char 3
|
|
1B,28,Char 4
|
|
1C,29,Char 5
|
|
1D,30,Char 6
|
|
1E,31,Char 7
|
|
1F,32,Char 8
|
|
20,33,Step Flag (?) 00-08
|
|
21,34,00
|
|
22,35,TX Off (01 - On; 00 - Off)
|
|
23,36,00
|
|
24,37,Reverse (1 - On; 00 - Off)
|
|
25,38,00
|
|
26,39,"Wide / Narrow (01 - NFM, 00 - FM)"
|
|
27,40,00
|
|
28,41,00
|
|
29,42,00
|
|
2A,43,00
|
|
2B,44,00
|
|
2C,45,00
|
|
2D,46,00
|
|
2E,47,Offset (01 - Negative; 02 - Positive)
|
|
2F,48,00
|
|
30,49,TX Power (00 - Low; 01 - Med; 02 - High)
|
|
31,50,00
|
|
32,51,"Talk Around (01 - On, 02 - Off)"
|
|
33,52,00
|
|
34,53,00
|
|
35,54,00
|
|
36,55,00
|
|
37,56,00
|
|
38,57,Compander (01 - On; 02 - Off)
|
|
39,58,00
|
|
3A,59,00
|
|
3B,60,00
|
|
3C,61,00
|
|
3D,62,00
|
|
3E,63,00
|
|
3F,64,00
|
|
40,65,00
|
|
41,66,00
|
|
42,67,00
|
|
43,68,00
|
|
44,69,"Encode Flag (1 - CTCSS, 2 - DN, 3 - DI, 46464646 is off)"
|
|
45,70,Bit 1 (CTCSS - 0; DCS 0-1)
|
|
46,71,Bit 2 (CTCSS - 0-3; DCS 0-F)
|
|
47,72,Bit 3 (CTCSS - 0-2; DCS 0-F)
|
|
48,73,"Decode Flag (1 - CTCSS, 2 - DN, 3 - DI) (0-32 / 0-1FF)"
|
|
49,74,Bit 1
|
|
4A,75,Bit 2
|
|
4B,76,Bit 3
|
|
4C,77,00
|
|
4D,78,00
|
|
4E,79,00
|
|
4F,80,00
|
|
50,81,00
|
|
51,82,00
|
|
52,83,"Scramble (00 - Off, 01-0B 1-11, 0C - Custom)"
|
|
53,84,00
|
|
54,85,DTMF (01 - Beg; 02 - End; 03 - Both; 00 - Off)
|
|
55,86,00
|
|
56,87,5 Tone (01 - Beg; 02 - End; 03 - Both; 00 - Off)
|
|
57,88,00
|
|
58,89,Squelch Mode (CTCSS/DCS - 01; Carrier - 00)
|
|
59,90,00
|
|
5A,91,E7
|
|
5B,92,05
|
|
5C,93,Noise CNX (01 - On; 00 - Off)
|
|
5D,94,00
|
|
5E,95,00
|
|
5F,96,00
|
|
60,97,00
|
|
61,98,00
|
|
62,99,00
|
|
63,100,00
|
|
64,101,00
|
|
65,102,00
|
|
66,103,00
|
|
67,104,00
|
|
68,105,"BCL (01 - Repeater; 02 - Busy, 00 - Off)"
|
|
69,106,00
|
|
6A,107,00
|
|
6B,108,00
|
|
6C,109,00
|
|
6D,110,00
|
|
6E,111,00
|
|
6F,112,30
|
|
70,113,30
|
|
71,114,30
|
|
72,115,30
|
|
73,116,Custom Scramble Value (1300 - 14) (0-255)
|
|
74,117,Custom Scramble Value (1300 - 05) (256-?) (Little Endian)
|
|
75,118,00
|
|
76,119,00
|
|
'''
|
|
|