roam-location/main.go
Adriano Caloiaro 1c8ab19426
feat: log GPS fixes every poll, geocode only on significant movement
Remove the in-memory cache from geocode/nominatim.go (cacheTTL, cacheMu,
cachedLoc, cacheExpiry) so CityCenter always makes a live request when called.
Call frequency is now controlled by the caller rather than a time-based TTL.

In the poll goroutine, log the raw GPS coordinates on every cycle. Track the
last geocoded position and only call CityCenter on the first fix or when the
device has moved more than 5 miles (haversine). WebDAV snapshots are written
every poll cycle using the cached city centroid with fresh weather data.
2026-04-25 11:19:06 -06:00

116 lines
3.1 KiB
Go

package main
import (
"context"
"log"
"math"
"os"
"strconv"
"sync"
"time"
"github.com/acaloiaro/roam-location/geocode"
"github.com/acaloiaro/roam-location/gpsd"
"github.com/acaloiaro/roam-location/service"
"github.com/acaloiaro/roam-location/weather"
"github.com/acaloiaro/roam-location/webdav"
)
type locationEntry struct {
Lat float64 `json:"lat"`
Lon float64 `json:"lon"`
Name string `json:"name,omitempty"`
}
type snapshot struct {
RecordedAt time.Time `json:"recorded_at"`
Location *locationEntry `json:"location,omitempty"`
Weather *weather.Data `json:"weather,omitempty"`
Conditions *weather.Condition `json:"conditions,omitempty"`
}
type cache struct {
mu sync.RWMutex
lat float64
lon float64
name string
ready bool
}
func (c *cache) set(lat, lon float64, name string) {
c.mu.Lock()
defer c.mu.Unlock()
c.lat, c.lon, c.name, c.ready = lat, lon, name, true
}
func (c *cache) get() (lat, lon float64, name string, ok bool) {
c.mu.RLock()
defer c.mu.RUnlock()
return c.lat, c.lon, c.name, c.ready
}
func milesApart(lat1, lon1, lat2, lon2 float64) float64 {
const r = 3958.8
lat1r, lat2r := lat1*math.Pi/180, lat2*math.Pi/180
dlat := (lat2 - lat1) * math.Pi / 180
dlon := (lon2 - lon1) * math.Pi / 180
a := math.Sin(dlat/2)*math.Sin(dlat/2) + math.Cos(lat1r)*math.Cos(lat2r)*math.Sin(dlon/2)*math.Sin(dlon/2)
return r * 2 * math.Atan2(math.Sqrt(a), math.Sqrt(1-a))
}
func pollInterval() time.Duration {
if s := os.Getenv("POLL_INTERVAL_SECONDS"); s != "" {
if n, err := strconv.Atoi(s); err == nil && n > 0 {
return time.Duration(n) * time.Second
}
}
return 30 * time.Second
}
func main() {
loc := &cache{}
wCache := weather.NewCache()
interval := pollInterval()
wdClient := webdav.NewClientFromEnv()
go func() {
const geocodeTriggerMiles = 5.0
var geocodedLat, geocodedLon float64
var currentCity *geocode.Location
for {
lat, lon, err := gpsd.GetLocation(context.Background())
if err != nil {
log.Printf("location poll error: %v", err)
} else {
log.Printf("gps location: %.6f, %.6f", lat, lon)
if currentCity == nil || milesApart(lat, lon, geocodedLat, geocodedLon) > geocodeTriggerMiles {
city, err := geocode.CityCenter(context.Background(), lat, lon)
if err != nil {
log.Printf("geocode error: %v", err)
} else {
geocodedLat, geocodedLon = lat, lon
currentCity = &city
loc.set(city.Lat, city.Lon, city.Name)
log.Printf("location updated: %.6f, %.6f (%s)", city.Lat, city.Lon, city.Name)
}
}
if currentCity != nil && wdClient != nil {
snap := snapshot{
RecordedAt: time.Now().UTC(),
Location: &locationEntry{Lat: currentCity.Lat, Lon: currentCity.Lon, Name: currentCity.Name},
}
if data, ok := wCache.Get(); ok {
snap.Weather = &data
snap.Conditions = weather.Infer(data, currentCity.Lat, currentCity.Lon)
}
if err := wdClient.Append(snap); err != nil {
log.Printf("webdav append error: %v", err)
}
}
}
time.Sleep(interval)
}
}()
service.Listen(loc.get, wCache)
}