Project

General

Profile

New Model #9241 » csv_to_dat.py

Fred DeKeyser, 01/11/2025 09:09 AM

 
# Convert a CHIRP CSV file and an AT779UV template settings file into a new AT779UV CPS data file.

# Free for non-commercial use.
# 01/10/25 CFD Initial release.
# 01/11/25 CFD Added multi-model check.

import sys, os, struct, csv, decimal

models = [b'AT779UV',b'RA_25UV'] # add your 7 character CPS radio model ID here
ctcss = ['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']
cDuplex = ['','-','+','split','off']
cMode = ['FM','NFM']
cStep = ['2.50','5.00','6.25','10.00','12.50','20.00','25.00','30.00','50.00']
cWatts = ['5.0W','10W','20W']
cPower = ['L','M','H']

def translate(D) :
B = bytearray(119)
rFreq = D['Frequency'].partition('.')
tOffset = D['Offset'].partition('.')
# Convert a split frequency to an offset.
rFreqVal = decimal.Decimal(D['Frequency'])
tOffsetVal = decimal.Decimal(D['Offset'])
tDuplex = cDuplex.index(D['Duplex'])
if 3 == tDuplex :
if tOffsetVal < rFreqVal : # negative offset
tDuplex = 1
tOffsetVal = rFreqVal - tOffsetVal
tOffset = str(tOffsetVal).partition('.')
else : # positive offset
tDuplex = 2
tOffsetVal = tOffsetVal - rFreqVal
tOffset = str(tOffsetVal).partition('.')
elif tDuplex > 2 : tDuplex = 0 # convert 'off' and invalid values
# Retrieve power and special feature configurations from the comment field.
cComment = D['Comment'].split(' ')
tPower = 2 # default transmit power to high
cTXO = False
cREV = False
cTA = False
cCPD = False
cNC = False
cBCLO = 0
for feat in cComment :
match feat :
case 'L' | 'M' | 'H' :
tPower = cPower.index(feat)
case 'TXO' :
cTXO = True
case 'REV' :
cREV = True
case 'TA' :
cTA = True
case 'CPD' :
cCPD = True
case 'NC' :
cNC = True
case 'BCLO=R' :
cBCLO = 1
case 'BCLO=B' :
cBCLO = 2
if 'Power' in D and D['Power'] in cWatts : tPower = cWatts.index(D['Power'])
# Decode CTCSS/DCS state.
tMode = b'F' # default off states
tCode = b'FFF'
rMode = b'F'
rCode = b'FFF'
tCTCSS = False
tDCS = False
rCTCSS = False
rDCS = False
rSquelch = False
match D['Tone'] :
case 'Tone' :
tCTCSS = True
case 'TSQL' :
tCTCSS = True
rCTCSS = True
case 'TSQL-R' :
rCTCSS = True
case 'DTCS' :
tDCS = True
rDCS = True
case 'DTCS-R':
rDCS = True
case 'Cross' :
if 'CrossMode' in D :
match D['CrossMode'] :
case 'Tone->' :
tCTCSS = True
case 'Tone->Tone' :
tCTCSS = True
rCTCSS = True
case '->Tone' :
rCTCSS = True
case 'Tone->DTCS' :
tCTCSS = True
rDCS = True
case 'DTCS->' :
tDCS = True
case 'DTCS->DTCS' :
tDCS = True
rDCS = True
case 'DTCS->Tone' :
tDCS = True
rCTCSS = True
case '->DTCS' :
rDCS = True
elif not tCTCSS and not rCTCSS :
print("Lost \'Cross\' mode configuration in Memory Channel", D['Location'])
if tCTCSS :
tMode = b'1'
tCode = bytes(format(ctcss.index(D['rToneFreq']), '03X'), 'utf-8')
elif tDCS :
if D['DtcsPolarity'][0] == 'N' : tMode = b'2'
else : tMode = b'3'
tCode = bytes(format(int(D['DtcsCode'], 8), '03X'), 'utf-8')
if rCTCSS :
rMode = b'1'
rCode = bytes(format(ctcss.index(D['cToneFreq']), '03X'), 'utf-8')
rSquelch = True
elif rDCS :
if D['DtcsPolarity'][1] == 'N' : rMode = b'2'
else : rMode = b'3'
if 'RxDtcsCode' in D : rCode = bytes(format(int(D['RxDtcsCode'], 8), '03X'), 'utf-8')
else : rCode = bytes(format(int(D['DtcsCode'], 8), '03X'), 'utf-8')
rSquelch = True
struct.pack_into("<3H9s9s8s4H6x3H4xH10xc3sc3s12xH2xH10xH13x", B, 0,
int(D['Location']), # memory location number
1, # has to be set to this
D['Skip'] == 'S', # scan skip flag
bytes(format(rFreq[0],'0>4'),'utf-8')+bytes(format(rFreq[2],'0<5'),'utf-8'), # receive frequency
bytes(format(tOffset[0],'0>4'),'utf-8')+bytes(format(tOffset[2],'0<5'),'utf-8'), # transmit offset
bytes(D['Name'],'utf-8'), # channel name (the radio will only display space, 0-9, A-Z and a-z)
cStep.index(D['TStep']), # step size
cTXO, # transmit off flag
cREV, # reverse flag
cMode.index(D['Mode']), # channel spacing (bandwidth)
tDuplex, # offset direction
tPower, # transmit power
cTA, # talk around flag
cCPD, # compander flag
tMode, tCode, # transmit encode CTCSS/DCS
rMode, rCode, # receive decode CTCSS/DCS
# skip optional signaling modes, PTT ID and scrambler settings
rSquelch, # squelch mode (set to enable CTCSS/DCS decoding)
cNC, # noise cancelling flag
# skip DTMF memory
cBCLO, # busy channel lockout flag
# skip scramble frequency
)
return B

if len(sys.argv) < 3 : sys.exit("usage: <settings filename> <csv filename> [<upper display line>,<lower display line>]")
sName = sys.argv[1]+".dat"
cName = sys.argv[2]+".csv"
dName = sys.argv[2]+".dat"
if False == os.path.isfile(sName) : sys.exit("Settings file not found.")
if False == os.path.isfile(cName) : sys.exit("CSV file not found.")
if True == os.path.isfile(dName) : dName = sys.argv[2]+"_New.dat"
display = ('','','')
if len(sys.argv) > 3 : display = sys.argv[3].partition(',')
# Load settings file into a buffer.
with open(sName, 'rb', buffering=0) as S :
settingsBuf = bytearray(0x25a3)
len = S.readinto(settingsBuf)
if len != 0x25a3 : sys.exit("Settings file is too short!")
if settingsBuf[0x258d:0x2594] not in models : sys.exit("Settings file is not an known CPS data file!")
# Preserve special scan limit and VFO memory channels.
specialList = []
specialNum = 0
while specialNum < 6 :
sBuf = bytearray(119)
len = S.readinto(sBuf)
if len < 119 : break
if int(struct.unpack('<H', sBuf[0:2])[0]) > 500 :
specialList.append(sBuf)
specialNum += 1
# Optional startup display update. Use command line quotes to includes spaces.
# Leave a line field blank to not update that line. Access CPS Function Setup
# to actually write this to the radio.
if display[0] : settingsBuf[0x1a22:0x1a2a] = bytes(display[0].ljust(8, ' '), 'utf-8')
if display[2] : settingsBuf[0x1a2a:0x1a32] = bytes(display[2].ljust(8, ' '), 'utf-8')
# Load and translate the CSV file into a memory channel buffer list.
with open(cName, 'r', newline='') as C :
R = csv.DictReader(C) # process CHIRP header
channelList = []
channelNum = 0
specialFlag = False
for row in R :
loc = int(row['Location'])
if loc < 1 or loc > 506 :
print("Dropping channel out of range with number", loc)
continue
if loc > 500 : specialFlag = True
channelList.append(translate(row))
channelNum += 1
# Create new CPS data file by combining the settings file and the CSV file channel data.
with open(dName, 'wb', buffering=0) as D :
# Update the number of memory channels in the settings data.
if specialFlag : settingsBuf[0x1a4c:0x1a4e] = struct.pack('<H', channelNum)
else : settingsBuf[0x1a4c:0x1a4e] = struct.pack('<H', channelNum+specialNum)
D.write(settingsBuf)
for buf in channelList : D.write(buf)
if not specialFlag :
for buf in specialList : D.write(buf)
(5-5/8)