FT8 demod: make OSD optional and log OSD information

This commit is contained in:
f4exb 2023-01-28 08:58:50 +01:00
parent cb548b7546
commit a86cc53945
20 changed files with 429 additions and 94 deletions

Binary file not shown.

Before

Width:  |  Height:  |  Size: 29 KiB

After

Width:  |  Height:  |  Size: 34 KiB

Binary file not shown.

View File

@ -894,7 +894,7 @@ void FT8::go(int npasses)
}
int off = order[ii].off_;
int ret = one(bins, samples_.size(), hz, off);
int ret = one_merge(bins, samples_.size(), hz, off);
if (ret)
{
@ -2499,12 +2499,11 @@ int FT8::decode(const float ll174[], int a174[], int use_osd, std::string &comme
if (ldpc_ok >= ok_thresh)
{
// plain[] is 91 systematic data bits, 83 parity bits.
for (int i = 0; i < 174; i++)
{
for (int i = 0; i < 174; i++) {
a174[i] = plain[i];
}
if (check_crc(a174))
{
if (OSD::check_crc(a174)) {
// success!
return 1;
}
@ -2512,17 +2511,15 @@ int FT8::decode(const float ll174[], int a174[], int use_osd, std::string &comme
if (use_osd && params.osd_depth >= 0 && ldpc_ok >= params.osd_ldpc_thresh)
{
extern int osd_decode(float codeword[174], int depth, int out[91], int *);
extern void ldpc_encode(int plain[91], int codeword[174]);
int oplain[91];
int got_depth = -1;
int osd_ok = osd_decode((float *)ll174, params.osd_depth, oplain, &got_depth);
int osd_ok = OSD::osd_decode((float *)ll174, params.osd_depth, oplain, &got_depth);
if (osd_ok)
{
// reconstruct all 174.
comment += "OSD-" + std::to_string(got_depth) + "-" + std::to_string(ldpc_ok);
ldpc_encode(oplain, a174);
OSD::ldpc_encode(oplain, a174);
return 1;
}
}
@ -2669,7 +2666,7 @@ std::vector<float> FT8::down_v7_f(const std::vector<std::complex<float>> &bins,
//
// XXX merge with one_iter().
//
int FT8::one(const std::vector<std::complex<float>> &bins, int len, float hz, int off)
int FT8::one_merge(const std::vector<std::complex<float>> &bins, int len, float hz, int off)
{
//
// set up to search for best frequency and time offset.
@ -3018,38 +3015,69 @@ int FT8::one_iter1(
if (params.soft_ones)
{
if (params.soft_ones == 1)
{
if (params.soft_ones == 1) {
soft_decode(m79, ll174);
}
else
{
} else {
c_soft_decode(m79, ll174);
}
int ret = try_decode(samples200, ll174, best_hz, best_off,
hz0_for_cb, hz1_for_cb, 1, "", m79);
if (ret)
int ret = try_decode(
samples200,
ll174,
best_hz,
best_off,
hz0_for_cb,
hz1_for_cb,
params.use_osd,
"",
m79
);
if (ret) {
return ret;
}
}
if (params.soft_pairs)
{
float p174[174];
soft_decode_pairs(m79, p174);
int ret = try_decode(samples200, p174, best_hz, best_off,
hz0_for_cb, hz1_for_cb, 1, "", m79);
if (ret)
int ret = try_decode(
samples200,
p174,
best_hz,
best_off,
hz0_for_cb,
hz1_for_cb,
params.use_osd,
"",
m79
);
if (ret) {
return ret;
if (params.soft_ones == 0)
}
if (params.soft_ones == 0) {
std::copy(p174, p174 + 174, ll174);
}
}
if (params.soft_triples)
{
float p174[174];
soft_decode_triples(m79, p174);
int ret = try_decode(samples200, p174, best_hz, best_off,
hz0_for_cb, hz1_for_cb, 1, "", m79);
int ret = try_decode(
samples200,
p174,
best_hz,
best_off,
hz0_for_cb,
hz1_for_cb,
params.use_osd,
"",
m79
);
if (ret)
return ret;
}
@ -3085,10 +3113,20 @@ int FT8::one_iter1(
n174[i] = ll174[i];
}
}
int ret = try_decode(samples200, n174, best_hz, best_off,
hz0_for_cb, hz1_for_cb, 0, "hint1", m79);
if (ret)
{
int ret = try_decode(
samples200,
n174,
best_hz,
best_off,
hz0_for_cb,
hz1_for_cb,
0,
"hint1",
m79
);
if (ret) {
return ret;
}
}
@ -3120,10 +3158,20 @@ int FT8::one_iter1(
n174[i] = ll174[i];
}
}
int ret = try_decode(samples200, n174, best_hz, best_off,
hz0_for_cb, hz1_for_cb, 0, "hint2", m79);
if (ret)
{
int ret = try_decode(
samples200,
n174,
best_hz,
best_off,
hz0_for_cb,
hz1_for_cb,
0,
"hint2",
m79
);
if (ret) {
return ret;
}
}

View File

@ -154,6 +154,7 @@ struct FT8_API FT8Params
int use_apriori;
int use_hints; // 1 means use all hints, 2 means just CQ hints
int win_type;
int use_osd;
int osd_depth; // 6; // don't increase beyond 6, produces too much garbage
int osd_ldpc_thresh; // demand this many correct LDPC parity bits before OSD
int ncoarse; // number of offsets per hz produced by coarse()
@ -222,6 +223,7 @@ struct FT8_API FT8Params
use_apriori = 1;
use_hints = 2; // 1 means use all hints, 2 means just CQ hints
win_type = 1;
use_osd = 1;
osd_depth = 0; // 6; // don't increase beyond 6, produces too much garbage
osd_ldpc_thresh = 70; // demand this many correct LDPC parity bits before OSD
ncoarse = 1; // number of offsets per hz produced by coarse()
@ -563,7 +565,7 @@ public:
//
// XXX merge with one_iter().
//
int one(const std::vector<std::complex<float>> &bins, int len, float hz, int off);
int one_merge(const std::vector<std::complex<float>> &bins, int len, float hz, int off);
// return 2 if it decodes to a brand-new message.
// return 1 if it decodes but we've already seen it,
// perhaps in a different pass.

View File

@ -34,7 +34,7 @@ namespace FT8 {
//
// check the FT8 CRC-14
//
int check_crc(const int a91[91])
int OSD::check_crc(const int a91[91])
{
int aa[91];
int non_zero = 0;
@ -73,7 +73,7 @@ int check_crc(const int a91[91])
// plain is 91 bits of plain-text.
// returns a 174-bit codeword.
// mimics wsjt-x's encode174_91.f90.
void ldpc_encode(int plain[91], int codeword[174])
void OSD::ldpc_encode(int plain[91], int codeword[174])
{
// the systematic 91 bits.
for (int i = 0; i < 91; i++)
@ -97,7 +97,7 @@ void ldpc_encode(int plain[91], int codeword[174])
// ll174 is what was received.
// ldpc-encode xplain; how close is the
// result to what we received?
float osd_score(int xplain[91], float ll174[174])
float OSD::osd_score(int xplain[91], float ll174[174])
{
int xcode[174];
ldpc_encode(xplain, xcode);
@ -121,7 +121,7 @@ float osd_score(int xplain[91], float ll174[174])
}
// does a decode look plausible?
int osd_check(const int plain[91])
int OSD::osd_check(const int plain[91])
{
int allzero = 1;
for (int i = 0; i < 91; i++)
@ -144,7 +144,7 @@ int osd_check(const int plain[91])
return 1;
}
void matmul(int a[91][91], int b[91], int c[91])
void OSD::matmul(int a[91][91], int b[91], int c[91])
{
for (int i = 0; i < 91; i++)
{
@ -164,10 +164,11 @@ void matmul(int a[91][91], int b[91], int c[91])
// first 91 bits are plaintext, remaining 83 are parity.
// returns 0 or 1, with decoded plain bits in out91[].
// and actual depth used in *out_depth.
int osd_decode(float codeword[174], int depth, int out[91], int *out_depth)
int OSD::osd_decode(float codeword[174], int depth, int out[91], int *out_depth)
{
// strength = abs(codeword)
float strength[174];
for (int i = 0; i < 174; i++)
{
float x = codeword[i];
@ -176,58 +177,62 @@ int osd_decode(float codeword[174], int depth, int out[91], int *out_depth)
// sort, strongest first; we'll use strongest 91.
std::vector<int> which(174);
for (int i = 0; i < 174; i++)
for (int i = 0; i < 174; i++) {
which[i] = i;
std::sort(which.begin(),
which.end(),
[=](int a, int b)
{
return strength[a] > strength[b];
});
}
std::sort(
which.begin(),
which.end(),
[=](int a, int b) {
return strength[a] > strength[b];
}
);
// gen_sys[174 rows][91 cols] has a row per each of the 174 codeword bits,
// indicating how to generate it by xor with each of the 91 plain bits.
// generator matrix, reordered strongest codeword bit first.
int b[174][91 * 2];
for (int i = 0; i < 174; i++)
{
int ii = which[i];
for (int j = 0; j < 91 * 2; j++)
{
if (j < 91)
{
if (j < 91) {
b[i][j] = gen_sys[ii][j];
}
else
{
} else {
b[i][j] = 0;
}
}
}
int xwhich[174];
for (int i = 0; i < 174; i++)
for (int i = 0; i < 174; i++) {
xwhich[i] = which[i];
}
int ok = 0;
gauss_jordan(91, 174, b, xwhich, &ok);
if (ok == 0)
{
if (ok == 0) {
fprintf(stderr, "gauss_jordan failed\n");
}
int gen1_inv[91][91];
for (int i = 0; i < 91; i++)
{
for (int j = 0; j < 91; j++)
{
for (int j = 0; j < 91; j++) {
gen1_inv[i][j] = b[i][91 + j];
}
}
for (int i = 0; i < 174; i++)
{
for (int i = 0; i < 174; i++) {
which[i] = xwhich[i];
}
@ -235,6 +240,7 @@ int osd_decode(float codeword[174], int depth, int out[91], int *out_depth)
// more or less strongest-first, converted from
// log-likihood to 0/1.
int y1[91];
for (int i = 0; i < 91; i++)
{
int j = which[i];
@ -251,9 +257,9 @@ int osd_decode(float codeword[174], int depth, int out[91], int *out_depth)
matmul(gen1_inv, y1, xplain); // also does mod 2
int osd_thresh = -500;
float xscore = osd_score(xplain, codeword);
int ch = osd_check(xplain);
if (xscore < osd_thresh && ch)
{
if (got_a_best == 0 || xscore < best_score)
@ -284,6 +290,7 @@ int osd_decode(float codeword[174], int depth, int out[91], int *out_depth)
y1[i] ^= 1;
float xscore = osd_score(xplain, codeword);
int ch = osd_check(xplain);
if (xscore < osd_thresh && ch)
{
if (got_a_best == 0 || xscore < best_score)
@ -308,7 +315,7 @@ int osd_decode(float codeword[174], int depth, int out[91], int *out_depth)
}
}
int gen_sys[174][91] = {
const int OSD::gen_sys[174][91] = {
{ 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, },
{ 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, },
{ 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, },

View File

@ -23,13 +23,17 @@
namespace FT8 {
extern int gen_sys[174][91];
int check_crc(const int a91[91]);
void ldpc_encode(int plain[91], int codeword[174]);
float osd_score(int xplain[91], float ll174[174]);
int osd_check(const int plain[91]);
void matmul(int a[91][91], int b[91], int c[91]);
int osd_decode(float codeword[174], int depth, int out[91], int *out_depth);
class OSD
{
public:
static const int gen_sys[174][91];
static int check_crc(const int a91[91]);
static void ldpc_encode(int plain[91], int codeword[174]);
static float osd_score(int xplain[91], float ll174[174]);
static int osd_check(const int plain[91]);
static void matmul(int a[91][91], int b[91], int c[91]);
static int osd_decode(float codeword[174], int depth, int out[91], int *out_depth);
};
} // namepsace FT8

View File

@ -245,6 +245,18 @@ void FT8DemodBaseband::applySettings(const FT8DemodSettings& settings, bool forc
m_ft8DemodWorker->setDecoderTimeBudget(settings.m_decoderTimeBudget);
}
if ((settings.m_useOSD != m_settings.m_useOSD) || force) {
m_ft8DemodWorker->setUseOSD(settings.m_useOSD);
}
if ((settings.m_osdDepth != m_settings.m_osdDepth) || force) {
m_ft8DemodWorker->setOSDDepth(settings.m_osdDepth);
}
if ((settings.m_osdLDPCThreshold != m_settings.m_osdLDPCThreshold) || force) {
m_ft8DemodWorker->setOSDLDPCThreshold(settings.m_osdLDPCThreshold);
}
m_sink.applySettings(settings, force);
m_settings = settings;
}

View File

@ -14,6 +14,7 @@
// You should have received a copy of the GNU General Public License //
// along with this program. If not, see <http://www.gnu.org/licenses/>. //
///////////////////////////////////////////////////////////////////////////////////
#include "ft8demodsettings.h"
#include "ft8demodfilterproxy.h"
FT8DemodFilterProxy::FT8DemodFilterProxy(QObject *parent) :
@ -56,6 +57,13 @@ void FT8DemodFilterProxy::setFilterLoc(const QString& locString)
invalidateFilter();
}
void FT8DemodFilterProxy::setFilterInfo(const QString& infoString)
{
m_filterActive = FILTER_INFO;
m_info= infoString;
invalidateFilter();
}
bool FT8DemodFilterProxy::filterAcceptsRow(int sourceRow, const QModelIndex &sourceParent) const
{
if (m_filterActive == FILTER_NONE) {
@ -64,30 +72,42 @@ bool FT8DemodFilterProxy::filterAcceptsRow(int sourceRow, const QModelIndex &sou
if (m_filterActive == FILTER_UTC)
{
QModelIndex index = sourceModel()->index(sourceRow, 0, sourceParent);
QModelIndex index = sourceModel()->index(sourceRow, FT8DemodSettings::MESSAGE_COL_UTC, sourceParent);
return sourceModel()->data(index).toString() == m_utc;
}
if (m_filterActive == FILTER_DF)
{
QModelIndex index = sourceModel()->index(sourceRow, 4, sourceParent);
QModelIndex index = sourceModel()->index(sourceRow, FT8DemodSettings::MESSAGE_COL_DF, sourceParent);
int df = sourceModel()->data(index).toInt();
return (df >= m_df - 4) && (df <= m_df + 4); // +/- 4 Hz tolerance which is about one symbol width
}
if (m_filterActive == FILTER_CALL)
{
QModelIndex indexCall1 = sourceModel()->index(sourceRow, 6, sourceParent);
QModelIndex indexCall2 = sourceModel()->index(sourceRow, 7, sourceParent);
QModelIndex indexCall1 = sourceModel()->index(sourceRow, FT8DemodSettings::MESSAGE_COL_CALL1, sourceParent);
QModelIndex indexCall2 = sourceModel()->index(sourceRow, FT8DemodSettings::MESSAGE_COL_CALL2, sourceParent);
return (sourceModel()->data(indexCall1).toString() == m_call) ||
(sourceModel()->data(indexCall2).toString() == m_call);
}
if (m_filterActive == FILTER_LOC)
{
QModelIndex index = sourceModel()->index(sourceRow, 8, sourceParent);
QModelIndex index = sourceModel()->index(sourceRow, FT8DemodSettings::MESSAGE_COL_LOC, sourceParent);
return sourceModel()->data(index).toString() == m_loc;
}
if (m_filterActive == FILTER_INFO)
{
QModelIndex index = sourceModel()->index(sourceRow, FT8DemodSettings::MESSAGE_COL_INFO, sourceParent);
const QString& content = sourceModel()->data(index).toString();
if (m_info.startsWith("OSD")) {
return content.startsWith("OSD");
} else {
return !content.startsWith("OSD");
}
}
return true;
}

View File

@ -30,6 +30,7 @@ public:
void setFilterDf(int df);
void setFilterCall(const QString& utcString);
void setFilterLoc(const QString& utcString);
void setFilterInfo(const QString& infoString);
protected:
bool filterAcceptsRow(int sourceRow, const QModelIndex &sourceParent) const override;
@ -41,7 +42,8 @@ private:
FILTER_UTC,
FILTER_DF,
FILTER_CALL,
FILTER_LOC
FILTER_LOC,
FILTER_INFO
};
bool dfInRange(int df) const;
@ -50,6 +52,7 @@ private:
int m_df;
QString m_call;
QString m_loc;
QString m_info;
};
#endif // INCLUDE_FT8DEMODFILTERPROXY_H

View File

@ -475,6 +475,24 @@ void FT8DemodGUI::on_settings_clicked()
changed = true;
}
if (settingsKeys.contains("useOSD"))
{
m_settings.m_useOSD = settings.m_useOSD;
changed = true;
}
if (settingsKeys.contains("osdDepth"))
{
m_settings.m_osdDepth = settings.m_osdDepth;
changed = true;
}
if (settingsKeys.contains("osdLDPCThreshold"))
{
m_settings.m_osdLDPCThreshold = settings.m_osdLDPCThreshold;
changed = true;
}
if (settingsKeys.contains("bandPresets"))
{
m_settings.m_bandPresets = settings.m_bandPresets;
@ -907,6 +925,8 @@ void FT8DemodGUI::filterMessages()
m_messagesFilterProxy.setFilterUTC(m_selectedData.toString());
} else if (m_selectedColumn == FT8DemodSettings::MESSAGE_COL_DF) {
m_messagesFilterProxy.setFilterDf(m_selectedData.toInt());
} else if (m_selectedColumn == FT8DemodSettings::MESSAGE_COL_INFO) {
m_messagesFilterProxy.setFilterInfo(m_selectedData.toString());
}
}

View File

@ -823,7 +823,7 @@
</size>
</property>
<property name="toolTip">
<string>Total number of decodes displayed</string>
<string>Total number of decodes since start</string>
</property>
<property name="text">
<string>0</string>

View File

@ -47,6 +47,9 @@ void FT8DemodSettings::resetToDefaults()
m_logMessages = false;
m_nbDecoderThreads = 3;
m_decoderTimeBudget = 0.5;
m_useOSD = false;
m_osdDepth = 0;
m_osdLDPCThreshold = 70;
m_volume = 1.0;
m_inputFrequencyOffset = 0;
m_rgbColor = QColor(0, 192, 255).rgb();
@ -107,6 +110,9 @@ QByteArray FT8DemodSettings::serialize() const
s.writeS32(8, m_nbDecoderThreads);
s.writeFloat(9, m_decoderTimeBudget);
s.writeBool(11, m_agc);
s.writeBool(12, m_useOSD);
s.writeS32(13, m_osdDepth);
s.writeS32(14, m_osdLDPCThreshold);
s.writeString(16, m_title);
s.writeBool(18, m_useReverseAPI);
s.writeString(19, m_reverseAPIAddress);
@ -172,6 +178,9 @@ bool FT8DemodSettings::deserialize(const QByteArray& data)
d.readS32(8, &m_nbDecoderThreads, 3);
d.readFloat(9, &m_decoderTimeBudget, 0.5);
d.readBool(11, &m_agc, false);
d.readBool(12, &m_useOSD, false);
d.readS32(13, &m_osdDepth, 0);
d.readS32(14, &m_osdLDPCThreshold, 70);
d.readString(16, &m_title, "SSB Demodulator");
d.readBool(18, &m_useReverseAPI, false);
d.readString(19, &m_reverseAPIAddress, "127.0.0.1");

View File

@ -72,6 +72,9 @@ struct FT8DemodSettings
bool m_logMessages;
int m_nbDecoderThreads;
float m_decoderTimeBudget;
bool m_useOSD;
int m_osdDepth;
int m_osdLDPCThreshold;
quint32 m_rgbColor;
QString m_title;
int m_streamIndex; //!< MIMO channel. Not relevant when connected to SI (single Rx).

View File

@ -29,6 +29,11 @@ FT8DemodSettingsDialog::FT8DemodSettingsDialog(FT8DemodSettings& settings, QStri
ui->setupUi(this);
ui->decoderNbThreads->setValue(m_settings.m_nbDecoderThreads);
ui->decoderTimeBudget->setValue(m_settings.m_decoderTimeBudget);
ui->osdEnable->setChecked(m_settings.m_useOSD);
ui->osdDepth->setValue(m_settings.m_osdDepth);
ui->osdDepthText->setText(tr("%1").arg(m_settings.m_osdDepth));
ui->osdLDPCThreshold->setValue(m_settings.m_osdLDPCThreshold);
ui->osdLDPCThresholdText->setText(tr("%1").arg(m_settings.m_osdLDPCThreshold));
resizeBandsTable();
populateBandsTable();
connect(ui->bands, &QTableWidget::cellChanged, this, &FT8DemodSettingsDialog::textCellChanged);
@ -118,6 +123,35 @@ void FT8DemodSettingsDialog::on_decoderTimeBudget_valueChanged(double value)
}
}
void FT8DemodSettingsDialog::on_osdEnable_toggled(bool checked)
{
m_settings.m_useOSD = checked;
if (!m_settingsKeys.contains("useOSD")) {
m_settingsKeys.append("useOSD");
}
}
void FT8DemodSettingsDialog::on_osdDepth_valueChanged(int value)
{
m_settings.m_osdDepth = value;
ui->osdDepthText->setText(tr("%1").arg(m_settings.m_osdDepth));
if (!m_settingsKeys.contains("osdDepth")) {
m_settingsKeys.append("osdDepth");
}
}
void FT8DemodSettingsDialog::on_osdLDPCThreshold_valueChanged(int value)
{
m_settings.m_osdLDPCThreshold = value;
ui->osdLDPCThresholdText->setText(tr("%1").arg(m_settings.m_osdLDPCThreshold));
if (!m_settingsKeys.contains("osdLDPCThreshold")) {
m_settingsKeys.append("osdLDPCThreshold");
}
}
void FT8DemodSettingsDialog::on_addBand_clicked()
{
int currentRow = ui->bands->currentRow();

View File

@ -51,6 +51,9 @@ private slots:
void reject();
void on_decoderNbThreads_valueChanged(int value);
void on_decoderTimeBudget_valueChanged(double value);
void on_osdEnable_toggled(bool checked);
void on_osdDepth_valueChanged(int value);
void on_osdLDPCThreshold_valueChanged(int value);
void on_addBand_clicked();
void on_deleteBand_clicked();
void on_moveBandUp_clicked();

View File

@ -7,7 +7,7 @@
<x>0</x>
<y>0</y>
<width>349</width>
<height>316</height>
<height>333</height>
</rect>
</property>
<property name="font">
@ -25,11 +25,8 @@
<property name="title">
<string>Decoder</string>
</property>
<layout class="QFormLayout" name="formLayout_2">
<property name="verticalSpacing">
<number>2</number>
</property>
<item row="0" column="1">
<layout class="QVBoxLayout" name="verticalLayout_3">
<item>
<layout class="QHBoxLayout" name="threadsGroup">
<item>
<widget class="QLabel" name="decoderNbThreadsLabel">
@ -98,6 +95,124 @@
</item>
</layout>
</item>
<item>
<layout class="QHBoxLayout" name="osdLayout">
<property name="spacing">
<number>2</number>
</property>
<item>
<widget class="ButtonSwitch" name="osdEnable">
<property name="toolTip">
<string>Toggle Ordered Statistics Decoding</string>
</property>
<property name="text">
<string>OSD</string>
</property>
<property name="checkable">
<bool>true</bool>
</property>
</widget>
</item>
<item>
<widget class="QLabel" name="osdDepthlLabel">
<property name="text">
<string>Depth</string>
</property>
</widget>
</item>
<item>
<widget class="QDial" name="osdDepth">
<property name="maximumSize">
<size>
<width>24</width>
<height>24</height>
</size>
</property>
<property name="toolTip">
<string>Order Statistics Decoding depth</string>
</property>
<property name="minimum">
<number>0</number>
</property>
<property name="maximum">
<number>6</number>
</property>
<property name="pageStep">
<number>1</number>
</property>
<property name="value">
<number>0</number>
</property>
</widget>
</item>
<item>
<widget class="QLabel" name="osdDepthText">
<property name="text">
<string>0</string>
</property>
</widget>
</item>
<item>
<widget class="Line" name="line">
<property name="orientation">
<enum>Qt::Vertical</enum>
</property>
</widget>
</item>
<item>
<widget class="QLabel" name="osdLDPCThresholdLabel">
<property name="text">
<string>LDPC Thr</string>
</property>
</widget>
</item>
<item>
<widget class="QDial" name="osdLDPCThreshold">
<property name="maximumSize">
<size>
<width>24</width>
<height>24</height>
</size>
</property>
<property name="toolTip">
<string>Number of correct LDPC bits required to activate OSD </string>
</property>
<property name="minimum">
<number>60</number>
</property>
<property name="maximum">
<number>83</number>
</property>
<property name="pageStep">
<number>1</number>
</property>
<property name="value">
<number>70</number>
</property>
</widget>
</item>
<item>
<widget class="QLabel" name="osdLDPCThresholdText">
<property name="text">
<string>60</string>
</property>
</widget>
</item>
<item>
<spacer name="horizontalSpacer_3">
<property name="orientation">
<enum>Qt::Horizontal</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>40</width>
<height>20</height>
</size>
</property>
</spacer>
</item>
</layout>
</item>
</layout>
</widget>
</item>
@ -282,6 +397,13 @@
</item>
</layout>
</widget>
<customwidgets>
<customwidget>
<class>ButtonSwitch</class>
<extends>QToolButton</extends>
<header>gui/buttonswitch.h</header>
</customwidget>
</customwidgets>
<resources>
<include location="../../../sdrgui/resources/res.qrc"/>
</resources>

View File

@ -110,6 +110,9 @@ FT8DemodWorker::FT8DemodWorker() :
m_recordSamples(false),
m_nbDecoderThreads(6),
m_decoderTimeBudget(0.5),
m_useOSD(false),
m_osdDepth(0),
m_osdLDPCThreshold(70),
m_lowFreq(200),
m_highFreq(3000),
m_invalidSequence(true),
@ -157,6 +160,9 @@ void FT8DemodWorker::processBuffer(int16_t *buffer, QDateTime periodTS)
int hints[2] = { 2, 0 }; // CQ
FT8Callback ft8Callback(periodTS, m_baseFrequency, m_packing, channelReference);
m_ft8Decoder.getParams().nthreads = m_nbDecoderThreads;
m_ft8Decoder.getParams().use_osd = m_useOSD ? 1 : 0;
m_ft8Decoder.getParams().osd_depth = m_osdDepth;
m_ft8Decoder.getParams().osd_ldpc_thresh = m_osdLDPCThreshold;
std::vector<float> samples(15*FT8DemodSettings::m_ft8SampleRate);
std::transform(
@ -217,7 +223,7 @@ void FT8DemodWorker::processBuffer(int16_t *buffer, QDateTime periodTS)
continue;
}
QString logMessage = QString("%1 %2 Rx FT8 %3 %4 %5 %6 %7 %8")
QString logMessage = QString("%1 %2 Rx FT8 %3 %4 %5 %6 %7 %8 %9")
.arg(periodTS.toString("yyyyMMdd_HHmmss"))
.arg(baseFrequencyMHz, 9, 'f', 3)
.arg(ft8Message.snr, 6)
@ -225,7 +231,8 @@ void FT8DemodWorker::processBuffer(int16_t *buffer, QDateTime periodTS)
.arg(ft8Message.df, 4, 'f', 0)
.arg(ft8Message.call1)
.arg(ft8Message.call2)
.arg(ft8Message.loc);
.arg(ft8Message.loc)
.arg(ft8Message.decoderInfo);
logMessage.remove(0, 2);
logFile << logMessage.toStdString() << std::endl;
}

View File

@ -40,6 +40,9 @@ public:
void setLogMessages(bool logMessages) { m_logMessages = logMessages; }
void setNbDecoderThreads(int nbDecoderThreads) { m_nbDecoderThreads = nbDecoderThreads; }
void setDecoderTimeBudget(float decoderTimeBudget) { m_decoderTimeBudget = decoderTimeBudget; }
void setUseOSD(bool useOSD) { m_useOSD = useOSD; }
void setOSDDepth(int osdDepth) { m_osdDepth = osdDepth; }
void setOSDLDPCThreshold(int osdLDPCThreshold) { m_osdLDPCThreshold = osdLDPCThreshold; }
void setLowFrequency(int lowFreq) { m_lowFreq = lowFreq; }
void setHighFrequency(int highFreq) { m_highFreq = highFreq; }
void setReportingMessageQueue(MessageQueue *messageQueue) { m_reportingMessageQueue = messageQueue; }
@ -86,6 +89,9 @@ private:
bool m_logMessages;
int m_nbDecoderThreads;
float m_decoderTimeBudget;
bool m_useOSD;
int m_osdDepth;
int m_osdLDPCThreshold;
int m_lowFreq;
int m_highFreq;
bool m_invalidSequence;

View File

@ -2,7 +2,7 @@
<h2>Introduction</h2>
This plugin can be used to demodulate and decode FT8 signals. FT8 is used by amateur radio to perform contacts (QSOs) with very weak signals. It is used mostly but not limited to HF bands. The protocol and modulation details are described [here](http://www.rtmrtm.org/basicft8/)
This plugin can be used to demodulate and decode FT8 signals. FT8 is used by amateur radio to perform contacts (QSOs) with very weak signals. It is used mostly but not limited to HF bands. The protocol and modulation details are described in [QEX July/August 2020 article](https://www.iz3mez.it/wp-content/library/appunti/Protocols%20FT4%20FT8%20QEX.pdf)
The decoder code is based on [ft8mon](https://github.com/rtmrtmrtmrtm/ft8mon) code written by Robert Morris, AB1HL. The core of the decoder is stored in a separate `libft8` library in the `ft8` folder of this repository and is placed under the `FT8` namespace.
@ -124,6 +124,7 @@ Toggles the filtering of messages. Messages are filtered based on the selected c
- **Call1**: will filter messages matching the call1 area value either in the call1 or call2 areas
- **Call2**: same as above but taking the call2 value
- **Loc**: will filter messages matching the value in the locator (loc) area
- **Info**: will filter values starting with "OSD" or not starting with "OSD" thus filter messages decoded via OSD or not
<h3>C.5: Band preset selection</h3>
@ -139,7 +140,26 @@ Empties the message table (C.10)
<h3>C.8: Log messages</h3>
Toggles the logging of messages. Messages will be logged in the same format as the original WSJT-X format. The splitting and naming of files is different however.
Toggles the logging of messages. Messages will be logged in the same format as the original WSJT-X format except if the message has decoder information in which case this information is appended at the end of the line.
Example:
<code><pre>
230128_003030 10.136 Rx FT8 -22 0.1 2049 KE0DKZ W2ZI EL99 OSD-0-71
---- 1 ------ --2--- -3- -4- --5- --6--- --7- --8- ---9----
</pre></code>
- **1**: Date and time of decoder slot in YYMMDD_HHmmss fomat
- **2**: Base frequency in MHz
- **3**: SNR in 2.5 kHz bandwidth
- **4**: Message start delay in seconds from standard sequence start
- **5**: Message carrier shift from base frequency in Hz
- **6**: First callsign area. May contain spaces (ex: "CQ DX")
- **7**: Second callsign area
- **8**: Locator area
- **9**: Decoder information if any
The splitting and naming of files is different from WSJT-X scheme.
The file name is constructed as follows where date is the day date in YYYYMMDD format:
@ -171,7 +191,7 @@ Displays the received messages in a table which columns are the following:
- **Call1**: This is the first call area and may contain the caller callsign, a CQ or a custom 13 character message in which case the second call and locator areas are empty
- **Call2**: This is the second call area and will contain the callsign of the responding station
- **Loc**: Locator area which contains the 4 character Maidenhead locator, a report, an acknowledgement (RRR) or a greetings (73 or RR73)
- **Info**: FT8 decoder information if any. This comes from he original `ft8mon` code
- **Info**: FT8 decoder information if any. If OSD is active (see C.1.3) and OSD was activated it reports the OSD decoder status as `OSD-N-MM` where N is the OSD depth reached and MM is the number of correct LDPC bits.
<h3>C.1: More FT8 decoder settings</h2>
@ -187,7 +207,21 @@ When processing the audio baseband several instances of the core decoder will be
This is the time in seconds after which the decoder threads will be prompted to stop. It can be varied from 0.1 to 5 seconds. You can imagine that the longer the decoder runs the more messages it will harvest however the default of 0.5s will be enough in most cases. You can still experiment with it to find what value is the best in your own case.
<h4>C.1.3: Band presets table</h4>
<h4>C.1.3: Toggle Ordered Statistics Decoding</h4>
This toggles the Ordered Statistics Decoding (OSD). OSD is used if the CRC check fails after LDPC processing. Be careful with OSD as it will try to find solutions that validate the CRC but these solutions can be wrong and thus yield false messages. When post processing the results it is recommended to check against a database of valid callsigns. At least you should use a list of valid country prefixes and test the locator against the country prefix.
With reasonable depth (C.1.4) and minimum correct LDPC bits (C.1.5) values the amount of false messages should be low so OSD may still be interesting. However if you require best accuracy you should filter messages in a post processing step as suggested above.
<h4>C.1.4: Ordered Statistics Decoding depth</h4>
Sets the maximum depth of OSD search.
<h4>C.1.5: LDPC correct bits threshold</h4>
Sets the minimum number of correct LDPC bits (out of 83) necessary to trigger OSD.
<h4>C.1.6: Band presets table</h4>
This table shows the band presets values that will appear in (C.5)
@ -197,23 +231,23 @@ This table shows the band presets values that will appear in (C.5)
You can edit these values by clicking on the cell in the table.
<h4>C.1.4: Add preset</h4>
<h4>C.1.7: Add preset</h4>
Use this button to create a new preset. It will take the values from the row of the selected cell in the table (if selected) and put the new preset at the bottom of the table
<h4>C.1.5: Delete preset</h4>
<h4>C.1.8: Delete preset</h4>
Delete the preset designated by the selected cell in the table.
<h4>C.1.6: Move up preset</h4>
<h4>C.1.9: Move up preset</h4>
Move up the preset designated by the selected cell in the table.
<h4>C.1.7: Move down preset</h4>
<h4>C.1.10: Move down preset</h4>
Move down the preset designated by the selected cell in the table.
<h4>C.1.8: Restore defaults</h4>
<h4>C.1.11: Restore defaults</h4>
This restores the default band preset values:
@ -235,10 +269,10 @@ This restores the default band preset values:
Channel offsets are all set to 0 kHz.
<h4>C.1.9 Commit changes</h4>
<h4>C.1.12 Commit changes</h4>
Click on the "OK" button to commit changes and close dialog.
<h4>C.1.9 Cancel changes</h4>
<h4>C.1.13 Cancel changes</h4>
Click on the "Cancel" button to close dialog without making changes.

View File

@ -205,6 +205,7 @@ void MainBench::testFT8(const QString& wavFile, const QString& argsStr)
FT8::FT8Decoder decoder;
decoder.getParams().nthreads = nthreads;
decoder.getParams().use_osd = 0;
decoder.entry(
samples.data(),