Feed a fixed lat/lon/alt into gpsd so clients
(WSJT-X, a Winlink packet-radio repeater, cgps, APRS software, etc.) see
an ordinary gpsd TPV fix -- with no real GPS hardware attached. Useful for
a fixed station (home shack, repeater site) where the position never
changes but the software downstream still wants to talk to gpsd.
No gpsd source patches, no AppArmor policy edits, no custom driver code. Swapping in real GPS hardware later (or back) is a live, one-command operation with no service restart.
gpsd's driver architecture (gps_type_t in include/gpsd.h) is built to
parse bytes from a real file descriptor -- there's no hook for "synthesize a
report on a timer, no I/O involved" without also patching gpsd's core
select/poll loop. That's a much bigger, more invasive change than it sounds,
and it'd need redoing on every gpsd upstream update.
Instead: gpsd already treats non-tty file descriptors specially --
gpsd_serial_isatty() in gpsd/serial.c skips all the termios/speed setup
when the source isn't a tty -- so an ordinary FIFO works as a device with
zero gpsd source changes. This is the same mechanism gpsd's own gpsfake
test harness relies on (see gpsfake.py.in), just without a pty.
The FIFO lives at /dev/gpsd0 specifically because Debian/Ubuntu's
gpsd package ships an AppArmor profile (/etc/apparmor.d/usr.sbin.gpsd)
that restricts which device paths gpsd may open. It allows
/dev/tty{,S,USB,AMA,ACM}[0-9]*, /dev/rfcomm*, /dev/pps[0-9]*, and
/dev/gpsd[0-9] -- but not /dev/pts/*. That's confirmed by testing:
pointing a pty-backed source (e.g. gpsfake) at this gpsd produces
apparmor="DENIED" ... name="/dev/pts/N" in the kernel log. Using the
pre-whitelisted /dev/gpsd0 path avoids editing AppArmor policy entirely.
(If your distro's gpsd doesn't run under AppArmor, /dev/gpsd0 still works
fine -- it's just a plain FIFO either way.)
- Linux with systemd
gpsd(tested against the Debian/Ubuntu 3.25 package; should work with any reasonably recent gpsd -- the design doesn't depend on gpsd version internals, just the documented non-tty behavior)- Python 3,
sudo
git clone https://github.com/<you>/gpsd-static-feed.git
cd gpsd-static-feed
sudo ./install.sh # installs to /opt/gpsd-static
# or: sudo ./install.sh /opt/somewhere-else
install.sh:
- copies
static-gps-feed.py,gpsd-static-ctl.sh, and this README to the install directory, - renders the
static-gps-feed.service,gpsd-static.tmpfiles.conf, andsudoers-static-gps-feedtemplates (they contain@USER@/@INSTALL_DIR@placeholders, substituted with whoever ransudoand where you're installing) and installs them to their real system locations, - points the system gpsd at
/dev/gpsd0(/etc/default/gpsd:DEVICES="/dev/gpsd0",GPSD_OPTIONS="-n", backing up the original to/etc/default/gpsd.bak), - enables and starts the feed.
Then edit /etc/default/static-gps-feed to your real position and:
sudo systemctl restart static-gps-feed
cgps # should show mode 3D at the configured lat/lon/alt
sudo ./uninstall.sh # stops/removes the service, tmpfiles rule,
# sudoers rule; offers to restore the
# original /etc/default/gpsd
sudo ./uninstall.sh --purge # also deletes /etc/default/static-gps-feed
# and the installed copy of this repo
static-gps-feed.py-- writes correctly-checksummed$GPGGA/$GPRMCat 1 Hz (live UTC timestamps, fixed lat/lon/alt) into a FIFO, openedO_RDWRso it never blocks waiting for gpsd and survives gpsd restarts.gpsd-static-ctl.sh-- start/stop/status the feed, and hot-swap gpsd between it and real hardware (see below).gpsd-static.tmpfiles.conf-- template; recreates/dev/gpsd0on every boot (devtmpfs doesn't persist hand-made nodes across a reboot).static-gps-feed.service-- template; systemd unit running the feeder.sudoers-static-gps-feed-- template; narrowly-scoped NOPASSWD rule sogpsd-static-ctl.shruns unattended from another launcher.default-static-gps-feed-- installed to/etc/default/static-gps-feed, holdsSTATIC_POSITION=lat,lon[,alt].install.sh/uninstall.sh-- see above.
sudoedit /etc/default/static-gps-feed
sudo systemctl restart static-gps-feed
No gpsd restart needed -- gpsd just keeps reading from the same FIFO.
static-gps-feed.service is enabled by install.sh, so it's already
running continuously in the background -- that's harmless to leave as-is
(gpsd just has no data to report until it's running). If you'd rather
start/stop it explicitly from another script (e.g. right before/after a
Winlink session), use the copy install.sh placed in your install
directory:
/opt/gpsd-static/gpsd-static-ctl.sh start
/opt/gpsd-static/gpsd-static-ctl.sh stop
/opt/gpsd-static/gpsd-static-ctl.sh status
This just wraps systemctl <verb> static-gps-feed.service. install.sh
also installs /etc/sudoers.d/static-gps-feed, a narrowly-scoped rule
allowing only systemctl {start,stop,restart,status} static-gps-feed.service (plus the gpsdctl commands below) without a
password, so the control script runs unattended from another launcher.
If you want it to not auto-start at boot and only run when explicitly started this way:
sudo systemctl disable static-gps-feed.service
(gpsd itself should stay running as a normal system service either way --
it's shared infrastructure other things may use, and with no data on
/dev/gpsd0 it just reports no fix.)
gpsd always keeps a control socket open (/run/gpsd.sock) and gpsdctl can
add/remove devices from a running gpsd through it -- the same mechanism
USBAUTO="true" in /etc/default/gpsd already uses to hotplug recognized
USB GPS chipsets (see /lib/udev/rules.d/60-gpsd.rules on Debian/Ubuntu).
So switching sources doesn't need a gpsd.service restart (which would drop
every client's connection) -- just:
gpsd-static-ctl.sh hardware /dev/ttyUSB0 # gpsd: /dev/gpsd0 -> your real GPS
...
gpsd-static-ctl.sh static # gpsd: back to /dev/gpsd0, feeder restarted
hardware remembers which device it attached (in
${XDG_RUNTIME_DIR:-/tmp}/gpsd-static-ctl.hwdevice) so static can detach
it again without you repeating the path.
Note the naming trap: this project's FIFO is /dev/gpsd0 (with a "d"). A
recognized USB GPS auto-symlinks to /dev/gps0 (no "d") via the udev rule
above -- different device, easy to mistype.
gpsdctl only works unprivileged against a private test instance
(/tmp/gpsd.sock, what gpsfake uses) -- talking to the real system gpsd
requires root, which is why every call above goes through sudo.
MIT, see LICENSE.