Project

General

Profile

New Model #9241 » dat_to_csv.py

Fred DeKeyser, 01/10/2025 01:55 PM

 
# 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)
'''
(3-3/8)