Sixfab Pico LTE DIY GPS Tracker via GPSLogger (and Webhook)

Hi All!

I couldn’t find an existing project for this, so I decided to make my own and share it!

The Sixfab Pico LTE is based on a Raspberry Pi Pico W and has a Quectel BG95-M3 LTE modem built onto it. It’s currently $49.00 USD (+shipping) and includes 1 year of data (up to 1GB).

Sixfab provides several examples to work with, you could use MQTT for this using their library. I have MQTT running, but didn’t want to expose it to the internet and I found that the GPSLogger integration configures it’s own webhook - so that works for me!

To configure this, you need a few setup pieces:

  1. In Home Assistant, configure GPSLogger. This will automatically setup a Webhook for you. Save the URL you get, you’ll need it in the code.
  2. Configure Thonny or your Python IDE of choice. This is technically optional, but Thonny gives you an easy way to load the library and Python script directly onto the Pico
  3. On the Raspberry Pi Pico, load the pico_lte library from Sixfab
  4. Load the Python script onto the pico and name it “main.py” so it runs automatically when power is connected.

Script:

url="<place your webhook URL here>"
device_name = "<your device name here>"

import time
import math

from machine import ADC
from pico_lte.core import PicoLTE
from pico_lte.common import debug
from pico_lte.utils.status import Status

def get_volt():
    R1 = 10040
    R2 = 2208
    pot = ADC(27)
    pot_val = pot.read_u16()
    vout = ((pot_val/65535) * 3.3)/ (R2/(R1+R2));
    return vout

def dms2dd(degrees, minutes, seconds, direction):
    degmin = minutes+"."+seconds
    dd = float(degrees) + float(degmin)/60;
    if direction == 'S' or direction == 'W':
        dd *= -1
    return dd;

def decode(coord):
    x = coord.split(".")
    head = x[0]
    tail = x[1]
    deg = head[0:-2]
    min = head[-2:]
    sec = tail[:4]
    dir = tail[-1:]    
    return dms2dd(deg, min, sec, dir)

def get_temp():
    sensor_temp = machine.ADC(4)
    conversion_factor = 3.3 / (65535)
    reading = sensor_temp.read_u16() * conversion_factor
    temperature = 27 - (reading - 0.706)/0.001721
    ftemp = temperature * 9 / 5 + 32
    return ftemp

fix = False
picoLTE = PicoLTE()
led = machine.Pin(22, machine.Pin.OUT)

picoLTE.peripherals.adjust_neopixel(0, 0, 0)
led.off()

while True:
    # First go to GNSS prior mode and turn on GPS.
    picoLTE.gps.set_priority(0)
    time.sleep(3)
    picoLTE.gps.turn_on(accuracy=3)
    debug.info("Trying to fix GPS...")

    for _ in range(0, 45):
        result = picoLTE.gps.get_location()
        debug.info(result)

        if result["status"] == Status.SUCCESS:
            debug.debug("GPS Fixed. Getting location data...")

            loc = result.get("value")
            debug.info("Lat-Lon:", loc)
            loc_message = ",".join(word for word in loc)            
            resp = result["response"][0].split(',')

            fix = True
            break
        time.sleep(2)  # 45*2 = 90 seconds timeout for GPS fix.

    if fix:
        # Go to WWAN prior mode and turn off GPS.
        picoLTE.gps.set_priority(1)
        picoLTE.gps.turn_off()

        debug.info("Sending message to the server...")
        picoLTE.network.register_network()
        picoLTE.http.set_context_id()
        picoLTE.network.get_pdp_ready()
        picoLTE.http.set_custom_header()
        
        lat = decode(loc[0])
        lon = decode(loc[1])
        temp = get_temp()
        volt = round(get_volt(),2)

        payload = "latitude="+str(lat)+"&longitude="+str(lon)+"&device="+device_name+"&accuracy="+str(resp[3])+"&speed="+str(resp[7])+"&direction="+str(resp[6])+"&provider="+str(temp)+"&battery="+str(volt)
        
        picoLTE.http.set_server_url(url)
        result = picoLTE.http.post(data=payload)
        debug.info(result)

        if result["status"] == Status.SUCCESS:
            debug.info("Message sent successfully.")
            led.on()
            fix = False
            picoLTE.network.deactivate_pdp_context()
        
        cnt = 30 # Start at 30 minute update intervals
        if (float(resp[7]) > 0):
            cnt = 1 #Once a minute if moving
         
        while (cnt > 0):
            cnt = cnt - 1
            if (get_temp() > 85 and cnt > 5):
                cnt = 5 #Once every 5 minutes if too warm
            time.sleep(60)  # 30 seconds between each request.
            led.off()

A few points I have customized:

  • I’m using ADC on GP27 as a voltage monitor for the 12v battery and created a divider circuit on a small PCB beside it. You can remove that if you don’t want to monitor voltage.
  • I’m monitoring temperature on the RPi’s temp sensor, but will be switching over to an external sensor on GP28 soon since the built in sensor reads really hot all the time
  • I’m outputting the temperature to the “provider” field, since I’m not using that for anything else. That way I don’t have to make a 2nd call to update a separate temperature entity.

Once this is all done, it shows up as a device under your GPSLogger integration and you can set zones for warnings in automations or trigger off of voltage/temperature events.