Compare commits

...

7 Commits

Author SHA1 Message Date
Synray 245a36459f UI and bug fixes (#3164)
* Fix BLE RX RSSI and Channel scale

* Properly reset gradient index 0

* Use move constructor in MenuView::add_item

* Avoid erasing during iteration

* Fix gradient rounding. Extend to end of table

* Add comment to add_item
2026-05-10 00:08:05 +02:00
janhaverkamp 590f19062b Add support for larger battery capacities (#3168) 2026-05-09 20:54:25 +02:00
Totoo f38ef4ebc8 Added CU8 playback support (#3166) 2026-05-09 12:23:43 +02:00
gullradriel 900d2f2482 fix missing header (#3167) 2026-05-09 12:23:10 +02:00
Totoo 5aff5ac277 Fix text color multiline handling (#3161) 2026-05-08 09:57:06 +02:00
Totoo cf4d245964 Fixes #3159 (#3160) 2026-05-07 23:54:30 +08:00
Pezsma c7f7173958 sleep warning fix (#3162) 2026-05-07 15:26:34 +02:00
16 changed files with 104 additions and 41 deletions
+2 -2
View File
@@ -322,10 +322,10 @@ class BLERxView : public View {
{UI_POS_X(21), UI_POS_Y(0)}};
RSSI rssi{
{UI_POS_X(24), 0, UI_POS_WIDTH_REMAINING(6), 4}};
{UI_POS_X(24), 0, UI_POS_WIDTH_REMAINING(24), 4}};
Channel channel{
{UI_POS_X(24), 5, UI_POS_WIDTH_REMAINING(6), 4}};
{UI_POS_X(24), 5, UI_POS_WIDTH_REMAINING(24), 4}};
Labels label_sort{
{{UI_POS_X(0), UI_POS_Y(1)}, "Sort:", Theme::getInstance()->fg_light->foreground}};
+4
View File
@@ -43,6 +43,7 @@ namespace ui {
static const fs::path txt_ext{u".TXT"};
static const fs::path ppl_ext{u".PPL"};
static const fs::path c8_ext{u".C8"};
static const fs::path cu8_ext{u".CU8"};
static const fs::path c16_ext{u".C16"};
static const fs::path cxx_ext{u".C*"};
static const fs::path png_ext{u".PNG"};
@@ -88,8 +89,11 @@ fs::path get_partner_file(fs::path path) {
path.replace_extension(txt_ext);
else if (path_iequal(ext, txt_ext)) {
path.replace_extension(c8_ext);
if (!fs::file_exists(path))
path.replace_extension(cu8_ext);
if (!fs::file_exists(path))
path.replace_extension(c16_ext);
} else
return {};
+1
View File
@@ -80,6 +80,7 @@ class FileManBaseView : public View {
{u".PNG", &bitmap_icon_file_image, ui::Color::green()},
{u".BMP", &bitmap_icon_file_image, ui::Color::green()},
{u".C8", &bitmap_icon_file_iq, ui::Color::dark_cyan()},
{u".CU8", &bitmap_icon_file_iq, ui::Color::dark_cyan()},
{u".C16", &bitmap_icon_file_iq, ui::Color::dark_cyan()},
{u".WAV", &bitmap_icon_file_wav, ui::Color::dark_magenta()},
{u".PPL", &bitmap_icon_file_iq, ui::Color::white()}, // Playlist/Replay
+1
View File
@@ -172,6 +172,7 @@ void IQTrimView::update_range_controls(iq::TrimRange trim_range) {
void IQTrimView::profile_capture() {
power_buckets_.clear();
power_buckets_.resize(screen_width);
iq::PowerBuckets buckets{
.p = power_buckets_.data(),
.size = power_buckets_.size()};
-8
View File
@@ -268,17 +268,11 @@ void EventDispatcher::charge_deep_sleep(const bool sleep) {
// Save interrupt states before mass disable
uint32_t saved_iser0 = NVIC->ISER[0];
uint32_t saved_iser1 = NVIC->ISER[1];
uint32_t saved_iser2 = NVIC->ISER[2];
// Disable and clear all pending interrupts
NVIC->ICER[0] = 0xFFFFFFFF;
NVIC->ICER[1] = 0xFFFFFFFF;
NVIC->ICER[2] = 0xFFFFFFFF;
NVIC->ICPR[0] = 0xFFFFFFFF;
NVIC->ICPR[1] = 0xFFFFFFFF;
NVIC->ICPR[2] = 0xFFFFFFFF;
// Re-enable only necessary wakeup sources
NVIC_EnableIRQ(RTC_IRQn);
@@ -326,8 +320,6 @@ void EventDispatcher::charge_deep_sleep(const bool sleep) {
// Restore original interrupt enable states
NVIC->ISER[0] = saved_iser0;
NVIC->ISER[1] = saved_iser1;
NVIC->ISER[2] = saved_iser2;
} // End of while(1) deep sleep loop
} else {
@@ -193,15 +193,12 @@ void WaterfallDesignerView::on_profile_changed(std::filesystem::path new_profile
auto reader = FileLineReader(playlist_file);
for (const auto& line : reader) {
// remove empty lines
if (line == "\n" || line == "\r\n" || line == "\r") continue;
profile_levels.push_back(line);
}
for (auto& line : profile_levels) {
// remove empty lines
if (line == "\n" || line == "\r\n" || line == "\r") {
profile_levels.erase(std::remove(profile_levels.begin(), profile_levels.end(), line), profile_levels.end());
}
// remove line end \n etc
if (line.length() > 0 && (line[line.length() - 1] == '\n' || line[line.length() - 1] == '\r')) {
line = line.substr(0, line.length() - 1);
+4 -1
View File
@@ -30,6 +30,7 @@
namespace fs = std::filesystem;
static const fs::path c8_ext{u".C8"};
static const fs::path cu8_ext{u".CU8"};
static const fs::path c16_ext{u".C16"};
Optional<File::Error> File::open_fatfs(const std::filesystem::path& filename, BYTE mode) {
@@ -543,12 +544,14 @@ bool path_iequal(
bool is_cxx_capture_file(const path& filename) {
auto ext = filename.extension();
return path_iequal(c8_ext, ext) || path_iequal(c16_ext, ext);
return path_iequal(c8_ext, ext) || path_iequal(cu8_ext, ext) || path_iequal(c16_ext, ext);
}
uint8_t capture_file_sample_size(const path& filename) {
if (path_iequal(filename.extension(), c8_ext))
return sizeof(complex8_t);
if (path_iequal(filename.extension(), cu8_ext))
return sizeof(complexu8_t);
if (path_iequal(filename.extension(), c16_ext))
return sizeof(complex16_t);
return 0;
+18 -10
View File
@@ -28,13 +28,11 @@ namespace fs = std::filesystem;
const std::filesystem::path default_gradient_file = u"waterfall.txt";
Gradient::Gradient() {
prev_index = 0;
prev_r = 0;
prev_g = 0;
prev_b = 0;
step(0, 0, 0, 0);
}
void Gradient::set_default() {
step(0, 0, 0, 0);
step(86, 0, 0, 255);
step(171, 0, 255, 0);
step(255, 255, 0, 0);
@@ -47,6 +45,9 @@ bool Gradient::load_file(const std::filesystem::path& file_path) {
if (error)
return false;
// make sure the table starts at 0
step(0, 0, 0, 0);
auto reader = FileLineReader(gradient_file);
for (const auto& line : reader) {
if (line.length() == 0 || line[0] == '#')
@@ -73,17 +74,24 @@ bool Gradient::load_file(const std::filesystem::path& file_path) {
}
}
// extend the gradient from the current index to the end of the table
step(255, prev_r, prev_g, prev_b);
return true;
}
void Gradient::step(int16_t index, int16_t r, int16_t g, int16_t b) {
for (int16_t i = prev_index; i <= index; i++) {
float x = (float)(i - prev_index) / (index - prev_index);
float y = 1.0f - x;
// prevent division by 0 if index == prev_index
int16_t range = index - prev_index;
if (range == 0) range = 1;
for (int16_t i = prev_index; i <= index; i++) {
int16_t t = i - prev_index;
int16_t new_r = prev_r + (t * (r - prev_r) + range / 2) / range;
int16_t new_g = prev_g + (t * (g - prev_g) + range / 2) / range;
int16_t new_b = prev_b + (t * (b - prev_b) + range / 2) / range;
int16_t new_r = prev_r * y + r * x;
int16_t new_g = prev_g * y + g * x;
int16_t new_b = prev_b * y + b * x;
if (i <= 255) lut[i] = ui::Color(new_r, new_g, new_b);
}
+23 -1
View File
@@ -24,6 +24,7 @@
namespace fs = std::filesystem;
static const fs::path c8_ext = u".C8";
static const fs::path cu8_ext = u".CU8";
namespace file_convert {
@@ -59,21 +60,42 @@ void c8_to_c16(const void* buffer, File::Size bytes) {
}
}
// Convert cu8 buffer to c16 buffer.
// Same buffer used for input & output; input size is bytes; output size is 2*bytes.
void cu8_to_c16(const void* buffer, File::Size bytes) {
complexu8_t* src = (complexu8_t*)buffer;
complex16_t* dest = (complex16_t*)buffer;
uint32_t i = bytes / sizeof(complexu8_t);
if (i != 0) {
do {
i--;
int16_t re_out = ((int16_t)src[i].real() - 128) * 256;
int16_t im_out = ((int16_t)src[i].imag() - 128) * 256;
dest[i] = {(int16_t)re_out, (int16_t)im_out};
} while (i != 0);
}
}
} /* namespace file_convert */
// Automatically enables C8/C16 conversion based on file extension
Optional<File::Error> FileConvertReader::open(const std::filesystem::path& filename) {
convert_c8_to_c16 = path_iequal(filename.extension(), c8_ext);
convert_cu8_to_c16 = path_iequal(filename.extension(), cu8_ext);
return file_.open(filename);
}
// If C8 conversion enabled, half the number of bytes are read from the file & expanded to fill the whole buffer.
File::Result<File::Size> FileConvertReader::read(void* const buffer, const File::Size bytes) {
auto read_result = file_.read(buffer, convert_c8_to_c16 ? bytes / 2 : bytes);
auto read_result = file_.read(buffer, (convert_c8_to_c16 || convert_cu8_to_c16) ? bytes / 2 : bytes);
if (read_result.is_ok()) {
if (convert_c8_to_c16) {
file_convert::c8_to_c16(buffer, read_result.value());
read_result = read_result.value() * 2;
} else if (convert_cu8_to_c16) {
file_convert::cu8_to_c16(buffer, read_result.value());
read_result = read_result.value() * 2;
}
bytes_read_ += read_result.value();
}
+1
View File
@@ -52,6 +52,7 @@ class FileConvertReader : public stream::Reader {
const File& file() const& { return file_; }
bool convert_c8_to_c16{};
bool convert_cu8_to_c16{};
protected:
File file_{};
+4 -5
View File
@@ -159,16 +159,15 @@ size_t MenuView::item_count() const {
return menu_items.size();
}
void MenuView::add_item(MenuItem new_item) {
menu_items.push_back(new_item);
void MenuView::add_item(MenuItem&& new_item) {
menu_items.push_back(std::move(new_item));
update_items();
}
void MenuView::add_items(std::initializer_list<MenuItem> new_items) {
for (auto item : new_items) {
add_item(item);
}
menu_items.insert(menu_items.end(), new_items);
update_items();
}
void MenuView::update_items() {
+4 -1
View File
@@ -83,7 +83,10 @@ class MenuView : public View {
~MenuView();
void add_item(MenuItem new_item);
// This function takes a temporary to avoid copies and heap allocations.
// To pass a variable 'item', use add_item(std::move(item)),
// or construct the MenuItem in the call, add_item(MenuItem{...})
void add_item(MenuItem&& new_item);
void add_items(std::initializer_list<MenuItem> new_items);
void clear();
size_t item_count() const;
+1
View File
@@ -27,6 +27,7 @@
#include <libopencm3/lpc43xx/rgu.h>
#include <libopencm3/lpc43xx/wwdt.h>
#include "delay.h"
#include "string.h"
volatile bool usb_bulk_block_done = false;
+2
View File
@@ -106,10 +106,12 @@ struct complex<int16_t> {
} /* namespace std */
using complex8_t = std::complex<int8_t>;
using complexu8_t = std::complex<uint8_t>;
using complex16_t = std::complex<int16_t>;
using complex32_t = std::complex<int32_t>;
static_assert(sizeof(complex8_t) == 2, "complex8_t size wrong");
static_assert(sizeof(complexu8_t) == 2, "complexu8_t size wrong");
static_assert(sizeof(complex16_t) == 4, "complex16_t size wrong");
static_assert(sizeof(complex32_t) == 8, "complex32_t size wrong");
@@ -47,7 +47,7 @@
// to check battery mins and maxes in pmem, and validity
#define BATT_18650_MIN_MAH 1000
#define BATT_18650_MAX_MAH 5000
#define BATT_18650_MAX_MAH 20000
using namespace modems;
using namespace serializer;
+36 -7
View File
@@ -393,27 +393,56 @@ void Text::getAccessibilityText(std::string& result) {
void Text::getWidgetName(std::string& result) {
result = "Text";
}
void Text::paint(Painter& painter) {
const auto rect = screen_rect();
auto s = has_focus() ? style().invert() : style();
painter.fill_rectangle(rect, s.background);
const int char_width = s.font.char_width();
const int line_height = s.font.line_height();
auto default_style = has_focus() ? style().invert() : style();
painter.fill_rectangle(rect, default_style.background);
const int char_width = default_style.font.char_width();
const int line_height = default_style.font.line_height();
if (char_width == 0 || line_height == 0) return;
const size_t chars_per_line = rect.width() / char_width;
if (chars_per_line == 0) return;
size_t lines_capacity = rect.height() / line_height;
if (lines_capacity == 0) lines_capacity = 1; // at least one line, even if it overflows vertically
if (lines_capacity == 0) lines_capacity = 1;
auto text_view = std::string_view{text};
size_t current_offset = 0;
// Track the active text color across line breaks
Color active_color = default_style.foreground;
for (size_t line_idx = 0; line_idx < lines_capacity; ++line_idx) {
if (current_offset >= text_view.length()) break;
size_t chunk_len = std::min(chars_per_line, text_view.length() - current_offset);
size_t scan_offset = current_offset;
size_t printable_count = 0;
Color next_color = active_color; // This will hold the color state as we scan the current chunk
// Scan forward to find chunk length AND track color changes
while (scan_offset < text_view.length() && printable_count < chars_per_line) {
if (text_view[scan_offset] == '\x1B') {
scan_offset++;
if (scan_offset < text_view.length()) {
uint8_t color_idx = text_view[scan_offset];
// Mirror the logic from Painter::draw_string
if (color_idx < std::size(term_colors)) {
next_color = term_colors[color_idx];
} else {
next_color = active_color;
}
scan_offset++;
}
} else {
scan_offset++;
printable_count++;
}
}
size_t chunk_len = scan_offset - current_offset;
// Create a temporary style for this line, initialized with the carried-over color
Style line_style = {.font = default_style.font, .background = default_style.background, .foreground = active_color};
painter.draw_string(
rect.location() + Point(0, line_idx * line_height),
s,
line_style,
text_view.substr(current_offset, chunk_len));
current_offset += chunk_len;
// Carry the final color state of this line into the start of the next line
active_color = next_color;
}
}