fix(tci): a filter button does what it says, and mute is put under a log

250 now means 0-250. It meant 575-825: the width was right and it was
centred on the CW note, on the reasoning that a CW filter should contain
the note. That reasoning may be right for a radio and it is still wrong
here, because it is not what the button says — and a button that does not
do what it says is worse than one that does something simple. The two
edges are editable underneath for anything else, which is what TCI takes
anyway.

And MUTE still lights the squelch on a real radio. Nothing in this code
can do that — the button sends mute and only mute, and the two are
separate state — so the radio's own announcements are logged as they
arrive. What it says after the command will settle whether this is our
reading or its doing; no more reasoning from here will.
This commit is contained in:
2026-08-26 20:59:31 +02:00
parent fbe01bf19d
commit dad71e9e4f
2 changed files with 28 additions and 9 deletions
+20 -9
View File
@@ -47,15 +47,16 @@ function widthLabel(w: number): string {
return w >= 1000 ? `${(w / 1000).toFixed(1)}k` : String(w);
}
// edgesFor turns a width into the pair TCI wants. Narrow filters are centred on
// the CW note: a 250 Hz filter from 100 to 350 would put the note outside its
// own passband.
function edgesFor(w: number, mode: string): { lo: number; hi: number } {
const cw = /CW/i.test(mode);
if (cw || w <= 1000) {
return { lo: Math.max(0, CW_PITCH - Math.round(w / 2)), hi: CW_PITCH + Math.round(w / 2) };
}
return { lo: 100, hi: 100 + w };
// edgesFor turns a width into the pair TCI wants: 0 to the width, and nothing
// clever.
//
// It centred narrow filters on the CW note first — 250 became 575-825 — on the
// reasoning that a CW filter should contain the note. That reasoning may even be
// right for a radio, but it is not what the button says, and a button that does
// not do what it says is worse than one that does something simple. 250 means
// 0-250. The two edges are editable underneath for anything else.
function edgesFor(w: number, _mode: string): { lo: number; hi: number } {
return { lo: 0, hi: w };
}
// dBm → S units. TCI reports a real signal level rather than a meter position,
@@ -311,6 +312,16 @@ export function TCIPanel({ onReportRST }: { onReportRST?: (rst: string) => void
<span className="text-[10px] font-bold uppercase tracking-wider text-muted-foreground">{t('tcip.filter')}</span>
<span className="text-xs font-mono tabular-nums">{st.filter_lo}{st.filter_hi} Hz</span>
</div>
<div className="flex items-center gap-2 pb-1">
<LevelRow label="LO" unit=" Hz" min={-5000} max={5000} step={10}
value={st.filter_lo} disabled={off}
onSet={(v) => call(() => SetTCIFilter(v, st.filter_hi))} />
</div>
<div className="flex items-center gap-2 pb-1">
<LevelRow label="HI" unit=" Hz" min={-5000} max={5000} step={10}
value={st.filter_hi} disabled={off}
onSet={(v) => call(() => SetTCIFilter(st.filter_lo, v))} />
</div>
<div className="grid grid-cols-4 sm:grid-cols-7 gap-2">
{WIDTHS.map((w) => {
const e = edgesFor(w, mode);
+8
View File
@@ -95,6 +95,14 @@ func (t *TCI) handlePanel(name string, get func(int) string, args string) bool {
yes := func(s string) bool { return strings.EqualFold(strings.TrimSpace(s), "true") }
p := &t.panel.st
// Mute and squelch are LOGGED as they change, because a report from a real
// radio says pressing MUTE lights the squelch and nothing here can explain
// it. What the radio actually announces after the command settles whether
// this is our reading or its doing, and no amount of reasoning will.
switch name {
case "mute", "sql_enable", "sql_level":
debugLog.Printf("TCI: %s:%s", name, args)
}
switch name {
case "protocol":
p.Protocol = strings.TrimSpace(args)