Compare commits
3
Commits
4aafe332a7
...
64a85afe6a
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
64a85afe6a | ||
|
|
3e81ee8015 | ||
|
|
61ba86c6e6 |
No files matched your search
@@ -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"'
|
||||
|
||||
Executable
+72
@@ -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
|
||||
@@ -1,6 +1,7 @@
|
||||
services:
|
||||
maptool:
|
||||
build: .
|
||||
image: maptool:latest
|
||||
restart: unless-stopped
|
||||
ports:
|
||||
- "4567:4567"
|
||||
@@ -26,3 +27,19 @@ services:
|
||||
# docs/development.adoc for how the deploy job treats this
|
||||
# directory).
|
||||
- ./data:/app/data
|
||||
|
||||
# Fakes a GNSS fix (a loop around Bratislava, see
|
||||
# bin/gnss_simulator.rb/docs/development.adoc) since there's no real
|
||||
# receiver attached here — the "GNSS" control would otherwise just show
|
||||
# "no fix" always. Remove this service (and nothing else) whenever a real
|
||||
# receiver/gpsd setup replaces it. network_mode ties it to maptool's own
|
||||
# network namespace rather than a bridge network, since GpsdClient
|
||||
# connects to a hardcoded 127.0.0.1:2947 — same as gpsd itself normally
|
||||
# being local to the machine it serves.
|
||||
gnss-simulator:
|
||||
image: maptool:latest
|
||||
depends_on:
|
||||
- maptool
|
||||
restart: unless-stopped
|
||||
network_mode: "service:maptool"
|
||||
command: ["ruby", "bin/gnss_simulator.rb"]
|
||||
@@ -107,6 +107,18 @@ id/secret to store as the two `OAUTH_CLIENT_*` secrets above.
|
||||
usernames, e.g. `randy,friend1,friend2` — not their emails, the
|
||||
`login` field from Gitea's `/api/v1/user`.
|
||||
|
||||
== GNSS simulator
|
||||
|
||||
`docker-compose.yaml`'s `gnss-simulator` service (`bin/gnss_simulator.rb`,
|
||||
also documented for local dev in `docs/development.adoc`) runs
|
||||
alongside `maptool` in production too, sharing its network namespace
|
||||
(`network_mode: "service:maptool"`) so it's reachable at the
|
||||
hardcoded `127.0.0.1:2947` `Maptool::GpsdClient` expects — there's no
|
||||
real GNSS receiver attached to the server, so without this the "GNSS"
|
||||
control would just always show "no fix". It drives a loop around
|
||||
Bratislava; remove the service block (nothing else needs to change)
|
||||
whenever a real receiver/gpsd setup replaces it.
|
||||
|
||||
== Garmin LiveTrack session webhook
|
||||
|
||||
`POST /api/livetrack/session` (`session_id`, `session_token`, `secret` form
|
||||
|
||||
+22
-3
@@ -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]
|
||||
----
|
||||
|
||||
+10
-3
@@ -105,21 +105,28 @@ export class MeasureControl {
|
||||
id: FILL_LAYER_ID,
|
||||
type: "fill",
|
||||
source: SOURCE_ID,
|
||||
filter: ["==", "$type", "Polygon"],
|
||||
filter: ["==", ["geometry-type"], "Polygon"],
|
||||
paint: { "fill-color": "#1e5fbf", "fill-opacity": 0.15 },
|
||||
});
|
||||
this._map.addLayer({
|
||||
id: LINE_LAYER_ID,
|
||||
type: "line",
|
||||
source: SOURCE_ID,
|
||||
filter: ["!=", "$type", "Point"],
|
||||
filter: ["!=", ["geometry-type"], "Point"],
|
||||
paint: { "line-color": "#1e5fbf", "line-width": 2 },
|
||||
});
|
||||
// "role" alone (not also a geometry-type check) — it's only ever set on
|
||||
// Point features to begin with (the line/polygon shape feature has no
|
||||
// "role" property at all), so it already implies the geometry type.
|
||||
// Mixing the legacy "$type" filter syntax with an expression like
|
||||
// ["get", "role"] in the same filter logs as unsupported in newer
|
||||
// MapLibre — this sidesteps that rather than fixing it by switching
|
||||
// "$type" to ["geometry-type"] and keeping both checks.
|
||||
this._map.addLayer({
|
||||
id: POINT_LAYER_ID,
|
||||
type: "circle",
|
||||
source: SOURCE_ID,
|
||||
filter: ["all", ["==", "$type", "Point"], ["==", ["get", "role"], "vertex"]],
|
||||
filter: ["==", ["get", "role"], "vertex"],
|
||||
paint: { "circle-radius": 4, "circle-color": "#fff", "circle-stroke-color": "#1e5fbf", "circle-stroke-width": 2 },
|
||||
});
|
||||
// Distance mode only (see _render) — each segment's own length, not
|
||||
|
||||
Reference in new issue
Block a user