feat(tci): a control console for the SunSDR

TCI already carries the frequency, the mode, the meters and now the audio.
It also carries everything else about the radio — and OpsLog was logging
most of it once as '(unhandled once)' and throwing it away. The console is
mostly a place to put what was already arriving.

That makes it the cheapest panel here, and it is worth saying why. A K3
console costs a command and a reply for every value it shows, which is why
it reads its settings in a rotation and its meters only while on screen.
TCI PUSHES: the radio announces its drive, its filters, its noise blanker
and the rest on connect, and again whenever any of them changes —
including when the operator changes them in ExpertSDR3's own window, which
this panel therefore follows without asking anything.

What it drives: drive and tune drive, mic gain, TUNE, volume, mute,
squelch and its threshold, NB, NR, ANF, APF, AGC speed, the passband, RIT
and XIT with their offsets, and the VFO lock. The S-meter is a real dBm
reading, so its S units are arithmetic rather than the calibration guess a
K3's meter needs.

Setters never update the cached state. The radio answers with the new
value, and taking its word is what keeps the panel honest when a setting
is refused, clamped, or changed at the radio a second later — the one
exception being a slider mid-drag, held for 900 ms so it is not dragged
back by its own echo.

Capped width and centred, like the other consoles. Also offered as a
docked pane — and the Elecraft console is offered there too now: App has
always had that pane, Settings simply never listed it.
This commit is contained in:
2026-08-26 18:31:44 +02:00
parent 8b9c835ca1
commit 98c49f6bbc
11 changed files with 1038 additions and 10 deletions
+40
View File
@@ -577,6 +577,8 @@ export function GetStationSettings():Promise<main.StationSettings>;
export function GetStationStatus():Promise<Array<main.StationDeviceStatus>>;
export function GetTCIPanel():Promise<cat.TCIPanelState>;
export function GetTelemetryEnabled():Promise<boolean>;
export function GetTrackedAwards():Promise<Array<string>>;
@@ -1159,6 +1161,44 @@ export function SetSpotMax(arg1:number):Promise<void>;
export function SetSpotTTLMinutes(arg1:number):Promise<void>;
export function SetTCIAGC(arg1:string):Promise<void>;
export function SetTCIANF(arg1:boolean):Promise<void>;
export function SetTCIAPF(arg1:boolean):Promise<void>;
export function SetTCIDrive(arg1:number):Promise<void>;
export function SetTCIFilter(arg1:number,arg2:number):Promise<void>;
export function SetTCILock(arg1:boolean):Promise<void>;
export function SetTCIMicLevel(arg1:number):Promise<void>;
export function SetTCIMute(arg1:boolean):Promise<void>;
export function SetTCINB(arg1:boolean):Promise<void>;
export function SetTCINR(arg1:boolean):Promise<void>;
export function SetTCIRIT(arg1:boolean):Promise<void>;
export function SetTCIRITOffset(arg1:number):Promise<void>;
export function SetTCISquelch(arg1:boolean):Promise<void>;
export function SetTCISquelchLevel(arg1:number):Promise<void>;
export function SetTCITune(arg1:boolean):Promise<void>;
export function SetTCITuneDrive(arg1:number):Promise<void>;
export function SetTCIVolume(arg1:number):Promise<void>;
export function SetTCIXIT(arg1:boolean):Promise<void>;
export function SetTCIXITOffset(arg1:number):Promise<void>;
export function SetTelemetryEnabled(arg1:boolean):Promise<void>;
export function SetUIPref(arg1:string,arg2:string):Promise<void>;
+80
View File
@@ -1094,6 +1094,10 @@ export function GetStationStatus() {
return window['go']['main']['App']['GetStationStatus']();
}
export function GetTCIPanel() {
return window['go']['main']['App']['GetTCIPanel']();
}
export function GetTelemetryEnabled() {
return window['go']['main']['App']['GetTelemetryEnabled']();
}
@@ -2258,6 +2262,82 @@ export function SetSpotTTLMinutes(arg1) {
return window['go']['main']['App']['SetSpotTTLMinutes'](arg1);
}
export function SetTCIAGC(arg1) {
return window['go']['main']['App']['SetTCIAGC'](arg1);
}
export function SetTCIANF(arg1) {
return window['go']['main']['App']['SetTCIANF'](arg1);
}
export function SetTCIAPF(arg1) {
return window['go']['main']['App']['SetTCIAPF'](arg1);
}
export function SetTCIDrive(arg1) {
return window['go']['main']['App']['SetTCIDrive'](arg1);
}
export function SetTCIFilter(arg1, arg2) {
return window['go']['main']['App']['SetTCIFilter'](arg1, arg2);
}
export function SetTCILock(arg1) {
return window['go']['main']['App']['SetTCILock'](arg1);
}
export function SetTCIMicLevel(arg1) {
return window['go']['main']['App']['SetTCIMicLevel'](arg1);
}
export function SetTCIMute(arg1) {
return window['go']['main']['App']['SetTCIMute'](arg1);
}
export function SetTCINB(arg1) {
return window['go']['main']['App']['SetTCINB'](arg1);
}
export function SetTCINR(arg1) {
return window['go']['main']['App']['SetTCINR'](arg1);
}
export function SetTCIRIT(arg1) {
return window['go']['main']['App']['SetTCIRIT'](arg1);
}
export function SetTCIRITOffset(arg1) {
return window['go']['main']['App']['SetTCIRITOffset'](arg1);
}
export function SetTCISquelch(arg1) {
return window['go']['main']['App']['SetTCISquelch'](arg1);
}
export function SetTCISquelchLevel(arg1) {
return window['go']['main']['App']['SetTCISquelchLevel'](arg1);
}
export function SetTCITune(arg1) {
return window['go']['main']['App']['SetTCITune'](arg1);
}
export function SetTCITuneDrive(arg1) {
return window['go']['main']['App']['SetTCITuneDrive'](arg1);
}
export function SetTCIVolume(arg1) {
return window['go']['main']['App']['SetTCIVolume'](arg1);
}
export function SetTCIXIT(arg1) {
return window['go']['main']['App']['SetTCIXIT'](arg1);
}
export function SetTCIXITOffset(arg1) {
return window['go']['main']['App']['SetTCIXITOffset'](arg1);
}
export function SetTelemetryEnabled(arg1) {
return window['go']['main']['App']['SetTelemetryEnabled'](arg1);
}
+66
View File
@@ -1210,6 +1210,72 @@ export namespace cat {
this.fixed = source["fixed"];
}
}
export class TCIPanelState {
connected: boolean;
device?: string;
protocol?: string;
drive: number;
tune_drive: number;
mic_level: number;
tx_enabled: boolean;
tx: boolean;
tuning: boolean;
volume: number;
mute: boolean;
agc?: string;
squelch_on: boolean;
squelch: number;
nb: boolean;
nr: boolean;
anf: boolean;
apf: boolean;
filter_lo: number;
filter_hi: number;
rit: boolean;
rit_offset: number;
xit: boolean;
xit_offset: number;
lock: boolean;
split: boolean;
smeter: number;
modulations?: string[];
static createFrom(source: any = {}) {
return new TCIPanelState(source);
}
constructor(source: any = {}) {
if ('string' === typeof source) source = JSON.parse(source);
this.connected = source["connected"];
this.device = source["device"];
this.protocol = source["protocol"];
this.drive = source["drive"];
this.tune_drive = source["tune_drive"];
this.mic_level = source["mic_level"];
this.tx_enabled = source["tx_enabled"];
this.tx = source["tx"];
this.tuning = source["tuning"];
this.volume = source["volume"];
this.mute = source["mute"];
this.agc = source["agc"];
this.squelch_on = source["squelch_on"];
this.squelch = source["squelch"];
this.nb = source["nb"];
this.nr = source["nr"];
this.anf = source["anf"];
this.apf = source["apf"];
this.filter_lo = source["filter_lo"];
this.filter_hi = source["filter_hi"];
this.rit = source["rit"];
this.rit_offset = source["rit_offset"];
this.xit = source["xit"];
this.xit_offset = source["xit_offset"];
this.lock = source["lock"];
this.split = source["split"];
this.smeter = source["smeter"];
this.modulations = source["modulations"];
}
}
export class YaesuTXState {
available: boolean;
model?: string;