Files

276 lines
6.4 KiB
Go

package serial
import (
"bufio"
"errors"
"fmt"
"io"
"path/filepath"
"sort"
"strings"
"sync"
"time"
"github.com/skinnyrad/Lora-Scanner/internal/beacon"
"github.com/skinnyrad/Lora-Scanner/internal/frames"
"github.com/skinnyrad/Lora-Scanner/internal/hub"
gs "go.bug.st/serial"
"go.bug.st/serial/enumerator"
)
const BaudRate = 9600
var (
ErrNotFound = errors.New("transceiver not found")
ErrPortInUse = errors.New("port already in use")
)
// Opener opens a serial device. It is a field so tests can inject a fake.
type Opener func(name string) (io.ReadWriteCloser, error)
func defaultOpener(name string) (io.ReadWriteCloser, error) {
return gs.Open(name, &gs.Mode{BaudRate: BaudRate})
}
// Transceiver is an attached Feather. Freq (MHz) is nil until detected from
// the firmware's boot banner.
type Transceiver struct {
ID int `json:"id"`
Port string `json:"port"`
Freq *float64 `json:"freq"`
}
// ListMsg is broadcast on the hub whenever the set of transceivers (or a
// detected frequency) changes.
type ListMsg struct {
Type string `json:"type"` // "transceivers"
Transceivers []Transceiver `json:"transceivers"`
}
type slot struct {
t Transceiver
conn io.ReadWriteCloser
done chan struct{}
beacon beacon.Beacon
writeMu sync.Mutex
}
type Manager struct {
mu sync.Mutex
slots map[int]*slot
used map[string]int // port -> id
lastFreq map[string]float64 // port -> last detected frequency
nextID int
hub *hub.Hub
frames *frames.Store
open Opener
}
func NewManager(h *hub.Hub, f *frames.Store) *Manager {
return &Manager{
slots: map[int]*slot{}, used: map[string]int{}, lastFreq: map[string]float64{},
nextID: 1, hub: h, frames: f, open: defaultOpener,
}
}
// ListPorts returns the serial device paths available on this machine.
func ListPorts() ([]string, error) {
details, err := enumerator.GetDetailedPortsList()
if err != nil {
return nil, err
}
ports := make([]string, 0, len(details))
for _, d := range details {
ports = append(ports, d.Name)
}
sort.Strings(ports)
return ports, nil
}
// Add opens the port and starts reading from it.
func (m *Manager) Add(port string) (Transceiver, error) {
port = strings.TrimSpace(port)
if port == "" {
return Transceiver{}, errors.New("port is required")
}
m.mu.Lock()
defer m.mu.Unlock()
if _, busy := m.used[port]; busy {
return Transceiver{}, ErrPortInUse
}
conn, err := m.open(port)
if err != nil {
return Transceiver{}, fmt.Errorf("open %s: %w", port, err)
}
s := &slot{conn: conn, done: make(chan struct{}), t: Transceiver{ID: m.nextID, Port: port}}
m.nextID++
// The firmware only prints its frequency at boot, so a reconnect to a
// board that is already running remembers what we saw last time.
if f, ok := m.lastFreq[port]; ok {
s.t.Freq = &f
}
m.slots[s.t.ID] = s
m.used[port] = s.t.ID
go m.read(s)
m.broadcastLocked()
return s.t, nil
}
func (m *Manager) Remove(id int) error {
m.mu.Lock()
s, ok := m.slots[id]
if !ok {
m.mu.Unlock()
return ErrNotFound
}
m.removeLocked(s)
m.mu.Unlock()
s.beacon.Stop()
s.conn.Close() // unblocks the reader
<-s.done
return nil
}
// removeLocked drops the slot from the registry and tells clients.
func (m *Manager) removeLocked(s *slot) {
delete(m.slots, s.t.ID)
delete(m.used, s.t.Port)
m.broadcastLocked()
}
func (m *Manager) listLocked() []Transceiver {
out := make([]Transceiver, 0, len(m.slots))
for _, s := range m.slots {
out = append(out, s.t)
}
sort.Slice(out, func(i, j int) bool { return out[i].ID < out[j].ID })
return out
}
func (m *Manager) broadcastLocked() {
m.hub.Broadcast(ListMsg{Type: "transceivers", Transceivers: m.listLocked()})
}
func (m *Manager) List() []Transceiver {
m.mu.Lock()
defer m.mu.Unlock()
return m.listLocked()
}
func (m *Manager) get(id int) (*slot, error) {
m.mu.Lock()
defer m.mu.Unlock()
s, ok := m.slots[id]
if !ok {
return nil, ErrNotFound
}
return s, nil
}
// Transmit writes the legacy "TX:<text> " frame.
func (m *Manager) Transmit(id int, text string) error {
s, err := m.get(id)
if err != nil {
return err
}
s.writeMu.Lock()
defer s.writeMu.Unlock()
_, err = s.conn.Write([]byte("TX:" + text + " "))
return err
}
func (m *Manager) StartBeacon(id int, text string, interval time.Duration) error {
s, err := m.get(id)
if err != nil {
return err
}
s.beacon.Start(text, interval, func(t string) error { return m.Transmit(id, t) })
return nil
}
func (m *Manager) StopBeacon(id int) error {
s, err := m.get(id)
if err != nil {
return err
}
s.beacon.Stop()
return nil
}
func (m *Manager) BeaconState(id int) (beacon.State, error) {
s, err := m.get(id)
if err != nil {
return beacon.State{}, err
}
return s.beacon.State(), nil
}
// setFreq records a detected frequency and returns the slot's current label info.
func (m *Manager) setFreq(s *slot, f float64) {
m.mu.Lock()
defer m.mu.Unlock()
m.lastFreq[s.t.Port] = f
if _, live := m.slots[s.t.ID]; !live {
return
}
s.t.Freq = &f
m.broadcastLocked()
}
func (m *Manager) snapshot(s *slot) Transceiver {
m.mu.Lock()
defer m.mu.Unlock()
return s.t
}
func sourceLabel(t Transceiver) string {
name := filepath.Base(t.Port)
if t.Freq != nil {
return fmt.Sprintf("%s (%s MHz)", name, fmtFreq(*t.Freq))
}
return name
}
func fmtFreq(f float64) string {
return strings.TrimRight(strings.TrimRight(fmt.Sprintf("%.3f", f), "0"), ".")
}
func (m *Manager) read(s *slot) {
defer close(s.done)
var p Parser
sc := bufio.NewScanner(s.conn)
for sc.Scan() {
line := strings.TrimSpace(sc.Text())
if f, ok := ParseFreq(line); ok {
m.setFreq(s, f)
}
t := m.snapshot(s)
source := sourceLabel(t)
m.frames.Add(frames.Frame{Kind: frames.KindRaw, Source: source, Transceiver: t.ID, Info: line, Raw: line})
if rssi, decoded, ok := p.Feed(line); ok {
id := "Raw LoRa Device (" + filepath.Base(t.Port) + ")"
var freq float64
if t.Freq != nil {
freq = *t.Freq
id = fmt.Sprintf("Raw LoRa Device %s MHz", fmtFreq(freq))
}
m.frames.Add(frames.Frame{
Kind: frames.KindLoRa, Source: source, Transceiver: t.ID, Identifier: id, Freq: freq, RSSI: &rssi,
Info: decoded, Raw: fmt.Sprintf("RSSI: %d\nDecoded Value: %s", rssi, decoded),
})
}
}
// The device vanished (unplugged) rather than an explicit Remove: release it.
m.mu.Lock()
if m.slots[s.t.ID] == s {
m.removeLocked(s)
m.mu.Unlock()
s.beacon.Stop()
s.conn.Close()
return
}
m.mu.Unlock()
}