Project

General

Profile

New Model #9241 » ChirpImport v1.0.py

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

 
'''
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
'''

(1-1/2)