From 61ba86c6e6d43a4fecea175b294e113324069755 Mon Sep 17 00:00:00 2001 From: Roman Priesol Date: Tue, 8 Sep 2026 00:28:34 +0200 Subject: [PATCH] Add a GNSS simulator for dev without gpsd running bin/gnss_simulator.rb speaks gpsd's own JSON wire protocol directly on its default port -- GpsdClient connects to it exactly as it would to real gpsd, no app changes needed, and no gpsd install/daemon required either (unlike the existing fake-NMEA-source approach in docs/development.adoc, which still needs gpsd itself in front of it). Drives a loop around a center point at a driving-ish pace rather than sitting still, since a fixed position isn't much of a "GNSS position" to look at. Verified end to end: started it, pointed a dev server at it, and confirmed /api/gnss/position reports a moving fix across two polls. --- .gitea/workflows/ci.yml | 2 +- bin/gnss_simulator.rb | 72 +++++++++++++++++++++++++++++++++++++++++ docs/development.adoc | 25 ++++++++++++-- 3 files changed, 95 insertions(+), 4 deletions(-) create mode 100755 bin/gnss_simulator.rb diff --git a/.gitea/workflows/ci.yml b/.gitea/workflows/ci.yml index d28e3ec..bf99610 100644 --- a/.gitea/workflows/ci.yml +++ b/.gitea/workflows/ci.yml @@ -18,7 +18,7 @@ jobs: bundler-cache: true - name: Ruby syntax check - run: find app.rb config lib -name "*.rb" -print0 | xargs -0 -n1 ruby -c + run: find app.rb config lib bin -name "*.rb" -print0 | xargs -0 -n1 ruby -c - name: App loads without errors run: bundle exec ruby -e 'require_relative "app"' diff --git a/bin/gnss_simulator.rb b/bin/gnss_simulator.rb new file mode 100755 index 0000000..c8e57fe --- /dev/null +++ b/bin/gnss_simulator.rb @@ -0,0 +1,72 @@ +#!/usr/bin/env ruby +# frozen_string_literal: true + +# Speaks gpsd's own JSON wire protocol (https://gpsd.io/gpsd_json.html) +# directly on gpsd's default port — GpsdClient (lib/maptool/gpsd_client.rb) +# connects to it exactly as it would to real gpsd, so no app code or config +# changes are needed. Unlike docs/development.adoc's fake-NMEA-source +# example, this doesn't need gpsd itself installed/running at all, and +# actually moves (a fixed point isn't much of a "GNSS position" to look +# at) — drives a loop around a center point at a driving-ish pace, one TPV +# report per second like a real receiver. +# +# Usage: bin/gnss_simulator.rb [lat] [lon] [radius_m] [port] +require "socket" +require "json" + +$stdout.sync = true + +LAT = (ARGV[0] || 48.1486).to_f +LON = (ARGV[1] || 17.1077).to_f +RADIUS_M = (ARGV[2] || 300).to_f +PORT = (ARGV[3] || 2947).to_i +LOOP_SECONDS = 120 # time to complete one full loop +EARTH_RADIUS_M = 6_371_000 + +def point_on_loop(center_lat, center_lon, radius_m, angle_rad) + # Flat-plane offset, not geodesic — fine at this radius (metres, not km), + # same reasoning as measure.js's own approximations at this scale. + lat_offset = (radius_m * Math.sin(angle_rad)) / EARTH_RADIUS_M + lon_offset = (radius_m * Math.cos(angle_rad)) / (EARTH_RADIUS_M * Math.cos(center_lat * Math::PI / 180)) + [center_lat + lat_offset * 180 / Math::PI, center_lon + lon_offset * 180 / Math::PI] +end + +server = TCPServer.new("127.0.0.1", PORT) +puts "[gnss-simulator] driving a #{RADIUS_M.to_i}m loop around #{LAT},#{LON} — listening on 127.0.0.1:#{PORT}" + +loop do + socket = server.accept + puts "[gnss-simulator] client connected" + start = Time.now + + begin + loop do + elapsed = Time.now - start + angle = (elapsed / LOOP_SECONDS) * 2 * Math::PI + lat, lon = point_on_loop(LAT, LON, RADIUS_M, angle) + + # Bearing of travel: tangent to the circle, 90 degrees ahead of the + # radius vector at this point. + track = (angle * 180 / Math::PI + 90) % 360 + speed = (2 * Math::PI * RADIUS_M) / LOOP_SECONDS # m/s, constant pace + + report = { + class: "TPV", + mode: 3, + lat: lat, + lon: lon, + altHAE: 150.0, + speed: speed, + track: track, + eph: 3.0, + time: Time.now.utc.strftime("%Y-%m-%dT%H:%M:%S.%3NZ"), + } + socket.puts(JSON.generate(report)) + sleep 1 + end + rescue Errno::EPIPE, IOError + puts "[gnss-simulator] client disconnected" + ensure + socket.close + end +end diff --git a/docs/development.adoc b/docs/development.adoc index bb34bec..5c5be88 100644 --- a/docs/development.adoc +++ b/docs/development.adoc @@ -52,9 +52,28 @@ up as). On openSUSE, `gpsd` is a package with systemd units package manager. No app-side configuration is needed — it just connects to `127.0.0.1:2947`, gpsd's default port. -To test without hardware, feed gpsd a fake NMEA stream over TCP -instead of a real serial device — gpsd accepts a `tcp://host:port` -source: +To test without hardware or gpsd itself installed/running, run +`bin/gnss_simulator.rb` — it speaks gpsd's own JSON wire protocol +directly on gpsd's default port (`127.0.0.1:2947`), so +`Maptool::GpsdClient` connects to it exactly as it would to real +gpsd, no app-side changes needed. Drives a loop around a center point +(Bratislava by default) at a driving-ish pace instead of sitting at a +fixed position, since a stationary "GNSS position" isn't much to look +at on the map: + +[source,shell] +---- +bin/gnss_simulator.rb # loop around Bratislava, 300m radius +bin/gnss_simulator.rb 48.85 2.35 500 # custom center/radius (Paris, 500m) +---- + +`GET /api/gnss/position` should report a fix within a couple of +seconds, moving on every poll. + +To test against gpsd itself instead (e.g. to exercise the actual +NMEA-parsing path, not just gpsd's JSON output), feed it a fake NMEA +stream over TCP instead of a real serial device — gpsd accepts a +`tcp://host:port` source: [source,shell] ----