mirror of
https://github.com/portapack-mayhem/mayhem-firmware.git
synced 2026-08-23 07:59:03 +00:00
Feature/sliding freq (#3280)
* Add sliding-frequency audio receiver tuning * Move ADS-B receiver to external app * Move AIS receiver to external app * Move APRS receiver to external app * Move APRS transmitter to external app * Isolate filtered spectrum collection from WFM * Fixed the issues #3280 * fpga_bridge.c: init registers per reference fpga_init, add quarter-shift mode setter * radio.hpp: expose cached FPGA quarter-rate shift * radio.cpp: program FPGA quarter-rate shift together with tuning offset * tuning.hpp: pass AFE rate and direction to tuning config, add quarter_shift field * tuning.cpp: import full PRALINE RX/TX tuning tables with quarter-shift offsets * clock_manager: stop writing bogus RX digital gain, keep quarter shift across rate changes * receiver_model: port LPF bandwidth from reference auto_bandwidth, use cached quarter shift * adsb_rx: enable RF amp by default on first run * ui_geomap.hpp: add hemisphere fields, degrees become magnitude * ui_geomap: use hemisphere selector for lat/lon sign, fix minute/second wrap carry * ui_menu: guard select against empty menu * waterfall_designer: header updates for profile file handling * waterfall_designer: exception-free profile parsing, CRLF handling, deferred nav callbacks, backup cleanup * external.ld: scope app section globs to their own object directories * CMakeLists: relink when external.ld changes * tools: add external app symbol placement checker * tools: add guru meditation address lookup script * tools: add per-function stack usage report script Co-authored-by: gullradriel <gullradriel@users.noreply.github.com>
This commit is contained in:
@@ -279,16 +279,12 @@ set(CPPSRC
|
||||
ui/ui_tone_key.cpp
|
||||
ui/ui_transmitter.cpp
|
||||
ui/ui_bmpview.cpp
|
||||
apps/ais_app.cpp
|
||||
apps/analog_audio_app.cpp
|
||||
apps/ble_rx_app.cpp
|
||||
apps/ble_tx_app.cpp
|
||||
apps/capture_app.cpp
|
||||
apps/pocsag_app.cpp
|
||||
apps/ui_about_simple.cpp
|
||||
apps/ui_adsb_rx.cpp
|
||||
apps/ui_aprs_rx.cpp
|
||||
apps/ui_aprs_tx.cpp
|
||||
apps/ui_battinfo.cpp
|
||||
apps/ui_bmp_file_viewer.cpp
|
||||
apps/ui_btle_rx.cpp
|
||||
@@ -516,4 +512,4 @@ add_custom_target(
|
||||
DEPENDS ${PROJECT_NAME}.bin
|
||||
)
|
||||
|
||||
add_dependencies(${PROJECT_NAME} baseband)
|
||||
add_dependencies(${PROJECT_NAME} baseband)
|
||||
|
||||
@@ -280,6 +280,12 @@ AnalogAudioView::AnalogAudioView(
|
||||
field_frequency.on_show_options = [this]() {
|
||||
this->on_show_options_frequency();
|
||||
};
|
||||
field_frequency.changing = [this](rf::Frequency frequency) {
|
||||
return this->on_frequency_changed(frequency);
|
||||
};
|
||||
field_frequency.entered = [this](rf::Frequency frequency) {
|
||||
this->set_frequency_absolute(frequency);
|
||||
};
|
||||
|
||||
field_lna.on_show_options = [this]() {
|
||||
this->on_show_options_rf_gain();
|
||||
@@ -308,8 +314,10 @@ AnalogAudioView::AnalogAudioView(
|
||||
};
|
||||
|
||||
waterfall.on_select = [this](int32_t offset) {
|
||||
field_frequency.set_value(receiver_model.target_frequency() + offset);
|
||||
field_frequency.set_value(
|
||||
field_frequency.value() + offset * receiver_model.frequency_step());
|
||||
};
|
||||
waterfall.set_live_tuning(true);
|
||||
|
||||
#ifdef PRALINE
|
||||
button_pro.on_select = [this](Button&) { this->on_show_options_praline(); };
|
||||
@@ -319,6 +327,7 @@ AnalogAudioView::AnalogAudioView(
|
||||
|
||||
// This call starts the correct baseband image to run
|
||||
// and sets the radio up as necessary for the given modulation.
|
||||
sliding_center_frequency = receiver_model.target_frequency();
|
||||
on_modulation_changed(modulation);
|
||||
}
|
||||
|
||||
@@ -328,7 +337,7 @@ AnalogAudioView::AnalogAudioView(
|
||||
: AnalogAudioView(nav) {
|
||||
// Settings to override when launched from another app (versus from AppSettings .ini file)
|
||||
// TODO: Which other settings make sense to override?
|
||||
field_frequency.set_value(override.frequency_app_override);
|
||||
set_frequency_absolute(override.frequency_app_override);
|
||||
on_frequency_step_changed(override.frequency_step);
|
||||
options_modulation.set_by_value(toUType(override.mode));
|
||||
}
|
||||
@@ -573,10 +582,14 @@ void AnalogAudioView::update_modulation(ReceiverModel::Mode modulation) {
|
||||
receiver_model.set_sampling_rate(is_wideband_spectrum_mode ? spec_bw : 3072000);
|
||||
receiver_model.set_baseband_bandwidth(is_wideband_spectrum_mode ? spec_bw / 2 : 1750000);
|
||||
|
||||
receiver_model.set_hidden_offset(modulation == ReceiverModel::Mode::AMAudioFMApt ? -2200 : 0); // wefax needs to be shifted, see wefax rx app.
|
||||
reset_sliding_frequency(modulation);
|
||||
|
||||
receiver_model.enable();
|
||||
|
||||
if (sliding_enabled) {
|
||||
baseband::set_audio_ddc_frequency(0);
|
||||
}
|
||||
|
||||
// TODO: This doesn't belong here! There's a better way.
|
||||
size_t sampling_rate = 0;
|
||||
switch (modulation) {
|
||||
@@ -610,7 +623,64 @@ void AnalogAudioView::handle_coded_squelch(uint32_t value) {
|
||||
}
|
||||
|
||||
void AnalogAudioView::on_freqchg(int64_t freq) {
|
||||
field_frequency.set_value(freq);
|
||||
set_frequency_absolute(freq);
|
||||
}
|
||||
|
||||
void AnalogAudioView::set_frequency_absolute(rf::Frequency frequency) {
|
||||
if (!sliding_enabled) {
|
||||
field_frequency.set_value(frequency);
|
||||
return;
|
||||
}
|
||||
|
||||
sliding_center_frequency = frequency;
|
||||
|
||||
/* set_value() does not call on_change when the displayed frequency is
|
||||
* already equal, so reset the hardware and DDC explicitly in that case. */
|
||||
if (field_frequency.value() == frequency) {
|
||||
receiver_model.set_target_frequency_with_hidden_offset(frequency, 0);
|
||||
baseband::set_audio_ddc_frequency(0);
|
||||
} else {
|
||||
field_frequency.set_value(frequency);
|
||||
}
|
||||
}
|
||||
|
||||
int32_t AnalogAudioView::sliding_limit() const {
|
||||
const bool zoom_x2 =
|
||||
receiver_model.modulation() == ReceiverModel::Mode::AMAudio &&
|
||||
previous_zoom != 0;
|
||||
return zoom_x2 ? sliding_limit_zoom_x2 : sliding_limit_zoom_x1;
|
||||
}
|
||||
|
||||
void AnalogAudioView::reset_sliding_frequency(ReceiverModel::Mode modulation) {
|
||||
sliding_enabled =
|
||||
modulation == ReceiverModel::Mode::AMAudio ||
|
||||
modulation == ReceiverModel::Mode::NarrowbandFMAudio;
|
||||
sliding_center_frequency = receiver_model.target_frequency();
|
||||
|
||||
/* AMFM keeps its existing Wefax offset; sliding applies to AM and NFM. */
|
||||
receiver_model.set_hidden_offset(
|
||||
modulation == ReceiverModel::Mode::AMAudioFMApt ? -2200 : 0);
|
||||
}
|
||||
|
||||
bool AnalogAudioView::on_frequency_changed(rf::Frequency frequency) {
|
||||
if (!sliding_enabled)
|
||||
return false;
|
||||
|
||||
const auto limit = sliding_limit();
|
||||
int64_t offset = frequency - sliding_center_frequency;
|
||||
if (offset > limit) {
|
||||
sliding_center_frequency = frequency - limit;
|
||||
offset = limit;
|
||||
} else if (offset < -limit) {
|
||||
sliding_center_frequency = frequency + limit;
|
||||
offset = -limit;
|
||||
}
|
||||
|
||||
/* Store the displayed frequency and retune the hardware centre atomically. */
|
||||
receiver_model.set_target_frequency_with_hidden_offset(
|
||||
frequency, sliding_center_frequency - frequency);
|
||||
baseband::set_audio_ddc_frequency(static_cast<int32_t>(offset));
|
||||
return true;
|
||||
}
|
||||
|
||||
#ifdef PRALINE
|
||||
|
||||
@@ -269,6 +269,10 @@ class AnalogAudioView : public View {
|
||||
uint8_t zoom_factor_amfm{0}; // initial zoom factor in AMFM mode
|
||||
uint8_t previous_AM_mode_option{0}; // GUI 5 AM modes : (0..4 ) (DSB9K, DSB6K, USB,LSB, CW). Used to select proper FIR filter (0..11) AM mode + offset 0 (zoom+1) or +6 (if zoom+2)
|
||||
uint8_t previous_zoom{0}; // GUI ZOOM+1, ZOOM+2 , equivalent to two values offset 0 (zoom+1) or +6 (if zoom+2)
|
||||
static constexpr int32_t sliding_limit_zoom_x1 = 50000;
|
||||
static constexpr int32_t sliding_limit_zoom_x2 = 30000;
|
||||
rf::Frequency sliding_center_frequency{0};
|
||||
bool sliding_enabled{false};
|
||||
|
||||
app_settings::SettingsManager settings_{
|
||||
"rx_audio",
|
||||
@@ -354,6 +358,10 @@ class AnalogAudioView : public View {
|
||||
void handle_coded_squelch(uint32_t value);
|
||||
|
||||
void on_freqchg(int64_t freq);
|
||||
bool on_frequency_changed(rf::Frequency frequency);
|
||||
void set_frequency_absolute(rf::Frequency frequency);
|
||||
int32_t sliding_limit() const;
|
||||
void reset_sliding_frequency(ReceiverModel::Mode modulation);
|
||||
|
||||
MessageHandlerRegistration message_handler_coded_squelch{
|
||||
Message::ID::CodedSquelch,
|
||||
|
||||
@@ -343,6 +343,11 @@ void set_spectrum(
|
||||
send_message(&message);
|
||||
}
|
||||
|
||||
void set_audio_ddc_frequency(int32_t frequency) {
|
||||
const AudioDDCConfigMessage message{frequency};
|
||||
send_message(&message);
|
||||
}
|
||||
|
||||
void set_time_sink(
|
||||
const size_t sampling_rate,
|
||||
const size_t trigger) {
|
||||
|
||||
@@ -100,6 +100,7 @@ void set_rds_data(const uint16_t message_length);
|
||||
void set_spectrum(
|
||||
const size_t sampling_rate,
|
||||
const size_t trigger);
|
||||
void set_audio_ddc_frequency(int32_t frequency);
|
||||
void set_time_sink(
|
||||
const size_t sampling_rate,
|
||||
const size_t trigger);
|
||||
|
||||
+85
@@ -0,0 +1,85 @@
|
||||
/*
|
||||
* Copyright (C) 2026 PortaPack Mayhem
|
||||
*
|
||||
* 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_adsb_rx.hpp"
|
||||
#include "ui_navigation.hpp"
|
||||
#include "external_app.hpp"
|
||||
|
||||
namespace ui::external_app::adsbrx {
|
||||
|
||||
void initialize_app(ui::NavigationView& nav) {
|
||||
nav.push<ADSBRxView>();
|
||||
}
|
||||
|
||||
} // namespace ui::external_app::adsbrx
|
||||
|
||||
extern "C" {
|
||||
|
||||
__attribute__((section(".external_app.app_adsbrx.application_information"), used)) application_information_t _application_information_adsbrx = {
|
||||
/*.memory_location = */ (uint8_t*)0x00000000,
|
||||
/*.externalAppEntry = */ ui::external_app::adsbrx::initialize_app,
|
||||
/*.header_version = */ CURRENT_HEADER_VERSION,
|
||||
/*.app_version = */ VERSION_MD5,
|
||||
|
||||
/*.app_name = */ "ADS-B",
|
||||
/*.bitmap_data = */ {
|
||||
0x80,
|
||||
0x01,
|
||||
0xC0,
|
||||
0x03,
|
||||
0xC0,
|
||||
0x03,
|
||||
0xC0,
|
||||
0x03,
|
||||
0xC0,
|
||||
0x03,
|
||||
0xE0,
|
||||
0x07,
|
||||
0xF8,
|
||||
0x1F,
|
||||
0xFE,
|
||||
0x7F,
|
||||
0xFF,
|
||||
0xFF,
|
||||
0xFF,
|
||||
0xFF,
|
||||
0xC0,
|
||||
0x03,
|
||||
0xC0,
|
||||
0x03,
|
||||
0xC0,
|
||||
0x03,
|
||||
0xE0,
|
||||
0x07,
|
||||
0xF0,
|
||||
0x0F,
|
||||
0xF8,
|
||||
0x1F,
|
||||
},
|
||||
/*.icon_color = */ ui::Color::green().v,
|
||||
/*.menu_location = */ app_location_t::RX,
|
||||
/*.desired_menu_position = */ -1,
|
||||
|
||||
/*.m4_app_tag = portapack::spi_flash::image_tag_adsbrx */ {'P', 'A', 'D', 'R'},
|
||||
/*.m4_app_offset = */ 0x00000000,
|
||||
};
|
||||
|
||||
} // extern "C"
|
||||
Vendored
+13
-3
@@ -38,7 +38,7 @@ using namespace portapack;
|
||||
|
||||
namespace pmem = portapack::persistent_memory;
|
||||
|
||||
namespace ui {
|
||||
namespace ui::external_app::adsbrx {
|
||||
|
||||
static const char speed_type_msg[][6] = {" Spd:", " IAS:", " TAS:"};
|
||||
|
||||
@@ -46,13 +46,19 @@ static std::string get_map_tag(const AircraftRecentEntry& entry) {
|
||||
return trimr(entry.callsign.empty() ? entry.icao_str : entry.callsign);
|
||||
}
|
||||
|
||||
} // namespace ui::external_app::adsbrx
|
||||
|
||||
namespace ui {
|
||||
|
||||
template <>
|
||||
void RecentEntriesTable<AircraftRecentEntries>::draw(
|
||||
void RecentEntriesTable<external_app::adsbrx::AircraftRecentEntries>::draw(
|
||||
const Entry& entry,
|
||||
const Rect& target_rect,
|
||||
Painter& painter,
|
||||
const Style& style,
|
||||
RecentEntriesColumns& columns) {
|
||||
using namespace external_app::adsbrx;
|
||||
|
||||
Color target_color;
|
||||
std::string entry_string;
|
||||
|
||||
@@ -107,6 +113,10 @@ void RecentEntriesTable<AircraftRecentEntries>::draw(
|
||||
bitmap_target, target_color, style.background);
|
||||
}
|
||||
|
||||
} // namespace ui
|
||||
|
||||
namespace ui::external_app::adsbrx {
|
||||
|
||||
/* ADSBLogger ********************************************/
|
||||
|
||||
void ADSBLogger::log(const ADSBLogEntry& log_entry) {
|
||||
@@ -828,4 +838,4 @@ void ADSBRxView::remove_expired_entries() {
|
||||
recent.erase(it.base(), recent.end());
|
||||
}
|
||||
|
||||
} /* namespace ui */
|
||||
} // namespace ui::external_app::adsbrx
|
||||
Vendored
+2
-2
@@ -39,7 +39,7 @@
|
||||
|
||||
using namespace adsb;
|
||||
|
||||
namespace ui {
|
||||
namespace ui::external_app::adsbrx {
|
||||
#define AIRCRAFT_ID_L 1 // aircraft ID message type (lowest type id)
|
||||
#define AIRCRAFT_ID_H 4 // aircraft ID message type (highest type id)
|
||||
|
||||
@@ -491,4 +491,4 @@ class ADSBRxView : public View {
|
||||
}};
|
||||
};
|
||||
|
||||
} /* namespace ui */
|
||||
} // namespace ui::external_app::adsbrx
|
||||
+37
-29
@@ -35,7 +35,8 @@ using namespace portapack;
|
||||
|
||||
namespace pmem = portapack::persistent_memory;
|
||||
|
||||
namespace ais {
|
||||
namespace ui::external_app::ais_rx {
|
||||
|
||||
namespace format {
|
||||
|
||||
static std::string latlon_abs_normalized(const int32_t normalized, const char suffixes[2]) {
|
||||
@@ -47,7 +48,7 @@ static std::string latlon_abs_normalized(const int32_t normalized, const char su
|
||||
return to_string_dec_uint(degrees) + "." + to_string_dec_uint(fraction, 6, '0') + suffix;
|
||||
}
|
||||
|
||||
static std::string latlon(const Latitude latitude, const Longitude longitude) {
|
||||
static std::string latlon(const ais::Latitude latitude, const ais::Longitude longitude) {
|
||||
if (latitude.is_valid() && longitude.is_valid()) {
|
||||
return latlon_abs_normalized(latitude.normalized(), "SN") + " " + latlon_abs_normalized(longitude.normalized(), "WE");
|
||||
} else if (latitude.is_not_available() && longitude.is_not_available()) {
|
||||
@@ -128,7 +129,7 @@ static std::string navigational_status(const unsigned int value) {
|
||||
}
|
||||
}
|
||||
|
||||
static std::string rate_of_turn(const RateOfTurn value) {
|
||||
static std::string rate_of_turn(const ais::RateOfTurn value) {
|
||||
switch (value) {
|
||||
case -128:
|
||||
return "not available";
|
||||
@@ -149,7 +150,7 @@ static std::string rate_of_turn(const RateOfTurn value) {
|
||||
}
|
||||
}
|
||||
|
||||
static std::string speed_over_ground(const SpeedOverGround value) {
|
||||
static std::string speed_over_ground(const ais::SpeedOverGround value) {
|
||||
if (value == 1023) {
|
||||
return "not available";
|
||||
} else if (value == 1022) {
|
||||
@@ -159,7 +160,7 @@ static std::string speed_over_ground(const SpeedOverGround value) {
|
||||
}
|
||||
}
|
||||
|
||||
static std::string course_over_ground(const CourseOverGround value) {
|
||||
static std::string course_over_ground(const ais::CourseOverGround value) {
|
||||
if (value > 3600) {
|
||||
return "invalid";
|
||||
} else if (value == 3600) {
|
||||
@@ -169,7 +170,7 @@ static std::string course_over_ground(const CourseOverGround value) {
|
||||
}
|
||||
}
|
||||
|
||||
static std::string true_heading(const TrueHeading value) {
|
||||
static std::string true_heading(const ais::TrueHeading value) {
|
||||
if (value == 511) {
|
||||
return "not available";
|
||||
} else if (value > 359) {
|
||||
@@ -180,7 +181,6 @@ static std::string true_heading(const TrueHeading value) {
|
||||
}
|
||||
|
||||
} /* namespace format */
|
||||
} /* namespace ais */
|
||||
|
||||
void AISLogger::on_packet(const ais::Packet& packet) {
|
||||
// TODO: Unstuff here, not in baseband!
|
||||
@@ -265,25 +265,33 @@ void AISRecentEntry::update(const ais::Packet& packet) {
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace ui::external_app::ais_rx
|
||||
|
||||
namespace ui {
|
||||
|
||||
template <>
|
||||
void RecentEntriesTable<AISRecentEntries>::draw(
|
||||
void RecentEntriesTable<external_app::ais_rx::AISRecentEntries>::draw(
|
||||
const Entry& entry,
|
||||
const Rect& target_rect,
|
||||
Painter& painter,
|
||||
const Style& style,
|
||||
RecentEntriesColumns&) {
|
||||
std::string line = ais::format::mmsi(entry.mmsi) + " ";
|
||||
using namespace external_app::ais_rx;
|
||||
|
||||
std::string line = format::mmsi(entry.mmsi) + " ";
|
||||
if (!entry.name.empty()) {
|
||||
line += ais::format::text(entry.name);
|
||||
line += format::text(entry.name);
|
||||
} else {
|
||||
line += ais::format::text(entry.call_sign);
|
||||
line += format::text(entry.call_sign);
|
||||
}
|
||||
line.resize(target_rect.width() / 8, ' ');
|
||||
painter.draw_string(target_rect.location(), style, line);
|
||||
}
|
||||
|
||||
} // namespace ui
|
||||
|
||||
namespace ui::external_app::ais_rx {
|
||||
|
||||
AISRecentEntryDetailView::AISRecentEntryDetailView(NavigationView& nav) {
|
||||
add_children({
|
||||
&button_done,
|
||||
@@ -298,12 +306,12 @@ AISRecentEntryDetailView::AISRecentEntryDetailView(NavigationView& nav) {
|
||||
|
||||
button_see_map.on_select = [this, &nav](Button&) {
|
||||
geomap_view = nav.push<GeoMapView>(
|
||||
ais::format::text(entry_.name),
|
||||
format::text(entry_.name),
|
||||
0,
|
||||
GeoPos::alt_unit::METERS,
|
||||
GeoPos::spd_unit::KNOTS,
|
||||
ais::format::latlon_float(entry_.last_position.latitude.normalized()),
|
||||
ais::format::latlon_float(entry_.last_position.longitude.normalized()),
|
||||
format::latlon_float(entry_.last_position.latitude.normalized()),
|
||||
format::latlon_float(entry_.last_position.longitude.normalized()),
|
||||
entry_.last_position.true_heading,
|
||||
[this]() {
|
||||
send_updates = false;
|
||||
@@ -324,14 +332,14 @@ AISRecentEntryDetailView& AISRecentEntryDetailView::operator=(const AISRecentEnt
|
||||
|
||||
void AISRecentEntryDetailView::update_position() {
|
||||
if (send_updates)
|
||||
geomap_view->update_position(ais::format::latlon_float(entry_.last_position.latitude.normalized()), ais::format::latlon_float(entry_.last_position.longitude.normalized()), (float)entry_.last_position.true_heading, 0, entry_.last_position.speed_over_ground > 1022 ? 0 : entry_.last_position.speed_over_ground / 10);
|
||||
geomap_view->update_position(format::latlon_float(entry_.last_position.latitude.normalized()), format::latlon_float(entry_.last_position.longitude.normalized()), (float)entry_.last_position.true_heading, 0, entry_.last_position.speed_over_ground > 1022 ? 0 : entry_.last_position.speed_over_ground / 10);
|
||||
}
|
||||
|
||||
bool AISRecentEntryDetailView::add_map_marker(const AISRecentEntry& entry) {
|
||||
if (geomap_view && send_updates) {
|
||||
GeoMarker marker{};
|
||||
marker.lon = ais::format::latlon_float(entry.last_position.longitude.normalized());
|
||||
marker.lat = ais::format::latlon_float(entry.last_position.latitude.normalized());
|
||||
marker.lon = format::latlon_float(entry.last_position.longitude.normalized());
|
||||
marker.lat = format::latlon_float(entry.last_position.latitude.normalized());
|
||||
marker.angle = entry.last_position.true_heading;
|
||||
marker.tag = entry.call_sign.empty() ? to_string_dec_uint(entry.mmsi) : entry.call_sign;
|
||||
auto markerStored = geomap_view->store_marker(marker);
|
||||
@@ -377,18 +385,18 @@ void AISRecentEntryDetailView::paint(Painter& painter) {
|
||||
|
||||
auto field_rect = Rect{rect.left(), rect.top() + 16, rect.width(), 16};
|
||||
|
||||
field_rect = draw_field(painter, field_rect, s, "MMSI", ais::format::mmsi(entry_.mmsi));
|
||||
field_rect = draw_field(painter, field_rect, s, "Ctry", ais::format::mid(entry_.mmsi));
|
||||
field_rect = draw_field(painter, field_rect, s, "Name", ais::format::text(entry_.name));
|
||||
field_rect = draw_field(painter, field_rect, s, "Call", ais::format::text(entry_.call_sign));
|
||||
field_rect = draw_field(painter, field_rect, s, "Dest", ais::format::text(entry_.destination));
|
||||
field_rect = draw_field(painter, field_rect, s, "MMSI", format::mmsi(entry_.mmsi));
|
||||
field_rect = draw_field(painter, field_rect, s, "Ctry", format::mid(entry_.mmsi));
|
||||
field_rect = draw_field(painter, field_rect, s, "Name", format::text(entry_.name));
|
||||
field_rect = draw_field(painter, field_rect, s, "Call", format::text(entry_.call_sign));
|
||||
field_rect = draw_field(painter, field_rect, s, "Dest", format::text(entry_.destination));
|
||||
field_rect = draw_field(painter, field_rect, s, "Last", to_string_datetime(entry_.last_position.timestamp));
|
||||
field_rect = draw_field(painter, field_rect, s, "Pos ", ais::format::latlon(entry_.last_position.latitude, entry_.last_position.longitude));
|
||||
field_rect = draw_field(painter, field_rect, s, "Stat", ais::format::navigational_status(entry_.navigational_status));
|
||||
field_rect = draw_field(painter, field_rect, s, "RoT ", ais::format::rate_of_turn(entry_.last_position.rate_of_turn));
|
||||
field_rect = draw_field(painter, field_rect, s, "SoG ", ais::format::speed_over_ground(entry_.last_position.speed_over_ground));
|
||||
field_rect = draw_field(painter, field_rect, s, "CoG ", ais::format::course_over_ground(entry_.last_position.course_over_ground));
|
||||
field_rect = draw_field(painter, field_rect, s, "Head", ais::format::true_heading(entry_.last_position.true_heading));
|
||||
field_rect = draw_field(painter, field_rect, s, "Pos ", format::latlon(entry_.last_position.latitude, entry_.last_position.longitude));
|
||||
field_rect = draw_field(painter, field_rect, s, "Stat", format::navigational_status(entry_.navigational_status));
|
||||
field_rect = draw_field(painter, field_rect, s, "RoT ", format::rate_of_turn(entry_.last_position.rate_of_turn));
|
||||
field_rect = draw_field(painter, field_rect, s, "SoG ", format::speed_over_ground(entry_.last_position.speed_over_ground));
|
||||
field_rect = draw_field(painter, field_rect, s, "CoG ", format::course_over_ground(entry_.last_position.course_over_ground));
|
||||
field_rect = draw_field(painter, field_rect, s, "Head", format::true_heading(entry_.last_position.true_heading));
|
||||
field_rect = draw_field(painter, field_rect, s, "Rx #", to_string_dec_uint(entry_.received_count));
|
||||
}
|
||||
|
||||
@@ -504,4 +512,4 @@ void AISAppView::on_show_detail(const AISRecentEntry& entry) {
|
||||
recent_entry_detail_view.update_map_markers(recent);
|
||||
}
|
||||
|
||||
} /* namespace ui */
|
||||
} // namespace ui::external_app::ais_rx
|
||||
+3
-3
@@ -50,6 +50,8 @@ using namespace lpc43xx;
|
||||
|
||||
#include "recent_entries.hpp"
|
||||
|
||||
namespace ui::external_app::ais_rx {
|
||||
|
||||
struct AISPosition {
|
||||
rtc::RTC timestamp{};
|
||||
ais::Latitude latitude{};
|
||||
@@ -109,8 +111,6 @@ class AISLogger {
|
||||
LogFile log_file{};
|
||||
};
|
||||
|
||||
namespace ui {
|
||||
|
||||
using AISRecentEntriesView = RecentEntriesView<AISRecentEntries>;
|
||||
|
||||
class AISRecentEntryDetailView : public View {
|
||||
@@ -239,6 +239,6 @@ class AISAppView : public View {
|
||||
void on_tick_second();
|
||||
};
|
||||
|
||||
} /* namespace ui */
|
||||
} // namespace ui::external_app::ais_rx
|
||||
|
||||
#endif /*__AIS_APP_H__*/
|
||||
+85
@@ -0,0 +1,85 @@
|
||||
/*
|
||||
* Copyright (C) 2026 PortaPack Mayhem
|
||||
*
|
||||
* 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 "ais_app.hpp"
|
||||
#include "ui_navigation.hpp"
|
||||
#include "external_app.hpp"
|
||||
|
||||
namespace ui::external_app::ais_rx {
|
||||
|
||||
void initialize_app(ui::NavigationView& nav) {
|
||||
nav.push<AISAppView>();
|
||||
}
|
||||
|
||||
} // namespace ui::external_app::ais_rx
|
||||
|
||||
extern "C" {
|
||||
|
||||
__attribute__((section(".external_app.app_ais_rx.application_information"), used)) application_information_t _application_information_ais_rx = {
|
||||
/*.memory_location = */ (uint8_t*)0x00000000,
|
||||
/*.externalAppEntry = */ ui::external_app::ais_rx::initialize_app,
|
||||
/*.header_version = */ CURRENT_HEADER_VERSION,
|
||||
/*.app_version = */ VERSION_MD5,
|
||||
|
||||
/*.app_name = */ "AIS Boats",
|
||||
/*.bitmap_data = */ {
|
||||
0x00,
|
||||
0x01,
|
||||
0x80,
|
||||
0x01,
|
||||
0xC0,
|
||||
0x01,
|
||||
0xC0,
|
||||
0x0D,
|
||||
0xE0,
|
||||
0x3D,
|
||||
0xF0,
|
||||
0x3D,
|
||||
0xF8,
|
||||
0x7D,
|
||||
0xFC,
|
||||
0x7D,
|
||||
0xFC,
|
||||
0x7D,
|
||||
0xFE,
|
||||
0x7D,
|
||||
0xFF,
|
||||
0x7D,
|
||||
0x00,
|
||||
0x00,
|
||||
0xF8,
|
||||
0x7F,
|
||||
0xF8,
|
||||
0x3F,
|
||||
0xF0,
|
||||
0x0F,
|
||||
0x00,
|
||||
0x00,
|
||||
},
|
||||
/*.icon_color = */ ui::Color::green().v,
|
||||
/*.menu_location = */ app_location_t::RX,
|
||||
/*.desired_menu_position = */ -1,
|
||||
|
||||
/*.m4_app_tag = portapack::spi_flash::image_tag_ais */ {'P', 'A', 'I', 'S'},
|
||||
/*.m4_app_offset = */ 0x00000000,
|
||||
};
|
||||
|
||||
} // extern "C"
|
||||
+85
@@ -0,0 +1,85 @@
|
||||
/*
|
||||
* Copyright (C) 2026 PortaPack Mayhem
|
||||
*
|
||||
* 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_aprs_rx.hpp"
|
||||
#include "ui_navigation.hpp"
|
||||
#include "external_app.hpp"
|
||||
|
||||
namespace ui::external_app::aprs_rx {
|
||||
|
||||
void initialize_app(ui::NavigationView& nav) {
|
||||
nav.push<APRSRXView>();
|
||||
}
|
||||
|
||||
} // namespace ui::external_app::aprs_rx
|
||||
|
||||
extern "C" {
|
||||
|
||||
__attribute__((section(".external_app.app_aprs_rx.application_information"), used)) application_information_t _application_information_aprs_rx = {
|
||||
/*.memory_location = */ (uint8_t*)0x00000000,
|
||||
/*.externalAppEntry = */ ui::external_app::aprs_rx::initialize_app,
|
||||
/*.header_version = */ CURRENT_HEADER_VERSION,
|
||||
/*.app_version = */ VERSION_MD5,
|
||||
|
||||
/*.app_name = */ "APRS",
|
||||
/*.bitmap_data = */ {
|
||||
0x00,
|
||||
0x00,
|
||||
0x00,
|
||||
0x00,
|
||||
0x00,
|
||||
0x00,
|
||||
0xF0,
|
||||
0x0F,
|
||||
0x4C,
|
||||
0x32,
|
||||
0xFE,
|
||||
0x7F,
|
||||
0x25,
|
||||
0xA4,
|
||||
0x25,
|
||||
0xA4,
|
||||
0xFF,
|
||||
0xFF,
|
||||
0x25,
|
||||
0xA4,
|
||||
0x25,
|
||||
0xA4,
|
||||
0xFE,
|
||||
0x7F,
|
||||
0x4C,
|
||||
0x32,
|
||||
0xF0,
|
||||
0x0F,
|
||||
0x00,
|
||||
0x00,
|
||||
0x00,
|
||||
0x00,
|
||||
},
|
||||
/*.icon_color = */ ui::Color::green().v,
|
||||
/*.menu_location = */ app_location_t::RX,
|
||||
/*.desired_menu_position = */ -1,
|
||||
|
||||
/*.m4_app_tag = portapack::spi_flash::image_tag_aprs_rx */ {'P', 'A', 'P', 'R'},
|
||||
/*.m4_app_offset = */ 0x00000000,
|
||||
};
|
||||
|
||||
} // extern "C"
|
||||
Vendored
+12
-2
@@ -31,19 +31,25 @@
|
||||
|
||||
using namespace portapack;
|
||||
|
||||
namespace ui::external_app::aprs_rx {
|
||||
|
||||
void APRSLogger::log_raw_data(const std::string& data) {
|
||||
log_file.write_entry(data);
|
||||
}
|
||||
|
||||
} // namespace ui::external_app::aprs_rx
|
||||
|
||||
namespace ui {
|
||||
|
||||
template <>
|
||||
void RecentEntriesTable<APRSRecentEntries>::draw(
|
||||
void RecentEntriesTable<external_app::aprs_rx::APRSRecentEntries>::draw(
|
||||
const Entry& entry,
|
||||
const Rect& target_rect,
|
||||
Painter& painter,
|
||||
const Style& style,
|
||||
RecentEntriesColumns& columns) {
|
||||
using namespace external_app::aprs_rx;
|
||||
|
||||
Color target_color;
|
||||
// auto entry_age = entry.age;
|
||||
|
||||
@@ -68,6 +74,10 @@ void RecentEntriesTable<APRSRecentEntries>::draw(
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace ui
|
||||
|
||||
namespace ui::external_app::aprs_rx {
|
||||
|
||||
void APRSRxView::focus() {
|
||||
options_region.focus();
|
||||
}
|
||||
@@ -402,4 +412,4 @@ void APRSRXView::focus() {
|
||||
|
||||
APRSRXView::~APRSRXView() {
|
||||
}
|
||||
} /* namespace ui */
|
||||
} // namespace ui::external_app::aprs_rx
|
||||
Vendored
+3
-3
@@ -38,6 +38,8 @@
|
||||
#include "utility.hpp"
|
||||
#include "file_path.hpp"
|
||||
|
||||
namespace ui::external_app::aprs_rx {
|
||||
|
||||
class APRSLogger {
|
||||
public:
|
||||
Optional<File::Error> append(const std::filesystem::path& filename) {
|
||||
@@ -50,8 +52,6 @@ class APRSLogger {
|
||||
LogFile log_file{};
|
||||
};
|
||||
|
||||
namespace ui {
|
||||
|
||||
struct APRSRecentEntry {
|
||||
using Key = uint64_t;
|
||||
|
||||
@@ -290,6 +290,6 @@ class APRSRXView : public View {
|
||||
}};
|
||||
};
|
||||
|
||||
} /* namespace ui */
|
||||
} // namespace ui::external_app::aprs_rx
|
||||
|
||||
#endif /*__UI_APRS_RX_H__*/
|
||||
+85
@@ -0,0 +1,85 @@
|
||||
/*
|
||||
* Copyright (C) 2026 PortaPack Mayhem
|
||||
*
|
||||
* 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_aprs_tx.hpp"
|
||||
#include "ui_navigation.hpp"
|
||||
#include "external_app.hpp"
|
||||
|
||||
namespace ui::external_app::aprs_tx {
|
||||
|
||||
void initialize_app(ui::NavigationView& nav) {
|
||||
nav.push<APRSTXView>();
|
||||
}
|
||||
|
||||
} // namespace ui::external_app::aprs_tx
|
||||
|
||||
extern "C" {
|
||||
|
||||
__attribute__((section(".external_app.app_aprs_tx.application_information"), used)) application_information_t _application_information_aprs_tx = {
|
||||
/*.memory_location = */ (uint8_t*)0x00000000,
|
||||
/*.externalAppEntry = */ ui::external_app::aprs_tx::initialize_app,
|
||||
/*.header_version = */ CURRENT_HEADER_VERSION,
|
||||
/*.app_version = */ VERSION_MD5,
|
||||
|
||||
/*.app_name = */ "APRS TX",
|
||||
/*.bitmap_data = */ {
|
||||
0x00,
|
||||
0x00,
|
||||
0x00,
|
||||
0x00,
|
||||
0x00,
|
||||
0x00,
|
||||
0xF0,
|
||||
0x0F,
|
||||
0x4C,
|
||||
0x32,
|
||||
0xFE,
|
||||
0x7F,
|
||||
0x25,
|
||||
0xA4,
|
||||
0x25,
|
||||
0xA4,
|
||||
0xFF,
|
||||
0xFF,
|
||||
0x25,
|
||||
0xA4,
|
||||
0x25,
|
||||
0xA4,
|
||||
0xFE,
|
||||
0x7F,
|
||||
0x4C,
|
||||
0x32,
|
||||
0xF0,
|
||||
0x0F,
|
||||
0x00,
|
||||
0x00,
|
||||
0x00,
|
||||
0x00,
|
||||
},
|
||||
/*.icon_color = */ ui::Color::green().v,
|
||||
/*.menu_location = */ app_location_t::TX,
|
||||
/*.desired_menu_position = */ -1,
|
||||
|
||||
/*.m4_app_tag = portapack::spi_flash::image_tag_afsk */ {'P', 'A', 'F', 'T'},
|
||||
/*.m4_app_offset = */ 0x00000000,
|
||||
};
|
||||
|
||||
} // extern "C"
|
||||
Vendored
+2
-2
@@ -37,7 +37,7 @@
|
||||
using namespace aprs;
|
||||
using namespace portapack;
|
||||
|
||||
namespace ui {
|
||||
namespace ui::external_app::aprs_tx {
|
||||
|
||||
void APRSTXView::focus() {
|
||||
tx_view.focus();
|
||||
@@ -242,4 +242,4 @@ APRSTXView::APRSTXView(NavigationView& nav) {
|
||||
// process_coordinates(last_lat, last_lon); //don't load last, so won't confuse users
|
||||
}
|
||||
|
||||
} /* namespace ui */
|
||||
} // namespace ui::external_app::aprs_tx
|
||||
Vendored
+2
-2
@@ -33,7 +33,7 @@
|
||||
#include "radio_state.hpp"
|
||||
#include "portapack.hpp"
|
||||
|
||||
namespace ui {
|
||||
namespace ui::external_app::aprs_tx {
|
||||
|
||||
class APRSTXView : public View {
|
||||
public:
|
||||
@@ -161,4 +161,4 @@ class APRSTXView : public View {
|
||||
}};
|
||||
};
|
||||
|
||||
} /* namespace ui */
|
||||
} // namespace ui::external_app::aprs_tx
|
||||
+20
-1
@@ -391,6 +391,22 @@ set(EXTCPPSRC
|
||||
external/tetra_rx/tetra_rcpc.cpp
|
||||
external/tetra_rx/tetra_viterbi.cpp
|
||||
|
||||
#adsb rx
|
||||
external/adsbrx/main.cpp
|
||||
external/adsbrx/ui_adsb_rx.cpp
|
||||
|
||||
#ais rx
|
||||
external/ais_rx/main.cpp
|
||||
external/ais_rx/ais_app.cpp
|
||||
|
||||
#aprs rx
|
||||
external/aprs_rx/main.cpp
|
||||
external/aprs_rx/ui_aprs_rx.cpp
|
||||
|
||||
#aprs tx
|
||||
external/aprs_tx/main.cpp
|
||||
external/aprs_tx/ui_aprs_tx.cpp
|
||||
|
||||
)
|
||||
|
||||
set(EXTAPPLIST
|
||||
@@ -485,6 +501,10 @@ set(EXTAPPLIST
|
||||
secplustx
|
||||
signal_hunter
|
||||
tetra_rx
|
||||
adsbrx
|
||||
ais_rx
|
||||
aprs_rx
|
||||
aprs_tx
|
||||
)
|
||||
|
||||
# sdusb has type conflicts with PRALINE (HackRF Pro) - add only for non-PRALINE builds
|
||||
@@ -495,4 +515,3 @@ if(NOT BOARD STREQUAL "PRALINE")
|
||||
)
|
||||
list(APPEND EXTAPPLIST sdusb)
|
||||
endif()
|
||||
|
||||
|
||||
+28
@@ -115,6 +115,10 @@ MEMORY
|
||||
ram_external_app_vor_tx (rwx) : org = 0xAE0A0000, len = 32k
|
||||
ram_external_app_signal_hunter (rwx) : org = 0xAE0B0000, len = 32k
|
||||
ram_external_app_tetra_rx (rwx) : org = 0xAE0C0000, len = 32k
|
||||
ram_external_app_adsbrx (rwx) : org = 0xAE0D0000, len = 32k
|
||||
ram_external_app_ais_rx (rwx) : org = 0xAE0E0000, len = 32k
|
||||
ram_external_app_aprs_rx (rwx) : org = 0xAE0F0000, len = 32k
|
||||
ram_external_app_aprs_tx (rwx) : org = 0xAE100000, len = 32k
|
||||
}
|
||||
|
||||
SECTIONS
|
||||
@@ -671,6 +675,30 @@ SECTIONS
|
||||
*/external/tetra_rx/*(*ui*external_app*tetra_rx*);
|
||||
} > ram_external_app_tetra_rx
|
||||
|
||||
.external_app_adsbrx : ALIGN(4) SUBALIGN(4)
|
||||
{
|
||||
KEEP(*(.external_app.app_adsbrx.application_information));
|
||||
*(*ui*external_app*adsbrx*);
|
||||
} > ram_external_app_adsbrx
|
||||
|
||||
.external_app_ais_rx : ALIGN(4) SUBALIGN(4)
|
||||
{
|
||||
KEEP(*(.external_app.app_ais_rx.application_information));
|
||||
*(*ui*external_app*ais_rx*);
|
||||
} > ram_external_app_ais_rx
|
||||
|
||||
.external_app_aprs_rx : ALIGN(4) SUBALIGN(4)
|
||||
{
|
||||
KEEP(*(.external_app.app_aprs_rx.application_information));
|
||||
*(*ui*external_app*aprs_rx*);
|
||||
} > ram_external_app_aprs_rx
|
||||
|
||||
.external_app_aprs_tx : ALIGN(4) SUBALIGN(4)
|
||||
{
|
||||
KEEP(*(.external_app.app_aprs_tx.application_information));
|
||||
*(*ui*external_app*aprs_tx*);
|
||||
} > ram_external_app_aprs_tx
|
||||
|
||||
|
||||
|
||||
}
|
||||
|
||||
@@ -88,6 +88,15 @@ void ReceiverModel::set_target_frequency(rf::Frequency f) {
|
||||
update_tuning_frequency();
|
||||
}
|
||||
|
||||
void ReceiverModel::set_target_frequency_with_hidden_offset(
|
||||
rf::Frequency f,
|
||||
rf::Frequency offset) {
|
||||
persistent_memory::set_target_frequency(f);
|
||||
settings_.frequency_app_override = f;
|
||||
hidden_offset = offset;
|
||||
update_tuning_frequency();
|
||||
}
|
||||
|
||||
uint32_t ReceiverModel::baseband_bandwidth() const {
|
||||
return settings_.baseband_bandwidth;
|
||||
}
|
||||
|
||||
@@ -66,6 +66,7 @@ class ReceiverModel {
|
||||
/* The frequency to receive (no offset). */
|
||||
rf::Frequency target_frequency() const;
|
||||
void set_target_frequency(rf::Frequency f);
|
||||
void set_target_frequency_with_hidden_offset(rf::Frequency f, rf::Frequency offset);
|
||||
|
||||
uint32_t baseband_bandwidth() const;
|
||||
void set_baseband_bandwidth(uint32_t v);
|
||||
|
||||
@@ -39,6 +39,8 @@ class BoundFrequencyField : public FrequencyField {
|
||||
|
||||
public:
|
||||
decltype(FrequencyField::on_change) updated{};
|
||||
std::function<bool(rf::Frequency)> changing{};
|
||||
std::function<void(rf::Frequency)> entered{};
|
||||
|
||||
BoundFrequencyField(Point parent_pos, NavigationView& nav)
|
||||
: FrequencyField(parent_pos) {
|
||||
@@ -47,7 +49,8 @@ class BoundFrequencyField : public FrequencyField {
|
||||
set_value(model->target_frequency());
|
||||
|
||||
on_change = [this](rf::Frequency f) {
|
||||
model->set_target_frequency(f);
|
||||
if (!changing || !changing(f))
|
||||
model->set_target_frequency(f);
|
||||
if (updated)
|
||||
updated(f);
|
||||
};
|
||||
@@ -57,7 +60,10 @@ class BoundFrequencyField : public FrequencyField {
|
||||
on_edit_shown();
|
||||
auto freq_view = nav.push<FrequencyKeypadView>(model->target_frequency());
|
||||
freq_view->on_changed = [this](rf::Frequency f) {
|
||||
set_value(f);
|
||||
if (entered)
|
||||
entered(f);
|
||||
else
|
||||
set_value(f);
|
||||
};
|
||||
nav.set_on_pop([this]() {
|
||||
if (on_edit_hidden)
|
||||
@@ -76,4 +82,4 @@ using TxFrequencyField = BoundFrequencyField<TransmitterModel, &portapack::trans
|
||||
|
||||
} // namespace ui
|
||||
|
||||
#endif // __UI_FREQ_FIELD_H__
|
||||
#endif // __UI_FREQ_FIELD_H__
|
||||
|
||||
@@ -88,16 +88,29 @@ void FrequencyScale::set_spectrum_sampling_rate(const int new_sampling_rate) {
|
||||
}
|
||||
|
||||
void FrequencyScale::set_channel_filter(
|
||||
const int offset,
|
||||
const int low_frequency,
|
||||
const int high_frequency,
|
||||
const int transition) {
|
||||
if ((channel_filter_low_frequency != low_frequency) ||
|
||||
const bool shape_changed =
|
||||
(channel_filter_low_frequency != low_frequency) ||
|
||||
(channel_filter_high_frequency != high_frequency) ||
|
||||
(channel_filter_transition != transition)) {
|
||||
(channel_filter_transition != transition);
|
||||
const bool offset_changed = channel_filter_offset != offset;
|
||||
|
||||
if (shape_changed) {
|
||||
channel_filter_offset = offset;
|
||||
channel_filter_low_frequency = low_frequency;
|
||||
channel_filter_high_frequency = high_frequency;
|
||||
channel_filter_transition = transition;
|
||||
set_dirty();
|
||||
} else if (offset_changed) {
|
||||
const auto old_offset = channel_filter_offset;
|
||||
channel_filter_offset = offset;
|
||||
if (live_tuning && spectrum_sampling_rate && drawn())
|
||||
redraw_filter_cursor(old_offset);
|
||||
else
|
||||
set_dirty();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -123,12 +136,14 @@ void FrequencyScale::paint(Painter& painter) {
|
||||
draw_filter_ranges(painter, r);
|
||||
draw_frequency_ticks(painter, r);
|
||||
|
||||
const Rect r_cursor{
|
||||
(screen_width / 2 - 2) + cursor_position, r.bottom() - filter_band_height,
|
||||
5, filter_band_height};
|
||||
painter.fill_rectangle(
|
||||
r_cursor,
|
||||
Color::red());
|
||||
if (!live_tuning) {
|
||||
const Rect r_cursor{
|
||||
(screen_width / 2 - 2) + cursor_position, r.bottom() - filter_band_height,
|
||||
5, filter_band_height};
|
||||
painter.fill_rectangle(
|
||||
r_cursor,
|
||||
Color::red());
|
||||
}
|
||||
}
|
||||
|
||||
void FrequencyScale::clear() {
|
||||
@@ -184,9 +199,79 @@ void FrequencyScale::draw_frequency_ticks(Painter& painter, const Rect r) {
|
||||
}
|
||||
}
|
||||
|
||||
void FrequencyScale::redraw_filter_cursor(const int old_offset) {
|
||||
const auto r = screen_rect();
|
||||
const auto x_center = r.width() / 2;
|
||||
const auto trans =
|
||||
channel_filter_transition * spectrum_bins / spectrum_sampling_rate;
|
||||
|
||||
const auto cursor_left = [&](const int offset) {
|
||||
return r.left() + x_center +
|
||||
(offset + channel_filter_low_frequency) *
|
||||
spectrum_bins / spectrum_sampling_rate -
|
||||
trans;
|
||||
};
|
||||
const auto cursor_right = [&](const int offset) {
|
||||
return r.left() + x_center +
|
||||
(offset + channel_filter_high_frequency) *
|
||||
spectrum_bins / spectrum_sampling_rate +
|
||||
trans;
|
||||
};
|
||||
|
||||
const auto dirty_left =
|
||||
std::min(cursor_left(old_offset), cursor_left(channel_filter_offset)) - 1;
|
||||
const auto dirty_right =
|
||||
std::max(cursor_right(old_offset), cursor_right(channel_filter_offset)) + 1;
|
||||
const Rect dirty{
|
||||
dirty_left,
|
||||
r.bottom() - filter_band_height,
|
||||
dirty_right - dirty_left,
|
||||
filter_band_height};
|
||||
|
||||
Painter painter;
|
||||
painter.fill_rectangle(dirty, Theme::getInstance()->bg_darkest->background);
|
||||
draw_filter_ranges(painter, r);
|
||||
restore_tick_lines(painter, r, dirty);
|
||||
}
|
||||
|
||||
void FrequencyScale::restore_tick_lines(
|
||||
Painter& painter,
|
||||
const Rect r,
|
||||
const Rect dirty) {
|
||||
const auto draw_if_dirty = [&](const Coord x) {
|
||||
if (x >= dirty.left() && x < dirty.right()) {
|
||||
painter.fill_rectangle(
|
||||
{x, dirty.top(), 1, dirty.height()},
|
||||
Theme::getInstance()->bg_darkest->foreground);
|
||||
}
|
||||
};
|
||||
|
||||
const auto x_center = r.left() + r.width() / 2;
|
||||
draw_if_dirty(x_center);
|
||||
|
||||
constexpr int tick_count_max = 4;
|
||||
float rough_tick_interval = float(spectrum_sampling_rate) / tick_count_max;
|
||||
int magnitude = 1;
|
||||
while (rough_tick_interval >= 10.0f) {
|
||||
rough_tick_interval /= 10;
|
||||
magnitude *= 10;
|
||||
}
|
||||
|
||||
const int tick_interval = std::ceil(rough_tick_interval);
|
||||
auto tick_offset = tick_interval;
|
||||
while ((tick_offset * magnitude) < spectrum_sampling_rate / 2) {
|
||||
const Dim pixel_offset =
|
||||
tick_offset * magnitude * spectrum_bins / spectrum_sampling_rate;
|
||||
draw_if_dirty(x_center - pixel_offset);
|
||||
draw_if_dirty(x_center + pixel_offset);
|
||||
tick_offset += tick_interval;
|
||||
}
|
||||
}
|
||||
|
||||
void FrequencyScale::draw_filter_ranges(Painter& painter, const Rect r) {
|
||||
if (channel_filter_low_frequency != channel_filter_high_frequency) {
|
||||
const auto x_center = r.width() / 2;
|
||||
const auto x_center = r.width() / 2 +
|
||||
channel_filter_offset * spectrum_bins / spectrum_sampling_rate;
|
||||
|
||||
const auto x_low = x_center + channel_filter_low_frequency * spectrum_bins / spectrum_sampling_rate;
|
||||
const auto x_high = x_center + channel_filter_high_frequency * spectrum_bins / spectrum_sampling_rate;
|
||||
@@ -220,6 +305,11 @@ void FrequencyScale::on_blur() {
|
||||
}
|
||||
|
||||
bool FrequencyScale::on_encoder(const EncoderEvent delta) {
|
||||
if (live_tuning) {
|
||||
if (on_select) on_select(delta);
|
||||
return true;
|
||||
}
|
||||
|
||||
cursor_position += delta;
|
||||
|
||||
cursor_position = std::min<int32_t>(cursor_position, screen_width / 2 - 1);
|
||||
@@ -326,9 +416,11 @@ WaterfallView::WaterfallView(const bool cursor) {
|
||||
frequency_scale.focus(); // focus on frequency scale to show cursor
|
||||
|
||||
if (sampling_rate) {
|
||||
// screen x to frequency scale x, NB we need two widgets align
|
||||
int32_t cursor_position = x - (screen_width / 2);
|
||||
frequency_scale.set_cursor_position(cursor_position);
|
||||
const int32_t cursor_position = x - (screen_width / 2);
|
||||
if (!frequency_scale.is_live_tuning()) {
|
||||
// screen x to frequency scale x, NB we need two widgets align
|
||||
frequency_scale.set_cursor_position(cursor_position);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
@@ -405,6 +497,7 @@ void WaterfallView::on_channel_spectrum(const ChannelSpectrum& spectrum) {
|
||||
sampling_rate = spectrum.sampling_rate;
|
||||
frequency_scale.set_spectrum_sampling_rate(sampling_rate);
|
||||
frequency_scale.set_channel_filter(
|
||||
spectrum.channel_filter_offset,
|
||||
spectrum.channel_filter_low_frequency,
|
||||
spectrum.channel_filter_high_frequency,
|
||||
spectrum.channel_filter_transition);
|
||||
|
||||
@@ -72,6 +72,8 @@ class AudioSpectrumView : public View {
|
||||
|
||||
class FrequencyScale : public Widget {
|
||||
public:
|
||||
/* Receives a frequency offset in Hz for key/touch selection. With live
|
||||
* tuning enabled, encoder events instead pass the raw encoder delta. */
|
||||
std::function<void(int32_t offset)> on_select{};
|
||||
|
||||
void on_show() override;
|
||||
@@ -83,8 +85,10 @@ class FrequencyScale : public Widget {
|
||||
bool on_touch(const TouchEvent touch) override;
|
||||
|
||||
void set_spectrum_sampling_rate(const int new_sampling_rate);
|
||||
void set_channel_filter(const int low_frequency, const int high_frequency, const int transition);
|
||||
void set_channel_filter(const int offset, const int low_frequency, const int high_frequency, const int transition);
|
||||
void set_cursor_position(const int32_t position);
|
||||
void set_live_tuning(const bool enabled) { live_tuning = enabled; }
|
||||
bool is_live_tuning() const { return live_tuning; }
|
||||
|
||||
void paint(Painter& painter) override;
|
||||
|
||||
@@ -94,15 +98,19 @@ class FrequencyScale : public Widget {
|
||||
int32_t cursor_position{0};
|
||||
int spectrum_sampling_rate{0};
|
||||
const int spectrum_bins = std::tuple_size<decltype(ChannelSpectrum::db)>::value;
|
||||
int channel_filter_offset{0};
|
||||
int channel_filter_low_frequency{0};
|
||||
int channel_filter_high_frequency{0};
|
||||
int channel_filter_transition{0};
|
||||
bool live_tuning{false};
|
||||
|
||||
void clear();
|
||||
void clear_background(Painter& painter, const Rect r);
|
||||
|
||||
void draw_frequency_ticks(Painter& painter, const Rect r);
|
||||
void draw_filter_ranges(Painter& painter, const Rect r);
|
||||
void redraw_filter_cursor(const int old_offset);
|
||||
void restore_tick_lines(Painter& painter, const Rect r, const Rect dirty);
|
||||
};
|
||||
|
||||
/* NB: These visualizations rely on having a baseband image running.
|
||||
@@ -147,6 +155,7 @@ class WaterfallView : public View {
|
||||
void set_parent_rect(const Rect new_parent_rect) override;
|
||||
void show_audio_spectrum_view(const bool show);
|
||||
void load_gradient();
|
||||
void set_live_tuning(const bool enabled) { frequency_scale.set_live_tuning(enabled); }
|
||||
|
||||
private:
|
||||
void update_widgets_rect();
|
||||
|
||||
@@ -30,9 +30,6 @@
|
||||
#include "portapack.hpp"
|
||||
|
||||
#include "ui_about_simple.hpp"
|
||||
#include "ui_adsb_rx.hpp"
|
||||
#include "ui_aprs_rx.hpp"
|
||||
#include "ui_aprs_tx.hpp"
|
||||
#include "ui_btle_rx.hpp"
|
||||
#include "ui_debug.hpp"
|
||||
#include "ui_encoders.hpp"
|
||||
@@ -60,7 +57,6 @@
|
||||
#include "ui_battinfo.hpp"
|
||||
#include "ui_external_items_menu_loader.hpp"
|
||||
|
||||
#include "ais_app.hpp"
|
||||
#include "analog_audio_app.hpp"
|
||||
#include "ble_rx_app.hpp"
|
||||
#include "ble_tx_app.hpp"
|
||||
@@ -99,9 +95,6 @@ const NavigationView::AppList NavigationView::appList = {
|
||||
{nullptr, "Games", HOME, Color::cyan(), &bitmap_icon_games, new ViewFactory<GamesMenuView>()},
|
||||
{nullptr, "Settings", HOME, Color::cyan(), &bitmap_icon_setup, new ViewFactory<SettingsMenuView>()},
|
||||
/* RX ********************************************************************/
|
||||
{"adsbrx", "ADS-B", RX, Color::green(), &bitmap_icon_adsb, new ViewFactory<ADSBRxView>()},
|
||||
{"ais", "AIS Boats", RX, Color::green(), &bitmap_icon_ais, new ViewFactory<AISAppView>()},
|
||||
{"aprsrx", "APRS", RX, Color::green(), &bitmap_icon_aprs, new ViewFactory<APRSRXView>()},
|
||||
{"audio", "Audio", RX, Color::green(), &bitmap_icon_speaker, new ViewFactory<AnalogAudioView>()},
|
||||
{"blerx", "BLE Rx", RX, Color::green(), &bitmap_icon_btle, new ViewFactory<BLERxView>()},
|
||||
{"pocsag", "POCSAG", RX, Color::green(), &bitmap_icon_pocsag, new ViewFactory<POCSAGAppView>()},
|
||||
@@ -110,7 +103,6 @@ const NavigationView::AppList NavigationView::appList = {
|
||||
{"subghzd", "SubGhzD", RX, Color::yellow(), &bitmap_icon_remote, new ViewFactory<SubGhzDView>()},
|
||||
{"weather", "Weather", RX, Color::green(), &bitmap_icon_thermometer, new ViewFactory<WeatherView>()},
|
||||
/* TX ********************************************************************/
|
||||
{"aprstx", "APRS TX", TX, ui::Color::green(), &bitmap_icon_aprs, new ViewFactory<APRSTXView>()},
|
||||
{"bletx", "BLE Tx", TX, ui::Color::green(), &bitmap_icon_btle, new ViewFactory<BLETxView>()},
|
||||
{"ooktx", "OOK", TX, ui::Color::yellow(), &bitmap_icon_remote, new ViewFactory<EncodersView>()},
|
||||
{"rdstx", "RDS", TX, ui::Color::green(), &bitmap_icon_rds, new ViewFactory<RDSView>()},
|
||||
|
||||
Reference in New Issue
Block a user