merge: the SunSDR control console, and the audio device fixes with it
TCI pushes everything about the radio unasked, so the console is mostly a place to put what was already arriving and being logged once as unhandled. Drive, tune drive, mic gain, TUNE, volume, mute, squelch, NB/NR/ANF/APF, AGC, passband, RIT and XIT, VFO lock, and an S-meter in real dBm. Merged with two corrections to the audio page the console's own work turned up: the radio was being offered as a recording microphone, and the RX monitor was enabled for a source it cannot play.
This commit is contained in:
@@ -0,0 +1,118 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
// The TCI control console — bindings.
|
||||||
|
//
|
||||||
|
// Thin on purpose: the state is a snapshot the radio pushed and the setters are
|
||||||
|
// one command each. Everything interesting is in internal/cat/tci_panel.go.
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
"hamlog/internal/cat"
|
||||||
|
)
|
||||||
|
|
||||||
|
// GetTCIPanel returns the console state. Connected=false when the active CAT
|
||||||
|
// backend is not a TCI radio, so the frontend has one thing to look at rather
|
||||||
|
// than an error to distinguish from a disconnected radio.
|
||||||
|
func (a *App) GetTCIPanel() cat.TCIPanelState {
|
||||||
|
if a.cat == nil {
|
||||||
|
return cat.TCIPanelState{}
|
||||||
|
}
|
||||||
|
st, _ := a.cat.TCIPanelState()
|
||||||
|
return st
|
||||||
|
}
|
||||||
|
|
||||||
|
// tciDo is the shape every setter below takes.
|
||||||
|
func (a *App) tciDo(fn func(cat.TCIPanelController) error) error {
|
||||||
|
if a.cat == nil {
|
||||||
|
return fmt.Errorf("CAT not initialized")
|
||||||
|
}
|
||||||
|
return a.cat.TCIPanelDo(fn)
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetTCIDrive sets the transmit drive (0-100).
|
||||||
|
func (a *App) SetTCIDrive(v int) error {
|
||||||
|
return a.tciDo(func(t cat.TCIPanelController) error { return t.SetDrive(v) })
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetTCITuneDrive sets the drive TUNE uses (0-100).
|
||||||
|
func (a *App) SetTCITuneDrive(v int) error {
|
||||||
|
return a.tciDo(func(t cat.TCIPanelController) error { return t.SetTuneDrive(v) })
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetTCIMicLevel sets the microphone gain (0-100).
|
||||||
|
func (a *App) SetTCIMicLevel(v int) error {
|
||||||
|
return a.tciDo(func(t cat.TCIPanelController) error { return t.SetMicLevel(v) })
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetTCIVolume sets the receive volume in dB (0 down to -60).
|
||||||
|
func (a *App) SetTCIVolume(db int) error {
|
||||||
|
return a.tciDo(func(t cat.TCIPanelController) error { return t.SetVolume(db) })
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetTCIMute mutes or unmutes the receiver.
|
||||||
|
func (a *App) SetTCIMute(on bool) error {
|
||||||
|
return a.tciDo(func(t cat.TCIPanelController) error { return t.SetMute(on) })
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetTCIAGC picks the AGC speed: off, long, slow, med, fast.
|
||||||
|
func (a *App) SetTCIAGC(mode string) error {
|
||||||
|
return a.tciDo(func(t cat.TCIPanelController) error { return t.SetAGC(mode) })
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetTCISquelch turns the squelch on or off.
|
||||||
|
func (a *App) SetTCISquelch(on bool) error {
|
||||||
|
return a.tciDo(func(t cat.TCIPanelController) error { return t.SetSquelch(on) })
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetTCISquelchLevel sets the squelch threshold in dBm.
|
||||||
|
func (a *App) SetTCISquelchLevel(v int) error {
|
||||||
|
return a.tciDo(func(t cat.TCIPanelController) error { return t.SetSquelchLevel(v) })
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetTCINB, SetTCINR, SetTCIANF and SetTCIAPF switch the receive processing.
|
||||||
|
func (a *App) SetTCINB(on bool) error {
|
||||||
|
return a.tciDo(func(t cat.TCIPanelController) error { return t.SetNB(on) })
|
||||||
|
}
|
||||||
|
func (a *App) SetTCINR(on bool) error {
|
||||||
|
return a.tciDo(func(t cat.TCIPanelController) error { return t.SetNR(on) })
|
||||||
|
}
|
||||||
|
func (a *App) SetTCIANF(on bool) error {
|
||||||
|
return a.tciDo(func(t cat.TCIPanelController) error { return t.SetANF(on) })
|
||||||
|
}
|
||||||
|
func (a *App) SetTCIAPF(on bool) error {
|
||||||
|
return a.tciDo(func(t cat.TCIPanelController) error { return t.SetAPF(on) })
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetTCIFilter sets the passband edges in Hz.
|
||||||
|
func (a *App) SetTCIFilter(lo, hi int) error {
|
||||||
|
return a.tciDo(func(t cat.TCIPanelController) error { return t.SetFilter(lo, hi) })
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetTCIRIT / SetTCIXIT switch the offsets on; the Offset calls move them.
|
||||||
|
func (a *App) SetTCIRIT(on bool) error {
|
||||||
|
return a.tciDo(func(t cat.TCIPanelController) error { return t.SetRIT(on) })
|
||||||
|
}
|
||||||
|
func (a *App) SetTCIXIT(on bool) error {
|
||||||
|
return a.tciDo(func(t cat.TCIPanelController) error { return t.SetXIT(on) })
|
||||||
|
}
|
||||||
|
func (a *App) SetTCIRITOffset(hz int) error {
|
||||||
|
return a.tciDo(func(t cat.TCIPanelController) error { return t.SetRITOffset(hz) })
|
||||||
|
}
|
||||||
|
func (a *App) SetTCIXITOffset(hz int) error {
|
||||||
|
return a.tciDo(func(t cat.TCIPanelController) error { return t.SetXITOffset(hz) })
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetTCILock locks the radio's VFO knob.
|
||||||
|
func (a *App) SetTCILock(on bool) error {
|
||||||
|
return a.tciDo(func(t cat.TCIPanelController) error { return t.SetLock(on) })
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetTCITune starts or stops the tune carrier.
|
||||||
|
//
|
||||||
|
// IT TRANSMITS, and at tune_drive rather than at drive — which is why the panel
|
||||||
|
// shows those two numbers next to the button rather than hiding one of them in
|
||||||
|
// a menu.
|
||||||
|
func (a *App) SetTCITune(on bool) error {
|
||||||
|
return a.tciDo(func(t cat.TCIPanelController) error { return t.SetTune(on) })
|
||||||
|
}
|
||||||
+21
-2
@@ -78,6 +78,7 @@ import { WorldMap, LocatorMap } from '@/components/MainMap';
|
|||||||
import { FlexPanel } from '@/components/FlexPanel';
|
import { FlexPanel } from '@/components/FlexPanel';
|
||||||
import { IcomPanel } from '@/components/IcomPanel';
|
import { IcomPanel } from '@/components/IcomPanel';
|
||||||
import { YaesuPanel } from '@/components/YaesuPanel';
|
import { YaesuPanel } from '@/components/YaesuPanel';
|
||||||
|
import { TCIPanel } from '@/components/TCIPanel';
|
||||||
import { ElecraftPanel } from '@/components/ElecraftPanel';
|
import { ElecraftPanel } from '@/components/ElecraftPanel';
|
||||||
import { AntGeniusPanel, type AGStatus } from '@/components/AntGeniusPanel';
|
import { AntGeniusPanel, type AGStatus } from '@/components/AntGeniusPanel';
|
||||||
import { MotorAntennaWidget, type AntStatus } from '@/components/MotorAntennaWidget';
|
import { MotorAntennaWidget, type AntStatus } from '@/components/MotorAntennaWidget';
|
||||||
@@ -1920,7 +1921,7 @@ export default function App() {
|
|||||||
// so it's loaded async on mount and re-read on profile:changed below.
|
// so it's loaded async on mount and re-read on profile:changed below.
|
||||||
// 'none' is only ever stored for the third and fourth panes: the first two are
|
// 'none' is only ever stored for the third and fourth panes: the first two are
|
||||||
// the Main view, and a layout with no panes at all is not a layout.
|
// the Main view, and a layout with no panes at all is not a layout.
|
||||||
type MainPaneKind = 'map1' | 'map2' | 'cluster' | 'worked' | 'flex' | 'recent' | 'icom' | 'yaesu' | 'elecraft' | 'netcontrol' | 'decodes' | 'none';
|
type MainPaneKind = 'map1' | 'map2' | 'cluster' | 'worked' | 'flex' | 'recent' | 'icom' | 'yaesu' | 'elecraft' | 'tci' | 'netcontrol' | 'decodes' | 'none';
|
||||||
const [mapZoomSignal, setMapZoomSignal] = useState(0); // bump → world map auto-zooms now
|
const [mapZoomSignal, setMapZoomSignal] = useState(0); // bump → world map auto-zooms now
|
||||||
const [mainPaneLeft, setMainPaneLeft] = useState<MainPaneKind>('map1');
|
const [mainPaneLeft, setMainPaneLeft] = useState<MainPaneKind>('map1');
|
||||||
const [mainPaneRight, setMainPaneRight] = useState<MainPaneKind>('map2');
|
const [mainPaneRight, setMainPaneRight] = useState<MainPaneKind>('map2');
|
||||||
@@ -1931,7 +1932,7 @@ export default function App() {
|
|||||||
// quarter-width map is unreadable.
|
// quarter-width map is unreadable.
|
||||||
const [mainLayout4, setMainLayout4] = useState<'cols' | 'quad'>('quad');
|
const [mainLayout4, setMainLayout4] = useState<'cols' | 'quad'>('quad');
|
||||||
const loadMainPanes = useCallback(async () => {
|
const loadMainPanes = useCallback(async () => {
|
||||||
const valid = (v: string): v is MainPaneKind => v === 'map1' || v === 'map2' || v === 'cluster' || v === 'worked' || v === 'flex' || v === 'recent' || v === 'icom' || v === 'yaesu' || v === 'elecraft' || v === 'netcontrol' || v === 'decodes';
|
const valid = (v: string): v is MainPaneKind => v === 'map1' || v === 'map2' || v === 'cluster' || v === 'worked' || v === 'flex' || v === 'recent' || v === 'icom' || v === 'yaesu' || v === 'elecraft' || v === 'tci' || v === 'netcontrol' || v === 'decodes';
|
||||||
const [l, r, p3, p4, lay] = await Promise.all([
|
const [l, r, p3, p4, lay] = await Promise.all([
|
||||||
GetUIPref('mainPaneLeft').catch(() => ''),
|
GetUIPref('mainPaneLeft').catch(() => ''),
|
||||||
GetUIPref('mainPaneRight').catch(() => ''),
|
GetUIPref('mainPaneRight').catch(() => ''),
|
||||||
@@ -6174,6 +6175,12 @@ export default function App() {
|
|||||||
<ElecraftPanel onReportRST={(r) => { setRstSent(r); rstUserEditedRef.current = true; }} />
|
<ElecraftPanel onReportRST={(r) => { setRstSent(r); rstUserEditedRef.current = true; }} />
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
case 'tci':
|
||||||
|
return (
|
||||||
|
<div className="h-full w-full min-h-0 rounded-lg overflow-hidden border border-border">
|
||||||
|
<TCIPanel onReportRST={(r) => { setRstSent(r); rstUserEditedRef.current = true; }} />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
case 'icom':
|
case 'icom':
|
||||||
return (
|
return (
|
||||||
<div className="h-full w-full min-h-0 rounded-lg overflow-hidden border border-border">
|
<div className="h-full w-full min-h-0 rounded-lg overflow-hidden border border-border">
|
||||||
@@ -7452,6 +7459,7 @@ export default function App() {
|
|||||||
{catState.backend === 'icom' && <TabsTrigger value="icom">Icom Console</TabsTrigger>}
|
{catState.backend === 'icom' && <TabsTrigger value="icom">Icom Console</TabsTrigger>}
|
||||||
{catState.backend === 'yaesu' && <TabsTrigger value="yaesu">Yaesu Console</TabsTrigger>}
|
{catState.backend === 'yaesu' && <TabsTrigger value="yaesu">Yaesu Console</TabsTrigger>}
|
||||||
{(catState.backend === 'elecraft' || catState.backend === 'kenwood') && <TabsTrigger value="elecraft">{t('k3.console')}</TabsTrigger>}
|
{(catState.backend === 'elecraft' || catState.backend === 'kenwood') && <TabsTrigger value="elecraft">{t('k3.console')}</TabsTrigger>}
|
||||||
|
{catState.backend === 'tci' && <TabsTrigger value="tci">{t('tcip.console')}</TabsTrigger>}
|
||||||
{statsTabOpen && (
|
{statsTabOpen && (
|
||||||
<TabsTrigger value="stats" className="gap-1.5">
|
<TabsTrigger value="stats" className="gap-1.5">
|
||||||
{t('stats.tab')}
|
{t('stats.tab')}
|
||||||
@@ -8140,6 +8148,15 @@ export default function App() {
|
|||||||
</TabsContent>
|
</TabsContent>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
{/* The SunSDR console. Everything on it is state the radio pushes
|
||||||
|
over TCI unasked, so unlike the serial consoles it costs nothing
|
||||||
|
to keep open. */}
|
||||||
|
{catState.backend === 'tci' && (
|
||||||
|
<TabsContent value="tci" className="flex-1 min-h-0 p-0">
|
||||||
|
<TCIPanel onReportRST={(r) => { setRstSent(r); rstUserEditedRef.current = true; }} />
|
||||||
|
</TabsContent>
|
||||||
|
)}
|
||||||
|
|
||||||
{catState.backend === 'icom' && (
|
{catState.backend === 'icom' && (
|
||||||
<TabsContent value="icom" className="flex-1 min-h-0 p-0">
|
<TabsContent value="icom" className="flex-1 min-h-0 p-0">
|
||||||
<IcomPanel isNetwork={catBackend === 'icom-net'} onReportRST={(r) => { setRstSent(r); rstUserEditedRef.current = true; }} />
|
<IcomPanel isNetwork={catBackend === 'icom-net'} onReportRST={(r) => { setRstSent(r); rstUserEditedRef.current = true; }} />
|
||||||
@@ -8538,6 +8555,8 @@ export default function App() {
|
|||||||
flexAvailable={catState.backend === 'flex'}
|
flexAvailable={catState.backend === 'flex'}
|
||||||
icomAvailable={catState.backend === 'icom'}
|
icomAvailable={catState.backend === 'icom'}
|
||||||
yaesuAvailable={catState.backend === 'yaesu'}
|
yaesuAvailable={catState.backend === 'yaesu'}
|
||||||
|
elecraftAvailable={catState.backend === 'elecraft' || catState.backend === 'kenwood'}
|
||||||
|
tciAvailable={catState.backend === 'tci'}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
|||||||
@@ -175,6 +175,8 @@ interface Props {
|
|||||||
flexAvailable?: boolean; // CAT backend is FlexRadio → offer it as a Main pane
|
flexAvailable?: boolean; // CAT backend is FlexRadio → offer it as a Main pane
|
||||||
icomAvailable?: boolean; // CAT backend is Icom → offer the Icom console as a Main pane
|
icomAvailable?: boolean; // CAT backend is Icom → offer the Icom console as a Main pane
|
||||||
yaesuAvailable?: boolean; // CAT backend is Yaesu → offer the Yaesu console as a Main pane
|
yaesuAvailable?: boolean; // CAT backend is Yaesu → offer the Yaesu console as a Main pane
|
||||||
|
elecraftAvailable?: boolean; // CAT backend is Elecraft/Kenwood → the K3/K4 console
|
||||||
|
tciAvailable?: boolean; // CAT backend is TCI → the SunSDR console
|
||||||
// Opens a QSO in the editor. Settings is not where a log is edited — but the
|
// Opens a QSO in the editor. Settings is not where a log is edited — but the
|
||||||
// RDA comparison lists contacts whose district is in dispute, and a list of
|
// RDA comparison lists contacts whose district is in dispute, and a list of
|
||||||
// things to fix that cannot be acted on is a list to write down and look up
|
// things to fix that cannot be acted on is a list to write down and look up
|
||||||
@@ -1085,7 +1087,7 @@ function RelayAutoPanel() {
|
|||||||
// profile-prefixed). Self-contained so it owns its async-loaded state.
|
// profile-prefixed). Self-contained so it owns its async-loaded state.
|
||||||
const MAIN_PANE_VALUES = ['map1', 'map2', 'cluster', 'worked', 'recent', 'netcontrol', 'decodes'];
|
const MAIN_PANE_VALUES = ['map1', 'map2', 'cluster', 'worked', 'recent', 'netcontrol', 'decodes'];
|
||||||
const PANE_NONE = 'none';
|
const PANE_NONE = 'none';
|
||||||
function MainViewPanes({ onChanged, flexAvailable, icomAvailable, yaesuAvailable }: { onChanged?: (side: 'left' | 'right' | 'p3' | 'p4' | 'layout', value: string) => void; flexAvailable?: boolean; icomAvailable?: boolean; yaesuAvailable?: boolean }) {
|
function MainViewPanes({ onChanged, flexAvailable, icomAvailable, yaesuAvailable, elecraftAvailable, tciAvailable }: { onChanged?: (side: 'left' | 'right' | 'p3' | 'p4' | 'layout', value: string) => void; flexAvailable?: boolean; icomAvailable?: boolean; yaesuAvailable?: boolean; elecraftAvailable?: boolean; tciAvailable?: boolean }) {
|
||||||
const { t } = useI18n();
|
const { t } = useI18n();
|
||||||
const [panes, setPanes] = useState<Record<string, string>>({ left: 'map1', right: 'map2', p3: PANE_NONE, p4: PANE_NONE });
|
const [panes, setPanes] = useState<Record<string, string>>({ left: 'map1', right: 'map2', p3: PANE_NONE, p4: PANE_NONE });
|
||||||
const [layout, setLayout] = useState('quad');
|
const [layout, setLayout] = useState('quad');
|
||||||
@@ -1095,11 +1097,16 @@ function MainViewPanes({ onChanged, flexAvailable, icomAvailable, yaesuAvailable
|
|||||||
...(flexAvailable ? ['flex'] : []),
|
...(flexAvailable ? ['flex'] : []),
|
||||||
...(icomAvailable ? ['icom'] : []),
|
...(icomAvailable ? ['icom'] : []),
|
||||||
...(yaesuAvailable ? ['yaesu'] : []),
|
...(yaesuAvailable ? ['yaesu'] : []),
|
||||||
|
// The Elecraft console could be docked from the start — App has always had
|
||||||
|
// the pane — but it was never offered here, so the only way to reach it was
|
||||||
|
// the tab. Listed with the others now.
|
||||||
|
...(elecraftAvailable ? ['elecraft'] : []),
|
||||||
|
...(tciAvailable ? ['tci'] : []),
|
||||||
].map((value) => ({ value, label: t(`settings.pane.${value}`) }))
|
].map((value) => ({ value, label: t(`settings.pane.${value}`) }))
|
||||||
.sort((a, b) => a.label.localeCompare(b.label));
|
.sort((a, b) => a.label.localeCompare(b.label));
|
||||||
const KEYS: Record<string, string> = { left: 'mainPaneLeft', right: 'mainPaneRight', p3: 'mainPane3', p4: 'mainPane4' };
|
const KEYS: Record<string, string> = { left: 'mainPaneLeft', right: 'mainPaneRight', p3: 'mainPane3', p4: 'mainPane4' };
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const valid = (v: string) => v === 'flex' || v === 'icom' || v === 'yaesu' || MAIN_PANE_VALUES.includes(v);
|
const valid = (v: string) => v === 'flex' || v === 'icom' || v === 'yaesu' || v === 'elecraft' || v === 'tci' || MAIN_PANE_VALUES.includes(v);
|
||||||
Promise.all([
|
Promise.all([
|
||||||
...Object.values(KEYS).map((k) => GetUIPref(k).catch(() => '')),
|
...Object.values(KEYS).map((k) => GetUIPref(k).catch(() => '')),
|
||||||
GetUIPref('mainPaneLayout').catch(() => ''),
|
GetUIPref('mainPaneLayout').catch(() => ''),
|
||||||
@@ -1553,7 +1560,7 @@ function brandOfBackend(backend: string, kenwoodLink?: string): { brand: string;
|
|||||||
// memo() cuts that off. It only works if the props hold still, which is why
|
// memo() cuts that off. It only works if the props hold still, which is why
|
||||||
// App passes callbacks that do not change identity on every render — see the
|
// App passes callbacks that do not change identity on every render — see the
|
||||||
// useCallback wrappers there.
|
// useCallback wrappers there.
|
||||||
function SettingsModalImpl({ onClose, onSaved, initialSection, onMainPaneChanged, flexAvailable, icomAvailable, yaesuAvailable, onEditQSO }: Props) {
|
function SettingsModalImpl({ onClose, onSaved, initialSection, onMainPaneChanged, flexAvailable, icomAvailable, yaesuAvailable, elecraftAvailable, tciAvailable, onEditQSO }: Props) {
|
||||||
const { t } = useI18n();
|
const { t } = useI18n();
|
||||||
const [selected, setSelected] = useState<SectionId>((initialSection as SectionId) || 'station');
|
const [selected, setSelected] = useState<SectionId>((initialSection as SectionId) || 'station');
|
||||||
const [loading, setLoading] = useState(true);
|
const [loading, setLoading] = useState(true);
|
||||||
@@ -6766,6 +6773,13 @@ function SettingsModalImpl({ onClose, onSaved, initialSection, onMainPaneChanged
|
|||||||
xiegu: t('cat.optXiegu'),
|
xiegu: t('cat.optXiegu'),
|
||||||
} as Record<string, string>)[catCfg.backend] ?? '';
|
} as Record<string, string>)[catCfg.backend] ?? '';
|
||||||
|
|
||||||
|
// The radio-over-network entry, which ListAudioInputDevices and
|
||||||
|
// ListAudioOutputDevices add when the CAT link can carry audio. It is a
|
||||||
|
// real choice for two of the four fields and nonsense for the other two.
|
||||||
|
const NET_DEVICE = 'net:radio';
|
||||||
|
const soundCardsOnly = (devs: AudioDev[]) => devs.filter((d) => d.id !== NET_DEVICE);
|
||||||
|
const fromRadioIsNetwork = audioCfg.from_radio === NET_DEVICE;
|
||||||
|
|
||||||
const deviceSelect = (
|
const deviceSelect = (
|
||||||
field: keyof AudioSettings,
|
field: keyof AudioSettings,
|
||||||
devices: AudioDev[],
|
devices: AudioDev[],
|
||||||
@@ -6800,10 +6814,16 @@ function SettingsModalImpl({ onClose, onSaved, initialSection, onMainPaneChanged
|
|||||||
{deviceSelect('from_radio', audioInputs, t('aud.phFromRadio'))}
|
{deviceSelect('from_radio', audioInputs, t('aud.phFromRadio'))}
|
||||||
<Label className="text-sm">{t('aud.toRadio')}</Label>
|
<Label className="text-sm">{t('aud.toRadio')}</Label>
|
||||||
{deviceSelect('to_radio', audioOutputs, t('aud.phToRadio'))}
|
{deviceSelect('to_radio', audioOutputs, t('aud.phToRadio'))}
|
||||||
|
{/* The radio is offered for the two fields it can BE — where the
|
||||||
|
received audio comes from, and where the voice keyer sends
|
||||||
|
its messages — and taken out of the other two. It is not a
|
||||||
|
microphone: choosing it here would record the station you are
|
||||||
|
listening to instead of your own voice. And it is not a pair of
|
||||||
|
speakers: the audio going that way is transmit audio. */}
|
||||||
<Label className="text-sm">{t('aud.recMic')}</Label>
|
<Label className="text-sm">{t('aud.recMic')}</Label>
|
||||||
{deviceSelect('recording_device', audioInputs, t('aud.phRecMic'))}
|
{deviceSelect('recording_device', soundCardsOnly(audioInputs), t('aud.phRecMic'))}
|
||||||
<Label className="text-sm">{t('aud.listening')}</Label>
|
<Label className="text-sm">{t('aud.listening')}</Label>
|
||||||
{deviceSelect('listening_device', audioOutputs, t('aud.phListening'))}
|
{deviceSelect('listening_device', soundCardsOnly(audioOutputs), t('aud.phListening'))}
|
||||||
</div>
|
</div>
|
||||||
<p className="text-[11px] text-muted-foreground">
|
<p className="text-[11px] text-muted-foreground">
|
||||||
<strong>{t('aud.fromRadioShort')}</strong> {t('aud.explainFrom')}{' '}
|
<strong>{t('aud.fromRadioShort')}</strong> {t('aud.explainFrom')}{' '}
|
||||||
@@ -6821,8 +6841,8 @@ function SettingsModalImpl({ onClose, onSaved, initialSection, onMainPaneChanged
|
|||||||
size="sm"
|
size="sm"
|
||||||
className="h-8"
|
className="h-8"
|
||||||
onClick={toggleMonitor}
|
onClick={toggleMonitor}
|
||||||
disabled={!monitorOn && !audioCfg.from_radio}
|
disabled={!monitorOn && (!audioCfg.from_radio || fromRadioIsNetwork)}
|
||||||
title={t('aud.monitorTitle')}
|
title={fromRadioIsNetwork ? t('aud.monitorNoTci') : t('aud.monitorTitle')}
|
||||||
>
|
>
|
||||||
{monitorOn ? t('aud.stopListening') : t('aud.listenRadio')}
|
{monitorOn ? t('aud.stopListening') : t('aud.listenRadio')}
|
||||||
</Button>
|
</Button>
|
||||||
@@ -7115,7 +7135,7 @@ function SettingsModalImpl({ onClose, onSaved, initialSection, onMainPaneChanged
|
|||||||
</label>
|
</label>
|
||||||
<TelemetryToggle />
|
<TelemetryToggle />
|
||||||
|
|
||||||
<MainViewPanes onChanged={onMainPaneChanged} flexAvailable={flexAvailable} icomAvailable={icomAvailable} yaesuAvailable={yaesuAvailable} />
|
<MainViewPanes onChanged={onMainPaneChanged} flexAvailable={flexAvailable} icomAvailable={icomAvailable} yaesuAvailable={yaesuAvailable} elecraftAvailable={elecraftAvailable} tciAvailable={tciAvailable} />
|
||||||
|
|
||||||
<div className="border-t border-border/60 pt-4 space-y-2">
|
<div className="border-t border-border/60 pt-4 space-y-2">
|
||||||
<h4 className="text-sm font-semibold text-foreground">{t('gen.pwEnc')}</h4>
|
<h4 className="text-sm font-semibold text-foreground">{t('gen.pwEnc')}</h4>
|
||||||
|
|||||||
@@ -0,0 +1,314 @@
|
|||||||
|
import { useEffect, useRef, useState } from 'react';
|
||||||
|
import { Radio, Activity, AudioLines, SlidersHorizontal, Mic } from 'lucide-react';
|
||||||
|
import {
|
||||||
|
GetTCIPanel, GetCATState,
|
||||||
|
SetTCIDrive, SetTCITuneDrive, SetTCIMicLevel, SetTCIVolume, SetTCIMute,
|
||||||
|
SetTCIAGC, SetTCISquelch, SetTCISquelchLevel,
|
||||||
|
SetTCINB, SetTCINR, SetTCIANF, SetTCIAPF, SetTCIFilter,
|
||||||
|
SetTCIRIT, SetTCIXIT, SetTCIRITOffset, SetTCIXITOffset, SetTCILock, SetTCITune,
|
||||||
|
} from '../../wailsjs/go/main/App';
|
||||||
|
import { cn } from '@/lib/utils';
|
||||||
|
import { useI18n } from '@/lib/i18n';
|
||||||
|
import { sMeterRST } from '@/lib/rst';
|
||||||
|
import { MeterBar } from '@/components/MeterBar';
|
||||||
|
import { WheelRange } from '@/components/WheelRange';
|
||||||
|
|
||||||
|
type TCIState = {
|
||||||
|
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[];
|
||||||
|
};
|
||||||
|
|
||||||
|
const ZERO: TCIState = {
|
||||||
|
connected: false, drive: 0, tune_drive: 0, mic_level: 0, tx_enabled: false, tx: false, tuning: false,
|
||||||
|
volume: 0, mute: false, squelch_on: false, squelch: 0,
|
||||||
|
nb: false, nr: false, anf: false, apf: false, filter_lo: 0, filter_hi: 0,
|
||||||
|
rit: false, rit_offset: 0, xit: false, xit_offset: 0, lock: false, split: false, smeter: 0,
|
||||||
|
};
|
||||||
|
|
||||||
|
// Passbands worth a button, as edges relative to the carrier. TCI takes the two
|
||||||
|
// edges rather than a width, which is more than a console needs: an operator
|
||||||
|
// picks "CW" or "SSB", not a pair of numbers.
|
||||||
|
const FILTERS: { label: string; lo: number; hi: number }[] = [
|
||||||
|
{ label: '250', lo: 300, hi: 550 },
|
||||||
|
{ label: '500', lo: 300, hi: 800 },
|
||||||
|
{ label: '1.0k', lo: 200, hi: 1200 },
|
||||||
|
{ label: '1.8k', lo: 100, hi: 1900 },
|
||||||
|
{ label: '2.4k', lo: 100, hi: 2500 },
|
||||||
|
{ label: '2.8k', lo: 100, hi: 2900 },
|
||||||
|
{ label: '3.5k', lo: 100, hi: 3600 },
|
||||||
|
];
|
||||||
|
|
||||||
|
// dBm → S units. TCI reports a real signal level rather than a meter position,
|
||||||
|
// which is the useful way round: S9 is -73 dBm by the IARU definition and every
|
||||||
|
// S unit below it is 6 dB, so this is arithmetic rather than a calibration
|
||||||
|
// table — nothing here is provisional the way the K3's meter reading is.
|
||||||
|
function sParts(dbm: number): { s: number; over: number; label: string } {
|
||||||
|
if (dbm === 0) return { s: 0, over: 0, label: '—' };
|
||||||
|
if (dbm >= -73) {
|
||||||
|
const over = Math.round((dbm + 73) / 10) * 10;
|
||||||
|
return { s: 9, over, label: over > 0 ? `S9+${over}` : 'S9' };
|
||||||
|
}
|
||||||
|
const s = Math.max(0, Math.min(9, Math.round(9 + (dbm + 73) / 6)));
|
||||||
|
return { s, over: 0, label: `S${s}` };
|
||||||
|
}
|
||||||
|
|
||||||
|
// The meter bar wants 0-100; -127 dBm is the bottom of the scale and -13 dBm
|
||||||
|
// (S9+60) the top.
|
||||||
|
function sBar(dbm: number): number {
|
||||||
|
if (dbm === 0) return 0;
|
||||||
|
return Math.max(0, Math.min(100, ((dbm + 127) / 114) * 100));
|
||||||
|
}
|
||||||
|
|
||||||
|
function sSegColor(frac: number): string {
|
||||||
|
return frac > 0.75 ? '#dc2626' : frac > 0.55 ? '#f59e0b' : '#16a34a';
|
||||||
|
}
|
||||||
|
|
||||||
|
function Card({ icon: Icon, title, children }: { icon: any; title: string; children: React.ReactNode }) {
|
||||||
|
return (
|
||||||
|
<div className="rounded-xl border border-border bg-card shadow-sm overflow-hidden">
|
||||||
|
<div className="flex items-center gap-2 px-3 py-2 border-b border-border/60 bg-muted/30">
|
||||||
|
<Icon className="size-4 text-primary" />
|
||||||
|
<span className="text-xs font-bold uppercase tracking-wider text-foreground/80">{title}</span>
|
||||||
|
</div>
|
||||||
|
<div className="p-3 space-y-3">{children}</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// One button shape for every on/off control, as on the other consoles.
|
||||||
|
function Toggle({ label, on, off, onClick, title }: {
|
||||||
|
label: string; on: boolean; off: boolean; onClick: () => void; title?: string;
|
||||||
|
}) {
|
||||||
|
return (
|
||||||
|
<button type="button" disabled={off} onClick={onClick} title={title}
|
||||||
|
className={cn('rounded-lg border-2 px-2 py-1.5 text-xs font-bold transition-all disabled:opacity-30',
|
||||||
|
on ? 'bg-primary text-primary-foreground border-primary shadow-[0_0_10px] shadow-primary/40'
|
||||||
|
: 'bg-card text-muted-foreground border-border hover:bg-muted')}>
|
||||||
|
{label}
|
||||||
|
</button>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function Row({ label, value, children }: { label: string; value: string; children: React.ReactNode }) {
|
||||||
|
return (
|
||||||
|
<div className="space-y-1">
|
||||||
|
<div className="flex items-baseline justify-between">
|
||||||
|
<span className="text-[10px] font-bold uppercase tracking-wider text-muted-foreground">{label}</span>
|
||||||
|
<span className="text-xs font-mono tabular-nums">{value}</span>
|
||||||
|
</div>
|
||||||
|
{children}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function TCIPanel({ onReportRST }: { onReportRST?: (rst: string) => void } = {}) {
|
||||||
|
const { t } = useI18n();
|
||||||
|
const [st, setSt] = useState<TCIState>(ZERO);
|
||||||
|
const [freqHz, setFreqHz] = useState(0);
|
||||||
|
const [mode, setMode] = useState('');
|
||||||
|
const [err, setErr] = useState('');
|
||||||
|
|
||||||
|
// A slider the operator is dragging must not be dragged back by the poll.
|
||||||
|
// The radio confirms every change by announcing it, but that answer takes a
|
||||||
|
// round trip — long enough for a drag to stutter against its own echo.
|
||||||
|
const holdRef = useRef<Record<string, { v: number; until: number }>>({});
|
||||||
|
const hold = (key: string, reported: number) => {
|
||||||
|
const h = holdRef.current[key];
|
||||||
|
return h && Date.now() < h.until ? h.v : reported;
|
||||||
|
};
|
||||||
|
const setHold = (key: string, v: number) => {
|
||||||
|
holdRef.current[key] = { v, until: Date.now() + 900 };
|
||||||
|
};
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
let alive = true;
|
||||||
|
const tick = async () => {
|
||||||
|
try {
|
||||||
|
const p: any = await GetTCIPanel();
|
||||||
|
if (!alive) return;
|
||||||
|
setSt(p as TCIState);
|
||||||
|
const cs: any = await GetCATState();
|
||||||
|
if (!alive) return;
|
||||||
|
setFreqHz(Number(cs?.rx_freq_hz) || Number(cs?.freq_hz) || 0);
|
||||||
|
setMode(String(cs?.mode || ''));
|
||||||
|
} catch (e: any) {
|
||||||
|
if (alive) setErr(String(e?.message ?? e));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
tick();
|
||||||
|
const id = window.setInterval(tick, 400);
|
||||||
|
return () => { alive = false; window.clearInterval(id); };
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const off = !st.connected;
|
||||||
|
const call = (fn: () => Promise<any>) => { fn().catch((e: any) => setErr(String(e?.message ?? e))); };
|
||||||
|
|
||||||
|
const drive = hold('drive', st.drive);
|
||||||
|
const tuneDrive = hold('tune_drive', st.tune_drive);
|
||||||
|
const mic = hold('mic', st.mic_level);
|
||||||
|
const vol = hold('vol', st.volume);
|
||||||
|
const sql = hold('sql', st.squelch);
|
||||||
|
const s = sParts(st.smeter);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="h-full min-h-0 overflow-auto bg-background">
|
||||||
|
{/* Capped and centred, like the Elecraft, Yaesu, Icom and Flex consoles.
|
||||||
|
Stretched across a wide window a console puts each slider a hand's
|
||||||
|
width from its own label and stops reading as one instrument. */}
|
||||||
|
<div className="max-w-5xl mx-auto p-3 space-y-3">
|
||||||
|
{/* VFO + identity */}
|
||||||
|
<div className="rounded-xl border border-border bg-card shadow-sm px-4 py-3 flex items-center justify-between gap-3 flex-wrap">
|
||||||
|
<div>
|
||||||
|
<div className="text-[10px] font-bold uppercase tracking-wider text-muted-foreground flex items-center gap-1.5">
|
||||||
|
<Radio className="size-3.5" />
|
||||||
|
{st.device || 'SunSDR'} {st.protocol ? `· ${st.protocol}` : ''}
|
||||||
|
<span className={cn('size-2 rounded-full', off ? 'bg-muted-foreground/40' : 'bg-success')} />
|
||||||
|
</div>
|
||||||
|
<div className="text-2xl font-mono tabular-nums font-bold">
|
||||||
|
{freqHz > 0 ? (freqHz / 1e6).toFixed(6) : '—'}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
{st.tx && <span className="rounded-md bg-danger px-2 py-1 text-[11px] font-bold text-danger-foreground">TX</span>}
|
||||||
|
{st.split && <span className="rounded-md border border-border px-2 py-1 text-[11px] font-bold">SPLIT</span>}
|
||||||
|
<Toggle label={t('tcip.lock')} on={st.lock} off={off} onClick={() => call(() => SetTCILock(!st.lock))} />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{off && <div className="text-xs text-muted-foreground px-1">{t('tcip.waiting')}</div>}
|
||||||
|
{!!err && <div className="text-[11px] text-danger px-1">{err}</div>}
|
||||||
|
|
||||||
|
{/* Meters — one for now: TCI reports the receive level and does not
|
||||||
|
publish a transmit power reading, so a PWR bar here would be an
|
||||||
|
empty promise. */}
|
||||||
|
<Card icon={Activity} title={t('tcip.meters')}>
|
||||||
|
<MeterBar label="S-METER" value={st.tx ? 0 : sBar(st.smeter)} lo={0} hi={100}
|
||||||
|
accent="#16a34a" segColor={sSegColor}
|
||||||
|
display={st.tx ? '—' : `${s.label} ${st.smeter} dBm`}
|
||||||
|
onClick={() => {
|
||||||
|
if (st.tx || !onReportRST) return;
|
||||||
|
onReportRST(sMeterRST(s.s, s.over, mode));
|
||||||
|
}}
|
||||||
|
title={t('tcip.sMeterHint')} />
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
{/* Transmit */}
|
||||||
|
<Card icon={SlidersHorizontal} title={t('tcip.transmit')}>
|
||||||
|
<div className="grid grid-cols-1 sm:grid-cols-2 gap-3">
|
||||||
|
<Row label={t('tcip.drive')} value={`${drive}%`}>
|
||||||
|
<WheelRange min={0} max={100} disabled={off} value={drive}
|
||||||
|
onChange={(v) => { setHold('drive', v); call(() => SetTCIDrive(v)); }} />
|
||||||
|
</Row>
|
||||||
|
<Row label={t('tcip.tuneDrive')} value={`${tuneDrive}%`}>
|
||||||
|
<WheelRange min={0} max={100} disabled={off} value={tuneDrive} accent="#f59e0b"
|
||||||
|
onChange={(v) => { setHold('tune_drive', v); call(() => SetTCITuneDrive(v)); }} />
|
||||||
|
</Row>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center gap-2 flex-wrap">
|
||||||
|
{/* TUNE transmits, and at the tune drive rather than the main one —
|
||||||
|
which is why both numbers are above the button rather than one of
|
||||||
|
them being in a menu somewhere. */}
|
||||||
|
<button type="button" disabled={off || !st.tx_enabled}
|
||||||
|
onClick={() => call(() => SetTCITune(!st.tuning))}
|
||||||
|
className={cn('rounded-lg border-2 px-4 py-2 text-sm font-extrabold tracking-wide transition-all disabled:opacity-30',
|
||||||
|
st.tuning ? 'bg-warning text-warning-foreground border-warning shadow-[0_0_14px] shadow-warning/50'
|
||||||
|
: 'bg-card text-warning border-warning hover:bg-warning-muted')}>
|
||||||
|
{st.tuning ? t('tcip.tuning') : t('tcip.tune')}
|
||||||
|
</button>
|
||||||
|
{!st.tx_enabled && !off && (
|
||||||
|
<span className="text-[11px] text-muted-foreground">{t('tcip.txDisabled')}</span>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<Row label={t('tcip.mic')} value={`${mic}%`}>
|
||||||
|
<WheelRange min={0} max={100} disabled={off} value={mic} accent="#a855f7"
|
||||||
|
onChange={(v) => { setHold('mic', v); call(() => SetTCIMicLevel(v)); }} />
|
||||||
|
</Row>
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
{/* Receive */}
|
||||||
|
<Card icon={AudioLines} title={t('tcip.receive')}>
|
||||||
|
<div className="grid grid-cols-1 sm:grid-cols-2 gap-3">
|
||||||
|
{/* TCI's volume is dB and NEGATIVE — 0 is full, -60 inaudible. Shown
|
||||||
|
as the radio's own number rather than converted to a percentage,
|
||||||
|
so it matches the figure in ExpertSDR3's window. */}
|
||||||
|
<Row label={t('tcip.volume')} value={`${vol} dB`}>
|
||||||
|
<WheelRange min={-60} max={0} disabled={off} value={vol}
|
||||||
|
onChange={(v) => { setHold('vol', v); call(() => SetTCIVolume(v)); }} />
|
||||||
|
</Row>
|
||||||
|
<Row label={t('tcip.squelch')} value={st.squelch_on ? `${sql} dBm` : t('tcip.off')}>
|
||||||
|
<WheelRange min={-140} max={0} disabled={off || !st.squelch_on} value={sql}
|
||||||
|
onChange={(v) => { setHold('sql', v); call(() => SetTCISquelchLevel(v)); }} />
|
||||||
|
</Row>
|
||||||
|
</div>
|
||||||
|
<div className="grid grid-cols-3 sm:grid-cols-6 gap-2">
|
||||||
|
<Toggle label="NB" on={st.nb} off={off} onClick={() => call(() => SetTCINB(!st.nb))} />
|
||||||
|
<Toggle label="NR" on={st.nr} off={off} onClick={() => call(() => SetTCINR(!st.nr))} />
|
||||||
|
<Toggle label="ANF" on={st.anf} off={off} onClick={() => call(() => SetTCIANF(!st.anf))} />
|
||||||
|
<Toggle label="APF" on={st.apf} off={off} onClick={() => call(() => SetTCIAPF(!st.apf))} />
|
||||||
|
<Toggle label="SQL" on={st.squelch_on} off={off} onClick={() => call(() => SetTCISquelch(!st.squelch_on))} />
|
||||||
|
<Toggle label={t('tcip.mute')} on={st.mute} off={off} onClick={() => call(() => SetTCIMute(!st.mute))} />
|
||||||
|
</div>
|
||||||
|
<div className="space-y-1">
|
||||||
|
<span className="text-[10px] font-bold uppercase tracking-wider text-muted-foreground">{t('tcip.agc')}</span>
|
||||||
|
<div className="grid grid-cols-5 gap-2">
|
||||||
|
{['off', 'long', 'slow', 'med', 'fast'].map((m) => (
|
||||||
|
<Toggle key={m} label={m.toUpperCase()} on={(st.agc || '') === m} off={off}
|
||||||
|
onClick={() => call(() => SetTCIAGC(m))} />
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="space-y-1">
|
||||||
|
<div className="flex items-baseline justify-between">
|
||||||
|
<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="grid grid-cols-4 sm:grid-cols-7 gap-2">
|
||||||
|
{FILTERS.map((f) => (
|
||||||
|
<Toggle key={f.label} label={f.label}
|
||||||
|
on={st.filter_lo === f.lo && st.filter_hi === f.hi} off={off}
|
||||||
|
onClick={() => call(() => SetTCIFilter(f.lo, f.hi))} />
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
{/* RIT / XIT */}
|
||||||
|
<Card icon={Mic} title="RIT / XIT">
|
||||||
|
<div className="grid grid-cols-1 sm:grid-cols-2 gap-3">
|
||||||
|
{([
|
||||||
|
{ key: 'rit', on: st.rit, offset: st.rit_offset, toggle: SetTCIRIT, set: SetTCIRITOffset },
|
||||||
|
{ key: 'xit', on: st.xit, offset: st.xit_offset, toggle: SetTCIXIT, set: SetTCIXITOffset },
|
||||||
|
] as const).map((r) => (
|
||||||
|
<div key={r.key} className="flex items-center gap-2">
|
||||||
|
<Toggle label={r.key.toUpperCase()} on={r.on} off={off} onClick={() => call(() => r.toggle(!r.on))} />
|
||||||
|
<span className="text-xs font-mono tabular-nums w-16 text-center">
|
||||||
|
{r.offset > 0 ? `+${r.offset}` : r.offset} Hz
|
||||||
|
</span>
|
||||||
|
{/* ±10 and ±100, and a zero. The radio's own knob does the rest;
|
||||||
|
a console that tries to replace it needs a knob, not more
|
||||||
|
buttons. */}
|
||||||
|
{[-100, -10, 10, 100].map((d) => (
|
||||||
|
<button key={d} type="button" disabled={off || !r.on}
|
||||||
|
onClick={() => call(() => r.set(r.offset + d))}
|
||||||
|
className="rounded-md border border-border bg-card px-1.5 py-1 text-[10px] font-mono hover:bg-muted disabled:opacity-30">
|
||||||
|
{d > 0 ? `+${d}` : d}
|
||||||
|
</button>
|
||||||
|
))}
|
||||||
|
<button type="button" disabled={off || !r.on}
|
||||||
|
onClick={() => call(() => r.set(0))}
|
||||||
|
className="rounded-md border border-border bg-card px-1.5 py-1 text-[10px] font-bold hover:bg-muted disabled:opacity-30">
|
||||||
|
0
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</Card>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -109,7 +109,7 @@ const en: Dict = {
|
|||||||
'settings.pane.map1': 'Map — great-circle + beam', 'settings.pane.map2': 'Map — locator (street)',
|
'settings.pane.map1': 'Map — great-circle + beam', 'settings.pane.map2': 'Map — locator (street)',
|
||||||
'settings.pane.cluster': 'Cluster spots', 'settings.pane.worked': 'Worked before',
|
'settings.pane.cluster': 'Cluster spots', 'settings.pane.worked': 'Worked before',
|
||||||
'settings.pane.recent': 'Recent QSOs', 'settings.pane.netcontrol': 'Net control', 'settings.pane.decodes': 'FT decodes',
|
'settings.pane.recent': 'Recent QSOs', 'settings.pane.netcontrol': 'Net control', 'settings.pane.decodes': 'FT decodes',
|
||||||
'settings.pane.flex': 'Flex Console', 'settings.pane.icom': 'Icom Console', 'settings.pane.yaesu': 'Yaesu Console', 'yaesu.notConnected': 'Yaesu CAT not connected', 'yaesu.meters': 'Meters', 'yaesu.bandMode': 'Band & mode', 'yaesu.receive': 'Receive', 'yaesu.noiseFilter': 'Noise & filter', 'yaesu.transmit': 'Transmit', 'yaesu.refresh': 'Refresh', 'yaesu.narrowHint': 'Narrow IF filter (NAR) — tightens the receive bandwidth. Only shown when the radio answers the command.', 'yaesu.tuneHint': 'Start an antenna-tuner cycle', 'yaesu.sToRst': 'Click to fill the RST sent', 'yaesu.sidebandHint': 'Click to select this mode; click again to switch sideband (U/L)', 'yaesu.splitUpHint': 'Transmit this far above the receive frequency, and turn split on', 'yaesu.txOn': 'TX', 'yaesu.cw': 'CW', 'yaesu.breakInHint': 'Break-in: the rig switches to receive between characters', 'yaesu.zinHint': 'Zero-in: retune so the station you hear lands on your CW pitch',
|
'settings.pane.flex': 'Flex Console', 'tcip.console': "SunSDR Console", 'settings.pane.tci': "SunSDR Console", 'settings.pane.elecraft': "Elecraft Console", 'tcip.waiting': "Waiting for the radio — TCI is not connected.", 'tcip.meters': "Meters", 'tcip.transmit': "Transmit", 'tcip.receive': "Receive", 'tcip.drive': "Drive", 'tcip.tuneDrive': "Tune drive", 'tcip.mic': "Mic gain", 'tcip.volume': "Volume", 'tcip.squelch': "Squelch", 'tcip.agc': "AGC", 'tcip.filter': "Filter", 'tcip.mute': "MUTE", 'tcip.lock': "LOCK", 'tcip.off': "off", 'tcip.tune': "TUNE", 'tcip.tuning': "TUNING", 'tcip.txDisabled': "The radio is not allowing transmit", 'tcip.sMeterHint': "Click to put this report in the entry form. The radio sends a real signal level in dBm, so the S units are arithmetic rather than a calibration guess.", 'settings.pane.icom': 'Icom Console', 'settings.pane.yaesu': 'Yaesu Console', 'yaesu.notConnected': 'Yaesu CAT not connected', 'yaesu.meters': 'Meters', 'yaesu.bandMode': 'Band & mode', 'yaesu.receive': 'Receive', 'yaesu.noiseFilter': 'Noise & filter', 'yaesu.transmit': 'Transmit', 'yaesu.refresh': 'Refresh', 'yaesu.narrowHint': 'Narrow IF filter (NAR) — tightens the receive bandwidth. Only shown when the radio answers the command.', 'yaesu.tuneHint': 'Start an antenna-tuner cycle', 'yaesu.sToRst': 'Click to fill the RST sent', 'yaesu.sidebandHint': 'Click to select this mode; click again to switch sideband (U/L)', 'yaesu.splitUpHint': 'Transmit this far above the receive frequency, and turn split on', 'yaesu.txOn': 'TX', 'yaesu.cw': 'CW', 'yaesu.breakInHint': 'Break-in: the rig switches to receive between characters', 'yaesu.zinHint': 'Zero-in: retune so the station you hear lands on your CW pitch',
|
||||||
'theme.auto': 'Auto (system)', 'theme.light-warm': 'Warm light', 'theme.light-cool': 'Cool light',
|
'theme.auto': 'Auto (system)', 'theme.light-warm': 'Warm light', 'theme.light-cool': 'Cool light',
|
||||||
'theme.light-sage': 'Sage light', 'theme.light-nordic': 'Nordic light', 'theme.sahara': 'Sahara', 'theme.dim-slate': 'Dim slate', 'theme.dark-warm': 'Warm dark',
|
'theme.light-sage': 'Sage light', 'theme.light-nordic': 'Nordic light', 'theme.sahara': 'Sahara', 'theme.dim-slate': 'Dim slate', 'theme.dark-warm': 'Warm dark',
|
||||||
'theme.dark-graphite': 'Graphite dark', 'theme.dark-indigo': 'Indigo', 'theme.dark-teal': 'Ocean', 'theme.dark-plum': 'Plum', 'theme.high-contrast': 'High contrast',
|
'theme.dark-graphite': 'Graphite dark', 'theme.dark-indigo': 'Indigo', 'theme.dark-teal': 'Ocean', 'theme.dark-plum': 'Plum', 'theme.high-contrast': 'High contrast',
|
||||||
@@ -494,7 +494,7 @@ const en: Dict = {
|
|||||||
'aud.noneDefault': '— none / system default —', 'aud.defaultTag': '(default)',
|
'aud.noneDefault': '— none / system default —', 'aud.defaultTag': '(default)',
|
||||||
'aud.fromRadioShort': 'From Radio', 'aud.toRadioShort': 'To Radio', 'aud.explainFrom': '= what you receive (used by the QSO recorder).', 'aud.explainTo': '= where voice-keyer messages are transmitted.',
|
'aud.fromRadioShort': 'From Radio', 'aud.toRadioShort': 'To Radio', 'aud.explainFrom': '= what you receive (used by the QSO recorder).', 'aud.explainTo': '= where voice-keyer messages are transmitted.',
|
||||||
'aud.monitorTitle': "Hear the rig's RX audio (From Radio) through your Listening device", 'aud.listenRadio': '▶ Listen to radio', 'aud.stopListening': '■ Stop listening',
|
'aud.monitorTitle': "Hear the rig's RX audio (From Radio) through your Listening device", 'aud.listenRadio': '▶ Listen to radio', 'aud.stopListening': '■ Stop listening',
|
||||||
'aud.monitorOn': 'RX monitor running — From Radio → Listening device.', 'aud.monitorHint': 'Live-monitor the rig here (USB codec now; network audio later).',
|
'aud.monitorOn': 'RX monitor running — From Radio → Listening device.', 'aud.monitorNoTci': 'Listening here is not available with the radio as the source: the TCI stream feeds the QSO recorder, not the monitor. Listen on the radio itself.', 'aud.monitorHint': 'Live-monitor the rig here (USB codec now; network audio later).',
|
||||||
'aud.txTitle': 'Key PTT and pipe your live mic into the rig (To Radio device)', 'aud.talkRadio': '🎙 Talk to radio (TX)', 'aud.stopTalk': '■ Stop talking (TX)',
|
'aud.txTitle': 'Key PTT and pipe your live mic into the rig (To Radio device)', 'aud.talkRadio': '🎙 Talk to radio (TX)', 'aud.stopTalk': '■ Stop talking (TX)',
|
||||||
'aud.txOn': 'TRANSMITTING — mic → To Radio, PTT keyed. Click to stop.', 'aud.txHint': 'Live mic → rig with PTT (USB now; network TX later).',
|
'aud.txOn': 'TRANSMITTING — mic → To Radio, PTT keyed. Click to stop.', 'aud.txHint': 'Live mic → rig with PTT (USB now; network TX later).',
|
||||||
'aud.recorder': 'QSO recorder', 'aud.recordEvery': 'Record every QSO to an audio file (From Radio + your mic)', 'aud.recFolder': 'Recordings folder', 'aud.browse': 'Browse…',
|
'aud.recorder': 'QSO recorder', 'aud.recordEvery': 'Record every QSO to an audio file (From Radio + your mic)', 'aud.recFolder': 'Recordings folder', 'aud.browse': 'Browse…',
|
||||||
@@ -596,7 +596,7 @@ const fr: Dict = {
|
|||||||
'settings.pane.map1': 'Carte — orthodromie + faisceau', 'settings.pane.map2': 'Carte — locator (rue)',
|
'settings.pane.map1': 'Carte — orthodromie + faisceau', 'settings.pane.map2': 'Carte — locator (rue)',
|
||||||
'settings.pane.cluster': 'Spots cluster', 'settings.pane.worked': 'Déjà contactés',
|
'settings.pane.cluster': 'Spots cluster', 'settings.pane.worked': 'Déjà contactés',
|
||||||
'settings.pane.recent': 'QSO récents', 'settings.pane.netcontrol': 'Gestion de net', 'settings.pane.decodes': 'Decodes FT',
|
'settings.pane.recent': 'QSO récents', 'settings.pane.netcontrol': 'Gestion de net', 'settings.pane.decodes': 'Decodes FT',
|
||||||
'settings.pane.flex': 'Flex Console', 'settings.pane.icom': 'Icom Console', 'settings.pane.yaesu': 'Yaesu Console', 'yaesu.notConnected': 'CAT Yaesu non connecté', 'yaesu.meters': 'Mesures', 'yaesu.bandMode': 'Bande et mode', 'yaesu.receive': 'Réception', 'yaesu.noiseFilter': 'Bruit et filtre', 'yaesu.transmit': 'Émission', 'yaesu.refresh': 'Actualiser', 'yaesu.narrowHint': "Filtre FI étroit (NAR) — resserre la bande passante de réception. N'apparaît que si la radio répond à la commande.", 'yaesu.tuneHint': "Lancer un cycle d'accord d'antenne", 'yaesu.sToRst': 'Cliquer pour remplir le RST envoyé', 'yaesu.sidebandHint': 'Cliquer pour choisir ce mode ; recliquer pour changer de bande latérale (U/L)', 'yaesu.splitUpHint': "Émettre à cette distance au-dessus de la fréquence de réception, et activer le split", 'yaesu.txOn': 'TX', 'yaesu.cw': 'CW', 'yaesu.breakInHint': "Break-in : la radio repasse en réception entre les caractères", 'yaesu.zinHint': "Zéro-in : réaccorde pour que la station entendue tombe sur votre note CW",
|
'settings.pane.flex': 'Flex Console', 'tcip.console': "Console SunSDR", 'settings.pane.tci': "Console SunSDR", 'settings.pane.elecraft': "Console Elecraft", 'tcip.waiting': "En attente de la radio — TCI n'est pas connecté.", 'tcip.meters': "Mesures", 'tcip.transmit': "Émission", 'tcip.receive': "Réception", 'tcip.drive': "Puissance", 'tcip.tuneDrive': "Puissance d'accord", 'tcip.mic': "Gain micro", 'tcip.volume': "Volume", 'tcip.squelch': "Squelch", 'tcip.agc': "AGC", 'tcip.filter': "Filtre", 'tcip.mute': "MUET", 'tcip.lock': "VERR", 'tcip.off': "désactivé", 'tcip.tune': "ACCORD", 'tcip.tuning': "EN ACCORD", 'tcip.txDisabled': "La radio n'autorise pas l'émission", 'tcip.sMeterHint': "Cliquer pour reporter dans la saisie. La radio envoie un vrai niveau en dBm, donc les points S sont un calcul et non une estimation d'étalonnage.", 'settings.pane.icom': 'Icom Console', 'settings.pane.yaesu': 'Yaesu Console', 'yaesu.notConnected': 'CAT Yaesu non connecté', 'yaesu.meters': 'Mesures', 'yaesu.bandMode': 'Bande et mode', 'yaesu.receive': 'Réception', 'yaesu.noiseFilter': 'Bruit et filtre', 'yaesu.transmit': 'Émission', 'yaesu.refresh': 'Actualiser', 'yaesu.narrowHint': "Filtre FI étroit (NAR) — resserre la bande passante de réception. N'apparaît que si la radio répond à la commande.", 'yaesu.tuneHint': "Lancer un cycle d'accord d'antenne", 'yaesu.sToRst': 'Cliquer pour remplir le RST envoyé', 'yaesu.sidebandHint': 'Cliquer pour choisir ce mode ; recliquer pour changer de bande latérale (U/L)', 'yaesu.splitUpHint': "Émettre à cette distance au-dessus de la fréquence de réception, et activer le split", 'yaesu.txOn': 'TX', 'yaesu.cw': 'CW', 'yaesu.breakInHint': "Break-in : la radio repasse en réception entre les caractères", 'yaesu.zinHint': "Zéro-in : réaccorde pour que la station entendue tombe sur votre note CW",
|
||||||
'theme.auto': 'Auto (système)', 'theme.light-warm': 'Clair chaud', 'theme.light-cool': 'Clair froid',
|
'theme.auto': 'Auto (système)', 'theme.light-warm': 'Clair chaud', 'theme.light-cool': 'Clair froid',
|
||||||
'theme.light-sage': 'Clair sauge', 'theme.light-nordic': 'Clair nordique', 'theme.sahara': 'Sahara', 'theme.dim-slate': 'Ardoise tamisé', 'theme.dark-warm': 'Sombre chaud',
|
'theme.light-sage': 'Clair sauge', 'theme.light-nordic': 'Clair nordique', 'theme.sahara': 'Sahara', 'theme.dim-slate': 'Ardoise tamisé', 'theme.dark-warm': 'Sombre chaud',
|
||||||
'theme.dark-graphite': 'Sombre graphite', 'theme.dark-indigo': 'Indigo', 'theme.dark-teal': 'Océan', 'theme.dark-plum': 'Prune', 'theme.high-contrast': 'Contraste élevé',
|
'theme.dark-graphite': 'Sombre graphite', 'theme.dark-indigo': 'Indigo', 'theme.dark-teal': 'Océan', 'theme.dark-plum': 'Prune', 'theme.high-contrast': 'Contraste élevé',
|
||||||
@@ -962,7 +962,7 @@ const fr: Dict = {
|
|||||||
'aud.noneDefault': '— aucun / défaut système —', 'aud.defaultTag': '(défaut)',
|
'aud.noneDefault': '— aucun / défaut système —', 'aud.defaultTag': '(défaut)',
|
||||||
'aud.fromRadioShort': 'Depuis la radio', 'aud.toRadioShort': 'Vers la radio', 'aud.explainFrom': "= ce que vous recevez (utilisé par l'enregistreur de QSO).", 'aud.explainTo': '= où sont émis les messages du manipulateur vocal.',
|
'aud.fromRadioShort': 'Depuis la radio', 'aud.toRadioShort': 'Vers la radio', 'aud.explainFrom': "= ce que vous recevez (utilisé par l'enregistreur de QSO).", 'aud.explainTo': '= où sont émis les messages du manipulateur vocal.',
|
||||||
'aud.monitorTitle': "Écouter l'audio RX du poste (Depuis la radio) sur votre périphérique d'écoute", 'aud.listenRadio': '▶ Écouter la radio', 'aud.stopListening': "■ Arrêter l'écoute",
|
'aud.monitorTitle': "Écouter l'audio RX du poste (Depuis la radio) sur votre périphérique d'écoute", 'aud.listenRadio': '▶ Écouter la radio', 'aud.stopListening': "■ Arrêter l'écoute",
|
||||||
'aud.monitorOn': "Écoute RX active — Depuis la radio → périphérique d'écoute.", 'aud.monitorHint': "Écoute directe du poste (codec USB pour l'instant ; audio réseau plus tard).",
|
'aud.monitorOn': "Écoute RX active — Depuis la radio → périphérique d'écoute.", 'aud.monitorNoTci': "L'écoute ici n'est pas disponible avec la radio comme source : le flux TCI alimente l'enregistreur de QSO, pas l'écoute. Écoute sur la radio elle-même.", 'aud.monitorHint': "Écoute directe du poste (codec USB pour l'instant ; audio réseau plus tard).",
|
||||||
'aud.txTitle': 'Activer le PTT et envoyer votre micro vers le poste (périphérique « Vers la radio »)', 'aud.talkRadio': '🎙 Parler à la radio (TX)', 'aud.stopTalk': '■ Arrêter de parler (TX)',
|
'aud.txTitle': 'Activer le PTT et envoyer votre micro vers le poste (périphérique « Vers la radio »)', 'aud.talkRadio': '🎙 Parler à la radio (TX)', 'aud.stopTalk': '■ Arrêter de parler (TX)',
|
||||||
'aud.txOn': 'ÉMISSION — micro → Vers la radio, PTT activé. Cliquez pour arrêter.', 'aud.txHint': "Micro direct → poste avec PTT (USB pour l'instant ; TX réseau plus tard).",
|
'aud.txOn': 'ÉMISSION — micro → Vers la radio, PTT activé. Cliquez pour arrêter.', 'aud.txHint': "Micro direct → poste avec PTT (USB pour l'instant ; TX réseau plus tard).",
|
||||||
'aud.recorder': 'Enregistreur de QSO', 'aud.recordEvery': 'Enregistrer chaque QSO dans un fichier audio (Depuis la radio + votre micro)', 'aud.recFolder': 'Dossier des enregistrements', 'aud.browse': 'Parcourir…',
|
'aud.recorder': 'Enregistreur de QSO', 'aud.recordEvery': 'Enregistrer chaque QSO dans un fichier audio (Depuis la radio + votre micro)', 'aud.recFolder': 'Dossier des enregistrements', 'aud.browse': 'Parcourir…',
|
||||||
|
|||||||
Vendored
+40
@@ -577,6 +577,8 @@ export function GetStationSettings():Promise<main.StationSettings>;
|
|||||||
|
|
||||||
export function GetStationStatus():Promise<Array<main.StationDeviceStatus>>;
|
export function GetStationStatus():Promise<Array<main.StationDeviceStatus>>;
|
||||||
|
|
||||||
|
export function GetTCIPanel():Promise<cat.TCIPanelState>;
|
||||||
|
|
||||||
export function GetTelemetryEnabled():Promise<boolean>;
|
export function GetTelemetryEnabled():Promise<boolean>;
|
||||||
|
|
||||||
export function GetTrackedAwards():Promise<Array<string>>;
|
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 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 SetTelemetryEnabled(arg1:boolean):Promise<void>;
|
||||||
|
|
||||||
export function SetUIPref(arg1:string,arg2:string):Promise<void>;
|
export function SetUIPref(arg1:string,arg2:string):Promise<void>;
|
||||||
|
|||||||
@@ -1094,6 +1094,10 @@ export function GetStationStatus() {
|
|||||||
return window['go']['main']['App']['GetStationStatus']();
|
return window['go']['main']['App']['GetStationStatus']();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function GetTCIPanel() {
|
||||||
|
return window['go']['main']['App']['GetTCIPanel']();
|
||||||
|
}
|
||||||
|
|
||||||
export function GetTelemetryEnabled() {
|
export function GetTelemetryEnabled() {
|
||||||
return window['go']['main']['App']['GetTelemetryEnabled']();
|
return window['go']['main']['App']['GetTelemetryEnabled']();
|
||||||
}
|
}
|
||||||
@@ -2258,6 +2262,82 @@ export function SetSpotTTLMinutes(arg1) {
|
|||||||
return window['go']['main']['App']['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) {
|
export function SetTelemetryEnabled(arg1) {
|
||||||
return window['go']['main']['App']['SetTelemetryEnabled'](arg1);
|
return window['go']['main']['App']['SetTelemetryEnabled'](arg1);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1210,6 +1210,72 @@ export namespace cat {
|
|||||||
this.fixed = source["fixed"];
|
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 {
|
export class YaesuTXState {
|
||||||
available: boolean;
|
available: boolean;
|
||||||
model?: string;
|
model?: string;
|
||||||
|
|||||||
+19
-2
@@ -34,6 +34,10 @@ type TCI struct {
|
|||||||
OnSpotClick func(callsign string, freqHz int64)
|
OnSpotClick func(callsign string, freqHz int64)
|
||||||
unhandledSeen map[string]bool // log each unknown TCI message type once
|
unhandledSeen map[string]bool // log each unknown TCI message type once
|
||||||
|
|
||||||
|
// panel is the control-console state — everything the radio announces about
|
||||||
|
// itself that is not frequency or mode. See tci_panel.go.
|
||||||
|
panel tciPanel
|
||||||
|
|
||||||
// audio holds the receive-audio stream — see tci_audio.go. TCI carries it
|
// audio holds the receive-audio stream — see tci_audio.go. TCI carries it
|
||||||
// on this same WebSocket, which is what lets a SunSDR record and decode
|
// on this same WebSocket, which is what lets a SunSDR record and decode
|
||||||
// without a virtual audio cable in the way.
|
// without a virtual audio cable in the way.
|
||||||
@@ -457,7 +461,20 @@ func (t *TCI) handle(msg string) {
|
|||||||
}
|
}
|
||||||
t.mu.Lock()
|
t.mu.Lock()
|
||||||
defer t.mu.Unlock()
|
defer t.mu.Unlock()
|
||||||
switch strings.ToLower(name) {
|
lower := strings.ToLower(name)
|
||||||
|
// The console's own messages first. Most of them were being logged once as
|
||||||
|
// unhandled and thrown away — the radio has been announcing its drive, its
|
||||||
|
// filters and its noise blanker since the first connection.
|
||||||
|
if t.handlePanel(lower, get, args) {
|
||||||
|
// Still falls through for the few the rig state also needs (split, tune),
|
||||||
|
// which is why this does not return.
|
||||||
|
switch lower {
|
||||||
|
case "split_enable", "trx", "modulation", "vfo":
|
||||||
|
default:
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
switch lower {
|
||||||
case "device":
|
case "device":
|
||||||
t.device = strings.TrimSpace(args)
|
t.device = strings.TrimSpace(args)
|
||||||
// The radio ANNOUNCES its audio format at connect —
|
// The radio ANNOUNCES its audio format at connect —
|
||||||
@@ -524,7 +541,7 @@ func (t *TCI) handle(msg string) {
|
|||||||
t.txAllowed, t.txAllowedKnown = allowed, true
|
t.txAllowed, t.txAllowedKnown = allowed, true
|
||||||
}
|
}
|
||||||
default:
|
default:
|
||||||
lname := strings.ToLower(name)
|
lname := lower
|
||||||
// A click on one of our panorama spots comes back as
|
// A click on one of our panorama spots comes back as
|
||||||
// CLICKED_ON_SPOT:<call>,<hz> (legacy)
|
// CLICKED_ON_SPOT:<call>,<hz> (legacy)
|
||||||
// RX_CLICKED_ON_SPOT:<rx>,<ch>,<call>,<hz>
|
// RX_CLICKED_ON_SPOT:<rx>,<ch>,<call>,<hz>
|
||||||
|
|||||||
@@ -37,3 +37,62 @@ func (m *Manager) TCIAudioDo(fn func(TCIAudioController) error) error {
|
|||||||
return fn(tc)
|
return fn(tc)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// TCIPanelController is the control console of a TCI radio — everything the
|
||||||
|
// panel reads and everything it sets.
|
||||||
|
//
|
||||||
|
// Listed one by one rather than accepted as *TCI, for the same reason the audio
|
||||||
|
// controller is: the manager hands out capabilities, not backends, and a
|
||||||
|
// station on OmniRig asking for the TCI console gets a sentence instead of a
|
||||||
|
// crash.
|
||||||
|
type TCIPanelController interface {
|
||||||
|
TCIPanel() TCIPanelState
|
||||||
|
SetDrive(v int) error
|
||||||
|
SetTuneDrive(v int) error
|
||||||
|
SetMicLevel(v int) error
|
||||||
|
SetVolume(db int) error
|
||||||
|
SetMute(on bool) error
|
||||||
|
SetAGC(mode string) error
|
||||||
|
SetSquelch(on bool) error
|
||||||
|
SetSquelchLevel(v int) error
|
||||||
|
SetNB(on bool) error
|
||||||
|
SetNR(on bool) error
|
||||||
|
SetANF(on bool) error
|
||||||
|
SetAPF(on bool) error
|
||||||
|
SetFilter(lo, hi int) error
|
||||||
|
SetRIT(on bool) error
|
||||||
|
SetXIT(on bool) error
|
||||||
|
SetRITOffset(hz int) error
|
||||||
|
SetXITOffset(hz int) error
|
||||||
|
SetLock(on bool) error
|
||||||
|
SetTune(on bool) error
|
||||||
|
}
|
||||||
|
|
||||||
|
// TCIPanelState returns the console snapshot, or (zero, false) when the active
|
||||||
|
// backend is not a TCI radio.
|
||||||
|
//
|
||||||
|
// Read WITHOUT going through the CAT goroutine: the state is a cached copy of
|
||||||
|
// what the radio pushed, guarded by its own lock, and the panel polls it several
|
||||||
|
// times a second. Queueing that behind whatever the poll loop is doing would put
|
||||||
|
// the console's smoothness at the mercy of a rig command's timeout.
|
||||||
|
func (m *Manager) TCIPanelState() (TCIPanelState, bool) {
|
||||||
|
m.mu.RLock()
|
||||||
|
b := m.backend
|
||||||
|
m.mu.RUnlock()
|
||||||
|
if tc, ok := b.(TCIPanelController); ok {
|
||||||
|
return tc.TCIPanel(), true
|
||||||
|
}
|
||||||
|
return TCIPanelState{}, false
|
||||||
|
}
|
||||||
|
|
||||||
|
// TCIPanelDo dispatches one console command onto the CAT goroutine, where every
|
||||||
|
// other write to the radio goes.
|
||||||
|
func (m *Manager) TCIPanelDo(fn func(TCIPanelController) error) error {
|
||||||
|
return m.exec(func(b Backend) error {
|
||||||
|
tc, ok := b.(TCIPanelController)
|
||||||
|
if !ok {
|
||||||
|
return fmt.Errorf("the active CAT backend is not a TCI radio")
|
||||||
|
}
|
||||||
|
return fn(tc)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|||||||
@@ -0,0 +1,308 @@
|
|||||||
|
//go:build windows
|
||||||
|
|
||||||
|
package cat
|
||||||
|
|
||||||
|
// The TCI control panel: what the radio already tells us, gathered up.
|
||||||
|
//
|
||||||
|
// This is the cheapest panel in OpsLog, and the reason is worth saying. A K3 is
|
||||||
|
// asked — every value on its console costs a command and a reply on a serial
|
||||||
|
// line, which is why that panel reads its settings in a rotation and its meters
|
||||||
|
// only while it is on screen. TCI PUSHES: the radio announces its drive, its
|
||||||
|
// volume, its filters, its noise blanker and everything else when a client
|
||||||
|
// connects, and again whenever any of them changes, whoever changed it. There
|
||||||
|
// is nothing to poll.
|
||||||
|
//
|
||||||
|
// So this file is mostly a place to PUT what was already arriving and being
|
||||||
|
// logged as "(unhandled once)". The setters are the same names sent back the
|
||||||
|
// other way, which is how TCI works throughout: one vocabulary, both directions.
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
// TCIPanelState is the whole console in one snapshot, polled by the frontend.
|
||||||
|
//
|
||||||
|
// Values the radio has not mentioned keep their zero, which is why the
|
||||||
|
// "Known" flags exist for the ones where zero is a real setting: a squelch at 0
|
||||||
|
// and a squelch never reported are different, and a panel that cannot tell them
|
||||||
|
// apart draws a control that lies until the operator touches it.
|
||||||
|
type TCIPanelState struct {
|
||||||
|
Connected bool `json:"connected"`
|
||||||
|
Device string `json:"device,omitempty"` // what the radio calls itself
|
||||||
|
Protocol string `json:"protocol,omitempty"` // "ExpertSDR3,1.5"
|
||||||
|
|
||||||
|
// Transmit.
|
||||||
|
Drive int `json:"drive"` // 0-100
|
||||||
|
TuneDrive int `json:"tune_drive"` // 0-100, used by TUNE
|
||||||
|
MicLevel int `json:"mic_level"` // 0-100
|
||||||
|
TXEnabled bool `json:"tx_enabled"` // the radio's own permission (tx_enable)
|
||||||
|
TX bool `json:"tx"`
|
||||||
|
Tuning bool `json:"tuning"`
|
||||||
|
|
||||||
|
// Receive.
|
||||||
|
Volume int `json:"volume"` // dB, negative — TCI's own scale
|
||||||
|
Mute bool `json:"mute"`
|
||||||
|
AGC string `json:"agc,omitempty"` // off/long/slow/med/fast
|
||||||
|
SquelchOn bool `json:"squelch_on"`
|
||||||
|
Squelch int `json:"squelch"` // dBm threshold
|
||||||
|
NB bool `json:"nb"`
|
||||||
|
NR bool `json:"nr"`
|
||||||
|
ANF bool `json:"anf"`
|
||||||
|
APF bool `json:"apf"`
|
||||||
|
|
||||||
|
// Filter edges in Hz, relative to the carrier (TCI's own convention).
|
||||||
|
FilterLo int `json:"filter_lo"`
|
||||||
|
FilterHi int `json:"filter_hi"`
|
||||||
|
|
||||||
|
// Tuning aids.
|
||||||
|
RIT bool `json:"rit"`
|
||||||
|
RITOffset int `json:"rit_offset"`
|
||||||
|
XIT bool `json:"xit"`
|
||||||
|
XITOffset int `json:"xit_offset"`
|
||||||
|
Lock bool `json:"lock"`
|
||||||
|
Split bool `json:"split"`
|
||||||
|
|
||||||
|
// SMeter is the last reported signal level in dBm — the radio pushes it
|
||||||
|
// several times a second while receiving.
|
||||||
|
SMeter int `json:"smeter"`
|
||||||
|
|
||||||
|
// Modulations is what this radio will accept, straight from its own
|
||||||
|
// announcement, so the mode buttons are the radio's and not a guess.
|
||||||
|
Modulations []string `json:"modulations,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// tciPanel is the backing state. Guarded by TCI.mu with everything else it
|
||||||
|
// arrives alongside.
|
||||||
|
type tciPanel struct {
|
||||||
|
st TCIPanelState
|
||||||
|
}
|
||||||
|
|
||||||
|
// handlePanel takes the messages the console cares about.
|
||||||
|
//
|
||||||
|
// Returns false when the message is none of its business, so the caller can go
|
||||||
|
// on to its own cases and to the unknown-message log. Called with t.mu held.
|
||||||
|
func (t *TCI) handlePanel(name string, get func(int) string, args string) bool {
|
||||||
|
// Most of these are per-receiver ("sql_level:0,20"), and OpsLog follows
|
||||||
|
// receiver 0 throughout. A message for another receiver is accepted as
|
||||||
|
// handled and dropped: it is understood, it is simply not ours.
|
||||||
|
forRX0 := func() bool { return get(0) == "0" || get(0) == "" }
|
||||||
|
num := func(s string) (int, bool) {
|
||||||
|
n, err := strconv.Atoi(strings.TrimSpace(s))
|
||||||
|
return n, err == nil
|
||||||
|
}
|
||||||
|
yes := func(s string) bool { return strings.EqualFold(strings.TrimSpace(s), "true") }
|
||||||
|
|
||||||
|
p := &t.panel.st
|
||||||
|
switch name {
|
||||||
|
case "protocol":
|
||||||
|
p.Protocol = strings.TrimSpace(args)
|
||||||
|
case "drive":
|
||||||
|
if n, ok := num(get(1)); ok && forRX0() {
|
||||||
|
p.Drive = n
|
||||||
|
} else if n, ok := num(get(0)); ok && get(1) == "" {
|
||||||
|
// Some firmware sends "drive:85" with no receiver index.
|
||||||
|
p.Drive = n
|
||||||
|
}
|
||||||
|
case "tune_drive":
|
||||||
|
if n, ok := num(get(1)); ok && forRX0() {
|
||||||
|
p.TuneDrive = n
|
||||||
|
} else if n, ok := num(get(0)); ok && get(1) == "" {
|
||||||
|
p.TuneDrive = n
|
||||||
|
}
|
||||||
|
case "mic_level":
|
||||||
|
if n, ok := num(get(0)); ok {
|
||||||
|
p.MicLevel = n
|
||||||
|
}
|
||||||
|
case "volume":
|
||||||
|
if n, ok := num(get(0)); ok {
|
||||||
|
p.Volume = n
|
||||||
|
}
|
||||||
|
case "mute":
|
||||||
|
p.Mute = yes(get(1))
|
||||||
|
case "agc_mode":
|
||||||
|
if forRX0() {
|
||||||
|
p.AGC = strings.ToLower(strings.TrimSpace(get(1)))
|
||||||
|
}
|
||||||
|
case "sql_enable":
|
||||||
|
if forRX0() {
|
||||||
|
p.SquelchOn = yes(get(1))
|
||||||
|
}
|
||||||
|
case "sql_level":
|
||||||
|
if n, ok := num(get(1)); ok && forRX0() {
|
||||||
|
p.Squelch = n
|
||||||
|
}
|
||||||
|
case "rx_nb_enable":
|
||||||
|
if forRX0() {
|
||||||
|
p.NB = yes(get(1))
|
||||||
|
}
|
||||||
|
case "rx_nr_enable":
|
||||||
|
if forRX0() {
|
||||||
|
p.NR = yes(get(1))
|
||||||
|
}
|
||||||
|
case "rx_anf_enable":
|
||||||
|
if forRX0() {
|
||||||
|
p.ANF = yes(get(1))
|
||||||
|
}
|
||||||
|
case "rx_apf_enable":
|
||||||
|
if forRX0() {
|
||||||
|
p.APF = yes(get(1))
|
||||||
|
}
|
||||||
|
case "rx_filter_band":
|
||||||
|
if forRX0() {
|
||||||
|
if lo, ok := num(get(1)); ok {
|
||||||
|
p.FilterLo = lo
|
||||||
|
}
|
||||||
|
if hi, ok := num(get(2)); ok {
|
||||||
|
p.FilterHi = hi
|
||||||
|
}
|
||||||
|
}
|
||||||
|
case "rit_enable":
|
||||||
|
if forRX0() {
|
||||||
|
p.RIT = yes(get(1))
|
||||||
|
}
|
||||||
|
case "xit_enable":
|
||||||
|
if forRX0() {
|
||||||
|
p.XIT = yes(get(1))
|
||||||
|
}
|
||||||
|
case "rit_offset":
|
||||||
|
if n, ok := num(get(1)); ok && forRX0() {
|
||||||
|
p.RITOffset = n
|
||||||
|
}
|
||||||
|
case "xit_offset":
|
||||||
|
if n, ok := num(get(1)); ok && forRX0() {
|
||||||
|
p.XITOffset = n
|
||||||
|
}
|
||||||
|
case "lock":
|
||||||
|
if forRX0() {
|
||||||
|
p.Lock = yes(get(1))
|
||||||
|
}
|
||||||
|
case "rx_smeter":
|
||||||
|
if n, ok := num(get(1)); ok && forRX0() {
|
||||||
|
p.SMeter = n
|
||||||
|
}
|
||||||
|
case "tune":
|
||||||
|
if forRX0() {
|
||||||
|
p.Tuning = yes(get(1))
|
||||||
|
}
|
||||||
|
case "modulations_list":
|
||||||
|
p.Modulations = splitAndTrim(args)
|
||||||
|
default:
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
// splitAndTrim turns "usb,lsb,cw" into a slice, upper-cased for display.
|
||||||
|
func splitAndTrim(s string) []string {
|
||||||
|
parts := strings.Split(s, ",")
|
||||||
|
out := make([]string, 0, len(parts))
|
||||||
|
for _, p := range parts {
|
||||||
|
if v := strings.ToUpper(strings.TrimSpace(p)); v != "" {
|
||||||
|
out = append(out, v)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return out
|
||||||
|
}
|
||||||
|
|
||||||
|
// TCIPanel returns the console snapshot.
|
||||||
|
func (t *TCI) TCIPanel() TCIPanelState {
|
||||||
|
t.mu.Lock()
|
||||||
|
defer t.mu.Unlock()
|
||||||
|
st := t.panel.st
|
||||||
|
st.Connected = t.conn != nil
|
||||||
|
st.Device = t.device
|
||||||
|
st.TX = t.tx
|
||||||
|
st.Split = t.split
|
||||||
|
st.TXEnabled = t.txAllowed || !t.txAllowedKnown
|
||||||
|
return st
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Setters ───────────────────────────────────────────────────────────────
|
||||||
|
//
|
||||||
|
// Every one of them is a SET in the same vocabulary the radio reports in, and
|
||||||
|
// none of them updates the cached state: the radio answers with the new value,
|
||||||
|
// and taking its word rather than our own is what keeps the panel honest when a
|
||||||
|
// setting is refused, clamped, or changed from the radio's own window a second
|
||||||
|
// later.
|
||||||
|
|
||||||
|
// SetDrive sets the transmit drive, 0-100.
|
||||||
|
func (t *TCI) SetDrive(v int) error { return t.send(fmt.Sprintf("drive:%d;", clampTCIPct(v))) }
|
||||||
|
|
||||||
|
// SetTuneDrive sets the drive used by TUNE, 0-100.
|
||||||
|
func (t *TCI) SetTuneDrive(v int) error { return t.send(fmt.Sprintf("tune_drive:%d;", clampTCIPct(v))) }
|
||||||
|
|
||||||
|
// SetMicLevel sets the microphone gain, 0-100.
|
||||||
|
func (t *TCI) SetMicLevel(v int) error { return t.send(fmt.Sprintf("mic_level:%d;", clampTCIPct(v))) }
|
||||||
|
|
||||||
|
// SetVolume sets the receive volume in dB. TCI's scale is negative — 0 is full
|
||||||
|
// and -60 is inaudible — so this is NOT clamped to a percentage.
|
||||||
|
func (t *TCI) SetVolume(db int) error {
|
||||||
|
if db > 0 {
|
||||||
|
db = 0
|
||||||
|
}
|
||||||
|
if db < -60 {
|
||||||
|
db = -60
|
||||||
|
}
|
||||||
|
return t.send(fmt.Sprintf("volume:%d;", db))
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetMute mutes or unmutes the receiver.
|
||||||
|
func (t *TCI) SetMute(on bool) error { return t.send(fmt.Sprintf("mute:%t;", on)) }
|
||||||
|
|
||||||
|
// SetAGC picks the AGC speed: off, long, slow, med, fast.
|
||||||
|
func (t *TCI) SetAGC(mode string) error {
|
||||||
|
m := strings.ToLower(strings.TrimSpace(mode))
|
||||||
|
switch m {
|
||||||
|
case "off", "long", "slow", "med", "fast":
|
||||||
|
default:
|
||||||
|
return fmt.Errorf("unknown AGC mode %q", mode)
|
||||||
|
}
|
||||||
|
return t.send(fmt.Sprintf("agc_mode:0,%s;", m))
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetSquelch turns the squelch on or off.
|
||||||
|
func (t *TCI) SetSquelch(on bool) error { return t.send(fmt.Sprintf("sql_enable:0,%t;", on)) }
|
||||||
|
|
||||||
|
// SetSquelchLevel sets the threshold in dBm.
|
||||||
|
func (t *TCI) SetSquelchLevel(v int) error { return t.send(fmt.Sprintf("sql_level:0,%d;", v)) }
|
||||||
|
|
||||||
|
// SetNB, SetNR, SetANF, SetAPF switch the receive processing.
|
||||||
|
func (t *TCI) SetNB(on bool) error { return t.send(fmt.Sprintf("rx_nb_enable:0,%t;", on)) }
|
||||||
|
func (t *TCI) SetNR(on bool) error { return t.send(fmt.Sprintf("rx_nr_enable:0,%t;", on)) }
|
||||||
|
func (t *TCI) SetANF(on bool) error { return t.send(fmt.Sprintf("rx_anf_enable:0,%t;", on)) }
|
||||||
|
func (t *TCI) SetAPF(on bool) error { return t.send(fmt.Sprintf("rx_apf_enable:0,%t;", on)) }
|
||||||
|
|
||||||
|
// SetFilter sets the passband edges in Hz.
|
||||||
|
func (t *TCI) SetFilter(lo, hi int) error {
|
||||||
|
if lo > hi {
|
||||||
|
lo, hi = hi, lo
|
||||||
|
}
|
||||||
|
return t.send(fmt.Sprintf("rx_filter_band:0,%d,%d;", lo, hi))
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetRIT / SetXIT switch the offsets on, SetRITOffset / SetXITOffset move them.
|
||||||
|
func (t *TCI) SetRIT(on bool) error { return t.send(fmt.Sprintf("rit_enable:0,%t;", on)) }
|
||||||
|
func (t *TCI) SetXIT(on bool) error { return t.send(fmt.Sprintf("xit_enable:0,%t;", on)) }
|
||||||
|
func (t *TCI) SetRITOffset(hz int) error { return t.send(fmt.Sprintf("rit_offset:0,%d;", hz)) }
|
||||||
|
func (t *TCI) SetXITOffset(hz int) error { return t.send(fmt.Sprintf("xit_offset:0,%d;", hz)) }
|
||||||
|
|
||||||
|
// SetLock locks the VFO knob on the radio.
|
||||||
|
func (t *TCI) SetLock(on bool) error { return t.send(fmt.Sprintf("lock:0,%t;", on)) }
|
||||||
|
|
||||||
|
// SetTune starts or stops the tune carrier.
|
||||||
|
//
|
||||||
|
// It TRANSMITS, at tune_drive rather than at drive — which is the setting to
|
||||||
|
// check before pressing it, and why the panel shows the two side by side.
|
||||||
|
func (t *TCI) SetTune(on bool) error { return t.send(fmt.Sprintf("tune:0,%t;", on)) }
|
||||||
|
|
||||||
|
func clampTCIPct(v int) int {
|
||||||
|
if v < 0 {
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
if v > 100 {
|
||||||
|
return 100
|
||||||
|
}
|
||||||
|
return v
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user