mirror of
https://github.com/kemenril/iR-APRSISD.git
synced 2024-11-09 23:24:07 -08:00
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:
parent
0ef9fd7004
commit
964efba308
257
ir-aprsisd
257
ir-aprsisd
|
@ -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
|
|
||||||
latitude = extended['Latitude']
|
|
||||||
longitude = extended['Longitude']
|
|
||||||
elevation = extended['Elevation']
|
|
||||||
velocity = extended['Velocity']
|
|
||||||
course = extended['Course']
|
|
||||||
uttime = extended['Time UTC']
|
|
||||||
device = extended['Device Type']
|
|
||||||
IMEI = extended['IMEI']
|
|
||||||
SSID = getSSID(IMEI)
|
|
||||||
|
|
||||||
#If there's no valid SSID mapping, we don't process this one.
|
#Make sure the device mapping is good.
|
||||||
if not SSID:
|
if not 'IMEI' in extended:
|
||||||
return None
|
return None
|
||||||
|
if not getSSID(extended['IMEI']):
|
||||||
|
return None
|
||||||
|
|
||||||
#Some time conversions. First the time struct.
|
#Now build the position vector
|
||||||
ts = time.strptime(''.join([uttime," UTC"]),"%m/%d/%Y %I:%M:%S %p %Z")
|
latitude = None
|
||||||
#Unix epoch time for local record-keeping.
|
longitude = None
|
||||||
etime = calendar.timegm(ts)
|
elevation = None
|
||||||
#MonthDayHoursMinutes_z for APRS-IS packet
|
velocity = None
|
||||||
aprstime = time.strftime("%d%H%Mz",ts)
|
course = None
|
||||||
|
uttime = None
|
||||||
#Skip this one if it's old.
|
device = None
|
||||||
if lastUpdate[IMEI] > etime:
|
IMEI = extended['IMEI']
|
||||||
time.sleep(conf.getfloat('General','Period'))
|
|
||||||
return None
|
|
||||||
|
|
||||||
#If we have SMS data, add that.
|
|
||||||
if 'Text' in extended:
|
|
||||||
comment = extended['Text']
|
|
||||||
else: #Default 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
|
|
||||||
#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.
|
|
||||||
longitude = float(longitude)
|
|
||||||
aprslong = str(abs(math.trunc(longitude))).zfill(3)
|
|
||||||
aprslong += '{:.02f}'.format(round((abs(longitude)-abs(math.trunc(longitude)))*60,2)).zfill(5)
|
|
||||||
if longitude > 0:
|
|
||||||
aprslong += "E"
|
|
||||||
else:
|
|
||||||
aprslong += "W"
|
|
||||||
|
|
||||||
|
if 'Latitude' in extended and 'Longitude' in extended:
|
||||||
|
latitude = float(extended['Latitude'])
|
||||||
|
longitude = float(extended['Longitude'])
|
||||||
|
if 'Elevation' in extended:
|
||||||
#Altitude needs to be in feet above sea-level
|
#Altitude needs to be in feet above sea-level
|
||||||
# what we get instead is a string with a number of meters
|
# what we get instead is a string with a number of meters
|
||||||
# at the beginning.
|
# at the beginning.
|
||||||
aprsalt = re.sub(r'^(\d+\.?\d+)\s*m.*',r'\1',elevation)
|
elevation = re.sub(r'^(\d+\.?\d+)\s*m.*',r'\1',extended['Elevation'])
|
||||||
#We need integer feet, six digits
|
elevation = float(elevation) * 3.2808399
|
||||||
aprsalt = str(round(float(aprsalt) * 3.2808399)).zfill(6)
|
if 'Velocity' in extended:
|
||||||
aprsalt = "/A=" + aprsalt[0:6]
|
#Velocity in knots, according to APRS.
|
||||||
|
velocity = float(re.sub(r'(\d+\.?\d+).*',r'\1', extended['Velocity']))*0.5399568
|
||||||
|
if 'Course' in extended:
|
||||||
|
#... and the course is just a heading in degrees.
|
||||||
|
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']
|
||||||
|
|
||||||
#Aprs position
|
#If we have SMS data, add that.
|
||||||
aprspos = ''.join([aprslat,conf['APRS']['Separator'],aprslong,conf['APRS']['Symbol']])
|
if 'Text' in extended:
|
||||||
|
comment = extended['Text']
|
||||||
|
else: #Default comment
|
||||||
|
comment = conf['APRS']['Comment']
|
||||||
|
|
||||||
#Course is already in degrees, so we just need to reformat it.
|
return [device,IMEI,ARPreamble,uttime,latitude,longitude,elevation,course,velocity,comment]
|
||||||
aprscourse = str(round(float(re.sub(r'(\d+\.?\d+).*',r'\1', course)))).zfill(3)
|
|
||||||
|
|
||||||
#Speed is in km/h.
|
#Return a list of all events available. Each one is a list of arguments
|
||||||
aprsspeed = float(re.sub(r'(\d+\.?\d+).*',r'\1',velocity))*0.53995681
|
# for the below sendAPRS function
|
||||||
aprsspeed = str(min(round(aprsspeed),999)).zfill(3)
|
def getEvents():
|
||||||
|
events = []
|
||||||
#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'))
|
||||||
|
|
||||||
|
|
||||||
|
|
Loading…
Reference in a new issue