fix(tci): send the tone near full scale, and log both levels

The transmit path works: six passes, the radio asked 231 times and was
answered 231 times, none missed, and the tone was there on the panadapter.

What was missing was power on the meter, and the cause was the level. The
tone went out at a quarter of full scale, out of caution, into a radio set
to 15% drive — enough to draw a clean signal and not enough to move a
needle. In a digital mode the radio expects a line level it can drive to
full output; the POWER is its own drive control, so sending quietly only
wastes the range. Now 0.7, short of 1.0 to leave room for the peaks.

The start line carries both numbers — ours and the radio's drive — because
a quiet transmission has two possible causes and one line should settle
which, rather than an evening of guessing. Reading 'drive' off the radio
is the only reason it is parsed at all.
This commit is contained in:
2026-08-25 23:35:08 +02:00
parent c6294d9eb3
commit 592dd08835
2 changed files with 25 additions and 6 deletions
+11 -1
View File
@@ -66,7 +66,11 @@ type TCI struct {
// txAllowedKnown keeps an OLDER ExpertSDR, or a TCI-compatible program that
// never sends TX_ENABLE at all, from being treated as refusing: without a
// word from the radio we key and let it decide.
txAllowed bool
txAllowed bool
// drive is the radio's transmit drive, 0-100. Kept because a quiet
// transmission has two possible causes — our level or the radio's — and a
// log that names both settles it in one line instead of an evening.
drive int
txAllowedKnown bool
lastSig string // last logged state signature (log only on change)
@@ -462,6 +466,12 @@ func (t *TCI) handle(msg string) {
t.noteTXTransition(t.tx)
}
}
case "drive":
if get(0) == "0" {
if v, err := strconv.Atoi(get(1)); err == nil {
t.drive = v
}
}
case "tx_enable":
if get(0) == "0" {
allowed := get(1) == "true"
+14 -5
View File
@@ -135,6 +135,7 @@ func (t *TCI) ProbeTXStream(seconds int, toneHz float64) error {
t.mu.Lock()
allowed, known, connected := t.txAllowed, t.txAllowedKnown, t.conn != nil
mode := t.mode
drive := t.drive
t.mu.Unlock()
if !connected {
return fmt.Errorf("not connected to the radio")
@@ -161,9 +162,15 @@ func (t *TCI) ProbeTXStream(seconds int, toneHz float64) error {
// sine restarted every frame is a click 47 times a second.
phase := 0.0
step := 2 * math.Pi * toneHz / float64(rate)
// A quarter of full scale: enough to read on a meter, short of the level
// where the radio's own processing starts deciding things for us.
const amp = 0.25
// Near full scale.
//
// A quarter was the first choice, out of caution, and the first real test
// showed exactly what that produces: a clean signal on the panadapter and a
// wattmeter that never moves. In a digital mode the radio expects a line
// level it can drive to full output — the POWER is set by its own drive
// control, not by how loud we send — so sending quietly just wastes the
// range. Short of 1.0 to leave room for the sine's peaks.
const amp = 0.7
const chans = 2
le := binary.LittleEndian
t.setTXFeed(func(samples int) []byte {
@@ -182,8 +189,10 @@ func (t *TCI) ProbeTXStream(seconds int, toneHz float64) error {
})
defer t.setTXFeed(nil)
debugLog.Printf("TCI: TX PROBE starting — %d s of a %.0f Hz tone answered to the radio's own requests, mode %s, INTO A DUMMY LOAD",
seconds, toneHz, mode)
// The drive is in the line because it is half of "how much power came out".
// A tone at full scale into a drive of 15 is still 15% of the radio.
debugLog.Printf("TCI: TX PROBE starting — %d s of a %.0f Hz tone at %.0f%% of full scale, answered to the radio's own requests, mode %s, radio drive %d%%, INTO A DUMMY LOAD",
seconds, toneHz, amp*100, mode, drive)
if err := t.SetPTT(true); err != nil {
return fmt.Errorf("could not key the radio: %w", err)