mirror of
https://github.com/kemenril/iR-APRSISD.git
synced 2024-11-09 23:24:07 -08:00
Untested support for multiple devices added. Single device should still be working ok.
This commit is contained in:
parent
6ae65a0738
commit
3f0600e2be
15
README.md
15
README.md
|
@ -31,4 +31,17 @@ This makes it possible to track an Iridium-based satellite device, say, on aprs.
|
||||||
|
|
||||||
* Run the daemon. It should begin to scan your KML feed.
|
* Run the daemon. It should begin to scan your KML feed.
|
||||||
|
|
||||||
|
### Multiple inReach devices
|
||||||
|
|
||||||
|
Some preliminary but untested support is now included for multiple inReach devices on the same feed. There are a few strategies for dealing with multiple devices:
|
||||||
|
|
||||||
|
* Just define a single SSID in the configuration file, leave the Devices section undefined, and the software will increment the number on your SSID for each new IMEI it finds in the feed. Mappings generated this way will be consistent within a single run, but may -- or may not -- change if you run the service again.
|
||||||
|
|
||||||
|
* Still define the SSID in the APRS section, since it's used for the login to APRS-IS, but also define the Devices section. Each line should have an SSID = IMEI mapping. All devices not present in the Devices section will be ignored.
|
||||||
|
|
||||||
|
* Run multiple instances of the daemon, each with an IMEI specified on the command-line, or each with a new configuration file and a Devices section that includes some but not all of the devices you want to watch.
|
||||||
|
|
||||||
|
If you have multiple inReach devices, try doing any one of the above, and if my guesses about the KML feed behavior are correct, they should all be accessible.
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
150
ir-aprsisd
150
ir-aprsisd
|
@ -28,32 +28,43 @@ from optparse import OptionParser
|
||||||
#Name of our configuration file.
|
#Name of our configuration file.
|
||||||
cf = "ir-aprsisd.cfg"
|
cf = "ir-aprsisd.cfg"
|
||||||
|
|
||||||
#Configuration
|
|
||||||
conf = configparser.ConfigParser()
|
|
||||||
#Try /etc, then try the current directory, or just quit.
|
|
||||||
try:
|
|
||||||
conf.read("/etc/" + cf)
|
|
||||||
if not conf.has_section('General'):
|
|
||||||
conf.read(cf)
|
|
||||||
if not conf.has_section('General'):
|
|
||||||
sys.exit("Can't read configuration file: " + cf)
|
|
||||||
except:
|
|
||||||
sys.exit("Can't read configuration file: " + cf)
|
|
||||||
|
|
||||||
|
|
||||||
#Command-line options
|
#Command-line options
|
||||||
op = OptionParser()
|
op = OptionParser()
|
||||||
|
|
||||||
|
op.add_option("-C","--config",action="store",type="string",dest="config",help="Load the named configuration file.")
|
||||||
op.add_option("-s","--ssid",action="store",type="string",dest="ssid",help="APRS SSID")
|
op.add_option("-s","--ssid",action="store",type="string",dest="ssid",help="APRS SSID")
|
||||||
op.add_option("-p","--pass",action="store",type="int",dest="passwd",help="APRS-IS password")
|
op.add_option("-p","--pass",action="store",type="int",dest="passwd",help="APRS-IS password")
|
||||||
op.add_option("-P","--port",action="store",type="int",dest="port",help="APRS-IS port")
|
op.add_option("--port",action="store",type="int",dest="port",help="APRS-IS port")
|
||||||
op.add_option("-u","--user",action="store",type="string",dest="user",help="inReach username")
|
op.add_option("-u","--user",action="store",type="string",dest="user",help="inReach username")
|
||||||
op.add_option("-i","--irpass",action="store",type="string",dest="irpass",help="inReach feed password")
|
op.add_option("-P","--irpass",action="store",type="string",dest="irpass",help="inReach feed password")
|
||||||
op.add_option("-U","--url",action="store",type="string",dest="url",help="URL for KML feed")
|
op.add_option("-U","--url",action="store",type="string",dest="url",help="URL for KML feed")
|
||||||
|
op.add_option("-i","--imei",action="store",type="int",dest="imei",help="This instance should watch *only* for the single IMEI given in this option. For a more complicated mapping, use the Device section in the configuration file.")
|
||||||
op.add_option("-c","--comment",action="store",type="string",dest="comment",help="APRS-IS location beacon comment text")
|
op.add_option("-c","--comment",action="store",type="string",dest="comment",help="APRS-IS location beacon comment text")
|
||||||
op.add_option("-d","--delay",action="store",type="int",dest="delay",help="Delay between polls of KML feed")
|
op.add_option("-d","--delay",action="store",type="int",dest="delay",help="Delay between polls of KML feed")
|
||||||
(opts,args) = op.parse_args()
|
(opts,args) = op.parse_args()
|
||||||
|
|
||||||
|
#This needs to be defined before the load below happens.
|
||||||
|
#Load a configuration file, and try to validate that it is loaded.
|
||||||
|
def loadConfig(cfile):
|
||||||
|
global conf
|
||||||
|
conf = configparser.ConfigParser()
|
||||||
|
if conf.read(cfile):
|
||||||
|
if conf.has_section('General'):
|
||||||
|
return True
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
|
#Handle loading of the configuration file first.
|
||||||
|
#Other command-line options may override things defined in the file.
|
||||||
|
if opts.config:
|
||||||
|
if not loadConfig(opts.config):
|
||||||
|
print("Can't load configuration: " + opts.config)
|
||||||
|
sys.exit(1)
|
||||||
|
else: #Default behavior if no file specified.
|
||||||
|
if not loadConfig("/etc/" + cf):
|
||||||
|
if not loadConfig(cf):
|
||||||
|
print("Can't find default configuration: " + cf)
|
||||||
|
|
||||||
#Allow command-line arguments to override the config file.
|
#Allow command-line arguments to override the config file.
|
||||||
if opts.ssid:
|
if opts.ssid:
|
||||||
conf['APRS']['SSID'] = opts.ssid
|
conf['APRS']['SSID'] = opts.ssid
|
||||||
|
@ -72,17 +83,31 @@ if opts.comment:
|
||||||
if opts.delay:
|
if opts.delay:
|
||||||
conf['General']['Period'] = opts.delay
|
conf['General']['Period'] = opts.delay
|
||||||
|
|
||||||
|
|
||||||
|
#Handle the special case where we've specified an IMEI on the command-line
|
||||||
|
if opts.imei:
|
||||||
|
conf['Devices'] = {}
|
||||||
|
conf['Devices'][conf['APRS']['SSID']] = str(opts.imei)
|
||||||
|
|
||||||
|
#Get the number and call from from the default SSID
|
||||||
|
#If we have multiple devices with non-specific ID mapping, we'll make it up
|
||||||
|
# from this.
|
||||||
|
(Call,SSNum) = re.search('(\w+)-(\d+)$',conf['APRS']['SSID']).groups()
|
||||||
|
SSNum = int(SSNum)
|
||||||
|
|
||||||
#The beginning of our APRS packets should contain a source, path,
|
#The beginning of our APRS packets should contain a source, path,
|
||||||
# q construct, and gateway address. We'll reuse the same SSID as a gate.
|
# q construct, and gateway address. We'll reuse the same SSID as a gate.
|
||||||
ARPreamble = ''.join([conf['APRS']['SSID'],'>APRS,','TCPIP*,','qAS,',conf['APRS']['SSID']])
|
# This is the part between the source and the gate.
|
||||||
|
ARPreamble = ''.join(['>APRS,','TCPIP*,','qAS,'])
|
||||||
|
|
||||||
NAME = "iR-APRSISD"
|
NAME = "iR-APRSISD"
|
||||||
REV = "0.1"
|
REV = "0.2"
|
||||||
|
|
||||||
#Start with the current time. We use this to keep track of whether we've
|
#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
|
# already sent a position, so if we cut it off when the program starts, we
|
||||||
# won't (re-)send old stale entry from inReach
|
# won't (re-)send old stale entry from inReach
|
||||||
lastUpdate = calendar.timegm(time.gmtime())
|
#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'):
|
||||||
|
@ -94,7 +119,6 @@ else:
|
||||||
http = urllib.request.build_opener()
|
http = urllib.request.build_opener()
|
||||||
urllib.request.install_opener(http)
|
urllib.request.install_opener(http)
|
||||||
|
|
||||||
|
|
||||||
#Handle connection to APRS-IS
|
#Handle connection to APRS-IS
|
||||||
def reconnect():
|
def reconnect():
|
||||||
global AIS
|
global AIS
|
||||||
|
@ -107,32 +131,49 @@ def reconnect():
|
||||||
print("Trouble connecting to APRS-IS server.")
|
print("Trouble connecting to APRS-IS server.")
|
||||||
print(e)
|
print(e)
|
||||||
|
|
||||||
|
#We'll store the IMEI to ssid mapping here.
|
||||||
|
SSIDList = {}
|
||||||
|
#We'll store timestamps here
|
||||||
|
lastUpdate = {}
|
||||||
|
|
||||||
|
#Load any preconfigured IMEI mappings
|
||||||
|
if conf.has_section('Devices'):
|
||||||
|
for device in conf['Devices'].keys():
|
||||||
|
SSIDList[conf['Devices'][device]] = device.upper()
|
||||||
|
|
||||||
|
|
||||||
|
#Get an SSID
|
||||||
|
def getSSID(IMEI):
|
||||||
|
global lastUpdate, SSIDList, SSNum, Call
|
||||||
|
#If we have a Devices section, the SSID list is static.
|
||||||
|
if IMEI not in SSIDList:
|
||||||
|
if conf.has_section('Devices'):
|
||||||
|
return None
|
||||||
|
SSIDList[IMEI] = ''.join([Call,"-",str(SSNum)])
|
||||||
|
SSNum = SSNum + 1
|
||||||
|
#Add a timestamp on the first call
|
||||||
|
# This prevents us from redelivering an old message, which can stay
|
||||||
|
# in the feed.
|
||||||
|
if IMEI not in lastUpdate:
|
||||||
|
lastUpdate[IMEI] = calendar.timegm(time.gmtime())
|
||||||
|
return SSIDList[IMEI]
|
||||||
|
|
||||||
|
|
||||||
#APRS-IS Setup
|
#APRS-IS Setup
|
||||||
AIS = None
|
AIS = None
|
||||||
reconnect()
|
reconnect()
|
||||||
|
|
||||||
|
#Generate an APRS packet from a Placemark
|
||||||
while True:
|
def sendAPRS(Placemark):
|
||||||
try:
|
#We only care about the Placemarks with the ExtendedData sections.
|
||||||
KML = http.open(conf['inReach']['URL']).read()
|
if not Placemark.getElementsByTagName('ExtendedData'):
|
||||||
except Exception as e:
|
return None
|
||||||
print(''.join(["Error reading URL: ", conf['inReach']['URL']]))
|
|
||||||
continue
|
|
||||||
try:
|
|
||||||
data = xml.dom.minidom.parseString(KML).documentElement
|
|
||||||
#The first placemark will have the expanded current location information.
|
|
||||||
position = data.getElementsByTagName('Placemark')[0]
|
|
||||||
|
|
||||||
#Now process the extended data into something easier to handle.
|
#Now process the extended data into something easier to handle.
|
||||||
extended = {}
|
extended = {}
|
||||||
for xd in position.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
|
||||||
except Exception as e:
|
|
||||||
print(''.join(["Could not parse data from URL: ",conf['inReach']['URL']]))
|
|
||||||
continue
|
|
||||||
|
|
||||||
try:
|
try:
|
||||||
#Here is the position vector
|
#Here is the position vector
|
||||||
latitude = extended['Latitude']
|
latitude = extended['Latitude']
|
||||||
|
@ -142,7 +183,12 @@ while True:
|
||||||
course = extended['Course']
|
course = extended['Course']
|
||||||
uttime = extended['Time UTC']
|
uttime = extended['Time UTC']
|
||||||
device = extended['Device Type']
|
device = extended['Device Type']
|
||||||
|
IMEI = extended['IMEI']
|
||||||
|
SSID = getSSID(IMEI)
|
||||||
|
|
||||||
|
#If there's no valid SSID mapping, we don't process this one.
|
||||||
|
if not SSID:
|
||||||
|
return None
|
||||||
|
|
||||||
#Some time conversions. First the time struct.
|
#Some time conversions. First the time struct.
|
||||||
ts = time.strptime(''.join([uttime," UTC"]),"%m/%d/%Y %I:%M:%S %p %Z")
|
ts = time.strptime(''.join([uttime," UTC"]),"%m/%d/%Y %I:%M:%S %p %Z")
|
||||||
|
@ -152,9 +198,9 @@ while True:
|
||||||
aprstime = time.strftime("%d%H%Mz",ts)
|
aprstime = time.strftime("%d%H%Mz",ts)
|
||||||
|
|
||||||
#Skip this one if it's old.
|
#Skip this one if it's old.
|
||||||
if lastUpdate > etime:
|
if lastUpdate[IMEI] > etime:
|
||||||
time.sleep(conf.getfloat('General','Period'))
|
time.sleep(conf.getfloat('General','Period'))
|
||||||
continue
|
return None
|
||||||
|
|
||||||
#If we have SMS data, add that.
|
#If we have SMS data, add that.
|
||||||
if 'Text' in extended:
|
if 'Text' in extended:
|
||||||
|
@ -217,12 +263,13 @@ while True:
|
||||||
# @092345z/4903.50N/07201.75W>088/036
|
# @092345z/4903.50N/07201.75W>088/036
|
||||||
# with a comment on the end that can include altitude and other
|
# with a comment on the end that can include altitude and other
|
||||||
# information.
|
# information.
|
||||||
aprsPacket = ''.join([ARPreamble,':@',aprstime,aprspos,aprscs,aprsalt,comment])
|
aprsPacket = ''.join([getSSID(IMEI),ARPreamble,getSSID(IMEI),':@',aprstime,aprspos,aprscs,aprsalt,comment])
|
||||||
print(aprsPacket)
|
|
||||||
#This will throw an exception if the packet is somehow wrong.
|
#This will throw an exception if the packet is somehow wrong.
|
||||||
aprslib.parse(aprsPacket)
|
aprslib.parse(aprsPacket)
|
||||||
AIS.sendall(aprsPacket)
|
AIS.sendall(aprsPacket)
|
||||||
lastUpdate = etime
|
lastUpdate[IMEI] = etime
|
||||||
|
return aprsPacket
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
print("Could not send the update: ")
|
print("Could not send the update: ")
|
||||||
if 'aprsPacket' in locals():
|
if 'aprsPacket' in locals():
|
||||||
|
@ -231,6 +278,21 @@ while True:
|
||||||
print("Attempting reconnection, just in case.")
|
print("Attempting reconnection, just in case.")
|
||||||
reconnect()
|
reconnect()
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
while True:
|
||||||
|
try:
|
||||||
|
KML = http.open(conf['inReach']['URL']).read()
|
||||||
|
except Exception as e:
|
||||||
|
print(''.join(["Error reading URL: ", conf['inReach']['URL']]))
|
||||||
|
continue
|
||||||
|
try:
|
||||||
|
data = xml.dom.minidom.parseString(KML).documentElement
|
||||||
|
except Exception as e:
|
||||||
|
print("Can't process KML feed on this pass.")
|
||||||
|
continue
|
||||||
|
#The first placemark will have the expanded current location information.
|
||||||
|
for PM in data.getElementsByTagName('Placemark'):
|
||||||
|
sendAPRS(PM)
|
||||||
time.sleep(conf.getfloat('General','Period'))
|
time.sleep(conf.getfloat('General','Period'))
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
[inReach]
|
[inReach]
|
||||||
User = Your-inReach-Username
|
User = Your inReach feed user
|
||||||
#If this is defined, we will authenticate to the inReach service.
|
#If this is defined, we will authenticate to the inReach service.
|
||||||
# If it is not, we will assume public access is ok.
|
# If it is not, we will assume public access is ok.
|
||||||
#Password = Your inReach feed password, if required
|
#Password = Your inReach feed password, if required
|
||||||
|
@ -8,7 +8,12 @@ User = Your-inReach-Username
|
||||||
URL = https://share.garmin.com/Feed/Share/%(User)s
|
URL = https://share.garmin.com/Feed/Share/%(User)s
|
||||||
|
|
||||||
[APRS]
|
[APRS]
|
||||||
SSID = N0CALL-10
|
#This SSID is used for logging into APRS-IS, and also as a base ID for
|
||||||
|
# generating callsigns for devices. The first device found will be this
|
||||||
|
# SSID, the next will be this ID + 1, and so on. If you define a [Devices]
|
||||||
|
# section, it is _only_ used for the login, and the device mapping must be
|
||||||
|
# given in full in the [Devices] section.
|
||||||
|
SSID = N0CALL
|
||||||
Password = 1234
|
Password = 1234
|
||||||
Port = 14580
|
Port = 14580
|
||||||
|
|
||||||
|
@ -22,9 +27,16 @@ Symbol = (
|
||||||
# other data.
|
# other data.
|
||||||
Comment = APRS-IS KML forwarder, by K0SIN
|
Comment = APRS-IS KML forwarder, by K0SIN
|
||||||
|
|
||||||
|
#Define this section if you'd like to enforce an SSID to IMEI mapping.
|
||||||
|
# It must contain all devices you want to publish. Anything without a
|
||||||
|
# mapping defined will be ignored if this section exists.
|
||||||
|
#[Devices]
|
||||||
|
#N0CALL-12 = 987654321987654
|
||||||
|
#N0CALL-15 = 987654321987656
|
||||||
|
#N0CALL-8 = 092847784398753
|
||||||
|
|
||||||
[General]
|
[General]
|
||||||
# KML polling interval in seconds.
|
# KML polling interval in seconds.
|
||||||
Period = 300
|
Period = 300
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
Loading…
Reference in a new issue