Files
OpsLog/app_tci_audio.go
T
rouggy 848ce68ec5 feat(tci): key the radio ourselves and push a tone, to see what it wants
A first real transmission settled one question and raised a better one.
With the receive stream open and six seconds of transmit, the radio sent
282 frames of receive audio and NOTHING else: no chrono, no transmit
audio. So the chrono the documentation describes is not offered to a
client that merely happens to be connected while the operator keys the
microphone, and waiting for it to appear is waiting for nothing.

The reading that fits is that the radio asks for audio when the
transmission is the CLIENT'S and takes the microphone when it is the
operator's — which makes the experiment obvious. Key it from here, push a
1 kHz tone, and watch. Chrono frames appearing gives their size and
cadence by measurement instead of by guesswork; no chrono but a tone on
the meter is just as useful, because then the pacing is optional and the
voice keyer can push frames at the rate the stream already runs at.

A tone rather than silence so the answer shows on the power meter and not
only in the log.

It transmits, so: an explicit button inside a warning box, five seconds,
capped at ten, and every path out unkeys — including the panic that has
not happened yet and a socket that dies mid-tone. A transmitter left keyed
by a defect is the one fault here that would reach somebody else's band.

Writes are now serialised too. send() held the lock only long enough to
read the connection, which was enough while every command came from the
poll loop; a stream of audio frames from a second goroutine is not, and
gorilla panics on a concurrent write rather than failing quietly.
2026-08-25 23:18:11 +02:00

81 lines
2.9 KiB
Go

package main
// TCI receive audio — bindings.
//
// A SunSDR carries its receive audio on the same WebSocket as its commands, so
// OpsLog can take it directly instead of asking the operator to install a
// virtual audio cable and wire ExpertSDR's output into it. This is the first
// half: RECEIVE only, which is what the QSO recorder and the CW decoder need.
// Transmit audio (the voice keyer) is the other half and is not here yet — it
// has to answer the radio's chrono packets at the right pace, and that is worth
// doing once the receive side has proved the format on a real radio.
import (
"fmt"
"hamlog/internal/applog"
"hamlog/internal/cat"
)
// StartTCIAudio opens the receive-audio stream for one receiver.
//
// rate 0 means 48 kHz, which is what ExpertSDR streams by default and what the
// recorder wants anyway.
func (a *App) StartTCIAudio(rx, rate int) error {
if a.cat == nil {
return fmt.Errorf("CAT not initialized")
}
applog.Printf("tci: opening the receive-audio stream (rx %d, %d Hz)", rx, rate)
return a.cat.TCIAudioDo(func(t cat.TCIAudioController) error { return t.StartTCIAudio(rx, rate) })
}
// StopTCIAudio closes it.
func (a *App) StopTCIAudio() error {
if a.cat == nil {
return fmt.Errorf("CAT not initialized")
}
applog.Printf("tci: closing the receive-audio stream")
return a.cat.TCIAudioDo(func(t cat.TCIAudioController) error { return t.StopTCIAudio() })
}
// GetTCIAudioStatus reports what is arriving: the sample rate the radio chose,
// how much has come in, and the peak level of the last second.
//
// The level is the point. "The stream is open" and "audio is arriving" are
// different claims, and only the second one is worth anything to someone
// testing this on a radio for the first time.
func (a *App) GetTCIAudioStatus() cat.TCIAudioStatus {
if a.cat == nil {
return cat.TCIAudioStatus{}
}
st, _ := a.cat.TCIAudioState()
return st
}
// ProbeTCITransmit keys the radio and pushes a tone over TCI, to find out
// whether the transmit half of the stream works at all.
//
// A first real transmission proved that the radio sends nothing extra while the
// OPERATOR keys it — 282 receive frames and no chrono over six seconds — so the
// only way forward is to key it from here and watch. Everything interesting
// lands in the log; see internal/cat/tci_tx_probe.go for what the two possible
// answers mean.
//
// THIS TRANSMITS. The button that calls it says so, and the radio must be on a
// dummy load.
func (a *App) ProbeTCITransmit(seconds int) error {
if a.cat == nil {
return fmt.Errorf("CAT not initialized")
}
return a.cat.TCIAudioDo(func(t cat.TCIAudioController) error {
p, ok := t.(interface {
ProbeTXStream(seconds int, toneHz float64) error
})
if !ok {
return fmt.Errorf("this CAT backend is not a TCI radio")
}
applog.Printf("tci: TX PROBE requested (%d s) — the radio should be on a dummy load", seconds)
return p.ProbeTXStream(seconds, 1000)
})
}