EC25-AF GNSS wiring (udev hotplug into gpsd) plus van-gps-owntracks, a port of the wayback-era gps_to_owntracks.py: apt-only deps (python3-gps instead of pip-only gpsdclient, paho 2.x callback API), broker secrets moved out of the code into /etc/van-gps/config.json (0600, seeded from a sanitized example), gpsd host now localhost. client_id is vanq-wan — a legacy client still holds vanq on the broker and shared IDs get kicked in a connect/disconnect loop. Exit-on-disconnect + systemd Restart=always is the reconnect logic. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
162 lines
5.0 KiB
Python
162 lines
5.0 KiB
Python
#!/usr/bin/python3 -u
|
|
# Publish gpsd fixes as OwnTracks location messages over MQTT.
|
|
# Ported from gps_to_owntracks.py (wayback era): gpsdclient -> python3-gps
|
|
# (apt-only deps), paho 1.x -> 2.x callback API, hard-coded broker secrets ->
|
|
# /etc/van-gps/config.json. Exits on MQTT disconnect/DNS failure by design —
|
|
# systemd Restart=always reconnects with a fresh session.
|
|
|
|
import sys
|
|
import json
|
|
import signal
|
|
import os
|
|
import time
|
|
import socket
|
|
|
|
import gps as gpsd
|
|
import paho.mqtt.client as mqtt
|
|
from paho.mqtt.properties import Properties
|
|
from paho.mqtt.packettypes import PacketTypes
|
|
|
|
from datetime import datetime
|
|
from math import radians, sin, cos, acos
|
|
|
|
debug = False # toggle at runtime with SIGHUP
|
|
config_path = sys.argv[1] if len(sys.argv) > 1 else "/etc/van-gps/config.json"
|
|
with open(config_path) as f:
|
|
cfg = json.load(f)
|
|
|
|
version = cfg.get("mqtt_version", "3") # '3' or '5'
|
|
mytransport = "tcp" # or 'websockets'
|
|
broker = cfg["broker"]
|
|
myport = cfg.get("port", 1883)
|
|
mq_user = cfg["username"]
|
|
mq_pw = cfg["password"]
|
|
mq_id = cfg.get("client_id", "vanq-wan")
|
|
gpsd_host = cfg.get("gpsd_host", "127.0.0.1")
|
|
idle_delay = cfg.get("idle_delay_s", 600) # seconds between messages when not moving
|
|
keep_alive = idle_delay + 60
|
|
distance_move_trigger = cfg.get("move_trigger_m", 250) # meters of movement that triggers a message
|
|
|
|
mytopic = cfg.get("topic", "owntracks/rv/gps")
|
|
tid = cfg.get("tid", "rv")
|
|
|
|
|
|
def handler(signum, frame):
|
|
global debug
|
|
debug = not debug
|
|
print('debug is now', debug)
|
|
|
|
|
|
def great_circle(llon1, llat1, llon2, llat2):
|
|
lon1, lat1, lon2, lat2 = map(radians, [llon1, llat1, llon2, llat2])
|
|
|
|
a = sin(lat1) * sin(lat2) + cos(lat1) * cos(lat2) * cos(lon1 - lon2)
|
|
if a >= 1.0:
|
|
return 0
|
|
try:
|
|
res = int(6371000 * (acos(a)))
|
|
except ValueError as e:
|
|
print("gc error: ", e)
|
|
print(" coords: ", llon1, llat1, llon2, llat2)
|
|
return 0
|
|
return res
|
|
|
|
|
|
def on_disconnect(client, userdata, flags, reason_code, properties):
|
|
print("mqtt disconnect:", reason_code)
|
|
os._exit(1)
|
|
|
|
|
|
def connect():
|
|
if version == '5':
|
|
properties = Properties(PacketTypes.CONNECT)
|
|
properties.SessionExpiryInterval = 30 * 60 # in seconds
|
|
|
|
client.connect(broker,
|
|
port=myport,
|
|
clean_start=mqtt.MQTT_CLEAN_START_FIRST_ONLY,
|
|
properties=properties,
|
|
keepalive=keep_alive)
|
|
elif version == '3':
|
|
client.connect(broker, port=myport, keepalive=keep_alive)
|
|
print("mqtt connect")
|
|
|
|
|
|
#
|
|
# Main
|
|
#
|
|
|
|
print("van-gps-owntracks start", time.asctime(), debug, os.getpid())
|
|
|
|
signal.signal(signal.SIGHUP, handler)
|
|
|
|
if version == '5':
|
|
client = mqtt.Client(mqtt.CallbackAPIVersion.VERSION2,
|
|
client_id=mq_id,
|
|
transport=mytransport,
|
|
protocol=mqtt.MQTTv5)
|
|
if version == '3':
|
|
client = mqtt.Client(mqtt.CallbackAPIVersion.VERSION2,
|
|
client_id=mq_id,
|
|
transport=mytransport,
|
|
protocol=mqtt.MQTTv311,
|
|
clean_session=True)
|
|
|
|
client.username_pw_set(mq_user, mq_pw)
|
|
client.on_disconnect = on_disconnect
|
|
|
|
try:
|
|
connect()
|
|
except socket.gaierror as e:
|
|
print("could not connect mqtt, %s" % e)
|
|
sys.exit(1)
|
|
client.loop_start()
|
|
|
|
pubproperties = Properties(PacketTypes.PUBLISH)
|
|
pubproperties.MessageExpiryInterval = keep_alive # in seconds; MQTT5 only
|
|
|
|
session = gpsd.gps(host=gpsd_host)
|
|
session.stream(gpsd.WATCH_ENABLE)
|
|
lastmsg = {'tst': 0, 'tid': tid, 'lat': 0, 'lon': 0}
|
|
while session.read() == 0:
|
|
result = session.data
|
|
if not result or result.get('class') != 'TPV':
|
|
continue
|
|
msg = {'_type': 'location', 'tid': tid, 't': 'p'}
|
|
|
|
if result.get('mode', 0) <= 1:
|
|
continue
|
|
if not ('lat' in result and 'lon' in result):
|
|
continue
|
|
msg['lat'] = result['lat']
|
|
msg['lon'] = result['lon']
|
|
if 'alt' in result:
|
|
msg['alt'] = int(result['alt'])
|
|
if 'speed' in result:
|
|
msg['vel'] = int(result['speed']) * 3.6
|
|
if 'eph' in result:
|
|
msg['acc'] = int(result['eph'])
|
|
if 'track' in result:
|
|
msg['cog'] = int(result['track'])
|
|
else:
|
|
continue
|
|
if 'time' not in result:
|
|
continue
|
|
msg['tst'] = int(datetime.fromisoformat(result['time']).timestamp())
|
|
jmsg = json.dumps(msg)
|
|
if lastmsg != msg:
|
|
gc = great_circle(lastmsg['lat'], lastmsg['lon'], msg['lat'], msg['lon'])
|
|
if debug:
|
|
print("\rgc is ", gc, ' age is ', msg['tst'] - lastmsg['tst'], " ", end="")
|
|
if msg['tst'] - lastmsg['tst'] >= idle_delay or gc > distance_move_trigger:
|
|
if debug:
|
|
print('* ', gc, jmsg)
|
|
print('* ', mytopic, pubproperties)
|
|
rc = client.publish(mytopic, jmsg, 0, properties=pubproperties)
|
|
if debug:
|
|
print("* client.publish rc=", rc)
|
|
lastmsg = msg
|
|
|
|
print("gpsd stream ended", file=sys.stderr)
|
|
sys.exit(1)
|