the klonoot blog

20260904 live tracking experiments

Hardware

  • 2x Seeed Studio XIAO ESP32S3 & Wio-SX1262 Kit
  • 2x HELTEC 868-915MHz Antenna (HT-LoRaIPEX1)

  • Server running mosquitto

  • Android phone with meshtastic app
  • Meshtastic web, on Mac running helium

  • USB-C cable from phone

  • Super high speed USB-C cable from projector

Goal

  1. Get a position packet from one radio to another, in a private channel, or in a private message.
  2. Upload that position packet to a local MQTT server
  3. Save the position and the ID of the first radio into a Postgres DB

Notes

  • The dev board USB connectors seem to be very sensitive to cable quality and movement. The high quality cable was much better. The cheaper phone data cable was less stable than connecting over Wi-Fi
  • Meshtastic web UI is buggy, will probably use the CLI or the phone app from now on.

Method

The two radios have been named BORT and BONK.

  • BORTis being used as a "base" node -- it is set to role CLIENT, and has the standard LongFast primary channel enabled. It has MQTT uplink enabled. The root MQTT topic was set to live-tracking, but this was unnecessary. It has only been noted here to make the examples later make sense. JSON output enabled withing the MQTT module.
  • It also has a second channel enabled, named private. The key for this channel was generated on this device.
  • BONK is set up (to the best of my knowledge) as a tracking node. Its role is TRACKER, and its primary channel is the privatechannel setup on BONK. MQTT uplink was disabled on this device.
  • Has a shared secret key
  • Has location enabled
  • Location configured to be shared every minute
  • For this test, the phone location was shared with the device, as it does not have a GPS chipset.

Result

BONK and BORT in same room
  1. Success (0 hops)
  2. Success
  3. Success
BONK indoors, BORT on balcony (wifi router is indoors)
  1. Success (0 hops)
  2. Success
  3. Success
BONK in neighbourhood, BORT on balcony
  1. Success (2 hops)
  2. Success
  3. Success
BONK on bike ride, BORT on balcony
  1. Partial success (2 hops)
  2. Success
  3. Success

Output

Two kinds of messages within the private channel made it to the MQTT broker. Less interesting was a message with binary encoding:

live-tracking/2/e/private/!d45aa270
�y1���%�r�j(x�� H5��މ=�r�jE�@Hx���private !d45aa270

And more usefully, a JSON-encoded message:

live-tracking/2/json/private/!d45aa270 {"channel":1,"from":2904406684,"hop_start":3,"hops_away":0,"id":2313080000,"payload":{"altitude":303,"latitude_i":494515737,"longitude_i":110622513,"precision_bits":32,"time":1788506792},"sender":"!d45aa270","snr":5.75,"timestamp":1788506792,"to":4294967295,"type":"position"}

Useful information:

  • The node name/ID is in the topic. Useful for identifying users later on.
  • The channel name is in the topic as well. This could be used to identify routes.
  • Most obviously, the lng, lat, timestamp and altitude are present within the payload
  • We can also filter messages by their type. We are only interested in "type": "position" for now

None of the messages were visible in the output of a local store-and-forward server which should be within range. This does need further testing.

Future work

The most obvious next steps are a range test, and a way to forward the packets into the DB in a more organised way.

Packet structure and persistence

  • Ditch the topic prefix (live-tracking)
  • Use the first part of the route ID as the channel name (the user would have to know this, so it might make sense to lower-case the whole thing, in order to improve ease of input on a phone)
  • For example, 2521EE10-F136-4BA4-AD0E-21E17E6E0C43 would become 2521ee10.
  • When reading from the topic, we could then add rows into the DB using a query like USING (SELECT id FROM user_routes WHERE id::string ILIKE "%<TOPIC>%") as routeand get the route id back from that.
  • Check whether id collision is more or less likely when using the start or end of the uuid
  • Research whether the packets can be encrypted in a way where the user would also be able to decrypt them, but the server cannot. Failing this, encrypt the packets before writing to the DB, so that a DB breach does not leak users' historical locations (they should also be deleted either on live-track session end, or after a predetermined period of time, whichever is shorter)
  • RLS
  • Is it possible to use the user saved in postgres as the MQTT user?

Querying the DB for packets

SELECT payload #> '{payload,latitude_i}', payload #> '{payload,longitude_i}' as lng, payload #> '{payload,altitude}' as alt FROM public.mqtt_messages
WHERE payload ->> 'type' = 'position'
AND topic ILIKE '%private%'
ORDER BY "createdAt" DESC