Project

General

Profile

New Model #9241 » ChirpImport v1.0.py

Chirp Translator - Matt Bickford, 11/12/2023 02:02 PM

 
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

    
(1-1/2)