Move lesson source into the teaching structure, align the course path with the learner's Go and DevOps experience, and add the interactive lesson guides. Add advanced DevOps lifecycle study as a separate section. Bound MQTT message work and drain accepted jobs during shutdown; align the backend implementation with the revised course through Section 6.
73 lines
1.6 KiB
Go
73 lines
1.6 KiB
Go
package deviceview
|
|
|
|
import (
|
|
"fmt"
|
|
"sync"
|
|
"time"
|
|
|
|
"example.com/mqtt5-learning/internal/contracts"
|
|
)
|
|
|
|
type Snapshot struct {
|
|
DeviceID string
|
|
MessageID string
|
|
ReportedAt time.Time
|
|
ReceivedAt time.Time
|
|
Temperature float64
|
|
TemperatureSet bool
|
|
}
|
|
|
|
type Status struct {
|
|
Snapshot Snapshot
|
|
HeartbeatFresh bool
|
|
}
|
|
|
|
type Store struct {
|
|
mu sync.RWMutex
|
|
latest map[string]Snapshot
|
|
}
|
|
|
|
func NewStore() *Store {
|
|
return &Store{latest: make(map[string]Snapshot)}
|
|
}
|
|
|
|
func (s *Store) RecordHeartbeat(message contracts.Heartbeat, receivedAt time.Time) (bool, error) {
|
|
reportedAt, err := time.Parse(time.RFC3339, message.OccurredAt)
|
|
if err != nil {
|
|
// Decode already validates this timestamp. Keep the store defensive too.
|
|
return false, fmt.Errorf("parse heartbeat timestamp: %w", err)
|
|
}
|
|
|
|
snapshot := Snapshot{
|
|
DeviceID: message.DeviceID,
|
|
MessageID: message.MessageID,
|
|
ReportedAt: reportedAt,
|
|
ReceivedAt: receivedAt,
|
|
}
|
|
if message.Temperature != nil {
|
|
snapshot.Temperature = *message.Temperature
|
|
snapshot.TemperatureSet = true
|
|
}
|
|
|
|
s.mu.Lock()
|
|
defer s.mu.Unlock()
|
|
previous, exists := s.latest[message.DeviceID]
|
|
if exists && !reportedAt.After(previous.ReportedAt) {
|
|
return false, nil
|
|
}
|
|
s.latest[message.DeviceID] = snapshot
|
|
return true, nil
|
|
}
|
|
|
|
func (s *Store) GetStatus(deviceID string, now time.Time, freshFor time.Duration) (Status, bool) {
|
|
s.mu.RLock()
|
|
snapshot, exists := s.latest[deviceID]
|
|
s.mu.RUnlock()
|
|
if !exists {
|
|
return Status{}, false
|
|
}
|
|
|
|
fresh := freshFor > 0 && !now.Before(snapshot.ReceivedAt) && now.Sub(snapshot.ReceivedAt) <= freshFor
|
|
return Status{Snapshot: snapshot, HeartbeatFresh: fresh}, true
|
|
}
|