mirror of
https://github.com/portapack-mayhem/mayhem-firmware.git
synced 2026-09-11 17:19:29 +00:00
add oscilloscope (time sink) app (#3059)
* POC * submodule * seperate bb * seperate bb * seperate bb * seperate bb * seperate bb * sadly have to reduce history * icon * bring it back * bring it back * bb * format and move bb outside
This commit is contained in:
@@ -334,12 +334,22 @@ void set_rds_data(const uint16_t message_length) {
|
|||||||
send_message(&message);
|
send_message(&message);
|
||||||
}
|
}
|
||||||
|
|
||||||
void set_spectrum(const size_t sampling_rate, const size_t trigger) {
|
void set_spectrum(
|
||||||
|
const size_t sampling_rate,
|
||||||
|
const size_t trigger) {
|
||||||
const WidebandSpectrumConfigMessage message{
|
const WidebandSpectrumConfigMessage message{
|
||||||
sampling_rate, trigger};
|
sampling_rate, trigger};
|
||||||
send_message(&message);
|
send_message(&message);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void set_time_sink(
|
||||||
|
const size_t sampling_rate,
|
||||||
|
const size_t trigger) {
|
||||||
|
const TimeSinkConfigMessage message{
|
||||||
|
sampling_rate, trigger};
|
||||||
|
send_message(&message);
|
||||||
|
}
|
||||||
|
|
||||||
void set_wefax_config(uint8_t lpm = 120, uint8_t ioc = 0) {
|
void set_wefax_config(uint8_t lpm = 120, uint8_t ioc = 0) {
|
||||||
const WeFaxRxConfigureMessage message{lpm, ioc};
|
const WeFaxRxConfigureMessage message{lpm, ioc};
|
||||||
send_message(&message);
|
send_message(&message);
|
||||||
|
|||||||
@@ -97,7 +97,12 @@ void set_pocsag(int8_t baud_config = -1);
|
|||||||
void set_adsb();
|
void set_adsb();
|
||||||
void set_jammer(const bool run, const jammer::JammerType type, const uint32_t speed);
|
void set_jammer(const bool run, const jammer::JammerType type, const uint32_t speed);
|
||||||
void set_rds_data(const uint16_t message_length);
|
void set_rds_data(const uint16_t message_length);
|
||||||
void set_spectrum(const size_t sampling_rate, const size_t trigger);
|
void set_spectrum(
|
||||||
|
const size_t sampling_rate,
|
||||||
|
const size_t trigger);
|
||||||
|
void set_time_sink(
|
||||||
|
const size_t sampling_rate,
|
||||||
|
const size_t trigger);
|
||||||
void set_siggen_tone(const uint32_t tone);
|
void set_siggen_tone(const uint32_t tone);
|
||||||
void set_siggen_config(const uint32_t bw, const uint32_t shape, const uint32_t duration);
|
void set_siggen_config(const uint32_t bw, const uint32_t shape, const uint32_t duration);
|
||||||
void set_spectrum_painter_config(const uint16_t width, const uint16_t height, bool update, int32_t bw);
|
void set_spectrum_painter_config(const uint16_t width, const uint16_t height, bool update, int32_t bw);
|
||||||
|
|||||||
+6
-1
@@ -312,6 +312,10 @@ set(EXTCPPSRC
|
|||||||
#pocsag_tx
|
#pocsag_tx
|
||||||
external/pocsag_tx/main.cpp
|
external/pocsag_tx/main.cpp
|
||||||
external/pocsag_tx/ui_pocsag_tx.cpp
|
external/pocsag_tx/ui_pocsag_tx.cpp
|
||||||
|
|
||||||
|
#time_sink
|
||||||
|
external/time_sink/main.cpp
|
||||||
|
external/time_sink/ui_time_sink.cpp
|
||||||
)
|
)
|
||||||
|
|
||||||
set(EXTAPPLIST
|
set(EXTAPPLIST
|
||||||
@@ -386,10 +390,11 @@ set(EXTAPPLIST
|
|||||||
siggen
|
siggen
|
||||||
morse_radio
|
morse_radio
|
||||||
morseradiotx
|
morseradiotx
|
||||||
keeloqtx
|
keeloqtx
|
||||||
rtty_rx
|
rtty_rx
|
||||||
rtty_tx
|
rtty_tx
|
||||||
pocsag_tx
|
pocsag_tx
|
||||||
|
time_sink
|
||||||
)
|
)
|
||||||
|
|
||||||
# sdusb has type conflicts with PRALINE (HackRF Pro) - add only for non-PRALINE builds
|
# sdusb has type conflicts with PRALINE (HackRF Pro) - add only for non-PRALINE builds
|
||||||
|
|||||||
+8
@@ -99,6 +99,8 @@ MEMORY
|
|||||||
ram_external_app_rtty_tx (rwx) : org = 0xADFA0000, len = 32k
|
ram_external_app_rtty_tx (rwx) : org = 0xADFA0000, len = 32k
|
||||||
ram_external_app_tpmstx (rwx) : org = 0xADFB0000, len = 32k
|
ram_external_app_tpmstx (rwx) : org = 0xADFB0000, len = 32k
|
||||||
ram_external_app_pocsag_tx (rwx) : org = 0xADFC0000, len = 32k
|
ram_external_app_pocsag_tx (rwx) : org = 0xADFC0000, len = 32k
|
||||||
|
ram_external_app_time_sink (rwx) : org = 0xADFD0000, len = 32k
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
SECTIONS
|
SECTIONS
|
||||||
@@ -559,5 +561,11 @@ SECTIONS
|
|||||||
KEEP(*(.external_app.app_pocsag_tx.application_information));
|
KEEP(*(.external_app.app_pocsag_tx.application_information));
|
||||||
*(*ui*external_app*pocsag_tx*);
|
*(*ui*external_app*pocsag_tx*);
|
||||||
} > ram_external_app_pocsag_tx
|
} > ram_external_app_pocsag_tx
|
||||||
|
|
||||||
|
.external_app_time_sink : ALIGN(4) SUBALIGN(4)
|
||||||
|
{
|
||||||
|
KEEP(*(.external_app.app_time_sink.application_information));
|
||||||
|
*(*ui*external_app*time_sink*);
|
||||||
|
} > ram_external_app_time_sink
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,82 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (C) 2025 Bernd Herzog
|
||||||
|
*
|
||||||
|
* This file is part of PortaPack.
|
||||||
|
*
|
||||||
|
* 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
|
||||||
|
* the Free Software Foundation; either version 2, or (at your option)
|
||||||
|
* any later version.
|
||||||
|
*
|
||||||
|
* This program is distributed in the hope that it will be useful,
|
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
* GNU General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU General Public License
|
||||||
|
* along with this program; see the file COPYING. If not, write to
|
||||||
|
* the Free Software Foundation, Inc., 51 Franklin Street,
|
||||||
|
* Boston, MA 02110-1301, USA.
|
||||||
|
*/
|
||||||
|
|
||||||
|
#include "ui.hpp"
|
||||||
|
#include "ui_time_sink.hpp"
|
||||||
|
#include "ui_navigation.hpp"
|
||||||
|
#include "external_app.hpp"
|
||||||
|
|
||||||
|
namespace ui::external_app::time_sink {
|
||||||
|
void initialize_app(ui::NavigationView& nav) {
|
||||||
|
nav.push<TimeSinkView>();
|
||||||
|
}
|
||||||
|
} // namespace ui::external_app::time_sink
|
||||||
|
extern "C" {
|
||||||
|
|
||||||
|
__attribute__((section(".external_app.app_time_sink.application_information"), used)) application_information_t _application_information_time_sink = {
|
||||||
|
/*.memory_location = */ (uint8_t*)0x00000000,
|
||||||
|
/*.externalAppEntry = */ ui::external_app::time_sink::initialize_app,
|
||||||
|
/*.header_version = */ CURRENT_HEADER_VERSION,
|
||||||
|
/*.app_version = */ VERSION_MD5,
|
||||||
|
|
||||||
|
/*.app_name = */ "Time Sink",
|
||||||
|
/*.bitmap_data = */ {
|
||||||
|
0x18,
|
||||||
|
0x00,
|
||||||
|
0x24,
|
||||||
|
0x00,
|
||||||
|
0x42,
|
||||||
|
0x00,
|
||||||
|
0x42,
|
||||||
|
0x00,
|
||||||
|
0x42,
|
||||||
|
0x00,
|
||||||
|
0x42,
|
||||||
|
0x00,
|
||||||
|
0x81,
|
||||||
|
0x00,
|
||||||
|
0xAB,
|
||||||
|
0x6A,
|
||||||
|
0x80,
|
||||||
|
0x40,
|
||||||
|
0x00,
|
||||||
|
0x21,
|
||||||
|
0x00,
|
||||||
|
0x21,
|
||||||
|
0x00,
|
||||||
|
0x21,
|
||||||
|
0x00,
|
||||||
|
0x21,
|
||||||
|
0x00,
|
||||||
|
0x12,
|
||||||
|
0x00,
|
||||||
|
0x0C,
|
||||||
|
0x00,
|
||||||
|
0x00,
|
||||||
|
},
|
||||||
|
/*.icon_color = */ ui::Color::cyan().v,
|
||||||
|
/*.menu_location = */ app_location_t::RX,
|
||||||
|
/*.desired_menu_position = */ -1,
|
||||||
|
|
||||||
|
/*.m4_app_tag = portapack::spi_flash::image_tag_none */ {'P', 'T', 'S', 'K'},
|
||||||
|
/*.m4_app_offset = */ 0x00000000, // will be filled at compile time
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -0,0 +1,352 @@
|
|||||||
|
/*
|
||||||
|
* Copyleft zxkmm (>) 2026
|
||||||
|
*
|
||||||
|
* This file is part of PortaPack.
|
||||||
|
*
|
||||||
|
* 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
|
||||||
|
* the Free Software Foundation; either version 2, or (at your option)
|
||||||
|
* any later version.
|
||||||
|
*
|
||||||
|
* This program is distributed in the hope that it will be useful,
|
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
* GNU General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU General Public License
|
||||||
|
* along with this program; see the file COPYING. If not, write to
|
||||||
|
* the Free Software Foundation, Inc., 51 Franklin Street,
|
||||||
|
* Boston, MA 02110-1301, USA.
|
||||||
|
*/
|
||||||
|
|
||||||
|
#include "ui_time_sink.hpp"
|
||||||
|
|
||||||
|
#include "baseband_api.hpp"
|
||||||
|
#include "portapack.hpp"
|
||||||
|
#include "ui_spectrum.hpp"
|
||||||
|
|
||||||
|
#include <algorithm>
|
||||||
|
|
||||||
|
using namespace portapack;
|
||||||
|
|
||||||
|
namespace ui::external_app::time_sink {
|
||||||
|
|
||||||
|
TimeSinkWaveformWidget::TimeSinkWaveformWidget(
|
||||||
|
Rect parent_rect,
|
||||||
|
const int16_t* data,
|
||||||
|
size_t length,
|
||||||
|
Color color)
|
||||||
|
: Widget{parent_rect},
|
||||||
|
data_{data},
|
||||||
|
length_{length},
|
||||||
|
color_{color} {
|
||||||
|
reset_cache();
|
||||||
|
}
|
||||||
|
|
||||||
|
void TimeSinkWaveformWidget::set_parent_rect(const Rect new_parent_rect) {
|
||||||
|
Widget::set_parent_rect(new_parent_rect);
|
||||||
|
reset_cache();
|
||||||
|
}
|
||||||
|
|
||||||
|
void TimeSinkWaveformWidget::on_show() {
|
||||||
|
reset_cache();
|
||||||
|
set_dirty();
|
||||||
|
}
|
||||||
|
|
||||||
|
void TimeSinkWaveformWidget::reset_cache() {
|
||||||
|
history_count_ = 0;
|
||||||
|
history_head_ = 0;
|
||||||
|
needs_clear_ = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
void TimeSinkWaveformWidget::set_persistence_frames(uint8_t frames) {
|
||||||
|
const auto clamped = static_cast<uint8_t>(std::clamp<size_t>(
|
||||||
|
frames,
|
||||||
|
1,
|
||||||
|
max_persistence_frames));
|
||||||
|
if (clamped != persistence_frames_) {
|
||||||
|
persistence_frames_ = clamped;
|
||||||
|
reset_cache();
|
||||||
|
set_dirty();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Coord TimeSinkWaveformWidget::sample_to_y(const Rect& r, int16_t sample) const {
|
||||||
|
const int32_t y_center = r.top() + (r.height() / 2);
|
||||||
|
const int32_t y_span = std::max<int32_t>(1, r.height() - 1);
|
||||||
|
const int32_t y = y_center - (static_cast<int32_t>(sample) * y_span) / 65536;
|
||||||
|
return static_cast<Coord>(std::clamp<int32_t>(y, r.top(), r.bottom() - 1));
|
||||||
|
}
|
||||||
|
|
||||||
|
void TimeSinkWaveformWidget::paint(Painter& painter) {
|
||||||
|
const auto r = screen_rect();
|
||||||
|
const auto background = Theme::getInstance()->bg_darkest->background;
|
||||||
|
|
||||||
|
if (!r || !data_ || !length_) {
|
||||||
|
if (needs_clear_) {
|
||||||
|
painter.fill_rectangle_unrolled8(r, background);
|
||||||
|
needs_clear_ = false;
|
||||||
|
}
|
||||||
|
history_count_ = 0;
|
||||||
|
history_head_ = 0;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const size_t columns = std::min<size_t>({
|
||||||
|
length_,
|
||||||
|
max_columns,
|
||||||
|
static_cast<size_t>(r.width()),
|
||||||
|
});
|
||||||
|
if (!columns) {
|
||||||
|
history_count_ = 0;
|
||||||
|
history_head_ = 0;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (needs_clear_) {
|
||||||
|
painter.fill_rectangle_unrolled8(r, background);
|
||||||
|
needs_clear_ = false;
|
||||||
|
history_count_ = 0;
|
||||||
|
history_head_ = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (size_t x = 0; x < columns; ++x) {
|
||||||
|
const size_t src_index = (x * length_) / columns;
|
||||||
|
current_y_[x] = sample_to_y(r, data_[src_index]);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Draw first, then erase stale pixels so the trace never disappears mid-refresh.
|
||||||
|
for (size_t x = 0; x < columns; ++x) {
|
||||||
|
display.draw_pixel(
|
||||||
|
{static_cast<Coord>(r.left() + x), current_y_[x]},
|
||||||
|
color_);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (history_count_ >= persistence_frames_) {
|
||||||
|
const size_t expired_slot = history_head_;
|
||||||
|
|
||||||
|
for (size_t x = 0; x < columns; ++x) {
|
||||||
|
const auto expired_y = history_y_[expired_slot][x];
|
||||||
|
bool keep = (expired_y == current_y_[x]);
|
||||||
|
|
||||||
|
if (!keep) {
|
||||||
|
for (size_t i = 1; i < history_count_; ++i) {
|
||||||
|
const size_t slot = (history_head_ + i) % max_persistence_frames;
|
||||||
|
if (history_y_[slot][x] == expired_y) {
|
||||||
|
keep = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!keep) {
|
||||||
|
display.draw_pixel(
|
||||||
|
{static_cast<Coord>(r.left() + x), expired_y},
|
||||||
|
background);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
history_head_ = (history_head_ + 1) % max_persistence_frames;
|
||||||
|
--history_count_;
|
||||||
|
}
|
||||||
|
|
||||||
|
const size_t tail_slot = (history_head_ + history_count_) % max_persistence_frames;
|
||||||
|
std::copy_n(current_y_.begin(), columns, history_y_[tail_slot].begin());
|
||||||
|
++history_count_;
|
||||||
|
}
|
||||||
|
|
||||||
|
TimeSinkView::TimeSinkView(NavigationView& nav)
|
||||||
|
: nav_(nav) {
|
||||||
|
baseband::run_prepared_image(portapack::memory::map::m4_code.base());
|
||||||
|
|
||||||
|
add_children({
|
||||||
|
&labels,
|
||||||
|
&field_frequency,
|
||||||
|
&field_frequency_step,
|
||||||
|
&field_rf_amp,
|
||||||
|
&field_lna,
|
||||||
|
&field_vga,
|
||||||
|
&options_sample_rate,
|
||||||
|
&field_trigger,
|
||||||
|
&options_persistence,
|
||||||
|
&options_trigger_mode,
|
||||||
|
&field_trigger_level,
|
||||||
|
&waveform,
|
||||||
|
});
|
||||||
|
|
||||||
|
field_frequency_step.set_by_value(receiver_model.frequency_step());
|
||||||
|
field_frequency_step.on_change = [this](size_t, OptionsField::value_t v) {
|
||||||
|
receiver_model.set_frequency_step(v);
|
||||||
|
field_frequency.set_step(v);
|
||||||
|
};
|
||||||
|
|
||||||
|
options_sample_rate.set_by_nearest_value(sampling_rate);
|
||||||
|
sampling_rate = options_sample_rate.selected_index_value();
|
||||||
|
options_sample_rate.on_change = [this](size_t, OptionsField::value_t v) {
|
||||||
|
sampling_rate = v;
|
||||||
|
apply_spectrum_config();
|
||||||
|
};
|
||||||
|
|
||||||
|
field_trigger.set_value(trigger);
|
||||||
|
field_trigger.on_change = [this](int32_t v) {
|
||||||
|
trigger = static_cast<uint8_t>(v);
|
||||||
|
apply_spectrum_config();
|
||||||
|
};
|
||||||
|
|
||||||
|
options_persistence.set_by_nearest_value(persistence_frames);
|
||||||
|
persistence_frames = options_persistence.selected_index_value();
|
||||||
|
waveform.set_persistence_frames(persistence_frames);
|
||||||
|
options_persistence.on_change = [this](size_t, OptionsField::value_t v) {
|
||||||
|
persistence_frames = static_cast<uint8_t>(v);
|
||||||
|
waveform.set_persistence_frames(persistence_frames);
|
||||||
|
};
|
||||||
|
|
||||||
|
options_trigger_mode.set_by_nearest_value(trigger_mode);
|
||||||
|
trigger_mode = static_cast<uint8_t>(options_trigger_mode.selected_index_value());
|
||||||
|
options_trigger_mode.on_change = [this](size_t, OptionsField::value_t v) {
|
||||||
|
trigger_mode = static_cast<uint8_t>(v);
|
||||||
|
trigger_lock_valid = false;
|
||||||
|
};
|
||||||
|
|
||||||
|
field_trigger_level.set_value(trigger_level);
|
||||||
|
field_trigger_level.on_change = [this](int32_t v) {
|
||||||
|
trigger_level = v;
|
||||||
|
trigger_lock_valid = false;
|
||||||
|
};
|
||||||
|
|
||||||
|
receiver_model.set_squelch_level(0);
|
||||||
|
receiver_model.enable();
|
||||||
|
apply_spectrum_config();
|
||||||
|
}
|
||||||
|
|
||||||
|
TimeSinkView::~TimeSinkView() {
|
||||||
|
receiver_model.disable();
|
||||||
|
baseband::shutdown();
|
||||||
|
}
|
||||||
|
|
||||||
|
void TimeSinkView::focus() {
|
||||||
|
field_frequency.focus();
|
||||||
|
}
|
||||||
|
|
||||||
|
void TimeSinkView::on_show() {
|
||||||
|
baseband::spectrum_streaming_start();
|
||||||
|
}
|
||||||
|
|
||||||
|
void TimeSinkView::on_hide() {
|
||||||
|
baseband::spectrum_streaming_stop();
|
||||||
|
}
|
||||||
|
|
||||||
|
void TimeSinkView::set_parent_rect(const Rect new_parent_rect) {
|
||||||
|
View::set_parent_rect(new_parent_rect);
|
||||||
|
waveform.set_parent_rect({0, header_height, new_parent_rect.width(), new_parent_rect.height() - header_height});
|
||||||
|
}
|
||||||
|
|
||||||
|
void TimeSinkView::apply_spectrum_config() {
|
||||||
|
receiver_model.set_sampling_rate(sampling_rate);
|
||||||
|
receiver_model.set_baseband_bandwidth(filter_bandwidth_for_sampling_rate(sampling_rate));
|
||||||
|
|
||||||
|
baseband::set_time_sink(
|
||||||
|
sampling_rate,
|
||||||
|
trigger);
|
||||||
|
}
|
||||||
|
|
||||||
|
size_t TimeSinkView::find_stable_trigger_index(const ChannelSpectrum& spectrum) {
|
||||||
|
if (spectrum.db.size() < 2) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
TriggerMode mode = TriggerMode::Rising;
|
||||||
|
switch (trigger_mode) {
|
||||||
|
case static_cast<uint8_t>(TriggerMode::Off):
|
||||||
|
mode = TriggerMode::Off;
|
||||||
|
break;
|
||||||
|
case static_cast<uint8_t>(TriggerMode::Rising):
|
||||||
|
mode = TriggerMode::Rising;
|
||||||
|
break;
|
||||||
|
case static_cast<uint8_t>(TriggerMode::Falling):
|
||||||
|
mode = TriggerMode::Falling;
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (mode == TriggerMode::Off) {
|
||||||
|
trigger_lock_valid = false;
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
constexpr int32_t hysteresis = 2;
|
||||||
|
const int32_t threshold = std::clamp<int32_t>(
|
||||||
|
128 + trigger_level,
|
||||||
|
hysteresis,
|
||||||
|
255 - hysteresis);
|
||||||
|
const size_t center = spectrum.db.size() / 2;
|
||||||
|
const size_t reference_index = trigger_lock_valid ? trigger_lock_index : center;
|
||||||
|
|
||||||
|
bool found = false;
|
||||||
|
size_t best_index = 0;
|
||||||
|
size_t best_distance = spectrum.db.size();
|
||||||
|
const auto circular_distance = [count = spectrum.db.size()](size_t a, size_t b) -> size_t {
|
||||||
|
const size_t linear =
|
||||||
|
(a > b) ? (a - b) : (b - a);
|
||||||
|
return std::min(linear, count - linear);
|
||||||
|
};
|
||||||
|
|
||||||
|
for (size_t i = 1; i < spectrum.db.size(); ++i) {
|
||||||
|
const int32_t prev = spectrum.db[i - 1];
|
||||||
|
const int32_t curr = spectrum.db[i];
|
||||||
|
|
||||||
|
bool crossing = false;
|
||||||
|
if (mode == TriggerMode::Rising) {
|
||||||
|
crossing =
|
||||||
|
(prev <= (threshold - hysteresis)) &&
|
||||||
|
(curr >= (threshold + hysteresis));
|
||||||
|
} else {
|
||||||
|
crossing =
|
||||||
|
(prev >= (threshold + hysteresis)) &&
|
||||||
|
(curr <= (threshold - hysteresis));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!crossing) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
const size_t distance = circular_distance(i, reference_index);
|
||||||
|
if (!found || (distance < best_distance)) {
|
||||||
|
found = true;
|
||||||
|
best_index = i;
|
||||||
|
best_distance = distance;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (found) {
|
||||||
|
trigger_lock_index = best_index;
|
||||||
|
trigger_lock_valid = true;
|
||||||
|
return best_index;
|
||||||
|
}
|
||||||
|
|
||||||
|
return trigger_lock_valid ? trigger_lock_index : 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
void TimeSinkView::on_channel_spectrum(const ChannelSpectrum& spectrum) {
|
||||||
|
const size_t trigger_index = find_stable_trigger_index(spectrum);
|
||||||
|
const size_t source_count = spectrum.db.size();
|
||||||
|
const size_t window_size = source_count;
|
||||||
|
|
||||||
|
for (size_t x = 0; x < waveform_points; x++) {
|
||||||
|
const size_t offset = (x * window_size) / waveform_points;
|
||||||
|
const size_t src_index =
|
||||||
|
(trigger_index + offset) % source_count;
|
||||||
|
const int32_t centered = static_cast<int32_t>(spectrum.db[src_index]) - 128;
|
||||||
|
const int32_t scaled = centered * 256;
|
||||||
|
waveform_buffer[x] = static_cast<int16_t>(std::clamp<int32_t>(scaled, -32768, 32767));
|
||||||
|
}
|
||||||
|
|
||||||
|
waveform.set_dirty();
|
||||||
|
}
|
||||||
|
|
||||||
|
void TimeSinkView::on_freqchg(int64_t freq) {
|
||||||
|
field_frequency.set_value(freq);
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace ui::external_app::time_sink
|
||||||
@@ -0,0 +1,238 @@
|
|||||||
|
/*
|
||||||
|
* Copyleft zxkmm (>) 2026
|
||||||
|
*
|
||||||
|
* This file is part of PortaPack.
|
||||||
|
*
|
||||||
|
* 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
|
||||||
|
* the Free Software Foundation; either version 2, or (at your option)
|
||||||
|
* any later version.
|
||||||
|
*
|
||||||
|
* This program is distributed in the hope that it will be useful,
|
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
* GNU General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU General Public License
|
||||||
|
* along with this program; see the file COPYING. If not, write to
|
||||||
|
* the Free Software Foundation, Inc., 51 Franklin Street,
|
||||||
|
* Boston, MA 02110-1301, USA.
|
||||||
|
*/
|
||||||
|
|
||||||
|
#ifndef __UI_TIME_SINK_APP_H__
|
||||||
|
#define __UI_TIME_SINK_APP_H__
|
||||||
|
|
||||||
|
#include "app_settings.hpp"
|
||||||
|
#include "baseband_api.hpp"
|
||||||
|
#include "message.hpp"
|
||||||
|
#include "radio_state.hpp"
|
||||||
|
#include "ui.hpp"
|
||||||
|
#include "ui_freq_field.hpp"
|
||||||
|
#include "ui_navigation.hpp"
|
||||||
|
#include "ui_receiver.hpp"
|
||||||
|
#include "ui_widget.hpp"
|
||||||
|
|
||||||
|
#include <array>
|
||||||
|
|
||||||
|
namespace ui::external_app::time_sink {
|
||||||
|
|
||||||
|
constexpr size_t time_sink_waveform_points = 240;
|
||||||
|
|
||||||
|
class TimeSinkWaveformWidget : public Widget {
|
||||||
|
public:
|
||||||
|
TimeSinkWaveformWidget(Rect parent_rect, const int16_t* data, size_t length, Color color);
|
||||||
|
TimeSinkWaveformWidget(const TimeSinkWaveformWidget&) = delete;
|
||||||
|
TimeSinkWaveformWidget(TimeSinkWaveformWidget&&) = delete;
|
||||||
|
TimeSinkWaveformWidget& operator=(const TimeSinkWaveformWidget&) = delete;
|
||||||
|
TimeSinkWaveformWidget& operator=(TimeSinkWaveformWidget&&) = delete;
|
||||||
|
|
||||||
|
void set_parent_rect(const Rect new_parent_rect) override;
|
||||||
|
void on_show() override;
|
||||||
|
void paint(Painter& painter) override;
|
||||||
|
void set_persistence_frames(uint8_t frames);
|
||||||
|
|
||||||
|
private:
|
||||||
|
static constexpr size_t max_columns = time_sink_waveform_points;
|
||||||
|
static constexpr size_t max_persistence_frames = 16; // this is sad that we cant have 32 histories in ext app due to memory constraints
|
||||||
|
|
||||||
|
void reset_cache();
|
||||||
|
Coord sample_to_y(const Rect& r, int16_t sample) const;
|
||||||
|
|
||||||
|
const int16_t* data_;
|
||||||
|
size_t length_;
|
||||||
|
Color color_;
|
||||||
|
std::array<Coord, max_columns> current_y_{};
|
||||||
|
std::array<std::array<Coord, max_columns>, max_persistence_frames> history_y_{};
|
||||||
|
size_t history_count_{0};
|
||||||
|
size_t history_head_{0};
|
||||||
|
uint8_t persistence_frames_{1};
|
||||||
|
bool needs_clear_{true};
|
||||||
|
};
|
||||||
|
|
||||||
|
class TimeSinkView : public View {
|
||||||
|
public:
|
||||||
|
TimeSinkView(NavigationView& nav);
|
||||||
|
~TimeSinkView();
|
||||||
|
|
||||||
|
TimeSinkView(const TimeSinkView&) = delete;
|
||||||
|
TimeSinkView(TimeSinkView&&) = delete;
|
||||||
|
TimeSinkView& operator=(const TimeSinkView&) = delete;
|
||||||
|
TimeSinkView& operator=(TimeSinkView&&) = delete;
|
||||||
|
|
||||||
|
std::string title() const override { return "Time Sink"; };
|
||||||
|
void focus() override;
|
||||||
|
void on_show() override;
|
||||||
|
void on_hide() override;
|
||||||
|
void set_parent_rect(const Rect new_parent_rect) override;
|
||||||
|
|
||||||
|
private:
|
||||||
|
enum class TriggerMode : uint8_t {
|
||||||
|
Off = 0,
|
||||||
|
Rising = 1,
|
||||||
|
Falling = 2,
|
||||||
|
};
|
||||||
|
|
||||||
|
static constexpr Dim header_height = 3 * 16;
|
||||||
|
static constexpr size_t waveform_points = time_sink_waveform_points;
|
||||||
|
|
||||||
|
NavigationView& nav_;
|
||||||
|
RxRadioState radio_state_{
|
||||||
|
433'920'000,
|
||||||
|
1'750'000,
|
||||||
|
2'000'000,
|
||||||
|
ReceiverModel::Mode::SpectrumAnalysis};
|
||||||
|
|
||||||
|
uint32_t sampling_rate{2'000'000};
|
||||||
|
uint8_t trigger{0};
|
||||||
|
uint8_t persistence_frames{1};
|
||||||
|
uint8_t trigger_mode{static_cast<uint8_t>(TriggerMode::Rising)};
|
||||||
|
int32_t trigger_level{0};
|
||||||
|
size_t trigger_lock_index{0};
|
||||||
|
bool trigger_lock_valid{false};
|
||||||
|
app_settings::SettingsManager settings_{
|
||||||
|
"rx_time_sink"sv,
|
||||||
|
app_settings::Mode::RX,
|
||||||
|
{
|
||||||
|
{"sampling_rate"sv, &sampling_rate},
|
||||||
|
{"trigger"sv, &trigger},
|
||||||
|
{"persistence_frames"sv, &persistence_frames},
|
||||||
|
{"trigger_mode"sv, &trigger_mode},
|
||||||
|
{"trigger_level"sv, &trigger_level},
|
||||||
|
}};
|
||||||
|
|
||||||
|
int16_t waveform_buffer[waveform_points]{0};
|
||||||
|
ChannelSpectrumFIFO* fifo = nullptr;
|
||||||
|
|
||||||
|
Labels labels{
|
||||||
|
{{UI_POS_X(0), UI_POS_Y(1)}, "SR:", Theme::getInstance()->fg_light->foreground},
|
||||||
|
{{UI_POS_X(11), UI_POS_Y(1)}, "DEC:", Theme::getInstance()->fg_light->foreground},
|
||||||
|
{{UI_POS_X(0), UI_POS_Y(2)}, "PST:", Theme::getInstance()->fg_light->foreground},
|
||||||
|
{{UI_POS_X(9), UI_POS_Y(2)}, "TRM:", Theme::getInstance()->fg_light->foreground},
|
||||||
|
{{UI_POS_X(19), UI_POS_Y(2)}, "LVL:", Theme::getInstance()->fg_light->foreground},
|
||||||
|
};
|
||||||
|
|
||||||
|
RxFrequencyField field_frequency{
|
||||||
|
{UI_POS_X(0), UI_POS_Y(0)},
|
||||||
|
nav_};
|
||||||
|
|
||||||
|
FrequencyStepView field_frequency_step{
|
||||||
|
{10 * 8, UI_POS_Y(0)}};
|
||||||
|
|
||||||
|
RFAmpField field_rf_amp{
|
||||||
|
{16 * 8, UI_POS_Y(0)}};
|
||||||
|
|
||||||
|
LNAGainField field_lna{
|
||||||
|
{18 * 8, UI_POS_Y(0)}};
|
||||||
|
|
||||||
|
VGAGainField field_vga{
|
||||||
|
{21 * 8, UI_POS_Y(0)}};
|
||||||
|
|
||||||
|
OptionsField options_sample_rate{
|
||||||
|
{UI_POS_X(3), UI_POS_Y(1)},
|
||||||
|
6,
|
||||||
|
{
|
||||||
|
{"1.0M ", 1'000'000},
|
||||||
|
{"2.0M ", 2'000'000},
|
||||||
|
{"5.0M ", 5'000'000},
|
||||||
|
{"10.0M ", 10'000'000},
|
||||||
|
{"20.0M ", 20'000'000},
|
||||||
|
}};
|
||||||
|
|
||||||
|
NumberField field_trigger{
|
||||||
|
{UI_POS_X(17), UI_POS_Y(1)},
|
||||||
|
3,
|
||||||
|
{0, 128},
|
||||||
|
1,
|
||||||
|
' '};
|
||||||
|
|
||||||
|
OptionsField options_persistence{
|
||||||
|
{UI_POS_X(3), UI_POS_Y(2)},
|
||||||
|
3,
|
||||||
|
{
|
||||||
|
{"1 ", 1},
|
||||||
|
{"2 ", 2},
|
||||||
|
{"4 ", 4},
|
||||||
|
{"8 ", 8},
|
||||||
|
{"16 ", 16},
|
||||||
|
}};
|
||||||
|
|
||||||
|
OptionsField options_trigger_mode{
|
||||||
|
{UI_POS_X(13), UI_POS_Y(2)},
|
||||||
|
4,
|
||||||
|
{
|
||||||
|
{"Off ", static_cast<int32_t>(TriggerMode::Off)},
|
||||||
|
{"Rise", static_cast<int32_t>(TriggerMode::Rising)},
|
||||||
|
{"Fall", static_cast<int32_t>(TriggerMode::Falling)},
|
||||||
|
}};
|
||||||
|
|
||||||
|
NumberField field_trigger_level{
|
||||||
|
{UI_POS_X(23), UI_POS_Y(2)},
|
||||||
|
4,
|
||||||
|
{-127, 127},
|
||||||
|
1,
|
||||||
|
' '};
|
||||||
|
|
||||||
|
TimeSinkWaveformWidget waveform{
|
||||||
|
{0, header_height, screen_width, screen_height - header_height},
|
||||||
|
waveform_buffer,
|
||||||
|
waveform_points,
|
||||||
|
Theme::getInstance()->fg_light->foreground};
|
||||||
|
|
||||||
|
MessageHandlerRegistration message_handler_spectrum_config{
|
||||||
|
Message::ID::ChannelSpectrumConfig,
|
||||||
|
[this](const Message* const p) {
|
||||||
|
const auto message = *reinterpret_cast<const ChannelSpectrumConfigMessage*>(p);
|
||||||
|
this->fifo = message.fifo;
|
||||||
|
}};
|
||||||
|
|
||||||
|
MessageHandlerRegistration message_handler_frame_sync{
|
||||||
|
Message::ID::DisplayFrameSync,
|
||||||
|
[this](const Message* const) {
|
||||||
|
if (this->fifo) {
|
||||||
|
ChannelSpectrum spectrum{};
|
||||||
|
bool has_spectrum = false;
|
||||||
|
while (fifo->out(spectrum)) {
|
||||||
|
has_spectrum = true;
|
||||||
|
}
|
||||||
|
if (has_spectrum) {
|
||||||
|
this->on_channel_spectrum(spectrum);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}};
|
||||||
|
|
||||||
|
MessageHandlerRegistration message_handler_freqchg{
|
||||||
|
Message::ID::FreqChangeCommand,
|
||||||
|
[this](Message* const p) {
|
||||||
|
const auto message = static_cast<const FreqChangeCommandMessage*>(p);
|
||||||
|
this->on_freqchg(message->freq);
|
||||||
|
}};
|
||||||
|
|
||||||
|
void apply_spectrum_config();
|
||||||
|
size_t find_stable_trigger_index(const ChannelSpectrum& spectrum);
|
||||||
|
void on_channel_spectrum(const ChannelSpectrum& spectrum);
|
||||||
|
void on_freqchg(int64_t freq);
|
||||||
|
};
|
||||||
|
|
||||||
|
} // namespace ui::external_app::time_sink
|
||||||
|
|
||||||
|
#endif // __UI_TIME_SINK_APP_H__
|
||||||
@@ -467,6 +467,7 @@ set(MODE_CPPSRC
|
|||||||
)
|
)
|
||||||
DeclareTargets(PSPE wideband_spectrum)
|
DeclareTargets(PSPE wideband_spectrum)
|
||||||
|
|
||||||
|
|
||||||
### WFM Audio
|
### WFM Audio
|
||||||
|
|
||||||
set(MODE_CPPSRC
|
set(MODE_CPPSRC
|
||||||
@@ -705,6 +706,13 @@ set(MODE_CPPSRC
|
|||||||
)
|
)
|
||||||
DeclareTargets(PMRT morsetx)
|
DeclareTargets(PMRT morsetx)
|
||||||
|
|
||||||
|
### Time Sink
|
||||||
|
|
||||||
|
set(MODE_CPPSRC
|
||||||
|
proc_time_sink.cpp
|
||||||
|
)
|
||||||
|
DeclareTargets(PTSK time_sink)
|
||||||
|
|
||||||
set(MODE_CPPSRC
|
set(MODE_CPPSRC
|
||||||
sd_over_usb/proc_sd_over_usb.cpp
|
sd_over_usb/proc_sd_over_usb.cpp
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,128 @@
|
|||||||
|
/*
|
||||||
|
* Copyleft zxkmm (>) 2026
|
||||||
|
*
|
||||||
|
* This file is part of PortaPack.
|
||||||
|
*
|
||||||
|
* 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
|
||||||
|
* the Free Software Foundation; either version 2, or (at your option)
|
||||||
|
* any later version.
|
||||||
|
*
|
||||||
|
* This program is distributed in the hope that it will be useful,
|
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
* GNU General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU General Public License
|
||||||
|
* along with this program; see the file COPYING. If not, write to
|
||||||
|
* the Free Software Foundation, Inc., 51 Franklin Street,
|
||||||
|
* Boston, MA 02110-1301, USA.
|
||||||
|
*/
|
||||||
|
|
||||||
|
#include "proc_time_sink.hpp"
|
||||||
|
|
||||||
|
#include "event_m4.hpp"
|
||||||
|
#include "portapack_shared_memory.hpp"
|
||||||
|
|
||||||
|
#include <algorithm>
|
||||||
|
#include <cstddef>
|
||||||
|
#include <cstdint>
|
||||||
|
#include <memory>
|
||||||
|
|
||||||
|
void TimeSinkProcessor::execute(const buffer_c8_t& buffer) {
|
||||||
|
// 2048 complex8_t samples per buffer.
|
||||||
|
// 102.4us per buffer. 20480 instruction cycles per buffer.
|
||||||
|
|
||||||
|
if (!configured) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
execute_time_domain(buffer);
|
||||||
|
}
|
||||||
|
|
||||||
|
void TimeSinkProcessor::execute_time_domain(const buffer_c8_t& buffer) {
|
||||||
|
if (!time_streaming || buffer.count == 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (phase < trigger) {
|
||||||
|
phase++;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
phase = 0;
|
||||||
|
|
||||||
|
if (time_domain_request_update) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const size_t stride = std::max<size_t>(1, buffer.count / time_domain_spectrum.db.size());
|
||||||
|
time_domain_spectrum.sampling_rate = buffer.sampling_rate / stride;
|
||||||
|
time_domain_spectrum.channel_filter_low_frequency = 0;
|
||||||
|
time_domain_spectrum.channel_filter_high_frequency = 0;
|
||||||
|
time_domain_spectrum.channel_filter_transition = 0;
|
||||||
|
|
||||||
|
for (size_t i = 0; i < time_domain_spectrum.db.size(); i++) {
|
||||||
|
const size_t sample_index = std::min(i * stride, buffer.count - 1);
|
||||||
|
const int32_t normalized = std::clamp<int32_t>(
|
||||||
|
static_cast<int32_t>(buffer.p[sample_index].real()) + 128,
|
||||||
|
0,
|
||||||
|
255);
|
||||||
|
time_domain_spectrum.db[i] = static_cast<uint8_t>(normalized);
|
||||||
|
}
|
||||||
|
|
||||||
|
time_domain_request_update = true;
|
||||||
|
EventDispatcher::events_flag(EVT_MASK_SPECTRUM);
|
||||||
|
}
|
||||||
|
|
||||||
|
void TimeSinkProcessor::set_time_streaming_state(const SpectrumStreamingConfigMessage& message) {
|
||||||
|
if (message.mode == SpectrumStreamingConfigMessage::Mode::Running) {
|
||||||
|
time_streaming = true;
|
||||||
|
ChannelSpectrumConfigMessage fifo_message{&fifo};
|
||||||
|
shared_memory.application_queue.push(fifo_message);
|
||||||
|
} else {
|
||||||
|
time_streaming = false;
|
||||||
|
time_domain_request_update = false;
|
||||||
|
fifo.reset_in();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void TimeSinkProcessor::update_time_domain() {
|
||||||
|
if (time_streaming && time_domain_request_update) {
|
||||||
|
fifo.in(time_domain_spectrum);
|
||||||
|
}
|
||||||
|
|
||||||
|
time_domain_request_update = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
void TimeSinkProcessor::on_message(const Message* const msg) {
|
||||||
|
switch (msg->id) {
|
||||||
|
case Message::ID::UpdateSpectrum:
|
||||||
|
update_time_domain();
|
||||||
|
break;
|
||||||
|
|
||||||
|
case Message::ID::SpectrumStreamingConfig:
|
||||||
|
set_time_streaming_state(*reinterpret_cast<const SpectrumStreamingConfigMessage*>(msg));
|
||||||
|
break;
|
||||||
|
|
||||||
|
case Message::ID::TimeSinkConfig: {
|
||||||
|
const auto& message = *reinterpret_cast<const TimeSinkConfigMessage*>(msg);
|
||||||
|
baseband_fs = message.sampling_rate;
|
||||||
|
trigger = message.trigger;
|
||||||
|
baseband_thread.set_sampling_rate(baseband_fs);
|
||||||
|
phase = 0;
|
||||||
|
time_domain_request_update = false;
|
||||||
|
configured = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
default:
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
int main() {
|
||||||
|
EventDispatcher event_dispatcher{std::make_unique<TimeSinkProcessor>()};
|
||||||
|
event_dispatcher.run();
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
@@ -0,0 +1,56 @@
|
|||||||
|
/*
|
||||||
|
* Copyleft zxkmm (>) 2026
|
||||||
|
*
|
||||||
|
* This file is part of PortaPack.
|
||||||
|
*
|
||||||
|
* 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
|
||||||
|
* the Free Software Foundation; either version 2, or (at your option)
|
||||||
|
* any later version.
|
||||||
|
*
|
||||||
|
* This program is distributed in the hope that it will be useful,
|
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
* GNU General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU General Public License
|
||||||
|
* along with this program; see the file COPYING. If not, write to
|
||||||
|
* the Free Software Foundation, Inc., 51 Franklin Street,
|
||||||
|
* Boston, MA 02110-1301, USA.
|
||||||
|
*/
|
||||||
|
|
||||||
|
#ifndef __PROC_TIME_SINK_H__
|
||||||
|
#define __PROC_TIME_SINK_H__
|
||||||
|
|
||||||
|
#include "baseband_processor.hpp"
|
||||||
|
#include "baseband_thread.hpp"
|
||||||
|
|
||||||
|
#include "message.hpp"
|
||||||
|
|
||||||
|
#include <cstddef>
|
||||||
|
|
||||||
|
class TimeSinkProcessor : public BasebandProcessor {
|
||||||
|
public:
|
||||||
|
void execute(const buffer_c8_t& buffer) override;
|
||||||
|
void on_message(const Message* const message) override;
|
||||||
|
|
||||||
|
private:
|
||||||
|
bool configured = false;
|
||||||
|
size_t baseband_fs = 20000000;
|
||||||
|
|
||||||
|
void execute_time_domain(const buffer_c8_t& buffer);
|
||||||
|
void set_time_streaming_state(const SpectrumStreamingConfigMessage& message);
|
||||||
|
void update_time_domain();
|
||||||
|
ChannelSpectrum time_domain_spectrum{};
|
||||||
|
ChannelSpectrum fifo_data[1 << ChannelSpectrumConfigMessage::fifo_k]{};
|
||||||
|
ChannelSpectrumFIFO fifo{fifo_data, ChannelSpectrumConfigMessage::fifo_k};
|
||||||
|
|
||||||
|
size_t phase = 0, trigger = 127;
|
||||||
|
bool time_streaming = false;
|
||||||
|
volatile bool time_domain_request_update = false;
|
||||||
|
|
||||||
|
/* NB: Threads should be the last members in the class definition. */
|
||||||
|
BasebandThread baseband_thread{baseband_fs, this, baseband::Direction::Receive};
|
||||||
|
};
|
||||||
|
|
||||||
|
#endif /*__PROC_TIME_SINK_H__*/
|
||||||
@@ -155,6 +155,7 @@ class Message {
|
|||||||
StreamTXConfiguration = 97,
|
StreamTXConfiguration = 97,
|
||||||
RTTYData = 98,
|
RTTYData = 98,
|
||||||
NotificationData = 99,
|
NotificationData = 99,
|
||||||
|
TimeSinkConfig = 100,
|
||||||
MAX
|
MAX
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -309,6 +310,20 @@ class WidebandSpectrumConfigMessage : public Message {
|
|||||||
size_t trigger{0};
|
size_t trigger{0};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
class TimeSinkConfigMessage : public Message {
|
||||||
|
public:
|
||||||
|
constexpr TimeSinkConfigMessage(
|
||||||
|
size_t sampling_rate,
|
||||||
|
size_t trigger)
|
||||||
|
: Message{ID::TimeSinkConfig},
|
||||||
|
sampling_rate{sampling_rate},
|
||||||
|
trigger{trigger} {
|
||||||
|
}
|
||||||
|
|
||||||
|
size_t sampling_rate{0};
|
||||||
|
size_t trigger{0};
|
||||||
|
};
|
||||||
|
|
||||||
struct AudioSpectrum {
|
struct AudioSpectrum {
|
||||||
std::array<uint8_t, 128> db{{0}};
|
std::array<uint8_t, 128> db{{0}};
|
||||||
// uint32_t sampling_rate { 0 };
|
// uint32_t sampling_rate { 0 };
|
||||||
|
|||||||
@@ -96,6 +96,7 @@ constexpr image_tag_t image_tag_sonde{'P', 'S', 'O', 'N'};
|
|||||||
constexpr image_tag_t image_tag_tpms{'P', 'T', 'P', 'M'};
|
constexpr image_tag_t image_tag_tpms{'P', 'T', 'P', 'M'};
|
||||||
constexpr image_tag_t image_tag_wfm_audio{'P', 'W', 'F', 'M'};
|
constexpr image_tag_t image_tag_wfm_audio{'P', 'W', 'F', 'M'};
|
||||||
constexpr image_tag_t image_tag_wideband_spectrum{'P', 'S', 'P', 'E'};
|
constexpr image_tag_t image_tag_wideband_spectrum{'P', 'S', 'P', 'E'};
|
||||||
|
constexpr image_tag_t image_tag_time_sink{'P', 'T', 'S', 'K'};
|
||||||
constexpr image_tag_t image_tag_test{'P', 'T', 'S', 'T'};
|
constexpr image_tag_t image_tag_test{'P', 'T', 'S', 'T'};
|
||||||
|
|
||||||
constexpr image_tag_t image_tag_adsb_tx{'P', 'A', 'D', 'T'};
|
constexpr image_tag_t image_tag_adsb_tx{'P', 'A', 'D', 'T'};
|
||||||
|
|||||||
Reference in New Issue
Block a user