Most all of the code refactored to be less ridiculous. Sending of APRS position reports is now decoupled from fetching position updates. Main loop nearly simplified into obvlivion. getEvents() function added to get a list of all new events. sendAPRS now works on an individual event in the list. Most of the large try blocks eleminated in favor of less absurd things. Some other minor changes too.

This commit is contained in:
Christopher Smith 2021-04-01 00:20:25 -05:00
parent 0ef9fd7004
commit 964efba308

View file

@ -102,13 +102,7 @@ SSNum = int(SSNum)
ARPreamble = ''.join(['>APRS,','TCPIP*,','qAS,',conf['APRS']['SSID']]) ARPreamble = ''.join(['>APRS,','TCPIP*,','qAS,',conf['APRS']['SSID']])
NAME = "iR-APRSISD" NAME = "iR-APRSISD"
REV = "0.2" REV = "0.3"
#Start with the current time. We use this to keep track of whether we've
# already sent a position, so if we cut it off when the program starts, we
# won't (re-)send old stale entry from inReach
#lastUpdate = calendar.timegm(time.gmtime())
#Set up the handler for HTTP connections #Set up the handler for HTTP connections
if conf.has_option('inReach','Password'): if conf.has_option('inReach','Password'):
@ -131,41 +125,50 @@ def reconnect():
except Exception as e: except Exception as e:
print("Trouble connecting to APRS-IS server.") print("Trouble connecting to APRS-IS server.")
print(e) print(e)
time.sleep(3)
continue
#We'll store the IMEI to ssid mapping here. #We'll store the device to ssid mapping here.
SSIDList = {} SSIDList = {}
#We'll store timestamps here #We'll store timestamps here
lastUpdate = {} lastUpdate = {}
#Load any preconfigured IMEI mappings #Load any preconfigured mappings
if conf.has_section('Devices'): if conf.has_section('Devices'):
for device in conf['Devices'].keys(): for device in conf['Devices'].keys():
SSIDList[conf['Devices'][device]] = device.upper() SSIDList[conf['Devices'][device]] = device.upper()
#Get an SSID #Get an SSID
def getSSID(IMEI): #An apocryphal concern is that this will happily generate an SSID for None,
# or whatever else might get passed along to it.
# There's a record in the usual set of inReach Placemarks where this happens,
# but because the record has no extended data, no packets are ever generated
# for it. Such a record is generally not interesting and on the end of the list,
# so there has been no reason to explicitly skip it.
def getSSID(DID):
global lastUpdate, SSIDList, SSNum, Call global lastUpdate, SSIDList, SSNum, Call
#If we have a Devices section, the SSID list is static. #If we have a Devices section, the SSID list is static.
if IMEI not in SSIDList: if DID not in SSIDList:
if conf.has_section('Devices'): if conf.has_section('Devices'):
print("No device mapping")
return None return None
SSIDList[IMEI] = ''.join([Call,"-",str(SSNum)]) SSIDList[DID] = ''.join([Call,"-",str(SSNum)])
SSNum = SSNum + 1 SSNum = SSNum + 1
#Add a timestamp on the first call #Add a timestamp on the first call
# This prevents us from redelivering an old message, which can stay # This prevents us from redelivering an old message, which can stay
# in the feed. # in the feed.
if IMEI not in lastUpdate: if DID not in lastUpdate:
lastUpdate[IMEI] = calendar.timegm(time.gmtime()) lastUpdate[DID] = calendar.timegm(time.gmtime())
return SSIDList[IMEI] return SSIDList[DID]
#APRS-IS Setup #APRS-IS Setup
AIS = None AIS = None
reconnect() reconnect()
#Generate an APRS packet from a Placemark #Get information from a Placemark
def sendAPRS(Placemark): def parsePlacemark(Placemark):
#We only care about the Placemarks with the ExtendedData sections. #We only care about the Placemarks with the ExtendedData sections.
if not Placemark.getElementsByTagName('ExtendedData'): if not Placemark.getElementsByTagName('ExtendedData'):
return None return None
@ -175,125 +178,145 @@ def sendAPRS(Placemark):
for xd in Placemark.getElementsByTagName('ExtendedData')[0].getElementsByTagName('Data'): for xd in Placemark.getElementsByTagName('ExtendedData')[0].getElementsByTagName('Data'):
if not xd.getElementsByTagName('value')[0].firstChild == None: if not xd.getElementsByTagName('value')[0].firstChild == None:
extended[xd.getAttribute('name')] = xd.getElementsByTagName('value')[0].firstChild.nodeValue extended[xd.getAttribute('name')] = xd.getElementsByTagName('value')[0].firstChild.nodeValue
try:
#Here is the position vector #Make sure the device mapping is good.
latitude = extended['Latitude'] if not 'IMEI' in extended:
longitude = extended['Longitude'] return None
elevation = extended['Elevation'] if not getSSID(extended['IMEI']):
velocity = extended['Velocity'] return None
course = extended['Course']
uttime = extended['Time UTC'] #Now build the position vector
device = extended['Device Type'] latitude = None
longitude = None
elevation = None
velocity = None
course = None
uttime = None
device = None
IMEI = extended['IMEI'] IMEI = extended['IMEI']
SSID = getSSID(IMEI)
#If there's no valid SSID mapping, we don't process this one. if 'Latitude' in extended and 'Longitude' in extended:
if not SSID: latitude = float(extended['Latitude'])
return None longitude = float(extended['Longitude'])
if 'Elevation' in extended:
#Some time conversions. First the time struct. #Altitude needs to be in feet above sea-level
ts = time.strptime(''.join([uttime," UTC"]),"%m/%d/%Y %I:%M:%S %p %Z") # what we get instead is a string with a number of meters
#Unix epoch time for local record-keeping. # at the beginning.
etime = calendar.timegm(ts) elevation = re.sub(r'^(\d+\.?\d+)\s*m.*',r'\1',extended['Elevation'])
#MonthDayHoursMinutes_z for APRS-IS packet elevation = float(elevation) * 3.2808399
aprstime = time.strftime("%d%H%Mz",ts) if 'Velocity' in extended:
#Velocity in knots, according to APRS.
#Skip this one if it's old. velocity = float(re.sub(r'(\d+\.?\d+).*',r'\1', extended['Velocity']))*0.5399568
if lastUpdate[IMEI] > etime: if 'Course' in extended:
time.sleep(conf.getfloat('General','Period')) #... and the course is just a heading in degrees.
return None course = float(re.sub(r'(\d+\.?\d+).*',r'\1', extended['Course']))
uttime = time.strptime(extended['Time UTC'] + " UTC","%m/%d/%Y %I:%M:%S %p %Z")
device = extended['Device Type']
#If we have SMS data, add that. #If we have SMS data, add that.
if 'Text' in extended: if 'Text' in extended:
comment = extended['Text'] comment = extended['Text']
else: #Default comment else: #Default comment
comment = conf['APRS']['Comment'] comment = conf['APRS']['Comment']
comment = ''.join([" ", comment, " : ", NAME, " v", REV, " : ", platform.system(), " on ", platform.machine(), " : ", device])
#In the format we're using, APRS comments can be 36 characters
# ... but APRS-IS doesn't seem to care, so leave this off.
#comment = comment[:36]
#Latitude conversion return [device,IMEI,ARPreamble,uttime,latitude,longitude,elevation,course,velocity,comment]
#We start with the truncated degrees, filled to two places
# then add fractional minutes two 2x2 digits.
latitude = float(latitude)
aprslat = str(abs(math.trunc(latitude))).zfill(2)
aprslat += '{:.02f}'.format(round((abs(latitude)-abs(math.trunc(latitude)))*60,2)).zfill(5)
if latitude > 0:
aprslat += "N"
else:
aprslat += "S"
#Longitude next. #Return a list of all events available. Each one is a list of arguments
longitude = float(longitude) # for the below sendAPRS function
aprslong = str(abs(math.trunc(longitude))).zfill(3) def getEvents():
aprslong += '{:.02f}'.format(round((abs(longitude)-abs(math.trunc(longitude)))*60,2)).zfill(5) events = []
if longitude > 0:
aprslong += "E"
else:
aprslong += "W"
#Altitude needs to be in feet above sea-level
# what we get instead is a string with a number of meters
# at the beginning.
aprsalt = re.sub(r'^(\d+\.?\d+)\s*m.*',r'\1',elevation)
#We need integer feet, six digits
aprsalt = str(round(float(aprsalt) * 3.2808399)).zfill(6)
aprsalt = "/A=" + aprsalt[0:6]
#Aprs position
aprspos = ''.join([aprslat,conf['APRS']['Separator'],aprslong,conf['APRS']['Symbol']])
#Course is already in degrees, so we just need to reformat it.
aprscourse = str(round(float(re.sub(r'(\d+\.?\d+).*',r'\1', course)))).zfill(3)
#Speed is in km/h.
aprsspeed = float(re.sub(r'(\d+\.?\d+).*',r'\1',velocity))*0.53995681
aprsspeed = str(min(round(aprsspeed),999)).zfill(3)
#Here's the course/speed combination
aprscs = ''.join([aprscourse,'/',aprsspeed])
except Exception as e:
print("Some relevant position vector data is missing or malformatted in this record.")
print(e)
#Pass? Maybe...
pass
try:
#According to spec, one valid format for location beacons is
# @092345z/4903.50N/07201.75W>088/036
# with a comment on the end that can include altitude and other
# information.
aprsPacket = ''.join([getSSID(IMEI),ARPreamble,':@',aprstime,aprspos,aprscs,aprsalt,comment])
#This will throw an exception if the packet is somehow wrong.
aprslib.parse(aprsPacket)
AIS.sendall(aprsPacket)
lastUpdate[IMEI] = etime
return aprsPacket
except Exception as e:
print("Could not send the update: ")
if 'aprsPacket' in locals():
print(aprsPacket)
print(e)
print("Attempting reconnection, just in case.")
reconnect()
pass
while True:
try: try:
KML = http.open(conf['inReach']['URL']).read() KML = http.open(conf['inReach']['URL']).read()
except Exception as e: except Exception as e:
print(''.join(["Error reading URL: ", conf['inReach']['URL']])) print(''.join(["Error reading URL: ", conf['inReach']['URL']]))
continue return None
try: try:
data = xml.dom.minidom.parseString(KML).documentElement data = xml.dom.minidom.parseString(KML).documentElement
except Exception as e: except Exception as e:
print("Can't process KML feed on this pass.") print("Can't process KML feed on this pass.")
continue return None
#The first placemark will have the expanded current location information. #The first placemark will have the expanded current location information.
for PM in data.getElementsByTagName('Placemark'): for PM in data.getElementsByTagName('Placemark'):
sendAPRS(PM) res = parsePlacemark(PM)
if res:
events.append(res)
return events
#Compile and send an APRS packet from the given information
#According to spec, one valid format for location beacons is
# @092345z/4903.50N/07201.75W>088/036
# with a comment on the end that can include altitude and other
# information.
## Arguments are:
# Device type, Device ID, APRS Preamble, struct Timestamp, float Latitude
# float Longitude, float Altitude in feet, int float course in degrees,
# float speed in knots, comment
def sendAPRS(device, DevID, ARPreamble, tstamp, lat, long, alt, course, speed, comment):
global conf
etime = calendar.timegm(tstamp)
#Latitude conversion
#We start with the truncated degrees, filled to two places
# then add fractional minutes two 2x2 digits.
slat = str(abs(math.trunc(lat))).zfill(2)
slat += '{:.02f}'.format(round((abs(lat)-abs(math.trunc(lat)))*60,2)).zfill(5)
if lat > 0:
slat += "N"
else:
slat += "S"
#Longitude
slong = str(abs(math.trunc(long))).zfill(3)
slong += '{:.02f}'.format(round((abs(long)-abs(math.trunc(long)))*60,2)).zfill(5)
if long > 0:
slong += "E"
else:
slong += "W"
pos = ''.join([slat,conf['APRS']['Separator'],slong,conf['APRS']['Symbol']])
gateInfo = ''.join([" ", comment, " : ", NAME, " v", REV, " : ", platform.system(), " on ", platform.machine(), " : "])
if device:
gateInfo = gateInfo + device
#In the format we're using, APRS comments can be 36 characters
# ... but APRS-IS doesn't seem to care, so leave this off.
#comment = comment[:36]
aprsPacket = ''.join([getSSID(DevID),ARPreamble, ':@', time.strftime("%d%H%Mz",tstamp), pos])
#Check to make sure the update is new:
if lastUpdate[DevID] > etime:
return None
#In theory a course/speed of 000/000 sholdn't be much different
# from not reporting one, but also in theory, more space is
# available for a comment if we don't add the data extension.
if speed and course:
aprsPacket = ''.join([aprsPacket,str(round(course)).zfill(3), '/', str(min(round(speed),999)).zfill(3)])
#Same with altitude:
if alt:
#We need six digits of altitude.
aprsPacket = aprsPacket + "/A=" + str(round(alt)).zfill(6)[0:6]
# Regardless:
aprsPacket = aprsPacket + comment + gateInfo
try:
aprslib.parse(aprsPacket) # For syntax
AIS.sendall(aprsPacket)
except Exception as e:
print("Could not send APRS packet: ")
print(aprsPacket)
print("Attempting reconnect just in case.")
reconnect()
pass
#Last update in UTC
lastUpdate[DevID] = calendar.timegm(tstamp)
#... and here is the main loop.
while True:
for packet in getEvents():
sendAPRS(*packet) #Otherwise a list of sendAPRS args for the next packet to send.
time.sleep(conf.getfloat('General','Period')) time.sleep(conf.getfloat('General','Period'))