mirror of
https://github.com/portapack-mayhem/mayhem-firmware.git
synced 2026-09-12 09:39:30 +00:00
Epirb afc wide capture (#3213)
* EPIRB RX: widen AFC capture range to +/-5 kHz The AFC estimate was only applied after carrier lock, so carrier acquisition ran on raw phase deltas and could only tolerate offsets of a few hundred Hz before the 0.6/0.7/1.6 rad detection thresholds (on the 12-sample accumulator) broke down. Track the carrier offset continuously in the IDLE state with a first-order loop (AFC_TRACK_ALPHA), so the de-biased accumulator self-centers for any offset within the discriminator Nyquist (~24 kHz) *before* the thresholds run. ALPHA = 0.005 pulls a +/-5 kHz offset under the 0.6 rad lock threshold in ~11 ms, well inside the 160 ms preamble / 80 ms stability window. The IDLE rise-detect threshold is also made symmetric (fabsf) now that the bias is removed. Also add the missing <cstdint> include to test_convert.cpp so the application_test suite compiles under the current toolchain. Verified: baseband_epirb_rx.elf builds (flash 53%, RAM 12%) and baseband_test passes.
This commit is contained in:
+12
-15
@@ -41,29 +41,26 @@ void BeaconUIList::paint(Painter& painter) {
|
|||||||
auto base_style = Theme::getInstance()->bg_darkest;
|
auto base_style = Theme::getInstance()->bg_darkest;
|
||||||
|
|
||||||
for (auto offset = 0u; offset < BEACON_HISTORY_SIZE; ++offset) {
|
for (auto offset = 0u; offset < BEACON_HISTORY_SIZE; ++offset) {
|
||||||
// The whole frame needs to be cleared so every line 'slot'
|
|
||||||
// is redrawn even when `text` just left empty.
|
|
||||||
auto text = std::string{};
|
|
||||||
auto index = start_index_ + offset;
|
auto index = start_index_ + offset;
|
||||||
auto line_position = rect.location() + Point{0, 1 + (int)offset * char_height};
|
|
||||||
auto is_selected = offset == selected_index_;
|
|
||||||
auto style = base_style;
|
|
||||||
|
|
||||||
if (index < db_->size()) {
|
if (index < db_->size()) {
|
||||||
|
auto line_position = rect.location() + Point{0, 1 + (int)offset * char_height};
|
||||||
|
auto is_selected = (offset == selected_index_);
|
||||||
|
auto style = base_style;
|
||||||
|
|
||||||
// Get beacon entry and format it's summary
|
// Get beacon entry and format it's summary
|
||||||
auto& entry = db_->get_beacon(index);
|
auto& entry = db_->get_beacon(index);
|
||||||
char buffer[64];
|
char buffer[64];
|
||||||
entry.formatSummary(buffer, true);
|
entry.formatSummary(buffer, true);
|
||||||
text = std::string(buffer);
|
|
||||||
|
if (index == db_->get_current_beacon_index())
|
||||||
|
// If this is the currently displayed beacon change color
|
||||||
|
style = Theme::getInstance()->bg_medium;
|
||||||
|
|
||||||
|
// Draw entry line using stack buffer directly to avoid heap allocation
|
||||||
|
painter.draw_string(
|
||||||
|
line_position, (is_selected ? style->invert() : *style), buffer);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (index == db_->get_current_beacon_index())
|
|
||||||
// If this is the currently displayed beacon change color
|
|
||||||
style = Theme::getInstance()->bg_medium;
|
|
||||||
|
|
||||||
// Draw entry line
|
|
||||||
painter.draw_string(
|
|
||||||
line_position, (is_selected ? style->invert() : *style), text);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Draw a bounding rectangle when focused.
|
// Draw a bounding rectangle when focused.
|
||||||
|
|||||||
+616
-600
File diff suppressed because it is too large
Load Diff
+323
-323
@@ -1,324 +1,324 @@
|
|||||||
/*
|
/*
|
||||||
* Copyright (C) 2024 EPIRB Decoder Implementation
|
* Copyright (C) 2024 EPIRB Decoder Implementation
|
||||||
* Copyright (C) 2026 Frederic BORRY - ADRASEC 31
|
* Copyright (C) 2026 Frederic BORRY - ADRASEC 31
|
||||||
*
|
*
|
||||||
* This file is part of PortaPack.
|
* This file is part of PortaPack.
|
||||||
*
|
*
|
||||||
* This program is free software; you can redistribute it and/or modify
|
* 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
|
* it under the terms of the GNU General Public License as published by
|
||||||
* the Free Software Foundation; either version 2, or (at your option)
|
* the Free Software Foundation; either version 2, or (at your option)
|
||||||
* any later version.
|
* any later version.
|
||||||
*
|
*
|
||||||
* This program is distributed in the hope that it will be useful,
|
* This program is distributed in the hope that it will be useful,
|
||||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
* GNU General Public License for more details.
|
* GNU General Public License for more details.
|
||||||
*
|
*
|
||||||
* You should have received a copy of the GNU General Public License
|
* You should have received a copy of the GNU General Public License
|
||||||
* along with this program; see the file COPYING. If not, write to
|
* along with this program; see the file COPYING. If not, write to
|
||||||
* the Free Software Foundation, Inc., 51 Franklin Street,
|
* the Free Software Foundation, Inc., 51 Franklin Street,
|
||||||
* Boston, MA 02110-1301, USA.
|
* Boston, MA 02110-1301, USA.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
#ifndef __UI_EPIRB_RX_H__
|
#ifndef __UI_EPIRB_RX_H__
|
||||||
#define __UI_EPIRB_RX_H__
|
#define __UI_EPIRB_RX_H__
|
||||||
|
|
||||||
#include "app_settings.hpp"
|
#include "app_settings.hpp"
|
||||||
#include "radio_state.hpp"
|
#include "radio_state.hpp"
|
||||||
#include "ui_widget.hpp"
|
#include "ui_widget.hpp"
|
||||||
#include "ui_navigation.hpp"
|
#include "ui_navigation.hpp"
|
||||||
#include "ui_receiver.hpp"
|
#include "ui_receiver.hpp"
|
||||||
#include "ui_geomap.hpp"
|
#include "ui_geomap.hpp"
|
||||||
|
|
||||||
// Specan is disable to keep application size below the 32k limit
|
// Specan is disable to keep application size below the 32k limit
|
||||||
// #define SPECAN
|
// #define SPECAN
|
||||||
|
|
||||||
// Comment to disable timout reset on select and save approx 200 bytes of flash
|
// Comment to disable timout reset on select and save approx 200 bytes of flash
|
||||||
#ifndef PRALINE
|
#ifndef PRALINE
|
||||||
// Application does not fit on Praline with RESET_TIMER enabled
|
// Application does not fit on Praline with RESET_TIMER enabled
|
||||||
#define RESET_TIMER
|
#define RESET_TIMER
|
||||||
#endif
|
#endif
|
||||||
// Comment to disable squelch control
|
// Comment to disable squelch control
|
||||||
#define SQUELCH
|
#define SQUELCH
|
||||||
// Comment to disable beacon selection by encoder on detail tab
|
// Comment to disable beacon selection by encoder on detail tab
|
||||||
#define DETAIL_TAB_BEACON_SEL
|
#define DETAIL_TAB_BEACON_SEL
|
||||||
// #define LOGGER
|
// #define LOGGER
|
||||||
|
|
||||||
#ifdef SPECAN
|
#ifdef SPECAN
|
||||||
#include "ui_spectrum.hpp"
|
#include "ui_spectrum.hpp"
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
#include "ui_tabview.hpp"
|
#include "ui_tabview.hpp"
|
||||||
|
|
||||||
#include "ui_qrcode.hpp"
|
#include "ui_qrcode.hpp"
|
||||||
|
|
||||||
#include "event_m0.hpp"
|
#include "event_m0.hpp"
|
||||||
#include "message.hpp"
|
#include "message.hpp"
|
||||||
#include "log_file.hpp"
|
#include "log_file.hpp"
|
||||||
|
|
||||||
#include "baseband_packet.hpp"
|
#include "baseband_packet.hpp"
|
||||||
|
|
||||||
#include "audio.hpp"
|
#include "audio.hpp"
|
||||||
|
|
||||||
#include "beacon.hpp"
|
#include "beacon.hpp"
|
||||||
#include "beacon_db.hpp"
|
#include "beacon_db.hpp"
|
||||||
#include "ui_beaconlist.hpp"
|
#include "ui_beaconlist.hpp"
|
||||||
#include "resources.hpp"
|
#include "resources.hpp"
|
||||||
|
|
||||||
namespace ui::external_app::epirb_rx {
|
namespace ui::external_app::epirb_rx {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Status of a packet
|
* Status of a packet
|
||||||
*/
|
*/
|
||||||
enum class PacketStatus : uint8_t {
|
enum class PacketStatus : uint8_t {
|
||||||
Valid = 0,
|
Valid = 0,
|
||||||
Corrected = 1,
|
Corrected = 1,
|
||||||
Error = 2
|
Error = 2
|
||||||
};
|
};
|
||||||
|
|
||||||
// Position of tabs in tab view
|
// Position of tabs in tab view
|
||||||
#define EPIRB_TAB_POS_Y (UI_POS_Y(4) + 3 * 8)
|
#define EPIRB_TAB_POS_Y (UI_POS_Y(4) + 3 * 8)
|
||||||
// Height of tabs in tab view
|
// Height of tabs in tab view
|
||||||
#define EPIRB_TAB_HEIGHT (screen_height - EPIRB_TAB_POS_Y - UI_POS_HEIGHT(1))
|
#define EPIRB_TAB_HEIGHT (screen_height - EPIRB_TAB_POS_Y - UI_POS_HEIGHT(1))
|
||||||
|
|
||||||
#ifdef LOGGER
|
#ifdef LOGGER
|
||||||
class EPIRBLogger {
|
class EPIRBLogger {
|
||||||
public:
|
public:
|
||||||
Optional<File::Error> append(const std::filesystem::path& filename) {
|
Optional<File::Error> append(const std::filesystem::path& filename) {
|
||||||
return log_file.append(filename);
|
return log_file.append(filename);
|
||||||
}
|
}
|
||||||
|
|
||||||
void on_packet(Beacon& beacon);
|
void on_packet(Beacon& beacon);
|
||||||
|
|
||||||
private:
|
private:
|
||||||
LogFile log_file{};
|
LogFile log_file{};
|
||||||
};
|
};
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Dedicated TextArea component used to optimize application code size
|
* Dedicated TextArea component used to optimize application code size
|
||||||
*/
|
*/
|
||||||
class TextArea : public Widget {
|
class TextArea : public Widget {
|
||||||
public:
|
public:
|
||||||
TextArea(Rect parent_rect);
|
TextArea(Rect parent_rect);
|
||||||
|
|
||||||
#ifdef RESET_TIMER
|
#ifdef RESET_TIMER
|
||||||
std::function<void(TextArea&)> on_select{};
|
std::function<void(TextArea&)> on_select{};
|
||||||
bool on_key(const KeyEvent key) override;
|
bool on_key(const KeyEvent key) override;
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
void set_content(std::string_view value);
|
void set_content(std::string_view value);
|
||||||
void paint(Painter& painter) override;
|
void paint(Painter& painter) override;
|
||||||
|
|
||||||
private:
|
private:
|
||||||
std::string content{};
|
std::string content{};
|
||||||
};
|
};
|
||||||
|
|
||||||
// Forward declaration
|
// Forward declaration
|
||||||
class EPIRBAppView;
|
class EPIRBAppView;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* View for beacon detail tab
|
* View for beacon detail tab
|
||||||
*/
|
*/
|
||||||
class EPIRBDetailView : public View {
|
class EPIRBDetailView : public View {
|
||||||
public:
|
public:
|
||||||
EPIRBDetailView(Rect parent_rect, EPIRBAppView& parent);
|
EPIRBDetailView(Rect parent_rect, EPIRBAppView& parent);
|
||||||
void set_beacon(Beacon& beacon);
|
void set_beacon(Beacon& beacon);
|
||||||
#ifdef DETAIL_TAB_BEACON_SEL
|
#ifdef DETAIL_TAB_BEACON_SEL
|
||||||
bool on_encoder(EncoderEvent delta) override;
|
bool on_encoder(EncoderEvent delta) override;
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
private:
|
private:
|
||||||
TextArea text_beacon{{UI_POS_X(0), UI_POS_Y(0), UI_POS_MAXWIDTH, EPIRB_TAB_HEIGHT}};
|
TextArea text_beacon{{UI_POS_X(0), UI_POS_Y(0), UI_POS_MAXWIDTH, EPIRB_TAB_HEIGHT}};
|
||||||
EPIRBAppView& parent_app;
|
EPIRBAppView& parent_app;
|
||||||
};
|
};
|
||||||
|
|
||||||
#define EPIRB_RX_DEFAULT_LATITUDE 43.604f
|
#define EPIRB_RX_DEFAULT_LATITUDE 43.604f
|
||||||
#define EPIRB_RX_DEFAULT_LONGITUDE 1.458f
|
#define EPIRB_RX_DEFAULT_LONGITUDE 1.458f
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* View for beacon map tab
|
* View for beacon map tab
|
||||||
*/
|
*/
|
||||||
class EPIRBMapView : public View {
|
class EPIRBMapView : public View {
|
||||||
public:
|
public:
|
||||||
EPIRBMapView(Rect parent_rect);
|
EPIRBMapView(Rect parent_rect);
|
||||||
void paint(Painter& painter) override;
|
void paint(Painter& painter) override;
|
||||||
void on_show() override;
|
void on_show() override;
|
||||||
void set_main_marker(const std::string& label, float lat, float lon);
|
void set_main_marker(const std::string& label, float lat, float lon);
|
||||||
void clear_markers();
|
void clear_markers();
|
||||||
void add_marker(GeoMarker& marker);
|
void add_marker(GeoMarker& marker);
|
||||||
void hide_map(bool hide);
|
void hide_map(bool hide);
|
||||||
void repaint();
|
void repaint();
|
||||||
|
|
||||||
private:
|
private:
|
||||||
GeoMap geomap{{0, 0, UI_POS_MAXWIDTH, EPIRB_TAB_HEIGHT}};
|
GeoMap geomap{{0, 0, UI_POS_MAXWIDTH, EPIRB_TAB_HEIGHT}};
|
||||||
float lat_{EPIRB_RX_DEFAULT_LATITUDE};
|
float lat_{EPIRB_RX_DEFAULT_LATITUDE};
|
||||||
float lon_{EPIRB_RX_DEFAULT_LONGITUDE};
|
float lon_{EPIRB_RX_DEFAULT_LONGITUDE};
|
||||||
bool map_hidden{true};
|
bool map_hidden{true};
|
||||||
};
|
};
|
||||||
|
|
||||||
#define QR_WIDTH 126
|
#define QR_WIDTH 126
|
||||||
#define QR_HEIGHT 127
|
#define QR_HEIGHT 127
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Vieaw for Beacon QR tab
|
* Vieaw for Beacon QR tab
|
||||||
*/
|
*/
|
||||||
class EPRIBQRView : public View {
|
class EPRIBQRView : public View {
|
||||||
public:
|
public:
|
||||||
EPRIBQRView(Rect parent_rect);
|
EPRIBQRView(Rect parent_rect);
|
||||||
EPRIBQRView(const EPRIBQRView&) = delete;
|
EPRIBQRView(const EPRIBQRView&) = delete;
|
||||||
EPRIBQRView& operator=(const EPRIBQRView&) = delete;
|
EPRIBQRView& operator=(const EPRIBQRView&) = delete;
|
||||||
|
|
||||||
void set_beacon(Beacon* beacon);
|
void set_beacon(Beacon* beacon);
|
||||||
void update_qr();
|
void update_qr();
|
||||||
void update_display();
|
void update_display();
|
||||||
|
|
||||||
private:
|
private:
|
||||||
bool show_map{true};
|
bool show_map{true};
|
||||||
Beacon* current_beacon{nullptr};
|
Beacon* current_beacon{nullptr};
|
||||||
char qr_url[128];
|
char qr_url[128];
|
||||||
|
|
||||||
OptionsField options_qr{
|
OptionsField options_qr{
|
||||||
{UI_POS_X(5), UI_POS_Y(1)},
|
{UI_POS_X(5), UI_POS_Y(1)},
|
||||||
6,
|
6,
|
||||||
{{"Map", 0},
|
{{"Map", 0},
|
||||||
{"Detail", 1}}};
|
{"Detail", 1}}};
|
||||||
|
|
||||||
QRCodeImage qr_code{
|
QRCodeImage qr_code{
|
||||||
{UI_POS_MAXWIDTH - QR_WIDTH - UI_POS_X(1), UI_POS_Y(1), QR_WIDTH, QR_HEIGHT}};
|
{UI_POS_MAXWIDTH - QR_WIDTH - UI_POS_X(1), UI_POS_Y(1), QR_WIDTH, QR_HEIGHT}};
|
||||||
|
|
||||||
TextArea text_data{{UI_POS_X(0), UI_POS_Y(1), UI_POS_MAXWIDTH, EPIRB_TAB_HEIGHT - UI_POS_Y(1)}};
|
TextArea text_data{{UI_POS_X(0), UI_POS_Y(1), UI_POS_MAXWIDTH, EPIRB_TAB_HEIGHT - UI_POS_Y(1)}};
|
||||||
};
|
};
|
||||||
|
|
||||||
#ifdef SPECAN
|
#ifdef SPECAN
|
||||||
class EPIRBRxView : public spectrum::WaterfallView {
|
class EPIRBRxView : public spectrum::WaterfallView {
|
||||||
public:
|
public:
|
||||||
EPIRBRxView(EPIRBAppView& parent, Rect parent_rect);
|
EPIRBRxView(EPIRBAppView& parent, Rect parent_rect);
|
||||||
void on_show() override;
|
void on_show() override;
|
||||||
void on_hide() override;
|
void on_hide() override;
|
||||||
|
|
||||||
private:
|
private:
|
||||||
EPIRBAppView& app_view;
|
EPIRBAppView& app_view;
|
||||||
};
|
};
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
class EPIRBAppView final : public ui::View {
|
class EPIRBAppView final : public ui::View {
|
||||||
public:
|
public:
|
||||||
EPIRBAppView(ui::NavigationView& nav);
|
EPIRBAppView(ui::NavigationView& nav);
|
||||||
~EPIRBAppView();
|
~EPIRBAppView();
|
||||||
|
|
||||||
void focus() override;
|
void focus() override;
|
||||||
void refresh();
|
void refresh();
|
||||||
|
|
||||||
// Message to configure rx baseband
|
// Message to configure rx baseband
|
||||||
EPIRBRXConfig epirb_rx_config_message{};
|
EPIRBRXConfig epirb_rx_config_message{};
|
||||||
void send_config();
|
void send_config();
|
||||||
// Beacons database
|
// Beacons database
|
||||||
BeaconDB beacon_db{};
|
BeaconDB beacon_db{};
|
||||||
// Update display when beacon selection changed0
|
// Update display when beacon selection changed0
|
||||||
void on_beacon_change();
|
void on_beacon_change();
|
||||||
|
|
||||||
std::string title() const override { return "EPIRB RX"; }
|
std::string title() const override { return "EPIRB RX"; }
|
||||||
|
|
||||||
private:
|
private:
|
||||||
uint8_t squelch{50};
|
uint8_t squelch{50};
|
||||||
// The delay between each frame
|
// The delay between each frame
|
||||||
uint32_t countdown{50};
|
uint32_t countdown{50};
|
||||||
app_settings::SettingsManager settings_{
|
app_settings::SettingsManager settings_{
|
||||||
"rx_epirb",
|
"rx_epirb",
|
||||||
app_settings::Mode::RX,
|
app_settings::Mode::RX,
|
||||||
{
|
{
|
||||||
{"epirb_squelch"sv, &squelch},
|
{"epirb_squelch"sv, &squelch},
|
||||||
{"countdown"sv, &countdown},
|
{"countdown"sv, &countdown},
|
||||||
}};
|
}};
|
||||||
|
|
||||||
ui::NavigationView& nav_;
|
ui::NavigationView& nav_;
|
||||||
|
|
||||||
#ifdef LOGGER
|
#ifdef LOGGER
|
||||||
std::unique_ptr<EPIRBLogger> logger{};
|
std::unique_ptr<EPIRBLogger> logger{};
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
OptionsField options_frequency{
|
OptionsField options_frequency{
|
||||||
{UI_POS_X(0), UI_POS_Y(0)},
|
{UI_POS_X(0), UI_POS_Y(0)},
|
||||||
7,
|
7,
|
||||||
{}};
|
{}};
|
||||||
|
|
||||||
ui::RFAmpField field_rf_amp{
|
ui::RFAmpField field_rf_amp{
|
||||||
{UI_POS_X(8), UI_POS_Y(0)}};
|
{UI_POS_X(8), UI_POS_Y(0)}};
|
||||||
|
|
||||||
ui::LNAGainField field_lna{
|
ui::LNAGainField field_lna{
|
||||||
{UI_POS_X(10), UI_POS_Y(0)}};
|
{UI_POS_X(10), UI_POS_Y(0)}};
|
||||||
|
|
||||||
ui::VGAGainField field_vga{
|
ui::VGAGainField field_vga{
|
||||||
{UI_POS_X(13), UI_POS_Y(0)}};
|
{UI_POS_X(13), UI_POS_Y(0)}};
|
||||||
|
|
||||||
ui::RSSI rssi{
|
ui::RSSI rssi{
|
||||||
{UI_POS_X(16), UI_POS_Y(0), UI_POS_WIDTH_REMAINING(22), 4}};
|
{UI_POS_X(16), UI_POS_Y(0), UI_POS_WIDTH_REMAINING(22), 4}};
|
||||||
|
|
||||||
ui::Channel channel{
|
ui::Channel channel{
|
||||||
{UI_POS_X(16), UI_POS_Y(0) + 5, UI_POS_WIDTH_REMAINING(22), 4}};
|
{UI_POS_X(16), UI_POS_Y(0) + 5, UI_POS_WIDTH_REMAINING(22), 4}};
|
||||||
|
|
||||||
// ui::Audio audio{
|
// ui::Audio audio{
|
||||||
// {UI_POS_X(16), UI_POS_Y(0) + 10, UI_POS_WIDTH_REMAINING(22), 4}};
|
// {UI_POS_X(16), UI_POS_Y(0) + 10, UI_POS_WIDTH_REMAINING(22), 4}};
|
||||||
|
|
||||||
ui::AudioVolumeField field_volume{
|
ui::AudioVolumeField field_volume{
|
||||||
{UI_POS_WIDTH_REMAINING(2), UI_POS_Y(0)}};
|
{UI_POS_WIDTH_REMAINING(2), UI_POS_Y(0)}};
|
||||||
|
|
||||||
#ifdef SQUELCH
|
#ifdef SQUELCH
|
||||||
NumberField field_squelch{
|
NumberField field_squelch{
|
||||||
{UI_POS_WIDTH_REMAINING(5), UI_POS_Y(0)},
|
{UI_POS_WIDTH_REMAINING(5), UI_POS_Y(0)},
|
||||||
2,
|
2,
|
||||||
{0, 99},
|
{0, 99},
|
||||||
1,
|
1,
|
||||||
' '};
|
' '};
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
// Status display
|
// Status display
|
||||||
TextArea text_status{{UI_POS_X(0), UI_POS_Y(1), UI_POS_MAXWIDTH, UI_POS_HEIGHT(3)}};
|
TextArea text_status{{UI_POS_X(0), UI_POS_Y(1), UI_POS_MAXWIDTH, UI_POS_HEIGHT(3)}};
|
||||||
TextArea text_timeout{
|
TextArea text_timeout{
|
||||||
{UI_POS_X(13), UI_POS_Y(1), UI_POS_WIDTH(3), UI_POS_HEIGHT(1)}};
|
{UI_POS_X(13), UI_POS_Y(1), UI_POS_WIDTH(3), UI_POS_HEIGHT(1)}};
|
||||||
SignalToken signal_token_tick_second{};
|
SignalToken signal_token_tick_second{};
|
||||||
// Timeout string
|
// Timeout string
|
||||||
int16_t timeout{0};
|
int16_t timeout{0};
|
||||||
|
|
||||||
// Tab View
|
// Tab View
|
||||||
Rect view_rect = {0, EPIRB_TAB_POS_Y, UI_POS_MAXWIDTH, EPIRB_TAB_HEIGHT};
|
Rect view_rect = {0, EPIRB_TAB_POS_Y, UI_POS_MAXWIDTH, EPIRB_TAB_HEIGHT};
|
||||||
|
|
||||||
BeaconUIList view_list{view_rect};
|
BeaconUIList view_list{view_rect};
|
||||||
EPIRBDetailView view_detail{view_rect, (*this)};
|
EPIRBDetailView view_detail{view_rect, (*this)};
|
||||||
EPIRBMapView view_map{view_rect};
|
EPIRBMapView view_map{view_rect};
|
||||||
#ifdef SPECAN
|
#ifdef SPECAN
|
||||||
EPIRBRxView view_rx{*this, view_rect};
|
EPIRBRxView view_rx{*this, view_rect};
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
EPRIBQRView view_qr{view_rect};
|
EPRIBQRView view_qr{view_rect};
|
||||||
|
|
||||||
TabView tab_view{
|
TabView tab_view{
|
||||||
{"List", Theme::getInstance()->fg_cyan->foreground, &view_list},
|
{"List", Theme::getInstance()->fg_cyan->foreground, &view_list},
|
||||||
{"Detail", Theme::getInstance()->fg_green->foreground, &view_detail},
|
{"Detail", Theme::getInstance()->fg_green->foreground, &view_detail},
|
||||||
{"Map", Theme::getInstance()->fg_yellow->foreground, &view_map},
|
{"Map", Theme::getInstance()->fg_yellow->foreground, &view_map},
|
||||||
#ifdef SPECAN
|
#ifdef SPECAN
|
||||||
{"RX", Theme::getInstance()->fg_orange->foreground, &view_rx},
|
{"RX", Theme::getInstance()->fg_orange->foreground, &view_rx},
|
||||||
#endif
|
#endif
|
||||||
{"QR", Theme::getInstance()->fg_orange->foreground, &view_qr}};
|
{"QR", Theme::getInstance()->fg_orange->foreground, &view_qr}};
|
||||||
|
|
||||||
uint16_t beacons_received = 0;
|
uint16_t beacons_received = 0;
|
||||||
uint16_t packets_valid = 0;
|
uint16_t packets_valid = 0;
|
||||||
uint16_t packets_corrected = 0;
|
uint16_t packets_corrected = 0;
|
||||||
uint16_t packets_error = 0;
|
uint16_t packets_error = 0;
|
||||||
|
|
||||||
MessageHandlerRegistration message_handler_packet{
|
MessageHandlerRegistration message_handler_packet{
|
||||||
Message::ID::EPIRBPacket,
|
Message::ID::EPIRBPacket,
|
||||||
[this](Message* const p) { on_packet(p); }};
|
[this](Message* const p) { on_packet(p); }};
|
||||||
|
|
||||||
static void decode_packet(const baseband::Packet& packet, Beacon& beacon);
|
static void decode_packet(const baseband::Packet& packet, Beacon& beacon);
|
||||||
void on_packet(Message* const p);
|
void on_packet(Message* const p);
|
||||||
void update_map();
|
void update_map();
|
||||||
void on_tick_second();
|
void on_tick_second();
|
||||||
|
|
||||||
void update_display();
|
void update_display();
|
||||||
};
|
};
|
||||||
|
|
||||||
} // namespace ui::external_app::epirb_rx
|
} // namespace ui::external_app::epirb_rx
|
||||||
|
|
||||||
#endif // __UI_EPIRB_RX_H__
|
#endif // __UI_EPIRB_RX_H__
|
||||||
@@ -36,10 +36,14 @@ void Channel::paint(Painter& painter) {
|
|||||||
const range_t<int> x_max_range{0, r.width() - 1};
|
const range_t<int> x_max_range{0, r.width() - 1};
|
||||||
const auto x_max = x_max_range.clip((max_db_ - db_min) * r.width() / db_delta);
|
const auto x_max = x_max_range.clip((max_db_ - db_min) * r.width() / db_delta);
|
||||||
|
|
||||||
|
const auto bar_style = (max_db_ >= overload_threshold_)
|
||||||
|
? Theme::getInstance()->fg_red
|
||||||
|
: Theme::getInstance()->fg_blue;
|
||||||
|
|
||||||
const Rect r0{r.left(), r.top(), x_max, r.height()};
|
const Rect r0{r.left(), r.top(), x_max, r.height()};
|
||||||
painter.fill_rectangle(
|
painter.fill_rectangle(
|
||||||
r0,
|
r0,
|
||||||
Theme::getInstance()->fg_blue->foreground);
|
bar_style->foreground);
|
||||||
|
|
||||||
const Rect r1{r.left() + x_max, r.top(), 1, r.height()};
|
const Rect r1{r.left() + x_max, r.top(), 1, r.height()};
|
||||||
painter.fill_rectangle(
|
painter.fill_rectangle(
|
||||||
|
|||||||
@@ -44,8 +44,16 @@ class Channel : public Widget {
|
|||||||
|
|
||||||
void paint(Painter& painter) override;
|
void paint(Painter& painter) override;
|
||||||
|
|
||||||
|
// Opt-in receiver-overload tint: when the channel power (peak IQ magnitude
|
||||||
|
// in dBFS, 0 = full scale) reaches this threshold the bar is drawn red
|
||||||
|
// instead of blue, flagging that the analog gain is too high and the ADC is
|
||||||
|
// clipping. Default is disabled (threshold above the 0 dBFS ceiling) so
|
||||||
|
// existing users are unaffected.
|
||||||
|
void set_overload_threshold(int32_t db) { overload_threshold_ = db; }
|
||||||
|
|
||||||
private:
|
private:
|
||||||
int32_t max_db_;
|
int32_t max_db_;
|
||||||
|
int32_t overload_threshold_{1};
|
||||||
|
|
||||||
MessageHandlerRegistration message_handler_stats{
|
MessageHandlerRegistration message_handler_stats{
|
||||||
Message::ID::ChannelStatistics,
|
Message::ID::ChannelStatistics,
|
||||||
|
|||||||
+338
-272
@@ -1,272 +1,338 @@
|
|||||||
/*
|
/*
|
||||||
* Copyright (C) 2024 EPIRB Receiver Implementation
|
* Copyright (C) 2024 EPIRB Receiver Implementation
|
||||||
* Copyright (C) 2026 Frederic BORRY - ADRASEC 31
|
* Copyright (C) 2026 Frederic BORRY - ADRASEC 31
|
||||||
*
|
*
|
||||||
* This file is part of PortaPack.
|
* This file is part of PortaPack.
|
||||||
*
|
*
|
||||||
* This program is free software; you can redistribute it and/or modify
|
* 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
|
* it under the terms of the GNU General Public License as published by
|
||||||
* the Free Software Foundation; either version 2, or (at your option)
|
* the Free Software Foundation; either version 2, or (at your option)
|
||||||
* any later version.
|
* any later version.
|
||||||
*
|
*
|
||||||
* This program is distributed in the hope that it will be useful,
|
* This program is distributed in the hope that it will be useful,
|
||||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
* GNU General Public License for more details.
|
* GNU General Public License for more details.
|
||||||
*
|
*
|
||||||
* You should have received a copy of the GNU General Public License
|
* You should have received a copy of the GNU General Public License
|
||||||
* along with this program; see the file COPYING. If not, write to
|
* along with this program; see the file COPYING. If not, write to
|
||||||
* the Free Software Foundation, Inc., 51 Franklin Street,
|
* the Free Software Foundation, Inc., 51 Franklin Street,
|
||||||
* Boston, MA 02110-1301, USA.
|
* Boston, MA 02110-1301, USA.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
#include "proc_epirb.hpp"
|
#include "proc_epirb.hpp"
|
||||||
|
|
||||||
#include "portapack_shared_memory.hpp"
|
#include "portapack_shared_memory.hpp"
|
||||||
|
|
||||||
#include "dsp_fir_taps.hpp"
|
#include "dsp_fir_taps.hpp"
|
||||||
|
|
||||||
#include "audio_dma.hpp"
|
#include "audio_dma.hpp"
|
||||||
|
|
||||||
#include "event_m4.hpp"
|
#include "event_m4.hpp"
|
||||||
#include <ch.h>
|
|
||||||
|
#include <algorithm>
|
||||||
EPIRBProcessor::EPIRBProcessor() {
|
#include <cmath>
|
||||||
// Configure the decimation filters for narrowband EPIRB signal
|
|
||||||
decim_0.configure(taps_11k0_decim_0.taps);
|
#include <ch.h>
|
||||||
decim_1.configure(taps_11k0_decim_1.taps);
|
EPIRBProcessor::EPIRBProcessor() {
|
||||||
// Configure channel filter for audio filtering
|
// Configure the decimation filters for narrowband EPIRB signal
|
||||||
channel_filter.configure(taps_11k0_channel.taps, 2);
|
decim_0.configure(taps_11k0_decim_0.taps);
|
||||||
// Configure demodulation for audio output
|
decim_1.configure(taps_11k0_decim_1.taps);
|
||||||
demod.configure(SAMPLE_RATE, 5000);
|
// Configure channel filter for audio filtering
|
||||||
// Configure audio output (+squelch level)
|
channel_filter.configure(taps_11k0_channel.taps, 2);
|
||||||
configure_audio();
|
// Configure demodulation for audio output
|
||||||
#ifdef SPECAN
|
demod.configure(SAMPLE_RATE, 5000);
|
||||||
channel_spectrum.set_decimation_factor(1);
|
// Configure audio output (+squelch level)
|
||||||
#endif
|
configure_audio();
|
||||||
baseband_thread.start();
|
#ifdef SPECAN
|
||||||
}
|
channel_spectrum.set_decimation_factor(1);
|
||||||
|
#endif
|
||||||
void EPIRBProcessor::configure_audio() {
|
baseband_thread.start();
|
||||||
// UI sends an squelch value ranging from 0 to 99, 0 disables squelch, dividing UI value by 40 gives a valid UI threashold around 50
|
}
|
||||||
audio_output.configure(audio_24k_hpf_300hz_config, audio_24k_deemph_300_6_config, ((float)squelch_level) / 40.0f);
|
|
||||||
}
|
void EPIRBProcessor::configure_audio() {
|
||||||
|
// UI sends an squelch value ranging from 0 to 99, 0 disables squelch, dividing UI value by 40 gives a valid UI threashold around 50
|
||||||
float EPIRBProcessor::get_phase_diff(const complex16_t& sample0, const complex16_t& sample1) {
|
audio_output.configure(audio_24k_hpf_300hz_config, audio_24k_deemph_300_6_config, ((float)squelch_level) / 40.0f);
|
||||||
// Calculate the phase difference between two samples
|
}
|
||||||
float dI = sample1.real() * sample0.real() + sample1.imag() * sample0.imag();
|
|
||||||
float dQ = sample1.imag() * sample0.real() - sample1.real() * sample0.imag();
|
float EPIRBProcessor::get_phase_diff(const complex16_t& sample0, const complex16_t& sample1) {
|
||||||
float phase_diff = atan2f(dQ, dI);
|
// Calculate the phase difference between two samples
|
||||||
// Prevent phase diff from wrapping around
|
float dI = sample1.real() * sample0.real() + sample1.imag() * sample0.imag();
|
||||||
if (phase_diff > M_PI) phase_diff -= 2.0f * M_PI;
|
float dQ = sample1.imag() * sample0.real() - sample1.real() * sample0.imag();
|
||||||
if (phase_diff < -M_PI) phase_diff += 2.0f * M_PI;
|
float phase_diff = atan2f(dQ, dI);
|
||||||
return phase_diff;
|
return phase_diff;
|
||||||
}
|
}
|
||||||
|
|
||||||
bool EPIRBProcessor::filtered_rise_detect(bool condition) {
|
bool EPIRBProcessor::filtered_rise_detect(bool condition) {
|
||||||
bool result = false;
|
bool result = false;
|
||||||
if (condition) {
|
if (condition) {
|
||||||
// If rise condition is matched, filter peaks that last less than 3 samples
|
// If rise condition is matched, filter peaks that last less than 3 samples
|
||||||
rise_detection_count++;
|
rise_detection_count++;
|
||||||
if (rise_detection_count >= RISE_FILTER_SAMPLES) {
|
if (rise_detection_count >= RISE_FILTER_SAMPLES) {
|
||||||
result = true;
|
result = true;
|
||||||
rise_detection_count = 0;
|
rise_detection_count = 0;
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
rise_detection_count = 0;
|
rise_detection_count = 0;
|
||||||
}
|
}
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
void EPIRBProcessor::execute(const buffer_c8_t& buffer) {
|
void EPIRBProcessor::execute(const buffer_c8_t& buffer) {
|
||||||
// First decimation stage: 3.072000 MHz / 8 -> 384 kHz
|
// First decimation stage: 3.072000 MHz / 8 -> 384 kHz
|
||||||
const auto decim_0_out = decim_0.execute(buffer, dst_buffer);
|
const auto decim_0_out = decim_0.execute(buffer, dst_buffer);
|
||||||
|
|
||||||
// Second decimation stage: 384 kHz / 8 -> 48 kHz
|
// Second decimation stage: 384 kHz / 8 -> 48 kHz
|
||||||
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);
|
||||||
// We use decim1 output as decimator output
|
// We use decim1 output as decimator output
|
||||||
const auto decimator_out = decim_1_out;
|
const auto decimator_out = decim_1_out;
|
||||||
|
|
||||||
#ifdef SPECAN
|
#ifdef SPECAN
|
||||||
// Feed IQ data into spectrum collector for the RF waterfall.
|
// Feed IQ data into spectrum collector for the RF waterfall.
|
||||||
if (spectrum_on) channel_spectrum.feed(decim_1_out, -5500, 5500, 3400);
|
if (spectrum_on) channel_spectrum.feed(decim_1_out, -5500, 5500, 3400);
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
feed_channel_stats(decimator_out);
|
feed_channel_stats(decimator_out);
|
||||||
|
|
||||||
// if (audio_on) {
|
// if (audio_on) {
|
||||||
// Channel filter for audio out
|
// Channel filter for audio out
|
||||||
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);
|
||||||
audio_output.write(audio);
|
audio_output.write(audio);
|
||||||
//}
|
//}
|
||||||
|
|
||||||
// Process each decimated sample through state machine
|
// Process each decimated sample through state machine
|
||||||
for (size_t i = 0; i < decimator_out.count; i++) {
|
for (size_t i = 0; i < decimator_out.count; i++) {
|
||||||
// Track sample count since last symbol and since begining of the frame
|
// Track sample count since last symbol and since begining of the frame
|
||||||
sample_count++;
|
sample_count++;
|
||||||
frame_sample_count++;
|
frame_sample_count++;
|
||||||
// Compute phase delta since last sample
|
// Compute phase delta since last sample
|
||||||
float phase_delta = get_phase_diff(last_sample, decimator_out.p[i]);
|
float phase_delta = get_phase_diff(last_sample, decimator_out.p[i]);
|
||||||
last_sample = decimator_out.p[i];
|
last_sample = decimator_out.p[i];
|
||||||
|
|
||||||
// Let's sum phase delta over a 12 sample window to get the full phase jump
|
// AFC: remove the estimated carrier frequency offset from the raw delta
|
||||||
phase_delta_acc -= phase_delta_buffer[pahse_delta_index];
|
// before any further processing. Done on the per-sample value so the
|
||||||
phase_delta_buffer[pahse_delta_index] = phase_delta;
|
// 12-sample accumulator below tracks it naturally.
|
||||||
phase_delta_acc += phase_delta_buffer[pahse_delta_index];
|
phase_delta -= freq_offset_est;
|
||||||
pahse_delta_index = (pahse_delta_index + 1) % PHASE_DELTA_ACC_SIZE;
|
|
||||||
|
// Keep the (de-biased) per-sample delta for AFC averaging over the carrier.
|
||||||
// Use accumulated delta
|
const float sample_phase_delta = phase_delta;
|
||||||
phase_delta = phase_delta_acc;
|
|
||||||
|
// Let's sum phase delta over a 12 sample window to get the full phase jump
|
||||||
// State machine for COSPAS frame detection
|
phase_delta_acc -= phase_delta_buffer[phase_delta_index];
|
||||||
switch (current_state) {
|
phase_delta_buffer[phase_delta_index] = phase_delta;
|
||||||
case IDLE:
|
phase_delta_acc += phase_delta_buffer[phase_delta_index];
|
||||||
// We are waiting for a 160ms empty carrier => phase shouls be stable during this period
|
phase_delta_index = (phase_delta_index + 1) % PHASE_DELTA_ACC_SIZE;
|
||||||
// We accept a 0.6 phase shift since phase may drift durring carrier if carrier frequency is not alligned with tuner frequency
|
|
||||||
if (filtered_rise_detect(phase_delta >= 0.6f)) {
|
// Use accumulated delta
|
||||||
stability_counter = 0;
|
phase_delta = phase_delta_acc;
|
||||||
} else {
|
|
||||||
stability_counter++;
|
// State machine for COSPAS frame detection
|
||||||
if (stability_counter > CARRIER_SAMPLES_THRESHOLD) {
|
switch (current_state) {
|
||||||
// Carrier has been stable long enought, go to locked state
|
case IDLE: {
|
||||||
current_state = CARRIER_LOCKED;
|
// Continuously pull the AFC estimate toward the mean per-sample
|
||||||
frame_sample_count = 0;
|
// rotation so the accumulator self-centers for any offset up to
|
||||||
}
|
// the discriminator Nyquist (~+/-24 kHz). On noise the de-biased
|
||||||
}
|
// deltas average to ~0, so the estimate stays put; on a real
|
||||||
break;
|
// carrier it converges within a few ms and the thresholds below
|
||||||
|
// then see a de-biased signal regardless of the actual offset.
|
||||||
case CARRIER_LOCKED:
|
// Only update AFC when the per-sample phase delta is small
|
||||||
// Carrier is locked, we now wait for a phase 1.1 rad phase jump corresponding to the befining of the frame
|
// (large jumps indicate noise or transient, which would cause
|
||||||
// Let's use a 0.7 phase jump threshold
|
// a random-walk drift if used for AFC updates).
|
||||||
if (filtered_rise_detect(phase_delta >= 0.7f)) {
|
if (fabsf(sample_phase_delta) <= AFC_UPDATE_PHASE_MAX) {
|
||||||
// Jump detected, frame starts now
|
freq_offset_est += AFC_TRACK_ALPHA * sample_phase_delta;
|
||||||
frame_sample_count = 0;
|
// Bounds checking: limit to ±5 kHz (~0.654 rad/sample at 48 kHz)
|
||||||
// Go to data sync state
|
freq_offset_est = std::clamp(freq_offset_est, -0.654f, 0.654f);
|
||||||
current_state = DATA_SYNC;
|
|
||||||
// Frame should always start with a positive phase shift
|
// Track AFC convergence using Welford's online algorithm
|
||||||
last_phase_positive = true;
|
afc_convergence_n++;
|
||||||
// And a 1 value
|
float delta = freq_offset_est - afc_mean;
|
||||||
last_bit = true;
|
afc_mean += delta / afc_convergence_n;
|
||||||
} else if (frame_sample_count > CARRIER_MAX_SAMPLES) {
|
float delta2 = freq_offset_est - afc_mean;
|
||||||
// We missed sync pattern
|
afc_m2 += delta * delta2;
|
||||||
frame_end();
|
}
|
||||||
}
|
|
||||||
break;
|
// We are waiting for a 160ms empty carrier => phase should be stable during this period
|
||||||
|
// Use a symmetric threshold: once AFC has removed the bias a stable
|
||||||
case DATA_SYNC: {
|
// carrier sits near 0, so both positive and negative excursions of
|
||||||
float abs_phase_delta = fabsf(phase_delta);
|
// the accumulated delta indicate the carrier is not yet stable.
|
||||||
|
if (filtered_rise_detect(fabsf(phase_delta) >= 0.6f)) {
|
||||||
if (abs_phase_delta >= 1.6f) {
|
stability_counter = 0;
|
||||||
// Phase should jump from 1.1 rad to -1.1 rad or the other way around
|
// Reset convergence tracking when the carrier is not stable,
|
||||||
// Absolute phase jump is expected to be 2.2 rad
|
// so variance is measured only over the current stable window.
|
||||||
// Phase jump is either positive or negative
|
afc_mean = 0.0f;
|
||||||
bool phase_positive = (phase_delta >= 0.0f);
|
afc_m2 = 0.0f;
|
||||||
|
afc_convergence_n = 0;
|
||||||
if (phase_positive != last_phase_positive) {
|
} else {
|
||||||
// Phase jumped to the opposit direction of last jump
|
stability_counter++;
|
||||||
last_phase_positive = phase_positive;
|
// Check both phase stability AND AFC convergence before transitioning
|
||||||
bool cur_bit;
|
if (stability_counter > CARRIER_SAMPLES_THRESHOLD) {
|
||||||
// Phase change => how long since last change ?
|
float afc_variance = (afc_convergence_n > 1) ? afc_m2 / (afc_convergence_n - 1) : 0.0f;
|
||||||
if ((frame_sample_count >= (SAMPLES_PER_SYMBOL - SAMPLES_MARGIN)) && (frame_sample_count <= (SAMPLES_PER_SYMBOL + SAMPLES_MARGIN))) {
|
if (afc_variance < AFC_CONVERGENCE_THRESHOLD) {
|
||||||
// Frame start
|
// Both phase and AFC have converged, go to locked state
|
||||||
if (!phase_positive) {
|
current_state = CARRIER_LOCKED;
|
||||||
// Symbol detection is made on falling edge
|
// Reset carrier accumulators so the latched update uses
|
||||||
cur_bit = true;
|
// only the residual measured while in the locked window
|
||||||
} else {
|
carrier_phase_sum = 0.0f;
|
||||||
// Ignore rising edge
|
carrier_phase_n = 0;
|
||||||
continue;
|
frame_sample_count = 0;
|
||||||
}
|
}
|
||||||
} else if (sample_count > (SAMPLES_PER_SYMBOL * 2 + SAMPLES_MARGIN)) {
|
}
|
||||||
// We missed something...
|
}
|
||||||
// Let's keep same value for current bit
|
} break;
|
||||||
cur_bit = last_bit;
|
|
||||||
} else if (sample_count >= (SAMPLES_PER_SYMBOL * 2 - SAMPLES_MARGIN)) {
|
case CARRIER_LOCKED:
|
||||||
// 2 symbols since last change => bit value changes
|
// Carrier is locked: this is the clean unmodulated carrier window.
|
||||||
cur_bit = !last_bit;
|
// Average the per-sample phase delta here to estimate the residual
|
||||||
} else if ((sample_count >= (SAMPLES_PER_SYMBOL - SAMPLES_MARGIN)) && (sample_count <= (SAMPLES_PER_SYMBOL + SAMPLES_MARGIN))) {
|
// frequency offset (rad/sample) used for AFC.
|
||||||
// Phase change occured in first half bit => we keep the same value
|
carrier_phase_sum += sample_phase_delta;
|
||||||
if ((phase_positive && last_bit) || (!phase_positive && !last_bit)) {
|
carrier_phase_n++;
|
||||||
sample_count = 0;
|
// Carrier is locked, we now wait for a phase 1.1 rad phase jump corresponding to the beginning of the frame
|
||||||
// Ignore rising edge if current value is 1 and falling edge if current value is 0 and move to next symbol
|
// Let's use a 0.7 phase jump threshold
|
||||||
continue;
|
if (filtered_rise_detect(phase_delta >= 0.7f)) {
|
||||||
}
|
// Latch the AFC estimate from the carrier we just measured so it
|
||||||
// Same value on falling/rising edge
|
// applies to the data burst that starts now. Accumulate so the
|
||||||
cur_bit = last_bit;
|
// residual is folded into any prior estimate.
|
||||||
} else {
|
if (carrier_phase_n > 0) {
|
||||||
// Filter the rest
|
freq_offset_est += carrier_phase_sum / carrier_phase_n;
|
||||||
continue;
|
// Bounds checking: limit to ±5 kHz (~0.654 rad/sample at 48 kHz)
|
||||||
}
|
freq_offset_est = std::clamp(freq_offset_est, -0.654f, 0.654f);
|
||||||
// Store new bit and move to next symbol
|
}
|
||||||
sample_count = 0;
|
// Jump detected, frame starts now
|
||||||
packet_builder.execute(cur_bit);
|
frame_sample_count = 0;
|
||||||
last_bit = cur_bit;
|
// Go to data sync state
|
||||||
}
|
current_state = DATA_SYNC;
|
||||||
}
|
// Frame should always start with a positive phase shift
|
||||||
if (frame_sample_count > FRAME_MAX_SAMPLES) {
|
last_phase_positive = true;
|
||||||
// End of frame
|
// And a 1 value
|
||||||
current_state = POST_FRAME;
|
last_bit = true;
|
||||||
packet_builder.flush();
|
} else if (frame_sample_count > CARRIER_MAX_SAMPLES) {
|
||||||
}
|
// We missed sync pattern
|
||||||
} break;
|
frame_end();
|
||||||
case POST_FRAME:
|
}
|
||||||
if (frame_sample_count > CARRIER_MAX_SAMPLES) {
|
break;
|
||||||
// End of carrier
|
|
||||||
frame_end();
|
case DATA_SYNC: {
|
||||||
}
|
float abs_phase_delta = fabsf(phase_delta);
|
||||||
default:
|
|
||||||
break;
|
if (abs_phase_delta >= 1.6f) {
|
||||||
}
|
// Phase should jump from 1.1 rad to -1.1 rad or the other way around
|
||||||
}
|
// Absolute phase jump is expected to be 2.2 rad
|
||||||
}
|
// Phase jump is either positive or negative
|
||||||
|
bool phase_positive = (phase_delta >= 0.0f);
|
||||||
void EPIRBProcessor::frame_end() {
|
|
||||||
sample_count = 0;
|
if (phase_positive != last_phase_positive) {
|
||||||
frame_sample_count = 0;
|
// Phase jumped to the opposite direction of last jump
|
||||||
stability_counter = 0;
|
last_phase_positive = phase_positive;
|
||||||
last_phase_positive = false;
|
bool cur_bit;
|
||||||
last_bit = false;
|
// Phase change => how long since last change ?
|
||||||
current_state = IDLE;
|
if ((frame_sample_count >= (SAMPLES_PER_SYMBOL - SAMPLES_MARGIN)) && (frame_sample_count <= (SAMPLES_PER_SYMBOL + SAMPLES_MARGIN))) {
|
||||||
packet_builder.reset_state();
|
// Frame start
|
||||||
}
|
if (!phase_positive) {
|
||||||
|
// Symbol detection is made on falling edge
|
||||||
void EPIRBProcessor::payload_handler(const baseband::Packet& packet) {
|
cur_bit = true;
|
||||||
// EPIRB packet received: create and send EPIRB packet message to application layer
|
} else {
|
||||||
const EPIRBPacketMessage message{packet};
|
// Ignore rising edge
|
||||||
shared_memory.application_queue.push(message);
|
continue;
|
||||||
}
|
}
|
||||||
|
} else if (sample_count > (SAMPLES_PER_SYMBOL * 2 + SAMPLES_MARGIN)) {
|
||||||
void EPIRBProcessor::on_message(const Message* const msg) {
|
// We missed something...
|
||||||
// Configure the processor
|
// Let's keep same value for current bit
|
||||||
switch (msg->id) {
|
cur_bit = last_bit;
|
||||||
#ifdef SPECAN
|
} else if (sample_count >= (SAMPLES_PER_SYMBOL * 2 - SAMPLES_MARGIN)) {
|
||||||
case Message::ID::UpdateSpectrum:
|
// 2 symbols since last change => bit value changes
|
||||||
case Message::ID::SpectrumStreamingConfig:
|
cur_bit = !last_bit;
|
||||||
channel_spectrum.on_message(msg);
|
} else if ((sample_count >= (SAMPLES_PER_SYMBOL - SAMPLES_MARGIN)) && (sample_count <= (SAMPLES_PER_SYMBOL + SAMPLES_MARGIN))) {
|
||||||
break;
|
// Phase change occurred in first half bit => we keep the same value
|
||||||
#endif
|
if ((phase_positive && last_bit) || (!phase_positive && !last_bit)) {
|
||||||
case Message::ID::EPIRBRXConfig: {
|
sample_count = 0;
|
||||||
const EPIRBRXConfig message = *reinterpret_cast<const EPIRBRXConfig*>(msg);
|
// Ignore rising edge if current value is 1 and falling edge if current value is 0 and move to next symbol
|
||||||
// audio_on = message.audio_on;
|
continue;
|
||||||
#ifdef SPECAN
|
}
|
||||||
spectrum_on = message.spectrum_on;
|
// Same value on falling/rising edge
|
||||||
#endif
|
cur_bit = last_bit;
|
||||||
if (message.squelch != squelch_level) {
|
} else {
|
||||||
// Update squelch config
|
// Filter the rest
|
||||||
squelch_level = message.squelch;
|
continue;
|
||||||
configure_audio();
|
}
|
||||||
}
|
// Store new bit and move to next symbol
|
||||||
} break;
|
sample_count = 0;
|
||||||
|
packet_builder.execute(cur_bit);
|
||||||
default:
|
last_bit = cur_bit;
|
||||||
break;
|
}
|
||||||
}
|
}
|
||||||
}
|
if (frame_sample_count > FRAME_MAX_SAMPLES) {
|
||||||
|
// End of frame
|
||||||
int main() {
|
current_state = POST_FRAME;
|
||||||
audio::dma::init_audio_out();
|
packet_builder.flush();
|
||||||
|
}
|
||||||
EventDispatcher event_dispatcher{std::make_unique<EPIRBProcessor>()};
|
} break;
|
||||||
event_dispatcher.run();
|
case POST_FRAME:
|
||||||
return 0;
|
if (frame_sample_count > CARRIER_MAX_SAMPLES) {
|
||||||
}
|
// End of carrier
|
||||||
|
frame_end();
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void EPIRBProcessor::frame_end() {
|
||||||
|
sample_count = 0;
|
||||||
|
frame_sample_count = 0;
|
||||||
|
stability_counter = 0;
|
||||||
|
last_phase_positive = false;
|
||||||
|
last_bit = false;
|
||||||
|
current_state = IDLE;
|
||||||
|
// Reset AFC so the next burst is re-estimated from its own carrier preamble.
|
||||||
|
freq_offset_est = 0.0f;
|
||||||
|
carrier_phase_sum = 0.0f;
|
||||||
|
carrier_phase_n = 0;
|
||||||
|
// Reset AFC convergence tracking for next frame
|
||||||
|
afc_mean = 0.0f;
|
||||||
|
afc_m2 = 0.0f;
|
||||||
|
afc_convergence_n = 0;
|
||||||
|
packet_builder.reset_state();
|
||||||
|
}
|
||||||
|
|
||||||
|
void EPIRBProcessor::payload_handler(const baseband::Packet& packet) {
|
||||||
|
// EPIRB packet received: create and send EPIRB packet message to application layer
|
||||||
|
const EPIRBPacketMessage message{packet};
|
||||||
|
shared_memory.application_queue.push(message);
|
||||||
|
}
|
||||||
|
|
||||||
|
void EPIRBProcessor::on_message(const Message* const msg) {
|
||||||
|
// Configure the processor
|
||||||
|
switch (msg->id) {
|
||||||
|
#ifdef SPECAN
|
||||||
|
case Message::ID::UpdateSpectrum:
|
||||||
|
case Message::ID::SpectrumStreamingConfig:
|
||||||
|
channel_spectrum.on_message(msg);
|
||||||
|
break;
|
||||||
|
#endif
|
||||||
|
case Message::ID::EPIRBRXConfig: {
|
||||||
|
const EPIRBRXConfig message = *reinterpret_cast<const EPIRBRXConfig*>(msg);
|
||||||
|
// audio_on = message.audio_on;
|
||||||
|
#ifdef SPECAN
|
||||||
|
spectrum_on = message.spectrum_on;
|
||||||
|
#endif
|
||||||
|
if (message.squelch != squelch_level) {
|
||||||
|
// Update squelch config
|
||||||
|
squelch_level = message.squelch;
|
||||||
|
configure_audio();
|
||||||
|
}
|
||||||
|
} break;
|
||||||
|
|
||||||
|
default:
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
int main() {
|
||||||
|
audio::dma::init_audio_out();
|
||||||
|
|
||||||
|
EventDispatcher event_dispatcher{std::make_unique<EPIRBProcessor>()};
|
||||||
|
event_dispatcher.run();
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|||||||
+296
-268
@@ -1,268 +1,296 @@
|
|||||||
/*
|
/*
|
||||||
* Copyright (C) 2024 EPIRB Receiver Implementation
|
* Copyright (C) 2024 EPIRB Receiver Implementation
|
||||||
* Copyright (C) 2026 Frederic BORRY - ADRASEC 31
|
* Copyright (C) 2026 Frederic BORRY - ADRASEC 31
|
||||||
*
|
*
|
||||||
* This file is part of PortaPack.
|
* This file is part of PortaPack.
|
||||||
*
|
*
|
||||||
* This program is free software; you can redistribute it and/or modify
|
* 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
|
* it under the terms of the GNU General Public License as published by
|
||||||
* the Free Software Foundation; either version 2, or (at your option)
|
* the Free Software Foundation; either version 2, or (at your option)
|
||||||
* any later version.
|
* any later version.
|
||||||
*
|
*
|
||||||
* This program is distributed in the hope that it will be useful,
|
* This program is distributed in the hope that it will be useful,
|
||||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
* GNU General Public License for more details.
|
* GNU General Public License for more details.
|
||||||
*
|
*
|
||||||
* You should have received a copy of the GNU General Public License
|
* You should have received a copy of the GNU General Public License
|
||||||
* along with this program; see the file COPYING. If not, write to
|
* along with this program; see the file COPYING. If not, write to
|
||||||
* the Free Software Foundation, Inc., 51 Franklin Street,
|
* the Free Software Foundation, Inc., 51 Franklin Street,
|
||||||
* Boston, MA 02110-1301, USA.
|
* Boston, MA 02110-1301, USA.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
#ifndef __PROC_EPIRB_H__
|
#ifndef __PROC_EPIRB_H__
|
||||||
#define __PROC_EPIRB_H__
|
#define __PROC_EPIRB_H__
|
||||||
|
|
||||||
#include <cstdint>
|
#include <cstdint>
|
||||||
#include <cstddef>
|
#include <cstddef>
|
||||||
#include <array>
|
#include <array>
|
||||||
#include <complex>
|
#include <complex>
|
||||||
|
|
||||||
#include "baseband_processor.hpp"
|
#include "baseband_processor.hpp"
|
||||||
#include "baseband_thread.hpp"
|
#include "baseband_thread.hpp"
|
||||||
#include "rssi_thread.hpp"
|
#include "rssi_thread.hpp"
|
||||||
#include "channel_decimator.hpp"
|
#include "channel_decimator.hpp"
|
||||||
#include "matched_filter.hpp"
|
#include "matched_filter.hpp"
|
||||||
#include "packet_builder.hpp"
|
#include "packet_builder.hpp"
|
||||||
#include "baseband_packet.hpp"
|
#include "baseband_packet.hpp"
|
||||||
#include "message.hpp"
|
#include "message.hpp"
|
||||||
#include "buffer.hpp"
|
#include "buffer.hpp"
|
||||||
|
|
||||||
// Specan is disable to keep application size below the 32k limit
|
// Specan is disable to keep application size below the 32k limit
|
||||||
// #define SPECAN
|
// #define SPECAN
|
||||||
|
|
||||||
#ifdef SPECAN
|
#ifdef SPECAN
|
||||||
#include "spectrum_collector.hpp"
|
#include "spectrum_collector.hpp"
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
#include "audio_output.hpp"
|
#include "audio_output.hpp"
|
||||||
#include "dsp_demodulate.hpp"
|
#include "dsp_demodulate.hpp"
|
||||||
|
|
||||||
// Forward declarations for types only used as pointers/references
|
// Forward declarations for types only used as pointers/references
|
||||||
class Message;
|
class Message;
|
||||||
namespace baseband {
|
namespace baseband {
|
||||||
class Packet;
|
class Packet;
|
||||||
}
|
}
|
||||||
|
|
||||||
// COSPAS / SARSAT 406 frame constants
|
// COSPAS / SARSAT 406 frame constants
|
||||||
// Size of preamble (bits)
|
// Size of preamble (bits)
|
||||||
#define COSPAS_PREAMBLE_SIZE 24
|
#define COSPAS_PREAMBLE_SIZE 24
|
||||||
// Size of long frame (bits)
|
// Size of long frame (bits)
|
||||||
#define COSPAS_LONG_FRAME_SIZE 144
|
#define COSPAS_LONG_FRAME_SIZE 144
|
||||||
// Siz of short frame (bits)
|
// Size of short frame (bits)
|
||||||
#define COSPAS_SHORT_FRAME_SIZE 112
|
#define COSPAS_SHORT_FRAME_SIZE 112
|
||||||
// Preamble for real frames
|
// Preamble for real frames
|
||||||
#define COSPAS_REAL_PREAMBLE 0b1111'1111'1111'1110'0010'1111
|
#define COSPAS_REAL_PREAMBLE 0b1111'1111'1111'1110'0010'1111
|
||||||
// Preable for test frames
|
// Preamble for test frames
|
||||||
#define COSPAS_TEST_PREAMBLE 0b1111'1111'1111'1110'1101'0000
|
#define COSPAS_TEST_PREAMBLE 0b1111'1111'1111'1110'1101'0000
|
||||||
|
|
||||||
// Dedicated EPIRB PacketBuilder
|
// Dedicated EPIRB PacketBuilder
|
||||||
// Usees diedicated preamble detection logic to find both real and test frames
|
// Uses dedicated preamble detection logic to find both real and test frames
|
||||||
// Also as a dedicated packet size detection based on frame's size bit
|
// Also as a dedicated packet size detection based on frame's size bit
|
||||||
class EPIRBPacketBuilder {
|
class EPIRBPacketBuilder {
|
||||||
public:
|
public:
|
||||||
using EPIRBHandler = void (*)(void* context, const baseband::Packet& packet);
|
using EPIRBHandler = void (*)(void* context, const baseband::Packet& packet);
|
||||||
|
|
||||||
EPIRBPacketBuilder(
|
EPIRBPacketBuilder(
|
||||||
void* context,
|
void* context,
|
||||||
EPIRBHandler handler)
|
EPIRBHandler handler)
|
||||||
: context(context),
|
: context(context),
|
||||||
handler(handler) {
|
handler(handler) {
|
||||||
}
|
}
|
||||||
|
|
||||||
void execute(
|
void execute(
|
||||||
const uint_fast8_t symbol) {
|
const uint_fast8_t symbol) {
|
||||||
bit_history.add(symbol);
|
bit_history.add(symbol);
|
||||||
|
|
||||||
switch (state) {
|
switch (state) {
|
||||||
case State::Preamble: {
|
case State::Preamble: {
|
||||||
// Detect both real and test fram preambles
|
// Detect both real and test fram preambles
|
||||||
bool is_real = real_sync_matcher(bit_history, packet.size());
|
bool is_real = real_sync_matcher(bit_history, packet.size());
|
||||||
bool is_test = test_sync_matcher(bit_history, packet.size());
|
bool is_test = test_sync_matcher(bit_history, packet.size());
|
||||||
if (is_real || is_test) {
|
if (is_real || is_test) {
|
||||||
// Append preamble to the begining of the packet
|
// Append preamble to the begining of the packet
|
||||||
uint64_t preamble = is_real ? COSPAS_REAL_PREAMBLE : COSPAS_TEST_PREAMBLE;
|
uint64_t preamble = is_real ? COSPAS_REAL_PREAMBLE : COSPAS_TEST_PREAMBLE;
|
||||||
for (int8_t i = (COSPAS_PREAMBLE_SIZE - 1); i >= 0; i--) {
|
for (int8_t i = (COSPAS_PREAMBLE_SIZE - 1); i >= 0; i--) {
|
||||||
packet.add((preamble >> i) & 0x1);
|
packet.add((preamble >> i) & 0x1);
|
||||||
}
|
}
|
||||||
state = State::Format;
|
state = State::Format;
|
||||||
}
|
}
|
||||||
} break;
|
} break;
|
||||||
case State::Format:
|
case State::Format:
|
||||||
packet.add(symbol);
|
packet.add(symbol);
|
||||||
// 144 bits for long frames and 112 for short frames
|
// 144 bits for long frames and 112 for short frames
|
||||||
size = symbol ? COSPAS_LONG_FRAME_SIZE : COSPAS_SHORT_FRAME_SIZE;
|
size = symbol ? COSPAS_LONG_FRAME_SIZE : COSPAS_SHORT_FRAME_SIZE;
|
||||||
state = State::Payload;
|
state = State::Payload;
|
||||||
|
|
||||||
break;
|
break;
|
||||||
case State::Payload:
|
case State::Payload:
|
||||||
packet.add(symbol);
|
packet.add(symbol);
|
||||||
|
|
||||||
if (packet.size() >= size) {
|
if (packet.size() >= size) {
|
||||||
flush();
|
flush();
|
||||||
} else {
|
} else {
|
||||||
if (packet.size() >= packet.capacity()) {
|
if (packet.size() >= packet.capacity()) {
|
||||||
reset_state();
|
reset_state();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
|
|
||||||
default:
|
default:
|
||||||
reset_state();
|
reset_state();
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void flush() {
|
void flush() {
|
||||||
// Timestamp is not set here to save some app space (not used on ui side)
|
// Timestamp is not set here to save some app space (not used on ui side)
|
||||||
// packet.set_timestamp(Timestamp::now());
|
// packet.set_timestamp(Timestamp::now());
|
||||||
if (handler) handler(context, packet);
|
if (handler) handler(context, packet);
|
||||||
reset_state();
|
reset_state();
|
||||||
}
|
}
|
||||||
|
|
||||||
void reset_state() {
|
void reset_state() {
|
||||||
packet.clear();
|
packet.clear();
|
||||||
bit_history = BitHistory();
|
bit_history = BitHistory();
|
||||||
state = State::Preamble;
|
state = State::Preamble;
|
||||||
}
|
}
|
||||||
|
|
||||||
private:
|
private:
|
||||||
enum State {
|
enum State {
|
||||||
Preamble,
|
Preamble,
|
||||||
Format,
|
Format,
|
||||||
Payload,
|
Payload,
|
||||||
};
|
};
|
||||||
|
|
||||||
BitHistory bit_history{};
|
BitHistory bit_history{};
|
||||||
BitPattern real_sync_matcher{COSPAS_REAL_PREAMBLE, COSPAS_PREAMBLE_SIZE};
|
BitPattern real_sync_matcher{COSPAS_REAL_PREAMBLE, COSPAS_PREAMBLE_SIZE};
|
||||||
BitPattern test_sync_matcher{COSPAS_TEST_PREAMBLE, COSPAS_PREAMBLE_SIZE};
|
BitPattern test_sync_matcher{COSPAS_TEST_PREAMBLE, COSPAS_PREAMBLE_SIZE};
|
||||||
void* context;
|
void* context;
|
||||||
EPIRBHandler handler;
|
EPIRBHandler handler;
|
||||||
uint8_t size{0};
|
uint8_t size{0};
|
||||||
|
|
||||||
State state{State::Preamble};
|
State state{State::Preamble};
|
||||||
baseband::Packet packet{};
|
baseband::Packet packet{};
|
||||||
};
|
};
|
||||||
|
|
||||||
class EPIRBProcessor : public BasebandProcessor {
|
class EPIRBProcessor : public BasebandProcessor {
|
||||||
public:
|
public:
|
||||||
EPIRBProcessor();
|
EPIRBProcessor();
|
||||||
|
|
||||||
void execute(const buffer_c8_t& buffer) override;
|
void execute(const buffer_c8_t& buffer) override;
|
||||||
|
|
||||||
void on_message(const Message* const message) override;
|
void on_message(const Message* const message) override;
|
||||||
|
|
||||||
private:
|
private:
|
||||||
// Baseband frequency is set to 3,072,000 samples / sec
|
// Baseband frequency is set to 3,072,000 samples / sec
|
||||||
static constexpr uint32_t BASEBAND_SAMPLE_RATE = 3072000;
|
static constexpr uint32_t BASEBAND_SAMPLE_RATE = 3072000;
|
||||||
static constexpr uint32_t SAMPLE_RATE = BASEBAND_SAMPLE_RATE / 8 / 8; // We use to decimators with factor 8 each
|
static constexpr uint32_t SAMPLE_RATE = BASEBAND_SAMPLE_RATE / 8 / 8; // We use to decimators with factor 8 each
|
||||||
static constexpr uint32_t SYMBOL_RATE = 800; // 400 bps + Manchester (2 1/2 bits per symbol) => 800
|
static constexpr uint32_t SYMBOL_RATE = 800; // 400 bps + Manchester (2 1/2 bits per symbol) => 800
|
||||||
static constexpr size_t SAMPLES_PER_SYMBOL = SAMPLE_RATE / SYMBOL_RATE; // = 60 samples per symbol
|
static constexpr size_t SAMPLES_PER_SYMBOL = SAMPLE_RATE / SYMBOL_RATE; // = 60 samples per symbol
|
||||||
static constexpr size_t SAMPLES_PER_BIT = SAMPLES_PER_SYMBOL * 2; // = 120 samples per bit
|
static constexpr size_t SAMPLES_PER_BIT = SAMPLES_PER_SYMBOL * 2; // = 120 samples per bit
|
||||||
static constexpr size_t SAMPLES_MARGIN = SAMPLES_PER_SYMBOL / 3; // = Allow 20 sample drift
|
static constexpr size_t SAMPLES_MARGIN = SAMPLES_PER_SYMBOL / 3; // = Allow 20 sample drift
|
||||||
static constexpr size_t SAMPLES_ACCUMUMLATOR = SAMPLES_PER_SYMBOL / 5; // Accumulate phase change across 12 samples
|
static constexpr size_t SAMPLES_ACCUMULATOR = SAMPLES_PER_SYMBOL / 5; // Accumulate phase change across 12 samples
|
||||||
static constexpr size_t RISE_FILTER_SAMPLES = SAMPLES_PER_SYMBOL / 20; // Filter peaks of less than 3 samples
|
static constexpr size_t RISE_FILTER_SAMPLES = SAMPLES_PER_SYMBOL / 20; // Filter peaks of less than 3 samples
|
||||||
|
|
||||||
static constexpr size_t CARRIER_SAMPLES_THRESHOLD = 0.080f * SAMPLE_RATE; // Carrier before frame lasts 160ms, require at least 80ms
|
static constexpr size_t CARRIER_SAMPLES_THRESHOLD = 0.080f * SAMPLE_RATE; // Carrier before frame lasts 160ms, require at least 80ms
|
||||||
static constexpr size_t CARRIER_MAX_SAMPLES = 0.900f * SAMPLE_RATE; // Carrier + frame lasts 160ms + 520ms + 100ms post carrier = 880ms
|
static constexpr size_t CARRIER_MAX_SAMPLES = 0.900f * SAMPLE_RATE; // Carrier + frame lasts 160ms + 520ms + 100ms post carrier = 880ms
|
||||||
static constexpr size_t FRAME_MAX_SAMPLES = SAMPLES_PER_BIT * (144 * 1.1f); // Frame max length (add 1% error margin)
|
static constexpr size_t FRAME_MAX_SAMPLES = SAMPLES_PER_BIT * (144 * 1.1f); // Frame max length (add 1% error margin)
|
||||||
|
|
||||||
AudioOutput audio_output{};
|
AudioOutput audio_output{};
|
||||||
|
|
||||||
// Config
|
// Config
|
||||||
uint8_t squelch_level{50};
|
uint8_t squelch_level{50};
|
||||||
// Audio on/off logic is disabled to save app space
|
// Audio on/off logic is disabled to save app space
|
||||||
// bool audio_on{true};
|
// bool audio_on{true};
|
||||||
#ifdef SPECAN
|
#ifdef SPECAN
|
||||||
bool spectrum_on{false};
|
bool spectrum_on{false};
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
std::array<float, 32> audio{};
|
std::array<float, 32> audio{};
|
||||||
const buffer_f32_t audio_buffer{
|
const buffer_f32_t audio_buffer{
|
||||||
audio.data(),
|
audio.data(),
|
||||||
audio.size()};
|
audio.size()};
|
||||||
|
|
||||||
// Last received bit (for manchester deconding)
|
// Last received bit (for manchester deconding)
|
||||||
bool last_bit = false;
|
bool last_bit = false;
|
||||||
// Sample count since last symbol
|
// Sample count since last symbol
|
||||||
uint16_t sample_count{0};
|
uint16_t sample_count{0};
|
||||||
// Sample count since frame start
|
// Sample count since frame start
|
||||||
uint16_t frame_sample_count{0};
|
uint16_t frame_sample_count{0};
|
||||||
// True if last phase shift was positive, false otherwise
|
// True if last phase shift was positive, false otherwise
|
||||||
bool last_phase_positive = false;
|
bool last_phase_positive = false;
|
||||||
// Counter used for peak filtering
|
// Counter used for peak filtering
|
||||||
uint16_t rise_detection_count{0};
|
uint16_t rise_detection_count{0};
|
||||||
|
|
||||||
// Frame detection state machine states
|
// Frame detection state machine states
|
||||||
enum State { IDLE,
|
enum State { IDLE,
|
||||||
CARRIER_LOCKED,
|
CARRIER_LOCKED,
|
||||||
DATA_SYNC,
|
DATA_SYNC,
|
||||||
POST_FRAME };
|
POST_FRAME };
|
||||||
// Current state for frame detection state machine
|
// Current state for frame detection state machine
|
||||||
State current_state = IDLE;
|
State current_state = IDLE;
|
||||||
// Carrier detection counter
|
// Carrier detection counter
|
||||||
uint32_t stability_counter = 0;
|
uint32_t stability_counter = 0;
|
||||||
|
|
||||||
// Phase delta accumulator (6 samples)
|
// Phase delta accumulator (12 samples)
|
||||||
static constexpr size_t PHASE_DELTA_ACC_SIZE = SAMPLES_ACCUMUMLATOR;
|
static constexpr size_t PHASE_DELTA_ACC_SIZE = SAMPLES_PER_SYMBOL / 5; // 12 samples
|
||||||
float phase_delta_buffer[PHASE_DELTA_ACC_SIZE] = {0.0f};
|
float phase_delta_buffer[PHASE_DELTA_ACC_SIZE] = {0.0f};
|
||||||
size_t pahse_delta_index = 0;
|
size_t phase_delta_index = 0;
|
||||||
float phase_delta_acc = 0.0f;
|
float phase_delta_acc = 0.0f;
|
||||||
|
|
||||||
std::array<complex16_t, 512> dst{};
|
// Automatic Frequency Control (AFC)
|
||||||
const buffer_c16_t dst_buffer{
|
// A residual carrier frequency offset between the tuner and the beacon shows
|
||||||
dst.data(),
|
// up as a constant per-sample phase rotation. We measure its mean over the
|
||||||
dst.size()};
|
// unmodulated carrier preamble and subtract it from every raw phase delta so
|
||||||
|
// the carrier-stability detection and the +/-2.2 rad data jumps stay centered.
|
||||||
// Decimation chain for 406 MHz EPIRB signal processing
|
// Current estimate (rad/sample), removed from each raw phase delta.
|
||||||
dsp::decimate::FIRC8xR16x24FS4Decim8 decim_0{};
|
float freq_offset_est = 0.0f;
|
||||||
dsp::decimate::FIRC16xR16x32Decim8 decim_1{};
|
// Carrier-tracking loop gain. Applied per sample in IDLE so the estimate
|
||||||
// Audio filtering
|
// pulls in any offset within the discriminator's +/-SAMPLE_RATE/2 (~24 kHz)
|
||||||
dsp::decimate::FIRAndDecimateComplex channel_filter{};
|
// range *before* the carrier-detection thresholds run. First-order loop with
|
||||||
// Audio demodulation
|
// time constant ~1/ALPHA samples (= 200 samples ~ 4 ms at 48 kHz). Tuned so a
|
||||||
dsp::demodulate::FM demod{};
|
// +/-5 kHz offset (0.654 rad/sample) decays the 12-sample accumulator below
|
||||||
#ifdef SPECAN
|
// the 0.6 rad lock threshold in ~11 ms (~13 ms at 7 kHz) -- well inside the
|
||||||
SpectrumCollector channel_spectrum{};
|
// 160 ms preamble / 80 ms stability window, even if reception starts partway
|
||||||
#endif
|
// through the carrier -- while keeping added acquisition jitter negligible.
|
||||||
// Store last stample for phase delta calculation
|
static constexpr float AFC_TRACK_ALPHA = 0.005f;
|
||||||
complex16_t last_sample{};
|
// AFC update gating: ignore large per-sample phase jumps (likely noise)
|
||||||
|
static constexpr float AFC_UPDATE_PHASE_MAX = 0.8f; // rad/sample
|
||||||
// EPIRB packet structure:
|
// AFC Convergence detection: track variance of AFC estimate to ensure it has stabilized
|
||||||
// - Sync pattern: 111111111111111 (15 bits)
|
// before transitioning from IDLE to CARRIER_LOCKED state
|
||||||
// - Frame sync: 000101111(real) / 011010000(test) (9 bits)
|
static constexpr float AFC_CONVERGENCE_THRESHOLD = 0.001f; // Max variance for convergence
|
||||||
// - Data: 120 bits (long frame) / // bits (short frame)
|
float afc_mean = 0.0f; // Running mean of AFC estimate
|
||||||
// - BCH error correction: 10 bits
|
float afc_m2 = 0.0f; // Sum of squared differences (Welford's algorithm)
|
||||||
// Total: 144 bits (long frame) / 112 bits (short frame)
|
uint32_t afc_convergence_n = 0; // Sample count for AFC convergence calculation
|
||||||
EPIRBPacketBuilder packet_builder{
|
// Running mean of the raw phase delta while a stable carrier is present.
|
||||||
this,
|
float carrier_phase_sum = 0.0f;
|
||||||
[](void* ctx, const baseband::Packet& p) {
|
uint32_t carrier_phase_n = 0;
|
||||||
static_cast<EPIRBProcessor*>(ctx)->payload_handler(p);
|
|
||||||
}};
|
std::array<complex16_t, 512> dst{};
|
||||||
|
const buffer_c16_t dst_buffer{
|
||||||
void payload_handler(const baseband::Packet& packet);
|
dst.data(),
|
||||||
// Compute phase diff between two samples
|
dst.size()};
|
||||||
float get_phase_diff(const complex16_t& sample0, const complex16_t& sample1);
|
|
||||||
// End current frame
|
// Decimation chain for 406 MHz EPIRB signal processing
|
||||||
void frame_end();
|
dsp::decimate::FIRC8xR16x24FS4Decim8 decim_0{};
|
||||||
// Rise detection with peak filtering
|
dsp::decimate::FIRC16xR16x32Decim8 decim_1{};
|
||||||
bool filtered_rise_detect(bool condition);
|
// Audio filtering
|
||||||
// Configure audio processing
|
dsp::decimate::FIRAndDecimateComplex channel_filter{};
|
||||||
void configure_audio();
|
// Audio demodulation
|
||||||
|
dsp::demodulate::FM demod{};
|
||||||
/* NB: Threads should be the last members in the class definition. */
|
#ifdef SPECAN
|
||||||
BasebandThread baseband_thread{
|
SpectrumCollector channel_spectrum{};
|
||||||
BASEBAND_SAMPLE_RATE, this, baseband::Direction::Receive, /*auto_start*/ false};
|
#endif
|
||||||
RSSIThread rssi_thread{};
|
// Store last stample for phase delta calculation
|
||||||
};
|
complex16_t last_sample{};
|
||||||
|
|
||||||
#endif /*__PROC_EPIRB_H__*/
|
// EPIRB packet structure:
|
||||||
|
// - Sync pattern: 111111111111111 (15 bits)
|
||||||
|
// - Frame sync: 000101111(real) / 011010000(test) (9 bits)
|
||||||
|
// - Data: 120 bits (long frame) / // bits (short frame)
|
||||||
|
// - BCH error correction: 10 bits
|
||||||
|
// Total: 144 bits (long frame) / 112 bits (short frame)
|
||||||
|
EPIRBPacketBuilder packet_builder{
|
||||||
|
this,
|
||||||
|
[](void* ctx, const baseband::Packet& p) {
|
||||||
|
static_cast<EPIRBProcessor*>(ctx)->payload_handler(p);
|
||||||
|
}};
|
||||||
|
|
||||||
|
void payload_handler(const baseband::Packet& packet);
|
||||||
|
// Compute phase diff between two samples
|
||||||
|
float get_phase_diff(const complex16_t& sample0, const complex16_t& sample1);
|
||||||
|
// End current frame
|
||||||
|
void frame_end();
|
||||||
|
// Rise detection with peak filtering
|
||||||
|
bool filtered_rise_detect(bool condition);
|
||||||
|
// Configure audio processing
|
||||||
|
void configure_audio();
|
||||||
|
|
||||||
|
/* NB: Threads should be the last members in the class definition. */
|
||||||
|
BasebandThread baseband_thread{
|
||||||
|
BASEBAND_SAMPLE_RATE, this, baseband::Direction::Receive, /*auto_start*/ false};
|
||||||
|
RSSIThread rssi_thread{};
|
||||||
|
};
|
||||||
|
|
||||||
|
#endif /*__PROC_EPIRB_H__*/
|
||||||
|
|||||||
@@ -21,6 +21,7 @@
|
|||||||
|
|
||||||
#include "doctest.h"
|
#include "doctest.h"
|
||||||
#include "convert.hpp"
|
#include "convert.hpp"
|
||||||
|
#include <cstdint>
|
||||||
#include <string>
|
#include <string>
|
||||||
#include <string_view>
|
#include <string_view>
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user