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:
zxkmm
2026-03-03 02:17:02 +08:00
committed by GitHub
parent f489d43259
commit 9ca36394d0
12 changed files with 911 additions and 3 deletions
+11 -1
View File
@@ -334,12 +334,22 @@ void set_rds_data(const uint16_t message_length) {
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{
sampling_rate, trigger};
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) {
const WeFaxRxConfigureMessage message{lpm, ioc};
send_message(&message);
+6 -1
View File
@@ -97,7 +97,12 @@ void set_pocsag(int8_t baud_config = -1);
void set_adsb();
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_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_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);
+6 -1
View File
@@ -312,6 +312,10 @@ set(EXTCPPSRC
#pocsag_tx
external/pocsag_tx/main.cpp
external/pocsag_tx/ui_pocsag_tx.cpp
#time_sink
external/time_sink/main.cpp
external/time_sink/ui_time_sink.cpp
)
set(EXTAPPLIST
@@ -386,10 +390,11 @@ set(EXTAPPLIST
siggen
morse_radio
morseradiotx
keeloqtx
keeloqtx
rtty_rx
rtty_tx
pocsag_tx
time_sink
)
# sdusb has type conflicts with PRALINE (HackRF Pro) - add only for non-PRALINE builds
+8
View File
@@ -99,6 +99,8 @@ MEMORY
ram_external_app_rtty_tx (rwx) : org = 0xADFA0000, len = 32k
ram_external_app_tpmstx (rwx) : org = 0xADFB0000, len = 32k
ram_external_app_pocsag_tx (rwx) : org = 0xADFC0000, len = 32k
ram_external_app_time_sink (rwx) : org = 0xADFD0000, len = 32k
}
SECTIONS
@@ -559,5 +561,11 @@ SECTIONS
KEEP(*(.external_app.app_pocsag_tx.application_information));
*(*ui*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
}
+82
View File
@@ -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
};
}
+352
View File
@@ -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
+238
View File
@@ -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__
+8
View File
@@ -467,6 +467,7 @@ set(MODE_CPPSRC
)
DeclareTargets(PSPE wideband_spectrum)
### WFM Audio
set(MODE_CPPSRC
@@ -705,6 +706,13 @@ set(MODE_CPPSRC
)
DeclareTargets(PMRT morsetx)
### Time Sink
set(MODE_CPPSRC
proc_time_sink.cpp
)
DeclareTargets(PTSK time_sink)
set(MODE_CPPSRC
sd_over_usb/proc_sd_over_usb.cpp
+128
View File
@@ -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;
}
+56
View File
@@ -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__*/
+15
View File
@@ -155,6 +155,7 @@ class Message {
StreamTXConfiguration = 97,
RTTYData = 98,
NotificationData = 99,
TimeSinkConfig = 100,
MAX
};
@@ -309,6 +310,20 @@ class WidebandSpectrumConfigMessage : public Message {
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 {
std::array<uint8_t, 128> db{{0}};
// uint32_t sampling_rate { 0 };
+1
View File
@@ -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_wfm_audio{'P', 'W', 'F', 'M'};
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_adsb_tx{'P', 'A', 'D', 'T'};