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.
6.0 KiB
Development guide
Web app only
bundle install
bundle exec puma -C config/puma.rb
Open http://127.0.0.1:4567. For auto-reload during development, run
via the rerun gem instead:
bundle exec rerun -- bundle exec puma -C config/puma.rb
|
Tip
|
Browsers apply heuristic caching to static files even without
explicit cache headers, so an edit to public/ (or views/) can
appear not to have taken effect on the next reload. Force a real
reload (e.g. Ctrl/Cmd+Shift+R) if that happens.
|
Offline map data
To enable the offline style, place a PMTiles file at
data/offline.pmtiles (see docs/architecture.adoc for how it is
served and styled). It is not part of the repository — obtain or
build one locally:
-
The simplest source is a pre-built extract from protomaps.com’s extract builder — pick a region and download it as
.pmtiles. Its schema is whatprotomaps-themes-base(the styling library used for the offline style) expects. -
To convert some other vector tile source (e.g. an
.mbtilesfile) into PMTiles, use the go-pmtiles CLI:pmtiles convert in.mbtiles data/offline.pmtiles.
Once the file is in place, GET /api/offline-tiles reports
available: true and the map defaults to the offline style.
GNSS position (gpsd)
The "GNSS" control needs gpsd running and pointed at
a receiver (gpsd /dev/ttyUSB0, or whatever device the receiver shows
up as). On openSUSE, gpsd is a package with systemd units
(gpsd.service / gpsd.socket); on other distributions, check your
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 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:
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:
# terminal 1: a TCP server repeating one NMEA GGA/RMC sentence pair
python3 - <<'PY' 5001
import socket, sys, time
def checksum(s):
cs = 0
for ch in s:
cs ^= ord(ch)
return f"{cs:02X}"
def sentence(body):
return f"${body}*{checksum(body)}\r\n"
port = int(sys.argv[1])
srv = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
srv.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
srv.bind(("127.0.0.1", port))
srv.listen(1)
conn, _ = srv.accept()
gga = "GPGGA,123519,4808.916,N,01706.462,E,1,08,0.9,140.4,M,46.9,M,,"
rmc = "GPRMC,123519,A,4808.916,N,01706.462,E,000.5,084.4,230824,003.1,W"
while True:
conn.sendall((sentence(gga) + sentence(rmc)).encode())
time.sleep(1)
PY
# terminal 2: point gpsd at it instead of a real device
gpsd -N -n tcp://127.0.0.1:5001
GET /api/gnss/position should then report the fake fix (Bratislava
Castle, in the sentences above) within a couple of seconds.
Tests
bundle exec rake test runs the whole suite (test/test_*.rb,
Minitest — a default gem, but pinned in the Gemfile’s `:test group
for reproducibility, same reasoning as rake itself). test_app.rb
drives app.rb’s routes end to end through `Rack::MockRequest rather
than a real server; everything else tests one lib/maptool/ module in
isolation. MAPTOOL_DATA_DIR (see app.rb) is pointed at a throwaway
Dir.mktmpdir by test/test_helper.rb before app.rb loads, so
running the suite never touches real data/ — the app-level tests
also wipe that directory between each test (see AppTestHelper#teardown),
since GPX_STORE/LIVETRACK_STORE are process-wide constants shared
by every test in the run.
LiveTrackClient’s own tests use a synthetic fixture (no network) —
its parsing was checked against real Garmin sessions by hand during
development, not by the suite, since that needs a live, unexpired
session token to fetch. `Fit’s tests likewise build a minimal FIT
file by hand rather than shipping a real device fixture; correctness
against real files (position/altitude fields, definition-message
framing, compressed-timestamp headers) was checked by hand against
`python-fitparse’s test fixtures during development — see
`docs/architecture.adoc.
Continuous integration
.gitea/workflows/ci.yml runs on every push/PR against a self-hosted
Gitea Actions runner: ruby -c on every .rb file, a require_relative
"app" smoke check that the app loads without error, bundle exec rake
test (see above), and node --check on every file under
public/js/ — no JS test suite, just a syntax check.
.gitea/workflows/renovate.yml runs
Renovate (the renovate/renovate image)
weekly (Mondays, also triggerable manually), opening PRs for outdated
dependencies (Bundler Gemfile/Gemfile.lock, auto-detected — no
renovate.json needed) against this Gitea instance
(RENOVATE_PLATFORM=gitea). It runs on the native deploy runner (see
docs/deployment.adoc) as a one-shot docker run, not a persistent
service. Needs a RENOVATE_TOKEN repo secret — a Gitea personal
access token (Settings → Applications → Generate New Token, with
repository read/write scope) from whichever account should author the
PRs; RENOVATE_ONBOARDING=false skips Renovate’s usual first-PR
"configure me" step and goes straight to dependency update PRs using
its defaults.