1
0
mirror of https://github.com/craigerl/aprsd.git synced 2025-06-24 21:15:18 -04:00

Require ~/.aprsd/config.yml

This patch completes the migration to using a config.yml file.
~/.aprsd/config.yml is now required and all options for callsign,
imap, aprs user, passwords are in the config.  If there is no existing
~/.aprsd/config.yml file, then the app will output a sample config
and exit.

This patch also adds a global logging facility that allows logging all
commands to aprsd.log as well as stdout.  You can disable logging to
stdout by adding --quiet on the command line.  You can specify the log
level with --loglevel INFO.  By default the log level is DEBUG.

This patch also updates some formatting issues and small refactoring
to ensure that the logging facility and config is read prior to starting
any network connections and/or services.
This commit is contained in:
Walter A. Boring IV 2018-11-29 08:22:41 -05:00
parent 4c8d9c3b2c
commit ce7a30aa78
2 changed files with 439 additions and 371 deletions

View File

@ -1,9 +1,10 @@
#!/usr/bin/python -u #!/usr/bin/python -u
# #
# Listen on amateur radio aprs-is network for messages and respond to them. # Listen on amateur radio aprs-is network for messages and respond to them.
# You must have an amateur radio callsign to use this software. Put your # You must have an amateur radio callsign to use this software. You must
# callsign in the "USER" variable and update your aprs-is password in "PASS". # create an ~/.aprsd/config.yml file with all of the required settings. To
# You must also have an imap email account available for polling. # generate an example config.yml, just run aprsd, then copy the sample config
# to ~/.aprsd/config.yml and edit the settings.
# #
# APRS messages: # APRS messages:
# l(ocation) = descriptive location of calling station # l(ocation) = descriptive location of calling station
@ -21,23 +22,24 @@
# python included libs # python included libs
import argparse import argparse
import json
import urllib
import sys
import os
import telnetlib
import time
import re
from random import randint
import smtplib
from email.mime.text import MIMEText
import subprocess
import datetime import datetime
import calendar
import email import email
import threading import json
import signal import logging
import os
import pprint import pprint
import re
import signal
import smtplib
import subprocess
import sys
import telnetlib
import threading
import time
import urllib
from email.mime.text import MIMEText
from logging.handlers import RotatingFileHandler
# external lib imports # external lib imports
from imapclient import IMAPClient, SEEN from imapclient import IMAPClient, SEEN
@ -46,87 +48,60 @@ from imapclient import IMAPClient, SEEN
from aprsd.fuzzyclock import fuzzy from aprsd.fuzzyclock import fuzzy
import utils import utils
# setup the global logger
LOG = logging.getLogger('APRSD')
# global for the config yaml
CONFIG = None
# localization, please edit: # localization, please edit:
HOST = "noam.aprs2.net" # north america tier2 servers round robin # HOST = "noam.aprs2.net" # north america tier2 servers round robin
USER = "KM6XXX-9" # callsign of this aprs client with SSID # USER = "KM6XXX-9" # callsign of this aprs client with SSID
PASS = "99999" # google how to generate this # PASS = "99999" # google how to generate this
BASECALLSIGN = "KM6XXX" # callsign of radio in the field to which we send email # BASECALLSIGN = "KM6XXX" # callsign of radio in the field to which we send email
shortcuts = { # shortcuts = {
"aa" : "5551239999@vtext.com", # "aa" : "5551239999@vtext.com",
"cl" : "craiglamparter@somedomain.org", # "cl" : "craiglamparter@somedomain.org",
"wb" : "5553909472@vtext.com" # "wb" : "5553909472@vtext.com"
} # }
# globals - tell me a better way to update data being used by threads # globals - tell me a better way to update data being used by threads
email_sent_dict = {} # message_number:time combos so we don't resend the same email in five mins {int:int} email_sent_dict = {} # message_number:time combos so we don't resend the same email in five mins {int:int}
ack_dict = {} # message_nubmer:ack combos so we stop sending a message after an ack from radio {int:int} ack_dict = {} # message_nubmer:ack combos so we stop sending a message after an ack from radio {int:int}
message_number = 0 # current aprs radio message number, increments for each message we send over rf {int} message_number = 0 # current aprs radio message number, increments for each message we send over rf {int}
# global telnet connection object
tn = None
# command line args # command line args
parser = argparse.ArgumentParser() parser = argparse.ArgumentParser()
parser.add_argument("--user", parser.add_argument("--loglevel",
metavar="<user>", default='DEBUG',
default=utils.env("APRS_USER"), choices=['CRITICAL', 'ERROR', 'WARNING', 'INFO', 'DEBUG'],
help="The callsign of this ARPS client with SSID" help="The log level to use for aprsd.log")
" Default=env[APRS_USER]") parser.add_argument("--quiet",
action='store_true',
parser.add_argument("--host", help="Don't log to stdout")
metavar="<host>",
default=utils.env("APRS_HOST"),
help="The aprs host to use Default=env[APRS_HOST]")
parser.add_argument("--password",
metavar="<password>",
default=utils.env("APRS_PASSWORD"),
help="The aprs password Default=env[APRS_PASSWORD]")
parser.add_argument("--callsign",
metavar="<callsign>",
default=utils.env("APRS_CALLSIGN"),
help="The callsign of radio in the field to which we send "
"email Default=env[APRS_CALLSIGN]")
args = parser.parse_args() args = parser.parse_args()
if not args.user:
print("Missing the aprs user (env[APRS_USER])")
parser.print_help()
parser.exit()
else:
USER = args.user
if not args.password:
print("Missing the aprs password (env[APRS_PASSWORD])")
parser.print_help()
parser.exit()
else:
PASS = args.password
if not args.callsign:
print("Missing the aprs callsign (env[APRS_CALLSIGN])")
parser.print_help()
parser.exit()
else:
BASECALLSIGN = args.callsign
# Now read the ~/.aprds/config.yml def setup_connection():
config = utils.get_config() global tn
if 'shortcuts' in config: host = CONFIG['aprs']['host']
shortcuts = config['shortcuts'] LOG.debug("Setting up telnet connection to '%s'" % host)
else: try:
print("missing 'shortcuts' section of config.yml") tn = telnetlib.Telnet(host, 14580)
sys.exit(-1) except Exception, e:
LOG.critical("Telnet session failed.\n", e)
try: sys.exit(-1)
tn = telnetlib.Telnet(HOST, 14580)
except Exception, e:
print "Telnet session failed.\n"
sys.exit(-1)
def signal_handler(signal, frame): def signal_handler(signal, frame):
print("Ctrl+C, exiting.") LOG.info("Ctrl+C, exiting.")
#sys.exit(0) # thread ignores this #sys.exit(0) # thread ignores this
os._exit(0) os._exit(0)
signal.signal(signal.SIGINT, signal_handler)
### end signal_handler ### end signal_handler
def parse_email(msgid, data, server): def parse_email(msgid, data, server):
@ -172,107 +147,116 @@ def parse_email(msgid, data, server):
def resend_email(count): def resend_email(count):
date = datetime.datetime.now() date = datetime.datetime.now()
month = date.strftime("%B")[:3] # Nov, Mar, Apr month = date.strftime("%B")[:3] # Nov, Mar, Apr
day = date.day day = date.day
year = date.year year = date.year
today = str(day) + "-" + month + "-" + str(year) today = str(day) + "-" + month + "-" + str(year)
global shortcuts shortcuts = CONFIG['shortcuts']
shortcuts_inverted = dict([[v,k] for k,v in shortcuts.items()]) # swap key/value shortcuts_inverted = dict([[v,k] for k,v in shortcuts.items()]) # swap key/value
server = IMAPClient('imap.yourdomain.com', use_uid=True) LOG.debug("resend_email: Connect to IMAP host '%s' with user '%s'" %
server.login('KM6XXX@yourdomain.org', 'yourpassword') (CONFIG['imap']['host'],
select_info = server.select_folder('INBOX') CONFIG['imap']['login']))
server = IMAPClient(CONFIG['imap']['host'], use_uid=True)
server.login(CONFIG['imap']['login'], CONFIG['imap']['password'])
# select_info = server.select_folder('INBOX')
messages = server.search(['SINCE', today]) messages = server.search(['SINCE', today])
#print("%d messages received today" % len(messages)) LOG.debug("%d messages received today" % len(messages))
msgexists = False msgexists = False
messages.sort(reverse=True) messages.sort(reverse=True)
del messages[int(count):] # only the latest "count" messages del messages[int(count):] # only the latest "count" messages
for message in messages: for message in messages:
for msgid, data in list(server.fetch(message, ['ENVELOPE']).items()): # one at a time, otherwise order is random for msgid, data in list(server.fetch(message, ['ENVELOPE']).items()): # one at a time, otherwise order is random
(body, from_addr) = parse_email(msgid, data, server) (body, from_addr) = parse_email(msgid, data, server)
server.remove_flags(msgid, [SEEN]) # unset seen flag, will stay bold in email client server.remove_flags(msgid, [SEEN]) # unset seen flag, will stay bold in email client
if from_addr in shortcuts_inverted: # reverse lookup of a shortcut if from_addr in shortcuts_inverted: # reverse lookup of a shortcut
from_addr = shortcuts_inverted[from_addr] from_addr = shortcuts_inverted[from_addr]
reply = "-" + from_addr + " * " + body # asterisk indicates a resend reply = "-" + from_addr + " * " + body # asterisk indicates a resend
send_message(fromcall, reply) send_message(fromcall, reply)
msgexists = True msgexists = True
if msgexists is not True: if msgexists is not True:
stm = time.localtime() stm = time.localtime()
h = stm.tm_hour h = stm.tm_hour
m = stm.tm_min m = stm.tm_min
s = stm.tm_sec s = stm.tm_sec
# append time as a kind of serial number to prevent FT1XDR from thinking this is a duplicate message. # append time as a kind of serial number to prevent FT1XDR from thinking this is a duplicate message.
# The FT1XDR pretty much ignores the aprs message number in this regard. The FTM400 gets it right. # The FT1XDR pretty much ignores the aprs message number in this regard. The FTM400 gets it right.
reply = "No new msg " + str(h).zfill(2) + ":" + str(m).zfill(2) + ":" + str(s).zfill(2) reply = "No new msg " + str(h).zfill(2) + ":" + str(m).zfill(2) + ":" + str(s).zfill(2)
send_message(fromcall, reply) send_message(fromcall, reply)
server.logout() server.logout()
### end resend_email() ### end resend_email()
def check_email_thread(): def check_email_thread():
# print "Email thread disabled."
# return
# print "Email thread disabled." LOG.debug("Starting Email thread")
# return threading.Timer(55, check_email_thread).start() # how do we skip first run?
threading.Timer(55, check_email_thread).start() # how do we skip first run? shortcuts = CONFIG['shortcuts']
shortcuts_inverted = dict([[v,k] for k,v in shortcuts.items()]) # swap key/value
global shortcuts date = datetime.datetime.now()
shortcuts_inverted = dict([[v,k] for k,v in shortcuts.items()]) # swap key/value month = date.strftime("%B")[:3] # Nov, Mar, Apr
day = date.day
year = date.year
today = str(day) + "-" + month + "-" + str(year)
date = datetime.datetime.now() LOG.debug("Connect to IMAP host '%s' with user '%s'" %
month = date.strftime("%B")[:3] # Nov, Mar, Apr (CONFIG['imap']['host'],
day = date.day CONFIG['imap']['login']))
year = date.year
today = str(day) + "-" + month + "-" + str(year)
server = IMAPClient('imap.yourdomain.com', use_uid=True) server = IMAPClient(CONFIG['imap']['host'], use_uid=True)
server.login('KM6XXX@yourdomain.org', 'yourpassword') server.login(CONFIG['imap']['login'], CONFIG['imap']['password'])
select_info = server.select_folder('INBOX') # select_info = server.select_folder('INBOX')
messages = server.search(['SINCE', today]) messages = server.search(['SINCE', today])
#print("%d messages received today" % len(messages)) LOG.debug("%d messages received today" % len(messages))
for msgid, data in server.fetch(messages, ['ENVELOPE']).items(): for msgid, data in server.fetch(messages, ['ENVELOPE']).items():
envelope = data[b'ENVELOPE'] envelope = data[b'ENVELOPE']
#print('ID:%d "%s" (%s)' % (msgid, envelope.subject.decode(), envelope.date )) LOG.debug('ID:%d "%s" (%s)' %
f = re.search('([[A-a][0-9]_-]+@[[A-a][0-9]_-\.]+)', str(envelope.from_[0]) ) (msgid, envelope.subject.decode(), envelope.date))
if f is not None: f = re.search('([[A-a][0-9]_-]+@[[A-a][0-9]_-\.]+)',
from_addr = f.group(1) str(envelope.from_[0]) )
else: if f is not None:
from_addr = "noaddr" from_addr = f.group(1)
else:
from_addr = "noaddr"
if "APRS" not in server.get_flags(msgid)[msgid]: #if msg not flagged as sent via aprs if "APRS" not in server.get_flags(msgid)[msgid]: #if msg not flagged as sent via aprs
m = server.fetch([msgid], ['RFC822']) m = server.fetch([msgid], ['RFC822'])
(body, from_addr) = parse_email(msgid, data, server) (body, from_addr) = parse_email(msgid, data, server)
server.remove_flags(msgid, [SEEN]) # unset seen flag, will stay bold in email client server.remove_flags(msgid, [SEEN]) # unset seen flag, will stay bold in email client
if from_addr in shortcuts_inverted: # reverse lookup of a shortcut if from_addr in shortcuts_inverted: # reverse lookup of a shortcut
from_addr = shortcuts_inverted[from_addr] from_addr = shortcuts_inverted[from_addr]
reply = "-" + from_addr + " " + body reply = "-" + from_addr + " " + body
#print "Sending message via aprs: " + reply #print "Sending message via aprs: " + reply
send_message(BASECALLSIGN, reply) #radio send_message(CONFIG['ham']['callsign'], reply) #radio
server.add_flags(msgid, ['APRS']) #flag message as sent via aprs server.add_flags(msgid, ['APRS']) #flag message as sent via aprs
server.remove_flags(msgid, [SEEN]) #unset seen flag, will stay bold in email client server.remove_flags(msgid, [SEEN]) #unset seen flag, will stay bold in email client
server.logout() server.logout()
### end check_email() ### end check_email()
def send_ack_thread(tocall, ack, retry_count): def send_ack_thread(tocall, ack, retry_count):
tocall = tocall.ljust(9) # pad to nine chars tocall = tocall.ljust(9) # pad to nine chars
line = USER + ">APRS::" + tocall + ":ack" + str(ack) + "\n" line = CONFIG['aprs']['login'] + ">APRS::" + tocall + ":ack" + str(ack) + "\n"
for i in range(retry_count, 0, -1): for i in range(retry_count, 0, -1):
print "Sending ack __________________ Tx(" + str(i) + ")" LOG.info("Sending ack __________________ Tx(" + str(i) + ")")
print "Raw : " + line, LOG.info("Raw : " + line)
print "To : " + tocall LOG.info("To : " + tocall)
print "Ack number : " + str(ack) LOG.info("Ack number : " + str(ack))
tn.write(line) tn.write(line)
time.sleep(31) # aprs duplicate detection is 30 secs? (21 only sends first, 28 skips middle) time.sleep(31) # aprs duplicate detection is 30 secs? (21 only sends first, 28 skips middle)
return() return()
@ -289,15 +273,17 @@ def send_ack(tocall, ack):
def send_message_thread(tocall, message, this_message_number, retry_count): def send_message_thread(tocall, message, this_message_number, retry_count):
global ack_dict global ack_dict
line = USER + ">APRS::" + tocall + ":" + message + "{" + str(this_message_number) + "\n" line = (CONFIG['aprs']['login'] + ">APRS::" + tocall + ":" + message +
"{" + str(this_message_number) + "\n")
for i in range(retry_count, 0, -1): for i in range(retry_count, 0, -1):
print "DEBUG: send_message_thread msg:ack combos are: " LOG.debug("DEBUG: send_message_thread msg:ack combos are: ")
pprint.pprint(ack_dict) LOG.debug(pprint.pformat(ack_dict))
if ack_dict[this_message_number] != 1: if ack_dict[this_message_number] != 1:
print "Sending message_______________ " + str(this_message_number) + "(Tx" + str(i) + ")" LOG.info("Sending message_______________ " +
print "Raw : " + line, str(this_message_number) + "(Tx" + str(i) + ")")
print "To : " + tocall LOG.info("Raw : " + line)
print "Message : " + message LOG.info("To : " + tocall)
LOG.info("Message : " + message)
tn.write(line) tn.write(line)
sleeptime = (retry_count - i + 1) * 31 # decaying repeats, 31 to 93 second intervals sleeptime = (retry_count - i + 1) * 31 # decaying repeats, 31 to 93 second intervals
time.sleep(sleeptime) time.sleep(sleeptime)
@ -308,242 +294,318 @@ def send_message_thread(tocall, message, this_message_number, retry_count):
def send_message(tocall, message): def send_message(tocall, message):
global message_number global message_number
global ack_dict global ack_dict
retry_count = 3 retry_count = 3
if message_number > 98: # global if message_number > 98: # global
message_number = 0 message_number = 0
message_number += 1 message_number += 1
if len(ack_dict) > 90: # empty ack dict if it's really big, could result in key error later if len(ack_dict) > 90: # empty ack dict if it's really big, could result in key error later
print "DEBUG: Length of ack dictionary is big at " + str(len(ack_dict)) + " clearing." LOG.debug("DEBUG: Length of ack dictionary is big at " + str(len(ack_dict)) + " clearing.")
ack_dict.clear() ack_dict.clear()
pprint.pprint(ack_dict) LOG.debug(pprint.pformat(ack_dict))
print "DEBUG: Cleared ack dictionary, ack_dict length is now " + str(len(ack_dict)) + "." LOG.debug("DEBUG: Cleared ack dictionary, ack_dict length is now " + str(len(ack_dict)) + ".")
ack_dict[message_number] = 0 # clear ack for this message number ack_dict[message_number] = 0 # clear ack for this message number
tocall = tocall.ljust(9) # pad to nine chars tocall = tocall.ljust(9) # pad to nine chars
message = message[:67] # max? ftm400 displays 64, raw msg shows 74 message = message[:67] # max? ftm400 displays 64, raw msg shows 74
# and ftm400-send is max 64. setting this to # and ftm400-send is max 64. setting this to
# 67 displays 64 on the ftm400. (+3 {01 suffix) # 67 displays 64 on the ftm400. (+3 {01 suffix)
# feature req: break long ones into two msgs # feature req: break long ones into two msgs
thread = threading.Thread(target = send_message_thread, args = (tocall, message, message_number, retry_count)) thread = threading.Thread(
thread.start() target = send_message_thread,
return() args = (tocall, message, message_number, retry_count))
thread.start()
return()
### end send_message() ### end send_message()
def process_message(line): def process_message(line):
f = re.search('^(.*)>', line) f = re.search('^(.*)>', line)
fromcall = f.group(1) fromcall = f.group(1)
searchstring = '::' + USER + '[ ]*:(.*)' # verify this, callsign is padded out with spaces to colon searchstring = '::' + CONFIG['aprs']['login'] + '[ ]*:(.*)' # verify this, callsign is padded out with spaces to colon
m = re.search(searchstring, line) m = re.search(searchstring, line)
fullmessage = m.group(1) fullmessage = m.group(1)
ack_attached = re.search('(.*){([0-9A-Z]+)', fullmessage) # ack formats include: {1, {AB}, {12 ack_attached = re.search('(.*){([0-9A-Z]+)', fullmessage) # ack formats include: {1, {AB}, {12
if ack_attached: # "{##" suffix means radio wants an ack back if ack_attached: # "{##" suffix means radio wants an ack back
message = ack_attached.group(1) # message content message = ack_attached.group(1) # message content
ack_num = ack_attached.group(2) # suffix number to use in ack ack_num = ack_attached.group(2) # suffix number to use in ack
else: else:
message = fullmessage message = fullmessage
ack_num = "0" # ack not requested, but lets send one as 0 ack_num = "0" # ack not requested, but lets send one as 0
print "Received message______________" LOG.info("Received message______________")
print "Raw : " + line LOG.info("Raw : " + line)
print "From : " + fromcall LOG.info("From : " + fromcall)
print "Message : " + message LOG.info("Message : " + message)
print "Msg number : " + str(ack_num) LOG.info("Msg number : " + str(ack_num))
return (fromcall, message, ack_num) return (fromcall, message, ack_num)
### end process_message() ### end process_message()
def send_email(to_addr, content): def send_email(to_addr, content):
print "Sending Email_________________" LOG.info("Sending Email_________________")
global shortcuts shortcuts = CONFIG['shortcuts']
if to_addr in shortcuts: if to_addr in shortcuts:
print "To : " + to_addr , LOG.info("To : " + to_addr)
to_addr = shortcuts[to_addr] to_addr = shortcuts[to_addr]
print " (" + to_addr + ")" LOG.info(" (" + to_addr + ")")
subject = BASECALLSIGN subject = CONFIG['ham']['callsign']
#content = content + "\n\n(NOTE: reply with one line)" # content = content + "\n\n(NOTE: reply with one line)"
print "Subject : " + subject LOG.info("Subject : " + subject)
print "Body : " + content LOG.info("Body : " + content)
msg = MIMEText(content) msg = MIMEText(content)
msg['Subject'] = subject msg['Subject'] = subject
msg['From'] = "KM6XXX@yourdomain.org" msg['From'] = "KM6XXX@yourdomain.org"
msg['To'] = to_addr msg['To'] = to_addr
s = smtplib.SMTP_SSL('smtp.yourdomain.com', 465) s = smtplib.SMTP_SSL('smtp.yourdomain.com', 465)
s.login("KM6XXX@yourdomain.org", "yourpassword") s.login("KM6XXX@yourdomain.org", "yourpassword")
try: try:
s.sendmail("KM6XXX@yourdomain.org", [to_addr], msg.as_string()) s.sendmail("KM6XXX@yourdomain.org", [to_addr], msg.as_string())
except Exception, e: except Exception:
print "Sendmail Error!!!!!!!!!" LOG.exception("Sendmail Error!!!!!!!!!")
s.quit() s.quit()
return(-1) return(-1)
s.quit() s.quit()
return(0) return(0)
### end send_email ### end send_email
# Setup the logging faciility
# to disable logging to stdout, but still log to file
# use the --quiet option on the cmdln
def setup_logging(args):
global LOG
levels = {
'CRITICAL': logging.CRITICAL,
'ERROR': logging.ERROR,
'WARNING': logging.WARNING,
'INFO': logging.INFO,
'DEBUG': logging.DEBUG}
log_level = levels[args.loglevel]
LOG.setLevel(log_level)
log_format = ("%(asctime)s [%(threadName)-12.12s] [%(levelname)-5.5s]"
" %(message)s")
date_format = '%m/%d/%Y %I:%M:%S %p'
log_formatter = logging.Formatter(fmt=log_format,
datefmt=date_format)
fh = RotatingFileHandler('aprsd.log',
maxBytes=(10248576*5),
backupCount=4)
fh.setFormatter(log_formatter)
LOG.addHandler(fh)
if not args.quiet:
sh = logging.StreamHandler(sys.stdout)
sh.setFormatter(log_formatter)
LOG.addHandler(sh)
# This method tries to parse the config yaml file
# and consume the settings.
# If the required params don't exist,
# it will look in the environment
def parse_config(args):
# for now we still use globals....ugh
global CONFIG, LOG
def fail(msg):
LOG.critical(msg)
sys.exit(-1)
def check_option(config, section, name=None):
if section in config:
if name and name not in config[section]:
fail("'%s' was not in '%s' section of config file" %
(name, section))
else:
fail("'%s' section wasn't in config file" % section)
# Now read the ~/.aprds/config.yml
config = utils.get_config()
check_option(config, 'shortcuts')
check_option(config, 'ham', 'callsign')
check_option(config, 'aprs', 'login')
check_option(config, 'aprs', 'password')
check_option(config, 'aprs', 'host')
check_option(config, 'imap', 'host')
check_option(config, 'imap', 'login')
check_option(config, 'imap', 'password')
CONFIG = config
LOG.info("aprsd config loaded")
### main() ### ### main() ###
def main(): def main(args=args):
setup_logging(args)
time.sleep(2) LOG.info("APRSD Started")
parse_config(args)
LOG.debug("Signal handler setup")
signal.signal(signal.SIGINT, signal_handler)
tn.write("user " + USER + " pass " + PASS + " vers aprsd 0.99\n" ) time.sleep(2)
setup_connection()
time.sleep(2) user = CONFIG['aprs']['login']
password = CONFIG['aprs']['password']
LOG.info("LOGIN to APRSD with user '%s'" % user)
tn.write("user " + user + " pass " + password + " vers aprsd 0.99\n" )
time.sleep(2)
check_email_thread() # start email reader thread check_email_thread() # start email reader thread
while True: while True:
line = "" line = ""
try: try:
for char in tn.read_until("\n",100): for char in tn.read_until("\n",100):
line = line + char line = line + char
line = line.replace('\n', '') line = line.replace('\n', '')
print line LOG.info(line)
searchstring = '::' + USER searchstring = '::' + user
if re.search(searchstring, line): # is aprs message to us, not beacon, status, etc # is aprs message to us, not beacon, status, etc
(fromcall, message, ack) = process_message(line) if re.search(searchstring, line):
else: (fromcall, message, ack) = process_message(line)
message = "noise" else:
continue message = "noise"
continue
# ACK (ack##) # ACK (ack##)
if re.search('^ack[0-9]+', message): if re.search('^ack[0-9]+', message):
a = re.search('^ack([0-9]+)', message) # put message_number:1 in dict to record the ack # put message_number:1 in dict to record the ack
ack_dict.update({int(a.group(1)):1}) a = re.search('^ack([0-9]+)', message)
continue ack_dict.update({int(a.group(1)):1})
continue
# EMAIL (-) # EMAIL (-)
elif re.search('^-.*', message): # is email command elif re.search('^-.*', message): # is email command
searchstring = '^' + BASECALLSIGN + '.*' searchstring = '^' + CONFIG['ham']['callsign'] + '.*'
if re.search(searchstring, fromcall): # only I can do email if re.search(searchstring, fromcall): # only I can do email
r = re.search('^-([0-9])[0-9]*$', message) # digits only, first one is number of emails to resend r = re.search('^-([0-9])[0-9]*$', message) # digits only, first one is number of emails to resend
if r is not None: if r is not None:
resend_email(r.group(1)) resend_email(r.group(1))
elif re.search('^-([A-Za-z0-9_\-\.@]+) (.*)', message): # -user@address.com body of email elif re.search('^-([A-Za-z0-9_\-\.@]+) (.*)', message): # -user@address.com body of email
a = re.search('^-([A-Za-z0-9_\-\.@]+) (.*)', message) # (same search again) a = re.search('^-([A-Za-z0-9_\-\.@]+) (.*)', message) # (same search again)
if a is not None: if a is not None:
to_addr = a.group(1) to_addr = a.group(1)
content = a.group(2) content = a.group(2)
if content == 'mapme': # send recipient link to aprs.fi map if content == 'mapme': # send recipient link to aprs.fi map
content = "Click for my location: http://aprs.fi/" + BASECALLSIGN content = "Click for my location: http://aprs.fi/" + CONFIG['ham']['callsign']
too_soon = 0 too_soon = 0
now = time.time() now = time.time()
if ack in email_sent_dict: # see if we sent this msg number recently if ack in email_sent_dict: # see if we sent this msg number recently
timedelta = now - email_sent_dict[ack] timedelta = now - email_sent_dict[ack]
if ( timedelta < 300 ): # five minutes if ( timedelta < 300 ): # five minutes
too_soon = 1 too_soon = 1
if not too_soon or ack == 0: if not too_soon or ack == 0:
send_result = send_email(to_addr, content) send_result = send_email(to_addr, content)
if send_result != 0: if send_result != 0:
send_message(fromcall, "-" + to_addr + " failed") send_message(fromcall, "-" + to_addr + " failed")
else: else:
#send_message(fromcall, "-" + to_addr + " sent") #send_message(fromcall, "-" + to_addr + " sent")
if len(email_sent_dict) > 98: # clear email sent dictionary if somehow goes over 100 if len(email_sent_dict) > 98: # clear email sent dictionary if somehow goes over 100
print "DEBUG: email_sent_dict is big (" + str(len(email_sent_dict)) + ") clearing out." LOG.debug("DEBUG: email_sent_dict is big (" + str(len(email_sent_dict)) + ") clearing out.")
email_sent_dict.clear() email_sent_dict.clear()
email_sent_dict[ack] = now email_sent_dict[ack] = now
else:
LOG.info("Email for message number " + ack + " recently sent, not sending again.")
else: else:
print "\nEmail for message number " + ack + " recently sent, not sending again.\n" send_message(fromcall, "Bad email address")
else:
send_message(fromcall, "Bad email address")
# TIME (t) # TIME (t)
elif re.search('^t', message): elif re.search('^t', message):
stm = time.localtime() stm = time.localtime()
h = stm.tm_hour h = stm.tm_hour
m = stm.tm_min m = stm.tm_min
cur_time = fuzzy(h, m, 1) cur_time = fuzzy(h, m, 1)
reply = cur_time + " (" + str(h) + ":" + str(m).rjust(2, '0') + "PDT)" + " (" + message.rstrip() + ")" reply = cur_time + " (" + str(h) + ":" + str(m).rjust(2, '0') + "PDT)" + " (" + message.rstrip() + ")"
thread = threading.Thread(target = send_message, args = (fromcall, reply)) thread = threading.Thread(target = send_message, args = (fromcall, reply))
thread.start() thread.start()
# FORTUNE (f) # FORTUNE (f)
elif re.search('^f', message): elif re.search('^f', message):
process = subprocess.Popen(['/usr/games/fortune', '-s', '-n 60'], stdout=subprocess.PIPE) process = subprocess.Popen(['/usr/games/fortune', '-s', '-n 60'], stdout=subprocess.PIPE)
reply = process.communicate()[0] reply = process.communicate()[0]
send_message(fromcall, reply.rstrip()) send_message(fromcall, reply.rstrip())
# PING (p) # PING (p)
elif re.search('^p', message): elif re.search('^p', message):
stm = time.localtime() stm = time.localtime()
h = stm.tm_hour h = stm.tm_hour
m = stm.tm_min m = stm.tm_min
s = stm.tm_sec s = stm.tm_sec
reply = "Pong! " + str(h).zfill(2) + ":" + str(m).zfill(2) + ":" + str(s).zfill(2) reply = "Pong! " + str(h).zfill(2) + ":" + str(m).zfill(2) + ":" + str(s).zfill(2)
send_message(fromcall, reply.rstrip()) send_message(fromcall, reply.rstrip())
# LOCATION (l) "8 Miles E Auburn CA 1771' 38.91547,-120.99500 0.1h ago" # LOCATION (l) "8 Miles E Auburn CA 1771' 38.91547,-120.99500 0.1h ago"
elif re.search('^l', message): elif re.search('^l', message):
# get my last location, get descriptive name from weather service # get my last location, get descriptive name from weather service
try: try:
url = "http://api.aprs.fi/api/get?name=" + fromcall + "&what=loc&apikey=104070.f9lE8qg34L8MZF&format=json" url = "http://api.aprs.fi/api/get?name=" + fromcall + "&what=loc&apikey=104070.f9lE8qg34L8MZF&format=json"
response = urllib.urlopen(url) response = urllib.urlopen(url)
aprs_data = json.loads(response.read()) aprs_data = json.loads(response.read())
lat = aprs_data['entries'][0]['lat'] lat = aprs_data['entries'][0]['lat']
lon = aprs_data['entries'][0]['lng'] lon = aprs_data['entries'][0]['lng']
try: # altitude not always provided try: # altitude not always provided
alt = aprs_data['entries'][0]['altitude'] alt = aprs_data['entries'][0]['altitude']
except: except:
alt = 0 alt = 0
altfeet = int(alt * 3.28084) altfeet = int(alt * 3.28084)
aprs_lasttime_seconds = aprs_data['entries'][0]['lasttime'] aprs_lasttime_seconds = aprs_data['entries'][0]['lasttime']
aprs_lasttime_seconds = aprs_lasttime_seconds.encode('ascii',errors='ignore') #unicode to ascii aprs_lasttime_seconds = aprs_lasttime_seconds.encode('ascii',errors='ignore') #unicode to ascii
delta_seconds = time.time() - int(aprs_lasttime_seconds) delta_seconds = time.time() - int(aprs_lasttime_seconds)
delta_hours = delta_seconds / 60 / 60 delta_hours = delta_seconds / 60 / 60
url2 = "https://forecast.weather.gov/MapClick.php?lat=" + str(lat) + "&lon=" + str(lon) + "&FcstType=json" url2 = "https://forecast.weather.gov/MapClick.php?lat=" + str(lat) + "&lon=" + str(lon) + "&FcstType=json"
response2 = urllib.urlopen(url2) response2 = urllib.urlopen(url2)
wx_data = json.loads(response2.read()) wx_data = json.loads(response2.read())
reply = wx_data['location']['areaDescription'] + " " + str(altfeet) + "' " + str(lat) + "," + str(lon) + " " + str("%.1f" % round(delta_hours,1)) + "h ago" reply = wx_data['location']['areaDescription'] + " " + str(altfeet) + "' " + str(lat) + "," + str(lon) + " " + str("%.1f" % round(delta_hours,1)) + "h ago"
reply = reply.encode('ascii',errors='ignore') # unicode to ascii reply = reply.encode('ascii',errors='ignore') # unicode to ascii
send_message(fromcall, reply.rstrip()) send_message(fromcall, reply.rstrip())
except: except:
reply = "Unable to find you (send beacon?)" reply = "Unable to find you (send beacon?)"
send_message(fromcall, reply.rstrip()) send_message(fromcall, reply.rstrip())
# WEATHER (w) "42F(68F/48F) Haze. Tonight, Haze then Chance Rain." # WEATHER (w) "42F(68F/48F) Haze. Tonight, Haze then Chance Rain."
elif re.search('^w', message): elif re.search('^w', message):
# get my last location from aprsis then get weather from weather service # get my last location from aprsis then get weather from weather service
try: try:
url = "http://api.aprs.fi/api/get?name=" + fromcall + "&what=loc&apikey=104070.f9lE8qg34L8MZF&format=json" url = "http://api.aprs.fi/api/get?name=" + fromcall + "&what=loc&apikey=104070.f9lE8qg34L8MZF&format=json"
response = urllib.urlopen(url) response = urllib.urlopen(url)
aprs_data = json.loads(response.read()) aprs_data = json.loads(response.read())
lat = aprs_data['entries'][0]['lat'] lat = aprs_data['entries'][0]['lat']
lon = aprs_data['entries'][0]['lng'] lon = aprs_data['entries'][0]['lng']
url2 = "https://forecast.weather.gov/MapClick.php?lat=" + str(lat) + "&lon=" + str(lon) + "&FcstType=json" url2 = "https://forecast.weather.gov/MapClick.php?lat=" + str(lat) + "&lon=" + str(lon) + "&FcstType=json"
response2 = urllib.urlopen(url2) response2 = urllib.urlopen(url2)
wx_data = json.loads(response2.read()) wx_data = json.loads(response2.read())
reply = wx_data['currentobservation']['Temp'] + "F(" + wx_data['data']['temperature'][0] + "F/" + wx_data['data']['temperature'][1] + "F) " + wx_data['data']['weather'][0] + ". " + wx_data['time']['startPeriodName'][1] + ", " + wx_data['data']['weather'][1] + "." reply = wx_data['currentobservation']['Temp'] + "F(" + wx_data['data']['temperature'][0] + "F/" + wx_data['data']['temperature'][1] + "F) " + wx_data['data']['weather'][0] + ". " + wx_data['time']['startPeriodName'][1] + ", " + wx_data['data']['weather'][1] + "."
reply = reply.encode('ascii',errors='ignore') # unicode to ascii reply = reply.encode('ascii',errors='ignore') # unicode to ascii
send_message(fromcall, reply.rstrip()) send_message(fromcall, reply.rstrip())
except: except:
reply = "Unable to find you (send beacon?)" reply = "Unable to find you (send beacon?)"
send_message(fromcall, reply) send_message(fromcall, reply)
# USAGE # USAGE
else: else:
reply = "usage: time, fortune, loc, weath, -emailaddr emailbody, -#(resend)" reply = "usage: time, fortune, loc, weath, -emailaddr emailbody, -#(resend)"
send_message(fromcall, reply) send_message(fromcall, reply)
time.sleep(1) # let any threads do their thing, then ack time.sleep(1) # let any threads do their thing, then ack
send_ack(fromcall, ack) # send an ack last send_ack(fromcall, ack) # send an ack last
except Exception, e: except Exception, e:
print "Error in mainline loop:" LOG.error("Error in mainline loop:")
print "%s" % str(e) LOG.error("%s" % str(e))
print "Exiting." LOG.error("Exiting.")
#sys.exit(1) # merely a suggestion #sys.exit(1) # merely a suggestion
os._exit(1) os._exit(1)
# end while True # end while True
tn.close() tn.close()
exit() exit()
if __name__ == "__main__": if __name__ == "__main__":
main() main(args)

View File

@ -1,12 +1,20 @@
"""Utilities and helper functions.""" """Utilities and helper functions."""
import logging
import os import os
import pprint
import sys import sys
import yaml import yaml
# an example of what should be in the ~/.aprsd/config.yml # an example of what should be in the ~/.aprsd/config.yml
example_config = ''' example_config = '''
ham:
callsign: KFART
aprs:
login: someusername
password: password
host: noam.aprs2.net
shortcuts: shortcuts:
'aa': '5551239999@vtext.com' 'aa': '5551239999@vtext.com'
'cl': 'craiglamparter@somedomain.org' 'cl': 'craiglamparter@somedomain.org'
@ -19,12 +27,10 @@ smtp:
imap: imap:
login: imapuser login: imapuser
password: something dumb password: something dumb
ham:
callsign: something
basename: somebasename
''' '''
log = logging.getLogger('APRSD')
def env(*vars, **kwargs): def env(*vars, **kwargs):
"""This returns the first environment variable set. """This returns the first environment variable set.
if none are non-empty, defaults to '' or keyword arg default if none are non-empty, defaults to '' or keyword arg default
@ -44,6 +50,6 @@ def get_config():
config = yaml.load(stream) config = yaml.load(stream)
return config return config
else: else:
print("%s is missing, please create a config file" % config_file) log.critical("%s is missing, please create a config file" % config_file)
print("example config is\n %s" % example_config) print("\nCopy to ~/.aprsd/config.yml and edit\n\nSample config:\n %s" % example_config)
sys.exit(-1) sys.exit(-1)