diff --git a/app.go b/app.go index b6fe4d8..535e3b4 100644 --- a/app.go +++ b/app.go @@ -14790,8 +14790,14 @@ func (a *App) AmpOperate(id string, on bool) error { } // AmpPower turns the given amp on/off (SPE and ACOM; the PGXL has no power -// command on its direct link). -func (a *App) AmpPower(id string, on bool) error { +// command on its direct link). Logged here because the UI buttons swallow the +// returned error — a failed power-on otherwise looks like a dead button. +func (a *App) AmpPower(id string, on bool) (err error) { + defer func() { + if err != nil { + applog.Printf("amp %s: power %v failed: %v", id, on, err) + } + }() inst := a.ampInstByID(id) if inst == nil { return fmt.Errorf("amplifier not running — check Settings → Amplifier") diff --git a/changelog.json b/changelog.json index 64c6340..48e35e5 100644 --- a/changelog.json +++ b/changelog.json @@ -5,12 +5,24 @@ "en": [ "Awards: the CW / Phone / Digital filter now applies to the whole award — bands, counts, confirmations and the contacts behind a cell — instead of only hiding rows from the reference list.", "Yaesu and Kenwood: a new option lowers the DTR and RTS lines on connect, for interfaces that read either as PTT and keep the radio transmitting while OpsLog is open. Off by default — lowering them stops other interfaces transmitting at all.", - "Rig panels: the mouse wheel only moves a slider you have clicked on first. Scrolling the panel used to change whatever control the pointer happened to be over — including transmit power." + "SPE amplifiers: the ON and OFF buttons now switch the amplifier on and off over USB.", + "Quieter diagnostic log: no more FlexRadio meter subscriptions, per-packet WSJT-X/MSHV lines, or repeated Tuner Genius / PGXL / SPE status frames.", + "Station Control: the cards now line up on a grid — same width, and same height across a row — and the column selector goes up to 6.", + "New amplifier widget beside the keyers (flame icon): Operate, power ON/OFF, the SPE power level and live watts / SWR / temperature. With several amplifiers a dropdown on the icon picks the one to watch, or all of them.", + "Icom: the meters and front-panel readings are polled only while the Icom console is on screen. They were half of the CI-V traffic at all times, which matters when the link is shared — a microHAM Router splitting one CI-V port between OpsLog and WSJT-X, for instance.", + "Icom: the power-ON button sends a wake preamble sized for the CI-V speed. It was a fixed length that only suited 19200 baud, so a radio set faster — 115200 is the default — ignored the command.", + "The Tuner Genius power meter now shows the peak the device itself measured, instead of whatever instant the poll happened to catch — a kilowatt transmission could read a few hundred watts. Every power meter also clears the moment the carrier drops, instead of trailing several seconds behind it." ], "fr": [ "Diplômes : le filtre CW / Phonie / Numérique s’applique désormais à tout le diplôme — bandes, comptes, confirmations et QSO derrière une case — au lieu de masquer seulement des lignes de la liste des références.", "Yaesu et Kenwood : une option abaisse les lignes DTR et RTS à la connexion, pour les interfaces qui les lisent comme PTT et laissent la radio en émission tant qu’OpsLog est ouvert. Désactivée par défaut — les abaisser empêche d’autres interfaces d’émettre.", - "Panneaux radio : la molette n’agit que sur un curseur préalablement cliqué. Faire défiler le panneau modifiait le réglage survolé par le pointeur — y compris la puissance d’émission." + "Amplificateurs SPE : les boutons ON et OFF allument et éteignent désormais l’amplificateur par l’USB.", + "Journal de diagnostic allégé : plus d’abonnements aux mètres FlexRadio, de ligne par paquet WSJT-X/MSHV, ni de trames statut Tuner Genius / PGXL / SPE répétées.", + "Contrôle station : les cartes s’alignent désormais sur une grille — même largeur, et même hauteur sur une ligne — et le sélecteur de colonnes va jusqu’à 6.", + "Nouveau widget amplificateur à côté des manipulateurs (icône flamme) : Operate, marche/arrêt, le niveau de puissance SPE et les watts / ROS / température en direct. Avec plusieurs amplis, une liste déroulante sur l’icône choisit celui à surveiller, ou tous.", + "Icom : les mesures et les réglages de façade ne sont interrogés que lorsque la console Icom est affichée. Ils représentaient la moitié du trafic CI-V en permanence, ce qui compte quand la liaison est partagée — un Router microHAM qui répartit un port CI-V entre OpsLog et WSJT-X, par exemple.", + "Icom : le bouton de mise sous tension envoie un préambule de réveil dimensionné pour la vitesse CI-V. Sa longueur était fixe et ne convenait qu’à 19200 bauds, si bien qu’une radio réglée plus vite — 115200 par défaut — ignorait la commande.", + "Le mètre de puissance du Tuner Genius affiche désormais la crête mesurée par l’appareil lui-même, au lieu de l’instant capté par l’interrogation — une émission d’un kilowatt pouvait afficher quelques centaines de watts. Tous les mètres de puissance retombent aussi dès la fin de l’émission, au lieu de traîner plusieurs secondes." ] }, { diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index b1ccdf3..b8201e1 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -1,6 +1,6 @@ import { useCallback, useEffect, useMemo, useRef, useState } from 'react'; import { - Activity, AlertCircle, Antenna, Bell, Check, CheckCircle2, ChevronDown, Clock, CloudOff, Compass, Database, Ear, Eraser, Gauge, Hash, Loader2, Lock, + Activity, AlertCircle, Antenna, Bell, Check, CheckCircle2, ChevronDown, Clock, CloudOff, Compass, Database, Ear, Eraser, Flame, Gauge, Hash, Loader2, Lock, Maximize2, Minimize2, Mic, MessageSquare, Pencil, Radio, RadioTower, RefreshCw, Satellite, Send, Settings, SlidersHorizontal, SpellCheck, Square, Terminal, Trash2, Unlock, X, Zap, } from 'lucide-react'; @@ -77,6 +77,7 @@ import { IcomPanel } from '@/components/IcomPanel'; import { YaesuPanel } from '@/components/YaesuPanel'; import { AntGeniusPanel, type AGStatus } from '@/components/AntGeniusPanel'; import { TunerGeniusPanel, type TGStatus } from '@/components/TunerGeniusPanel'; +import { AmpWidget } from '@/components/AmpWidget'; import { ScpPanel, type ScpResult } from '@/components/ScpPanel'; import { FilterBuilder, type QueryFilter } from '@/components/FilterBuilder'; import { AwardsPanel } from '@/components/AwardsPanel'; @@ -570,6 +571,16 @@ export default function App() { // PGXL's OPERATE state comes from the radio when it reports the amp. const [ampSts, setAmpSts] = useState([]); const [flexAmp, setFlexAmp] = useState(null); + // Amplifier widget: whether it is shown, and which amp — "all", or one amp id. + // Several amps side by side make a wide widget, so an operator running two + // picks the one he watches while transmitting. Declared here because the poll + // below runs faster while the widget is open. + const [showAmpWidget, setShowAmpWidget] = useState(() => localStorage.getItem('opslog.showAmpWidget') !== '0'); + const [ampWidgetSel, setAmpWidgetSel] = useState(() => localStorage.getItem('opslog.ampSel.widget') || 'all'); + // Poll fast only while the amplifier widget is open: its meters must track TX + // the way the FlexRadio panel's do (400 ms), or the power reads seconds behind + // the transmission and looks like the app is struggling. The status-bar chips + // alone need nothing like that rate. useEffect(() => { let alive = true; const tick = () => Promise.all([ @@ -577,9 +588,9 @@ export default function App() { GetFlexState().catch(() => null), ]).then(([l, fx]: any[]) => { if (alive) { setAmpSts((l ?? []) as any[]); setFlexAmp(fx); } }); tick(); - const id = window.setInterval(tick, 2000); + const id = window.setInterval(tick, showAmpWidget ? 400 : 2000); return () => { alive = false; window.clearInterval(id); }; - }, []); + }, [showAmpWidget]); // Multi-op "who's on air" widget: every operator's live status from the shared // MySQL logbook (freq/mode/version). Only polled on a MySQL logbook. type LiveStation = { operator: string; station: string; freq_hz: number; band: string; mode: string; online: boolean; version: string; age_sec: number }; @@ -4849,6 +4860,60 @@ export default function App() { {showTuner && tgStatus.connected && } )} + {/* Amplifier widget. With one amp the icon is a plain toggle; with + several it also carries a dropdown to choose which one the widget + shows (or All), since two amps stacked make a tall widget. */} + {ampSts.length > 0 && ( +
+ + {ampSts.length > 1 && ( + + + + + + {[{ id: 'all', name: t('ampw.all') }, ...ampSts.map((a: any) => ({ id: a.id, name: a.name || a.id }))].map((o) => ( + { + setAmpWidgetSel(o.id); + writeUiPref('opslog.ampSel.widget', o.id); + // Picking an amp means "show me this one" — opening the + // widget too saves a second click on a hidden one. + if (!showAmpWidget) { setShowAmpWidget(true); writeUiPref('opslog.showAmpWidget', '1'); } + }}> + + {ampWidgetSel === o.id + ? + : } + {o.name} + + + ))} + + + )} +
+ )} {scpEnabled && ( + {/* Power ON pulses RTS then DTR — it must stay clickable while the amp + is off (no status = not "connected"), and serial only. */}
- - - {operate ? t('flxp.ampInLine') : t('flxp.ampBypassed')} - {(pg.host || pg.connected) && (