// Package station is the headset station. It loads the config, keeps one // reflector session, and exposes the state the window shows. package station import ( "context" "net" "os" "sync" "time" "gitea.neitzel.de/konrad/funknetz/internal/opus" ) // Options selects the config file and the clock used for heartbeats and retries. type Options struct { ConfigPath string Clock Clock } // State is what the window shows. type State struct { Connection string `json:"connection"` Detail string `json:"detail"` Callsign string `json:"callsign"` TalkGroup int `json:"talkGroup"` Protocol string `json:"protocol"` Talker string `json:"talker"` Transmitting bool `json:"transmitting"` } // Station is the seam tests and the window use. type Station struct { cfg loaded clock Clock mu sync.Mutex state State wake chan struct{} buttonHeld bool spaceHeld bool fatal string retryReason string protocol string remoteTalker string roomBusy bool audioHeard bool lastAudio time.Time flushWaiting bool flushAt time.Time rxSeq uint16 secure bool udpWait bool ivPrefix []byte udpKey []byte rxCounter uint32 txCounter uint32 mic []int16 play []int16 clientID uint16 seq uint16 udp *net.UDPConn udpPeer *net.UDPAddr lastUDPSend time.Time enc *opus.Encoder dec *opus.Decoder logFile *os.File logPath string logNotice string up bool outage bool sending bool sendStart time.Time talkCall string talkStart time.Time } // New loads the config at opt.ConfigPath. A missing file is created mode 0600 // and New returns an error so the caller can exit before opening a window. func New(opt Options) (*Station, error) { cfg, err := loadConfig(opt.ConfigPath) if err != nil { return nil, err } clock := opt.Clock if clock == nil { clock = wallClock{} } detail := "Idle" if cfg.certFatal != "" { detail = cfg.certFatal } else if cfg.logNotice != "" { detail = cfg.logNotice } s := &Station{ cfg: cfg, clock: clock, wake: make(chan struct{}, 1), logPath: cfg.logPath, logNotice: cfg.logNotice, fatal: cfg.certFatal, state: State{ Connection: "Disconnected", Detail: detail, Callsign: cfg.callsign, TalkGroup: cfg.talkGroup, }, } if cfg.enableLogging { s.openLog() } return s, nil } // State returns the latest window state. func (s *Station) State() State { s.mu.Lock() defer s.mu.Unlock() return s.state } // Wait blocks until pred is true or ctx ends. func (s *Station) Wait(ctx context.Context, pred func(State) bool) (State, error) { s.mu.Lock() defer s.mu.Unlock() for !pred(s.state) { s.mu.Unlock() select { case <-ctx.Done(): s.mu.Lock() return State{}, ctx.Err() case <-s.wake: s.mu.Lock() } } return s.state, nil } // HoldButton reports that the on-screen button is held. func (s *Station) HoldButton() { s.setButton(true) } // ReleaseButton reports that the on-screen button was released. func (s *Station) ReleaseButton() { s.setButton(false) } // HoldSpace reports that Space is held. func (s *Station) HoldSpace() { s.setSpace(true) } // ReleaseSpace reports that Space was released. func (s *Station) ReleaseSpace() { s.setSpace(false) } func (s *Station) setButton(held bool) { s.mu.Lock() defer s.mu.Unlock() was := s.transmitting() s.buttonHeld = held s.afterHold(was) } func (s *Station) setSpace(held bool) { s.mu.Lock() defer s.mu.Unlock() was := s.transmitting() s.spaceHeld = held s.afterHold(was) } func (s *Station) afterHold(was bool) { if was && !s.transmitting() { s.endTransmit() } s.publish() } // WriteMicrophone accepts 16 kHz mono samples. Samples are sent only while // this station is allowed to talk; otherwise they are discarded. func (s *Station) WriteMicrophone(pcm []int16) { s.mu.Lock() defer s.mu.Unlock() if !s.transmitting() || s.enc == nil || s.udp == nil { s.mic = nil return } s.mic = append(s.mic, pcm...) for len(s.mic) >= opus.FrameSamples { frame := append([]int16(nil), s.mic[:opus.FrameSamples]...) s.mic = append([]int16(nil), s.mic[opus.FrameSamples:]...) pkt, err := s.enc.Encode(frame) if err != nil { continue } // A lost datagram ends the session when the receive timer expires. _ = s.sendUDP(udpAudio, appendBlob(nil, pkt)) } } // TakePlayback returns PCM queued for the speaker since the last take. func (s *Station) TakePlayback() []int16 { s.mu.Lock() defer s.mu.Unlock() if s.transmitting() { s.play = nil return nil } out := s.play s.play = nil return out } // WaitPlayback waits until at least n samples are queued, then returns those samples. func (s *Station) WaitPlayback(ctx context.Context, n int) ([]int16, error) { s.mu.Lock() defer s.mu.Unlock() for len(s.play) < n { s.mu.Unlock() select { case <-ctx.Done(): s.mu.Lock() return nil, ctx.Err() case <-s.wake: s.mu.Lock() } } out := append([]int16(nil), s.play[:n]...) s.play = append([]int16(nil), s.play[n:]...) return out, nil }