First pass

This commit is contained in:
mloebl 2018-03-18 17:20:00 -04:00 committed by GitHub
parent fa05a668e0
commit ec06d78bb6
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
5 changed files with 591 additions and 0 deletions

25
README.md Normal file
View File

@ -0,0 +1,25 @@
# mqtt-aprs
Connects to the specified APRS-IS server, and posts the APRS output to MQTT. Can parse parameters, or dump the raw JSON.
This script uses the aprslib python function to do the heavy APRS lifting.
INSTALL
=================
```
sudo apt-get install git python-pip
sudo pip install setuptools
sudo pip install setproctitle
sudo pip install paho-mqtt
sudo pip install aprslib
mkdir /etc/mqtt-aprs/
git clone git://github.com/mloebl/mqtt-mqtt-aprs.git /usr/local/mqtt-aprs/
cp /usr/local/mqtt-aprs/mqtt-aprs.cfg.example /etc/mqtt-aprs/mqtt-aprs.cfg
cp /usr/local/mqtt-aprs/mqtt-aprs.init /etc/init.d/mqtt-aprs
pdate-rc.d mqtt-aprs defaults
cp /usr/local/mqtt-aprsp/mqtt-aprs.default /etc/default/mqtt-aprs
```
Edit /etc/default/mqtt-aprs and /etc/mqtt-aprs/mqtt-aprs.cfg to suit:
`/etc/init.d/mqtt-aprs start`

18
mqtt-aprs.cfg.example Normal file
View File

@ -0,0 +1,18 @@
[global]
DEBUG = False
LOGFILE = /var/log/mqtt-aprs.log
MQTT_HOST = 10.0.0.1
MQTT_PORT = 1883
MQTT_TOPIC = /#
MQTT_SUBTOPIC = aprs
MQTT_USERNAME =
MQTT_PASSWORD =
APRS_LATITUDE = 0.00000
APRS_LONGITUDE = 0.00000
APRS_CALLSIGN = N0CALL
APRS_PASSWORD = -1
APRS_HOST =
APRS_PORT =
APRS_FILTER =
APRS_PROCESS = True
METRICUNITS = 0

2
mqtt-aprs.default Normal file
View File

@ -0,0 +1,2 @@
START_DAEMON=true
VERBOSE=true

147
mqtt-aprs.init Normal file
View File

@ -0,0 +1,147 @@
#!/bin/sh
### BEGIN INIT INFO
# Provides: mqtt-aprs
# Required-Start: $remote_fs $syslog $network
# Should-Start: bluetooth dbus udev
# Required-Stop: $remote_fs $syslog $network
# Default-Start: 2 3 4 5
# Default-Stop: 0 1 6
# Short-Description: mqtt-aprs daemon start/stop script
# Description: Start/Stop script for the mqtt-aprs daemon
### END INIT INFO
# Author: Kyle Gordon <kyle@lodge.glasgownet.com
# Modified by Mike Loebl for custom usage
# Do NOT "set -e"
# PATH should only include /usr/* if it runs after the mountnfs.sh script
PATH=/sbin:/usr/sbin:/bin:/usr/bin
DESC="mqtt-aprs daemon"
NAME=mqtt-aprs
DAEMON=/usr/local/$NAME/$NAME.py
PIDFILE=/var/run/$NAME.pid
SCRIPTNAME=/etc/init.d/$NAME
# Exit if the package is not installed
[ -x "$DAEMON" ] || exit 0
# Read configuration variable file if it is present
[ -r /etc/default/$NAME ] && . /etc/default/$NAME
# Load the VERBOSE setting and other rcS variables
. /lib/init/vars.sh
# Define LSB log_* functions.
# Depend on lsb-base (>= 3.0-6) to ensure that this file is present.
. /lib/lsb/init-functions
#
# Function that starts the daemon/service
#
do_start()
{
# Return
# 0 if daemon has been started
# 1 if daemon was already running
# 2 if daemon could not be started
echo "Starting mqtt-aprs"
start-stop-daemon -b --start --quiet -m --pidfile $PIDFILE --exec $DAEMON --test > /dev/null \
|| return 1
start-stop-daemon -b --start --quiet -m --pidfile $PIDFILE --exec $DAEMON -- \
$OPTIONS \
|| return 2
}
#
# Function that stops the daemon/service
#
do_stop()
{
# Return
# 0 if daemon has been stopped
# 1 if daemon was already stopped
# 2 if daemon could not be stopped
# other if a failure occurred
start-stop-daemon --stop --quiet --retry=TERM/30/KILL/5 --pidfile $PIDFILE
RETVAL="$?"
[ "$RETVAL" = 2 ] && return 2
# Many daemons don't delete their pidfiles when they exit.
rm -f $PIDFILE
return "$RETVAL"
}
#
# Function that sends a SIGHUP to the daemon/service
#
do_reload() {
#
# If the daemon can reload its configuration without
# restarting (for example, when it is sent a SIGHUP),
# then implement that here.
#
start-stop-daemon --stop --signal 1 --quiet --pidfile $PIDFILE --name $NAME
return 0
}
case "$1" in
start)
if [ "$START_DAEMON" = "true" ]; then
[ "$VERBOSE" != no ] && log_daemon_msg "Starting $DESC" "$NAME"
do_start
case "$?" in
0|1) [ "$VERBOSE" != no ] && log_end_msg 0 ;;
2) [ "$VERBOSE" != no ] && log_end_msg 1 ;;
esac
else
[ "$VERBOSE" != no ] && \
log_daemon_msg "Not starting $DESC" "$NAME" && \
log_end_msg 0
fi
;;
stop)
[ "$VERBOSE" != no ] && log_daemon_msg "Stopping $DESC" "$NAME"
do_stop
case "$?" in
0|1) [ "$VERBOSE" != no ] && log_end_msg 0 ;;
2) [ "$VERBOSE" != no ] && log_end_msg 1 ;;
esac
;;
status)
status_of_proc "$DAEMON" "$NAME" && exit 0 || exit $?
;;
reload|force-reload)
log_daemon_msg "Reloading $DESC" "$NAME"
do_reload
log_end_msg $?
;;
restart)
#
# If the "reload" option is implemented then remove the
# 'force-reload' alias
#
log_daemon_msg "Restarting $DESC" "$NAME"
do_stop
case "$?" in
0|1)
do_start
case "$?" in
0) log_end_msg 0 ;;
1) log_end_msg 1 ;; # Old process is still running
*) log_end_msg 1 ;; # Failed to start
esac
;;
*)
# Failed to stop
log_end_msg 1
;;
esac
;;
*)
echo "Usage: $SCRIPTNAME {start|stop|status|restart|force-reload}" >&2
exit 3
;;
esac
:

399
mqtt-aprs.py Normal file
View File

@ -0,0 +1,399 @@
#!/usr/bin/env python
# -*- coding: iso-8859-1 -*-
__author__ = "Mike Loebl"
__copyright__ = "Copyright (C) Mike Loebl"
# Script based on mqtt-owfs-temp written by Kyle Gordon and converted for use with APRS
# Source: https://github.com/kylegordon/mqtt-owfs-temp
import os
import logging
import signal
import socket
import time
import sys
import binascii
import paho.mqtt.client as paho
import ConfigParser
import setproctitle
import aprslib
from datetime import datetime, timedelta
# Read the config file
config = ConfigParser.RawConfigParser()
# TODO: Fix path:
config.read("./mqtt-aprs.cfg")
# Use ConfigParser to pick out the settings
DEBUG = config.getboolean("global", "debug")
LOGFILE = config.get("global", "logfile")
MQTT_HOST = config.get("global", "mqtt_host")
MQTT_PORT = config.getint("global", "mqtt_port")
MQTT_SUBTOPIC = config.get("global", "MQTT_SUBTOPIC")
MQTT_TOPIC = "/raw/" + socket.getfqdn() + "/" + MQTT_SUBTOPIC
MQTT_USERNAME = config.get("global", "MQTT_USERNAME")
MQTT_PASSWORD = config.get("global", "MQTT_PASSWORD")
METRICUNITS = config.get("global", "METRICUNITS")
APRS_CALLSIGN = config.get("global", "APRS_CALLSIGN")
APRS_PASSWORD = config.get("global", "APRS_PASSWORD")
APRS_HOST = config.get("global", "APRS_HOST")
APRS_PORT = config.get("global", "APRS_PORT")
APRS_FILTER = config.get("global", "APRS_FILTER")
APRS_PROCESS = config.get("global", "APRS_PROCESS")
APRS_LATITUDE = config.get("global", "APRS_LATITUDE")
APRS_LONGITUDE = config.get("global", "APRS_LONGITUDE")
APPNAME = MQTT_SUBTOPIC
PRESENCETOPIC = "clients/" + socket.getfqdn() + "/" + APPNAME + "/state"
setproctitle.setproctitle(APPNAME)
client_id = APPNAME + "_%d" % os.getpid()
mqttc = paho.Client()
LOGFORMAT = '%(asctime)-15s %(message)s'
if DEBUG:
logging.basicConfig(filename=LOGFILE,
level=logging.DEBUG,
format=LOGFORMAT)
else:
logging.basicConfig(filename=LOGFILE,
level=logging.INFO,
format=LOGFORMAT)
logging.info("Starting " + APPNAME)
logging.info("INFO MODE")
logging.debug("DEBUG MODE")
def celsiusCon(farenheit):
return (farenheit - 32)*(5/9)
def farenheitCon(celsius):
return ((celsius*(9/5)) + 32)
# All the MQTT callbacks start here
def on_publish(mosq, obj, mid):
"""
What to do when a message is published
"""
logging.debug("MID " + str(mid) + " published.")
def on_subscribe(mosq, obj, mid, qos_list):
"""
What to do in the event of subscribing to a topic"
"""
logging.debug("Subscribe with mid " + str(mid) + " received.")
def on_unsubscribe(mosq, obj, mid):
"""
What to do in the event of unsubscribing from a topic
"""
logging.debug("Unsubscribe with mid " + str(mid) + " received.")
def on_connect(self, mosq, obj, result_code):
"""
Handle connections (or failures) to the broker.
This is called after the client has received a CONNACK message
from the broker in response to calling connect().
The parameter rc is an integer giving the return code:
0: Success
1: Refused unacceptable protocol version
2: Refused identifier rejected
3: Refused server unavailable
4: Refused bad user name or password (MQTT v3.1 broker only)
5: Refused not authorised (MQTT v3.1 broker only)
"""
logging.debug("on_connect RC: " + str(result_code))
if result_code == 0:
logging.info("Connected to %s:%s", MQTT_HOST, MQTT_PORT)
# Publish retained LWT as per
# http://stackoverflow.com/q/97694
# See also the will_set function in connect() below
mqttc.publish(PRESENCETOPIC, "1", retain=True)
process_connection()
elif result_code == 1:
logging.info("Connection refused - unacceptable protocol version")
cleanup()
elif result_code == 2:
logging.info("Connection refused - identifier rejected")
cleanup()
elif result_code == 3:
logging.info("Connection refused - server unavailable")
logging.info("Retrying in 30 seconds")
time.sleep(30)
elif result_code == 4:
logging.info("Connection refused - bad user name or password")
cleanup()
elif result_code == 5:
logging.info("Connection refused - not authorised")
cleanup()
else:
logging.warning("Something went wrong. RC:" + str(result_code))
cleanup()
def on_disconnect(mosq, obj, result_code):
"""
Handle disconnections from the broker
"""
if result_code == 0:
logging.info("Clean disconnection")
else:
logging.info("Unexpected disconnection! Reconnecting in 5 seconds")
logging.debug("Result code: %s", result_code)
time.sleep(5)
def on_message(mosq, obj, msg):
"""
What to do when the client recieves a message from the broker
"""
logging.debug("Received: " + msg.payload +
" received on topic " + msg.topic +
" with QoS " + str(msg.qos))
process_message(msg)
def on_log(mosq, obj, level, string):
"""
What to do with debug log output from the MQTT library
"""
logging.debug(string)
# End of MQTT callbacks
def cleanup(signum, frame):
"""
Signal handler to ensure we disconnect cleanly
in the event of a SIGTERM or SIGINT.
"""
logging.info("Disconnecting from broker")
# Publish a retained message to state that this client is offline
mqttc.publish(PRESENCETOPIC, "0", retain=True)
mqttc.disconnect()
mqttc.loop_stop()
logging.info("Exiting on signal %d", signum)
sys.exit(signum)
def connect():
"""
Connect to the broker, define the callbacks, and subscribe
This will also set the Last Will and Testament (LWT)
The LWT will be published in the event of an unclean or
unexpected disconnection.
"""
logging.info("Connecting to %s:%s", MQTT_HOST, MQTT_PORT)
if MQTT_USERNAME:
logging.info("Found username %s", MQTT_USERNAME)
mqttc.username_pw_set(MQTT_USERNAME, MQTT_PASSWORD)
# Set the Last Will and Testament (LWT) *before* connecting
mqttc.will_set(PRESENCETOPIC, "0", qos=0, retain=True)
result = mqttc.connect(MQTT_HOST, MQTT_PORT, 60)
if result != 0:
logging.info("Connection failed with error code %s. Retrying", result)
time.sleep(10)
connect()
# Define the callbacks
mqttc.on_connect = on_connect
mqttc.on_disconnect = on_disconnect
mqttc.on_publish = on_publish
mqttc.on_subscribe = on_subscribe
mqttc.on_unsubscribe = on_unsubscribe
mqttc.on_message = on_message
if DEBUG:
mqttc.on_log = on_log
mqttc.loop_start()
def process_connection():
"""
What to do when a new connection is established
"""
logging.debug("Processing connection")
def process_message(mosq, obj, msg):
"""
What to do with the message that's arrived
"""
logging.debug("Received: %s", msg.topic)
def find_in_sublists(lst, value):
for sub_i, sublist in enumerate(lst):
try:
return (sub_i, sublist.index(value))
except ValueError:
pass
raise ValueError("%s is not in lists" % value)
def callback(packet):
logging.debug("Raw Packet: %s", packet)
if APRS_PROCESS == "True":
aprspacket = aprs._parse(packet)
ssid = aprspacket.get('from', None)
logging.debug("SSID: %s", ssid)
rawpacket = aprspacket.get('raw', None)
logging.debug("RAW: %s", rawpacket)
publish_aprstomqtt(ssid, "raw", rawpacket)
aprspath = aprspacket.get('path', None)
if aprspath:
logging.debug("path: %s", aprspath)
publish_aprstomqtt(ssid, "path",aprspath)
packet_format = aprspacket.get('format', None)
logging.debug("format: %s", packet_format)
publish_aprstomqtt(ssid, "format", packet_format)
symbol_table = aprspacket.get('symbol_table', None)
symbol = aprspacket.get('symbol', None)
if symbol_table and symbol:
icon = symbol_table + symbol
logging.debug("icon: %s", icon)
publish_aprstomqtt(ssid, "icon", icon)
aprs_lat = aprspacket.get('latitude', None)
aprs_lon = aprspacket.get('longitude', None)
if aprs_lat and aprs_lon:
aprs_lat = round(aprs_lat, 4)
aprs_lon = round(aprs_lon, 4)
logging.debug("latitude: %s", aprs_lat)
logging.debug("longitude: %s", aprs_lon)
distance = get_distance(aprs_lat, aprs_lon)
publish_aprstomqtt(ssid, "latitude", aprs_lat)
publish_aprstomqtt(ssid, "longitude",aprs_lon)
logging.debug("Distance away: %s", distance)
publish_aprstomqtt(ssid, "distance",distance)
altitude = aprspacket.get('altitude', None)
if altitude:
if METRICUNITS is "0":
altitude = altitude / 0.3048
logging.debug("altitude: %s", altitude)
publish_aprstomqtt(ssid, "altitude",altitude)
comment = aprspacket.get('comment', None)
if comment:
logging.debug("comment: %s", comment)
publish_aprstomqtt(ssid, "comment",comment)
telemetry = aprspacket.get('telemetry', None)
if telemetry:
logging.debug("telemetry: %s", telemetry)
publish_aprstomqtt(ssid, "telemetry",telemetry)
message = aprspacket.get('message_text', None)
if message:
logging.debug("message: %s", message)
publish_aprstomqtt(ssid, "message",message)
else:
publish_aprstomqtt_nossid(packet)
def publish_aprstomqtt(inssid, inname, invalue):
topic_path = MQTT_TOPIC + "/" + inssid + "/" + inname
logging.debug("Publishing topic: %s with value %s" % (topic_path, invalue))
mqttc.publish(topic_path, unicode(invalue).encode('utf-8').strip())
def publish_aprstomqtt_nossid(invalue):
topic_path = MQTT_TOPIC
logging.debug("Publishing topic: %s with value %s" % (topic_path, invalue))
mqttc.publish(topic_path, unicode(invalue).encode('utf-8').strip())
def get_distance(inlat, inlon):
if APRS_LATITUDE and APRS_LONGITUDE:
# From: https://stackoverflow.com/questions/19412462/getting-distance-between-two-points-based-on-latitude-longitude
# approximate radius of earth in km
R = 6373.0
from math import sin, cos, sqrt, atan2, radians
lat1 = radians(float(APRS_LATITUDE))
lon1 = radians(float(APRS_LONGITUDE))
lat2 = radians(float(inlat))
lon2 = radians(float(inlon))
dlon = lon2 - lon1
dlat = lat2 - lat1
a = sin(dlat / 2)**2 + cos(lat1) * cos(lat2) * sin(dlon / 2)**2
c = 2 * atan2(sqrt(a), sqrt(1 - a))
distance = R * c
if METRICUNITS is "0":
distance = distance * 0.621371
return round(distance, 2)
def aprs_connect():
# Listen for specific packet
aprs.set_filter(APRS_FILTER)
# Open the APRS connection to the server
aprs.connect(blocking=True)
logging.debug("APRS Processing: %s", APRS_PROCESS)
# Set a callback for when a packet is received
if APRS_PROCESS == "True":
aprs.consumer(callback, raw=True)
else:
aprs.consumer(callback)
# Use the signal module to handle signals
signal.signal(signal.SIGTERM, cleanup)
signal.signal(signal.SIGINT, cleanup)
try:
# Prepare the APRS connection
aprs = aprslib.IS(APRS_CALLSIGN,
passwd=APRS_PASSWORD,
host=APRS_HOST,
port=APRS_PORT,
skip_login=False)
# Connect to the broker and enter the main loop
connect()
aprs_connect()
except KeyboardInterrupt:
logging.info("Interrupted by keypress")
sys.exit(0)
except aprslib.exceptions.ConnectionDrop:
logging.info("Connection to APRS server dropped, trying again in 30 seconds...")
time.sleep(30)
aprs_connect
except aprslib.exceptions.ConnectionError:
logging.info("Connection to APRS server failed, trying again in 30 seconds...")
time.sleep(30)
aprs_connect