Skip to content

Commit 6d6b77f

Browse files
committed
Completed initial development of QRP tracker
1 parent bc72ae4 commit 6d6b77f

4 files changed

Lines changed: 239 additions & 10 deletions

File tree

BalloonTelemetry.cfg

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -8,14 +8,14 @@
88
# ldatetime = last date/time of successful upload of balloon data, you can change this value to reprocess past data
99
#
1010

11-
[VE6AZX-1]
11+
[VE6AZX-15]
1212
tracker = Q
1313
uploadcallsign = K5MAP
1414
wsprcallsign = VE6AZX
15-
ballooncallsign = VE6AZX-15
15+
ballooncallsign = VE6AZX-14
1616
band = 14
1717
channel = 06
18-
timeslot = 4
18+
timeslot = 6
1919
comment = This is for testing
2020
uploadsite = T
2121
telemetryfile = N

constants.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@
1111
# (at your option) any later version. #
1212
# #
1313
#==============================================================================================================#
14-
__version__ = '0.4.0'
14+
__version__ = '0.4.1'
1515

1616
SOFTWARE_NAME = "k5map-python"
1717

getQRPLabs.py

Lines changed: 234 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,160 @@
4444

4545
#--------------------------------------------------------------------------------------------------------------#
4646

47-
def getQRPLabs(bCfg: Dict, last_date: str):
47+
def matchQRPRecords(jWSPRRec1: List, jWSPRRec2: List) -> List:
48+
# determine if 2nd record avilable to process
49+
logging.info(f" Starting record matching process")
50+
51+
print(f"jWSPRRec1 len = {len(jWSPRRec1)}")
52+
print(f"jWSPRRec2 len = {len(jWSPRRec2)}")
53+
54+
aDateTime = []
55+
aMatch = []
56+
for i in range(0, len(jWSPRRec1)):
57+
try:
58+
aDateTime.index(jWSPRRec1[i]['time'])
59+
except ValueError:
60+
aDateTime.append(jWSPRRec1[i]['time'])
61+
sDateTime = adjDateTime(jWSPRRec1[i]['time']) # find 2nd record time based on 1st record
62+
match = False
63+
for j, element in enumerate(jWSPRRec2):
64+
#for j in range(len(jWSPRRec2)):
65+
if element['time'] == sDateTime:
66+
match = True
67+
break
68+
# process both records
69+
if match == True:
70+
aMatch.append(jWSPRRec1[i])
71+
aMatch.append(jWSPRRec2[j])
72+
logging.debug(f" Found 1st record to process = {jWSPRRec1[i]['tx_sign']}, {jWSPRRec1[i]['time']}, {jWSPRRec1[i]['tx_loc']}, {jWSPRRec1[i]['band']}")
73+
logging.debug(f" Found 2nd record to process = {jWSPRRec2[j]['tx_sign']}, {jWSPRRec2[j]['time']}, {jWSPRRec2[j]['tx_loc']}, {jWSPRRec2[j]['band']}")
74+
else:
75+
logging.debug(f" Found 1st record to process but no match = {jWSPRRec1[i]['tx_sign']}, {jWSPRRec1[i]['time']}, {jWSPRRec1[i]['tx_loc']}, {jWSPRRec1[i]['band']}")
76+
return aMatch
77+
78+
#--------------------------------------------------------------------------------------------------------------#
79+
80+
def decodeQRP(JSON1: Dict, JSON2: Dict) -> Dict:
81+
pow2dec = {0:0,3:1,7:2,10:3,13:4,17:5,20:6,23:7,27:8,30:9,33:10,37:11,40:12,43:13,47:14,50:15,53:16,57:17,60:18}
82+
83+
spot_pos_time = JSON1['time']
84+
spot_pos_call = JSON1['tx_sign']
85+
spot_pos_loc = JSON1['tx_loc']
86+
#spot_pos_power = 17
87+
spot_tele_call = JSON2['tx_sign']
88+
spot_tele_loc = JSON2['tx_loc']
89+
spot_tele_power = int(JSON2['power'])
90+
91+
# Convert call to numbers
92+
c1 = spot_tele_call[1]
93+
# print("C1=",c1)
94+
if c1.isalpha():
95+
c1 = ord(c1) - 55
96+
else:
97+
c1 = ord(c1) - 48
98+
99+
c2 = ord(spot_tele_call[3]) - 65
100+
c3 = ord(spot_tele_call[4]) - 65
101+
c4 = ord(spot_tele_call[5]) - 65
102+
103+
# Convert locator to numbers
104+
l1 = ord(spot_tele_loc[0]) - 65
105+
l2 = ord(spot_tele_loc[1]) - 65
106+
l3 = ord(spot_tele_loc[2]) - 48
107+
l4 = ord(spot_tele_loc[3]) - 48
108+
109+
#
110+
# Convert power
111+
#
112+
p = pow2dec[spot_tele_power]
113+
sum1 = c1 * 26 * 26 * 26
114+
sum2 = c2 * 26 * 26
115+
sum3 = c3 * 26
116+
sum4 = c4
117+
sum1_tot = sum1 + sum2 + sum3 + sum4
118+
119+
sum1 = l1 * 18 * 10 * 10 * 19
120+
sum2 = l2 * 10 * 10 * 19
121+
sum3 = l3 * 10 * 19
122+
sum4 = l4 * 19
123+
sum2_tot = sum1 + sum2 + sum3 + sum4 + p
124+
# print("sum_tot1/2:", sum1_tot,sum2_tot)
125+
126+
# 24*1068
127+
lsub1 = int(sum1_tot / 25632)
128+
lsub2_tmp = sum1_tot - lsub1 * 25632
129+
lsub2 = int(lsub2_tmp / 1068)
130+
# print("lsub1/2",lsub1,lsub2)
131+
alt = (lsub2_tmp - lsub2 * 1068) * 20
132+
133+
# Handle bogus altitudes
134+
if alt > 14000:
135+
# print("Bogus packet. Too high altitude!! locking to 9999")
136+
alt = 9999
137+
138+
if alt == 2760:
139+
# print("Bogus packet. 2760 m locking to 9998")
140+
alt = 9998
141+
142+
if alt == 0:
143+
# print("Zero alt detected. Locking to 10000")
144+
alt = 10000
145+
146+
# Sublocator
147+
lsub1 = lsub1 + 65
148+
lsub2 = lsub2 + 65
149+
subloc = (chr(lsub1) + chr(lsub2)).lower()
150+
151+
# Temperature
152+
# 40*42*2*2
153+
temp_1 = int(sum2_tot / 6720)
154+
temp_2 = temp_1 * 2 + 457
155+
temp_3 = temp_2 * 5 / 1024
156+
temp = (temp_2 * 500 / 1024) - 273
157+
# print("Temp: %5.2f %5.2f %5.2f %5.2f" % (temp_1, temp_2, temp_3, temp))
158+
159+
#
160+
# Battery
161+
#
162+
# =I7-J7*(40*42*2*2)
163+
batt_1 = int(sum2_tot - temp_1 * 6720)
164+
batt_2 = int(batt_1 / 168)
165+
batt_3 = batt_2 * 10 + 614
166+
# 5*M8/1024
167+
batt = batt_3 * 5 / 1024
168+
169+
#
170+
# Speed / GPS / Sats
171+
#
172+
# =I7-J7*(40*42*2*2)
173+
# =INT(L7/(42*2*2))
174+
t1 = sum2_tot - temp_1 * 6720
175+
t2 = int(t1 / 168)
176+
t3 = t1 - t2 * 168
177+
t4 = int(t3 / 4)
178+
speed = t4 * 2
179+
r7 = t3 - t4 * 4
180+
gps = int(r7 / 2)
181+
sats = r7 % 2
182+
# print("T1-4,R7:",t1, t2, t3, t4, r7)
183+
184+
#
185+
# Calc lat/lon from loc+subbloc
186+
#
187+
loc = spot_pos_loc + subloc
188+
lat, lon = GridtoLatLon(loc)
189+
190+
pstr = ("Spot %s Call: %6s Latlon: %10.5f %10.5f Grid: %6s Alt: %5d Temp: %4.1f Batt: %5.2f Speed: %3d GPS: %1d Sats: %1d" %
191+
( spot_pos_time, spot_pos_call, lat, lon, loc, alt, temp, batt, speed, gps, sats ))
192+
193+
telemetry = {'time':spot_pos_time, "call":spot_pos_call, "lat":round(lat,3), "lon":round(lon,3), "grid":loc, "alt": alt,
194+
"temp":round(temp,1), "batt":round(batt,2), "speed":speed, "gps":gps, "sats":sats }
195+
196+
return telemetry
197+
198+
#--------------------------------------------------------------------------------------------------------------#
199+
200+
def getQRPLabs(bCfg: Dict, lastdate: str):
48201
"""
49202
Function to retrieve WSPR records, create data structure and then upload to APRS-IS or SondeHub
50203
@@ -61,7 +214,7 @@ def getQRPLabs(bCfg: Dict, last_date: str):
61214
timeslot = bCfg['timeslot']
62215
band = bCfg['band']
63216

64-
query = "SELECT * FROM rx WHERE tx_sign='" + wCallsign + "' AND time > '" + last_date + "' ORDER BY time"
217+
query = "SELECT * FROM rx WHERE tx_sign='" + wCallsign + "' AND time > '" + lastdate + "' ORDER BY time"
65218
logging.info(" SQL query = " + query )
66219

67220
url = "https://db1.wspr.live/?query=" + urllib.parse.quote_plus(query + " FORMAT JSON")
@@ -90,8 +243,84 @@ def getQRPLabs(bCfg: Dict, last_date: str):
90243
logging.warning(" Exit function, insufficient WSPR records to process" )
91244
return 0, None, None
92245

93-
# eliminate duplicates
246+
# remove any duplicate first packets
247+
print(40*"-")
248+
logging.debug(f" starting record count = {len(jWsprData)}")
94249
jWsprData = deldupWspr(jWsprData)
95-
logging.info(f" WSPR Live records after removing duplicates = {len(jWsprData)}" )
250+
logging.debug(f" ending record count after removing duplicates = {len(jWsprData)}")
251+
252+
# process CFG values to search for 2nd packets
253+
ch1 = channel[0]
254+
ch3 = channel[1]
255+
ts = str(int(timeslot)+2)
256+
sSign = f"{ch1}_{ch3}%"
257+
#sTime = '____-__-__ __:_' + ts + '%'
258+
logging.info(f" Values to use for 2nd packet: ch1 = {ch1}, ch3 = {ch3}, ts = {ts}, band = {band}, sSign = {sSign}")
259+
260+
# build query for 2nd packet
261+
query = "SELECT * FROM rx WHERE tx_sign LIKE '" + sSign + "' AND band=" + band + " AND time > '" + lastdate + "' ORDER BY time"
262+
logging.info(" SQL query = " + query )
263+
url = "https://db1.wspr.live/?query=" + urllib.parse.quote_plus(query + " FORMAT JSON")
264+
265+
# download contents from wspr.live
266+
try:
267+
contents = urllib.request.urlopen(url).read()
268+
except urllib.error.URLError as erru:
269+
logging.critical(f" URL error - {erru.reason}" )
270+
return -1, None, None
271+
except urllib.error.HTTPError as errh:
272+
logging.critical(f" HTTP error - {errh}" )
273+
return -1, None, None
274+
except socket.timeout as errt:
275+
logging.critical(f" Connection timeout - {errt}" )
276+
return -1, None, None
277+
except:
278+
logging.critical(f" Unexpected error calling URL - {traceback.format_exc()}" )
279+
return -1, None, None
280+
281+
jWsprData2 = json.loads(contents.decode("UTF-8"))["data"]
282+
record_count = len(jWsprData2)
283+
logging.info(f" WSPR Live records downloaded = {record_count}" )
284+
if record_count < 1:
285+
logging.warning(" Exit function, insufficient matching WSPR records to process" )
286+
return 0, None, None
287+
#pprint.pp(jWsprData2[len(jWsprData2)-1], indent=2)
288+
289+
# process records downloaded and match
290+
aMatch = matchQRPRecords(jWsprData, jWsprData2)
291+
logging.info(f" Number of matched records = {len(aMatch)}" )
292+
if len(aMatch) < 2:
293+
# no matches to process
294+
logging.warning(f" Insuficient number of records to process" )
295+
return 0, None, None
296+
297+
# decode each pair of matches and build upload data list
298+
logging.info(f" Starting decoding process" )
299+
jDecodedData = {}
300+
jUploadData = []
301+
for i in range(0, len(aMatch), 2):
302+
jDecodedData[i] = decodeQRP(aMatch[i], aMatch[i+1])
303+
304+
# reformat time from WSPR format to Zulu
305+
datetime1 = reformatDateTime(aMatch[i]['time'], 0)
306+
datetime2 = reformatDateTime(aMatch[i]['time'], 10)
307+
308+
# add telemetry data
309+
# build strComment grid, temp, volt, To:?, Up:?, V?, Sun:?, comment
310+
strComment = jDecodedData[i]['grid'] + " " + str(jDecodedData[i]['temp']) + "C " + str(jDecodedData[i]['batt']) + "V "
311+
strComment += "To:?? Up:??m/s V:??Km/h Sun:?? " + bCfg['comment']
312+
313+
# put data into jUploadData format for uploading
314+
lat, lon = GridtoLatLon(jDecodedData[i]['grid'])
315+
JSON = {"software_name":SOFTWARE_NAME, "software_version": __version__, "uploader_callsign": bCfg['uploadcallsign'], "time_received": datetime1,
316+
"payload_callsign":BalloonCallsign, "datetime":datetime2, "lat":round(lat,3), "lon":round(lon,3), "alt":jDecodedData[i]['alt'],
317+
"sats":jDecodedData[i]['sats'], "temp":jDecodedData[i]['temp'], "batt":jDecodedData[i]['batt'], "grid":jDecodedData[i]['grid'], "comment":strComment}
318+
jUploadData.append(JSON)
319+
320+
logging.info(f" Decoding completed, record count = {len(jUploadData)}" )
321+
pprint.pp(jUploadData)
322+
323+
324+
96325

97-
return
326+
return 0, None, None

getU4B.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -268,7 +268,7 @@ def getU4B(bCfg: Dict, lastdate: str):
268268
#sTime = '____-__-__ __:_' + ts + '%'
269269
logging.info(f" Values to use for 2nd packet: ch1 = {ch1}, ch3 = {ch3}, ts = {ts}, band = {band}, sSign = {sSign}")
270270

271-
# download contents from wspr.live
271+
# build query for 2nd packet
272272
query = "SELECT * FROM rx WHERE tx_sign LIKE '" + sSign + "' AND band=" + band + " AND time > '" + lastdate + "' ORDER BY time"
273273
logging.info(" SQL query = " + query )
274274
url = "https://db1.wspr.live/?query=" + urllib.parse.quote_plus(query + " FORMAT JSON")

0 commit comments

Comments
 (0)