samples

package
v0.18.76 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: May 15, 2026 License: MIT Imports: 8 Imported by: 0

Documentation

Overview

Package samples persists raw vehicle_state polling snapshots.

Index

Constants

This section is empty.

Variables

This section is empty.

Functions

This section is empty.

Types

type Factory

type Factory struct {
	// contains filtered or unexported fields
}

Factory hands out per-user *Store instances. See drives.Factory for the rationale.

func NewFactory

func NewFactory(d *sql.DB, resolvers *db.VehicleResolverFactory) *Factory

NewFactory wraps the shared pool + resolver factory.

func (*Factory) For

func (f *Factory) For(userID uuid.UUID) *Store

For returns the *Store for userID, constructing one on first sight.

type PartitionJanitor

type PartitionJanitor struct {
	// contains filtered or unexported fields
}

PartitionJanitor keeps `vehicle_state` ahead of the clock by at least `lookaheadMonths` partitions. It does this by calling the `rivolt_ensure_vehicle_state_partition(ts)` SQL helper once per target month.

func NewPartitionJanitor

func NewPartitionJanitor(db *sql.DB) *PartitionJanitor

NewPartitionJanitor wires a janitor with sensible defaults. 3 months of lookahead is enough that a pod that runs continuously for a month still never writes into an unpartitioned range, and a pod that restarts hourly (deploys, node drains) has slack for clock skew. The interval is also 1 hour — cheap, boring, and avoids the "I deployed at 23:59 UTC on the last of the month and the janitor fired at 23:30" edge.

func (*PartitionJanitor) EnsureLookahead

func (j *PartitionJanitor) EnsureLookahead(ctx context.Context) error

EnsureLookahead creates the current partition plus `lookaheadMonths` future partitions if they don't already exist. Idempotent; safe to call repeatedly. Returns the first error encountered but always attempts every month.

func (*PartitionJanitor) Run

func (j *PartitionJanitor) Run(ctx context.Context)

Run blocks until ctx is cancelled, calling EnsureLookahead on an `interval` ticker. Intended to be launched as a dedicated goroutine at startup. Errors are logged, never returned — the next tick will retry.

type Sample

type Sample struct {
	VehicleID       string
	At              time.Time
	BatteryLevelPct float64
	RangeMi         float64
	OdometerMi      float64
	Lat, Lon        float64
	// LocationFixAt is the timestamp Rivian's GNSS module reported on
	// the (Lat, Lon) fix itself, NOT the wall-clock time the recorder
	// observed it. Differs from At when the modem loses sky view and
	// keeps replaying the last known fix — see migration 0013 for
	// the field rationale and the Fort Stockton incident that
	// motivated it. Pointer (rather than time.Time + omitempty)
	// because Go's encoding/json doesn't honor omitempty on struct
	// types: a zero time.Time would serialize as "0001-01-01T00:00:00Z"
	// and the frontend would compute a 2000-year fix age. nil
	// renders as "fix age unknown" upstream.
	LocationFixAt  *time.Time `json:",omitempty"`
	SpeedMph       float64
	ShiftState     string
	DriveMode      string `json:"drive_mode,omitempty"` // "everyday", "sport", "all-terrain", etc.; nullable in DB for legacy samples
	ChargingState  string
	ChargerPowerKW float64
	ChargeLimitPct float64
	InsideTempC    float64
	OutsideTempC   float64
	DriveNumber    int64
	ChargeNumber   int64
	Source         string // "live" | "electrafi_import"
	// AltitudeM is the elevation above sea level (meters) at
	// (Lat, Lon), looked up by the live recorder against the
	// Mapzen Terrarium DEM at sample-write time. Pointer because
	// it's nullable: NULL on legacy rows written before migration
	// 0019, on samples without a GPS fix, on ElectraFi imports,
	// and on cold-cache misses where the tile fetch was kicked
	// off async (the next sample on that tile will hit the cache).
	AltitudeM *float64 `json:"altitude_m,omitempty"`
	// TirePressureMinBar is the minimum of the four corner pressures
	// at sample time, in bar. Pointer because nullable: NULL on
	// legacy rows pre-migration 0024, ElectraFi imports, and live
	// rows where Rivian reported zero on all four corners (sensor
	// outage / vehicle parked w/ no recent reading). The efficiency
	// analyzer pulls the median across a drive's samples; min-of-4
	// keeps the worst tire (the one that actually hurts efficiency)
	// dominant without bloating the schema with all four corners.
	TirePressureMinBar *float64 `json:"tire_pressure_min_bar,omitempty"`
}

Sample is a single polling snapshot. VehicleID is the Rivian gateway string id ("01-242521064"); the store resolves it to the internal vehicles.id UUID on write.

type Store

type Store struct {
	// contains filtered or unexported fields
}

Store wraps the vehicle_state table, scoped to one user.

func OpenStore

func OpenStore(d *sql.DB, userID uuid.UUID, v *db.VehicleResolver) (*Store, error)

OpenStore binds a pooled connection to a user + vehicle resolver.

func (*Store) Close

func (s *Store) Close() error

Close is a no-op; the pool is managed by main.

func (*Store) Count

func (s *Store) Count(ctx context.Context) (int, error)

Count returns the total number of samples for this user.

func (*Store) InsertBatch

func (s *Store) InsertBatch(ctx context.Context, batch []Sample) error

InsertBatch inserts many samples in a single transaction. Duplicate (vehicle_id, at) tuples are ignored so re-imports are idempotent. All samples must belong to the same user.

func (*Store) ListAll

func (s *Store) ListAll(ctx context.Context) ([]Sample, error)

ListAll returns every sample for this user, oldest first. Used by the Settings → Backup endpoint. Unbounded — a year of 60 s polls is on the order of 500k rows, ~100 MB JSON; acceptable for a full backup but NOT suitable for UI consumption.

func (*Store) ListBetween

func (s *Store) ListBetween(ctx context.Context, since, until time.Time, limit int) ([]Sample, error)

ListBetween returns samples in (since, until], oldest first. Zero `until` means "no upper bound" (back-compat with ListSince). The upper bound matters for the drive detail page: without it, opening any drive ages back pulls every sample from the drive's start through now, which can be days of charging+parked rows.

func (*Store) ListSince

func (s *Store) ListSince(ctx context.Context, since time.Time, limit int) ([]Sample, error)

ListSince returns samples newer than since, up to limit, oldest first.

Limit semantics:

  • <= 0 falls back to a reasonable default (1000).
  • Anything above the hard cap is clamped DOWN to the cap, not reset to the default. The previous "if limit > 10000 { limit = 1000 }" branch was a foot-gun: the UI asks for 20k samples so a 90-minute drive (~1.7k rows at the WS cadence) returns fully, but the store silently truncated to 1k → the route polyline ended halfway through the trip and the user reported "no GPS coordinates for the end of the route".

func (*Store) Reset

func (s *Store) Reset(ctx context.Context) (int64, error)

Reset deletes every raw sample (vehicle_state row) for this store's user. See drives.Store.Reset.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL