Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 6 additions & 7 deletions src/core/KiwiSdrClient.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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;
}

Expand All @@ -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();
}
Expand Down Expand Up @@ -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()
Expand Down
3 changes: 2 additions & 1 deletion src/core/KiwiSdrClient.h
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down Expand Up @@ -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};
Expand Down
24 changes: 14 additions & 10 deletions src/core/KiwiSdrManager.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}
Expand Down Expand Up @@ -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;
Expand All @@ -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);
Expand All @@ -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);
Expand Down Expand Up @@ -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);
});
}
Expand Down Expand Up @@ -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()) {
Expand All @@ -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);
});
}
}
Expand Down
6 changes: 3 additions & 3 deletions src/core/KiwiSdrManager.h
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down
29 changes: 29 additions & 0 deletions src/core/KiwiSdrProtocol.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
2 changes: 2 additions & 0 deletions src/core/KiwiSdrProtocol.h
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down
9 changes: 6 additions & 3 deletions src/gui/MainWindow_KiwiSdr.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down Expand Up @@ -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(),
Expand Down Expand Up @@ -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,
Expand Down
5 changes: 5 additions & 0 deletions src/gui/MainWindow_Wiring.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand Down
18 changes: 18 additions & 0 deletions tests/kiwi_sdr_protocol_test.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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<MsgToken> msgTokens = parseMsgTokens(
QStringLiteral("MSG wb_only password_timeout inactivity_timeout=15 "
"kiwi_kick=1%2coperator%20request badp=5 =ignored"));
Expand Down
Loading