Files

136 lines
3.4 KiB
Go

// Package frames holds received frames and the devices aggregated from them.
package frames
import (
"sort"
"sync"
"time"
"github.com/skinnyrad/Lora-Scanner/internal/hub"
)
// DefaultMax is how many frames the ring keeps.
const DefaultMax = 10000
const (
KindLoRa = "lora" // packet decoded by a Feather
KindLoRaWAN = "lorawan" // row scraped from a gateway
KindRaw = "raw" // any serial line
)
type Frame struct {
ID uint64 `json:"id"`
Time time.Time `json:"time"`
Kind string `json:"kind"`
Source string `json:"source"`
// Transceiver is the ID of the attached Feather that produced the frame (0 for gateway frames).
Transceiver int `json:"transceiver"`
Identifier string `json:"identifier"`
Freq float64 `json:"freq"`
RSSI *int `json:"rssi"`
Info string `json:"info"`
Raw string `json:"raw"`
}
type Device struct {
Identifier string `json:"identifier"`
Freq float64 `json:"freq"`
RSSI *int `json:"rssi"`
FirstSeen time.Time `json:"firstSeen"`
LastSeen time.Time `json:"lastSeen"`
Frames int `json:"frames"`
}
// Messages broadcast on the hub.
type FrameMsg struct {
Type string `json:"type"` // "frame"
Frame Frame `json:"frame"`
}
type DeviceMsg struct {
Type string `json:"type"` // "device"
Device Device `json:"device"`
}
type Store struct {
mu sync.RWMutex
hub *hub.Hub
max int
nextID uint64
frames []Frame
devices map[string]*Device
}
// NewStore keeps at most max frames (DefaultMax if <= 0). hub may be nil.
func NewStore(h *hub.Hub, max int) *Store {
if max <= 0 {
max = DefaultMax
}
return &Store{hub: h, max: max, nextID: 1, devices: map[string]*Device{}}
}
// Add assigns the ID, stores the frame, updates its device (non-raw only)
// and publishes both to the hub. A zero Time is set to now.
func (s *Store) Add(f Frame) Frame {
if f.Time.IsZero() {
f.Time = time.Now()
}
s.mu.Lock()
f.ID = s.nextID
s.nextID++
s.frames = append(s.frames, f)
if len(s.frames) > s.max {
s.frames = append([]Frame(nil), s.frames[len(s.frames)-s.max:]...)
}
var dev *Device
if f.Kind != KindRaw && f.Identifier != "" {
d, ok := s.devices[f.Identifier]
if !ok {
d = &Device{Identifier: f.Identifier, FirstSeen: f.Time}
s.devices[f.Identifier] = d
}
d.Freq, d.RSSI, d.LastSeen = f.Freq, f.RSSI, f.Time
d.Frames++
cp := *d
dev = &cp
}
s.mu.Unlock()
if s.hub != nil {
s.hub.Broadcast(FrameMsg{Type: "frame", Frame: f})
if dev != nil {
s.hub.Broadcast(DeviceMsg{Type: "device", Device: *dev})
}
}
return f
}
// Since returns frames with ID > after, oldest first.
func (s *Store) Since(after uint64) []Frame {
s.mu.RLock()
defer s.mu.RUnlock()
i := sort.Search(len(s.frames), func(i int) bool { return s.frames[i].ID > after })
return append([]Frame{}, s.frames[i:]...)
}
// Devices returns a copy sorted by identifier.
func (s *Store) Devices() []Device {
s.mu.RLock()
defer s.mu.RUnlock()
out := make([]Device, 0, len(s.devices))
for _, d := range s.devices {
out = append(out, *d)
}
sort.Slice(out, func(i, j int) bool { return out[i].Identifier < out[j].Identifier })
return out
}
// Clear drops all frames and devices. IDs keep increasing so clients that
// resume with ?after= never see a repeated ID.
func (s *Store) Clear() {
s.mu.Lock()
s.frames = nil
s.devices = map[string]*Device{}
s.mu.Unlock()
}