require "socket" require "json" module Maptool # A minimal client for gpsd's JSON protocol (https://gpsd.io/gpsd_json.html). # Connects over TCP, asks gpsd to watch for reports, and yields a plain # position hash for every TPV ("time-position-velocity") report — gpsd's # term for a GNSS fix — for as long as the block keeps running. class GpsdClient RECONNECT_DELAY = 5 # seconds, after gpsd is unreachable or drops us def initialize(host: "127.0.0.1", port: 2947) @host = host @port = port end # Never returns under normal operation: reconnects on any I/O error # (gpsd not running yet, connection dropped, etc.) after a short delay, # so a missing/disconnected receiver is not fatal to the caller. def each_position loop do watch { |position| yield position } rescue SystemCallError, EOFError, SocketError => e warn "[gpsd] #{e.class}: #{e.message} — retrying in #{RECONNECT_DELAY}s" sleep RECONNECT_DELAY end end private def watch socket = TCPSocket.new(@host, @port) socket.puts('?WATCH={"enable":true,"json":true};') socket.each_line do |line| report = JSON.parse(line) next unless report["class"] == "TPV" next unless report["lat"] && report["lon"] yield( lat: report["lat"], lon: report["lon"], # altHAE (height above ellipsoid) is gpsd's modern field; older # gpsd versions only send "alt". alt: report["altHAE"] || report["alt"], speed: report["speed"], track: report["track"], mode: report["mode"], # 0/1 = no fix, 2 = 2D, 3 = 3D eph: report["eph"], # estimated horizontal position error, metres time: report["time"], ) end ensure socket&.close end end end