From 79e9f2aee81a4416673899027734a28763b9f95f Mon Sep 17 00:00:00 2001 From: Ralph Martin Fischer Date: Wed, 4 Nov 2026 17:35:44 +0100 Subject: [PATCH] Chat zeigt immer UKW, auch bei einem Kurzwellen-Contest MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Betreiber am 2026-11-04: „im chat sind übrigens nachrichten, welche kurzwelle betreffen ... chat ist für mich nur ukw wichtig", auf die Rückfrage „immer UKW". Die Ursache war nicht der Filter, sondern wofür er arbeitete. In seinen Einstellungen am Mac mini stand: active_contest_id = CQ_WW_SSB active_bands = ["14"] Der Chatfilter folgt den Bändern des aktiven Contests. Er ließ also 20 m durch und blendete UKW aus -- korrekt zu den Einstellungen und trotzdem das Falsche. Zwei Quellen speisen den Strom: ON4KST (reiner UKW-Chat, bei ihm verbunden auf 5.196.193.63:23001) und der DX-Cluster ve7cc.net (207.81.98.214:23), der fast ausschließlich Kurzwelle führt. Neu ist `ChatFeedModel::bandPassesChatFilter()`: * Kurzwelle kommt nie durch -- auch nicht als eigenes Contestband. * Die Verengung auf die Contest-Bänder bleibt, aber nur INNERHALB von UKW. Hat der aktive Contest kein UKW-Band (oder ist keiner aktiv), gilt ganz UKW statt „alles" -- letzteres war die zweite stille Lücke. * Die Grenze ist 50 MHz (`kUkwUntergrenzeHz` in BandUtils), 6 m zählt mit. Eine Frequenz von 0 oder aus keinem bekannten Band gilt NICHT als Kurzwelle: eine freie Chatzeile hat keine Frequenz und darf nicht wegen Unkenntnis verschwinden. Es sitzt im Sichtbarkeitsfilter, NICHT am Eingang. Ein erster Versuch, in addCandidate() abzuweisen, ist an `test_chat_panel`'s `shortwaveClusterSpotsStayOutOfAVhfContestChat()` zu Recht gescheitert: „gefiltert heißt verborgen, nicht weggeworfen" -- die Ansicht „alle Zeilen zeigen" muss jede Zeile behalten. Der Versuch ist zurückgenommen. Prüfstand `test_chat_nur_ukw` (7 Fälle): die Kante bei 50 MHz Band für Band, unbekannte Frequenzen, Martins Lage nachgebildet (Contest-Bänder ["14"]), ein reiner 2-m-Contest, der innerhalb UKW weiter verengt, und der Beleg, dass die Rohansicht alles behält. Gegenprobe gegen den alten Filter gefahren -- mit ihm fällt genau der Fall um, und die Fehlermeldung zeigt den gemeldeten Fehler: FAIL! anHfContestDoesNotDragShortwaveIntoTheChat() '!zuSehen.contains("K1ABC")' returned FALSE. (K1ABC,HB9JKL) also der 20-m-Spot sichtbar, alles UKW weg. Der ganze Prüflauf ist grün außer `test_seitenband_folgt_dem_band` und `test_rigctld_supervision` (SEGFAULT) -- beide fallen auf dem unveränderten main genauso um, nachgemessen. Altlasten, hier nicht angefasst. Co-Authored-By: Claude Opus 5 --- src/core/BandUtils.cpp | 11 +++ src/core/BandUtils.h | 11 +++ src/models/ChatFeedModel.cpp | 28 +++++- src/models/ChatFeedModel.h | 19 ++++ tests/CMakeLists.txt | 1 + tests/test_chat_nur_ukw.cpp | 178 +++++++++++++++++++++++++++++++++++ 6 files changed, 247 insertions(+), 1 deletion(-) create mode 100644 tests/test_chat_nur_ukw.cpp diff --git a/src/core/BandUtils.cpp b/src/core/BandUtils.cpp index e46a951..b878406 100644 --- a/src/core/BandUtils.cpp +++ b/src/core/BandUtils.cpp @@ -76,6 +76,17 @@ QString usualModeForFrequencyHz(qint64 hz) return QString(); } +bool isUkwOrUnknownHz(qint64 hz) +{ + if (hz <= 0) { + return true; // keine Frequenz -- freie Chatzeile + } + if (bandLabelForFrequencyHz(hz).isEmpty()) { + return true; // kein bekanntes Band -- nicht wegwerfen + } + return hz >= kUkwUntergrenzeHz; +} + QString bandLabelForFrequencyHz(qint64 hz) { for (const BandRange& band : kBands) { diff --git a/src/core/BandUtils.h b/src/core/BandUtils.h index 383ebac..433b279 100644 --- a/src/core/BandUtils.h +++ b/src/core/BandUtils.h @@ -20,6 +20,17 @@ namespace Contestprogramm { // antenna, and both ends need a range and a base frequency. QString bandLabelForFrequencyHz(qint64 hz); +// Die UKW-Grenze: 50 MHz und darueber (6 m, 4 m, 2 m, 70 cm, 23 cm und +// hoeher); alles darunter ist Kurzwelle. +// +// Eine Frequenz von 0 oder eine, die in kein bekanntes Band faellt, +// gilt NICHT als Kurzwelle -- eine freie Chatzeile hat keine Frequenz +// und darf nicht wegen Unkenntnis verschwinden. Dieselbe Haltung wie in +// ChatFeedModel::bandBelongsToContest: unbekanntes Band nicht heimlich +// wegwerfen. +constexpr qint64 kUkwUntergrenzeHz = 50000000LL; +bool isUkwOrUnknownHz(qint64 hz); + // The bands bandLabelForFrequencyHz() knows, lowest first. QStringList knownBands(); diff --git a/src/models/ChatFeedModel.cpp b/src/models/ChatFeedModel.cpp index d4b6ac8..1741570 100644 --- a/src/models/ChatFeedModel.cpp +++ b/src/models/ChatFeedModel.cpp @@ -302,6 +302,32 @@ ChatFeedModel::FeedLine ChatFeedModel::lineAt(int index) const // Ein Spot OHNE Frequenz (jede Chatzeile) bleibt drin: über den sagt // die Frequenz nichts aus. Und ohne bekannte Contestbänder wird nichts // aussortiert -- lieber zu viel zeigen als etwas Richtiges verschlucken. +bool ChatFeedModel::bandPassesChatFilter(qint64 freqHz) const +{ + if (!isUkwOrUnknownHz(freqHz)) { + return false; // Kurzwelle: nie im Chat + } + if (freqHz <= 0) { + return true; // freie Chatzeile + } + // Hat der aktive Contest ueberhaupt ein UKW-Band? Wenn nicht, darf + // er die UKW-Sicht nicht verengen -- sonst blendete ein + // Kurzwellen-Contest genau das aus, was der Betreiber sehen will. + bool contestHatUkwBand = false; + for (const QString& band : m_contestBands) { + qint64 lowHz = 0; + qint64 highHz = 0; + if (bandRangeHz(band, lowHz, highHz) && lowHz >= kUkwUntergrenzeHz) { + contestHatUkwBand = true; + break; + } + } + if (!contestHatUkwBand) { + return true; // ganz UKW + } + return bandBelongsToContest(freqHz); // innerhalb UKW auf die Baender verengen +} + bool ChatFeedModel::bandBelongsToContest(qint64 freqHz) const { if (freqHz <= 0 || m_contestBands.isEmpty()) { @@ -359,7 +385,7 @@ QVector ChatFeedModel::sichtbareZeilen() const QVector survivorScores; for (int i = 0; i < m_allEntries.size(); ++i) { const Entry& entry = m_allEntries.at(i); - if (entry.geo.inRange && !entry.worked && bandBelongsToContest(entry.candidate.freqHz)) { + if (entry.geo.inRange && !entry.worked && bandPassesChatFilter(entry.candidate.freqHz)) { survivorIndices.append(i); survivorScores.append(entry.score); } diff --git a/src/models/ChatFeedModel.h b/src/models/ChatFeedModel.h index 45423b4..8a1dfc6 100644 --- a/src/models/ChatFeedModel.h +++ b/src/models/ChatFeedModel.h @@ -88,6 +88,25 @@ class ChatFeedModel : public QAbstractTableModel { // Clusters haben in einem UKW-Contest nichts im Chat verloren. bool bandBelongsToContest(qint64 freqHz) const; + // Das Bandtor des Chats. Betreiber am 2026-11-04: "chat ist fuer + // mich nur ukw wichtig ... immer UKW". + // + // Kurzwelle kommt nie durch -- auch dann nicht, wenn gerade ein + // KURZWELLEN-Contest eingestellt ist. Genau das war seine Lage: + // active_contest_id = CQ_WW_SSB, active_bands = ["14"], und + // bandBelongsToContest() liess folgerichtig 20 m durch und blendete + // UKW aus. Der Chat zeigte also korrekt, was eingestellt war, und + // trotzdem das Falsche. + // + // Die Verengung auf die Contest-Baender bleibt -- aber nur + // INNERHALB von UKW. Hat der aktive Contest kein UKW-Band (oder ist + // keiner aktiv), gilt ganz UKW statt "alles". + // + // Sitzt im Sichtbarkeitsfilter, nicht am Eingang: gefiltert heisst + // verborgen, nicht weggeworfen -- die Ansicht "alle Zeilen zeigen" + // hat weiter jede Zeile (test_chat_panel verlangt das ausdruecklich). + bool bandPassesChatFilter(qint64 freqHz) const; + // Optional (nullptr = no multiplier boost, e.g. before // AppController has recomputed one yet). Non-owning; the caller // keeps it alive. Lets the importance scorer tell a still-needed diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index f1d251e..6343a85 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -161,6 +161,7 @@ contestprogramm_add_test(test_next_target_suggester) contestprogramm_add_test(test_message_drafter) contestprogramm_add_test(test_weather_client) contestprogramm_add_test(test_chatfeedmodel_refresh) +contestprogramm_add_test(test_chat_nur_ukw) contestprogramm_add_test(test_recent_propagation_tracker) contestprogramm_add_test(test_color_theme) contestprogramm_add_test(test_country_borders) diff --git a/tests/test_chat_nur_ukw.cpp b/tests/test_chat_nur_ukw.cpp new file mode 100644 index 0000000..a31a47c --- /dev/null +++ b/tests/test_chat_nur_ukw.cpp @@ -0,0 +1,178 @@ +// Chat immer UKW -- Betreiber am 2026-11-04: "chat ist fuer mich nur +// ukw wichtig", auf die Rueckfrage "immer UKW". +// +// Der Befund, der dahinter steckt: in seinen Einstellungen stand +// active_contest_id = CQ_WW_SSB und active_bands = ["14"]. Der +// Chatfilter folgte diesen Baendern, liess also 20 m durch und blendete +// UKW aus. Das Programm zeigte korrekt, was eingestellt war, und +// trotzdem das Falsche -- zwei Quellen speisen den Strom, ON4KST +// (reiner UKW-Chat) und der DX-Cluster ve7cc.net (fast nur Kurzwelle). +// +// Das Tor sitzt im Sichtbarkeitsfilter, NICHT am Eingang: gefiltert +// heisst verborgen, nicht weggeworfen. test_chat_panel verlangt +// ausdruecklich, dass ein ausgefilterter Kurzwellen-Spot in der Ansicht +// "alle Zeilen zeigen" noch da ist -- ein erster Versuch, ihn in +// addCandidate() abzuweisen, hat genau daran zu Recht scheitern. + +#include + +#include +#include +#include + +#include "core/BandUtils.h" +#include "core/GeoFilter.h" +#include "core/SpotCandidate.h" +#include "data/ContestDatabase.h" +#include "data/DupeChecker.h" +#include "models/ChatFeedModel.h" + +using namespace Contestprogramm; + +namespace { +SpotCandidate spot(const QString& call, qint64 hz) +{ + SpotCandidate c; + c.callsign = call; + c.freqHz = hz; + c.timestampUtc = QDateTime::currentDateTimeUtc(); + return c; +} + +// Alle Rufzeichen, die der Filter gerade sichtbar laesst. +QStringList sichtbar(ChatFeedModel& model) +{ + QStringList raus; + for (int r = 0; r < model.rowCount(); ++r) { + raus << model.data(model.index(r, 0)).toString(); + } + return raus; +} +} // namespace + +class TestChatNurUkw : public QObject +{ + Q_OBJECT + +private slots: + void theUkwEdgeSitsAtFiftyMegahertz(); + void unknownFrequenciesAreNotThrownAway(); + void anHfContestDoesNotDragShortwaveIntoTheChat(); + void aVhfContestStillNarrowsToItsOwnBands(); + void filteredLinesAreOnlyHiddenNotDiscarded(); +}; + +void TestChatNurUkw::theUkwEdgeSitsAtFiftyMegahertz() +{ + QVERIFY(!isUkwOrUnknownHz(1810000LL)); // 160 m + QVERIFY(!isUkwOrUnknownHz(3650000LL)); // 80 m + QVERIFY(!isUkwOrUnknownHz(7050000LL)); // 40 m + QVERIFY(!isUkwOrUnknownHz(10120000LL)); // 30 m + QVERIFY(!isUkwOrUnknownHz(14080000LL)); // 20 m + QVERIFY(!isUkwOrUnknownHz(18100000LL)); // 17 m + QVERIFY(!isUkwOrUnknownHz(21200000LL)); // 15 m + QVERIFY(!isUkwOrUnknownHz(24930000LL)); // 12 m + QVERIFY(!isUkwOrUnknownHz(28400000LL)); // 10 m + QVERIFY(!isUkwOrUnknownHz(29700000LL)); // die Oberkante von 10 m + + QVERIFY(isUkwOrUnknownHz(kUkwUntergrenzeHz)); // genau die Kante + QVERIFY(isUkwOrUnknownHz(50150000LL)); // 6 m zaehlt mit + QVERIFY(isUkwOrUnknownHz(70200000LL)); // 4 m + QVERIFY(isUkwOrUnknownHz(144300000LL)); // 2 m + QVERIFY(isUkwOrUnknownHz(432200000LL)); // 70 cm + QVERIFY(isUkwOrUnknownHz(1296200000LL)); // 23 cm + QVERIFY(isUkwOrUnknownHz(10368000000LL)); // 3 cm +} + +void TestChatNurUkw::unknownFrequenciesAreNotThrownAway() +{ + QVERIFY(isUkwOrUnknownHz(0)); + QVERIFY(isUkwOrUnknownHz(-1)); + // 40 MHz liegt in keinem Amateurband -- unbekannt, nicht + // "Kurzwelle". Lieber eine Zeile zu viel als eine zu wenig. + QVERIFY(isUkwOrUnknownHz(40000000LL)); +} + +void TestChatNurUkw::anHfContestDoesNotDragShortwaveIntoTheChat() +{ + QTemporaryDir dir; + QVERIFY(dir.isValid()); + ContestDatabase db; + QVERIFY(db.open(dir.filePath(QStringLiteral("ukw1.sqlite")), QStringLiteral("chat_ukw_hf"))); + GeoFilter geo; + geo.setOwnGrid(QStringLiteral("JN67VV")); + geo.setRadiusKm(20000.0); + DupeChecker dupes(db); + ChatFeedModel model(geo, dupes, nullptr); + + // Genau Martins Lage am 2026-11-04. + model.setContestBands(QStringList{QStringLiteral("14")}); + + model.addCandidate(spot(QStringLiteral("XE1AOH"), 7164000LL)); // 40 m + model.addCandidate(spot(QStringLiteral("K1ABC"), 14080000LL)); // 20 m: das Contestband! + model.addCandidate(spot(QStringLiteral("OE1ABC"), 144300000LL)); // 2 m + model.addCandidate(spot(QStringLiteral("DL1DEF"), 432200000LL)); // 70 cm + model.addCandidate(spot(QStringLiteral("HB9JKL"), 0)); // freie Zeile + + const QStringList zuSehen = sichtbar(model); + QVERIFY2(!zuSehen.contains(QStringLiteral("XE1AOH")), qPrintable(zuSehen.join(QLatin1Char(',')))); + // Das eigene Contestband hilft nichts, wenn es Kurzwelle ist. + QVERIFY2(!zuSehen.contains(QStringLiteral("K1ABC")), qPrintable(zuSehen.join(QLatin1Char(',')))); + QVERIFY2(zuSehen.contains(QStringLiteral("OE1ABC")), qPrintable(zuSehen.join(QLatin1Char(',')))); + QVERIFY2(zuSehen.contains(QStringLiteral("DL1DEF")), qPrintable(zuSehen.join(QLatin1Char(',')))); + QVERIFY2(zuSehen.contains(QStringLiteral("HB9JKL")), qPrintable(zuSehen.join(QLatin1Char(',')))); +} + +void TestChatNurUkw::aVhfContestStillNarrowsToItsOwnBands() +{ + QTemporaryDir dir; + QVERIFY(dir.isValid()); + ContestDatabase db; + QVERIFY(db.open(dir.filePath(QStringLiteral("ukw2.sqlite")), QStringLiteral("chat_ukw_vhf"))); + GeoFilter geo; + geo.setOwnGrid(QStringLiteral("JN67VV")); + geo.setRadiusKm(20000.0); + DupeChecker dupes(db); + ChatFeedModel model(geo, dupes, nullptr); + + // Ein reiner 2-m-Contest: die Verengung innerhalb UKW bleibt, sonst + // waere die Contest-Disziplin weg. + model.setContestBands(QStringList{QStringLiteral("144")}); + + model.addCandidate(spot(QStringLiteral("OE1ABC"), 144300000LL)); // sein Band + model.addCandidate(spot(QStringLiteral("DL1DEF"), 432200000LL)); // UKW, aber nicht sein Band + model.addCandidate(spot(QStringLiteral("XE1AOH"), 7164000LL)); // Kurzwelle + + const QStringList zuSehen = sichtbar(model); + QVERIFY2(zuSehen.contains(QStringLiteral("OE1ABC")), qPrintable(zuSehen.join(QLatin1Char(',')))); + QVERIFY2(!zuSehen.contains(QStringLiteral("DL1DEF")), qPrintable(zuSehen.join(QLatin1Char(',')))); + QVERIFY2(!zuSehen.contains(QStringLiteral("XE1AOH")), qPrintable(zuSehen.join(QLatin1Char(',')))); +} + +void TestChatNurUkw::filteredLinesAreOnlyHiddenNotDiscarded() +{ + QTemporaryDir dir; + QVERIFY(dir.isValid()); + ContestDatabase db; + QVERIFY(db.open(dir.filePath(QStringLiteral("ukw3.sqlite")), QStringLiteral("chat_ukw_roh"))); + GeoFilter geo; + geo.setOwnGrid(QStringLiteral("JN67VV")); + geo.setRadiusKm(20000.0); + DupeChecker dupes(db); + ChatFeedModel model(geo, dupes, nullptr); + model.setContestBands(QStringList{QStringLiteral("144")}); + + model.addCandidate(spot(QStringLiteral("XE1AOH"), 7164000LL)); + model.addCandidate(spot(QStringLiteral("OE1ABC"), 144300000LL)); + + QVERIFY(!sichtbar(model).contains(QStringLiteral("XE1AOH"))); + // Beide Zeilen liegen weiter im Strom ... + QCOMPARE(model.lineCount(), 2); + // ... und "alle Zeilen zeigen" holt die Kurzwelle wieder hervor. + model.setShowRawFeed(true); + QVERIFY2(sichtbar(model).contains(QStringLiteral("XE1AOH")), + "der Kurzwellen-Spot ist ganz verschwunden"); +} + +QTEST_MAIN(TestChatNurUkw) +#include "test_chat_nur_ukw.moc"