diff --git a/src/core/KiwiSdrClient.cpp b/src/core/KiwiSdrClient.cpp index 4bed5f890..fb4069d8c 100644 --- a/src/core/KiwiSdrClient.cpp +++ b/src/core/KiwiSdrClient.cpp @@ -975,7 +975,7 @@ void KiwiSdrClient::disconnectFromEndpoint() void KiwiSdrClient::setTrackedSlice(int sliceId, double frequencyMhz, const QString& mode, int filterLowHz, int filterHighHz, const QString& panId, - const QString& bandName) + const QString& bandName, int cwPitchHz) { const QString normalizedBandName = bandName.trimmed(); if (m_trackedSliceId == sliceId @@ -984,7 +984,8 @@ void KiwiSdrClient::setTrackedSlice(int sliceId, double frequencyMhz, && m_trackedFilterLowHz == filterLowHz && m_trackedFilterHighHz == filterHighHz && m_trackedPanId == panId - && m_trackedBandName == normalizedBandName) { + && m_trackedBandName == normalizedBandName + && m_trackedCwPitchHz == cwPitchHz) { return; } @@ -998,6 +999,7 @@ void KiwiSdrClient::setTrackedSlice(int sliceId, double frequencyMhz, m_trackedFilterLowHz = filterLowHz; m_trackedFilterHighHz = filterHighHz; m_trackedPanId = panId; + m_trackedCwPitchHz = cwPitchHz; if (bandChanged) { resetWaterfallAutoScaleHistory(); } @@ -1391,11 +1393,8 @@ void KiwiSdrClient::sendTrackedSliceToServer() << "high_cut=" << highCutHz; return; } - sendSoundCommand(QStringLiteral("SET mod=%1 low_cut=%2 high_cut=%3 freq=%4") - .arg(mode) - .arg(lowCutHz) - .arg(highCutHz) - .arg(freqKhz, 0, 'f', 3)); + sendSoundCommand(KiwiSdrProtocol::formatSoundTuneCommand( + mode, lowCutHz, highCutHz, freqKhz, m_trackedCwPitchHz)); } void KiwiSdrClient::sendReceiverControlsToServer() diff --git a/src/core/KiwiSdrClient.h b/src/core/KiwiSdrClient.h index ea33fb529..a879fbddd 100644 --- a/src/core/KiwiSdrClient.h +++ b/src/core/KiwiSdrClient.h @@ -105,7 +105,7 @@ public slots: void setTrackedSlice(int sliceId, double frequencyMhz, const QString& mode, int filterLowHz, int filterHighHz, const QString& panId, - const QString& bandName); + const QString& bandName, int cwPitchHz); void setWaterfallView(const QString& panId, double centerMhz, double bandwidthMhz); void setWaterfallLineDurationMs(int lineDurationMs); @@ -278,6 +278,7 @@ public slots: QString m_trackedMode; int m_trackedFilterLowHz{0}; int m_trackedFilterHighHz{0}; + int m_trackedCwPitchHz{600}; QString m_trackedPanId; bool m_userDisconnecting{false}; bool m_secureWebSocket{false}; diff --git a/src/core/KiwiSdrManager.cpp b/src/core/KiwiSdrManager.cpp index 3a0825122..9e20f7ae4 100644 --- a/src/core/KiwiSdrManager.cpp +++ b/src/core/KiwiSdrManager.cpp @@ -440,7 +440,7 @@ void KiwiSdrManager::connectProfile(const QString& id) m_clientHasTrackedSlice.insert(id, false); invokeClient(id, [](KiwiSdrClient* client) { client->setTrackedSlice(-1, 0.0, QString(), 0, 0, QString(), - QString()); + QString(), 0); }); emit profileNeedsInitialTracking(id); } @@ -521,7 +521,8 @@ void KiwiSdrManager::primeProfileTracking(const QString& id, int sliceId, double centerMhz, double bandwidthMhz, int lineDurationMs, - const QString& bandName) + const QString& bandName, + int cwPitchHz) { if (!hasProfile(id) || sliceId < 0 || frequencyMhz <= 0.0) { return; @@ -532,10 +533,10 @@ void KiwiSdrManager::primeProfileTracking(const QString& id, int sliceId, m_clientHasTrackedSlice.insert(id, true); invokeClient(id, [sliceId, frequencyMhz, mode, filterLowHz, filterHighHz, panId, lineDurationMs, - centerMhz, bandwidthMhz, bandName]( + centerMhz, bandwidthMhz, bandName, cwPitchHz]( KiwiSdrClient* client) { client->setTrackedSlice(sliceId, frequencyMhz, mode, filterLowHz, - filterHighHz, panId, bandName); + filterHighHz, panId, bandName, cwPitchHz); client->setWaterfallLineDurationMs(lineDurationMs); if (!panId.isEmpty() && centerMhz > 0.0 && bandwidthMhz > 0.0) { client->setWaterfallView(panId, centerMhz, bandwidthMhz); @@ -549,7 +550,8 @@ void KiwiSdrManager::assignSliceToProfile(int sliceId, const QString& profileId, const QString& mode, int filterLowHz, int filterHighHz, const QString& panId, - const QString& bandName) + const QString& bandName, + int cwPitchHz) { if (sliceId < 0 || !hasProfile(profileId)) { clearSliceAssignment(sliceId); @@ -597,10 +599,11 @@ void KiwiSdrManager::assignSliceToProfile(int sliceId, const QString& profileId, const bool connected = KiwiSdrClient::stateHasReceiveAudio(state(profileId)); invokeClient(profileId, [sliceId, frequencyMhz, mode, filterLowHz, - filterHighHz, panId, bandName, connected]( + filterHighHz, panId, bandName, connected, + cwPitchHz]( KiwiSdrClient* client) { client->setTrackedSlice(sliceId, frequencyMhz, mode, filterLowHz, - filterHighHz, panId, bandName); + filterHighHz, panId, bandName, cwPitchHz); client->setAudioActive(connected); }); } @@ -639,7 +642,8 @@ void KiwiSdrManager::updateSliceTracking(int sliceId, double frequencyMhz, const QString& mode, int filterLowHz, int filterHighHz, const QString& panId, - const QString& bandName) + const QString& bandName, + int cwPitchHz) { const QString profileId = m_sliceAssignments.value(sliceId); if (profileId.isEmpty()) { @@ -650,10 +654,10 @@ void KiwiSdrManager::updateSliceTracking(int sliceId, double frequencyMhz, m_clientHasTrackedSlice.insert(profileId, sliceId >= 0 && frequencyMhz > 0.0); invokeClient(profileId, [sliceId, frequencyMhz, mode, filterLowHz, filterHighHz, panId, - bandName]( + bandName, cwPitchHz]( KiwiSdrClient* client) { client->setTrackedSlice(sliceId, frequencyMhz, mode, filterLowHz, - filterHighHz, panId, bandName); + filterHighHz, panId, bandName, cwPitchHz); }); } } diff --git a/src/core/KiwiSdrManager.h b/src/core/KiwiSdrManager.h index 90fc2c0b6..0e0f26d35 100644 --- a/src/core/KiwiSdrManager.h +++ b/src/core/KiwiSdrManager.h @@ -97,17 +97,17 @@ public slots: int filterLowHz, int filterHighHz, const QString& panId, double centerMhz, double bandwidthMhz, int lineDurationMs, - const QString& bandName); + const QString& bandName, int cwPitchHz); void assignSliceToProfile(int sliceId, const QString& profileId, double frequencyMhz, const QString& mode, int filterLowHz, int filterHighHz, const QString& panId, - const QString& bandName); + const QString& bandName, int cwPitchHz); void clearSliceAssignment(int sliceId); void updateSliceTracking(int sliceId, double frequencyMhz, const QString& mode, int filterLowHz, int filterHighHz, const QString& panId, - const QString& bandName); + const QString& bandName, int cwPitchHz); void updateWaterfallView(int sliceId, const QString& panId, double centerMhz, double bandwidthMhz, int lineDurationMs); diff --git a/src/core/KiwiSdrProtocol.cpp b/src/core/KiwiSdrProtocol.cpp index 707e01be6..ab4166281 100644 --- a/src/core/KiwiSdrProtocol.cpp +++ b/src/core/KiwiSdrProtocol.cpp @@ -1585,6 +1585,35 @@ QString formatAgcCommand(bool enabled, bool hang, int thresholdDb, .arg(clampedManualGain); } +QString formatSoundTuneCommand(const QString& mode, int lowCutHz, int highCutHz, + double freqKhz, int cwPitchHz) +{ + int tunedLowCutHz = lowCutHz; + int tunedHighCutHz = highCutHz; + double tunedFreqKhz = freqKhz; + if (mode == QStringLiteral("cw") && cwPitchHz != 0) { + // Flex reports the CW passband symmetric about the carrier (e.g. + // low_cut=-400 high_cut=400) — the sidetone pitch is a DSP shift + // Flex applies AFTER that filter, not something baked into it. The + // Kiwi has no such post-filter shift: 'freq' is the BFO/mixdown + // point and low_cut/high_cut are an audio passband relative to it. + // Sending freq=carrier with the passband as reported puts the + // carrier at 0 Hz (audible only as a DC thump, not a tone) — #4423. + // Reproduce the Flex behavior by shifting the whole receive chain + // by the pitch: move the BFO down so the carrier demodulates to + // +cwPitchHz, and slide the passband up by the same amount so that + // frequency is still inside it. + tunedLowCutHz += cwPitchHz; + tunedHighCutHz += cwPitchHz; + tunedFreqKhz -= cwPitchHz / 1000.0; + } + return QStringLiteral("SET mod=%1 low_cut=%2 high_cut=%3 freq=%4") + .arg(mode) + .arg(tunedLowCutHz) + .arg(tunedHighCutHz) + .arg(tunedFreqKhz, 0, 'f', 3); +} + MeterReading meterUnavailable(MeterSource source, const QString& notes) { MeterReading reading; diff --git a/src/core/KiwiSdrProtocol.h b/src/core/KiwiSdrProtocol.h index e3d781860..6689716b7 100644 --- a/src/core/KiwiSdrProtocol.h +++ b/src/core/KiwiSdrProtocol.h @@ -325,6 +325,8 @@ QString formatSquelchCommand(bool enabled, int thresholdDb, int agcDecayMsForMode(const QString& mode); QString formatAgcCommand(bool enabled, bool hang, int thresholdDb, int manualGainDb, int decayMs); +QString formatSoundTuneCommand(const QString& mode, int lowCutHz, int highCutHz, + double freqKhz, int cwPitchHz); MeterReading meterUnavailable(MeterSource source, const QString& notes = {}); MeterReading extractMeterFromSndVerifiedLayout(const QByteArray& frame, const MeterContext& context); diff --git a/src/gui/MainWindow_KiwiSdr.cpp b/src/gui/MainWindow_KiwiSdr.cpp index f73b80615..b6aded9e2 100644 --- a/src/gui/MainWindow_KiwiSdr.cpp +++ b/src/gui/MainWindow_KiwiSdr.cpp @@ -602,7 +602,8 @@ void MainWindow::setKiwiSdrVirtualAntennaForSliceInternal(int sliceId, m_kiwiSdrManager->assignSliceToProfile( sliceId, profileId, slice->frequency(), slice->mode(), slice->filterLow(), slice->filterHigh(), slice->panId(), - BandSettings::bandForFrequency(slice->frequency())); + BandSettings::bandForFrequency(slice->frequency()), + m_radioModel.transmitModel().cwPitch()); updateKiwiSdrVirtualTrackingForSlice(slice); updateKiwiSdrVirtualAudioControlsForSlice(slice); updateKiwiSdrVirtualReceiverControlsForSlice(slice); @@ -1035,7 +1036,8 @@ void MainWindow::updateKiwiSdrVirtualTrackingForSlice(SliceModel* slice) m_kiwiSdrManager->updateSliceTracking( slice->sliceId(), slice->frequency(), slice->mode(), slice->filterLow(), slice->filterHigh(), slice->panId(), - BandSettings::bandForFrequency(slice->frequency())); + BandSettings::bandForFrequency(slice->frequency()), + m_radioModel.transmitModel().cwPitch()); if (SpectrumWidget* spectrum = spectrumForSlice(slice)) { m_kiwiSdrManager->updateWaterfallView( slice->sliceId(), slice->panId(), spectrum->centerMhz(), @@ -1674,7 +1676,8 @@ void MainWindow::wireKiwiSdr() spectrum ? spectrum->centerMhz() : slice->frequency(), spectrum ? spectrum->bandwidthMhz() : 0.2, spectrum ? spectrum->wfLineDuration() : 100, - BandSettings::bandForFrequency(slice->frequency())); + BandSettings::bandForFrequency(slice->frequency()), + m_radioModel.transmitModel().cwPitch()); }); if (m_audio) { connect(m_kiwiSdrManager, &KiwiSdrManager::decodedAudioReady, diff --git a/src/gui/MainWindow_Wiring.cpp b/src/gui/MainWindow_Wiring.cpp index 6d6039286..abb277be2 100644 --- a/src/gui/MainWindow_Wiring.cpp +++ b/src/gui/MainWindow_Wiring.cpp @@ -4688,6 +4688,11 @@ void MainWindow::wireVfoWidget(VfoWidget* w, SliceModel* s) connect(s, &SliceModel::panIdChanged, this, [this, s](const QString&) { updateKiwiSdrVirtualTrackingForSlice(s); }); + // Re-send the tracked-slice command when the radio's CW pitch changes so + // an already-active KiwiSDR CW session's BFO offset stays in sync (#4423) + // instead of going stale until the next frequency/mode/filter edit. + connect(&m_radioModel.transmitModel(), &TransmitModel::phoneStateChanged, + this, [this, s]() { updateKiwiSdrVirtualTrackingForSlice(s); }); connect(s, &SliceModel::audioGainChanged, this, [this, s](float) { updateKiwiSdrVirtualAudioControlsForSlice(s); updateAetherDspModePolicy(); diff --git a/tests/kiwi_sdr_protocol_test.cpp b/tests/kiwi_sdr_protocol_test.cpp index 9eab17db0..78112e444 100644 --- a/tests/kiwi_sdr_protocol_test.cpp +++ b/tests/kiwi_sdr_protocol_test.cpp @@ -294,6 +294,24 @@ int main() return fail("AGC command formatting is wrong"); } + // #4423: Flex reports the CW passband symmetric about the carrier (e.g. + // -400..400, confirmed from a real session log) and applies the sidetone + // pitch as a DSP shift AFTER that filter. The Kiwi has no such shift: + // 'freq' is the BFO and low_cut/high_cut are relative to it, so sending + // freq=carrier with an unshifted passband puts the carrier at 0 Hz (a DC + // thump, not a tone). Both the BFO and the passband must move by the + // pitch so the carrier lands audibly inside it. + if (formatSoundTuneCommand(QStringLiteral("cw"), -400, 400, 7000.0, 600) + != QStringLiteral("SET mod=cw low_cut=200 high_cut=1000 freq=6999.400") + || formatSoundTuneCommand(QStringLiteral("cw"), -400, 400, 7000.0, 0) + != QStringLiteral("SET mod=cw low_cut=-400 high_cut=400 freq=7000.000") + || formatSoundTuneCommand(QStringLiteral("usb"), 100, 2900, 7000.0, 600) + != QStringLiteral("SET mod=usb low_cut=100 high_cut=2900 freq=7000.000") + || formatSoundTuneCommand(QStringLiteral("lsb"), -2900, -100, 7000.0, 600) + != QStringLiteral("SET mod=lsb low_cut=-2900 high_cut=-100 freq=7000.000")) { + return fail("sound tune command formatting is wrong"); + } + const QVector msgTokens = parseMsgTokens( QStringLiteral("MSG wb_only password_timeout inactivity_timeout=15 " "kiwi_kick=1%2coperator%20request badp=5 =ignored"));