1
0
mirror of https://github.com/f4exb/sdrangel.git synced 2026-08-14 23:43:43 -04:00

Automatically detect baud rate. Report in table.

Use matched filter and Gardner timing loop to improve decoder performance.
Fix bug where last message of a transmission is lost.
This commit is contained in:
Jon Beniston
2026-08-01 20:08:24 +01:00
parent fd72109d37
commit cd7611f3bf
11 changed files with 515 additions and 286 deletions
+3 -9
View File
@@ -205,7 +205,8 @@ bool PagerDemod::handleMessage(const Message& cmd)
<< CSV::escape(report.getAlphaMessage()) << ","
<< report.getNumericMessage() << ","
<< QString::number(report.getEvenParityErrors()) << ","
<< QString::number(report.getBCHParityErrors()) << "\n";
<< QString::number(report.getBCHParityErrors()) << ","
<< QString::number(report.getBaud()) << "\n";
m_logStream.flush();
}
@@ -292,7 +293,7 @@ void PagerDemod::applySettings(const QStringList& settingsKeys, const PagerDemod
if (newFile)
{
// Write header
m_logStream << "Date,Time,Address,Function Bits,Alpha,Numeric,Even Parity Errors,BCH Parity Errors\n";
m_logStream << "Date,Time,Address,Function Bits,Alpha,Numeric,Even Parity Errors,BCH Parity Errors,Baud\n";
}
}
else
@@ -411,9 +412,6 @@ void PagerDemod::webapiUpdateChannelSettings(
const QStringList& channelSettingsKeys,
SWGSDRangel::SWGChannelSettings& response)
{
if (channelSettingsKeys.contains("baud")) {
settings.m_baud = response.getPagerDemodSettings()->getBaud();
}
if (channelSettingsKeys.contains("decode")) {
settings.m_decode = (PagerDemodSettings::Decode) response.getPagerDemodSettings()->getDecode();
}
@@ -481,7 +479,6 @@ void PagerDemod::webapiUpdateChannelSettings(
void PagerDemod::webapiFormatChannelSettings(SWGSDRangel::SWGChannelSettings& response, const PagerDemodSettings& settings)
{
response.getPagerDemodSettings()->setBaud(settings.m_baud);
response.getPagerDemodSettings()->setDecode((int) settings.m_decode);
response.getPagerDemodSettings()->setReverse(settings.m_reverse ? 1 : 0);
response.getPagerDemodSettings()->setInputFrequencyOffset(settings.m_inputFrequencyOffset);
@@ -607,9 +604,6 @@ void PagerDemod::webapiFormatChannelSettings(
// transfer data that has been modified. When force is on transfer all data except reverse API data
if (channelSettingsKeys.contains("baud") || force) {
swgPagerDemodSettings->setBaud(settings.m_baud);
}
if (channelSettingsKeys.contains("decode") || force) {
swgPagerDemodSettings->setDecode((int) settings.m_decode);
}
+7 -2
View File
@@ -82,6 +82,7 @@ public:
QString getNumericMessage() const { return m_numericMessage; }
int getEvenParityErrors() const { return m_evenParityErrors; }
int getBCHParityErrors() const { return m_bchParityErrors; }
int getBaud() const { return m_baud; }
QDateTime getDateTime() const { return m_dateTime; }
static MsgPagerMessage* create(
@@ -90,7 +91,8 @@ public:
const QString& alphaMessage,
const QString& numericMessage,
int evenParityErrors,
int bchParityErrors
int bchParityErrors,
int baud
)
{
return new MsgPagerMessage(
@@ -100,6 +102,7 @@ public:
numericMessage,
evenParityErrors,
bchParityErrors,
baud,
QDateTime::currentDateTime()
);
}
@@ -111,9 +114,10 @@ public:
QString m_numericMessage;
int m_evenParityErrors;
int m_bchParityErrors;
int m_baud;
QDateTime m_dateTime;
MsgPagerMessage(int address, int functionBits, const QString& alphaMessage, const QString& numericMessage, int evenParityErrors, int bchParityErrors, QDateTime dateTime) :
MsgPagerMessage(int address, int functionBits, const QString& alphaMessage, const QString& numericMessage, int evenParityErrors, int bchParityErrors, int baud, QDateTime dateTime) :
Message(),
m_address(address),
m_functionBits(functionBits),
@@ -121,6 +125,7 @@ public:
m_numericMessage(numericMessage),
m_evenParityErrors(evenParityErrors),
m_bchParityErrors(bchParityErrors),
m_baud(baud),
m_dateTime(dateTime)
{
}
+12 -18
View File
@@ -64,6 +64,7 @@ void PagerDemodGUI::resizeTable()
ui->messages->setItem(row, PagerDemodSettings::MESSAGE_COL_NUMERIC, new QTableWidgetItem("123456789123456789123456789123456789123456789123456789"));
ui->messages->setItem(row, PagerDemodSettings::MESSAGE_COL_EVEN_PE, new QTableWidgetItem("0"));
ui->messages->setItem(row, PagerDemodSettings::MESSAGE_COL_BCH_PE, new QTableWidgetItem("0"));
ui->messages->setItem(row, PagerDemodSettings::MESSAGE_COL_BAUD, new QTableWidgetItem("2400-"));
ui->messages->resizeColumnsToContents();
ui->messages->removeRow(row);
}
@@ -220,7 +221,7 @@ QString PagerDemodGUI::selectMessage(int functionBits, const QString &numericMes
}
// Add row to table
void PagerDemodGUI::messageReceived(const QDateTime dateTime, int address, int functionBits,
void PagerDemodGUI::messageReceived(const QDateTime dateTime, int address, int functionBits, int baud,
const QString &numericMessage, const QString &alphaMessage,
int evenParityErrors, int bchParityErrors)
{
@@ -280,6 +281,7 @@ void PagerDemodGUI::messageReceived(const QDateTime dateTime, int address, int f
QTableWidgetItem *numericItem = new QTableWidgetItem();
QTableWidgetItem *evenPEItem = new QTableWidgetItem();
QTableWidgetItem *bchPEItem = new QTableWidgetItem();
QTableWidgetItem *baudItem = new QTableWidgetItem();
ui->messages->setItem(row, PagerDemodSettings::MESSAGE_COL_DATE, dateItem);
ui->messages->setItem(row, PagerDemodSettings::MESSAGE_COL_TIME, timeItem);
ui->messages->setItem(row, PagerDemodSettings::MESSAGE_COL_ADDRESS, addressItem);
@@ -289,6 +291,7 @@ void PagerDemodGUI::messageReceived(const QDateTime dateTime, int address, int f
ui->messages->setItem(row, PagerDemodSettings::MESSAGE_COL_NUMERIC, numericItem);
ui->messages->setItem(row, PagerDemodSettings::MESSAGE_COL_EVEN_PE, evenPEItem);
ui->messages->setItem(row, PagerDemodSettings::MESSAGE_COL_BCH_PE, bchPEItem);
ui->messages->setItem(row, PagerDemodSettings::MESSAGE_COL_BAUD, baudItem);
dateItem->setText(dateTime.date().toString());
timeItem->setText(dateTime.time().toString());
addressItem->setText(addressString);
@@ -298,6 +301,7 @@ void PagerDemodGUI::messageReceived(const QDateTime dateTime, int address, int f
numericItem->setText(numericMessage);
evenPEItem->setText(QString("%1").arg(evenParityErrors));
bchPEItem->setText(QString("%1").arg(bchParityErrors));
baudItem->setData(Qt::DisplayRole, baud);
if (!m_loadingData)
{
filterRow(row);
@@ -326,7 +330,7 @@ bool PagerDemodGUI::handleMessage(const Message& message)
else if (PagerDemod::MsgPagerMessage::match(message))
{
const PagerDemod::MsgPagerMessage& report = (const PagerDemod::MsgPagerMessage&) message;
messageReceived(report.getDateTime(), report.getAddress(), report.getFunctionBits(),
messageReceived(report.getDateTime(), report.getAddress(), report.getFunctionBits(), report.getBaud(),
report.getNumericMessage(), report.getAlphaMessage(),
report.getEvenParityErrors(), report.getBCHParityErrors());
return true;
@@ -394,13 +398,6 @@ void PagerDemodGUI::on_fmDev_valueChanged(int value)
applySettings(QStringList("fmDeviation"));
}
void PagerDemodGUI::on_baud_currentIndexChanged(int index)
{
(void)index;
m_settings.m_baud = ui->baud->currentText().toInt();
applySettings(QStringList("baud"));
}
void PagerDemodGUI::on_decode_currentIndexChanged(int index)
{
m_settings.m_decode = (PagerDemodSettings::Decode)index;
@@ -677,13 +674,6 @@ void PagerDemodGUI::displaySettings()
ui->deltaFrequency->setValue(m_channelMarker.getCenterFrequency());
if (m_settings.m_baud == 512) {
ui->baud->setCurrentIndex(0);
} else if (m_settings.m_baud == 1200) {
ui->baud->setCurrentIndex(1);
} else {
ui->baud->setCurrentIndex(2);
}
ui->decode->setCurrentIndex((int)m_settings.m_decode);
ui->rfBWText->setText(QString("%1k").arg(m_settings.m_rfBandwidth / 1000.0, 0, 'f', 1));
@@ -850,6 +840,7 @@ void PagerDemodGUI::on_logOpen_clicked()
int numericCol = colIndexes.value("Numeric");
int evenCol = colIndexes.value("Even Parity Errors");
int bchCol = colIndexes.value("BCH Parity Errors");
int baudCol = colIndexes.value("Baud", -1);
int maxCol = std::max({dateCol, timeCol, addressCol, functionCol, alphaCol, numericCol, evenCol, bchCol});
QMessageBox dialog(this);
@@ -879,7 +870,11 @@ void PagerDemodGUI::on_logOpen_clicked()
int evenErrors = cols[evenCol].toInt();
int bchErrors = cols[bchCol].toInt();
messageReceived(dateTime, address, functionBits,
// Baud is absent from logs written before it was recorded
int baud = ((baudCol >= 0) && (baudCol < cols.size()))
? cols[baudCol].toInt() : 0;
messageReceived(dateTime, address, functionBits, baud,
cols[numericCol], cols[alphaCol],
evenErrors, bchErrors);
@@ -918,7 +913,6 @@ void PagerDemodGUI::makeUIConnections()
QObject::connect(ui->deltaFrequency, &ValueDialZ::changed, this, &PagerDemodGUI::on_deltaFrequency_changed);
QObject::connect(ui->rfBW, &QSlider::valueChanged, this, &PagerDemodGUI::on_rfBW_valueChanged);
QObject::connect(ui->fmDev, &QSlider::valueChanged, this, &PagerDemodGUI::on_fmDev_valueChanged);
QObject::connect(ui->baud, QOverload<int>::of(&QComboBox::currentIndexChanged), this, &PagerDemodGUI::on_baud_currentIndexChanged);
QObject::connect(ui->decode, QOverload<int>::of(&QComboBox::currentIndexChanged), this, &PagerDemodGUI::on_decode_currentIndexChanged);
QObject::connect(ui->charset, &QToolButton::clicked, this, &PagerDemodGUI::on_charset_clicked);
QObject::connect(ui->filterAddress, &QLineEdit::editingFinished, this, &PagerDemodGUI::on_filterAddress_editingFinished);
+1 -2
View File
@@ -107,7 +107,7 @@ private:
void applySettings(const QStringList& settingsKeys, bool force = false);
void displaySettings();
QString selectMessage(int functionBits, const QString &numericMessage, const QString &alphaMessage) const;
void messageReceived(const QDateTime dateTime, int address, int functionBits,
void messageReceived(const QDateTime dateTime, int address, int functionBits, int baud,
const QString &numericMessage, const QString &alphaMessage,
int evenParityErrors, int bchParityErrors);
bool handleMessage(const Message& message);
@@ -132,7 +132,6 @@ private slots:
void on_deltaFrequency_changed(qint64 value);
void on_rfBW_valueChanged(int index);
void on_fmDev_valueChanged(int value);
void on_baud_currentIndexChanged(int index);
void on_decode_currentIndexChanged(int index);
void on_charset_clicked();
void on_filterAddress_editingFinished();
+8 -38
View File
@@ -372,44 +372,6 @@
</property>
</widget>
</item>
<item>
<widget class="QLabel" name="baudLabel">
<property name="text">
<string>Baud</string>
</property>
</widget>
</item>
<item>
<widget class="QComboBox" name="baud">
<property name="minimumSize">
<size>
<width>60</width>
<height>0</height>
</size>
</property>
<property name="toolTip">
<string>Baud rate</string>
</property>
<property name="currentIndex">
<number>1</number>
</property>
<item>
<property name="text">
<string>512</string>
</property>
</item>
<item>
<property name="text">
<string>1200</string>
</property>
</item>
<item>
<property name="text">
<string>2400</string>
</property>
</item>
</widget>
</item>
<item>
<widget class="Line" name="line_11">
<property name="orientation">
@@ -851,6 +813,14 @@
<string>Number of BCH parity errors detected in message</string>
</property>
</column>
<column>
<property name="text">
<string>Baud</string>
</property>
<property name="toolTip">
<string>Baud rate the message was received at, detected from the preamble</string>
</property>
</column>
</widget>
</item>
</layout>
@@ -36,7 +36,6 @@ PagerDemodSettings::PagerDemodSettings() :
void PagerDemodSettings::resetToDefaults()
{
m_baud = 1200;
m_inputFrequencyOffset = 0;
m_rfBandwidth = 20000.0f;
m_fmDeviation = 4500.0f;
@@ -81,7 +80,6 @@ QByteArray PagerDemodSettings::serialize() const
s.writeS32(1, m_inputFrequencyOffset);
s.writeFloat(2, m_rfBandwidth);
s.writeFloat(3, m_fmDeviation);
s.writeS32(4, m_baud);
s.writeString(5, m_filterAddress);
s.writeS32(6, (int)m_decode);
s.writeBool(7, m_udpEnabled);
@@ -154,7 +152,6 @@ bool PagerDemodSettings::deserialize(const QByteArray& data)
d.readS32(1, &m_inputFrequencyOffset, 0);
d.readFloat(2, &m_rfBandwidth, 20000.0f);
d.readFloat(3, &m_fmDeviation, 4500.0f);
d.readS32(4, &m_baud, 1200);
d.readString(5, &m_filterAddress, "");
d.readS32(6, (int*)&m_decode, (int)Standard);
d.readBool(7, &m_udpEnabled);
@@ -244,9 +241,6 @@ bool PagerDemodSettings::deserialize(const QByteArray& data)
void PagerDemodSettings::applySettings(const QStringList& settingsKeys, const PagerDemodSettings& settings)
{
if (settingsKeys.contains("baud")) {
m_baud = settings.m_baud;
}
if (settingsKeys.contains("inputFrequencyOffset")) {
m_inputFrequencyOffset = settings.m_inputFrequencyOffset;
}
@@ -353,9 +347,6 @@ QString PagerDemodSettings::getDebugString(const QStringList& settingsKeys, bool
{
std::ostringstream ostr;
if (settingsKeys.contains("baud") || force) {
ostr << " m_baud: " << m_baud;
}
if (settingsKeys.contains("inputFrequencyOffset") || force) {
ostr << " m_inputFrequencyOffset: " << m_inputFrequencyOffset;
}
@@ -33,7 +33,7 @@ class QDataStream;
class Serializable;
// Number of columns in the tables
#define PAGERDEMOD_MESSAGE_COLUMNS 9
#define PAGERDEMOD_MESSAGE_COLUMNS 10
struct PagerDemodSettings
{
@@ -46,7 +46,8 @@ struct PagerDemodSettings
MESSAGE_COL_ALPHA,
MESSAGE_COL_NUMERIC,
MESSAGE_COL_EVEN_PE,
MESSAGE_COL_BCH_PE
MESSAGE_COL_BCH_PE,
MESSAGE_COL_BAUD
};
struct NotificationSettings {
@@ -66,7 +67,6 @@ struct PagerDemodSettings
bool deserialize(const QByteArray& data);
};
qint32 m_baud; //!< 512, 1200 or 2400
qint32 m_inputFrequencyOffset;
Real m_rfBandwidth;
Real m_fmDeviation; //<! 4.5k for POCSAG
+310 -183
View File
@@ -1,6 +1,7 @@
///////////////////////////////////////////////////////////////////////////////////
// Copyright (C) 2021-2022 Jon Beniston, M7RCE <jon@beniston.com> //
// Copyright (C) 2021-2022 Edouard Griffiths, F4EXB <f4exb06@gmail.com> //
// Some code by AI //
// //
// This program is free software; you can redistribute it and/or modify //
// it under the terms of the GNU General Public License as published by //
@@ -39,13 +40,11 @@ PagerDemodSink::PagerDemodSink() :
m_magsqCount(0),
m_messageQueueToChannel(nullptr),
m_dcOffset(0.0f),
m_dataPrev(0),
m_inverted(false),
m_bit(0),
m_gotSOP(false),
m_bits(0),
m_bitCount(0),
m_syncCount(75),
m_batchNumber(0),
m_wordCount(0),
m_addressValid(false),
@@ -55,6 +54,14 @@ PagerDemodSink::PagerDemodSink() :
m_bchErrors(0),
m_alphaBitBuffer(0),
m_alphaBitBufferBits(0),
m_baud(0),
m_mfSum(0.0f),
m_mfPtr(0),
m_timing(0.0f),
m_yPrev(0.0f),
m_yMid(0.0f),
m_gotMid(false),
m_yPower(0.0f),
m_sampleBufferIndex(0)
{
m_magsq = 0.0;
@@ -272,33 +279,28 @@ void PagerDemodSink::decodeBatch()
{
for (int word = 0; word < PAGERDEMOD_CODEWORDS_PER_FRAME; word++)
{
// If BCH decoding failed, we can't trust any of the bits in this codeword,
// including the address/message flag in the MSB. Treat it as an erasure, so it
// can't start a phantom address or prematurely terminate a valid message.
// The 20 message bits are still fed into the alpha bit buffer below, to keep
// the 7-bit character boundaries aligned - m_bchErrors flags the message as
// containing errors
if (m_codeWordsBCHError[i])
{
if (m_addressValid)
{
m_bchErrors++;
addMessageBits((m_codeWords[i] >> 11) & 0xfffff);
}
i++;
continue;
}
bool addressCodeWord = ((m_codeWords[i] >> 31) & 1) == 0;
// Stop decoding current message if we receive a new address
if (addressCodeWord && m_addressValid)
{
m_numericMessage = m_numericMessage.trimmed(); // Remove trailing spaces
if (getMessageQueueToChannel())
{
// Convert from 7-bit to UTF-8 using user specified encoding
for (int i = 0; i < m_alphaMessage.size(); i++)
{
QChar c = m_alphaMessage[i];
int idx = m_settings.m_sevenbit.indexOf(c.toLatin1());
if (idx >= 0) {
c = QChar(m_settings.m_unicode[idx]);
}
m_alphaMessage[i] = c;
}
// Reverse reading order, if required
if (m_settings.m_reverse) {
std::reverse(m_alphaMessage.begin(), m_alphaMessage.end());
}
// Send to channel and GUI
PagerDemod::MsgPagerMessage *msg = PagerDemod::MsgPagerMessage::create(m_address, m_functionBits, m_alphaMessage, m_numericMessage, m_parityErrors, m_bchErrors);
getMessageQueueToChannel()->push(msg);
}
m_addressValid = false;
if (addressCodeWord && m_addressValid) {
sendMessage();
}
// Check parity bit
@@ -321,7 +323,7 @@ void PagerDemodSink::decodeBatch()
m_alphaBitBufferBits = 0;
m_alphaBitBuffer = 0;
m_parityErrors = parityError ? 1 : 0;
m_bchErrors = m_codeWordsBCHError[i] ? 1 : 0;
m_bchErrors = 0; // Erased codewords never get here, so this one decoded cleanly
m_addressValid = true;
}
else if (m_addressValid)
@@ -329,48 +331,11 @@ void PagerDemodSink::decodeBatch()
// Message - decode as both numeric and ASCII - not all operators use functionBits to indidcate encoding
// Only decoded after an address codeword, otherwise we'd be decoding a message we've
// tuned in to the middle of, without knowing who it's for
int messageBits = (m_codeWords[i] >> 11) & 0xfffff;
if (parityError) {
m_parityErrors++;
}
if (m_codeWordsBCHError[i]) {
m_bchErrors++;
}
// Numeric format
for (int j = 16; j >= 0; j -= 4)
{
quint32 numericBits = (messageBits >> j) & 0xf;
numericBits = reverse(numericBits) >> (32-4);
// Spec has 0xa as 'spare', but other decoders treat is as .
const char numericChars[] = {
'0', '1', '2', '3', '4', '5', '6', '7', '8', '9', '.', 'U', ' ', '-', ')', '('
};
char numericChar = numericChars[numericBits];
m_numericMessage.append(numericChar);
}
// 7-bit ASCII alpnanumeric format
m_alphaBitBuffer = (m_alphaBitBuffer << 20) | messageBits;
m_alphaBitBufferBits += 20;
while (m_alphaBitBufferBits >= 7)
{
// Extract next 7-bit character from bit buffer
char c = (m_alphaBitBuffer >> (m_alphaBitBufferBits-7)) & 0x7f;
// Reverse bit ordering
c = reverse(c) >> (32-7);
// Add to received message string (excluding, null, end of text, end ot transmission)
if (c != 0 && c != 0x3 && c != 0x4) {
m_alphaMessage.append(c);
}
// Remove from bit buffer
m_alphaBitBufferBits -= 7;
if (m_alphaBitBufferBits == 0) {
m_alphaBitBuffer = 0;
} else {
m_alphaBitBuffer &= (1 << m_alphaBitBufferBits) - 1;
}
}
addMessageBits((m_codeWords[i] >> 11) & 0xfffff);
}
// Move to next codeword
@@ -379,6 +344,262 @@ void PagerDemodSink::decodeBatch()
}
}
// Retune the bit level demodulator to a different baud rate. This is only called while
// unsynced, so there is no partially received message to lose
void PagerDemodSink::setBaud(int baud)
{
m_baud = baud;
m_samplesPerSymbol = PagerDemodSettings::m_channelSampleRate / baud;
// Signal is a square wave - so include several harmonics
m_lowpassBaud.create(301, PagerDemodSettings::m_channelSampleRate, baud * 5.0f);
m_bits = 0;
m_bitCount = 0;
// Restart the matched filter and timing loop at the new symbol period
m_mfBuf.assign(m_samplesPerSymbol, 0.0f);
m_mfSum = 0.0f;
m_mfPtr = 0;
m_timing = m_samplesPerSymbol;
m_yPrev = 0.0f;
m_yMid = 0.0f;
m_gotMid = false;
m_yPower = 0.0f;
qDebug() << "PagerDemodSink::setBaud: " << baud << " m_samplesPerSymbol: " << m_samplesPerSymbol;
}
// Matched filter (boxcar integrate and dump over one symbol, which is the matched filter
// for NRZ) and Gardner timing recovery.
//
// The symbol clock free runs and is only nudged, rather than being reset by every signal
// edge. That matters because an isolated noise transition could previously delay or skip a
// bit, and a single inserted or deleted bit destroys codeword alignment for the rest of
// the batch, which BCH cannot recover from.
bool PagerDemodSink::matchedFilterAndDpll(Real v)
{
// Matched filter: running sum over one symbol period
m_mfSum -= m_mfBuf[m_mfPtr];
m_mfBuf[m_mfPtr] = v;
m_mfSum += v;
m_mfPtr = (m_mfPtr + 1) % m_samplesPerSymbol;
m_timing -= 1.0f;
// Sample midway between the previous and current symbol instants, for the timing error
if (!m_gotMid && (m_timing <= m_samplesPerSymbol / 2.0f))
{
m_yMid = m_mfSum;
m_gotMid = true;
}
if (m_timing > 0.0f) {
return false;
}
Real y = m_mfSum;
// Gardner timing error detector - needs no decision and is zero when we're on time
Real e = (y - m_yPrev) * m_yMid;
// Normalise out the amplitude, so loop gain doesn't depend on signal level
m_yPower = 0.999f * m_yPower + 0.001f * (y * y);
Real eNorm = (m_yPower > 1e-9f) ? (e / m_yPower) : 0.0f;
eNorm = std::max(-1.0f, std::min(1.0f, eNorm));
m_timing += m_samplesPerSymbol - m_dpllGain * eNorm;
// Keep the correction sane when the loop is being driven by noise
if (m_timing < m_samplesPerSymbol * 0.5f) {
m_timing = m_samplesPerSymbol * 0.5f;
} else if (m_timing > m_samplesPerSymbol * 1.5f) {
m_timing = m_samplesPerSymbol * 1.5f;
}
m_yPrev = y;
m_gotMid = false;
// According to a variety of places on the web, high frequency is a 0, low is 1.
// While this seems to be correct in the UK, some IQ files I've obtained seem
// to be reversed, so we support both. The shift register always holds bits in the
// standard mapping and polarity is applied when a codeword is extracted, so the
// retained history stays consistent if polarity changes on sync loss
int data = y >= 0.0f;
handleBit(!data);
return true;
}
// Accumulate a demodulated bit, looking for the frame sync code and then codewords
void PagerDemodSink::handleBit(int bit)
{
m_bit = bit;
// Store in shift reg. MSB transmitted first
m_bits = (m_bits << 1) | m_bit;
m_bitCount++;
if (m_bitCount > 32) {
m_bitCount = 32;
}
if ((m_bitCount == 32) && !m_gotSOP)
{
// Look for synccode that starts a batch - allow two errors that can be corrected
if (m_bits == PAGERDEMOD_POCSAG_SYNCCODE)
{
m_gotSOP = true;
m_inverted = false;
}
else if (m_bits == PAGERDEMOD_POCSAG_SYNCCODE_INV)
{
m_gotSOP = true;
m_inverted = true;
}
else if (popcount((m_bits ^ PAGERDEMOD_POCSAG_SYNCCODE) & 0xfffffffeU) <= 2)
{
quint32 correctedCW;
if (bchDecode(m_bits, correctedCW)
&& ((correctedCW & 0xfffffffeU) == (PAGERDEMOD_POCSAG_SYNCCODE & 0xfffffffeU)))
{
m_gotSOP = true;
m_inverted = false;
}
}
else if (popcount((m_bits ^ PAGERDEMOD_POCSAG_SYNCCODE_INV) & 0xfffffffeU) <= 2)
{
quint32 correctedCW;
if (bchDecode(~m_bits, correctedCW)
&& ((correctedCW & 0xfffffffeU) == (PAGERDEMOD_POCSAG_SYNCCODE & 0xfffffffeU)))
{
m_gotSOP = true;
m_inverted = true;
}
}
if (m_gotSOP)
{
// Reset demod state
m_bits = 0;
m_bitCount = 0;
m_codeWords[0] = PAGERDEMOD_POCSAG_SYNCCODE;
m_wordCount = 1;
m_addressValid = false;
}
}
else if ((m_bitCount == 32) && m_gotSOP)
{
// Got a complete codeword - apply the detected polarity, then use BCH decoding
// to fix any bit errors
quint32 cw = m_inverted ? ~m_bits : m_bits;
quint32 correctedCW;
m_codeWordsBCHError[m_wordCount] = !bchDecode(cw, correctedCW);
m_codeWords[m_wordCount] = correctedCW;
m_wordCount++;
// Check for sync code at start of batch
if ((m_wordCount == 1)
&& ((correctedCW & 0xfffffffeU) != (PAGERDEMOD_POCSAG_SYNCCODE & 0xfffffffeU)))
{
// A message is only sent when the *next* address arrives, so without
// this the last message of a transmission would be silently dropped
if (m_addressValid) {
sendMessage();
}
m_gotSOP = false;
m_addressValid = false;
// m_inverted is deliberately not reset - m_bits holds standard mapped bits,
// so the retained history stays valid and the sync search sets polarity again
}
// Have we received a complete batch
if (m_wordCount == PAGERDEMOD_BATCH_WORDS)
{
// Decode it to addresses and messages
decodeBatch();
// Start a new batch
m_batchNumber++;
m_wordCount = 0;
}
if (m_gotSOP)
{
m_bits = 0;
m_bitCount = 0;
}
// If we've just lost sync, keep the shift register, so the sliding search
// can resume on the next bit rather than waiting for 32 new ones
}
}
// Send the message that has been accumulated for the current address
void PagerDemodSink::sendMessage()
{
m_numericMessage = m_numericMessage.trimmed(); // Remove trailing spaces
if (getMessageQueueToChannel())
{
// Convert from 7-bit to UTF-8 using user specified encoding
for (int i = 0; i < m_alphaMessage.size(); i++)
{
QChar c = m_alphaMessage[i];
int idx = m_settings.m_sevenbit.indexOf(c.toLatin1());
if (idx >= 0) {
c = QChar(m_settings.m_unicode[idx]);
}
m_alphaMessage[i] = c;
}
// Reverse reading order, if required
if (m_settings.m_reverse) {
std::reverse(m_alphaMessage.begin(), m_alphaMessage.end());
}
// Send to channel and GUI
PagerDemod::MsgPagerMessage *msg = PagerDemod::MsgPagerMessage::create(m_address, m_functionBits, m_alphaMessage, m_numericMessage, m_parityErrors, m_bchErrors, m_baud);
getMessageQueueToChannel()->push(msg);
}
m_addressValid = false;
}
// Decode the 20 message bits of a codeword as both numeric and 7-bit alphanumeric
void PagerDemodSink::addMessageBits(int messageBits)
{
// Numeric format
for (int j = 16; j >= 0; j -= 4)
{
quint32 numericBits = (messageBits >> j) & 0xf;
numericBits = reverse(numericBits) >> (32-4);
// Spec has 0xa as 'spare', but other decoders treat is as .
const char numericChars[] = {
'0', '1', '2', '3', '4', '5', '6', '7', '8', '9', '.', 'U', ' ', '-', ')', '('
};
char numericChar = numericChars[numericBits];
m_numericMessage.append(numericChar);
}
// 7-bit ASCII alpnanumeric format
m_alphaBitBuffer = (m_alphaBitBuffer << 20) | messageBits;
m_alphaBitBufferBits += 20;
while (m_alphaBitBufferBits >= 7)
{
// Extract next 7-bit character from bit buffer
char c = (m_alphaBitBuffer >> (m_alphaBitBufferBits-7)) & 0x7f;
// Reverse bit ordering
c = reverse(c) >> (32-7);
// Add to received message string (excluding, null, end of text, end ot transmission)
if (c != 0 && c != 0x3 && c != 0x4) {
m_alphaMessage.append(c);
}
// Remove from bit buffer
m_alphaBitBufferBits -= 7;
if (m_alphaBitBufferBits == 0) {
m_alphaBitBuffer = 0;
} else {
m_alphaBitBuffer &= (1 << m_alphaBitBufferBits) - 1;
}
}
}
void PagerDemodSink::processOneSample(Complex &ci)
{
// FM demodulation
@@ -398,6 +619,22 @@ void PagerDemodSink::processOneSample(Complex &ci)
m_magsqCount++;
// Detect the baud rate from the preamble. Only run while unsynced: that is when the
// rate can change, and it keeps the per-rate filters out of the decoding path
if (!m_gotSOP)
{
int best = m_baudDetector.process(fmDemod);
if (m_baudDetector.metric(best) >= m_preambleThreshold)
{
int baud = PagerDemodBaudDetector::m_rates[best];
if (baud != m_baud) {
setBaud(baud);
}
}
}
// Low pass filter
Real filt = m_lowpassBaud.filter(fmDemod);
@@ -414,120 +651,10 @@ void PagerDemodSink::processOneSample(Complex &ci)
// Slice data
int data = (filt - m_dcOffset) >= 0.0;
// Look for edge - A PLL here would be less susceptible to noise
if (data != m_dataPrev)
{
// Center in middle of bit
m_syncCount = m_samplesPerSymbol/2;
}
else
{
// Wait until centre of bit to sample it
m_syncCount--;
if (m_syncCount <= 0)
{
// According to a variety of places on the web, high frequency is a 0, low is 1.
// While this seems to be correct in the UK, some IQ files I've obtained seem
// to be reversed, so we support both.
if (m_inverted) {
m_bit = data;
} else {
m_bit = !data;
}
sample = true;
// Store in shift reg. MSB transmitted first
m_bits = (m_bits << 1) | m_bit;
m_bitCount++;
if (m_bitCount > 32) {
m_bitCount = 32;
}
if ((m_bitCount == 32) && !m_gotSOP)
{
// Look for synccode that starts a batch - allow two errors that can be corrected
if (m_bits == PAGERDEMOD_POCSAG_SYNCCODE)
{
m_gotSOP = true;
m_inverted = false;
}
else if (m_bits == PAGERDEMOD_POCSAG_SYNCCODE_INV)
{
m_gotSOP = true;
m_inverted = true;
}
else if (popcount((m_bits ^ PAGERDEMOD_POCSAG_SYNCCODE) & 0xfffffffeU) <= 2)
{
quint32 correctedCW;
if (bchDecode(m_bits, correctedCW)
&& ((correctedCW & 0xfffffffeU) == (PAGERDEMOD_POCSAG_SYNCCODE & 0xfffffffeU)))
{
m_gotSOP = true;
m_inverted = false;
}
}
else if (popcount((m_bits ^ PAGERDEMOD_POCSAG_SYNCCODE_INV) & 0xfffffffeU) <= 2)
{
quint32 correctedCW;
if (bchDecode(~m_bits, correctedCW)
&& ((correctedCW & 0xfffffffeU) == (PAGERDEMOD_POCSAG_SYNCCODE & 0xfffffffeU)))
{
m_gotSOP = true;
m_inverted = true;
}
}
if (m_gotSOP)
{
// Reset demod state
m_bits = 0;
m_bitCount = 0;
m_codeWords[0] = PAGERDEMOD_POCSAG_SYNCCODE;
m_wordCount = 1;
m_addressValid = false;
}
}
else if ((m_bitCount == 32) && m_gotSOP)
{
// Got a complete codeword - use BCH decoding to fix any bit errors
quint32 correctedCW;
m_codeWordsBCHError[m_wordCount] = !bchDecode(m_bits, correctedCW);
m_codeWords[m_wordCount] = correctedCW;
m_wordCount++;
// Check for sync code at start of batch
if ((m_wordCount == 1)
&& ((correctedCW & 0xfffffffeU) != (PAGERDEMOD_POCSAG_SYNCCODE & 0xfffffffeU)))
{
m_gotSOP = false;
//m_thresholdMet = false;
m_addressValid = false;
m_inverted = false;
}
// Have we received a complete batch
if (m_wordCount == PAGERDEMOD_BATCH_WORDS)
{
// Decode it to addresses and messages
decodeBatch();
// Start a new batch
m_batchNumber++;
m_wordCount = 0;
}
m_bits = 0;
m_bitCount = 0;
}
m_syncCount = m_samplesPerSymbol;
}
}
// Matched filter and timing recovery produce the bits
sample = matchedFilterAndDpll(filt - m_dcOffset);
// Save data for edge detection
m_dataPrev = data;
// Select signals to feed to scope
Complex scopeSample;
@@ -666,13 +793,13 @@ void PagerDemodSink::applySettings(const QStringList& settingsKeys, const PagerD
m_phaseDiscri.setFMScaling(PagerDemodSettings::m_channelSampleRate / (2.0f * settings.m_fmDeviation));
}
if ((settingsKeys.contains("baud") && (settings.m_baud != m_settings.m_baud)) || force)
if (force)
{
m_samplesPerSymbol = PagerDemodSettings::m_channelSampleRate / settings.m_baud;
qDebug() << "PagerDemodSink::applySettings: m_samplesPerSymbol: " << m_samplesPerSymbol;
// Signal is a square wave - so include several harmonics
m_lowpassBaud.create(301, PagerDemodSettings::m_channelSampleRate, settings.m_baud * 5.0f);
// The baud rate is detected from the preamble rather than configured, as POCSAG
// networks mix rates and a single channel can carry different rates at different
// times. Start at the most common rate until the first preamble is detected.
m_baudDetector.create(PagerDemodSettings::m_channelSampleRate);
setBaud(1200);
}
if (force) {
+155 -2
View File
@@ -22,6 +22,10 @@
#include <QVector>
#include <algorithm>
#include <cmath>
#include <vector>
#include "dsp/channelsamplesink.h"
#include "dsp/phasediscri.h"
#include "dsp/nco.h"
@@ -43,6 +47,131 @@ class ChannelAPI;
class PagerDemod;
class ScopeVis;
// Detects the POCSAG preamble, which is 576 bits of 1010..., i.e. a square wave at half
// the baud rate. Correlates against that frequency and returns the fraction of the
// window's power in that bin: 1.0 for a pure sinusoid, 8/pi^2 = 0.81 for an ideal square
// wave and ~2/N for noise. The window is a whole number of cycles, so a sample leaving it
// has the same twiddle index as the one entering, making the update a single subtract.
class PagerDemodPreambleDetector
{
public:
void create(int samplesPerSymbol, int cycles)
{
m_period = 2 * samplesPerSymbol; // one cycle of the 1010... pattern
m_n = cycles * m_period;
m_cos.resize(m_period);
m_sin.resize(m_period);
for (int i = 0; i < m_period; i++)
{
double a = 2.0 * M_PI * i / m_period;
m_cos[i] = cos(a);
m_sin[i] = sin(a);
}
m_hist.assign(m_n, 0.0);
m_ptr = 0;
m_idx = 0;
m_i = 0.0;
m_q = 0.0;
m_p = 0.0;
m_filled = 0;
}
double process(double x)
{
double old = m_hist[m_ptr];
m_hist[m_ptr] = x;
m_ptr = (m_ptr + 1) % m_n;
double d = x - old;
m_i += d * m_cos[m_idx];
m_q += d * m_sin[m_idx];
m_p += x*x - old*old;
m_idx = (m_idx + 1) % m_period;
if (m_filled < m_n)
{
m_filled++;
return 0.0;
}
double denom = (m_n / 2.0) * m_p;
if (denom <= 0.0) {
return 0.0;
}
double m = (m_i*m_i + m_q*m_q) / denom;
return std::min(1.0, std::max(0.0, m));
}
private:
int m_period = 0;
int m_n = 0;
std::vector<double> m_cos;
std::vector<double> m_sin;
std::vector<double> m_hist;
int m_ptr = 0;
int m_idx = 0;
int m_filled = 0;
double m_i = 0.0;
double m_q = 0.0;
double m_p = 0.0;
};
// Determines which POCSAG baud rate is being transmitted, by running a preamble detector
// per rate. Each rate needs its own post detection filter, as the metric is a fraction of
// the window's power - sharing one wide filter would load the low rate detectors with
// noise they would not otherwise see, biasing detection towards the high rates.
class PagerDemodBaudDetector
{
public:
static const int m_numRates = 3;
static constexpr int m_rates[m_numRates] = {512, 1200, 2400};
void create(int channelSampleRate)
{
for (int r = 0; r < m_numRates; r++)
{
// Short filters: the detector only has to give each correlator a roughly
// rate-appropriate noise bandwidth, so it doesn't need the decoder's
// selectivity - and this runs on every sample while unsynced
m_lowpass[r].create(63, channelSampleRate, m_rates[r] * 5.0f);
m_detector[r].create(channelSampleRate / m_rates[r], 8);
m_metric[r] = 0.0;
}
}
//!< Feed the raw FM demodulator output; returns the index of the most likely rate
int process(Real fmDemod)
{
int best = 0;
for (int r = 0; r < m_numRates; r++)
{
Real filt = m_lowpass[r].filter(fmDemod);
m_dc[r](filt);
m_metric[r] = m_detector[r].process(filt - m_dc[r].asDouble());
if (m_metric[r] > m_metric[best]) {
best = r;
}
}
return best;
}
double metric(int r) const { return m_metric[r]; }
private:
Lowpass<Real> m_lowpass[m_numRates];
PagerDemodPreambleDetector m_detector[m_numRates];
MovingAverageUtil<Real, double, 2048> m_dc[m_numRates];
double m_metric[m_numRates] = {0.0};
};
class PagerDemodSink : public ChannelSampleSink {
public:
PagerDemodSink();
@@ -93,6 +222,27 @@ private:
int m_channelSampleRate;
int m_channelFrequencyOffset;
int m_samplesPerSymbol; // Number of samples per symbol
int m_baud; // Currently detected baud rate
PagerDemodBaudDetector m_baudDetector;
// Matched filter (boxcar integrate and dump over a symbol, which is the matched filter
// for NRZ) and Gardner timing loop. The symbol clock free runs and is only nudged, so
// an isolated noise transition can't insert or delete a bit - and a bit slip destroys
// codeword alignment for the rest of the batch
std::vector<Real> m_mfBuf; // Last symbol's worth of samples
Real m_mfSum; // Sum of them, i.e. the matched filter output
int m_mfPtr;
Real m_timing; // Samples until the next symbol instant
Real m_yPrev; // Matched filter output at the last symbol instant
Real m_yMid; // ...and midway between the last two
bool m_gotMid;
Real m_yPower; // Running power, to normalise the timing error
//!< Gardner loop gain. Must be positive - a negative gain locks to the wrong phase and
//!< decodes nothing, while still sampling at the full symbol rate
static constexpr Real m_dpllGain = 0.2f;
//!< Fraction of power at half the baud rate needed to accept a preamble. Measured
//!< separation is wide - noise and data sit below 0.2, a real preamble above 0.85
static constexpr double m_preambleThreshold = 0.5;
NCO m_nco;
Interpolator m_interpolator;
@@ -114,13 +264,11 @@ private:
PhaseDiscriminators m_phaseDiscri; // FM demodulator
Lowpass<Real> m_lowpassBaud; // Low pass filter for FM demod output
Real m_dcOffset; // Calculated DC offset of preamble
int m_dataPrev; // m_data for previous sample
bool m_inverted; // Whether low frequency is a 1 or 0
int m_bit; // Sampled bit
bool m_gotSOP; // Set when sync word received
quint32 m_bits; // Received bit shift register
int m_bitCount; // Number of bits in m_bits
int m_syncCount; // Sample count to centre of bit
int m_batchNumber; // Count of batches in current transmission
quint32 m_codeWords[PAGERDEMOD_BATCH_WORDS]; // Received codewords within a batch
@@ -147,6 +295,11 @@ private:
MessageQueue *getMessageQueueToChannel() { return m_messageQueueToChannel; }
void sampleToScope(Complex sample);
void decodeBatch();
void sendMessage();
void addMessageBits(int messageBits);
void setBaud(int baud);
void handleBit(int bit);
bool matchedFilterAndDpll(Real v);
int xorBits(quint32 word, int firstBit, int lastBit);
bool evenParity(quint32 word, int firstBit, int lastBit, int parityBit);
quint32 reverse(quint32 x);
+16 -17
View File
@@ -36,15 +36,13 @@ Adjusts the expected peak frequency deviation in 0.1 kHz steps from 1 to 6 kHz.
Specifies the pager modulation. Currently only POCSAG is supported.
POCSAG uses FSK with 4.5kHz frequency shift, at 512, 1200 or 2400 baud.
POCSAG uses FSK with 4.5kHz frequency shift, at 512, 1200 or 2400 baud. The baud rate is
detected automatically from the preamble, as networks mix rates and a single channel can
carry different rates at different times.
High frequency is typically 0, with low 1, but occasionally this appears to be reversed, so the demodulator supports either.
Data is framed as specified in [ITU-R M.584-2](https://www.itu.int/dms_pubrec/itu-r/rec/m/R-REC-M.584-2-199711-I!!PDF-E.pdf)
<h3>7: Baud</h3>
Specifies the baud rate. For POCSAG, this can be 512, 1200 or 2400.
<h3>8: Decode</h3>
<h3>7: Decode</h3>
Specifies how messages are decoded in the Message column in the table:
@@ -56,7 +54,7 @@ Specifies how messages are decoded in the Message column in the table:
The table has Numeric and Alphanumeric columns which always display the corresponding decode.
<h3>9: Character encoding</h3>
<h3>8: Character encoding</h3>
Click to open the character encoding dialog, which allows a mapping from the received 7-bit alphanumeric characters to Unicode.
@@ -64,36 +62,36 @@ Click to open the character encoding dialog, which allows a mapping from the rec
Each row contains a mapping from a 7-bit value to a Unicode code point. Values should be entered in hexadecimal
<h3>10: Find</h3>
<h3>9: Find</h3>
Entering a regular expression in the Find field displays only messages where the address matches the given regular expression.
<h3>11: Clear Messages from table</h3>
<h3>10: Clear Messages from table</h3>
Pressing this button clears all messages from the table.
<h3>12: UDP</h3>
<h3>11: UDP</h3>
When checked, received messages are forwarded to the specified UDP address (12) and port (13).
The messages are forwarded as null terminated ASCII strings, in the format: data time address function alpha numeric
<h3>13: UDP address</h3>
<h3>12: UDP address</h3>
IP address of the host to forward received messages to via UDP.
<h3>14: UDP port</h3>
<h3>13: UDP port</h3>
UDP port number to forward received messages to.
<h3>15: Filter Duplicates</h3>
<h3>14: Filter Duplicates</h3>
Check to filter (discard) duplicate messages. Right click to show the Duplicate Filter options dialog:
- Match message only: When unchecked, compare address and message. When checked, compare only message, ignoring the address.
- Match last message only: When unchecked the message is compared against all messages in the table. When checked, the message is compared against the last received message only.
<h3>16: Open Notifications Dialog</h3>
<h3>15: Open Notifications Dialog</h3>
When clicked, opens the Notifications Dialog, which allows speech notifications or programs/scripts to be run when messages matching user-defined rules are received.
@@ -116,15 +114,15 @@ In the Speech and Command strings, variables can be used to substitute data from
To experiment with regular expressions, try [https://regexr.com/](https://regexr.com/).
<h3>17: Start/stop Logging Messages to .csv File</h3>
<h3>16: Start/stop Logging Messages to .csv File</h3>
When checked, writes all received messages to a .csv file.
<h3>18: .csv Log Filename</h3>
<h3>17: .csv Log Filename</h3>
Click to specify the name of the .csv file which received messages are logged to.
<h3>19: Read Data from .csv File</h3>
<h3>18: Read Data from .csv File</h3>
Click to specify a previously written .csv log file, which is read and used to update the table.
@@ -143,5 +141,6 @@ The received messages table displays each pager message received.
* Numeric - Message decoded as numeric, regardless of Decode setting (8).
* Even PE - Number of even parity errors detected in the code words of the message.
* BCH PE - Number of uncorrectable BCH parity errors detected in the code words of the message.
* Baud - The baud rate the message was received at, detected automatically from the preamble.
Right clicking on the table header allows you to select which columns to show. The columns can be reordered by left clicking and dragging the column header. Right clicking on an item in the table allows you to copy the value to the clipboard.
@@ -1,9 +1,6 @@
PagerDemodSettings:
description: PagerDemod
properties:
baud:
description: "Baud rate"
type: integer
decode:
type: integer
description: >