Off-grid GPS (race) tracker client and server

GPS tracker map server and client for use in remote locations with no internet coverage or mains power.

Similar projects worth following
Battery powered trackers send their positions back to a Raspberry Pi tracking server via LoRa radio link. Device positions and tracks are served to mobile devices connected to the Raspberry Pi wifi hotspot, using offline Openstreetmap tiles or other saved maps.

There are many GPS tracker client/server combinations already in public domain, but these generally rely on a mobile internet connection (via GPRS/3G for example) to report competitor positions back to the tracking server. For tracking competitors in race segments where mobile data coverage is not available, some of the more elegant existing solutions will cache the device position until mobile coverage is available again, then update the server with the backlog of track logs.

This approach has a few downsides for my application:

  • each tracker device must have a GSM/GPRS/3G modem, SIM and data contract
  • each tracker needs mobile data coverage at SOME point, in order to upload cached tracks
  • the tracker server application needs an internet connection to receive position updates and serve up map tiles.
  • people wishing to track devices must have an internet connection to view the tracker server portal.

There are a number of mountain bike races and triathlons around the Scottish Highlands where no mobile signal is available on any part of the route, and certainly not at any spectator or team pit area where the tracker server web portal needs to be set up.

Apart from Satellite modems, the only other solution is to use some radio modem for the data backhaul from tracker device to the server. The (unfortunately closed-source) LoRa protocol is a candidate for a suitable platform; offering longer range data links for small packets (<250bytes) and low power consumption.

The LoRaWAN protocol would be ideally suited to forming a mesh of receivers along a race route, with backhaul links (possibly over mobile data/satellite); where the heavy lifting of physical and data layer protocol, multiple client access and data-rate negotiation are transparent to the end user. LoRaWAN gateways and compatible remote devices are currently prohibitively expensive (although some crowdfunded solutions are emerging), so the alternative is just to use the LoRa data protocol for its long range data link capability, and our own method of multiple access and reception.

Unit Design Outline:

GPS trackers

LiPo powered GPS + LoRa radio + microcontroller unit to be mounted on bike/wristband.

Demo units are using Adafruit Feather M0 LoRa units with Adafruit GPS featherwing for development. Code on the Feather determines the unit device ID and timeslot for transmitting position/battery voltage/panic button back to Raspberry Pi. TDMA synchronised transmission slots synchronised to GPS are used to avoid collisions between multiple devices. With a 30 second refresh rate and suitable LoRa parameters, tens or hundreds of devices can share one frequency band and one receiver instance. Multiple receivers can be cheaply deployed (<£30 each receiver) on multiple frequencies across a licence-exempt band, negating any spectrum congestion. Trackers out of range of the base station may form an ad-hoc soft mesh to relay positions onwards to the receiver.

Receiver/Tracker Server

Raspberry Pi with HopeRF LoRa radio shield receives LoRa packets and decodes position reports/deviceid. Data is forwarded to GPS tracking server running on the same device.

Each LoRa receiver (HopeRF RFM98W) can handle 60 clients (at 30 second poll and ~400ms packet duration) . 2 HopeRF units can be handled with ease on a single raspberry pi. With care, around 10 LoRa modems can be chained to one Raspberry Pi over SPI, giving capacity for 600 tracker devices per raspberry pi. Additional raspberry Pi receivers can be configured to receive packets and forward to the tracking server if needed.

The same Raspberry Pi is set up as a WiFi access point and captive portal, redirecting mobile/tablet clients to GPS tracking web portal. The tracking web portal then displays position of competitors. For small numbers of trackers (<120), the LoRa receiver and GPS tracking portal server can all be on the same device

  • 1 × Adafruit Feather LoRa M0 433Mhz Tracker - radio, microcontroller and charge/battery management
  • 1 × Adafruit Ultimate GPS Featherwing Tracker - GPS position source and timing source for pseudo-TDMA
  • 1 × 3.7v >1200mAh LiPo cell w/ JST connector. Tracker - power supply 1200mAh lasts around 24 hours with 20 second transmissions. Consider 1x18650@2200mAh
  • 1 × 433Mhz 1/4 wave antenna - u.Fl Tracker - rubber duck antenna for tracker. Consider SMA-u.Fl to increase buying options
  • 1 × u.Fl SMT Tracker - adds u.Fl connector to Feather LoRa M0 board

View all 10 components

  • Code on Github

    steve04/24/2017 at 13:06 1 comment

    starting to put some code up on GitHub - warning - very messy, crude, uncommented, but works for me. YMMV

  • UPS / 12v car power for raspberry pi

    steve12/01/2016 at 16:11 0 comments

    Every man and his dog needs to power their Pi from a battery, and deal with filesafe shutdown, but I can't get one that works!

    I'm now on Pi UPS number 3 in trying to find a system that will work - I want 'idiot proof' power - plug it in to cigarette lighter socket and it starts up. unplug it and it gracefully shuts down without corruption. Plug it back in and it reboots automatically.

    With the LoRa board and the LCD display as well as fairly processor intensive operation and wifi, the power requirements of the setup are over 1.5A@5V.

    I tried first an early generation UPSPico from modmypi, but this had pin conflicts with the LoRa module.

    I then tried a LiFePO4weredPi UPS, but the version available at November 2016 was limited to 450mA. I could just about get it to work if I isolated the LCD backlight 5V power from the Pi power, but then the battery still drained as there was about a 50mA deficit. I might wait for the 18650 version... the form factor is great.

    I then tried an ebay CPT 12V to 5V 15W (3A max) power supply, but this was atrocious - voltage at no-load was a healthy 5.15V but dropped to around 4.75V under >1A load.

    I then tried a new UPSPico from modmypi (HV3.0A Stack). This works fine with an official mains-to-micro USB adapter, but with anything else I struggle with current draw brownout over any of the USB to micro USB cables I have tried, and any of the 12V to 5V USB cigarette lighter adapters I use, even though they claim 2.1A output. When the UPS Pico charger kicks in, even the 2.1A output droops to ~4.7V and causes the UPS pico to brownout.

    Next to try:

    1) homemade USB micro cable to reduce cable loss

    2) LM2596 adjustable buck regulator to homemade USB socket to boost USB voltage available.

    3) UPS Pico HV3.0A stack PLUS, which has an external 7-24V input that I will wire to a 12V cigarette lighter socket directly (fused!)

    4) LiFePO4wered 18650 version.

  • optimised interrupt handler for less jitter in TDMA window

    steve08/23/2016 at 11:13 0 comments

    void loop()
        //if the current timestamp 'seconds' matches our device's transmission slot,
        // then do nothing else other than sit in a super-tight loop awaiting the interrupt
        if (systemSeconds == txSlotSeconds) {
            //do nothing else but loop tightly, awaiting PPS interrupt
            while (PPSflag !=True)
            //PPS flag is True - go and transmit straight away!
            //delay if necessary for inter-second timeslots
            delay(txSlotMillis); //or micros if needed!
        } else {
            //we are not due to transmit in the next second, so go and do other things - 

    I think this will work for devices that need to transmit on the second (i.e. txSlotMillis==0), as our code will start its tight loop in the second PRIOR to the scheduled txSlotSeconds, and wait in the tight loop for up to 1 second for the following PPS to come along.

    Doing a really crude measurement of a loop with digitalWrite(high);digitalWrite(low), the Cortex M0 with arduino compiler executes a loop (including 2 GPIO writes) in 3.6 microseconds. Each GPIO write takes16 clock cycles (various google sources differ for this), giving a corrected loop duration of 3 microseconds

    I need to work out how to do the loop in assembler perhaps, then our loop just needs to be a few clock cycles:

    tight while loop:

    //does our register (pps flag in r0) eq 0?
    mov r1,#1
    loop:cmp    r0,r1;       // compare pps register to 1
    brneloop ;                 //branch if not equal
    nop;                           //exit loop

    cost: 1 cycle if not equal, 3 cycles if equal (from ARM reference material)

    Interrupt latency: 16 cycles (various sources)

    ISR: 1 cycle ( mov r0,#1 )

    this gives a tight loop with interrupt-exit in about 18 cycles (probably missing something here)... or 0.375 microseconds at 48MHz

    I'm sure the limit on TDMA will now lie with either the HopeRF LoRa transmission timing or in the receiver.

  • TDMA slot timing - more detail

    steve08/23/2016 at 09:56 0 comments

    Somebody asked how the pseudocode worked out what timeslot it should use.

    Each tracker device is assigned an ID, which in turn we use to calculate the device's timeslot for transmission.

    We have an access slot window of 333ms (packet on-air duration is ~280ms)

    We require transmissions once every minute, which gives 180 devices per radio channel.

    We have a precise 1 pulse-per-second (PPS) reference signal from the GPS

    Therefore each device is assigned a 'seconds past the minute' timeslot, AND a 'milliseconds past the s30

    econd' timeslot.

    • Device 1 transmits on 'second timeslot' = 00 and 'millisecondtimeslot' = 000
    • Device 2 transmits on second 00 and millisecond 333
    • ...
    • Device 9 transmits on second 02 and millisecond 666


    On each PPS we set an interrupt flag.

    in the main LOOP() program, we only perform other actions (get GPS position, get battery voltage etc) if the PPS interrupt flag is NOT set. We also keep these other actions really short - so that if a PPS interrupt comes along whilst we are performing an action, we can exit quickly and service the interrupt.

    In the main LOOP() we are continually fetching and updating the the current time from the GPS (hh:mm:ss) - and we store the SECONDS (ss) in a local variable each time it is updated.

    When our PPS interrupt is flagged, we ADD ONE SECOND to the current time, and if this second value matches our device's 'second timeslot', we then DELAY() by our 'millisecond timeslot' value (possibly zero), then transmit our packet.

    Why do we add one second to the current time?

    Because on the exact moment that the PPS interrupt is triggered, our system has not yet had chance to fetch the the new timestamp from the GPS, but we know it has advanced by one second as we have just received the PPS pulse from the GPS! The value for the current timestamp is exactly 1 second out of date.

    The worst that will happen is that the PPS interrupt flag is set whilst we have just initiated a serial read from the GPS to update the time. We only receive one byte from the serial port before we check for a PPS flag, so there is no danger of updating the microcontroller's memory with the current timestamp before we act on it - system time is only updated once a complete NMEA sentence is received and parsed by the microcontroller and certainly not after just one byte.

    Keeping a fast LOOP() is crucial, so we must perform any actions very quicky (so they complete quickly and we iterate around the loop quickly back to servicing the PPS routine), and can be skipped if a PPS flag comes along.

    What is the minimum window time we can use?

    Our main loop() code completes in 7.8 microseconds (worst case scenario).

    Jitter - the difference in start time between subsequent transmissions in the best and worst case scenarios - varies between 0 and 7.8 microseconds

    If our PPS flag happens to be set when we are in the point in the loop() code that checks for the PPS flag, then we have the minimum delay before it is acted upon.

    If we have just finished checking for a PPS flag, and are now busy reading a byte from the GPS over serial - we have to wait for this to finish, then skip over all other checks before the loop() starts again and we service the interrupt flag. This is measured at taking 7.8 microseconds.

    Future Optimisation

    We can then theoretically have timeslots with, say, 10 microsecond guard period, if we have a precise delay(microseconds) function which itself does not have any jitter, and our activity (transmitting) within each window itself is not subject to jitter.

    In our scenario, if our LoRa transmission takes 280ms +- 1 ms, we could easily define TDMA access slots 282ms apart. i.e. with a 1ms guard period either side of a transmission window that easily accounts for the jitter in our loop() timing

    Moving from a TDMA slot of 333ms to 282ms gives us 212 slots per minute - an extra 32 slots/devices we can squeeze onto our 1 minute cycle.

    Clearly the limiting factor for the number of devices per radio channel is the time-on-air of each device...

    Read more »

  • Tracker Payload and LoRa parameters

    steve08/22/2016 at 20:37 0 comments


    Device ID: 1 byte (0-255)

    (Latitude: 3 bytes (0-90 degrees represented by a signed (3 byte integer) gives ~1.1m resolution at 56 degrees

    Longitude:4 bytes, ( 0-180 degrees represented by 4-byte signed integer)

    battery level: 1 byte (3.3-4.5v mapped onto 0-255) - probably overkill resolution

    Speed over ground 1 byte (0-63.75kph mapped on 0-255)

    COG - 1 byte 0-360 mapped on 0-255

    #satellites (4 bits) of one byte

    #valid fix (1 bit) of one byte

    #panic button (1 bit) of on byte

    # gpio flags 2 bits spare

    Total 12 bytes

    LoRa Parameters

    With a set of 'long range' LoRa parameters (SF=10, BW=125kHz,CR=4/8), implicit header and a 12 Byte payload, the time on air is 280ms (610bps equivalent bit rate).

  • microsecond resolution TDMA using GPS PPS on Arduino

    steve08/22/2016 at 20:35 0 comments


    GPS Pulse-Per-Second drives hardware interrupt. Tight loop() can give 10 microsecond TDMA time slots whilst still fetching and parsing GPS data. (48MHz Cortex M0)

    I need a way of synchronising multiple tracker units so their transmissions do not overlap. The obvious way I can think of is just to assign each device a timeslot - a number of milliseconds past the minute that each device should transmit.

    We know some rough figures - we want position updates about once per minute, we want to support > 100 devices per RF channel, and one LoRa 'packet' takes ~ 280ms on-air (See the other project log entry for how these values were calculated).

    In its most simple form, we could have 60 devices, and each one transmits on their designated second-past-the-minute. We have a fantastically precise "Pulse Per Second" (PPS) output from the GPS that will synchronise the start of every second across all devices.

    But I want more than 60 devices. So I need to come up with a way to accurately time the transmissions to fit in, say, a 1/3 second (333ms) transmit window, to give 3*60=180 possible devices per radio channel (or 90 devices at 30 second refresh rate etc).

    Fudging millisecond Time Division Multiple Access in Arduino IDE C

    Small real-time-clock modules are available for around £1, which we could add to the Feather, initialise with the time from the GPS, then use a tight loop to reference the time and get the transmission to take place.

    I decided against the added hardware complexity and managed it all in software, using the Pulse Per Second output from the GPS in an interrupt routine to trigger the transmission.

    In an ideal world, we would have the PPS signal from the GPS trigger a transmission from within the Interrupt Service Routine (ISR). This isn't possible for lots of reasons I could not fix, and isn't advisable as the amount of code, 'delay' functions etc that goes into transmitting a LoRa packet cannot be accomplished inside an ISR (Delay function uses interrupts itself).

    The main hurdle I had was accurately triggering a transmission on the sub-second "+333ms" or "second+666ms" timeslots.

    I think the most precise way to do this would be for the I/O triggered PPS interrupt to then trigger a timer-based interrupt for the sub-millisecond slot (333/666ms), whose own ISR would then trigger the LoRa transmission, however I couldnt get my head around how to code this up with the Cortex M0 core on the feather.

    Instead, I just use the main PPS interrupt to set a flag, then inside the main LOOP() function, keep the function steps small and skippable - so that if a PPS interrupt comes along, our code can skip right to sending the LoRa packet - utilising the perfectly adequate DELAY() function to achieve the inter-second slot timings, since interrupts are once again enabled outside the ISR itself.

    Pseudocode below.

    Inside the main program loop I perform a number of functions - get serial data from GPS, construct a NMEA sentence, parse into a GPS Object (to store current position, time etc), measure the battery level etc. So long as I can skip over each one, and am not tied up inside each function for any significant time, then we can keep our loop execution duration small and our jitter in potential slot timings low enough.

    Is this adequate? For measurement processes I stick a simple digitalWrite(debugpin,HIGH) at the beginning of void loop(), and a matching digitalWrite(debugpin,LOW) at the end, then measure the repetition rate of a loop on a scope when feeding it a constant stream of NMEA data (to represent worst case scenario).

    I measure a loop speed of ~128KHz - or a period of 7.8 microseconds, which is a conservative measure of the jitter in the timing of transmission slots when using this method. More than adequate for our purposes! We need to fit a 280ms packet inside a 333ms transmission window, so have many orders of magnitude better precision on our slot timing than is needed. We could easily drop to, say 140ms packet lengths (BW...

    Read more »

  • Tracker unit Code

    steve08/22/2016 at 18:40 0 comments

    Adafruit Feather M0 LoRa is programmed using Arduino IDE

    TinyGPS library talks to the Adafruit Ultimate GPS featherwing to retrieve position and current timestamp.

    Pulse Per Second hardware output from GPS triggers an interrupt routine on the M0 to synchronise transmission slots across all devices.

    Code outline:

    On Interrupt:

    • Set flag for 'transmit required=TRUE'


    • Configure LoRa and GPS serial
    • Determine our timeslot for sending telemetry.

    Await valid GPS fix before continuing


    • If 'transmit require' flag is set AND the current timestamp is within our timeslot
      • transmit our packet.
    • Else:
      • update device location when bytes on serial from GPS are available
      • construct a data packet ready for transmission
        • Device ID - Voltage (from onboard resistor divider) - position - fix status
      • get the current (number of seconds past the minute) from GPS

  • Tracker server software configuration

    steve08/22/2016 at 16:36 0 comments

    Server software: open source GPS tracking server

    running modified 'traccar-web' UI to allow auto-login as guest on mobile devices:

    hostapd on Pi sets up the Wifi hotspot

    dnsmasq on Raspberry pi acts as DNS and DHCP server for connected clients.

    dnsmasq also catches all HTTP requests and redirects to localhost - end users just connect to the hostpot then browse to any (non-https) web address - good for catchy marketing! (e.g. or any URL would work when connected to the Pi AP)

    Apache2 on Pi gives some flexibility in redirecting http requests to localhost to our auto-logon guest URL

    The off-the-shelf tracking portal can accept our offline tilestache maps quite easily (need to define the custom map source in the Config section AND configure the user accounts to use the custom map source), but the traccar-web mod required some butchering. See here for rough instructions (basically overwrite any reference to servers with your Pi tilestache server - editing the traccar.war file in ubuntu File Roller (archive manager) works quite well)

    Offline openstreetmap tiles are created with software and saved to the Pi

    OSM tiles are served to Traccar

    Python code receives position and battery reports via the LoRa radio (For the HopeRf RFM98 lora module you have to adapt the DIO and SPI port definitions - instructions here:

    , and reports to traccar software via OSMAND HTTP protocol (all done on loopback interface)

  • GPS tracker server

    steve08/04/2016 at 21:29 0 comments

    HopeRF RFM98W modem on uputronics shield is mounted on a Raspberry Pi.

    Python code on the Pi adapted from receives LoRa telemetry, formats and fowards it over loopback connection to GPS tracker server running on the same device.

    Open-source GPS tracker software or receives the reconstructed GPS position reports and displays on webpage map interface.

    Openstreetmap tiles or other custom map tiles are downloaded to the Pi ahead of time, and served on the Pi via installed on the Pi. Map tiles in .mbtile format are created beforehand using and Openstreetmap data; overlayed with GPX trails of race route if required, and then stored on the Pi SD card.

    The Raspberry pi is set up as a WiFi hotspot with DHCP and DNS, and serves the GPS map server to any connected devices (mobile phones, tablets) as a captive portal. This way no internet connection is needed at any point for tracking, logging or displaying positions on a map.

  • GPS tracker unit details

    steve08/04/2016 at 21:21 0 comments

    GPS tracker unit philosophy

    Adafruit Feather M0 LoRa 433MHz board

    Adafruit GPS featherwing board

    small <1000mAh lipo, or possible power feed from bike torch battery (4x18650)

    Code is based around Radiohead RFM95 library for handling (currently) one-way telemetry.

    Code to be uploaded when ready. Current status of code is that it will transmit position, speed, heading, altitude, battery level and 'panic button' status back to the server every 30 seconds, with a 500ms transmission slot per device, synchronised to the GPS clock and PPS output to prevent collisions with other devices.

    LoRa transmission parameters need to balance reception distance, update frequency, battery life etc with the fact that the device is likely to be moving, and the pseudo time-division multiple access (TDMA) regime for avoiding collisions.

View all 10 project logs

Enjoy this project?



Similar Projects

Does this project spark your interest?

Become a member to follow this project and never miss any updates