From 1704d8a833c0db5f0f42279f1e23f8c53b152936 Mon Sep 17 00:00:00 2001 From: Hermes Date: Fri, 22 May 2026 22:31:54 -0400 Subject: [PATCH] CUB-228: add battery_calibration_offset to cameras table - Add column to 001_create_tables.sql for fresh databases - Add migration 002 for existing databases (idempotent via pragma_table_info check) - Implement runIncrementalMigrations in db.go - Add BatteryCalibrationOffset to Camera model - Update all camera SELECT queries (cameras List, detail, MQTT subscriber getCamera, register) --- internal/api/cameras.go | 4 +-- internal/db/db.go | 28 +++++++++++++++++++ internal/db/migrations/001_create_tables.sql | 11 ++++---- .../migrations/002_add_camera_calibration.sql | 5 ++++ internal/mqtt/subscriber.go | 4 +-- pkg/models/camera.go | 11 ++++---- 6 files changed, 49 insertions(+), 14 deletions(-) create mode 100644 internal/db/migrations/002_add_camera_calibration.sql diff --git a/internal/api/cameras.go b/internal/api/cameras.go index f8d987b..7973592 100644 --- a/internal/api/cameras.go +++ b/internal/api/cameras.go @@ -132,10 +132,10 @@ func GetCameraDetail(database *db.DB) http.HandlerFunc { // Get camera info var c models.Camera err := database.QueryRowContext(r.Context(), ` - SELECT camera_id, friendly_name, mac_address, created_at, updated_at + SELECT camera_id, friendly_name, mac_address, battery_calibration_offset, created_at, updated_at FROM cameras WHERE camera_id = ? `, cameraID).Scan( - &c.CameraID, &c.FriendlyName, &c.MacAddress, + &c.CameraID, &c.FriendlyName, &c.MacAddress, &c.BatteryCalibrationOffset, &c.CreatedAt, &c.UpdatedAt, ) if err == sql.ErrNoRows { diff --git a/internal/db/db.go b/internal/db/db.go index 1af91c0..e7f2007 100644 --- a/internal/db/db.go +++ b/internal/db/db.go @@ -14,6 +14,9 @@ import ( //go:embed migrations/001_create_tables.sql var migration001 string +//go:embed migrations/002_add_camera_calibration.sql +var migration002 string + // DB wraps the sql.DB with connection-level settings. type DB struct { *sql.DB @@ -62,6 +65,12 @@ func Open(path string) (*DB, error) { return nil, err } log.Println("Migrations complete") + } else { + // Run incremental migrations on existing databases + if err := runIncrementalMigrations(db); err != nil { + db.Close() + return nil, err + } } return &DB{db}, nil @@ -83,6 +92,25 @@ func migrate(db *sql.DB, sql string) error { return nil } +// runIncrementalMigrations applies migrations that haven't been run yet on +// an existing database (one where the 001 schema already exists). +func runIncrementalMigrations(db *sql.DB) error { + // Migration 002: add battery_calibration_offset if it doesn't exist + var colCount int + err := db.QueryRow(`SELECT COUNT(*) FROM pragma_table_info('cameras') WHERE name = 'battery_calibration_offset'`).Scan(&colCount) + if err != nil { + return err + } + if colCount == 0 { + log.Println("Running migration 002: add battery_calibration_offset") + if err := migrate(db, migration002); err != nil { + return err + } + } + + return nil +} + // splitSQL splits a SQL string on semicolons, respecting quoted strings. func splitSQL(sql string) []string { var stmts []string diff --git a/internal/db/migrations/001_create_tables.sql b/internal/db/migrations/001_create_tables.sql index d0f29cb..ab44ae2 100644 --- a/internal/db/migrations/001_create_tables.sql +++ b/internal/db/migrations/001_create_tables.sql @@ -3,11 +3,12 @@ -- Cameras table: registry of all GoPro cameras CREATE TABLE IF NOT EXISTS cameras ( - camera_id TEXT PRIMARY KEY, - friendly_name TEXT NOT NULL, - mac_address TEXT UNIQUE, - created_at DATETIME NOT NULL DEFAULT (datetime('now')), - updated_at DATETIME NOT NULL DEFAULT (datetime('now')) + camera_id TEXT PRIMARY KEY, + friendly_name TEXT NOT NULL, + mac_address TEXT UNIQUE, + battery_calibration_offset REAL, + created_at DATETIME NOT NULL DEFAULT (datetime('now')), + updated_at DATETIME NOT NULL DEFAULT (datetime('now')) ); CREATE INDEX IF NOT EXISTS idx_cameras_mac ON cameras(mac_address); diff --git a/internal/db/migrations/002_add_camera_calibration.sql b/internal/db/migrations/002_add_camera_calibration.sql new file mode 100644 index 0000000..59490e2 --- /dev/null +++ b/internal/db/migrations/002_add_camera_calibration.sql @@ -0,0 +1,5 @@ +-- Migration 002: Add battery_calibration_offset to cameras table +-- This column stores a per-camera calibration value for converting raw battery +-- readings (e.g. GoPro Hero 3 byte at offset 57) into percentage values. + +ALTER TABLE cameras ADD COLUMN battery_calibration_offset REAL; diff --git a/internal/mqtt/subscriber.go b/internal/mqtt/subscriber.go index 4ea0480..87655f3 100644 --- a/internal/mqtt/subscriber.go +++ b/internal/mqtt/subscriber.go @@ -404,9 +404,9 @@ func extractCameraID(topic string) string { func getCamera(db *db.DB, cameraID string) (models.Camera, error) { var cam models.Camera err := db.QueryRow( - "SELECT camera_id, friendly_name, COALESCE(mac_address,''), created_at, updated_at FROM cameras WHERE camera_id = ?", + "SELECT camera_id, friendly_name, COALESCE(mac_address,''), COALESCE(battery_calibration_offset, NULL), created_at, updated_at FROM cameras WHERE camera_id = ?", cameraID, - ).Scan(&cam.CameraID, &cam.FriendlyName, &cam.MacAddress, &cam.CreatedAt, &cam.UpdatedAt) + ).Scan(&cam.CameraID, &cam.FriendlyName, &cam.MacAddress, &cam.BatteryCalibrationOffset, &cam.CreatedAt, &cam.UpdatedAt) return cam, err } diff --git a/pkg/models/camera.go b/pkg/models/camera.go index e87f109..92dddbe 100644 --- a/pkg/models/camera.go +++ b/pkg/models/camera.go @@ -7,11 +7,12 @@ import ( // Camera represents a registered GoPro camera in the system. type Camera struct { - CameraID string `json:"camera_id"` - FriendlyName string `json:"friendly_name"` - MacAddress string `json:"mac_address,omitempty"` - CreatedAt time.Time `json:"created_at"` - UpdatedAt time.Time `json:"updated_at"` + CameraID string `json:"camera_id"` + FriendlyName string `json:"friendly_name"` + MacAddress string `json:"mac_address,omitempty"` + BatteryCalibrationOffset *float64 `json:"battery_calibration_offset,omitempty"` + CreatedAt time.Time `json:"created_at"` + UpdatedAt time.Time `json:"updated_at"` } // StatusLog records a single status poll from an ESP8266 node. -- 2.53.0