feat(icom): a DATA button, native PSK, real watts — and the audio codec
The console gains a DATA mode button (USB-D, what FT8 and friends ride on); the PSK button now maps to the rigs' native PSK mode (0x12) instead of erroring as an unknown mode — rigs without one NAK it, exactly as RS-BA1's button does there. The power meter shows real watts on the IC-7760, whose meter face runs to 250 W where a 100 W rig's percentage was the watts. The clickable S-meter gets a pointer cursor so it looks like what it is. Network audio: rxcodec 0x04 — the experiments on the real 7760 settle the codec table (0x10 = 16-bit stereo, 0x02 = 8-bit mono, both heard as garble), and 0x04 is the 16-bit mono the playback path actually plays.
This commit is contained in:
+6
-2
@@ -8,7 +8,9 @@
|
|||||||
"Icom: the IC-7760 joins the model list (CI-V address B2h).",
|
"Icom: the IC-7760 joins the model list (CI-V address B2h).",
|
||||||
"Icom console: the IC-7760 gets its real attenuator steps — 6/12/18 dB.",
|
"Icom console: the IC-7760 gets its real attenuator steps — 6/12/18 dB.",
|
||||||
"Icom network audio: the RX stream is decoded correctly — mono is requested and the payload offset was confirmed on a real IC-7760, curing the garbled, clicking audio.",
|
"Icom network audio: the RX stream is decoded correctly — mono is requested and the payload offset was confirmed on a real IC-7760, curing the garbled, clicking audio.",
|
||||||
"Icom: switching back from a data mode (USB-D1) to plain USB now sticks — if the rig ignores the data-flag command, it is repeated with the modern one-frame form (0x26). Seen on the IC-7760."
|
"Icom: switching back from a data mode (USB-D1) to plain USB now sticks — if the rig ignores the data-flag command, it is repeated with the modern one-frame form (0x26). Seen on the IC-7760.",
|
||||||
|
"Icom console: a DATA mode button (USB-D, what FT8 wants), the PSK button drives the rigs that have a native PSK mode, and the power meter reads in real watts on the IC-7760’s 250 W scale.",
|
||||||
|
"Icom network audio: the right codec is requested (16-bit mono LPCM), settled by experiment on a real IC-7760."
|
||||||
],
|
],
|
||||||
"fr": [
|
"fr": [
|
||||||
"Console Elecraft : le S-mètre est calibré sur un vrai K3 — S9 et les +dB correspondent désormais à l’affichage de la radio (il lisait environ deux points S trop bas).",
|
"Console Elecraft : le S-mètre est calibré sur un vrai K3 — S9 et les +dB correspondent désormais à l’affichage de la radio (il lisait environ deux points S trop bas).",
|
||||||
@@ -16,7 +18,9 @@
|
|||||||
"Icom : l’IC-7760 rejoint la liste des modèles (adresse CI-V B2h).",
|
"Icom : l’IC-7760 rejoint la liste des modèles (adresse CI-V B2h).",
|
||||||
"Console Icom : l’IC-7760 reçoit ses vrais crans d’atténuateur — 6/12/18 dB.",
|
"Console Icom : l’IC-7760 reçoit ses vrais crans d’atténuateur — 6/12/18 dB.",
|
||||||
"Audio réseau Icom : le flux RX est décodé correctement — le mono est demandé et l’offset du payload a été confirmé sur un vrai IC-7760, ce qui guérit le son inaudible et les clics.",
|
"Audio réseau Icom : le flux RX est décodé correctement — le mono est demandé et l’offset du payload a été confirmé sur un vrai IC-7760, ce qui guérit le son inaudible et les clics.",
|
||||||
"Icom : revenir d’un mode data (USB-D1) au USB simple tient désormais — si la radio ignore la commande du drapeau data, elle est répétée sous la forme moderne en une trame (0x26). Constaté sur l’IC-7760."
|
"Icom : revenir d’un mode data (USB-D1) au USB simple tient désormais — si la radio ignore la commande du drapeau data, elle est répétée sous la forme moderne en une trame (0x26). Constaté sur l’IC-7760.",
|
||||||
|
"Console Icom : un bouton de mode DATA (USB-D, celui de FT8), le bouton PSK pilote les radios qui ont un vrai mode PSK, et le wattmètre lit en watts réels sur l’échelle 250 W de l’IC-7760.",
|
||||||
|
"Audio réseau Icom : le bon codec est demandé (LPCM mono 16 bits), déterminé par l’expérience sur un vrai IC-7760."
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -79,7 +79,7 @@ function bandsFor(model?: string): Band[] {
|
|||||||
|
|
||||||
// Mode buttons for the console (like RS-BA1's row). SetCATMode picks USB/LSB for
|
// Mode buttons for the console (like RS-BA1's row). SetCATMode picks USB/LSB for
|
||||||
// SSB by frequency and the rig's data variant for digital modes.
|
// SSB by frequency and the rig's data variant for digital modes.
|
||||||
const MODES = ['SSB', 'CW', 'RTTY', 'PSK', 'AM', 'FM'];
|
const MODES = ['SSB', 'CW', 'RTTY', 'PSK', 'AM', 'FM', 'DATA'];
|
||||||
|
|
||||||
// Attenuator steps are MODEL-dependent even though the CI-V command (0x11) is the
|
// Attenuator steps are MODEL-dependent even though the CI-V command (0x11) is the
|
||||||
// same: the value byte is the dB. The IC-7610 (and 7700/7800/7851) have a 6/12/18
|
// same: the value byte is the dB. The IC-7610 (and 7700/7800/7851) have a 6/12/18
|
||||||
@@ -132,6 +132,10 @@ function fmtVFO(hz?: number): string {
|
|||||||
function modeMatches(btn: string, cur?: string): boolean {
|
function modeMatches(btn: string, cur?: string): boolean {
|
||||||
if (!cur) return false;
|
if (!cur) return false;
|
||||||
if (btn === 'SSB') return cur === 'SSB' || cur === 'USB' || cur === 'LSB';
|
if (btn === 'SSB') return cur === 'SSB' || cur === 'USB' || cur === 'LSB';
|
||||||
|
// The backend surfaces USB-D as the operator's digital default (FT8…), or as
|
||||||
|
// plain DATA — either way it is the DATA button that should light.
|
||||||
|
if (btn === 'DATA') return ['DATA', 'FT8', 'FT4', 'JS8', 'JT65', 'JT9', 'MFSK', 'OLIVIA'].includes(cur);
|
||||||
|
if (btn === 'PSK') return cur === 'PSK' || cur === 'PSK31';
|
||||||
return btn === cur;
|
return btn === cur;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -246,7 +250,7 @@ function Meter({ label, value, accent, scale, onClick, title }: { label: string;
|
|||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
if (onClick) {
|
if (onClick) {
|
||||||
return <button type="button" onClick={onClick} title={title} className="flex items-center gap-2 w-full rounded hover:bg-muted/50 -mx-1 px-1 py-0.5">{body}</button>;
|
return <button type="button" onClick={onClick} title={title} className="flex items-center gap-2 w-full rounded cursor-pointer hover:bg-muted/50 -mx-1 px-1 py-0.5">{body}</button>;
|
||||||
}
|
}
|
||||||
return <div className="flex items-center gap-2">{body}</div>;
|
return <div className="flex items-center gap-2">{body}</div>;
|
||||||
}
|
}
|
||||||
@@ -718,7 +722,7 @@ export function IcomPanel({ onReportRST, isNetwork = false }: { onReportRST?: (r
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{/* Mode selector row (RS-BA1's SSB/CW/RTTY/PSK/AM/FM). */}
|
{/* Mode selector row (RS-BA1's SSB/CW/RTTY/PSK/AM/FM). */}
|
||||||
<div className="grid grid-cols-6 border-t border-border/60 divide-x divide-border/60">
|
<div className="grid grid-cols-7 border-t border-border/60 divide-x divide-border/60">
|
||||||
{MODES.map((m) => {
|
{MODES.map((m) => {
|
||||||
const on = modeMatches(m, curMode);
|
const on = modeMatches(m, curMode);
|
||||||
return (
|
return (
|
||||||
@@ -739,7 +743,11 @@ export function IcomPanel({ onReportRST, isNetwork = false }: { onReportRST?: (r
|
|||||||
title={onReportRST ? t('rst.clickToFill') : undefined}
|
title={onReportRST ? t('rst.clickToFill') : undefined}
|
||||||
onClick={onReportRST ? () => onReportRST(sMeterRST(sp.s, sp.over, st.mode)) : undefined} />
|
onClick={onReportRST ? () => onReportRST(sMeterRST(sp.s, sp.over, st.mode)) : undefined} />
|
||||||
); })()}
|
); })()}
|
||||||
<Meter label="Po" value={st.power_meter} accent="#ef4444" scale={`${st.power_meter} W`} />
|
{/* The meter reads 0-100% of the rig's own scale. On a 100 W rig the
|
||||||
|
percentage IS the watts; the IC-7760's meter runs to 250 W (the rig
|
||||||
|
makes 200), so its percentage is worth 2.5 W a point — same face as
|
||||||
|
the radio's. */}
|
||||||
|
<Meter label="Po" value={st.power_meter} accent="#ef4444" scale={`${Math.round(st.power_meter * (((st.model ?? '').includes('7760')) ? 2.5 : 1))} W`} />
|
||||||
<Meter label="SWR" value={st.swr_meter} accent="#f59e0b" scale={st.swr_meter > 0 ? `${(1 + st.swr_meter / 33.3).toFixed(1)}` : '1.0'} />
|
<Meter label="SWR" value={st.swr_meter} accent="#f59e0b" scale={st.swr_meter > 0 ? `${(1 + st.swr_meter / 33.3).toFixed(1)}` : '1.0'} />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
@@ -131,6 +131,8 @@ const (
|
|||||||
ModeFM = 0x05
|
ModeFM = 0x05
|
||||||
ModeCWR = 0x07
|
ModeCWR = 0x07
|
||||||
ModeRTTYR = 0x08
|
ModeRTTYR = 0x08
|
||||||
|
ModePSK = 0x12 // native PSK (IC-7610/7760/7851 class; older rigs NAK it)
|
||||||
|
ModePSKR = 0x13
|
||||||
)
|
)
|
||||||
|
|
||||||
// Frame builds a complete CI-V frame (preamble … end) for payload, which is the
|
// Frame builds a complete CI-V frame (preamble … end) for payload, which is the
|
||||||
@@ -306,6 +308,8 @@ func ModeToADIF(m byte, data bool) string {
|
|||||||
return "CW"
|
return "CW"
|
||||||
case ModeRTTY, ModeRTTYR:
|
case ModeRTTY, ModeRTTYR:
|
||||||
return "RTTY"
|
return "RTTY"
|
||||||
|
case ModePSK, ModePSKR:
|
||||||
|
return "PSK31"
|
||||||
case ModeAM:
|
case ModeAM:
|
||||||
return "AM"
|
return "AM"
|
||||||
case ModeFM:
|
case ModeFM:
|
||||||
|
|||||||
@@ -849,12 +849,12 @@ func icnConnInfo(seq, innerSeq, tokReq uint16, sentid, rcvdid, token uint32, use
|
|||||||
copy(b[0x60:0x70], icnPasscode(user))
|
copy(b[0x60:0x70], icnPasscode(user))
|
||||||
b[0x70] = rxEnable // rxenable: 1 opens the 50003 RX audio stream, 0 = CI-V only
|
b[0x70] = rxEnable // rxenable: 1 opens the 50003 RX audio stream, 0 = CI-V only
|
||||||
b[0x71] = 0x00 // txenable (Phase 5)
|
b[0x71] = 0x00 // txenable (Phase 5)
|
||||||
// rxcodec 0x02 = LPCM, ONE channel, 16-bit. 0x10 (two channels) was asked
|
// rxcodec 0x04 = LPCM, ONE channel, 16-BIT — settled by experiment on a
|
||||||
// for at first, and a real IC-7760 duly sent interleaved stereo — which the
|
// real IC-7760, one wrong guess at a time: 0x10 produced 1280-byte payloads
|
||||||
// 16 kHz mono playback path rendered as garble at double speed. Mono is what
|
// of interleaved 16-bit stereo, 0x02 produced 320-byte payloads of 8-bit
|
||||||
// the monitor plays and half the bandwidth; the second channel of an RX
|
// mono (samples hugging 0x80). 0x04 sits between them in the codec table
|
||||||
// stream carries nothing a logger wants.
|
// (as in wfview): 16-bit mono, the format the 16 kHz playback path plays.
|
||||||
b[0x72] = 0x02 // rxcodec
|
b[0x72] = 0x04 // rxcodec
|
||||||
b[0x73] = 0x04 // txcodec
|
b[0x73] = 0x04 // txcodec
|
||||||
icnBE.PutUint32(b[0x74:], 16000)
|
icnBE.PutUint32(b[0x74:], 16000)
|
||||||
icnBE.PutUint32(b[0x78:], 8000)
|
icnBE.PutUint32(b[0x78:], 8000)
|
||||||
|
|||||||
@@ -1494,6 +1494,12 @@ func (b *IcomSerial) modeCode(mode string) (code byte, data bool, err error) {
|
|||||||
return civ.ModeFM, false, nil
|
return civ.ModeFM, false, nil
|
||||||
case "RTTY", "FSK":
|
case "RTTY", "FSK":
|
||||||
return civ.ModeRTTY, false, nil
|
return civ.ModeRTTY, false, nil
|
||||||
|
case "PSK":
|
||||||
|
// The console button, not the ADIF mode: the rigs that HAVE a native PSK
|
||||||
|
// mode (7610/7760/7851 class) get it; a 7300 NAKs 0x12 and the button
|
||||||
|
// stays dead there, exactly as RS-BA1's does. Soundcard PSK31 stays on
|
||||||
|
// USB-D below, where every rig can do it.
|
||||||
|
return civ.ModePSK, false, nil
|
||||||
case "FT8", "FT4", "PSK31", "MFSK", "JS8", "JT65", "JT9", "OLIVIA", "DATA", "DIGITALVOICE":
|
case "FT8", "FT4", "PSK31", "MFSK", "JS8", "JT65", "JT9", "OLIVIA", "DATA", "DIGITALVOICE":
|
||||||
// Digital data modes ride on USB with the data flag set (FT8 etc.).
|
// Digital data modes ride on USB with the data flag set (FT8 etc.).
|
||||||
return civ.ModeUSB, true, nil
|
return civ.ModeUSB, true, nil
|
||||||
|
|||||||
Reference in New Issue
Block a user