1
|
'''
|
2
|
RA25 Channel Merge v1.0 - CowboyPilot
|
3
|
License: Use it however you want!
|
4
|
|
5
|
Usage: This script looks for two files - settings.dat and channels.csv - it then merges the header
|
6
|
from settings.dat and the channels in channels.csv.
|
7
|
|
8
|
settings.dat should be a file you save into the directory from the stock RA25 software. Make sure that
|
9
|
it is configured for the correct model of radio along with the radio specific settings you want
|
10
|
(Display options, alarm options, etc).
|
11
|
|
12
|
channels.csv should be a chirp standard format CSV file. It expects that Channel 1 is Channel 1 (Not zero)
|
13
|
|
14
|
The script searchs the settings.dat file for the signature of \x00\x01\x05 which immediately proceeds the
|
15
|
number of channels that RA25 software will read-in. You can actually set this much higher than how many
|
16
|
channels are actually in the file and it will work although it will give you a blank popup when you load
|
17
|
the file if you do that.
|
18
|
|
19
|
After that it writes the number of channels (in little endian) and continues to seek along the header file
|
20
|
until it finds the next signature that indicates the channel information is starting. All the settings for
|
21
|
the radio itself are in the header then it just lists channels until the last channel and the EOF is the
|
22
|
stop marker. It will export a file called RA25-Import.dat that you can open in the RA25 software and
|
23
|
send to your radio.
|
24
|
|
25
|
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
|
26
|
and second column is a 1 based index.
|
27
|
|
28
|
To-Do:
|
29
|
-All channels currently write as high power
|
30
|
-DTCS RX Support
|
31
|
'''
|
32
|
|
33
|
import csv, os
|
34
|
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']
|
35
|
|
36
|
def read_csv_to_array(file_path):
|
37
|
data_array = []
|
38
|
|
39
|
with open(file_path, 'r') as csvfile:
|
40
|
csvreader = csv.reader(csvfile)
|
41
|
next(csvreader)
|
42
|
for row in csvreader:
|
43
|
data_array.append(row)
|
44
|
#Remove any empty rows
|
45
|
data_array = [row for row in data_array if row[1]]
|
46
|
return data_array
|
47
|
|
48
|
def check_and_process_row(value):
|
49
|
if int(value[0]) >= 500:
|
50
|
return('Skip')
|
51
|
chst = b''
|
52
|
DTCSPositive = False # Added initialization
|
53
|
#Write Channel
|
54
|
channel = int(value[0])
|
55
|
chst += channel.to_bytes(2,'little')
|
56
|
chst += b'\x01\x00'
|
57
|
|
58
|
if value[14] == 'S':
|
59
|
chst += b'\x01'
|
60
|
else:
|
61
|
chst += b'\x00'
|
62
|
|
63
|
chst += b'\x00\x30'
|
64
|
|
65
|
#Write Frequency
|
66
|
without_period = value[2].replace('.', '')[:8]
|
67
|
padded_string = without_period.ljust(8, '0')
|
68
|
hex_representation = padded_string.encode('UTF-8')
|
69
|
chst += hex_representation
|
70
|
chst +=b'\x30'
|
71
|
|
72
|
#Write Offset
|
73
|
if(value[4]):
|
74
|
# Split the string around the period
|
75
|
left, right = value[4].split('.')
|
76
|
|
77
|
# Pad the left side to three characters
|
78
|
left_padded = left.rjust(3, '0')
|
79
|
|
80
|
# Pad the right side to four characters
|
81
|
right_padded = right[:5].ljust(5, '0')
|
82
|
|
83
|
# Concatenate the padded parts
|
84
|
offset = left_padded + right_padded
|
85
|
hex_representation = offset.encode('UTF-8')
|
86
|
chst += hex_representation
|
87
|
else:
|
88
|
chst += b'\x00\x00\x00\x00\x00\x00\x00\x00'
|
89
|
|
90
|
#Write Channel Name
|
91
|
# Convert item to Unicode string
|
92
|
unicode_string = str(value[1])
|
93
|
|
94
|
# Truncate or pad to exactly 8 Unicode characters
|
95
|
truncated_padded_unicode = unicode_string[:8].ljust(8, '\x20')
|
96
|
|
97
|
# Convert Unicode string to bytes and write out channel name.
|
98
|
byte_data = truncated_padded_unicode.encode('utf-8')
|
99
|
chst += byte_data
|
100
|
chst += b'\x00\x00'
|
101
|
#TX Off Flag
|
102
|
if value[3] == 'off':
|
103
|
chst += b'\x01'
|
104
|
else:
|
105
|
chst += b'\x00'
|
106
|
chst += b'\x00\x00\x00'
|
107
|
|
108
|
if value[12] == "NFM":
|
109
|
chst += b'\x01'
|
110
|
else:
|
111
|
chst += b'\x00'
|
112
|
chst += b'\x00\x00\x00\x00\x00\x00\x00'
|
113
|
|
114
|
if value[3] == '+':
|
115
|
chst += b'\x02'
|
116
|
elif value[3] == '-':
|
117
|
chst += b'\x01'
|
118
|
else:
|
119
|
chst += b'\x00'
|
120
|
|
121
|
chst += b'\x00\x02'
|
122
|
chst += b'\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00'
|
123
|
|
124
|
SetCTtx = False
|
125
|
SetCTrx = False
|
126
|
SetDTtx = False
|
127
|
SetDTrx = False
|
128
|
sqString = ''
|
129
|
if value[5] == "Tone":
|
130
|
SetCTtx = True
|
131
|
SetCTrx = False
|
132
|
elif value[5] == "TSQL":
|
133
|
SetCTtx = True
|
134
|
SetCTrx = True
|
135
|
elif value[5] == "DTCS":
|
136
|
SetDTtx = True
|
137
|
SetDTrx = False
|
138
|
elif value[5] == "Cross":
|
139
|
print(value[11][-5:])
|
140
|
if value[11][-5:] == ">DTCS":
|
141
|
print("Found RX DTCS")
|
142
|
SetDTrx = True
|
143
|
if value[11][:4] == "DTCS":
|
144
|
SetDTtx = True
|
145
|
if value[11][-5:] == ">Tone":
|
146
|
SetCTrx = True
|
147
|
if value[11][:4] == "Tone":
|
148
|
SetCTtx = True
|
149
|
|
150
|
if value[9] == "RR" or "RN":
|
151
|
DTCSPolTX = '2'
|
152
|
else:
|
153
|
DTCSPolTX = '3'
|
154
|
if value[9] == "RR" or "NR":
|
155
|
DTCSPolRX = '2'
|
156
|
else:
|
157
|
DTCSPolRX = '3'
|
158
|
|
159
|
if SetCTtx:
|
160
|
sqString += '1'
|
161
|
# Find the index of the search_value in the keys_array
|
162
|
index_of_search_value = keys_array.index(value[6])
|
163
|
# Convert the index to hex and format as three digits
|
164
|
hex_index = format(index_of_search_value, '03X')
|
165
|
sqString += hex_index
|
166
|
|
167
|
elif SetDTtx:
|
168
|
sqString += DTCSPolTX
|
169
|
#Convert to hex and format as three digits
|
170
|
decimal_value = int(value[8], 8)
|
171
|
# Convert to RA25 index format.
|
172
|
hex_value = format(decimal_value, '03x')
|
173
|
sqString += hex_value
|
174
|
|
175
|
else:
|
176
|
sqString="FFFF"
|
177
|
|
178
|
if SetCTrx:
|
179
|
sqString += '1'
|
180
|
# Find the index of the search_value in the keys_array
|
181
|
index_of_search_value = keys_array.index(value[7])
|
182
|
|
183
|
# Convert the index to hex and format as three digits
|
184
|
hex_index = format(index_of_search_value, '03X')
|
185
|
sqString += hex_index
|
186
|
|
187
|
elif SetDTrx:
|
188
|
sqString += DTCSPolRX
|
189
|
#Convert to hex and format as three digits
|
190
|
decimal_value = int(value[10], 8)
|
191
|
# Convert to RA25 index format.
|
192
|
hex_value = format(decimal_value, '03x')
|
193
|
sqString += hex_value
|
194
|
else:
|
195
|
sqString+="FFFF"
|
196
|
|
197
|
chst += sqString.encode('utf-8')
|
198
|
#end Encode Decode
|
199
|
|
200
|
#Pad to the end of the line
|
201
|
chst += b'\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00'
|
202
|
chst += b'\xe7\x05'
|
203
|
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'
|
204
|
return chst
|
205
|
|
206
|
|
207
|
|
208
|
def process_array(input_array):
|
209
|
rowcount=0
|
210
|
channel_bin=b''
|
211
|
byte_sequence = b''
|
212
|
|
213
|
#with open(header_file, 'rb') as file1:
|
214
|
# header_bin = file1.read()
|
215
|
|
216
|
for row in input_array:
|
217
|
if rowcount >= 500:
|
218
|
break
|
219
|
rowcount=rowcount+1
|
220
|
byte_sequence = check_and_process_row(row)
|
221
|
if byte_sequence != 'Skip':
|
222
|
channel_bin += byte_sequence
|
223
|
return channel_bin, rowcount
|
224
|
|
225
|
|
226
|
def write_to_binary_file(header_file, output_file, result_string):
|
227
|
with open(header_file, 'rb') as file1:
|
228
|
header_bin = file1.read()
|
229
|
result_string = header_bin + result_string
|
230
|
with open(output_file, 'wb') as binary_file:
|
231
|
binary_file.write(result_string)
|
232
|
|
233
|
def generate_donor_header(donor_file, rowcount):
|
234
|
#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
|
235
|
pattern_to_search = b'\x00\x01\x05'
|
236
|
replacement_bytes = rowcount.to_bytes(2,'little') +b'\x00'
|
237
|
#This is where the header cuts off and the channel entries start.
|
238
|
second_pattern_to_search = b'\x56\x32\x30\x30\x04\x00'
|
239
|
|
240
|
input_file_path = donor_file
|
241
|
output_file_path = 'header.bin'
|
242
|
|
243
|
|
244
|
with open(input_file_path, 'rb') as file:
|
245
|
file_content = file.read()
|
246
|
|
247
|
# Find the index of the first pattern
|
248
|
index_of_pattern = file_content.find(pattern_to_search)
|
249
|
|
250
|
if index_of_pattern != -1:
|
251
|
# Calculate the index of the bytes to replace
|
252
|
index_to_replace = index_of_pattern + len(pattern_to_search)
|
253
|
|
254
|
# Replace the bytes in the content
|
255
|
modified_content = (
|
256
|
file_content[:index_to_replace] +
|
257
|
replacement_bytes +
|
258
|
file_content[index_to_replace + len(replacement_bytes):]
|
259
|
)
|
260
|
|
261
|
# Find the index of the second pattern in the modified content
|
262
|
index_of_second_pattern = modified_content.find(second_pattern_to_search)
|
263
|
|
264
|
if index_of_second_pattern != -1:
|
265
|
# Truncate the content after the second pattern
|
266
|
modified_content = modified_content[:index_of_second_pattern + len(second_pattern_to_search)]
|
267
|
|
268
|
# Write the modified content to a new file
|
269
|
with open(output_file_path, 'wb') as output_file:
|
270
|
output_file.write(modified_content)
|
271
|
else:
|
272
|
print("Second pattern not found in the modified content.")
|
273
|
else:
|
274
|
print("First pattern not found in the file.")
|
275
|
|
276
|
|
277
|
# Example usage:
|
278
|
donor_file = 'settings.dat'
|
279
|
header_file = 'header.bin'
|
280
|
chirp_csv = 'channels.csv'
|
281
|
output_file = 'RA25-Import.dat'
|
282
|
csv_data = read_csv_to_array(chirp_csv)
|
283
|
channel_string = process_array(csv_data)[0]
|
284
|
generate_donor_header(donor_file, process_array(csv_data)[1])
|
285
|
write_to_binary_file(header_file, output_file, channel_string)
|
286
|
os.remove('header.bin')
|
287
|
print("Complete!")
|
288
|
|
289
|
'''
|
290
|
0,01,Memory Location 01-255 (00-FF)
|
291
|
1,02,Memory Location (01 - Add 255) Little endian
|
292
|
2,03,Always 01
|
293
|
3,04,Always 00
|
294
|
4,05,"Scan Off Flag (01 - On, 00 - Off)"
|
295
|
5,06,Always 00
|
296
|
6,07,Begin Frequency Segment (always 30) XYZ.ABCDE
|
297
|
7,08,X (31 = 1)
|
298
|
8,09,Y (33=3)
|
299
|
9,10,Z (36 = 6)
|
300
|
A,11,A
|
301
|
B,12,B
|
302
|
C,13,C
|
303
|
D,14,D
|
304
|
E,15,E
|
305
|
F,16,End of Freq - Start Offset Delta (XYZ.ABCDE) (30)
|
306
|
10,17,X
|
307
|
11,18,Y
|
308
|
12,19,Z
|
309
|
13,20,A
|
310
|
14,21,B
|
311
|
15,22,C
|
312
|
16,23,D
|
313
|
17,24,E
|
314
|
18,25,Channel Name Char 1
|
315
|
19,26,Char 2
|
316
|
1A,27,Char 3
|
317
|
1B,28,Char 4
|
318
|
1C,29,Char 5
|
319
|
1D,30,Char 6
|
320
|
1E,31,Char 7
|
321
|
1F,32,Char 8
|
322
|
20,33,Step Flag (?) 00-08
|
323
|
21,34,00
|
324
|
22,35,TX Off (01 - On; 00 - Off)
|
325
|
23,36,00
|
326
|
24,37,Reverse (1 - On; 00 - Off)
|
327
|
25,38,00
|
328
|
26,39,"Wide / Narrow (01 - NFM, 00 - FM)"
|
329
|
27,40,00
|
330
|
28,41,00
|
331
|
29,42,00
|
332
|
2A,43,00
|
333
|
2B,44,00
|
334
|
2C,45,00
|
335
|
2D,46,00
|
336
|
2E,47,Offset (01 - Negative; 02 - Positive)
|
337
|
2F,48,00
|
338
|
30,49,TX Power (00 - Low; 01 - Med; 02 - High)
|
339
|
31,50,00
|
340
|
32,51,"Talk Around (01 - On, 02 - Off)"
|
341
|
33,52,00
|
342
|
34,53,00
|
343
|
35,54,00
|
344
|
36,55,00
|
345
|
37,56,00
|
346
|
38,57,Compander (01 - On; 02 - Off)
|
347
|
39,58,00
|
348
|
3A,59,00
|
349
|
3B,60,00
|
350
|
3C,61,00
|
351
|
3D,62,00
|
352
|
3E,63,00
|
353
|
3F,64,00
|
354
|
40,65,00
|
355
|
41,66,00
|
356
|
42,67,00
|
357
|
43,68,00
|
358
|
44,69,"Encode Flag (1 - CTCSS, 2 - DN, 3 - DI, 46464646 is off)"
|
359
|
45,70,Bit 1 (CTCSS - 0; DCS 0-1)
|
360
|
46,71,Bit 2 (CTCSS - 0-3; DCS 0-F)
|
361
|
47,72,Bit 3 (CTCSS - 0-2; DCS 0-F)
|
362
|
48,73,"Decode Flag (1 - CTCSS, 2 - DN, 3 - DI) (0-32 / 0-1FF)"
|
363
|
49,74,Bit 1
|
364
|
4A,75,Bit 2
|
365
|
4B,76,Bit 3
|
366
|
4C,77,00
|
367
|
4D,78,00
|
368
|
4E,79,00
|
369
|
4F,80,00
|
370
|
50,81,00
|
371
|
51,82,00
|
372
|
52,83,"Scramble (00 - Off, 01-0B 1-11, 0C - Custom)"
|
373
|
53,84,00
|
374
|
54,85,DTMF (01 - Beg; 02 - End; 03 - Both; 00 - Off)
|
375
|
55,86,00
|
376
|
56,87,5 Tone (01 - Beg; 02 - End; 03 - Both; 00 - Off)
|
377
|
57,88,00
|
378
|
58,89,Squelch Mode (CTCSS/DCS - 01; Carrier - 00)
|
379
|
59,90,00
|
380
|
5A,91,E7
|
381
|
5B,92,05
|
382
|
5C,93,Noise CNX (01 - On; 00 - Off)
|
383
|
5D,94,00
|
384
|
5E,95,00
|
385
|
5F,96,00
|
386
|
60,97,00
|
387
|
61,98,00
|
388
|
62,99,00
|
389
|
63,100,00
|
390
|
64,101,00
|
391
|
65,102,00
|
392
|
66,103,00
|
393
|
67,104,00
|
394
|
68,105,"BCL (01 - Repeater; 02 - Busy, 00 - Off)"
|
395
|
69,106,00
|
396
|
6A,107,00
|
397
|
6B,108,00
|
398
|
6C,109,00
|
399
|
6D,110,00
|
400
|
6E,111,00
|
401
|
6F,112,30
|
402
|
70,113,30
|
403
|
71,114,30
|
404
|
72,115,30
|
405
|
73,116,Custom Scramble Value (1300 - 14) (0-255)
|
406
|
74,117,Custom Scramble Value (1300 - 05) (256-?) (Little Endian)
|
407
|
75,118,00
|
408
|
76,119,00
|
409
|
'''
|
410
|
|