Decode status widget (#1431)

* Initial cleanup of pocsag beta, using DSP filters

* Better filter params

* Better filter

* Add signal diagnostics widgets

* POCSAG procs sends stats messages

* Only draw 32 bits

* Add AudioNormalizer filter
This commit is contained in:
Kyle Reed 2023-09-03 21:49:44 -07:00 committed by GitHub
parent 2435ee780f
commit 4819a2f4e2
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
9 changed files with 305 additions and 230 deletions

View File

@ -105,6 +105,8 @@ POCSAGAppView::POCSAGAppView(NavigationView& nav)
&field_volume, &field_volume,
&image_status, &image_status,
&text_packet_count, &text_packet_count,
&widget_bits,
&widget_frames,
&button_ignore_last, &button_ignore_last,
&button_config, &button_config,
&console}); &console});
@ -271,7 +273,31 @@ void POCSAGAppView::on_packet(const POCSAGPacketMessage* message) {
image_status.set_foreground(get_status_color(pocsag_state)); image_status.set_foreground(get_status_color(pocsag_state));
} }
void POCSAGAppView::on_stats(const POCSAGStatsMessage*) { void POCSAGAppView::on_stats(const POCSAGStatsMessage* stats) {
widget_bits.set_bits(stats->current_bits);
widget_frames.set_frames(stats->current_frames);
widget_frames.set_sync(stats->has_sync);
}
void BitsIndicator::paint(Painter&) {
auto p = screen_pos();
for (size_t i = 0; i < sizeof(bits_) * 8; ++i) {
auto is_set = ((bits_ >> i) & 0x1) == 1;
int x = p.x() + (i / height);
int y = p.y() + (i % height);
display.draw_pixel({x, y}, is_set ? Color::white() : Color::black());
}
}
void FrameIndicator::paint(Painter& painter) {
auto p = screen_pos();
painter.draw_rectangle({p, {2, height}}, has_sync_ ? Color::green() : Color::grey());
for (size_t i = 0; i < height; ++i) {
auto p2 = p + Point{2, 16 - (int)i};
painter.draw_hline(p2, 2, i < frame_count_ ? Color::white() : Color::black());
}
} }
} /* namespace ui */ } /* namespace ui */

View File

@ -52,6 +52,50 @@ class POCSAGLogger {
namespace ui { namespace ui {
class BitsIndicator : public Widget {
public:
BitsIndicator(Point position)
: Widget{{position, {2, height}}} {}
void paint(Painter& painter) override;
void set_bits(uint32_t bits) {
if (bits != bits_) {
bits_ = bits;
set_dirty();
}
}
private:
static constexpr uint8_t height = 16;
uint32_t bits_ = 0;
};
class FrameIndicator : public Widget {
public:
FrameIndicator(Point position)
: Widget{{position, {4, height}}} {}
void paint(Painter& painter) override;
void set_frames(uint8_t frame_count) {
if (frame_count != frame_count_) {
frame_count_ = frame_count;
set_dirty();
}
}
void set_sync(bool has_sync) {
if (has_sync != has_sync_) {
has_sync_ = has_sync;
set_dirty();
}
}
private:
static constexpr uint8_t height = 16;
uint8_t frame_count_ = 0;
bool has_sync_ = false;
};
struct POCSAGSettings { struct POCSAGSettings {
bool enable_small_font = false; bool enable_small_font = false;
bool enable_logging = false; bool enable_logging = false;
@ -187,7 +231,8 @@ class POCSAGAppView : public View {
2, 2,
{0, 99}, {0, 99},
1, 1,
' '}; ' ',
true /*wrap*/};
AudioVolumeField field_volume{ AudioVolumeField field_volume{
{28 * 8, 0 * 16}}; {28 * 8, 0 * 16}};
@ -201,6 +246,12 @@ class POCSAGAppView : public View {
{3 * 8, 1 * 16 + 2, 5 * 8, 16}, {3 * 8, 1 * 16 + 2, 5 * 8, 16},
"0"}; "0"};
BitsIndicator widget_bits{
{9 * 7 + 6, 1 * 16 + 2}};
FrameIndicator widget_frames{
{9 * 8, 1 * 16 + 2}};
Button button_ignore_last{ Button button_ignore_last{
{10 * 8, 1 * 16, 12 * 8, 20}, {10 * 8, 1 * 16, 12 * 8, 20},
"Ignore Last"}; "Ignore Last"};

View File

@ -449,7 +449,7 @@ DeclareTargets(PPOC pocsag)
set(MODE_CPPSRC set(MODE_CPPSRC
proc_pocsag2.cpp proc_pocsag2.cpp
) )
DeclareTargets(PPOC pocsag2) DeclareTargets(PPO2 pocsag2)
### RDS ### RDS

View File

@ -56,6 +56,12 @@ void POCSAGProcessor::execute(const buffer_c8_t& buffer) {
processDemodulatedSamples(audio.p, 16); processDemodulatedSamples(audio.p, 16);
extractFrames(); extractFrames();
samples_processed += buffer.count;
if (samples_processed >= stat_update_threshold) {
send_stats();
samples_processed = 0;
}
// Clear the output before sending to audio chip. // Clear the output before sending to audio chip.
// Only clear the audio buffer when there hasn't been any audio for a while. // Only clear the audio buffer when there hasn't been any audio for a while.
if (squelch_.enabled() && squelch_history == 0) { if (squelch_.enabled() && squelch_history == 0) {
@ -137,6 +143,11 @@ void POCSAGProcessor::configure() {
configured = true; configured = true;
} }
void POCSAGProcessor::send_stats() const {
POCSAGStatsMessage message(m_fifo.codeword, m_numCode, m_gotSync);
shared_memory.application_queue.push(message);
}
// ----------------------------- // -----------------------------
// Frame extractraction methods // Frame extractraction methods
// ----------------------------- // -----------------------------

View File

@ -137,6 +137,9 @@ class POCSAGProcessor : public BasebandProcessor {
private: private:
static constexpr size_t baseband_fs = 3072000; static constexpr size_t baseband_fs = 3072000;
static constexpr uint8_t stat_update_interval = 10;
static constexpr uint32_t stat_update_threshold =
baseband_fs / stat_update_interval;
std::array<complex16_t, 512> dst{}; std::array<complex16_t, 512> dst{};
const buffer_c16_t dst_buffer{ const buffer_c16_t dst_buffer{
@ -158,7 +161,10 @@ class POCSAGProcessor : public BasebandProcessor {
bool configured = false; bool configured = false;
pocsag::POCSAGPacket packet{}; pocsag::POCSAGPacket packet{};
uint32_t samples_processed = 0;
void configure(); void configure();
void send_stats() const;
// ---------------------------------------- // ----------------------------------------
// Frame extractraction methods and members // Frame extractraction methods and members

View File

@ -25,7 +25,6 @@
#include "proc_pocsag2.hpp" #include "proc_pocsag2.hpp"
#include "dsp_iir_config.hpp"
#include "event_m4.hpp" #include "event_m4.hpp"
#include <algorithm> #include <algorithm>
@ -33,50 +32,106 @@
#include <cstdint> #include <cstdint>
#include <cstddef> #include <cstddef>
using namespace std;
void POCSAGProcessor::execute(const buffer_c8_t& buffer) { void POCSAGProcessor::execute(const buffer_c8_t& buffer) {
if (!configured) return; if (!configured) return;
// buffer has 2048 samples
// decim0 out: 2048/8 = 256 samples
// decim1 out: 256/8 = 32 samples
// channel out: 32/2 = 16 samples
// Get 24kHz audio // Get 24kHz audio
const auto decim_0_out = decim_0.execute(buffer, dst_buffer); const auto decim_0_out = decim_0.execute(buffer, dst_buffer);
const auto decim_1_out = decim_1.execute(decim_0_out, dst_buffer); const auto decim_1_out = decim_1.execute(decim_0_out, dst_buffer);
const auto channel_out = channel_filter.execute(decim_1_out, dst_buffer); const auto channel_out = channel_filter.execute(decim_1_out, dst_buffer);
auto audio = demod.execute(channel_out, audio_buffer); auto audio = demod.execute(channel_out, audio_buffer);
// If squelching, check for audio before smoothing because smoothing // Check if there's any signal in the audio buffer.
// causes the squelch noise detection to fail. Likely because squelch bool has_audio = squelch.execute(audio);
// looks for HF noise and smoothing is basically a lowpass filter. squelch_history = (squelch_history << 1) | (has_audio ? 1 : 0);
// NB: Squelch in this processor is only for the the audio output.
// Squelching will likely drop data "noise" and break processing. // Has there been any signal?
if (squelch_.enabled()) { if (squelch_history == 0) {
bool has_audio = squelch_.execute(audio); // No signal for a while, flush and reset.
squelch_history = (squelch_history << 1) | (has_audio ? 1 : 0); if (!has_been_reset) {
OnDataFrame(m_numCode, getRate());
resetVals();
send_stats();
}
// Clear the audio stream before sending.
for (size_t i = 0; i < audio.count; ++i)
audio.p[i] = 0.0;
audio_output.write(audio);
return;
} }
smooth.Process(audio.p, audio.count); // Filter out high-frequency noise. TODO: compensate gain?
lpf.execute_in_place(audio);
normalizer.execute_in_place(audio);
audio_output.write(audio);
processDemodulatedSamples(audio.p, 16); processDemodulatedSamples(audio.p, 16);
extractFrames(); extractFrames();
// Clear the output before sending to audio chip. samples_processed += buffer.count;
if (squelch_.enabled() && squelch_history == 0) { if (samples_processed >= stat_update_threshold) {
for (size_t i = 0; i < audio.count; ++i) { send_stats();
audio.p[i] = 0.0; samples_processed = 0;
}
} }
audio_output.write(audio);
} }
// ==================================================================== void POCSAGProcessor::on_message(const Message* const message) {
// switch (message->id) {
// ==================================================================== case Message::ID::POCSAGConfigure:
configure();
break;
case Message::ID::NBFMConfigure: {
auto config = reinterpret_cast<const NBFMConfigureMessage*>(message);
squelch.set_threshold(config->squelch_level / 99.0);
break;
}
default:
break;
}
}
void POCSAGProcessor::configure() {
constexpr size_t decim_0_output_fs = baseband_fs / decim_0.decimation_factor;
constexpr size_t decim_1_output_fs = decim_0_output_fs / decim_1.decimation_factor;
const size_t channel_filter_output_fs = decim_1_output_fs / 2;
const size_t demod_input_fs = channel_filter_output_fs;
decim_0.configure(taps_11k0_decim_0.taps);
decim_1.configure(taps_11k0_decim_1.taps);
channel_filter.configure(taps_11k0_channel.taps, 2);
demod.configure(demod_input_fs, 4'500); // FSK +/- 4k5Hz.
// Don't process the audio stream.
audio_output.configure(false);
// Set up the frame extraction, limits of baud.
setFrameExtractParams(demod_input_fs, 4000, 300, 32);
// Set ready to process data.
configured = true;
}
void POCSAGProcessor::send_stats() const {
POCSAGStatsMessage message(m_fifo.codeword, m_numCode, m_gotSync);
shared_memory.application_queue.push(message);
}
int POCSAGProcessor::OnDataWord(uint32_t word, int pos) { int POCSAGProcessor::OnDataWord(uint32_t word, int pos) {
packet.set(pos, word); packet.set(pos, word);
return 0; return 0;
} }
// ====================================================================
//
// ====================================================================
int POCSAGProcessor::OnDataFrame(int len, int baud) { int POCSAGProcessor::OnDataFrame(int len, int baud) {
if (len > 0) { if (len > 0) {
packet.set_bitrate(baud); packet.set_bitrate(baud);
@ -88,57 +143,6 @@ int POCSAGProcessor::OnDataFrame(int len, int baud) {
return 0; return 0;
} }
void POCSAGProcessor::on_message(const Message* const message) {
switch (message->id) {
case Message::ID::POCSAGConfigure:
configure();
break;
case Message::ID::NBFMConfigure: {
auto config = reinterpret_cast<const NBFMConfigureMessage*>(message);
squelch_.set_threshold(config->squelch_level / 100.0);
break;
}
default:
break;
}
}
void POCSAGProcessor::configure() {
constexpr size_t decim_0_input_fs = baseband_fs;
constexpr size_t decim_0_output_fs = decim_0_input_fs / decim_0.decimation_factor;
constexpr size_t decim_1_input_fs = decim_0_output_fs;
constexpr size_t decim_1_output_fs = decim_1_input_fs / decim_1.decimation_factor;
constexpr size_t channel_filter_input_fs = decim_1_output_fs;
const size_t channel_filter_output_fs = channel_filter_input_fs / 2;
const size_t demod_input_fs = channel_filter_output_fs;
decim_0.configure(taps_11k0_decim_0.taps);
decim_1.configure(taps_11k0_decim_1.taps);
channel_filter.configure(taps_11k0_channel.taps, 2);
demod.configure(demod_input_fs, 4'500); // FSK +/- 4k5Hz.
// Smoothing should be roughly sample rate over max baud
// 24k / 3.2k = 7.5
smooth.SetSize(8);
// Don't have audio process the stream.
audio_output.configure(false);
// Set up the frame extraction, limits of baud
setFrameExtractParams(demod_input_fs, 4000, 300, 32);
// Mark the class as ready to accept data
configured = true;
}
// -----------------------------
// Frame extractraction methods
// -----------------------------
#define BAUD_STABLE (104) #define BAUD_STABLE (104)
#define MAX_CONSEC_SAME (32) #define MAX_CONSEC_SAME (32)
#define MAX_WITHOUT_SINGLE (64) #define MAX_WITHOUT_SINGLE (64)
@ -149,9 +153,6 @@ void POCSAGProcessor::configure() {
#define M_IDLE (0x7a89c197) #define M_IDLE (0x7a89c197)
// ====================================================================
//
// ====================================================================
inline int bitsDiff(unsigned long left, unsigned long right) { inline int bitsDiff(unsigned long left, unsigned long right) {
unsigned long xord = left ^ right; unsigned long xord = left ^ right;
int count = 0; int count = 0;
@ -162,9 +163,6 @@ inline int bitsDiff(unsigned long left, unsigned long right) {
return (count); return (count);
} }
// ====================================================================
//
// ====================================================================
void POCSAGProcessor::initFrameExtraction() { void POCSAGProcessor::initFrameExtraction() {
m_averageSymbolLen_1024 = m_maxSymSamples_1024; m_averageSymbolLen_1024 = m_maxSymSamples_1024;
m_lastStableSymbolLen_1024 = m_minSymSamples_1024; m_lastStableSymbolLen_1024 = m_minSymSamples_1024;
@ -177,12 +175,10 @@ void POCSAGProcessor::initFrameExtraction() {
resetVals(); resetVals();
} }
// ====================================================================
//
// ====================================================================
void POCSAGProcessor::resetVals() { void POCSAGProcessor::resetVals() {
if (has_been_reset) return;
// Reset the parameters // Reset the parameters
// --------------------
m_goodTransitions = 0; m_goodTransitions = 0;
m_badTransitions = 0; m_badTransitions = 0;
m_averageSymbolLen_1024 = m_maxSymSamples_1024; m_averageSymbolLen_1024 = m_maxSymSamples_1024;
@ -190,7 +186,6 @@ void POCSAGProcessor::resetVals() {
m_valMid = 0; m_valMid = 0;
// And reset the counts // And reset the counts
// --------------------
m_lastTransPos_1024 = 0; m_lastTransPos_1024 = 0;
m_lastBitPos_1024 = 0; m_lastBitPos_1024 = 0;
m_lastSample = 0; m_lastSample = 0;
@ -200,13 +195,14 @@ void POCSAGProcessor::resetVals() {
// Extraction // Extraction
m_fifo.numBits = 0; m_fifo.numBits = 0;
m_fifo.codeword = 0;
m_gotSync = false; m_gotSync = false;
m_numCode = 0; m_numCode = 0;
has_been_reset = true;
samples_processed = 0;
} }
// ====================================================================
//
// ====================================================================
void POCSAGProcessor::setFrameExtractParams(long a_samplesPerSec, long a_maxBaud, long a_minBaud, long maxRunOfSameValue) { void POCSAGProcessor::setFrameExtractParams(long a_samplesPerSec, long a_maxBaud, long a_minBaud, long maxRunOfSameValue) {
m_samplesPerSec = a_samplesPerSec; m_samplesPerSec = a_samplesPerSec;
m_minSymSamples_1024 = (uint32_t)(1024.0f * (float)a_samplesPerSec / (float)a_maxBaud); m_minSymSamples_1024 = (uint32_t)(1024.0f * (float)a_samplesPerSec / (float)a_maxBaud);
@ -223,14 +219,13 @@ void POCSAGProcessor::setFrameExtractParams(long a_samplesPerSec, long a_maxBaud
initFrameExtraction(); initFrameExtraction();
} }
// ====================================================================
//
// ====================================================================
int POCSAGProcessor::processDemodulatedSamples(float* sampleBuff, int noOfSamples) { int POCSAGProcessor::processDemodulatedSamples(float* sampleBuff, int noOfSamples) {
bool transition = false; bool transition = false;
uint32_t samplePos_1024 = 0; uint32_t samplePos_1024 = 0;
uint32_t len_1024 = 0; uint32_t len_1024 = 0;
has_been_reset = false;
// Loop through the block of data // Loop through the block of data
// ------------------------------ // ------------------------------
for (int pos = 0; pos < noOfSamples; ++pos) { for (int pos = 0; pos < noOfSamples; ++pos) {
@ -399,9 +394,6 @@ int POCSAGProcessor::processDemodulatedSamples(float* sampleBuff, int noOfSample
return getNoOfBits(); return getNoOfBits();
} }
// ====================================================================
//
// ====================================================================
void POCSAGProcessor::storeBit() { void POCSAGProcessor::storeBit() {
if (++m_bitsStart >= BIT_BUF_SIZE) { if (++m_bitsStart >= BIT_BUF_SIZE) {
m_bitsStart = 0; m_bitsStart = 0;
@ -429,9 +421,6 @@ void POCSAGProcessor::storeBit() {
} }
} }
// ====================================================================
//
// ====================================================================
int POCSAGProcessor::extractFrames() { int POCSAGProcessor::extractFrames() {
int msgCnt = 0; int msgCnt = 0;
// While there is unread data in the bits buffer // While there is unread data in the bits buffer
@ -471,7 +460,7 @@ int POCSAGProcessor::extractFrames() {
// If at the end of a 16 word block // If at the end of a 16 word block
// -------------------------------- // --------------------------------
if (m_numCode >= 15) { if (m_numCode >= 15) {
msgCnt += OnDataFrame(m_numCode + 1, (m_samplesPerSec << 10) / m_lastStableSymbolLen_1024); msgCnt += OnDataFrame(m_numCode + 1, getRate());
m_gotSync = false; m_gotSync = false;
m_numCode = -1; m_numCode = -1;
} }
@ -482,9 +471,6 @@ int POCSAGProcessor::extractFrames() {
return msgCnt; return msgCnt;
} // extractFrames } // extractFrames
// ====================================================================
//
// ====================================================================
short POCSAGProcessor::getBit() { short POCSAGProcessor::getBit() {
if (m_bitsEnd != m_bitsStart) { if (m_bitsEnd != m_bitsStart) {
if (++m_bitsEnd >= BIT_BUF_SIZE) { if (++m_bitsEnd >= BIT_BUF_SIZE) {
@ -496,9 +482,6 @@ short POCSAGProcessor::getBit() {
} }
} }
// ====================================================================
//
// ====================================================================
int POCSAGProcessor::getNoOfBits() { int POCSAGProcessor::getNoOfBits() {
int bits = m_bitsEnd - m_bitsStart; int bits = m_bitsEnd - m_bitsStart;
if (bits < 0) { if (bits < 0) {
@ -507,16 +490,10 @@ int POCSAGProcessor::getNoOfBits() {
return bits; return bits;
} }
// ====================================================================
//
// ====================================================================
uint32_t POCSAGProcessor::getRate() { uint32_t POCSAGProcessor::getRate() {
return ((m_samplesPerSec << 10) + 512) / m_lastStableSymbolLen_1024; return ((m_samplesPerSec << 10) + 512) / m_lastStableSymbolLen_1024;
} }
// ====================================================================
//
// ====================================================================
int main() { int main() {
EventDispatcher event_dispatcher{std::make_unique<POCSAGProcessor>()}; EventDispatcher event_dispatcher{std::make_unique<POCSAGProcessor>()};
event_dispatcher.run(); event_dispatcher.run();

View File

@ -26,107 +26,87 @@
#ifndef __PROC_POCSAG2_H__ #ifndef __PROC_POCSAG2_H__
#define __PROC_POCSAG2_H__ #define __PROC_POCSAG2_H__
#include "audio_output.hpp"
#include "baseband_processor.hpp" #include "baseband_processor.hpp"
#include "baseband_thread.hpp" #include "baseband_thread.hpp"
#include "rssi_thread.hpp"
#include "dsp_decimate.hpp" #include "dsp_decimate.hpp"
#include "dsp_demodulate.hpp" #include "dsp_demodulate.hpp"
#include "dsp_iir_config.hpp"
#include "pocsag_packet.hpp"
#include "pocsag.hpp"
#include "message.hpp" #include "message.hpp"
#include "audio_output.hpp" #include "pocsag.hpp"
#include "pocsag_packet.hpp"
#include "portapack_shared_memory.hpp" #include "portapack_shared_memory.hpp"
#include "rssi_thread.hpp"
#include <cstdint> #include <cstdint>
#include <bitset>
using namespace std;
// Class used to smooth demodulated waveform prior to decoding
// -----------------------------------------------------------
template <class ValType, class CalcType>
class SmoothVals {
protected:
ValType* m_lastVals; // Previous N values
int m_size; // The size N
CalcType m_sumVal; // Running sum of lastVals
int m_pos; // Current position in last vals ring buffer
int m_count; //
/* Takes audio stream and automatically normalizes it to +/-1.0f */
class AudioNormalizer {
public: public:
SmoothVals() void execute_in_place(const buffer_f32_t& audio) {
: m_lastVals(NULL), m_size(1), m_sumVal(0), m_pos(0), m_count(0) { // Decay min/max every second (@24kHz).
m_lastVals = new ValType[m_size]; if (counter_ >= 24'000) {
} // 90% decay factor seems to work well.
// This keeps large transients from wrecking the filter.
// -------------------------------------------------- max_ *= 0.9f;
// -------------------------------------------------- min_ *= 0.9f;
virtual ~SmoothVals() { counter_ = 0;
delete[] m_lastVals; calculate_thresholds();
}
SmoothVals(const SmoothVals<float, float>&) {
}
SmoothVals& operator=(const SmoothVals<float, float>&) {
return *this;
}
// --------------------------------------------------
// Set size of smoothing
// --------------------------------------------------
void SetSize(int size) {
m_size = std::max(size, 1);
m_pos = 0;
delete[] m_lastVals;
m_lastVals = new ValType[m_size];
m_sumVal = 0;
}
// --------------------------------------------------
// Get size of smoothing
// --------------------------------------------------
int Size() { return m_size; }
// --------------------------------------------------
// In place processing
// --------------------------------------------------
void Process(ValType* valBuff, int numVals) {
ValType tmpVal;
if (m_count > (1024 * 10)) {
// Recalculate the sum value occasionaly, stops accumulated errors when using float
m_count = 0;
m_sumVal = 0;
for (int i = 0; i < m_size; ++i) {
m_sumVal += (CalcType)m_lastVals[i];
}
} }
// Use a rolling smoothed value while processing the buffer counter_ += audio.count;
for (int buffPos = 0; buffPos < numVals; ++buffPos) {
m_pos++; for (size_t i = 0; i < audio.count; ++i) {
if (m_pos >= m_size) { auto& val = audio.p[i];
m_pos = 0;
if (val > max_) {
max_ = val;
calculate_thresholds();
}
if (val < min_) {
min_ = val;
calculate_thresholds();
} }
m_sumVal -= (CalcType)m_lastVals[m_pos]; // Subtract the oldest value if (val >= t_hi_)
m_lastVals[m_pos] = valBuff[buffPos]; // Store the new value val = 1.0f;
m_sumVal += (CalcType)m_lastVals[m_pos]; // Add on the new value else if (val <= t_lo_)
val = -1.0f;
tmpVal = (ValType)(m_sumVal / m_size); // Scale by number of values smoothed else
valBuff[buffPos] = tmpVal; val = 0.0;
} }
m_count += numVals;
} }
private:
void calculate_thresholds() {
auto center = (max_ + min_) / 2.0f;
auto range = (max_ - min_) / 2.0f;
// 10% off center force either +/-1.0f.
// Higher == larger dead zone.
// Lower == more false positives.
auto threshold = range * 0.1;
t_hi_ = center + threshold;
t_lo_ = center - threshold;
}
uint32_t counter_ = 0;
float min_ = 99.0f;
float max_ = -99.0f;
float t_hi_ = 1.0;
float t_lo_ = 1.0;
}; };
// -------------------------------------------------- // How to detect clock signal across baud rates?
// Class to process base band data to pocsag frames // Maybe have a bit extraction state machine that reset
// -------------------------------------------------- // then watches for the clocks, but there are multiple
// clock and the last one is the right one.
// So keep updating clock until a sync?
class BitExtractor {};
class WordExtractor {};
class POCSAGProcessor : public BasebandProcessor { class POCSAGProcessor : public BasebandProcessor {
public: public:
void execute(const buffer_c8_t& buffer) override; void execute(const buffer_c8_t& buffer) override;
@ -137,28 +117,54 @@ class POCSAGProcessor : public BasebandProcessor {
private: private:
static constexpr size_t baseband_fs = 3072000; static constexpr size_t baseband_fs = 3072000;
static constexpr uint8_t stat_update_interval = 10;
std::array<complex16_t, 512> dst{}; static constexpr uint32_t stat_update_threshold =
const buffer_c16_t dst_buffer{ baseband_fs / stat_update_interval;
dst.data(),
dst.size()};
std::array<float, 32> audio{};
const buffer_f32_t audio_buffer{
audio.data(),
audio.size()};
dsp::decimate::FIRC8xR16x24FS4Decim8 decim_0{};
dsp::decimate::FIRC16xR16x32Decim8 decim_1{};
dsp::decimate::FIRAndDecimateComplex channel_filter{};
dsp::demodulate::FM demod{};
SmoothVals<float, float> smooth = {};
AudioOutput audio_output{};
bool configured = false;
pocsag::POCSAGPacket packet{};
void configure(); void configure();
void send_stats() const;
// Set once app is ready to receive messages.
bool configured = false;
// Buffer for decimated IQ data.
std::array<complex16_t, 512> dst{};
const buffer_c16_t dst_buffer{dst.data(), dst.size()};
// Buffer for demodulated audio.
std::array<float, 32> audio{};
const buffer_f32_t audio_buffer{audio.data(), audio.size()};
// Decimate to 48kHz.
dsp::decimate::FIRC8xR16x24FS4Decim8 decim_0{};
dsp::decimate::FIRC16xR16x32Decim8 decim_1{};
// Filter to 24kHz and demodulate.
dsp::decimate::FIRAndDecimateComplex channel_filter{};
dsp::demodulate::FM demod{};
// LPF to reduce noise.
// scipy.signal.butter(2, 1800, "lowpass", fs=24000, analog=False)
IIRBiquadFilter lpf{{{0.04125354f, 0.082507070f, 0.04125354f},
{1.00000000f, -1.34896775f, 0.51398189f}}};
// Squelch to ignore noise.
FMSquelch squelch{};
uint64_t squelch_history = 0;
// Attempts to de-noise signal and normalize to +/- 1.0f.
AudioNormalizer normalizer{};
// Handles writing audio stream to hardware.
AudioOutput audio_output{};
// Holds the data sent to the app.
pocsag::POCSAGPacket packet{};
bool has_been_reset = true;
uint32_t samples_processed = 0;
//--------------------------------------------------
// ---------------------------------------- // ----------------------------------------
// Frame extractraction methods and members // Frame extractraction methods and members
@ -169,8 +175,6 @@ class POCSAGProcessor : public BasebandProcessor {
int numBits; int numBits;
}; };
#define BIT_BUF_SIZE (64)
void resetVals(); void resetVals();
void setFrameExtractParams(long a_samplesPerSec, long a_maxBaud = 8000, long a_minBaud = 200, long maxRunOfSameValue = 32); void setFrameExtractParams(long a_samplesPerSec, long a_maxBaud = 8000, long a_minBaud = 200, long maxRunOfSameValue = 32);
@ -207,7 +211,8 @@ class POCSAGProcessor : public BasebandProcessor {
uint32_t m_maxSymSamples_1024{0}; uint32_t m_maxSymSamples_1024{0};
uint32_t m_maxRunOfSameValue{0}; uint32_t m_maxRunOfSameValue{0};
bitset<(size_t)BIT_BUF_SIZE> m_bits{0}; static constexpr long BIT_BUF_SIZE = 64;
std::bitset<64> m_bits{0};
long m_bitsStart{0}; long m_bitsStart{0};
long m_bitsEnd{0}; long m_bitsEnd{0};
@ -216,8 +221,7 @@ class POCSAGProcessor : public BasebandProcessor {
int m_numCode{0}; int m_numCode{0};
bool m_inverted{false}; bool m_inverted{false};
FMSquelch squelch_{}; //--------------------------------------------------
uint64_t squelch_history = 0;
/* NB: Threads should be the last members in the class definition. */ /* NB: Threads should be the last members in the class definition. */
BasebandThread baseband_thread{baseband_fs, this, baseband::Direction::Receive}; BasebandThread baseband_thread{baseband_fs, this, baseband::Direction::Receive};

View File

@ -77,12 +77,6 @@ constexpr iir_biquad_config_t audio_24k_deemph_300_6_config{
{0.03780475f, 0.03780475f, 0.00000000f}, {0.03780475f, 0.03780475f, 0.00000000f},
{1.00000000f, -0.92439049f, 0.00000000f}}; {1.00000000f, -0.92439049f, 0.00000000f}};
// scipy.signal.butter(1, 2400 / 12000.0, 'lowpass', analog=False)
// NOTE: Technically, order-1 filter, b[2] = a[2] = 0.
constexpr iir_biquad_config_t audio_24k_lpf_2400hz_config{
{0.03780475f, 0.03780475f, 0.00000000f},
{1.00000000f, -0.92439049f, 0.00000000f}};
// scipy.signal.butter(1, 300 / 8000.0, 'lowpass', analog=False) // scipy.signal.butter(1, 300 / 8000.0, 'lowpass', analog=False)
// NOTE: Technically, order-1 filter, b[2] = a[2] = 0. // NOTE: Technically, order-1 filter, b[2] = a[2] = 0.
constexpr iir_biquad_config_t audio_16k_deemph_300_6_config{ constexpr iir_biquad_config_t audio_16k_deemph_300_6_config{

View File

@ -344,12 +344,18 @@ class POCSAGPacketMessage : public Message {
class POCSAGStatsMessage : public Message { class POCSAGStatsMessage : public Message {
public: public:
constexpr POCSAGStatsMessage( constexpr POCSAGStatsMessage(
uint16_t baud_rate) uint32_t current_bits,
uint8_t current_frames,
bool has_sync)
: Message{ID::POCSAGStats}, : Message{ID::POCSAGStats},
baud_rate_{baud_rate} { current_bits{current_bits},
current_frames{current_frames},
has_sync{has_sync} {
} }
uint16_t baud_rate_; uint32_t current_bits = 0;
uint8_t current_frames = 0;
bool has_sync = false;
}; };
class ACARSPacketMessage : public Message { class ACARSPacketMessage : public Message {