package feichibridge import ( "crypto/sha256" "encoding/hex" "encoding/json" "errors" "fmt" "os" "path/filepath" "sync" "time" ) type VehicleState struct { LastRealtimeTime time.Time `json:"last_realtime_time,omitempty"` LastRealtimeHash string `json:"last_realtime_hash,omitempty"` LastRealtimeACKAt time.Time `json:"last_realtime_ack_at,omitempty"` LastSnapshotReissueTime time.Time `json:"last_snapshot_reissue_time,omitempty"` LastSnapshotReissueHash string `json:"last_snapshot_reissue_hash,omitempty"` LastSnapshotReissueACKAt time.Time `json:"last_snapshot_reissue_ack_at,omitempty"` BackfillCursor time.Time `json:"backfill_cursor,omitempty"` LastBackfillACKAt time.Time `json:"last_backfill_ack_at,omitempty"` } func (s *StateStore) CommitSnapshotReissue(vin string, at time.Time, hash string) error { s.mu.Lock() defer s.mu.Unlock() current := s.state.Vehicles[vin] current.LastSnapshotReissueTime = at current.LastSnapshotReissueHash = hash current.LastSnapshotReissueACKAt = time.Now() s.state.Vehicles[vin] = current return s.saveLocked() } type persistentState struct { Version int `json:"version"` PlatformSerial uint16 `json:"platform_serial"` Vehicles map[string]VehicleState `json:"vehicles"` } type StateStore struct { mu sync.Mutex path string state persistentState } func OpenStateStore(path string) (*StateStore, error) { store := &StateStore{ path: path, state: persistentState{ Version: 1, Vehicles: map[string]VehicleState{}, }, } encoded, err := os.ReadFile(path) if errors.Is(err, os.ErrNotExist) { return store, nil } if err != nil { return nil, fmt.Errorf("read bridge state: %w", err) } if err := json.Unmarshal(encoded, &store.state); err != nil { return nil, fmt.Errorf("decode bridge state: %w", err) } if store.state.Vehicles == nil { store.state.Vehicles = map[string]VehicleState{} } return store, nil } func (s *StateStore) Vehicle(vin string) VehicleState { s.mu.Lock() defer s.mu.Unlock() return s.state.Vehicles[vin] } func (s *StateStore) CommitRealtime(vin string, at time.Time, hash string) error { s.mu.Lock() defer s.mu.Unlock() current := s.state.Vehicles[vin] current.LastRealtimeTime = at current.LastRealtimeHash = hash current.LastRealtimeACKAt = time.Now() s.state.Vehicles[vin] = current return s.saveLocked() } func (s *StateStore) CommitBackfill(vin string, at time.Time) error { s.mu.Lock() defer s.mu.Unlock() current := s.state.Vehicles[vin] current.BackfillCursor = at current.LastBackfillACKAt = time.Now() s.state.Vehicles[vin] = current return s.saveLocked() } func (s *StateStore) AdvanceBackfillCursor(vin string, at time.Time) error { s.mu.Lock() defer s.mu.Unlock() current := s.state.Vehicles[vin] current.BackfillCursor = at s.state.Vehicles[vin] = current return s.saveLocked() } func (s *StateStore) NextPlatformSerial() (uint16, error) { s.mu.Lock() defer s.mu.Unlock() s.state.PlatformSerial++ if s.state.PlatformSerial == 0 { s.state.PlatformSerial = 1 } if err := s.saveLocked(); err != nil { return 0, err } return s.state.PlatformSerial, nil } func (s *StateStore) saveLocked() error { if err := os.MkdirAll(filepath.Dir(s.path), 0o750); err != nil { return fmt.Errorf("create bridge state directory: %w", err) } encoded, err := json.MarshalIndent(s.state, "", " ") if err != nil { return err } temp, err := os.CreateTemp(filepath.Dir(s.path), ".state-*.json") if err != nil { return err } tempName := temp.Name() defer os.Remove(tempName) if err := temp.Chmod(0o600); err != nil { temp.Close() return err } if _, err := temp.Write(encoded); err != nil { temp.Close() return err } if err := temp.Sync(); err != nil { temp.Close() return err } if err := temp.Close(); err != nil { return err } if err := os.Rename(tempName, s.path); err != nil { return fmt.Errorf("replace bridge state: %w", err) } return nil } func hashBytes(value []byte) string { sum := sha256.Sum256(value) return hex.EncodeToString(sum[:]) }