feat(entry): type a frequency or a band into the call field
An operator reading a cluster page sees 28500 and wants to be there. The hands are on the callsign field; reaching for the frequency box, or the mouse, is the part that loses the station. So the call field takes a bare number on Enter: kHz for a frequency, or a band on its own. Ambiguity resolves to the band on purpose -- 40 is 40 m, not 40 kHz -- because neither of those kHz values is anywhere a radio tunes, and it reads the way an operator says it. Anything with a letter in it is a callsign and behaves exactly as before; a number that is neither a band nor a plausible frequency does nothing at all. Bands go through the ordinary band change, so the antennas, the per-band power table and the outbound integrations hear about it as they would from the dropdown. Also adds 4 m to the QSY table, so a band that can now be typed is a band that has a frequency to go to.
This commit is contained in:
@@ -292,6 +292,37 @@ function shortCatError(err?: string): string {
|
||||
}
|
||||
// bandForMHz maps a dial frequency (MHz) to its ADIF band, or '' if outside
|
||||
// every known allocation (used to auto-fill the band when the freq changes).
|
||||
// A number typed into the CALLSIGN field is a QSY, not a callsign.
|
||||
//
|
||||
// It is where the hands already are: an operator watching a cluster or a
|
||||
// spotting page reads "28500" and wants to be there, and reaching for the
|
||||
// frequency box — or worse, the mouse — is the part that loses the station. So
|
||||
// the call field takes it, on Enter, and nothing else in the entry form changes.
|
||||
//
|
||||
// Two forms, and the AMBIGUOUS ones resolve to the band deliberately. "160" is
|
||||
// the 160 m band, not 160 kHz; "40" is 40 m, not 40 kHz. Neither number is a
|
||||
// frequency anyone tunes to, so reading them as bands costs nothing and reads
|
||||
// the way an operator says it out loud.
|
||||
const ENTRY_BAND_WORDS: Record<string, string> = {
|
||||
'2200': '2200m', '630': '630m', '160': '160m', '80': '80m', '60': '60m',
|
||||
'40': '40m', '30': '30m', '20': '20m', '17': '17m', '15': '15m', '12': '12m',
|
||||
'10': '10m', '6': '6m', '4': '4m', '2': '2m', '70': '70cm',
|
||||
};
|
||||
|
||||
// entryQSYCommand reads what was typed. Returns null for anything that is not
|
||||
// unambiguously one of the two — a callsign must never be mistaken for a QSY.
|
||||
function entryQSYCommand(text: string): { band?: string; hz?: number } | null {
|
||||
const v = text.trim();
|
||||
if (!/^\d{1,7}$/.test(v)) return null; // digits only: a callsign has letters
|
||||
const band = ENTRY_BAND_WORDS[v];
|
||||
if (band) return { band };
|
||||
const khz = Number(v);
|
||||
// 1500 kHz to 1.3 GHz: below that is not a band anyone works, above it is a
|
||||
// typo. A bare "500" is neither a band nor a frequency, so nothing happens.
|
||||
if (khz >= 1500 && khz <= 1_300_000) return { hz: Math.round(khz * 1000) };
|
||||
return null;
|
||||
}
|
||||
|
||||
function bandForMHz(mhz: number): string {
|
||||
if (!mhz || isNaN(mhz)) return '';
|
||||
const plan: [number, number, string][] = [
|
||||
@@ -702,6 +733,7 @@ export default function App() {
|
||||
'15m': { SSB: 21300000, CW: 21030000, FT8: 21074000, FT4: 21140000, RTTY: 21080000, default: 21300000 },
|
||||
'12m': { SSB: 24950000, CW: 24910000, FT8: 24915000, FT4: 24919000, RTTY: 24920000, default: 24950000 },
|
||||
'10m': { SSB: 28500000, CW: 28030000, FT8: 28074000, FT4: 28180000, RTTY: 28080000, default: 28500000 },
|
||||
'4m': { SSB: 70200000, CW: 70100000, FT8: 70154000, default: 70200000 },
|
||||
'6m': { SSB: 50150000, CW: 50080000, FT8: 50313000, FT4: 50318000, default: 50150000 },
|
||||
'2m': { SSB: 144300000, CW: 144050000, FT8: 144174000, default: 144300000 },
|
||||
'70cm': { SSB: 432300000, CW: 432050000, FT8: 432174000, default: 432300000 },
|
||||
@@ -5469,6 +5501,25 @@ export default function App() {
|
||||
noteManualEdit();
|
||||
SetCATFrequency(Math.round(mhz * 1_000_000)).catch(() => {});
|
||||
};
|
||||
// Carry out what was typed in the call field. Bands go through the ordinary
|
||||
// band change, so the antennas, the power table and the outbound integrations
|
||||
// all hear about it exactly as they would from the dropdown.
|
||||
const applyEntryQSY = (q: { band?: string; hz?: number }) => {
|
||||
if (q.band) {
|
||||
onBandUserChange(q.band);
|
||||
showToast(q.band);
|
||||
return;
|
||||
}
|
||||
const hz = q.hz ?? 0;
|
||||
if (hz <= 0) return;
|
||||
noteManualEdit();
|
||||
setFreqMhz((hz / 1_000_000).toFixed(6));
|
||||
const b = bandForMHz(hz / 1_000_000);
|
||||
if (b) setBand(b);
|
||||
if (catState.enabled && catState.connected) SetCATFrequency(hz).catch(() => {});
|
||||
showToast((hz / 1_000_000).toFixed(3) + ' MHz');
|
||||
};
|
||||
|
||||
// Mouse-wheel over a kHz digit of the top frequency readout: step the frequency
|
||||
// and (if CAT is connected) QSY the rig. The display updates optimistically on
|
||||
// every notch; the actual radio tune is debounced so a fast scroll doesn't flood
|
||||
@@ -6835,6 +6886,16 @@ export default function App() {
|
||||
onKeyDown={(e) => {
|
||||
if (e.key === 'Enter' && (e.target as HTMLElement).tagName === 'INPUT') {
|
||||
e.preventDefault();
|
||||
// A number in the call field is a QSY. Checked BEFORE ESM and before
|
||||
// logging: both would otherwise act on a "callsign" of 28500.
|
||||
if (e.target === callsignRef.current) {
|
||||
const q = entryQSYCommand(callsignRef.current?.value ?? '');
|
||||
if (q) {
|
||||
applyEntryQSY(q);
|
||||
setCallsign(''); // the field goes back to what it is for
|
||||
return;
|
||||
}
|
||||
}
|
||||
// ESM (Enter Sends Message): fire the stage-appropriate CW macro instead
|
||||
// of logging. Falls through to the normal log when ESM isn't active.
|
||||
if (esmHandleEnter(e.target as HTMLElement)) return;
|
||||
|
||||
Reference in New Issue
Block a user