Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Not working #6

Closed
ghost opened this issue Aug 21, 2023 · 9 comments
Closed

Not working #6

ghost opened this issue Aug 21, 2023 · 9 comments

Comments

@ghost
Copy link

ghost commented Aug 21, 2023

Hello,
unfortunately the script doesn't work.
However, no packages will be sent.
Can someone fix the script?
Greeting

@ghost
Copy link
Author

ghost commented Aug 21, 2023

ir-aprsisd

#!/usr/bin/python3

############################################################################
##
## ir-aprsisd
##
##	This is a KML-feed to APRS-IS forwarding daemon.  It was written
##	for Garmin/DeLorme inReach devices, but might work elsewhere too.
##	It polls a KML feed in which it expects to find a point in rougly
##	the form that the inReach online feeds use, with attendant course
##	and altitude data.  It transfers each new point found there to 
##	APRS-IS.
##
##	K0SIN
##
###########################################################################


import aprslib
import urllib.request
import xml.dom.minidom
import time, calendar, math, re
import platform, sys, os, signal
import configparser
from optparse import OptionParser


#Name of our configuration file.
cf = "ir-aprsisd.cfg"

#Command-line options
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("-p","--pass",action="store",type="int",dest="passwd",help="APRS-IS password")
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("-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("-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("-d","--delay",action="store",type="int",dest="delay",help="Delay between polls of KML feed")
op.add_option("--genpass",action="store_true",dest="genpass",help="Generate the correct passcode for the SSID given in the configuration, or on the command line, print it, and exit.")
(opts,args) = op.parse_args()

#Handle term and int signals
def trapexit(_signo,_stack_frame):
	print()
	print("Exiting.")
	sys.exit(0)

signal.signal(signal.SIGTERM,trapexit)
signal.signal(signal.SIGINT,trapexit)

#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'):
			print("Loaded configuration: " + cfile)
			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(os.path.join("/etc", cf)):
		if not loadConfig(os.path.join(os.path.dirname(os.path.abspath(__file__)),cf)):
			if not loadConfig(cf):
				print("Can't find configuration: " + cf)
				sys.exit(1)

#Allow command-line arguments to override the config file.
if opts.ssid:
	conf['APRS']['SSID'] =		opts.ssid
if opts.passwd:
	conf['APRS']['Password'] =		str(opts.passwd)
if opts.port:
	conf['APRS']['Port'] =		str(opts.port)
if opts.user:
	conf['inReach']['User'] = 	opts.user
if opts.irpass:
	conf['inReach']['Password'] = 	opts.irpass
if opts.url:
	conf['inReach']['URL'] = 	opts.url
if opts.comment:
	conf['APRS']['Comment'] =	opts.comment
if opts.delay:
	conf['General']['Period'] =	opts.delay

#SSID should be standardized to upper-case.
conf['APRS']['SSID'] = conf['APRS']['SSID'].upper()

#Running in passcode generator mode.
if opts.genpass:
	print("Using SSID: " + conf['APRS']['SSID'])
	print("The passcode is: " + str(aprslib.passcode(conf['APRS']['SSID'])))
	print()
	sys.exit(0)


#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.
match = re.search('(\w+)-(\d+)$',conf['APRS']['SSID'])
if match is not None:
    (Call,SSNum) = match.groups()
    SSNum = int(SSNum)
else:
    # Handle the case when there is no match
    # For example, assign some default values or raise an exception
    Call = (conf['APRS']['SSID'])
    SSNum = 1

#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.
# This is the part between the source and the gate.
ARPreamble	= ''.join(['>APRS,','TCPIP*,','qAS,',conf['APRS']['SSID']])

NAME		= "iR-APRSISD"
REV		= "0.3"

#Set up the handler for HTTP connections
if conf.has_option('inReach','Password'):
	passman		= urllib.request.HTTPPasswordMgrWithDefaultRealm()
	passman.add_password(None, conf['inReach']['URL'],'',conf['inReach']['Password'])
	httpauth	= urllib.request.HTTPBasicAuthHandler(passman)
	http		= urllib.request.build_opener(httpauth)
else:
	http		= urllib.request.build_opener()
urllib.request.install_opener(http)

#Handle connection to APRS-IS
def reconnect():
	global AIS
	attempt = 1
	while True:
		AIS = aprslib.IS(conf['APRS']['SSID'],passwd=conf['APRS']['Password'],host=conf['APRS']['Host'],port=conf['APRS']['Port'],skip_login=conf['APRS']['Skip_Login'])
		try:
			AIS.connect()
			break
		except Exception as e:
			print("Connection failed.  Reconnecting: " + str(attempt))
			attempt += 1
			time.sleep(3)
			continue

#We'll store the device to ssid mapping here.
SSIDList = {}
#We'll store timestamps here
lastUpdate = {}

#Packet counts here
transmitted = {}
#Last time stats() was run:
lastStats = calendar.timegm(time.gmtime())


#Load any preconfigured mappings
if conf.has_section('Devices'):
	print("Loading predefined SSID mappings.")
	for device in conf['Devices'].keys():
		SSIDList[conf['Devices'][device]] = device.upper()
		print("Static mapping: " + SSIDList[conf['Devices'][device]] + " -> " + device.upper())
		

#Get an SSID
def getSSID(DID):
	global lastUpdate, SSIDList, transmitted, SSNum, Call
	if not DID:	# Don't map None
		return None
	#If we have a Devices section, the SSID list is static.
	if DID not in SSIDList:
		if conf.has_section('Devices'):
			return None
		SSIDList[DID] = ''.join([Call,"-",str(SSNum)])
		SSNum = SSNum + 1
		print("Mapping: " + DID + " -> " + SSIDList[DID])
	#Add a timestamp on the first call
	# This prevents us from redelivering an old message, which can stay
	# in the feed.
	if DID not in lastUpdate:
		lastUpdate[DID] = calendar.timegm(time.gmtime())
	if DID not in transmitted:
		transmitted[DID] = 0
	return SSIDList[DID]


#APRS-IS Setup
AIS = None
reconnect()

#Get information from a Placemark
def parsePlacemark(Placemark):
	#We only care about the Placemarks with the ExtendedData sections.
	if not Placemark.getElementsByTagName('ExtendedData'):
		return None
		
	#Now process the extended data into something easier to handle.
	extended = {}
	for xd in Placemark.getElementsByTagName('ExtendedData')[0].getElementsByTagName('Data'):
		if not xd.getElementsByTagName('value')[0].firstChild == None:
			extended[xd.getAttribute('name')] = xd.getElementsByTagName('value')[0].firstChild.nodeValue	

	#Make sure the device mapping is good.
	if not 'IMEI' in extended:
		return None
	if not getSSID(extended['IMEI']):
		return None

	#Now build the position vector
	latitude	= None
	longitude	= None
	elevation	= None
	velocity	= None
	course		= None
	uttime		= None
	device		= None
	IMEI		= extended['IMEI']

	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
		# what we get instead is a string with a number of meters
		# at the beginning.
		elevation       = re.sub(r'^(-?\d+\.?\d+)\s*m.*',r'\1',extended['Elevation'])
		elevation       = float(elevation) * 3.2808399
	if 'Velocity' in extended:
		#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']

	#If we have SMS data, add that.
	if 'Text' in extended:
		comment = extended['Text']
	else:	#Default comment
		comment = conf['APRS']['Comment']

	return [device,IMEI,ARPreamble,uttime,latitude,longitude,elevation,course,velocity,comment]

#Return a list of all events available.  Each one is a list of arguments
# for the below sendAPRS function
def getEvents():
	events = []
	try:
		KML = http.open(conf['inReach']['URL']).read()
	except Exception as e:
		print("Error reading URL: " + conf['inReach']['URL'])
		return events
	try:
		data = xml.dom.minidom.parseString(KML).documentElement
	except Exception as e:
		print("Can't process KML feed on this pass.")
		return events
	#The first placemark will have the expanded current location information.
	for PM in data.getElementsByTagName('Placemark'):
		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, transmitted, lastUpdate
	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([" : ", NAME, " v", REV, " : ", platform.system(), " on ", platform.machine(), " : "])
	
	if device:
		gateInfo = gateInfo + device

	aprsPacket	= ''.join([getSSID(DevID),ARPreamble, ':@', time.strftime("%d%H%Mz",tstamp), pos]) 

	#Check to make sure the update is new:
	if not etime > lastUpdate[DevID]:
		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 != None) and (course != None):
		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.
		comment = "/A=" + str(round(alt)).zfill(6)[0:6] + comment

	#Regardless:
	comment = comment + gateInfo

	#We have the whole comment in one place now.
	#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		= aprsPacket + comment

	try:
		aprslib.parse(aprsPacket)	# For syntax
		AIS.sendall(aprsPacket)
		#If the above doesn't raise an exception, we should assume we've sent the packet.
		lastUpdate[DevID] = calendar.timegm(tstamp)
		transmitted[DevID] += 1
		print("Sent: " + aprsPacket)
	except Exception as e:
		print("Could not send APRS packet: ")
		print(aprsPacket)
		print("Attempting reconnect just in case.")
		reconnect()
		return None

def stats():
	global transmitted, lastStats
	lastStats = calendar.timegm(time.gmtime())
	print("----------------Packet Forwarding Summary----------------")
	print("|\t" + time.strftime("%Y-%m-%d %R",time.localtime()))
	print("| SSID		DevID			Packets forwarded")
	for device in transmitted:
		print("| " + getSSID(device) + "\t" + device + "\t\t" + str(transmitted[device]))
	print("---------------------------------------------------------")
	print()

#... 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.
	if "Logstats" in conf["General"]:
		if calendar.timegm(time.gmtime()) > lastStats + conf.getint("General","Logstats"):
			stats()
	time.sleep(conf.getfloat('General','Period'))

@ghost
Copy link
Author

ghost commented Aug 21, 2023

The following adjustment was made to change the server

ir-aprsisd

AIS = aprslib.IS(conf['APRS']['SSID'],passwd=conf['APRS']['Password'],host=conf['APRS']['Host'],port=conf['APRS']['Port'],skip_login=conf['APRS']['Skip_Login'])

@ghost
Copy link
Author

ghost commented Aug 21, 2023

ir-aprsisd.cfg

[inReach]
User		= ABC
#If this is defined, we will authenticate to the inReach service.
# If it is not, we will assume public access is ok.
Password	= ABC

# This should be the location of your KML feed.
URL		= https://share.garmin.com/Feed/Share/%(User)s

[APRS]
#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		= ABC
# If you don't have a password, you can use the --genpass command-line
# option to calculate it.
Password	= ABC
Skip_Login  = True
Port		= 27235 
Host        = cbaprs.de 

#If the separator is /, your icon will come from the primary symbol table.
# if it is \, it will draw from the secondary table.
Separator	= /
#This character represents an APRS icon from the table tied to Separator.
Symbol		= (

#This information is included at the end of each packet, along with some 
# other data.
Comment		= APRS-IS KML forwarder

#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-1	= 987654322987656
#N0CALL-15	= 987654321987656
#N0CALL-8	= 092847784398753

[General]

# Frequency in seconds with which to log packet forwarding stats to STDOUT
# If this is less than Period, stats will only be logged every Period seconds.
# Comment it out to skip printing packet stats entirely.
Logstats        = 10

# KML polling interval in seconds.
Period          = 10




@ghost
Copy link
Author

ghost commented Aug 21, 2023

Unfortunately, the original script doesn't work.
With the adjustments, unfortunately it doesn't work either.
No packages will be sent

Info:

PS C:\temp\iR-APRSISD-main> python.exe ir-aprsisd
Loaded configuration: C:\temp\iR-APRSISD-main\ir-aprsisd.cfg
Mapping: XXXXXXXXXXXXXXX -> C64IR1-1
----------------Packet Forwarding Summary----------------
|       2023-08-21 22:58
| SSID          DevID                   Packets forwarded
| C64IR1-1      XXXXXXXXXXXXXXX         0
---------------------------------------------------------

----------------Packet Forwarding Summary----------------
|       2023-08-21 22:58
| SSID          DevID                   Packets forwarded
| C64IR1-1      XXXXXXXXXXXXXXX         0
---------------------------------------------------------

@kemenril
Copy link
Owner

Hello, it's been a bit of time since I wrote this, so I'll need to dig in a bit to see what I've done. First, it looks like you're using Windows. This may not be related to the problem, but I feel like I should mention that I have never tested this on a Windows machine, and I don't know whether anyone has. You might run into trouble that the rest of us have not, though I'm happy to help you work it out.

I should ask you what the original error was before you changed the script. It might give me some direction. Would you mind pasting the error in?

@ghost
Copy link
Author

ghost commented Aug 24, 2023

The problem is that no packets are sent and the connection to the APRS server cannot be established.
I created new scripts so I can send the data.
Greeting

@kemenril
Copy link
Owner

If you can't connect to the APRS server, that's definitely a problem. So did the program initially start up and run ok, and just not send any packets?

Also, I never really wanted to connect to an alternate host, and since nobody else requested the feature, you might actually be the first to need it, but it looks reasonably easy to add that feature, and I think it's a good idea. I'll try to go ahead and do that soon.

@kemenril
Copy link
Owner

Also, do you get "could not send APRS packet" errors?

@ghost
Copy link
Author

ghost commented Aug 29, 2023

Hi,
I also create a script.
Unfortunately without DB, I don't have the experience.
Of course, it would be great if the data is stored in a DB, then you could read the data with e.g. Google Earth.
Maybe this is an inspiration for you to revise your script.
Greeting

@ghost ghost closed this as completed Aug 29, 2023
This issue was closed.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

1 participant