|
# Convert an AT779UV radio CPS data file to a CHIRP CSV file for CHIRP import and editing.
|
|
|
|
# Free for non-commercial use.
|
|
# 01/10/25 CFD Initial release.
|
|
|
|
import sys, os, struct, csv
|
|
|
|
chirpHdr = ['Location','Name','Frequency','Duplex','Offset','Tone','rToneFreq','cToneFreq',
|
|
'DtcsCode','DtcsPolarity','Mode','TStep','Skip','Comment','URCALL','RPT1CALL','RPT2CALL','DVCODE',
|
|
'RxDtcsCode','CrossMode','Power'] # placed at end for legacy CHIRP compatability
|
|
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']
|
|
cSkip = ['','S']
|
|
cPower = ['L','M','H']
|
|
cWatts = ['5.0W','10W','20W']
|
|
|
|
'''
|
|
Translate the memory channel data structure tuple into an ordered list of CSV file values.
|
|
|
|
Note: View the CTCSS/DCS tone/code modes as encoded by a bit field for transmit and receive
|
|
states of [off, CTCSS, DCS N, DCS I] (so two sets of 2 bits). Legacy CHIRP makes the historical
|
|
radio assumption that the transmit and receive states and codes are the same (or CTCSS
|
|
transmit only). So there are 2 more bits for modes and codes equal or not. Thus there
|
|
are 2**6 = 64 possible states. Since legacy CHIRP CSV files do not support "cross" modes,
|
|
they cannot encode all of these states, and their may be some configuration loss as a result.
|
|
|
|
CHIRP defaults the CSV file CTCSS/DCS column values to '' (off), '88.5', '88.5', '023', 'NN',
|
|
and the new column values to '023', 'Tone->Tone', '50W' (see chirpHdr).
|
|
'''
|
|
def translate(T) :
|
|
A = []
|
|
A.append(T[0]) # location number
|
|
A.append(T[5].strip(b'\x00\x20').decode()) # name
|
|
A.append(float(T[3][:4]+b'.'+T[3][4:])) # frequency
|
|
A.append(cDuplex[T[10]]) # duplex
|
|
A.append(float(T[4][:4]+b'.'+T[4][4:])) # offset
|
|
''' CTCSS/DCS encode/decode fields '''
|
|
tMode = int(chr(T[14][0]), 16) # transmit encode
|
|
tCode = int(T[14][1:], 16)
|
|
rMode = int(chr(T[15][0]), 16) # receive decode
|
|
rCode = int(T[15][1:], 16)
|
|
if tMode > 3 : tMode = 0 # create safe values, zero is disabled now
|
|
if rMode > 3 : rMode = 0
|
|
cCross = tCode != rCode
|
|
cTone = '' # safe CTCSS/DCS disabled CSV default values
|
|
sMode = 'Tone->Tone'
|
|
tToneIdx = 9
|
|
rToneIdx = 9
|
|
eCode = '023'
|
|
dCode = '023'
|
|
match (tMode, rMode) : # set DCS polarity [transmit:receive] as [Normal,Reverse(inverted)]
|
|
case (2, 3) : cPol = 'NR'
|
|
case (3, 2) : cPol = 'RN'
|
|
case (3, 3) : cPol = 'RR'
|
|
case _ : cPol = 'NN'
|
|
match (tMode, rMode) : # set CTCSS and DCS modes
|
|
case (0, 0) : pass
|
|
case (0, 1) :
|
|
cTone = 'Cross' # legacy TSQL-R
|
|
sMode = '->Tone'
|
|
rToneIdx = rCode # no error checking
|
|
case (1, 0) :
|
|
cTone = 'Tone'
|
|
tToneIdx = tCode # no error checking
|
|
case (1, 1) :
|
|
if cCross : cTone = 'Cross'
|
|
else : cTone = 'TSQL'
|
|
tToneIdx = tCode
|
|
rToneIdx = rCode
|
|
case (0, 2) | (0, 3) :
|
|
cTone = 'Cross' # legacy DTCS-R
|
|
sMode = '->DTCS'
|
|
dCode = format(rCode, '03o')
|
|
case (1, 2) | (1, 3) :
|
|
cTone = 'Cross'
|
|
sMode = 'Tone->DTCS'
|
|
tToneIdx = tCode
|
|
dCode = format(rCode, '03o')
|
|
case (2, 0) | (3, 0) :
|
|
cTone = 'Cross'
|
|
sMode = 'DTCS->'
|
|
eCode = format(tCode, '03o')
|
|
case (2, 1) | (3, 1) :
|
|
cTone = 'Cross'
|
|
sMode = 'DTCS->Tone'
|
|
eCode = format(tCode, '03o')
|
|
rToneIdx = rCode
|
|
case _ :
|
|
if cCross :
|
|
cTone = 'Cross'
|
|
sMode = 'DTCS->DTCS'
|
|
else : cTone = 'DTCS'
|
|
eCode = format(tCode, '03o')
|
|
dCode = format(rCode, '03o')
|
|
if rMode and not T[22] :
|
|
print("Losing squelch setting of \'carrier\' with receive CTCSS/DCS decode set in Memory Channel", T[0])
|
|
if T[16] or T[17] or T[18] or T[20] or T[21] or T[25]:
|
|
print("Losing other signaling mode(s) in Memory Channel", T[0])
|
|
A.append(cTone) # tone mode
|
|
A.append(ctcss[tToneIdx]) # transmit only CTCSS tone
|
|
A.append(ctcss[rToneIdx]) # transmit and/or receive CTCSS tone
|
|
A.append(eCode) # transmit DCS code
|
|
A.append(cPol) # DCS polarity
|
|
''' remaining fields '''
|
|
A.append(cMode[T[9]]) # bandwidth mode
|
|
A.append(cStep[T[6]]) # step size
|
|
A.append(cSkip[T[2]]) # skip
|
|
# The comment field has the power and special feature configurations.
|
|
cComment = cPower[T[11]]
|
|
if T[7] : cComment += ' TXO' # transmit off
|
|
if T[8] : cComment += ' REV' # reverse
|
|
if T[12] : cComment += ' TA' # talk around
|
|
if T[13] : cComment += ' CPD' # compander
|
|
if T[24] : cComment += ' NC' # noise cancelling
|
|
if T[26] == 1 : cComment += ' BCLO=R' # busy channel lockout
|
|
elif T[26] == 2 : cComment += ' BCLO=B'
|
|
if T[19] : print("Losing scrambler configuration in Memory Channel", T[0])
|
|
A.append(cComment) # comment
|
|
A.append('') # URCALL
|
|
A.append('') # RPT1CALL
|
|
A.append('') # RPT2CALL
|
|
A.append('') # DVCODE
|
|
A.append(dCode) # RxDtcsCode
|
|
A.append(sMode) # CrossMode
|
|
A.append(cWatts[T[11]]) # Power
|
|
return A
|
|
|
|
if len(sys.argv) <= 1 : sys.exit("File name not specified.")
|
|
if False == os.path.isfile(sys.argv[1]+".dat") : sys.exit("File not found.")
|
|
with open(sys.argv[1]+".dat", 'rb', buffering=0) as F :
|
|
cnt = bytearray(2)
|
|
F.seek(0x1a4c) # channel count file offset
|
|
len = F.readinto(cnt)
|
|
if len < 2 : sys.exit("File read error.")
|
|
count = struct.unpack_from('<H', cnt) # 2 byte unsigned short little-endian
|
|
buf = bytearray(119*count[0])
|
|
F.seek(0x25a3) # file offset of start of memory channel data array
|
|
len = F.readinto(buf)
|
|
if len < 119 : sys.exit("No memory channel data.")
|
|
with open(sys.argv[1]+".csv", 'w', newline='') as C :
|
|
W = csv.writer(C)
|
|
W.writerow(chirpHdr)
|
|
for i in range(0, len, 119) :
|
|
tuple = struct.unpack_from("<3H9s9s8s4H6x3H4xH10x4s4s9H6xH2xH4x5sH2x", buf, i)
|
|
W.writerow(translate(tuple))
|
|
print("Processed %d memory channels." %(count[0]))
|
|
|
|
'''
|
|
Special channel indices: (Scan Limits:) PL1=501 PH1=502 PL2=503 PH2=504 (VFOs:) VFO1=505 VFO2=506
|
|
The channel indices will be out of order when the file is edited, but reordered when read from the radio.
|
|
|
|
AT779UV Memory Channel Data Structure:
|
|
< Numeric values are little-endian
|
|
0 H 0 Memory Location
|
|
2 H 1 01 00
|
|
4 H 2 Scan Skip Flag [off,on]
|
|
6 9s 3 Receive Frequency ("012345678" decodes as 123.45678 MHz)
|
|
15 9s 4 Transmit Offset (decodes as receive frequency above)
|
|
24 8s 5 Channel Name (sometimes null terminated, often space filled)
|
|
32 H 6 Step Size in kHz [2.5,5,6.25,10,12.5,20,25,30,50] (for VFO mode)
|
|
34 H 7 Transmit Off Flag [off,on] (on means transmit disabled)
|
|
36 H 8 Reverse Flag [off,on] (reverse transmit and receive frequencies)
|
|
38 H 9 Channel Spacing [wide(25 kHz),narrow(12.5 kHz)] (bandwidth)
|
|
40 6x 00 00 00 00 00 00
|
|
46 H 10 Offset Direction [off,negative,positive]
|
|
48 H 11 Transmit Power [low,medium,high]
|
|
50 H 12 Talk Around Flag [off,on] (transmit on receive frequency)
|
|
52 4x 00 00 00 00
|
|
56 H 13 Compander [off,on]
|
|
58 10x 00 00 00 00 00 00 00 00 00 00 (sometimes ... 14 00 14 00 on special channels)
|
|
68 4s 14 CTCSS/DCS Encode (transmit) (see below)
|
|
72 4s 15 CTCSS/DCS Decode (receive) (see below)
|
|
76 H 16 Optional Signaling [off,DTMF,2Tone,5Tone]
|
|
78 H 17 2Tone Signaling Memory [0...31]
|
|
80 H 18 5Tone Signaling Memory [0...99]
|
|
82 H 19 Scrambler Switch [off,1...11,custom] (see custom frequency below)
|
|
84 H 20 DTMF PTT ID [off,begin,end,both]
|
|
86 H 21 5 Tone PTT ID [off,begin,end,both]
|
|
88 H 22 Squelch Mode [carrier,CTCSS/DCS]
|
|
90 H 23 (sometimes E7 05 (1511), often 00 00)
|
|
92 H 24 Noise Cancelling (NC) Flag [off,on]
|
|
94 6x 00 00 00 00 00 00
|
|
100 H 25 DTMF Signaling Memory [M1...M16]
|
|
102 2x 00 00
|
|
104 H 26 Busy Channel Lockout [off,repeater,busy]
|
|
106 4x 00 00 00 00
|
|
110 5s 27 (sometimes "00000", often nulls)
|
|
115 H 28 Scramble Frequency in Hz (e.g., 1500, 3300, often zero when not set)
|
|
117 2x 00 00
|
|
(119 is the total byte length)
|
|
|
|
The radio will only display the characters space, 0-9, A-Z and a-z in the channel name.
|
|
The names are often right filled with spaces, and can have an embedded null termination
|
|
after the non-space characters. All spaces or all nulls will cause the frequency to be
|
|
displayed. Embedded non-displayable characters will display as a space. Leading non-
|
|
displayable characters will cause the frequency to be displayed. The radio will right
|
|
space fill after a terminating null (so null filling does not matter).
|
|
|
|
AT779UV CTCSS/DCS signals are specified by 4 hexadecimal characters in the format 'TNNN':
|
|
"FFFF" = signaling off. T values are: '1' = CTCSS, '2' = DN, '3' = DI. NNN values range
|
|
from "000" to "032" for 51 CTCSS tones or "1FF" for 512 (octal 777) DCS codes. Note both
|
|
can encode more than the standard signal sets. CTCSS "000" is nonstandard 62.5 Hz. The
|
|
CPS software UI identifies the DCS codes with octal number notation (i.e., 000-777).
|
|
|
|
A CHIRP CSV file does not support all of the possible AT779UV radio special features
|
|
so it may lose some AT779UV memory configuration information depending on usage.
|
|
|
|
AT779UV Data File Structure (approximate):
|
|
0x0000 0x0002 Frequency Range (0x00 = Full, 0x06 = US Amateur, 0x10 = US GMRS)
|
|
0x0002 0x0168 DTMF Encode Memories (an array of 18 byte blocks)
|
|
0x016a 0x001b DTMF Settings
|
|
0x0185 0x002a 2Tone Settings
|
|
0x01af 0x0220 2Tone Encode Memories (an array of 17 byte blocks)
|
|
0x03cf 0x001c 5Tone Settings
|
|
0x03eb 0x00b0 5Tone Information IDs (an array of 22 byte blocks)
|
|
0x049b 0x151e 5Tone Encode IDs (an array of 53 byte blocks)
|
|
0x19b9 0x001d Zeros
|
|
0x19d6 0x004c Function Setup Settings
|
|
0x1a22 0x0010 Startup Display Text (2 lines of 8 characters)
|
|
0x1a32 0x000e Scan Information Settings
|
|
0x1a40 0x000c Emergency Information Settings
|
|
0x1a4c 0x0002 Memory Channel Count ('n' below)
|
|
0x1a4e 0x04be Zeros
|
|
0x1f0c 0x0681 Communication Notes (an array of 13 byte blocks plus a byte)
|
|
0x258d 0x0010 Radio Model Text
|
|
0x259d 0x0004 CPS Version Text
|
|
0x25a1 0x0002 File Data Origin (0 = CPS, 4 = radio)
|
|
0x25a3 n*0x77 Channel Memory Data (an array of 119 byte blocks)
|
|
EOF-6 0x0006 Pad (EOF-4 byte seems to be with Scan Info)
|
|
'''
|