Compare commits

...

7 Commits

Author SHA1 Message Date
Kyle Reed 29e495a17f Freqman UI (#1255)
* FreqmanDB direct file
* Clear UI for short lists
* Final touches on freqlist UI.
* Support vertical alignment in NewButton
* New buttons in FreqMan
* Wiring up UI to filewrapper actions
* Work around empty file
2023-07-11 22:48:36 +02:00
Tragic nihilist 0c599f7d3a Changelog fix 3rd way (#1259)
* init_commit_for_changelog_fix_2nd_way

* 2nd_commit_for_changelog_fix_2nd_way

* methods-3

* add_more_exception
2023-07-11 09:51:22 +02:00
jLynx 006e4c9fce Smaller SD card ZIP (#1257)
* Update create_nightly_release.yml

* Update create_stable_release.yml
2023-07-10 19:43:39 +12:00
Kyle Reed 63be4de418 Add some basic validation to freqman parsing (#1256)
Co-authored-by: kallanreed <kallanreed@noreply.github.com>
2023-07-09 11:15:14 -07:00
Kyle Reed 497ca3f934 Refactor freqman_db parsing (#1244)
* WIP freqman changes/memory perf/stash
* Split ui tone_key function out for testing.
* Add more tests and fix bugs.
* Use default max_entries in recond
* Set limit back to 90 for now
2023-07-08 22:04:12 +02:00
Mark Thompson 60de625c37 Added fonts viewer (debug) app (#1251)
* Added fonts viewer (debug) app

* Added fonts viewer (debug) app

* Clang

* Use degree symbol vs asterisk for latitude/longitude degrees

* Save a few bytes by not overriding on_key

* Save a few bytes by not overriding on_key

* Clang
2023-07-08 10:26:05 -05:00
gullradriel a09c0e4c2d Recon and Level fix (#1250)
* Level: Fix for SPEC to other modes
* Recon: Better difference between RAW and AUDIO. Launch Capture instead of audio in SPEC mode
2023-07-07 22:21:48 +02:00
46 changed files with 2331 additions and 1285 deletions
+62 -8
View File
@@ -1,19 +1,50 @@
import os
import re
import sys
import json
from urllib.error import HTTPError
raw_git = os.popen('git log next --since="24 hours" --pretty=format:"- %h - {USERNAME}*+%al-%an*: %s"').read()
main_repo_url = "https://github.com/eried/portapack-mayhem.git"
main_repo_identifier = "eried/portapack-mayhem"
# ^^&^ change these if main repo changed
os.system('gh repo set-default ' + main_repo_url)
raw_git = os.popen('git log next --since="1200 hours" --pretty=format:"- ^%h^ - {USERNAME}*_%al_%an*: %s"').read()
# ^ as github's rule, a real username can contains "-" but not "_" and "*", so use these two to seperate things.
# ^ during test at 2023-07-10, %al could return 3 different types of values:
# : 123+abc <<<< abc is real username
# : def <<<< def is 3rd level domain of unhidden email, drop this, fetch info from github cli client
# : note that github will probably change in the future
def compute_username(line):
stripped = re.search(r'(?<=\*)(.*?)(?=\*)', line).group(0)
line_org = line
stripped = re.search(r'(?<=\*)(.*?)(?=\*)', line).group(0) # get the string between "*" and "*"
now_item_hash = re.search(r'(?<=\^)(.*?)(?=\^)', line).group(0) # get the string between "*" and "*"
stripped_org = stripped
stripped = re.search(r'(?<=\_)(.*?)(?=\_)', stripped).group(0) # get the string between "_" and "_"
stripped_fallback = stripped
# now it's like "123+abc" or "def"
if ("+" in stripped): # 123+abc
stripped = re.sub(r'^.*?\+', "", stripped)
elif not ("+" in stripped): # maybe not real username, dropped, fetch from github cli client
fetched_now_item_json = os.popen(
'gh search commits repo:' + main_repo_identifier + ' --json author --hash ' + now_item_hash).read()
stripped = extract_first_login(fetched_now_item_json)
if stripped is False: # 403
return "@" + stripped_fallback
elif stripped == "app/": # user did their commit with github app
return "@" + stripped_fallback
else: # exception and edge cases
return "@" + stripped_fallback
if stripped is None: # if commit hash can't find AKA commit is only in fork
return "@" + stripped_fallback
pattern = re.compile("[$@+&?].*[$@+&?]")
if pattern.match(stripped):
stripped = re.sub("[$@+&?].*[$@+&?]", "", stripped)
stripped = re.match(r'.+?(?=-)', stripped).group(0)
else:
stripped = re.sub(r'^.*?-', "", stripped)
return "@" + stripped
@@ -21,8 +52,31 @@ def compile_line(line):
username = compute_username(line)
line = re.sub(r'[*].*[*]', "", line)
line = line.replace("{USERNAME}", username)
line = re.sub(r'\^', '', line, count=2)
return line
def extract_first_login(json_data):
if not json_data: # if returned null
return None
try:
data = json.loads(json_data)
if isinstance(data, list) and len(data) > 0:
first_object = data[0]
if 'author' in first_object and 'login' in first_object['author']:
return first_object['author']['login']
except HTTPError as e:
if e.code == 403 and "API rate limit exceeded" in str(e):
return False
except json.decoder.JSONDecodeError:
return None
return None
for row in raw_git.splitlines():
print(compile_line(row))
@@ -47,6 +47,9 @@ jobs:
run: mkdir ${{ github.workspace }}/build
- name: Run the Docker image
run: docker run -e VERSION_STRING=${{ steps.version_date.outputs.date }} -i -v ${{ github.workspace }}:/havoc portapack-dev
- name: Create Small SD Card ZIP - No World Map
run: |
mkdir -p sdcard/FIRMWARE && cp build/firmware/portapack-h1_h2-mayhem.bin sdcard/FIRMWARE/portapack-mayhem_${{ steps.version_date.outputs.date }}.bin && zip -r sdcard-no-map.zip sdcard
- name: Download world map
run: |
wget https://github.com/eried/portapack-mayhem/releases/download/world_map/world_map.zip
@@ -60,6 +63,8 @@ jobs:
run: |
mkdir -p sdcard/FIRMWARE && cp build/firmware/portapack-h1_h2-mayhem.bin sdcard/FIRMWARE/portapack-mayhem_${{ steps.version_date.outputs.date }}.bin && zip -r sdcard.zip sdcard
- name: Create changelog
env:
GH_TOKEN: ${{ secrets.GH_TOKEN }}
run: |
CHANGELOG=$(python3 .github/workflows/changelog.py)
CHANGELOG="${CHANGELOG//'%'/'%25'}"
@@ -103,3 +108,13 @@ jobs:
asset_path: ./sdcard.zip
asset_name: mayhem_nightly_${{ steps.version_date.outputs.date }}_COPY_TO_SDCARD.zip
asset_content_type: application/zip
- name: Upload SD Card Assets - No Map
id: upload-sd-card-asset-no-map
uses: actions/upload-release-asset@v1
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
upload_url: ${{ steps.create_release.outputs.upload_url }}
asset_path: ./sdcard-no-map.zip
asset_name: mayhem_nightly_${{ steps.version_date.outputs.date }}_COPY_TO_SDCARD-no-world-map.zip
asset_content_type: application/zip
@@ -31,6 +31,9 @@ jobs:
run: mkdir ${{ github.workspace }}/build
- name: Run the Docker image
run: docker run -e VERSION_STRING=${{ steps.version.outputs.version }} -i -v ${{ github.workspace }}:/havoc portapack-dev
- name: Create Small SD Card ZIP - No World Map
run: |
mkdir -p sdcard/FIRMWARE && cp build/firmware/portapack-h1_h2-mayhem.bin sdcard/FIRMWARE/portapack-mayhem_${{ steps.version.outputs.version }}.bin && zip -r sdcard-no-map.zip sdcard
- name: Download world map
run: |
wget https://github.com/eried/portapack-mayhem/releases/download/world_map/world_map.zip
@@ -44,6 +47,8 @@ jobs:
run: |
mkdir -p sdcard/FIRMWARE && cp build/firmware/portapack-h1_h2-mayhem.bin sdcard/FIRMWARE/portapack-mayhem_${{ steps.version.outputs.version }}.bin && zip -r sdcard.zip sdcard
- name: Create changelog
env:
GH_TOKEN: ${{ secrets.GH_TOKEN }}
run: |
CHANGELOG=$(python3 .github/workflows/stable_changelog.py ${{ steps.past_version.outputs.past_version }})
CHANGELOG="${CHANGELOG//'%'/'%25'}"
@@ -96,4 +101,14 @@ jobs:
asset_path: ./sdcard.zip
asset_name: mayhem_${{ steps.version.outputs.version }}_COPY_TO_SDCARD.zip
asset_content_type: application/zip
- name: Upload SD Card Assets - No Map
id: upload-sd-card-asset-no-map
uses: actions/upload-release-asset@v1
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
upload_url: ${{ steps.create_release.outputs.upload_url }}
asset_path: ./sdcard-no-map.zip
asset_name: mayhem_${{ steps.version.outputs.version }}_COPY_TO_SDCARD-no-world-map.zip
asset_content_type: application/zip
+62 -8
View File
@@ -1,21 +1,52 @@
import os
import re
import sys
import json
from urllib.error import HTTPError
past_version = sys.argv[1]
raw_git = os.popen('git log ' + past_version + '..next --pretty=format:"- %h - {USERNAME}*+%al-%an*: %s"').read()
main_repo_url = "https://github.com/eried/portapack-mayhem.git"
main_repo_identifier = "eried/portapack-mayhem"
# ^^&^ change these if main repo changed
os.system('gh repo set-default ' + main_repo_url)
raw_git = os.popen('git log ' + past_version + '..next --pretty=format:"- ^%h^ - {USERNAME}*_%al_%an*: %s"').read()
# ^ as github's rule, a real username can contains "-" but not "_" and "*", so use these two to seperate things
# during test at 2023-07-10, %al could return 3 different types of values:
# 123+abc <<<< abc is real username
# def <<<< def is 3rd level domain of unhidden email, drop this, fetch info from github cli client
# note that github will probably change in the future
def compute_username(line):
stripped = re.search(r'(?<=\*)(.*?)(?=\*)', line).group(0)
line_org = line
stripped = re.search(r'(?<=\*)(.*?)(?=\*)', line).group(0) # get the string between "*" and "*"
now_item_hash = re.search(r'(?<=\^)(.*?)(?=\^)', line).group(0) # get the string between "*" and "*"
stripped_org = stripped
stripped = re.search(r'(?<=\_)(.*?)(?=\_)', stripped).group(0) # get the string between "_" and "_"
stripped_fallback = stripped
# now it's like "123+abc" or "def"
if ("+" in stripped): # 123+abc
stripped = re.sub(r'^.*?\+', "", stripped)
elif not ("+" in stripped): # maybe not real username, dropped, fetch from github cli client
fetched_now_item_json = os.popen(
'gh search commits repo:' + main_repo_identifier + ' --json author --hash ' + now_item_hash).read()
stripped = extract_first_login(fetched_now_item_json)
if stripped is False: # 403
return "@" + stripped_fallback
elif stripped == "app/":
return "@" + stripped_fallback
else: # exception and edge cases
return "@" + stripped_fallback
if stripped is None: # if commit hash can't find AKA commit is only in fork
return "@" + stripped_fallback
pattern = re.compile("[$@+&?].*[$@+&?]")
if pattern.match(stripped):
stripped = re.sub("[$@+&?].*[$@+&?]", "", stripped)
stripped = re.match(r'.+?(?=-)', stripped).group(0)
else:
stripped = re.sub(r'^.*?-', "", stripped)
return "@" + stripped
@@ -23,8 +54,31 @@ def compile_line(line):
username = compute_username(line)
line = re.sub(r'[*].*[*]', "", line)
line = line.replace("{USERNAME}", username)
line = re.sub(r'\^', '', line, count=2)
return line
def extract_first_login(json_data):
if not json_data: # if returned null
return None
try:
data = json.loads(json_data)
if isinstance(data, list) and len(data) > 0:
first_object = data[0]
if 'author' in first_object and 'login' in first_object['author']:
return first_object['author']['login']
except HTTPError as e:
if e.code == 403 and "API rate limit exceeded" in str(e):
return False
except json.decoder.JSONDecodeError:
return None
return None
for row in raw_git.splitlines():
print(compile_line(row))
+2
View File
@@ -177,6 +177,7 @@ set(CPPSRC
event_m0.cpp
file_reader.cpp
file.cpp
freqman_db.cpp
freqman.cpp
io_file.cpp
io_wave.cpp
@@ -234,6 +235,7 @@ set(CPPSRC
ui/ui_styles.cpp
ui/ui_tabview.cpp
ui/ui_textentry.cpp
ui/ui_tone_key.cpp
ui/ui_transmitter.cpp
apps/ui_about_simple.cpp
apps/ui_adsb_rx.cpp
@@ -25,6 +25,7 @@
#include "soundboard_app.hpp"
#include "string_format.hpp"
#include "tonesets.hpp"
#include "ui_tone_key.hpp"
using namespace tonekey;
using namespace portapack;
@@ -29,7 +29,6 @@
#include "baseband_api.hpp"
#include "lfsr_random.hpp"
#include "io_wave.hpp"
#include "tone_key.hpp"
#include "app_settings.hpp"
#include "radio_state.hpp"
+41
View File
@@ -30,6 +30,9 @@
#include "audio.hpp"
#include "ui_sd_card_debug.hpp"
#include "ui_font_fixed_8x16.hpp"
#include "ui_styles.hpp"
#include "ui_painter.hpp"
#include "portapack.hpp"
#include "portapack_persistent_memory.hpp"
@@ -401,6 +404,7 @@ DebugMenuView::DebugMenuView(NavigationView& nav) {
{"Temperature", ui::Color::dark_cyan(), &bitmap_icon_temperature, [&nav]() { nav.push<TemperatureView>(); }},
{"Buttons Test", ui::Color::dark_cyan(), &bitmap_icon_controls, [&nav]() { nav.push<DebugControlsView>(); }},
{"P.Memory", ui::Color::dark_cyan(), &bitmap_icon_memory, [&nav]() { nav.push<DebugPmemView>(); }},
{"Fonts Viewer", ui::Color::dark_cyan(), &bitmap_icon_notepad, [&nav]() { nav.push<DebugFontsView>(); }},
{"Debug Dump", ui::Color::dark_cyan(), &bitmap_icon_memory, [&nav]() { portapack::persistent_memory::debug_dump(); }},
});
set_max_rows(2); // allow wider buttons
@@ -462,6 +466,43 @@ uint32_t DebugPmemView::registers_widget_feed(const size_t register_number) {
return data.regfile[(page_size * page + register_number) / 4] >> (register_number % 4 * 8);
}
/* DebugFontsView *******************************************************/
uint16_t DebugFontsView::display_font(Painter& painter, uint16_t y_offset, const Style* font_style, std::string_view font_name) {
auto char_width{font_style->font.char_width()};
auto char_height{font_style->font.line_height()};
auto cpl{((screen_width / char_width) - 6) & 0xF8}; // Display a multiple of 8 characters per line
uint16_t line_pos{y_offset};
painter.draw_string({0, y_offset}, *font_style, font_name);
// Displaying ASCII+extended characters from 0x20 to 0xFF
for (uint8_t c = 0; c <= 0xDF; c++) {
line_pos = y_offset + ((c / cpl) + 2) * char_height;
if ((c % cpl) == 0)
painter.draw_string({0, line_pos}, *font_style, "Ox" + to_string_hex(c + 0x20, 2));
painter.draw_char({((c % cpl) + 5) * char_width, line_pos}, *font_style, (char)(c + 0x20));
}
return line_pos + char_height;
}
void DebugFontsView::paint(Painter& painter) {
int16_t line_pos;
line_pos = display_font(painter, 32, &Styles::white, "Fixed 8x16");
display_font(painter, line_pos + 16, &Styles::white_small, "Fixed 5x8");
}
DebugFontsView::DebugFontsView(NavigationView& nav)
: nav_{nav} {
set_focusable(true);
}
/* DebugLCRView *******************************************************/
/*DebugLCRView::DebugLCRView(NavigationView& nav, std::string lcr_string) {
std::string debug_text;
+11
View File
@@ -310,6 +310,17 @@ class DebugPmemView : public View {
uint32_t registers_widget_feed(const size_t register_number);
};
class DebugFontsView : public View {
public:
DebugFontsView(NavigationView& nav);
void paint(Painter& painter) override;
std::string title() const override { return "Fonts"; };
private:
uint16_t display_font(Painter& painter, uint16_t y_offset, const Style* font_style, std::string_view font_name);
NavigationView& nav_;
};
/*class DebugLCRView : public View {
public:
DebugLCRView(NavigationView& nav, std::string lcrstring);
+198 -193
View File
@@ -1,6 +1,7 @@
/*
* Copyright (C) 2015 Jared Boone, ShareBrained Technology, Inc.
* Copyright (C) 2016 Furrtek
* Copyright (C) 2023 Kyle Reed
*
* This file is part of PortaPack.
*
@@ -22,280 +23,284 @@
#include "ui_freqman.hpp"
#include "portapack.hpp"
#include "event_m0.hpp"
#include "portapack.hpp"
#include "rtc_time.hpp"
#include "utility.hpp"
#include <memory>
using namespace portapack;
namespace fs = std::filesystem;
namespace ui {
static int32_t current_category_id = 0;
/* FreqManBaseView ***************************************/
size_t FreqManBaseView::current_category_index = 0;
FreqManBaseView::FreqManBaseView(
NavigationView& nav)
: nav_(nav) {
add_children({&options_category,
&label_category,
&button_exit});
add_children(
{&label_category,
&options_category,
&button_exit});
// initialize
refresh_list();
options_category.on_change = [this](size_t category_id, int32_t) {
change_category(category_id);
options_category.on_change = [this](size_t new_index, int32_t) {
change_category(new_index);
};
options_category.set_selected_index(current_category_id);
button_exit.on_select = [this, &nav](Button&) {
nav.pop();
};
refresh_categories();
};
void FreqManBaseView::focus() {
button_exit.focus();
// TODO: Shouldn't be on focus.
if (error_ == ERROR_ACCESS) {
nav_.display_modal("Error", "File acces error", ABORT, nullptr);
nav_.display_modal("Error", "File access error", ABORT, nullptr);
} else if (error_ == ERROR_NOFILES) {
nav_.display_modal("Error", "No database files\nin /freqman", ABORT, nullptr);
nav_.display_modal("Error", "No database files\nin /FREQMAN", ABORT, nullptr);
} else {
options_category.focus();
}
}
void FreqManBaseView::get_freqman_files() {
std::vector<std::string>().swap(file_list);
void FreqManBaseView::change_category(size_t new_index) {
if (categories().empty())
return;
auto files = scan_root_files(u"FREQMAN", u"*.TXT");
for (auto file : files) {
std::string file_name = file.stem().string();
// don't propose tmp / hidden files in freqman's list
if (file_name.length() && file_name[0] != '.') {
file_list.emplace_back(file_name);
}
}
};
void FreqManBaseView::change_category(int32_t category_id) {
current_category_id = category_id;
if (file_list.empty()) return;
if (!load_freqman_file(file_list[categories[category_id].second], database)) {
current_category_index = new_index;
if (!db_.open(get_freqman_path(current_category()))) {
error_ = ERROR_ACCESS;
}
freqlist_view.set_db(database);
text_empty.hidden(!database.empty());
set_dirty();
freqlist_view.set_db(db_);
}
void FreqManBaseView::refresh_list() {
categories.clear();
get_freqman_files();
void FreqManBaseView::refresh_categories() {
OptionsField::options_t new_categories;
for (size_t n = 0; n < file_list.size(); n++)
categories.emplace_back(std::make_pair(file_list[n].substr(0, 14), n));
scan_root_files(
freqman_dir, u"*.TXT", [&new_categories](const fs::path& path) {
// Skip temp/hidden files.
if (path.empty() || path.native()[0] == u'.')
return;
// Alphabetical sort
std::sort(categories.begin(), categories.end(), [](auto& left, auto& right) {
// The UI layer will truncate long file names when displaying.
new_categories.emplace_back(path.stem().string(), new_categories.size());
});
// Alphabetically sort the categories.
std::sort(new_categories.begin(), new_categories.end(), [](auto& left, auto& right) {
return left.first < right.first;
});
options_category.set_options(categories);
if ((unsigned)current_category_id >= categories.size())
current_category_id = categories.size() - 1;
// Preserve last selection; ensure in range.
current_category_index = clip(current_category_index, 0u, new_categories.size());
auto saved_index = current_category_index;
options_category.set_options(std::move(new_categories));
options_category.set_selected_index(saved_index);
}
void FrequencySaveView::save_current_file() {
save_freqman_file(file_list[categories[current_category_id].second], database);
nav_.pop();
void FreqManBaseView::refresh_list(int delta_selected) {
// Update the index and ensures in bounds.
freqlist_view.set_index(freqlist_view.get_index() + delta_selected);
freqlist_view.set_dirty();
}
void FrequencySaveView::on_save_name() {
text_prompt(nav_, desc_buffer, 28, [this](std::string& buffer) {
database.push_back({value_, 0, buffer, SINGLE});
save_current_file();
});
}
void FrequencySaveView::on_save_timestamp() {
database.push_back({value_, 0, live_timestamp.string(), SINGLE});
save_current_file();
}
/* FrequencySaveView *************************************/
FrequencySaveView::FrequencySaveView(
NavigationView& nav,
const rf::Frequency value)
: FreqManBaseView(nav),
value_(value) {
desc_buffer.reserve(28);
// Todo: add back ?
/*for (size_t n = 0; n < database.size(); n++) {
if (database[n].value == value_) {
error_ = ERROR_DUPLICATE;
break;
}
}*/
add_children({&labels,
&big_display,
&button_save_name,
&button_save_timestamp,
&live_timestamp});
big_display.set(value);
button_save_name.on_select = [this, &nav](Button&) {
on_save_name();
};
button_save_timestamp.on_select = [this, &nav](Button&) {
on_save_timestamp();
};
options_category.on_change = [this, value](size_t category_id, int32_t) {
change_category(category_id);
big_display.set(value);
};
}
FrequencyLoadView::~FrequencyLoadView() {
std::vector<freqman_entry>().swap(database);
}
void FrequencyLoadView::refresh_widgets(const bool v) {
freqlist_view.hidden(v);
text_empty.hidden(!v);
// display.fill_rectangle(freqlist_view.screen_rect(), Color::black());
set_dirty();
}
FrequencyLoadView::FrequencyLoadView(
NavigationView& nav)
: FreqManBaseView(nav) {
on_refresh_widgets = [this](bool v) {
refresh_widgets(v);
add_children(
{&labels,
&big_display,
&button_clear,
&button_edit,
&button_save,
&text_description});
entry_.type = freqman_type::Single;
entry_.frequency_a = value;
entry_.description = to_string_timestamp(rtc_time::now());
refresh_ui();
button_clear.on_select = [this, &nav](Button&) {
entry_.description = "";
refresh_ui();
};
add_children({&freqlist_view,
&text_empty});
button_edit.on_select = [this, &nav](Button&) {
temp_buffer_ = entry_.description;
text_prompt(nav_, temp_buffer_, 30, [this](std::string& new_desc) {
entry_.description = new_desc;
refresh_ui();
});
};
// Resize menu view to fill screen
freqlist_view.set_parent_rect({0, 3 * 8, 240, 30 * 8});
freqlist_view.on_select = [&nav, this](FreqManUIList&) {
auto& entry = database[freqlist_view.get_index()];
if (entry.type == RANGE) {
// User chose a frequency range entry
if (on_range_loaded)
on_range_loaded(entry.frequency_a, entry.frequency_b);
else if (on_frequency_loaded)
on_frequency_loaded(entry.frequency_a);
// TODO: Maybe return center of range if user choses a range when the app needs a unique frequency, instead of frequency_a ?
} else {
// User chose an unique frequency entry
if (on_frequency_loaded)
on_frequency_loaded(entry.frequency_a);
}
// swap with empty vector to ensure memory is immediately released
std::vector<freqman_entry>().swap(database);
button_save.on_select = [this, &nav](Button&) {
db_.insert_entry(entry_, db_.entry_count());
nav_.pop();
};
}
void FrequencyManagerView::on_edit_freq(rf::Frequency f) {
database[freqlist_view.get_index()].frequency_a = f;
save_freqman_file(file_list[categories[current_category_id].second], database);
change_category(current_category_id);
void FrequencySaveView::refresh_ui() {
big_display.set(entry_.frequency_a);
text_description.set(entry_.description);
}
void FrequencyManagerView::on_edit_desc(NavigationView& nav) {
text_prompt(nav, desc_buffer, 28, [this](std::string& buffer) {
database[freqlist_view.get_index()].description = buffer;
save_freqman_file(file_list[categories[current_category_id].second], database);
change_category(current_category_id);
/* FrequencyLoadView *************************************/
FrequencyLoadView::FrequencyLoadView(
NavigationView& nav)
: FreqManBaseView(nav) {
add_children({&freqlist_view});
// Resize to fill screen. +2 keeps text out of border.
freqlist_view.set_parent_rect({0, 3 * 8, screen_width, 15 * 16 + 2});
freqlist_view.on_select = [&nav, this](size_t index) {
auto entry = db_[index];
// TODO: Maybe return center of range if user choses a range when the app
// needs a unique frequency, instead of frequency_a?
auto has_range = entry.type == freqman_type::Range ||
entry.type == freqman_type::HamRadio;
if (on_range_loaded && has_range)
on_range_loaded(entry.frequency_a, entry.frequency_b);
else if (on_frequency_loaded)
on_frequency_loaded(entry.frequency_a);
nav_.pop(); // NB: this will call dtor.
};
freqlist_view.on_leave = [this]() {
button_exit.focus();
};
}
/* FrequencyManagerView **********************************/
void FrequencyManagerView::on_edit_freq() {
// TODO: range edit support?
auto freq_edit_view = nav_.push<FrequencyKeypadView>(current_entry().frequency_a);
freq_edit_view->on_changed = [this](rf::Frequency f) {
auto entry = current_entry();
entry.frequency_a = f;
db_.replace_entry(current_index(), entry);
freqlist_view.set_dirty();
};
}
void FrequencyManagerView::on_edit_desc() {
temp_buffer_ = current_entry().description;
text_prompt(nav_, temp_buffer_, 28, [this](std::string& new_desc) {
auto entry = current_entry();
entry.description = std::move(new_desc);
db_.replace_entry(current_index(), entry);
freqlist_view.set_dirty();
});
}
void FrequencyManagerView::on_new_category(NavigationView& nav) {
text_prompt(nav, desc_buffer, 12, [this](std::string& buffer) {
File freqman_file;
create_freqman_file(buffer, freqman_file);
refresh_list();
change_category(current_category_id);
void FrequencyManagerView::on_add_category() {
temp_buffer_.clear();
text_prompt(nav_, temp_buffer_, 12, [this](std::string& new_name) {
if (!new_name.empty()) {
create_freqman_file(new_name);
refresh_categories();
}
});
}
void FrequencyManagerView::on_delete() {
if (database.empty()) {
delete_freqman_file(file_list[categories[current_category_id].second]);
refresh_list();
} else {
database.erase(database.begin() + freqlist_view.get_index());
save_freqman_file(file_list[categories[current_category_id].second], database);
}
change_category(current_category_id);
void FrequencyManagerView::on_del_category() {
nav_.push<ModalMessageView>(
"Delete", "Delete " + current_category() + "\nAre you sure?", YESNO,
[this](bool choice) {
if (choice) {
db_.close(); // Ensure file is closed.
auto path = get_freqman_path(current_category());
delete_file(path);
refresh_categories();
}
});
}
void FrequencyManagerView::refresh_widgets(const bool v) {
button_edit_freq.hidden(v);
button_edit_desc.hidden(v);
button_delete.hidden(v);
text_empty.hidden(!v);
freqlist_view.hidden(v);
labels.hidden(v);
// display.fill_rectangle(freqlist_view.screen_rect(), Color::black());
set_dirty();
void FrequencyManagerView::on_add_entry() {
freqman_entry entry{
.frequency_a = 100'000'000,
.description = std::string{"Entry "} + to_string_dec_uint(db_.entry_count()),
.type = freqman_type::Single,
};
// Add will insert below the currently selected item.
db_.insert_entry(entry, current_index() + 1);
refresh_list(1);
}
FrequencyManagerView::~FrequencyManagerView() {
// save_freqman_file(file_list[categories[current_category_id].second], database);
void FrequencyManagerView::on_del_entry() {
if (db_.empty())
return;
nav_.push<ModalMessageView>(
"Delete", "Delete" + pretty_string(current_entry(), 23) + "\nAre you sure?", YESNO,
[this](bool choice) {
if (choice) {
db_.delete_entry(current_index());
refresh_list();
}
});
}
FrequencyManagerView::FrequencyManagerView(
NavigationView& nav)
: FreqManBaseView(nav) {
on_refresh_widgets = [this](bool v) {
refresh_widgets(v);
add_children(
{&freqlist_view,
&labels,
&button_add_category,
&button_del_category,
&button_edit_freq,
&button_edit_desc,
&button_add_entry,
&button_del_entry});
freqlist_view.on_select = [this](size_t) {
button_edit_freq.focus();
};
add_children({&labels,
&button_new_category,
&freqlist_view,
&text_empty,
&button_edit_freq,
&button_edit_desc,
&button_delete});
freqlist_view.on_select = [this](FreqManUIList&) {
// Allows for quickly exiting control.
freqlist_view.on_leave = [this]() {
button_edit_freq.focus();
};
button_new_category.on_select = [this, &nav](Button&) {
desc_buffer = "";
on_new_category(nav);
button_add_category.on_select = [this]() {
on_add_category();
};
button_edit_freq.on_select = [this, &nav](Button&) {
if (database.empty()) {
database.push_back({0, 0, "", SINGLE});
}
auto new_view = nav.push<FrequencyKeypadView>(database[freqlist_view.get_index()].frequency_a);
new_view->on_changed = [this](rf::Frequency f) {
on_edit_freq(f);
};
button_del_category.on_select = [this]() {
on_del_category();
};
button_edit_desc.on_select = [this, &nav](Button&) {
if (database.empty()) {
database.push_back({0, 0, "", SINGLE});
}
desc_buffer = database[freqlist_view.get_index()].description;
on_edit_desc(nav);
button_edit_freq.on_select = [this](Button&) {
on_edit_freq();
};
button_delete.on_select = [this, &nav](Button&) {
on_delete();
button_edit_desc.on_select = [this](Button&) {
on_edit_desc();
};
button_add_entry.on_select = [this]() {
on_add_entry();
};
button_del_entry.on_select = [this]() {
on_del_entry();
};
}
+84 -64
View File
@@ -1,6 +1,7 @@
/*
* Copyright (C) 2015 Jared Boone, ShareBrained Technology, Inc.
* Copyright (C) 2016 Furrtek
* Copyright (C) 2023 Kyle Reed
*
* This file is part of PortaPack.
*
@@ -20,15 +21,16 @@
* Boston, MA 02110-1301, USA.
*/
#include "freqman.hpp"
#include "freqman_db.hpp"
#include "ui.hpp"
#include "ui_widget.hpp"
#include "ui_painter.hpp"
#include "ui_freqlist.hpp"
#include "ui_menu.hpp"
#include "ui_navigation.hpp"
#include "ui_painter.hpp"
#include "ui_receiver.hpp"
#include "ui_textentry.hpp"
#include "freqman.hpp"
#include "ui_freqlist.hpp"
#include "ui_widget.hpp"
namespace ui {
@@ -40,74 +42,77 @@ class FreqManBaseView : public View {
void focus() override;
protected:
using option_t = std::pair<std::string, int32_t>;
using options_t = std::vector<option_t>;
using options_t = OptionsField::options_t;
NavigationView& nav_;
freqman_error error_{NO_ERROR};
options_t categories{};
std::function<void(void)> on_select_frequency{nullptr};
std::function<void(bool)> on_refresh_widgets{nullptr};
void get_freqman_files();
void change_category(int32_t category_id);
void refresh_list();
void change_category(size_t new_index);
/* Access the categories directly from the OptionsField.
* This avoids holding multiple copies of the file list. */
const options_t& categories() const { return options_category.options(); }
const auto& current_category() const { return options_category.selected_index_name(); }
auto current_index() const { return freqlist_view.get_index(); }
freqman_entry current_entry() const { return db_[current_index()]; }
void refresh_categories();
void refresh_list(int delta_selected = 0);
freqman_db database{};
std::vector<std::string> file_list{};
FreqmanDB db_{};
/* The top section (category) is 20px tall. */
Labels label_category{
{{0, 4}, "Category:", Color::light_grey()}};
{{0, 2}, "Category:", Color::light_grey()}};
OptionsField options_category{
{9 * 8, 4},
14,
{9 * 8, 2},
14 /* length */,
{}};
FreqManUIList freqlist_view{
{0, 3 * 8, 240, 23 * 8}};
Text text_empty{
{7 * 8, 12 * 8, 16 * 8, 16},
"Empty category !",
};
{0, 3 * 8, screen_width, 12 * 16 + 2 /* 2 Keeps text out of border. */}};
Button button_exit{
{16 * 8, 34 * 8, 14 * 8, 4 * 8},
{15 * 8, 17 * 16, 15 * 8, 2 * 16},
"Exit"};
private:
protected:
/* Static so selected category is persisted across UI instances. */
static size_t current_category_index;
};
// TODO: support for new category.
class FrequencySaveView : public FreqManBaseView {
public:
FrequencySaveView(NavigationView& nav, const rf::Frequency value);
std::string title() const override { return "Save freq."; };
std::string title() const override { return "Save freq"; };
private:
std::string desc_buffer{};
rf::Frequency value_{};
std::string temp_buffer_{};
freqman_entry entry_{};
void on_save_name();
void on_save_timestamp();
void save_current_file();
void refresh_ui();
BigFrequency big_display{
{4, 2 * 16, 28 * 8, 32},
{0, 2 * 16, 28 * 8, 4 * 16},
0};
Labels labels{
{{1 * 8, 12 * 8}, "Save as:", Color::white()}};
{{0 * 8, 6 * 16}, "Description:", Color::white()}};
Button button_save_name{
{1 * 8, 17 * 8, 12 * 8, 48},
"Name (set)"};
Button button_save_timestamp{
{1 * 8, 25 * 8, 12 * 8, 48},
"Timestamp:"};
LiveDateTime live_timestamp{
{14 * 8, 27 * 8, 16 * 8, 16}};
Text text_description{{0 * 8, 7 * 16, 30 * 8, 1 * 16}};
Button button_clear{
{4 * 8, 10 * 16, 10 * 8, 2 * 16},
"Clear"};
Button button_edit{
{16 * 8, 10 * 16, 10 * 8, 2 * 16},
"Edit"};
Button button_save{
{0 * 8, 17 * 16, 15 * 8, 2 * 16},
"Save"};
};
class FrequencyLoadView : public FreqManBaseView {
@@ -116,47 +121,62 @@ class FrequencyLoadView : public FreqManBaseView {
std::function<void(rf::Frequency, rf::Frequency)> on_range_loaded{};
FrequencyLoadView(NavigationView& nav);
~FrequencyLoadView();
std::string title() const override { return "Load freq."; };
private:
void refresh_widgets(const bool v);
std::string title() const override { return "Load freq"; };
};
class FrequencyManagerView : public FreqManBaseView {
public:
FrequencyManagerView(NavigationView& nav);
~FrequencyManagerView();
std::string title() const override { return "Freqman"; };
private:
std::string desc_buffer{};
std::string temp_buffer_{};
void refresh_widgets(const bool v);
void on_edit_freq(rf::Frequency f);
void on_edit_desc(NavigationView& nav);
void on_new_category(NavigationView& nav);
void on_delete();
void on_edit_freq();
void on_edit_desc();
void on_add_category();
void on_del_category();
void on_add_entry();
void on_del_entry();
Labels labels{
{{4 * 8 + 4, 26 * 8}, "Edit:", Color::light_grey()}};
{{5 * 8, 14 * 16 - 4}, "Edit:", Color::light_grey()}};
Button button_new_category{
{23 * 8, 2, 7 * 8, 20},
"New"};
NewButton button_add_category{
{23 * 8, 0 * 16, 7 * 4, 20},
{},
&bitmap_icon_new_file,
Color::white(),
true};
NewButton button_del_category{
{26 * 8 + 4, 0 * 16, 7 * 4, 20},
{},
&bitmap_icon_trash,
Color::red(),
true};
Button button_edit_freq{
{0 * 8, 29 * 8, 14 * 8, 32},
{0 * 8, 15 * 16, 15 * 8, 2 * 16},
"Frequency"};
Button button_edit_desc{
{0 * 8, 34 * 8, 14 * 8, 32},
{0 * 8, 17 * 16, 15 * 8, 2 * 16},
"Description"};
Button button_delete{
{16 * 8, 29 * 8, 14 * 8, 32},
"Delete"};
NewButton button_add_entry{
{15 * 8, 15 * 16, 7 * 8 + 4, 2 * 16},
{},
&bitmap_icon_add,
Color::white(),
true};
NewButton button_del_entry{
{22 * 8 + 4, 15 * 16, 7 * 8 + 4, 2 * 16},
{},
&bitmap_icon_delete,
Color::red(),
true};
};
} /* namespace ui */
+7
View File
@@ -26,6 +26,7 @@
#include "file.hpp"
using namespace portapack;
using namespace tonekey;
using portapack::memory::map::backup_ram;
namespace ui {
@@ -212,6 +213,12 @@ size_t LevelView::change_mode(freqman_index_t new_mod) {
default:
break;
}
if (new_mod != SPEC_MODULATION) {
// reset receiver model to fix bug when going from SPEC to audio, the sound is distorded
receiver_model.set_sampling_rate(3072000);
receiver_model.set_baseband_bandwidth(1750000);
}
return step_mode.selected_index();
}
+6 -7
View File
@@ -22,21 +22,20 @@
#include "ui_mictx.hpp"
#include "baseband_api.hpp"
#include "audio.hpp"
#include "wm8731.hpp"
using wolfson::wm8731::WM8731;
#include "tonesets.hpp"
#include "baseband_api.hpp"
#include "irq_controls.hpp"
#include "portapack_hal.hpp"
#include "string_format.hpp"
#include "irq_controls.hpp"
#include "tonesets.hpp"
#include "ui_tone_key.hpp"
#include "wm8731.hpp"
#include <cstring>
using namespace tonekey;
using namespace portapack;
using wolfson::wm8731::WM8731;
WM8731 audio_codec_wm8731{i2c0, 0x1a};
-1
View File
@@ -35,7 +35,6 @@
#include "ui_navigation.hpp"
#include "ui_receiver.hpp"
#include "transmitter_model.hpp"
#include "tone_key.hpp"
#include "message.hpp"
#include "receiver_model.hpp"
#include "ui_transmitter.hpp"
+153 -171
View File
@@ -23,13 +23,24 @@
#include "ui_recon.hpp"
#include "ui_fileman.hpp"
#include "capture_app.hpp"
#include "file.hpp"
#include "tone_key.hpp"
using namespace portapack;
using namespace tonekey;
using portapack::memory::map::backup_ram;
namespace ui {
bool ReconView::current_is_valid() {
return (unsigned)current_index < frequency_list.size();
}
freqman_entry& ReconView::current_entry() {
return *frequency_list[current_index];
}
void ReconView::set_loop_config(bool v) {
continuous = v;
button_loop_config.set_style(v ? &Styles::green : &Styles::white);
@@ -38,6 +49,10 @@ void ReconView::set_loop_config(bool v) {
void ReconView::recon_stop_recording() {
if (is_recording) {
if (field_mode.selected_index_value() == SPEC_MODULATION)
button_audio_app.set_text("RAW");
else
button_audio_app.set_text("AUDIO");
button_audio_app.set_style(&Styles::white);
record_view->stop();
button_config.set_style(&Styles::white); // disable config while recording as it's causing an IO error pop up at exit
@@ -51,22 +66,41 @@ void ReconView::clear_freqlist_for_ui_action() {
audio::output::stop();
// flag to detect and reload frequency_list
if (!manual_mode) {
// clear and shrink_to_fit are not enough to really start with a new, clean, empty vector
// swap is the only way to achieve a perfect memory liberation
std::vector<freqman_entry>().swap(frequency_list);
// Clear doesn't actually free, re-assign so destructor runs on previous instance.
frequency_list = freqman_db{};
} else
frequency_list.shrink_to_fit();
freqlist_cleared_for_ui_action = true;
}
void ReconView::reset_indexes() {
last_entry.modulation = -1;
last_entry.bandwidth = -1;
last_entry.step = -1;
description = "...no description...";
last_entry.modulation = freqman_invalid_index;
last_entry.bandwidth = freqman_invalid_index;
last_entry.step = freqman_invalid_index;
current_index = 0;
}
void ReconView::update_description() {
if (frequency_list.empty() || current_entry().description.empty()) {
description = "...no description...";
return;
} else {
switch (current_entry().type) {
case freqman_type::Range:
description = "R: ";
break;
case freqman_type::HamRadio:
description = "H: ";
break;
default:
description = "S: ";
}
description += current_entry().description;
}
desc_cycle.set(description);
}
void ReconView::colorize_waits() {
// colorize wait on match
if (wait == 0) {
@@ -93,18 +127,18 @@ void ReconView::colorize_waits() {
bool ReconView::recon_save_freq(const std::string& freq_file_path, size_t freq_index, bool warn_if_exists) {
File recon_file;
if (frequency_list.size() == 0 || (frequency_list.size() && current_index > (int32_t)frequency_list.size()))
if (frequency_list.size() == 0 || !current_is_valid())
return false;
freqman_entry entry = frequency_list[freq_index];
freqman_entry entry = *frequency_list[freq_index]; // Makes a copy.
entry.frequency_a = freq;
entry.frequency_b = 0;
entry.modulation = last_entry.modulation;
entry.bandwidth = last_entry.bandwidth;
entry.type = SINGLE;
entry.type = freqman_type::Single;
std::string frequency_to_add;
get_freq_string(entry, frequency_to_add);
// TODO: Use FreqmanDB
auto frequency_to_add = to_freqman_string(entry);
auto result = recon_file.open(freq_file_path); // First recon if freq is already in txt
if (!result.is_valid()) {
@@ -171,7 +205,7 @@ bool ReconView::recon_load_config_from_sd() {
pos = line_start;
while ((line_end = strstr(line_start, "\x0A"))) {
length = line_end - line_start - 1;
params[it] = string(pos, length);
params[it] = std::string(pos, length);
it++;
line_start = line_end + 1;
pos = line_start;
@@ -306,53 +340,38 @@ void ReconView::handle_retune() {
receiver_model.set_target_frequency(freq); // Retune
}
if (frequency_list.size() > 0) {
if (last_entry.modulation != frequency_list[current_index].modulation && frequency_list[current_index].modulation >= 0) {
last_entry.modulation = frequency_list[current_index].modulation;
field_mode.set_selected_index(frequency_list[current_index].modulation);
last_entry.bandwidth = -1;
if (last_entry.modulation != current_entry().modulation && is_valid(current_entry().modulation)) {
last_entry.modulation = current_entry().modulation;
field_mode.set_selected_index(current_entry().modulation);
last_entry.bandwidth = freqman_invalid_index;
}
// Set bandwidth if any
if (last_entry.bandwidth != frequency_list[current_index].bandwidth && frequency_list[current_index].bandwidth >= 0) {
last_entry.bandwidth = frequency_list[current_index].bandwidth;
field_bw.set_selected_index(frequency_list[current_index].bandwidth);
if (last_entry.bandwidth != current_entry().bandwidth && is_valid(current_entry().bandwidth)) {
last_entry.bandwidth = current_entry().bandwidth;
field_bw.set_selected_index(current_entry().bandwidth);
}
if (last_entry.step != frequency_list[current_index].step && frequency_list[current_index].step >= 0) {
last_entry.step = frequency_list[current_index].step;
if (last_entry.step != current_entry().step && is_valid(current_entry().step)) {
last_entry.step = current_entry().step;
step = freqman_entry_get_step_value(last_entry.step);
step_mode.set_selected_index(step);
}
if (last_index != current_index) {
last_index = current_index;
if ((int32_t)frequency_list.size() && current_index < (int32_t)frequency_list.size() && frequency_list[current_index].type == RANGE) {
if (frequency_list.size() && current_is_valid() && current_entry().type == freqman_type::Range) {
if (update_ranges && !manual_mode) {
button_manual_start.set_text(to_string_short_freq(frequency_list[current_index].frequency_a));
frequency_range.min = frequency_list[current_index].frequency_a;
if (frequency_list[current_index].frequency_b != 0) {
button_manual_end.set_text(to_string_short_freq(frequency_list[current_index].frequency_b));
frequency_range.max = frequency_list[current_index].frequency_b;
button_manual_start.set_text(to_string_short_freq(current_entry().frequency_a));
frequency_range.min = current_entry().frequency_a;
if (current_entry().frequency_b != 0) {
button_manual_end.set_text(to_string_short_freq(current_entry().frequency_b));
frequency_range.max = current_entry().frequency_b;
} else {
button_manual_end.set_text(to_string_short_freq(frequency_list[current_index].frequency_a));
frequency_range.max = frequency_list[current_index].frequency_a;
button_manual_end.set_text(to_string_short_freq(current_entry().frequency_a));
frequency_range.max = current_entry().frequency_a;
}
}
}
text_cycle.set_text(to_string_dec_uint(current_index + 1, 3));
if (frequency_list[current_index].description.size() > 0) {
switch (frequency_list[current_index].type) {
case RANGE:
desc_cycle.set("R: " + frequency_list[current_index].description); // Show new description
break;
case HAMRADIO:
desc_cycle.set("H: " + frequency_list[current_index].description); // Show new description
break;
default:
case SINGLE:
desc_cycle.set("S: " + frequency_list[current_index].description); // Show new description
break;
}
} else {
desc_cycle.set("...no description..."); // Show new description
}
update_description();
}
}
}
@@ -550,7 +569,10 @@ ReconView::ReconView(NavigationView& nav)
button_audio_app.on_select = [this](Button&) {
auto settings = receiver_model.settings();
settings.frequency_step = step_mode.selected_index_value();
nav_.replace<AnalogAudioView>(settings);
if (field_mode.selected_index_value() == SPEC_MODULATION)
nav_.replace<CaptureAppView>();
else
nav_.replace<AnalogAudioView>(settings);
};
button_loop_config.on_select = [this](Button&) {
@@ -568,11 +590,11 @@ ReconView::ReconView(NavigationView& nav)
// TODO: *BUG* Both transmitter_model and receiver_model share the same pmem setting for target_frequency.
button_mic_app.on_select = [this](Button&) {
if (frequency_list.size() > 0 && current_index >= 0 && (unsigned)current_index < frequency_list.size()) {
if (frequency_list[current_index].type == HAMRADIO) {
// if it's a HAMRADIO entry, then frequency_a is the freq at which the repeater receives, so we have to set it in transmit in mic app
transmitter_model.set_target_frequency(frequency_list[current_index].frequency_a);
// if it's a HAMRADIO entry, then frequency_b is the freq at which the repeater transmits, so we have to set it in receive in mic app
receiver_model.set_target_frequency(frequency_list[current_index].frequency_b);
if (current_entry().type == freqman_type::HamRadio) {
// if it's a HamRadio entry, then frequency_a is the freq at which the repeater receives, so we have to set it in transmit in mic app
transmitter_model.set_target_frequency(current_entry().frequency_a);
// if it's a HamRadio entry, then frequency_b is the freq at which the repeater transmits, so we have to set it in receive in mic app
receiver_model.set_target_frequency(current_entry().frequency_b);
} else {
// it's single or range so we us actual tuned frequency
transmitter_model.set_target_frequency(freq);
@@ -585,6 +607,7 @@ ReconView::ReconView(NavigationView& nav)
};
button_remove.on_select = [this](ButtonWithEncoder&) {
// TODO: Use FreqmanDB
if (frequency_list.size() > 0) {
if (!manual_mode) {
// scanner or recon (!scanner) mode
@@ -597,23 +620,8 @@ ReconView::ReconView(NavigationView& nav)
current_index = frequency_list.size() - 1;
}
if (frequency_list.size() > 0) {
if (frequency_list[current_index].description.size() > 0) {
switch (frequency_list[current_index].type) {
case RANGE:
desc_cycle.set("R: " + frequency_list[current_index].description);
break;
case HAMRADIO:
desc_cycle.set("H: " + frequency_list[current_index].description);
break;
default:
case SINGLE:
desc_cycle.set("S: " + frequency_list[current_index].description);
break;
}
} else {
desc_cycle.set("...no description...");
}
text_cycle.set_text(to_string_dec_uint(current_index + 1, 3));
update_description();
}
// also remove from output file if in scanner mode
if (scanner_mode) {
@@ -622,8 +630,7 @@ ReconView::ReconView(NavigationView& nav)
auto result = freqman_file.create(freq_file_path);
if (!result.is_valid()) {
for (size_t n = 0; n < frequency_list.size(); n++) {
std::string line;
get_freq_string(frequency_list[n], line);
auto line = to_freqman_string(*frequency_list[n]);
freqman_file.write_line(line);
}
}
@@ -633,16 +640,15 @@ ReconView::ReconView(NavigationView& nav)
File recon_file{};
File tmp_recon_file{};
std::string tmp_freq_file_path{freq_file_path + ".TMP"};
std::string frequency_to_add{};
freqman_entry entry = frequency_list[current_index];
freqman_entry entry = current_entry();
entry.frequency_a = freq;
entry.frequency_b = 0;
entry.modulation = last_entry.modulation;
entry.bandwidth = last_entry.bandwidth;
entry.type = SINGLE;
entry.type = freqman_type::Single;
get_freq_string(entry, frequency_to_add);
auto frequency_to_add = to_freqman_string(entry);
delete_file(tmp_freq_file_path);
auto result = tmp_recon_file.create(tmp_freq_file_path); // First recon if freq is already in txt
@@ -675,7 +681,7 @@ ReconView::ReconView(NavigationView& nav)
}
}
}
receiver_model.set_target_frequency(frequency_list[current_index].frequency_a); // retune
receiver_model.set_target_frequency(current_entry().frequency_a); // retune
}
if (frequency_list.size() == 0) {
text_cycle.set_text(" ");
@@ -703,25 +709,23 @@ ReconView::ReconView(NavigationView& nav)
} else {
if (field_mode.selected_index_value() != SPEC_MODULATION)
audio::output::stop();
// clear and shrink_to_fit are not enough to really start with a new, clean, empty vector
// swap is the only way to achieve a perfect memory liberation
std::vector<freqman_entry>().swap(frequency_list);
freqman_entry manual_freq_entry;
// Clear doesn't actually free, re-assign so destructor runs on previous instance.
frequency_list = freqman_db{};
current_index = 0;
frequency_list.push_back(std::make_unique<freqman_entry>());
def_step = step_mode.selected_index(); // max range val
manual_freq_entry.type = RANGE;
manual_freq_entry.description =
to_string_short_freq(frequency_range.min).erase(0, 1) + ">" + to_string_short_freq(frequency_range.max).erase(0, 1) + " S:" // current Manual range
+ freqman_entry_get_step_string_short(def_step); // euquiq: lame kludge to reduce spacing in step freq
manual_freq_entry.frequency_a = frequency_range.min; // min range val
manual_freq_entry.frequency_b = frequency_range.max; // max range val
manual_freq_entry.modulation = -1;
manual_freq_entry.bandwidth = -1;
manual_freq_entry.step = def_step;
frequency_list.push_back(manual_freq_entry);
def_step = step_mode.selected_index();
current_entry().type = freqman_type::Range;
current_entry().description =
to_string_short_freq(frequency_range.min).erase(0, 1) + ">" + // euquiq: lame kludge to reduce spacing in step freq
to_string_short_freq(frequency_range.max).erase(0, 1) + " S:" +
freqman_entry_get_step_string_short(def_step);
current_entry().frequency_a = frequency_range.min;
current_entry().frequency_b = frequency_range.max;
current_entry().modulation = freqman_invalid_index;
current_entry().bandwidth = freqman_invalid_index;
current_entry().step = def_step;
big_display.set_style(&Styles::white); // Back to white color
@@ -736,13 +740,12 @@ ReconView::ReconView(NavigationView& nav)
file_name.set("MANUAL RANGE RECON");
desc_cycle.set_style(&Styles::white);
last_entry.modulation = -1;
last_entry.bandwidth = -1;
last_entry.step = -1;
last_entry.modulation = freqman_invalid_index;
last_entry.bandwidth = freqman_invalid_index;
last_entry.step = freqman_invalid_index;
last_index = -1;
current_index = 0;
freq = manual_freq_entry.frequency_a;
freq = current_entry().frequency_a;
handle_retune();
recon_redraw();
recon_resume();
@@ -787,7 +790,7 @@ ReconView::ReconView(NavigationView& nav)
}
};
button_add.on_select = [this](ButtonWithEncoder&) { // frequency_list[current_index]
button_add.on_select = [this](ButtonWithEncoder&) {
if (!scanner_mode) {
recon_save_freq(freq_file_path, current_index, true);
}
@@ -829,7 +832,7 @@ ReconView::ReconView(NavigationView& nav)
open_view->on_changed = [this](std::vector<std::string> result) {
input_file = result[0];
output_file = result[1];
freq_file_path = "/FREQMAN/" + output_file + ".TXT";
freq_file_path = get_freqman_path(output_file).string();
recon_save_config_to_sd();
autosave = persistent_memory::recon_autosave_freqs();
@@ -888,7 +891,7 @@ ReconView::ReconView(NavigationView& nav)
// Loading input and output file from settings
recon_load_config_from_sd();
freq_file_path = "/FREQMAN/" + output_file + ".TXT";
freq_file_path = get_freqman_path(output_file).string();
field_recon_match_mode.set_selected_index(recon_match_mode);
field_squelch.set_value(squelch);
@@ -937,7 +940,11 @@ void ReconView::frequency_file_load(bool stop_all_before) {
desc_cycle.set_style(&Styles::blue);
button_scanner_mode.set_text("RECON");
}
if (!load_freqman_file(file_input, frequency_list, load_freqs, load_ranges, load_hamradios)) {
freqman_load_options options{
.load_freqs = load_freqs,
.load_ranges = load_ranges,
.load_hamradios = load_hamradios};
if (!load_freqman_file(file_input, frequency_list, options)) {
file_name.set_style(&Styles::red);
desc_cycle.set(" NO " + file_input + ".TXT FILE ...");
file_name.set("=> NO DATA");
@@ -954,71 +961,39 @@ void ReconView::frequency_file_load(bool stop_all_before) {
}
}
if (frequency_list[0].step >= 0)
step = freqman_entry_get_step_value(frequency_list[0].step);
else
step = freqman_entry_get_step_value(def_step);
switch (frequency_list[0].type) {
case SINGLE:
freq = frequency_list[0].frequency_a;
break;
case RANGE:
minfreq = frequency_list[0].frequency_a;
maxfreq = frequency_list[0].frequency_b;
if (fwd) {
freq = minfreq;
} else {
freq = maxfreq;
}
if (frequency_list[0].step >= 0)
step = freqman_entry_get_step_value(frequency_list[0].step);
break;
case HAMRADIO:
minfreq = frequency_list[0].frequency_a;
maxfreq = frequency_list[0].frequency_b;
if (fwd) {
freq = minfreq;
} else {
freq = maxfreq;
}
break;
default:
break;
if (frequency_list.empty()) {
text_cycle.set_text(" ");
return; // Can't really do much.
}
reset_indexes();
step = freqman_entry_get_step_value(
is_valid(current_entry().step) ? current_entry().step : def_step);
if (current_entry().type == freqman_type::Single) {
freq = current_entry().frequency_a;
} else if (current_entry().type != freqman_type::Unknown) {
minfreq = current_entry().frequency_a;
maxfreq = current_entry().frequency_b;
freq = fwd ? minfreq : maxfreq;
}
step_mode.set_selected_index(def_step); // Impose the default step into the manual step selector
receiver_model.enable();
receiver_model.set_squelch_level(0);
if (frequency_list.size() != 0) {
switch (frequency_list[current_index].type) {
case RANGE:
description = "R: " + frequency_list[current_index].description;
break;
case HAMRADIO:
description = "H: " + frequency_list[current_index].description;
break;
default:
case SINGLE:
description = "S: " + frequency_list[current_index].description;
break;
text_cycle.set_text(to_string_dec_uint(current_index + 1, 3));
if (update_ranges && !manual_mode) {
button_manual_start.set_text(to_string_short_freq(current_entry().frequency_a));
frequency_range.min = current_entry().frequency_a;
if (current_entry().frequency_b != 0) {
button_manual_end.set_text(to_string_short_freq(current_entry().frequency_b));
frequency_range.max = current_entry().frequency_b;
} else {
button_manual_end.set_text(to_string_short_freq(current_entry().frequency_a));
frequency_range.max = current_entry().frequency_a;
}
text_cycle.set_text(to_string_dec_uint(current_index + 1, 3));
if (update_ranges && !manual_mode) {
button_manual_start.set_text(to_string_short_freq(frequency_list[current_index].frequency_a));
frequency_range.min = frequency_list[current_index].frequency_a;
if (frequency_list[current_index].frequency_b != 0) {
button_manual_end.set_text(to_string_short_freq(frequency_list[current_index].frequency_b));
frequency_range.max = frequency_list[current_index].frequency_b;
} else {
button_manual_end.set_text(to_string_short_freq(frequency_list[current_index].frequency_a));
frequency_range.max = frequency_list[current_index].frequency_a;
}
}
} else {
text_cycle.set_text(" ");
}
desc_cycle.set(description);
update_description();
handle_retune();
}
@@ -1102,6 +1077,10 @@ void ReconView::on_statistics_update(const ChannelStatistics& statistics) {
// contents of a possible recon_start_recording(), but not yet since it's only called once
if (auto_record_locked && !is_recording) {
button_audio_app.set_style(&Styles::red);
if (field_mode.selected_index_value() == SPEC_MODULATION)
button_audio_app.set_text("RAW REC");
else
button_audio_app.set_text("WAV REC");
record_view->start();
button_config.set_style(&Styles::light_grey); // disable config while recording as it's causing an IO error pop up at exit
is_recording = true;
@@ -1144,7 +1123,7 @@ void ReconView::on_statistics_update(const ChannelStatistics& statistics) {
if (recon || stepper != 0 || index_stepper != 0) {
if (index_stepper == 0) {
/* we are doing a range */
if (frequency_list[current_index].type == RANGE) {
if (current_entry().type == freqman_type::Range) {
if ((fwd && stepper == 0) || stepper > 0) {
// forward
freq += step;
@@ -1174,7 +1153,7 @@ void ReconView::on_statistics_update(const ChannelStatistics& statistics) {
}
}
}
} else if (frequency_list[current_index].type == SINGLE) {
} else if (current_entry().type == freqman_type::Single) {
if ((fwd && stepper == 0) || stepper > 0) { // forward
current_index++;
entry_has_changed = true;
@@ -1193,7 +1172,7 @@ void ReconView::on_statistics_update(const ChannelStatistics& statistics) {
current_index = frequency_list.size() - 1;
}
}
} else if (frequency_list[current_index].type == HAMRADIO) {
} else if (current_entry().type == freqman_type::HamRadio) {
if ((fwd && stepper == 0) || stepper > 0) { // forward
if ((minfreq != maxfreq) && freq == minfreq) {
freq = maxfreq;
@@ -1247,22 +1226,22 @@ void ReconView::on_statistics_update(const ChannelStatistics& statistics) {
// reload entry if changed
if (entry_has_changed) {
timer = 0;
switch (frequency_list[current_index].type) {
case SINGLE:
freq = frequency_list[current_index].frequency_a;
switch (current_entry().type) {
case freqman_type::Single:
freq = current_entry().frequency_a;
break;
case RANGE:
minfreq = frequency_list[current_index].frequency_a;
maxfreq = frequency_list[current_index].frequency_b;
case freqman_type::Range:
minfreq = current_entry().frequency_a;
maxfreq = current_entry().frequency_b;
if ((fwd && !stepper && !index_stepper) || stepper > 0 || index_stepper > 0) {
freq = minfreq;
} else if ((!fwd && !stepper && !index_stepper) || stepper < 0 || index_stepper < 0) {
freq = maxfreq;
}
break;
case HAMRADIO:
minfreq = frequency_list[current_index].frequency_a;
maxfreq = frequency_list[current_index].frequency_b;
case freqman_type::HamRadio:
minfreq = current_entry().frequency_a;
maxfreq = current_entry().frequency_b;
if ((fwd && !stepper && !index_stepper) || stepper > 0 || index_stepper > 0) {
freq = minfreq;
} else if ((!fwd && !stepper && !index_stepper) || stepper < 0 || index_stepper < 0) {
@@ -1421,10 +1400,13 @@ size_t ReconView::change_mode(freqman_index_t new_mod) {
break;
}
if (new_mod != SPEC_MODULATION) {
button_audio_app.set_text("AUDIO");
record_view->set_sampling_rate(recording_sampling_rate);
// reset receiver model to fix bug when going from SPEC to audio, the sound is distorded
receiver_model.set_sampling_rate(3072000);
receiver_model.set_baseband_bandwidth(1750000);
} else {
button_audio_app.set_text("RAW");
}
field_mode.set_selected_index(new_mod);
+5
View File
@@ -67,6 +67,7 @@ class ReconView : public View {
void set_loop_config(bool v);
void clear_freqlist_for_ui_action();
void reset_indexes();
void update_description();
void audio_output_start();
bool check_sd_card();
size_t change_mode(freqman_index_t mod_type);
@@ -87,6 +88,10 @@ class ReconView : public View {
// placeholder for possible void recon_start_recording();
void recon_stop_recording();
// Returns true if 'current_index' is in bounds of frequency_list.
bool current_is_valid();
freqman_entry& current_entry();
jammer::jammer_range_t frequency_range{false, 0, MAX_UFREQ}; // perfect for manual recon task too...
int32_t squelch{0};
int32_t db{0};
@@ -21,12 +21,13 @@
* Boston, MA 02110-1301, USA.
*/
#include "ui_recon_settings.hpp"
#include "ui_navigation.hpp"
#include "ui_fileman.hpp"
#include "ui_navigation.hpp"
#include "ui_recon_settings.hpp"
#include "ui_textentry.hpp"
#include "file.hpp"
#include "freqman_db.hpp"
#include "portapack.hpp"
#include "portapack_persistent_memory.hpp"
@@ -55,11 +56,9 @@ ReconSetupViewMain::ReconSetupViewMain(NavigationView& nav, Rect parent_rect, st
button_load_freqs.on_select = [this, &nav](Button&) {
auto open_view = nav.push<FileLoadView>(".TXT");
open_view->push_dir(freqman_dir);
open_view->on_changed = [this, &nav](std::filesystem::path new_file_path) {
std::string dir_filter = "FREQMAN/";
std::string str_file_path = new_file_path.string();
if (str_file_path.find(dir_filter) != string::npos) { // assert file from the FREQMAN folder
// get the filename without txt extension so we can use load_freqman_file fcn
if (new_file_path.native().find(freqman_dir.native()) == 0) {
_input_file = new_file_path.stem().string();
text_input_file.set(_input_file);
} else {
+57 -60
View File
@@ -314,17 +314,13 @@ ScannerView::ScannerView(
// Button to load txt files from the FREQMAN folder
button_load.on_select = [this, &nav](Button&) {
auto open_view = nav.push<FileLoadView>(".TXT");
open_view->on_changed = [this](std::filesystem::path new_file_path) {
std::string dir_filter = "FREQMAN/";
std::string str_file_path = new_file_path.string();
if (str_file_path.find(dir_filter) != string::npos) { // assert file from the FREQMAN folder
open_view->push_dir(freqman_dir);
open_view->on_changed = [this, &nav](std::filesystem::path new_file_path) {
if (new_file_path.native().find(freqman_dir.native()) == 0) {
scan_pause();
// get the filename without txt extension so we can use load_freqman_file fcn
std::string str_file_name = new_file_path.stem().string();
frequency_file_load(str_file_name, true);
frequency_file_load(new_file_path.stem().string(), true);
} else {
nav_.display_modal("LOAD ERROR", "A valid file from\nFREQMAN directory is\nrequired.");
nav.display_modal("LOAD ERROR", "A valid file from\nFREQMAN directory is\nrequired.");
}
};
};
@@ -494,6 +490,7 @@ ScannerView::ScannerView(
bigdisplay_update(BDC_GREY); // Back to grey color
};
// TODO: remove this parsing?
// Button to add current frequency (found during Search) to the Scan Frequency List
button_add.on_select = [this](Button&) {
File scanner_file;
@@ -576,9 +573,9 @@ ScannerView::ScannerView(
void ScannerView::frequency_file_load(std::string file_name, bool stop_all_before) {
bool found_range{false};
bool found_single{false};
freqman_index_t def_mod_index{-1};
freqman_index_t def_bw_index{-1};
freqman_index_t def_step_index{-1};
freqman_index_t def_mod_index{freqman_invalid_index};
freqman_index_t def_bw_index{freqman_invalid_index};
freqman_index_t def_step_index{freqman_invalid_index};
// stop everything running now if required
if (stop_all_before) {
@@ -587,56 +584,56 @@ void ScannerView::frequency_file_load(std::string file_name, bool stop_all_befor
description_list.clear();
}
if (load_freqman_file(file_name, database)) {
loaded_file_name = file_name; // keep loaded filename in memory
for (auto& entry : database) { // READ LINE PER LINE
if (frequency_list.size() < FREQMAN_MAX_PER_FILE) { // We got space!
//
// Get modulation & bw & step from file if specified
// Note these values could be different for each line in the file, but we only look at the first one
//
// Note that freqman requires a very specific string for these parameters,
// so check syntax in frequency file if specified value isn't being loaded
//
if (def_mod_index == -1)
def_mod_index = entry.modulation;
if (load_freqman_file(file_name, database, {})) {
loaded_file_name = file_name;
for (auto& entry_ptr : database) {
if (frequency_list.size() >= FREQMAN_MAX_PER_FILE)
break;
if (def_bw_index == -1)
def_bw_index = entry.bandwidth;
auto& entry = *entry_ptr;
if (def_step_index == -1)
def_step_index = entry.step;
// Get modulation & bw & step from file if specified
// Note these values could be different for each line in the file, but we only look at the first one
//
// Note that freqman requires a very specific string for these parameters,
// so check syntax in frequency file if specified value isn't being loaded
//
if (is_invalid(def_mod_index))
def_mod_index = entry.modulation;
// Get frequency
if (entry.type == RANGE) {
if (!found_range) {
// Set Start & End Search Range instead of populating the small in-memory frequency table
// NOTE: There may be multiple single frequencies in file, but only one search range is supported.
found_range = true;
frequency_range.min = entry.frequency_a;
button_manual_start.set_text(to_string_short_freq(frequency_range.min));
frequency_range.max = entry.frequency_b;
button_manual_end.set_text(to_string_short_freq(frequency_range.max));
}
} else if (entry.type == SINGLE) {
found_single = true;
frequency_list.push_back(entry.frequency_a);
description_list.push_back(entry.description);
} else if (entry.type == HAMRADIO) {
// For HAM repeaters, add both receive & transmit frequencies to scan list and modify description
// (FUTURE fw versions might handle these differently)
found_single = true;
frequency_list.push_back(entry.frequency_a);
description_list.push_back("R:" + entry.description);
if (is_invalid(def_bw_index))
def_bw_index = entry.bandwidth;
if ((entry.frequency_a != entry.frequency_b) &&
(frequency_list.size() < FREQMAN_MAX_PER_FILE)) {
frequency_list.push_back(entry.frequency_b);
description_list.push_back("T:" + entry.description);
}
if (is_invalid(def_step_index))
def_step_index = entry.step;
// Get frequency
if (entry.type == freqman_type::Range) {
if (!found_range) {
// Set Start & End Search Range instead of populating the small in-memory frequency table
// NOTE: There may be multiple single frequencies in file, but only one search range is supported.
found_range = true;
frequency_range.min = entry.frequency_a;
button_manual_start.set_text(to_string_short_freq(frequency_range.min));
frequency_range.max = entry.frequency_b;
button_manual_end.set_text(to_string_short_freq(frequency_range.max));
}
} else if (entry.type == freqman_type::Single) {
found_single = true;
frequency_list.push_back(entry.frequency_a);
description_list.push_back(entry.description);
} else if (entry.type == freqman_type::HamRadio) {
// For HAM repeaters, add both receive & transmit frequencies to scan list and modify description
// (FUTURE fw versions might handle these differently)
found_single = true;
frequency_list.push_back(entry.frequency_a);
description_list.push_back("R:" + entry.description);
if ((entry.frequency_a != entry.frequency_b) &&
(frequency_list.size() < FREQMAN_MAX_PER_FILE)) {
frequency_list.push_back(entry.frequency_b);
description_list.push_back("T:" + entry.description);
}
} else {
break; // No more space: Stop reading the txt file !
}
}
} else {
@@ -650,13 +647,13 @@ void ScannerView::frequency_file_load(std::string file_name, bool stop_all_befor
desc_freq_list_scan = desc_freq_list_scan + "...";
}
if ((def_mod_index != -1) && (def_mod_index != (freqman_index_t)field_mode.selected_index_value()))
if (is_valid(def_mod_index) && def_mod_index != (freqman_index_t)field_mode.selected_index_value())
field_mode.set_by_value(def_mod_index); // Update mode (also triggers a change callback that disables & reenables RF background)
if (def_bw_index != -1) // Update BW if specified in file
if (is_valid(def_bw_index)) // Update BW if specified in file
field_bw.set_selected_index(def_bw_index);
if (def_step_index != -1) // Update step if specified in file
if (is_valid(def_step_index)) // Update step if specified in file
field_step.set_selected_index(def_step_index);
audio::output::stop();
+1 -1
View File
@@ -90,7 +90,7 @@ class ScannerView : public View {
std::string title() const override { return "Scanner"; };
std::vector<rf::Frequency> frequency_list{};
std::vector<string> description_list{};
std::vector<std::string> description_list{};
// void set_parent_rect(const Rect new_parent_rect) override;
+38
View File
@@ -3819,6 +3819,44 @@ static constexpr Bitmap bitmap_icon_looking{
{16, 16},
bitmap_icon_looking_data};
static constexpr uint8_t bitmap_icon_add_data[] = {
0x00,
0x00,
0x00,
0x00,
0x00,
0x00,
0x80,
0x01,
0x80,
0x01,
0x80,
0x01,
0x80,
0x01,
0xF8,
0x1F,
0xF8,
0x1F,
0x80,
0x01,
0x80,
0x01,
0x80,
0x01,
0x80,
0x01,
0x00,
0x00,
0x00,
0x00,
0x00,
0x00,
};
static constexpr Bitmap bitmap_icon_add{
{16, 16},
bitmap_icon_add_data};
static constexpr uint8_t bitmap_icon_delete_data[] = {
0x00,
0x00,
+14 -17
View File
@@ -252,12 +252,9 @@ std::filesystem::path next_filename_matching_pattern(const std::filesystem::path
std::vector<std::filesystem::path> scan_root_files(const std::filesystem::path& directory,
const std::filesystem::path& extension) {
std::vector<std::filesystem::path> file_list{};
for (const auto& entry : std::filesystem::directory_iterator(directory, extension)) {
if (std::filesystem::is_regular_file(entry.status())) {
file_list.push_back(entry.path());
}
}
scan_root_files(directory, extension, [&file_list](const std::filesystem::path& p) {
file_list.push_back(p);
});
return file_list;
}
@@ -287,7 +284,7 @@ std::filesystem::filesystem_error rename_file(
std::filesystem::filesystem_error copy_file(
const std::filesystem::path& file_path,
const std::filesystem::path& dest_path) {
// Decent compromise between memory and speed.
// 512 seems to be the largest block size FatFS likes.
constexpr size_t buffer_size = 512;
uint8_t buffer[buffer_size];
File src;
@@ -324,10 +321,11 @@ FATTimestamp file_created_date(const std::filesystem::path& file_path) {
std::filesystem::filesystem_error make_new_file(
const std::filesystem::path& file_path) {
File f;
auto result = f.create(file_path);
return result.is_valid()
? result.value()
: std::filesystem::filesystem_error{};
auto error = f.create(file_path);
if (error)
return *error;
return {};
}
std::filesystem::filesystem_error make_new_directory(
@@ -507,13 +505,12 @@ bool path_iequal(
}
directory_iterator::directory_iterator(
std::filesystem::path path,
std::filesystem::path wild)
: pattern{wild} {
const std::filesystem::path& path,
const std::filesystem::path& wild)
: path_{path}, wild_{wild} {
impl = std::make_shared<Impl>();
const auto result = f_findfirst(&impl->dir, &impl->filinfo,
reinterpret_cast<const TCHAR*>(path.c_str()),
reinterpret_cast<const TCHAR*>(pattern.c_str()));
auto result = f_findfirst(&impl->dir, &impl->filinfo,
path_.tchar(), wild_.tchar());
if (result != FR_OK || impl->filinfo.fname[0] == (TCHAR)'\0') {
impl.reset();
// TODO: Throw exception if/when I enable exceptions...
+23 -16
View File
@@ -76,36 +76,30 @@ struct path {
: _s{} {
}
path(
const path& p)
path(const path& p)
: _s{p._s} {
}
path(
path&& p)
path(path&& p)
: _s{std::move(p._s)} {
}
template <class Source>
path(
const Source& source)
path(const Source& source)
: path{std::begin(source), std::end(source)} {
}
template <class InputIt>
path(
InputIt first,
InputIt last)
path(InputIt first,
InputIt last)
: _s{first, last} {
}
path(
const char16_t* const s)
path(const char16_t* const s)
: _s{s} {
}
path(
const TCHAR* const s)
path(const TCHAR* const s)
: _s{reinterpret_cast<const std::filesystem::path::value_type*>(s)} {
}
@@ -132,6 +126,10 @@ struct path {
return native().c_str();
}
const TCHAR* tchar() const {
return reinterpret_cast<const TCHAR*>(native().c_str());
}
const string_type& native() const {
return _s;
}
@@ -149,7 +147,7 @@ struct path {
}
path& operator/=(const path& p) {
if (_s.back() != preferred_separator)
if (_s.back() != preferred_separator && p._s.front() != preferred_separator)
_s += preferred_separator;
_s += p._s;
return *this;
@@ -207,7 +205,8 @@ class directory_iterator {
};
std::shared_ptr<Impl> impl{};
const path pattern{};
std::filesystem::path path_{};
std::filesystem::path wild_{};
friend bool operator!=(const directory_iterator& lhs, const directory_iterator& rhs);
@@ -219,7 +218,8 @@ class directory_iterator {
using iterator_category = std::input_iterator_tag;
directory_iterator() noexcept {};
directory_iterator(std::filesystem::path path, std::filesystem::path wild);
directory_iterator(const std::filesystem::path& path,
const std::filesystem::path& wild);
~directory_iterator() {}
@@ -266,6 +266,13 @@ std::filesystem::filesystem_error make_new_file(const std::filesystem::path& fil
std::filesystem::filesystem_error make_new_directory(const std::filesystem::path& dir_path);
std::filesystem::filesystem_error ensure_directory(const std::filesystem::path& dir_path);
template <typename TCallback>
void scan_root_files(const std::filesystem::path& directory, const std::filesystem::path& extension, const TCallback& fn) {
for (const auto& entry : std::filesystem::directory_iterator(directory, extension)) {
if (std::filesystem::is_regular_file(entry.status()))
fn(entry.path());
}
}
std::vector<std::filesystem::path> scan_root_files(const std::filesystem::path& directory, const std::filesystem::path& extension);
std::vector<std::filesystem::path> scan_root_directories(const std::filesystem::path& directory);
+16 -1
View File
@@ -36,7 +36,8 @@
*/
/* Iterates lines in buffer split on '\n'.
* NB: very basic iterator impl, don't try anything fancy with it. */
* NB: very basic iterator impl, don't try anything fancy with it.
* For example, you _must_ deref the iterator after advancing it. */
template <typename BufferType>
class BufferLineReader {
public:
@@ -133,4 +134,18 @@ using FileLineReader = BufferLineReader<File>;
* are used or they will dangle. */
std::vector<std::string_view> split_string(std::string_view str, char c);
/* Returns the number of lines in a file. */
template <typename BufferType>
uint32_t count_lines(BufferLineReader<BufferType>& reader) {
uint32_t count = 0;
auto it = reader.begin();
do {
*it; // Necessary to force the file read.
++count;
} while (++it != reader.end());
return count;
}
#endif
+2 -1
View File
@@ -59,6 +59,7 @@ class BufferWrapper {
using Offset = uint32_t;
using Line = uint32_t;
using Column = uint32_t;
using Size = File::Size;
using Range = struct {
// Offset of the start, inclusive.
Offset start;
@@ -108,7 +109,7 @@ class BufferWrapper {
}
/* Gets the size of the buffer in bytes. */
File::Size size() const { return wrapped_->size(); }
Size size() const { return wrapped_->size(); }
/* Get the count of the lines in the buffer. */
uint32_t line_count() const { return line_count_; }
+51 -423
View File
@@ -2,6 +2,7 @@
* Copyright (C) 2014 Jared Boone, ShareBrained Technology, Inc.
* Copyright (C) 2016 Furrtek
* Copyright (C) 2023 gullradriel, Nilorea Studio Inc.
* Copyright (C) 2023 Kyle Reed
*
* This file is part of PortaPack.
*
@@ -21,467 +22,94 @@
* Boston, MA 02110-1301, USA.
*/
#include "file.hpp"
#include "freqman.hpp"
#include "tone_key.hpp"
#include "ui_widget.hpp"
#include <algorithm>
namespace fs = std::filesystem;
using namespace tonekey;
using namespace ui;
using option_t = std::pair<std::string, int32_t>;
using options_t = std::vector<option_t>;
// TODO: Consolidate with receiver_model.
// These definitions are spread all over and stiched together with indices.
options_t freqman_entry_modulations = {
{"AM", 0},
{"NFM", 1},
{"WFM", 2},
{"SPEC", 3}};
extern options_t freqman_modulations;
extern options_t freqman_bandwidths[4];
extern options_t freqman_steps;
extern options_t freqman_steps_short;
options_t freqman_entry_bandwidths[4] = {
{// AM
{"DSB 9k", 0},
{"DSB 6k", 1},
{"USB+3k", 2},
{"LSB-3k", 3},
{"CW", 4}},
{// NFM
{"8k5", 0},
{"11k", 1},
{"16k", 2}},
{
// WFM
{"40k", 2},
{"180k", 1},
{"200k", 0},
},
{
// SPEC
{"8k5", 8500},
{"11k", 11000},
{"16k", 16000},
{"25k", 25000},
{"50k", 50000},
{"100k", 100000},
{"250k", 250000},
{"500k", 500000}, /* Previous Limit bandwith Option with perfect micro SD write .C16 format operaton.*/
{"600k", 600000}, /* That extended option is still possible to record with FW version Mayhem v1.41 (< 2,5MB/sec) */
{"650k", 650000},
{"750k", 750000}, /* From this BW onwards, the LCD is ok, but the recorded file is decimated, (not real file size) */
{"1100k", 1100000},
{"1750k", 1750000},
{"2000k", 2000000},
{"2500k", 2500000},
{"2750k", 2750000}, // That is our max Capture option, to keep using later / 8 decimation (22Mhz sampling ADC)
}};
extern const option_t* find_by_index(const options_t& options, freqman_index_t index);
options_t freqman_entry_steps = {
{"0.1kHz ", 100},
{"1kHz ", 1000},
{"5kHz (SA AM)", 5000},
{"6.25kHz(NFM)", 6250},
{"8.33kHz(AIR)", 8330},
{"9kHz (EU AM)", 9000},
{"10kHz(US AM)", 10000},
{"12.5kHz(NFM)", 12500},
{"15kHz (HFM)", 15000},
{"25kHz (N1)", 25000},
{"30kHz (OIRT)", 30000},
{"50kHz (FM1)", 50000},
{"100kHz (FM2)", 100000},
{"250kHz (N2)", 250000},
{"500kHz (WFM)", 500000},
{"1MHz ", 1000000}};
// TODO: remove in favor of FreqmanDB
/* Freqman file handling. */
bool load_freqman_file(const std::string& file_stem, freqman_db& db, freqman_load_options options) {
return parse_freqman_file(get_freqman_path(file_stem), db, options);
}
options_t freqman_entry_steps_short = {
{"0.1kHz", 100},
{"1kHz", 1000},
{"5kHz", 5000},
{"6.25kHz", 6250},
{"8.33kHz", 8330},
{"9kHz", 9000},
{"10kHz", 10000},
{"12.5kHz", 12500},
{"15kHz", 15000},
{"25kHz", 25000},
{"30kHz", 30000},
{"50kHz", 50000},
{"100kHz", 100000},
{"250kHz", 250000},
{"500kHz", 500000},
{"1MHz", 1000000}};
bool delete_freqman_file(const std::string& file_stem) {
delete_file(get_freqman_path(file_stem));
return true;
}
bool load_freqman_file(std::string& file_stem, freqman_db& db, bool load_freqs, bool load_ranges, bool load_hamradios, uint8_t max_num_freqs) {
// swap with empty vector to ensure memory is immediately released
std::vector<freqman_entry>().swap(db);
File freqman_file{};
size_t length = 0, n = 0, file_position = 0;
char* pos = NULL;
char* line_start = NULL;
char* line_end = NULL;
std::string description{NULL};
rf::Frequency frequency_a = 0, frequency_b = 0;
char file_data[FREQMAN_READ_BUF_SIZE + 1] = {0};
freqman_entry_type type = NOTYPE;
freqman_index_t modulation = -1;
freqman_index_t bandwidth = -1;
freqman_index_t step = -1;
freqman_index_t tone = -1;
uint32_t tone_freq;
char c;
bool save_freqman_file(const std::string& file_stem, freqman_db& db) {
auto path = get_freqman_path(file_stem);
delete_file(path);
auto result = freqman_file.open("FREQMAN/" + file_stem + ".TXT");
if (result.is_valid())
File freqman_file;
auto error = freqman_file.create(path);
if (error)
return false;
while (1) {
// Read a FREQMAN_READ_BUF_SIZE block from file
freqman_file.seek(file_position);
memset(file_data, 0, FREQMAN_READ_BUF_SIZE + 1);
auto read_size = freqman_file.read(file_data, FREQMAN_READ_BUF_SIZE);
if (read_size.is_error())
return false; // Read error
file_position += FREQMAN_READ_BUF_SIZE;
// Reset line_start to beginning of buffer
line_start = file_data;
// If EOF reached, insert 0x0A after, in case the last line doesn't have a C/R
if (read_size.value() < FREQMAN_READ_BUF_SIZE)
*(line_start + read_size.value()) = 0x0A;
// Look for complete lines in buffer
while ((line_end = strstr(line_start, "\x0A"))) {
*line_end = 0; // Stop strstr() searches below at EOL
modulation = -1;
bandwidth = -1;
step = -1;
tone = -1;
type = NOTYPE;
frequency_a = frequency_b = 0;
// Read frequency
pos = strstr(line_start, "f=");
if (pos) {
pos += 2;
frequency_a = strtoll(pos, nullptr, 10);
type = SINGLE;
} else {
// ...or range
pos = strstr(line_start, "a=");
if (pos) {
pos += 2;
frequency_a = strtoll(pos, nullptr, 10);
type = RANGE;
pos = strstr(line_start, "b=");
if (pos) {
pos += 2;
frequency_b = strtoll(pos, nullptr, 10);
} else
frequency_b = 0;
} else {
// ... or hamradio
pos = strstr(line_start, "r=");
if (pos) {
pos += 2;
frequency_a = strtoll(pos, nullptr, 10);
type = HAMRADIO;
pos = strstr(line_start, "t=");
if (pos) {
pos += 2;
frequency_b = strtoll(pos, nullptr, 10);
} else
frequency_b = frequency_a;
} else
frequency_a = 0;
}
}
// modulation if any
pos = strstr(line_start, "m=");
if (pos) {
pos += 2;
modulation = freqman_entry_get_modulation_from_str(pos);
}
// bandwidth if any
pos = strstr(line_start, "bw=");
if (pos) {
pos += 3;
bandwidth = freqman_entry_get_bandwidth_from_str(modulation, pos);
}
// step if any
pos = strstr(line_start, "s=");
if (pos) {
pos += 2;
step = freqman_entry_get_step_from_str_short(pos);
}
// ctcss tone if any
pos = strstr(line_start, "c=");
if (pos) {
pos += 2;
// find decimal point and replace with 0 if there is one, for strtoll
length = strcspn(pos, ".,\x0A");
if (pos + length <= line_end) {
c = *(pos + length);
*(pos + length) = 0;
// ASCII Hz to integer Hz x 100
tone_freq = strtoll(pos, nullptr, 10) * 100;
// stuff saved character back into string in case it was not a decimal point
*(pos + length) = c;
// now get first digit after decimal point (10ths of Hz)
pos += length + 1;
if (c == '.' && *pos >= '0' && *pos <= '9')
tone_freq += (*pos - '0') * 10;
// convert tone_freq (100x the freq in Hz) to a tone_key index
tone = tone_key_index_by_value(tone_freq);
}
}
// Read description until , or LF
pos = strstr(line_start, "d=");
if (pos) {
pos += 2;
length = std::min(strcspn(pos, ",\x0A"), (size_t)FREQMAN_DESC_MAX_LEN);
description = string(pos, length);
description.shrink_to_fit();
}
if ((type == SINGLE && load_freqs) || (type == RANGE && load_ranges) || (type == HAMRADIO && load_hamradios)) {
freqman_entry entry = {frequency_a, frequency_b, std::move(description), type, modulation, bandwidth, step, tone};
db.emplace_back(entry);
n++;
if (n > max_num_freqs) return true;
}
line_start = line_end + 1;
if (line_start - file_data >= FREQMAN_READ_BUF_SIZE) break;
}
if (read_size.value() != FREQMAN_READ_BUF_SIZE)
break; // End of file
// Restart at beginning of last incomplete line
file_position -= (file_data + FREQMAN_READ_BUF_SIZE - line_start);
}
/* populate implicitly specified modulation / bandwidth */
if (db.size() > 2) {
modulation = db[0].modulation;
bandwidth = db[0].bandwidth;
for (unsigned int it = 1; it < db.size(); it++) {
if (db[it].modulation < 0) {
db[it].modulation = modulation;
} else {
modulation = db[it].modulation;
}
if (db[it].bandwidth < 0) {
db[it].bandwidth = bandwidth;
} else {
modulation = db[it].bandwidth;
}
}
}
db.shrink_to_fit();
return true;
}
bool get_freq_string(freqman_entry& entry, std::string& item_string) {
rf::Frequency frequency_a, frequency_b;
frequency_a = entry.frequency_a;
if (entry.type == SINGLE) {
// Single
item_string = "f=" + to_string_dec_uint(frequency_a / 1000) + to_string_dec_uint(frequency_a % 1000UL, 3, '0');
} else if (entry.type == RANGE) {
// Range
frequency_b = entry.frequency_b;
item_string = "a=" + to_string_dec_uint(frequency_a / 1000) + to_string_dec_uint(frequency_a % 1000UL, 3, '0');
item_string += ",b=" + to_string_dec_uint(frequency_b / 1000) + to_string_dec_uint(frequency_b % 1000UL, 3, '0');
if (entry.step >= 0) {
item_string += ",s=" + freqman_entry_get_step_string_short(entry.step);
}
} else if (entry.type == HAMRADIO) {
frequency_b = entry.frequency_b;
item_string = "r=" + to_string_dec_uint(frequency_a / 1000) + to_string_dec_uint(frequency_a % 1000UL, 3, '0');
item_string += ",t=" + to_string_dec_uint(frequency_b / 1000) + to_string_dec_uint(frequency_b % 1000UL, 3, '0');
if (entry.tone >= 0) {
item_string += ",c=" + tone_key_value_string(entry.tone);
}
}
if (entry.modulation >= 0 && (unsigned)entry.modulation < freqman_entry_modulations.size()) {
item_string += ",m=" + freqman_entry_get_modulation_string(entry.modulation);
if (entry.bandwidth >= 0 && (unsigned)entry.bandwidth < freqman_entry_bandwidths[entry.modulation].size()) {
item_string += ",bw=" + freqman_entry_get_bandwidth_string(entry.modulation, entry.bandwidth);
}
}
if (entry.description.size())
item_string += ",d=" + entry.description;
for (size_t n = 0; n < db.size(); n++)
freqman_file.write_line(to_freqman_string(*db[n]));
return true;
}
bool delete_freqman_file(std::string& file_stem) {
File freqman_file;
std::string freq_file_path = "/FREQMAN/" + file_stem + ".TXT";
delete_file(freq_file_path);
return false;
}
bool save_freqman_file(std::string& file_stem, freqman_db& db) {
File freqman_file;
std::string freq_file_path = "/FREQMAN/" + file_stem + ".TXT";
delete_file(freq_file_path);
auto result = freqman_file.create(freq_file_path);
if (!result.is_valid()) {
for (size_t n = 0; n < db.size(); n++) {
std::string line;
get_freq_string(db[n], line);
freqman_file.write_line(line);
}
return true;
}
return false;
}
bool create_freqman_file(std::string& file_stem, File& freqman_file) {
auto result = freqman_file.create("FREQMAN/" + file_stem + ".TXT");
if (result.is_valid())
return false;
return true;
}
std::string freqman_item_string(freqman_entry& entry, size_t max_length) {
std::string item_string;
switch (entry.type) {
case SINGLE:
item_string = to_string_short_freq(entry.frequency_a) + "M: " + entry.description;
break;
case RANGE:
item_string = "R: " + entry.description;
break;
case HAMRADIO:
item_string = "H: " + entry.description;
break;
default:
item_string = "!UNKNOW TYPE " + entry.description;
break;
}
if (item_string.size() > max_length)
return item_string.substr(0, max_length - 3) + "...";
return item_string;
bool create_freqman_file(const std::string& file_stem) {
auto fs_error = make_new_file(get_freqman_path(file_stem));
return fs_error.ok();
}
/* Set options. */
void freqman_set_modulation_option(OptionsField& option) {
option.set_options(freqman_entry_modulations);
option.set_options(freqman_modulations);
}
void freqman_set_bandwidth_option(freqman_index_t modulation, OptionsField& option) {
option.set_options(freqman_entry_bandwidths[modulation]);
if (is_valid(modulation))
option.set_options(freqman_bandwidths[modulation]);
}
void freqman_set_step_option(OptionsField& option) {
option.set_options(freqman_entry_steps);
option.set_options(freqman_steps);
}
void freqman_set_step_option_short(OptionsField& option) {
option.set_options(freqman_entry_steps_short);
}
std::string freqman_entry_get_modulation_string(freqman_index_t modulation) {
if (modulation < 0 || (unsigned)modulation >= freqman_entry_modulations.size()) {
return std::string(""); // unknown modulation
}
return freqman_entry_modulations[modulation].first;
}
std::string freqman_entry_get_bandwidth_string(freqman_index_t modulation, freqman_index_t bandwidth) {
if (modulation < 0 || (unsigned)modulation >= freqman_entry_modulations.size()) {
return std::string(""); // unknown modulation
}
if (bandwidth < 0 || (unsigned)bandwidth > freqman_entry_bandwidths[modulation].size()) {
return std::string(""); // unknown modulation
}
return freqman_entry_bandwidths[modulation][bandwidth].first;
}
std::string freqman_entry_get_step_string(freqman_index_t step) {
if (step < 0 || (unsigned)step >= freqman_entry_steps.size()) {
return std::string(""); // unknown modulation
}
return freqman_entry_steps[step].first;
}
std::string freqman_entry_get_step_string_short(freqman_index_t step) {
if (step < 0 || (unsigned)step >= freqman_entry_steps_short.size()) {
return std::string(""); // unknown modulation
}
return freqman_entry_steps_short[step].first;
option.set_options(freqman_steps_short);
}
/* Option value lookup. */
// TODO: use Optional instead of magic values.
int32_t freqman_entry_get_modulation_value(freqman_index_t modulation) {
if (modulation < 0 || (unsigned)modulation >= freqman_entry_modulations.size()) {
return -1; // unknown modulation
}
return freqman_entry_modulations[modulation].second;
if (auto opt = find_by_index(freqman_modulations, modulation))
return opt->second;
return -1;
}
int32_t freqman_entry_get_bandwidth_value(freqman_index_t modulation, freqman_index_t bandwidth) {
if (modulation < 0 || (unsigned)modulation >= freqman_entry_modulations.size()) {
return -1; // unknown modulation
if (modulation < freqman_modulations.size()) {
if (auto opt = find_by_index(freqman_bandwidths[modulation], bandwidth))
return opt->second;
}
if (bandwidth < 0 || (unsigned)bandwidth > freqman_entry_bandwidths[modulation].size()) {
return -1; // unknown bandwidth for modulation
}
return freqman_entry_bandwidths[modulation][bandwidth].second;
return -1;
}
int32_t freqman_entry_get_step_value(freqman_index_t step) {
if (step < 0 || (unsigned)step >= freqman_entry_steps.size()) {
return -1; // unknown modulation
}
return freqman_entry_steps[step].second;
}
freqman_index_t freqman_entry_get_modulation_from_str(char* str) {
if (!str)
return -1;
for (freqman_index_t index = 0; (unsigned)index < freqman_entry_modulations.size(); index++) {
if (strncmp(freqman_entry_modulations[index].first.c_str(), str, freqman_entry_modulations[index].first.size()) == 0)
return index;
}
return -1;
}
freqman_index_t freqman_entry_get_bandwidth_from_str(freqman_index_t modulation, char* str) {
if (!str)
return -1;
if (modulation < 0 || (unsigned)modulation >= freqman_entry_modulations.size())
return -1;
for (freqman_index_t index = 0; (unsigned)index < freqman_entry_bandwidths[modulation].size(); index++) {
if (strncmp(freqman_entry_bandwidths[modulation][index].first.c_str(), str, freqman_entry_bandwidths[modulation][index].first.size()) == 0)
return index;
}
return -1;
}
freqman_index_t freqman_entry_get_step_from_str(char* str) {
if (!str)
return -1;
for (freqman_index_t index = 0; (unsigned)index < freqman_entry_steps.size(); index++) {
if (strncmp(freqman_entry_steps[index].first.c_str(), str, freqman_entry_steps[index].first.size()) == 0)
return index;
}
return -1;
}
freqman_index_t freqman_entry_get_step_from_str_short(char* str) {
if (!str)
return -1;
for (freqman_index_t index = 0; (unsigned)index < freqman_entry_steps_short.size(); index++) {
if (strncmp(freqman_entry_steps_short[index].first.c_str(), str, freqman_entry_steps_short[index].first.size()) == 0)
return index;
}
if (auto opt = find_by_index(freqman_steps, step))
return opt->second;
return -1;
}
+17 -70
View File
@@ -2,6 +2,7 @@
* Copyright (C) 2014 Jared Boone, ShareBrained Technology, Inc.
* Copyright (C) 2016 Furrtek
* Copyright (C) 2023 gullradriel, Nilorea Studio Inc.
* Copyright (C) 2023 Kyle Reed
*
* This file is part of PortaPack.
*
@@ -27,23 +28,13 @@
#include <cstring>
#include <string>
#include "file.hpp"
#include "ui_receiver.hpp"
#include "tone_key.hpp"
#include "freqman_db.hpp"
#include "string_format.hpp"
#include "ui_receiver.hpp"
#include "ui_widget.hpp"
#define FREQMAN_DESC_MAX_LEN 24 // This is the number of characters that can be drawn in front of "R: TEXT..." before taking a full screen line
#define FREQMAN_MAX_PER_FILE 90 // Maximum of entries we can read. This is a hardware limit
// It was tested and lowered to leave a bit of space to the caller
#define FREQMAN_READ_BUF_SIZE 96 // max freqman line size including desc is 90, + a bit of space
using namespace ui;
using namespace std;
using namespace tonekey;
// needs to be signed as -1 means not set
typedef int8_t freqman_index_t;
// Defined for back-compat.
#define FREQMAN_MAX_PER_FILE freqman_default_max_entries
enum freqman_error : int8_t {
NO_ERROR = 0,
@@ -52,72 +43,28 @@ enum freqman_error : int8_t {
ERROR_DUPLICATE
};
enum freqman_entry_type : int8_t {
SINGLE = 0, // f=
RANGE, // a=,b=
HAMRADIO, // r=,t=
NOTYPE // undetected
};
enum freqman_entry_modulation : uint8_t {
AM_MODULATION = 0,
NFM_MODULATION,
WFM_MODULATION,
SPEC_MODULATION,
SPEC_MODULATION
};
// Entry step placed for AlainD freqman version (or any other enhanced version)
enum freqman_entry_step : int8_t {
AM_US, // 10 kHz AM/CB
AM_EUR, // 9 kHz LW/MW
NFM_1, // 12,5 kHz (Analogic PMR 446)
NFM_2, // 6,25 kHz (Digital PMR 446)
FM_1, // 100 kHz
FM_2, // 50 kHz
N_1, // 25 kHz
N_2, // 250 kHz
AIRBAND, // AIRBAND 8,33 kHz
};
// TODO: Replace with FreqmanDB.
bool load_freqman_file(const std::string& file_stem, freqman_db& db, freqman_load_options options);
bool delete_freqman_file(const std::string& file_stem);
bool save_freqman_file(const std::string& file_stem, freqman_db& db);
bool create_freqman_file(const std::string& file_stem);
struct freqman_entry {
rf::Frequency frequency_a{0}; // 'f=freq' or 'a=freq_start' or 'r=recv_freq'
rf::Frequency frequency_b{0}; // 'b=freq_end' or 't=tx_freq'
std::string description{NULL}; // 'd=desc'
freqman_entry_type type{SINGLE}; // SINGLE,RANGE,HAMRADIO
freqman_index_t modulation{AM_MODULATION}; // AM,NFM,WFM
freqman_index_t bandwidth{0}; // AM_DSB, ...
freqman_index_t step{0}; // 5khz (SA AM,...
tone_index tone{0}; // 0XZ, 11 1ZB,...
};
using freqman_db = std::vector<freqman_entry>;
bool load_freqman_file(std::string& file_stem, freqman_db& db, bool load_freqs = true, bool load_ranges = true, bool load_hamradios = true, uint8_t max_num_freqs = FREQMAN_MAX_PER_FILE);
bool get_freq_string(freqman_entry& entry, std::string& item_string);
bool delete_freqman_file(std::string& file_stem);
bool save_freqman_file(std::string& file_stem, freqman_db& db);
bool create_freqman_file(std::string& file_stem, File& freqman_file);
std::string freqman_item_string(freqman_entry& item, size_t max_length);
void freqman_set_bandwidth_option(freqman_index_t modulation, OptionsField& option);
void freqman_set_modulation_option(OptionsField& option);
void freqman_set_step_option(OptionsField& option);
void freqman_set_step_option_short(OptionsField& option);
void freqman_set_tone_option(OptionsField& option);
std::string freqman_entry_get_modulation_string(freqman_index_t modulation);
std::string freqman_entry_get_bandwidth_string(freqman_index_t modulation, freqman_index_t bandwidth);
std::string freqman_entry_get_step_string(freqman_index_t step);
std::string freqman_entry_get_step_string_short(freqman_index_t step);
void freqman_set_bandwidth_option(freqman_index_t modulation, ui::OptionsField& option);
void freqman_set_modulation_option(ui::OptionsField& option);
void freqman_set_step_option(ui::OptionsField& option);
void freqman_set_step_option_short(ui::OptionsField& option);
void freqman_set_tone_option(ui::OptionsField& option);
// TODO: Can these be removed after Recon is migrated to FreqmanDB?
int32_t freqman_entry_get_modulation_value(freqman_index_t modulation);
int32_t freqman_entry_get_bandwidth_value(freqman_index_t modulation, freqman_index_t bandwidth);
int32_t freqman_entry_get_step_value(freqman_index_t step);
freqman_index_t freqman_entry_get_modulation_from_str(char* str);
freqman_index_t freqman_entry_get_bandwidth_from_str(freqman_index_t modulation, char* str);
freqman_index_t freqman_entry_get_step_from_str(char* str);
freqman_index_t freqman_entry_get_step_from_str_short(char* str);
#endif /*__FREQMAN_H__*/
+460
View File
@@ -0,0 +1,460 @@
/*
* Copyright (C) 2014 Jared Boone, ShareBrained Technology, Inc.
* Copyright (C) 2016 Furrtek
* Copyright (C) 2023 gullradriel, Nilorea Studio Inc.
* Copyright (C) 2023 Kyle Reed
*
* 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 "convert.hpp"
#include "file.hpp"
#include "file_reader.hpp"
#include "freqman_db.hpp"
#include "string_format.hpp"
#include "tone_key.hpp"
#include "utility.hpp"
#include <array>
#include <cctype>
#include <string_view>
#include <vector>
namespace fs = std::filesystem;
const std::filesystem::path freqman_dir{u"/FREQMAN"};
const std::filesystem::path freqman_extension{u".TXT"};
const std::filesystem::path get_freqman_path(const std::string& stem) {
return freqman_dir / stem + freqman_extension;
}
// NB: Don't include UI headers to keep this code unit testable.
using option_t = std::pair<std::string, int32_t>;
using options_t = std::vector<option_t>;
options_t freqman_modulations = {
{"AM", 0},
{"NFM", 1},
{"WFM", 2},
{"SPEC", 3},
};
options_t freqman_bandwidths[4] = {
{
// AM
{"DSB 9k", 0},
{"DSB 6k", 1},
{"USB+3k", 2},
{"LSB-3k", 3},
{"CW", 4},
},
{
// NFM
{"8k5", 0},
{"11k", 1},
{"16k", 2},
},
{
// WFM
{"40k", 2},
{"180k", 1},
{"200k", 0},
},
{
// SPEC -- TODO: these should be indexes.
{"8k5", 8500},
{"11k", 11000},
{"16k", 16000},
{"25k", 25000},
{"50k", 50000},
{"100k", 100000},
{"250k", 250000},
{"500k", 500000}, /* Previous Limit bandwith Option with perfect micro SD write .C16 format operaton.*/
{"600k", 600000}, /* That extended option is still possible to record with FW version Mayhem v1.41 (< 2,5MB/sec) */
{"650k", 650000},
{"750k", 750000}, /* From this BW onwards, the LCD is ok, but the recorded file is decimated, (not real file size) */
{"1100k", 1100000},
{"1750k", 1750000},
{"2000k", 2000000},
{"2500k", 2500000},
{"2750k", 2750000}, // That is our max Capture option, to keep using later / 8 decimation (22Mhz sampling ADC)
},
};
options_t freqman_steps = {
{"0.1kHz ", 100},
{"1kHz ", 1000},
{"5kHz (SA AM)", 5000},
{"6.25kHz(NFM)", 6250},
{"8.33kHz(AIR)", 8330},
{"9kHz (EU AM)", 9000},
{"10kHz(US AM)", 10000},
{"12.5kHz(NFM)", 12500},
{"15kHz (HFM)", 15000},
{"25kHz (N1)", 25000},
{"30kHz (OIRT)", 30000},
{"50kHz (FM1)", 50000},
{"100kHz (FM2)", 100000},
{"250kHz (N2)", 250000},
{"500kHz (WFM)", 500000},
{"1MHz ", 1000000},
};
options_t freqman_steps_short = {
{"0.1kHz", 100},
{"1kHz", 1000},
{"5kHz", 5000},
{"6.25kHz", 6250},
{"8.33kHz", 8330},
{"9kHz", 9000},
{"10kHz", 10000},
{"12.5kHz", 12500},
{"15kHz", 15000},
{"25kHz", 25000},
{"30kHz", 30000},
{"50kHz", 50000},
{"100kHz", 100000},
{"250kHz", 250000},
{"500kHz", 500000},
{"1MHz", 1000000},
};
uint8_t find_by_name(const options_t& options, std::string_view name) {
for (auto ix = 0u; ix < options.size(); ++ix)
if (options[ix].first == name)
return ix;
return freqman_invalid_index;
}
const option_t* find_by_index(const options_t& options, freqman_index_t index) {
if (index < options.size())
return &options[index];
else
return nullptr;
}
/* Impl for next round of changes.
*template <typename T, size_t N>
*const T* find_by_name(const std::array<T, N>& info, std::string_view name) {
* for (const auto& it : info) {
* if (it.name == name)
* return &it;
* }
*
* return nullptr;
*}
*/
std::string freqman_entry_get_modulation_string(freqman_index_t modulation) {
if (auto opt = find_by_index(freqman_modulations, modulation))
return opt->first;
return {};
}
std::string freqman_entry_get_bandwidth_string(freqman_index_t modulation, freqman_index_t bandwidth) {
if (modulation < freqman_modulations.size()) {
if (auto opt = find_by_index(freqman_bandwidths[modulation], bandwidth))
return opt->first;
}
return {};
}
std::string freqman_entry_get_step_string(freqman_index_t step) {
if (auto opt = find_by_index(freqman_steps, step))
return opt->first;
return {};
}
std::string freqman_entry_get_step_string_short(freqman_index_t step) {
if (auto opt = find_by_index(freqman_steps_short, step))
return opt->first;
return {};
}
std::string pretty_string(const freqman_entry& entry, size_t max_length) {
std::string str;
switch (entry.type) {
case freqman_type::Single:
str = to_string_short_freq(entry.frequency_a) + "M: " + entry.description;
break;
case freqman_type::Range:
str = to_string_dec_uint(entry.frequency_a / 1'000'000) + "M-" +
to_string_dec_uint(entry.frequency_b / 1'000'000) + "M: " + entry.description;
break;
case freqman_type::HamRadio:
str = "" + to_string_dec_uint(entry.frequency_a / 1'000'000) + "M," +
to_string_dec_uint(entry.frequency_b / 1'000'000) + "M: " + entry.description;
break;
default:
str = "UNK:" + entry.description;
break;
}
// Truncate. '+' indicates if string has been truncated.
if (str.size() > max_length)
return str.substr(0, max_length - 1) + "+";
return str;
}
std::string to_freqman_string(const freqman_entry& entry) {
std::string serialized;
serialized.reserve(0x80);
// Append a key=value to the string.
auto append_field = [&serialized](std::string_view name, std::string_view value) {
if (!serialized.empty())
serialized += ",";
serialized += std::string{name} + "=" + std::string{value};
};
switch (entry.type) {
case freqman_type::Single:
append_field("f", to_string_dec_uint64(entry.frequency_a));
break;
case freqman_type::Range:
append_field("a", to_string_dec_uint64(entry.frequency_a));
append_field("b", to_string_dec_uint64(entry.frequency_b));
if (is_valid(entry.step))
append_field("s", freqman_entry_get_step_string_short(entry.step));
break;
case freqman_type::HamRadio:
append_field("r", to_string_dec_uint64(entry.frequency_a));
append_field("t", to_string_dec_uint64(entry.frequency_b));
if (is_valid(entry.tone))
append_field("c", tonekey::tone_key_value_string(entry.tone));
break;
default:
return {}; // TODO: Comment type with description?
};
if (is_valid(entry.modulation) && entry.modulation < freqman_modulations.size()) {
append_field("m", freqman_entry_get_modulation_string(entry.modulation));
if (is_valid(entry.bandwidth) && (unsigned)entry.bandwidth < freqman_bandwidths[entry.modulation].size())
append_field("bw", freqman_entry_get_bandwidth_string(entry.modulation, entry.bandwidth));
}
if (entry.description.size() > 0)
append_field("d", entry.description);
serialized.shrink_to_fit();
return serialized;
}
freqman_index_t parse_tone_key(std::string_view value) {
// Split into whole and fractional parts.
auto parts = split_string(value, '.');
int32_t tone_freq = 0;
int32_t whole_part = 0;
parse_int(parts[0], whole_part);
// Tones are stored as frequency / 100 for some reason.
// E.g. 14572 would be 145.7 (NB: 1s place is dropped).
// TODO: Might be easier to just store the codes?
// Multiply the whole part by 100 to get the tone frequency.
tone_freq = whole_part * 100;
// Add the fractional part, if present.
if (parts.size() > 1) {
auto c = parts[1].front();
auto digit = std::isdigit(c) ? c - '0' : 0;
tone_freq += digit * 10;
}
return static_cast<freqman_index_t>(tonekey::tone_key_index_by_value(tone_freq));
}
bool parse_freqman_entry(std::string_view str, freqman_entry& entry) {
if (str.empty() || str[0] == '#')
return false;
entry = freqman_entry{};
auto cols = split_string(str, ',');
for (auto col : cols) {
if (col.empty())
continue;
auto pair = split_string(col, '=');
if (pair.size() != 2)
continue;
auto key = pair[0];
auto value = pair[1];
if (key == "a") {
entry.type = freqman_type::Range;
parse_int(value, entry.frequency_a);
} else if (key == "b") {
parse_int(value, entry.frequency_b);
} else if (key == "bw") {
// NB: Requires modulation to be set first
if (entry.modulation < std::size(freqman_bandwidths)) {
entry.bandwidth = find_by_name(freqman_bandwidths[entry.modulation], value);
}
} else if (key == "c") {
entry.tone = parse_tone_key(value);
} else if (key == "d") {
entry.description = trim(value);
} else if (key == "f") {
entry.type = freqman_type::Single;
parse_int(value, entry.frequency_a);
} else if (key == "m") {
entry.modulation = find_by_name(freqman_modulations, value);
} else if (key == "r") {
entry.type = freqman_type::HamRadio;
parse_int(value, entry.frequency_a);
} else if (key == "s") {
entry.step = find_by_name(freqman_steps_short, value);
} else if (key == "t") {
parse_int(value, entry.frequency_b);
}
}
// No valid frequency combination was set.
if (entry.type == freqman_type::Unknown)
return false;
// Ranges should have both frequencies set and A <= B.
if (entry.type == freqman_type::Range || entry.type == freqman_type::HamRadio) {
if (entry.frequency_a == 0 || entry.frequency_b == 0)
return false;
if (entry.frequency_a > entry.frequency_b)
return false;
}
// TODO: Consider additional validation:
// - Tone only on HamRadio.
// - Fail on failed parse_int.
// - Fail if bandwidth set before modulation.
return true;
}
// TODO: Use FreqmanDB iterator.
bool parse_freqman_file(const fs::path& path, freqman_db& db, freqman_load_options options) {
File f;
auto error = f.open(path);
if (error)
return false;
auto reader = FileLineReader(f);
auto line_count = count_lines(reader);
// Attempt to avoid a re-alloc if possible.
db.clear();
db.reserve(line_count);
for (const auto& line : reader) {
freqman_entry entry{};
if (!parse_freqman_entry(line, entry))
continue;
// Filter by entry type.
if ((entry.type == freqman_type::Single && !options.load_freqs) ||
(entry.type == freqman_type::Range && !options.load_ranges) ||
(entry.type == freqman_type::HamRadio && !options.load_hamradios)) {
continue;
}
// Use previous entry's mod/band if current's aren't set.
if (!db.empty()) {
if (is_invalid(entry.modulation))
entry.modulation = db.back()->modulation;
if (is_invalid(entry.bandwidth))
entry.bandwidth = db.back()->bandwidth;
}
// Move the entry onto the heap and push.
db.push_back(std::make_unique<freqman_entry>(std::move(entry)));
// Limit to max_entries when specified.
if (options.max_entries > 0 && db.size() >= options.max_entries)
break;
}
db.shrink_to_fit();
return true;
}
/* FreqmanDB ***********************************/
bool FreqmanDB::open(const std::filesystem::path& path) {
auto result = FileWrapper::open(path);
if (!result)
return false;
wrapper_ = *std::move(result);
return true;
}
void FreqmanDB::close() {
wrapper_.reset();
}
freqman_entry FreqmanDB::operator[](FileWrapper::Line line) const {
auto length = wrapper_->line_length(line);
auto line_text = wrapper_->get_text(line, 0, length);
if (line_text) {
freqman_entry entry;
if (parse_freqman_entry(*line_text, entry))
return entry;
}
return {};
}
void FreqmanDB::insert_entry(const freqman_entry& entry, FileWrapper::Line line) {
// TODO: Can be more efficient.
line = clip<uint32_t>(line, 0u, entry_count());
wrapper_->insert_line(line);
replace_entry(line, entry);
}
void FreqmanDB::replace_entry(FileWrapper::Line line, const freqman_entry& entry) {
auto range = wrapper_->line_range(line);
if (!range)
return; // TODO: Message?
// Don't overwrite the '\n'.
range->end--;
wrapper_->replace_range(*range, to_freqman_string(entry));
}
void FreqmanDB::delete_entry(FileWrapper::Line line) {
wrapper_->delete_line(line);
}
uint32_t FreqmanDB::entry_count() const {
// FileWrapper always presents a single line even for empty files.
return empty() ? 0u : wrapper_->line_count();
}
bool FreqmanDB::empty() const {
// FileWrapper always presents a single line even for empty files.
// A DB is only really empty if the file size is 0.
return !wrapper_ || wrapper_->size() == 0;
}
+234
View File
@@ -0,0 +1,234 @@
/*
* Copyright (C) 2014 Jared Boone, ShareBrained Technology, Inc.
* Copyright (C) 2016 Furrtek
* Copyright (C) 2023 gullradriel, Nilorea Studio Inc.
* Copyright (C) 2023 Kyle Reed
*
* 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 __FREQMAN_DB_H__
#define __FREQMAN_DB_H__
#include "file.hpp"
#include "file_wrapper.hpp"
#include "utility.hpp"
#include <array>
#include <memory>
#include <string>
#include <string_view>
#include <vector>
/* Defined in freqman_db.cpp */
extern const std::filesystem::path freqman_dir;
extern const std::filesystem::path freqman_extension;
using freqman_index_t = uint8_t;
constexpr freqman_index_t freqman_invalid_index = static_cast<freqman_index_t>(-1);
/* Returns true if the value is not invalid_index. */
constexpr bool is_valid(freqman_index_t index) {
return index != freqman_invalid_index;
}
/* Returns true if the value is invalid_index. */
constexpr bool is_invalid(freqman_index_t index) {
return index == freqman_invalid_index;
}
/* Gets the full path for a given file stem (no extension). */
const std::filesystem::path get_freqman_path(const std::string& stem);
enum class freqman_type : uint8_t {
Single, // f=
Range, // a=,b=
HamRadio, // r=,t=
Unknown,
};
/* Tables for next round of changes.
* // TODO: attempt to consolidate all of these values
* // and settings into a single location.
* // Should tone_keys get consolidated here too?
* enum class freqman_modulation : uint8_t {
AM,
NFM,
WFM,
SPEC,
Unknown,
* };
*
* struct freqman_modulation_info {
* freqman_modulation modulation;
* std::string_view name;
* };
*
* constexpr std::array<freqman_modulation_info, 5> freqman_modulations = {
freqman_modulation_info{ freqman_modulation::AM, "AM" },
freqman_modulation_info{ freqman_modulation::NFM, "NFM" },
freqman_modulation_info{ freqman_modulation::WFM, "WFM" },
freqman_modulation_info{ freqman_modulation::SPEC, "SPEC" },
freqman_modulation_info{ freqman_modulation::Unknown, "Unknown" }
* };
* static_assert(std::size(freqman_modulations) == (size_t)freqman_modulation::Unknown + 1);
*
* enum class freqman_step : uint8_t {
_100Hz,
_1kHz,
_5kHz,
_6_25kHz,
_8_33kHz,
_9kHz,
_10kHz,
_12_5kHz,
_15kHz,
_25kHz,
_30kHz,
_50kHz,
_100kHz,
_250kHz,
_500kHz,
_1MHz,
Unknown,
* };
*
* struct freqman_step_info {
* freqman_step step;
* std::string_view name;
* std::string_view display_name;
* uint32_t value;
* };
*
* // TODO: FrequencyStepView should use this list.
* constexpr std::array<freqman_step_info, 18> freqman_steps = {
freqman_step_info{ freqman_step::_100Hz, "0.1kHz", "0.1kHz ", 100 },
freqman_step_info{ freqman_step::_1kHz, "1kHz", "1kHz ", 1'000 },
freqman_step_info{ freqman_step::_5kHz, "5kHz", "5kHz (SA AM)", 5'000 },
freqman_step_info{ freqman_step::_6_25kHz, "6.25kHz", "6.25kHz(NFM)", 6'250 },
freqman_step_info{ freqman_step::_8_33kHz, "8.33kHz", "8.33kHz(AIR)", 8'330 },
freqman_step_info{ freqman_step::_9kHz, "9kHz", "9kHz (EU AM)", 9'000 },
freqman_step_info{ freqman_step::_10kHz, "10kHz", "10kHz(US AM)", 10'000 },
freqman_step_info{ freqman_step::_12_5kHz, "12.5kHz", "12.5kHz(NFM)", 12'500 },
freqman_step_info{ freqman_step::_15kHz, "15kHz", "15kHz (HFM)", 15'000 },
freqman_step_info{ freqman_step::_25kHz, "25kHz", "25kHz (N1)", 25'000 },
freqman_step_info{ freqman_step::_30kHz, "30kHz", "30kHz (OIRT)", 30'000 },
freqman_step_info{ freqman_step::_50kHz, "50kHz", "50kHz (FM1)", 50'000 },
freqman_step_info{ freqman_step::_100kHz, "100kHz", "100kHz (FM2)", 100'000 },
freqman_step_info{ freqman_step::_250kHz, "250kHz", "250kHz (N2)", 250'000 },
freqman_step_info{ freqman_step::_500kHz, "500kHz", "500kHz (WFM)", 500'000 },
freqman_step_info{ freqman_step::_1MHz, "1MHz", "1MHz ", 1'000'000 },
freqman_step_info{ freqman_step::Unknown, "Unknown", "Unknown ", 0 },
* };
* static_assert(std::size(freqman_steps) == (size_t)freqman_step::Unknown + 1);
*/
/* Freqman Entry *******************************/
struct freqman_entry {
int64_t frequency_a{0}; // 'f=freq' or 'a=freq_start' or 'r=recv_freq'
int64_t frequency_b{0}; // 'b=freq_end' or 't=tx_freq'
std::string description{0}; // 'd=desc'
freqman_type type{freqman_type::Unknown};
freqman_index_t modulation{freqman_invalid_index};
freqman_index_t bandwidth{freqman_invalid_index};
freqman_index_t step{freqman_invalid_index};
freqman_index_t tone{freqman_invalid_index};
};
// TODO: These shouldn't be exported.
std::string freqman_entry_get_modulation_string(freqman_index_t modulation);
std::string freqman_entry_get_bandwidth_string(freqman_index_t modulation, freqman_index_t bandwidth);
std::string freqman_entry_get_step_string(freqman_index_t step);
std::string freqman_entry_get_step_string_short(freqman_index_t step);
/* A reasonable maximum number of items to load from a freqman file.
* Apps using freqman_db should be tested and this value tuned to
* ensure app memory stability. */
constexpr size_t freqman_default_max_entries = 150;
struct freqman_load_options {
/* Loads all entries when set to 0. */
size_t max_entries{freqman_default_max_entries};
bool load_freqs{true};
bool load_ranges{true};
bool load_hamradios{true};
};
using freqman_entry_ptr = std::unique_ptr<freqman_entry>;
using freqman_db = std::vector<freqman_entry_ptr>;
/* Gets a pretty string representation for an entry. */
std::string pretty_string(const freqman_entry& item, size_t max_length = 30);
/* Gets the freqman file representation for an entry. */
std::string to_freqman_string(const freqman_entry& entry);
bool parse_freqman_entry(std::string_view str, freqman_entry& entry);
bool parse_freqman_file(const std::filesystem::path& path, freqman_db& db, freqman_load_options options);
/* The tricky part of using the file directly is that there can be comments
* and empty lines in the file. This messes up the 'count' calculation.
* Either have to live with 'count' being an upper bound have the callers
* know to expect that entries may be empty. */
// NB: This won't apply implicit mod/bandwidth.
// TODO: Reuse for parse_freqman_file
class FreqmanDB {
public:
class iterator {
public:
iterator(FreqmanDB& db, FileWrapper::Offset line)
: db_{db}, line_{line} {}
iterator& operator++() {
line_++;
return *this;
}
freqman_entry operator*() const {
return db_[line_];
}
bool operator!=(const iterator& other) {
return &db_ != &other.db_ || line_ != other.line_;
}
private:
FreqmanDB& db_;
FileWrapper::Line line_;
};
bool open(const std::filesystem::path& path);
void close();
freqman_entry operator[](FileWrapper::Line line) const;
void insert_entry(const freqman_entry& entry, FileWrapper::Line line);
void replace_entry(FileWrapper::Line line, const freqman_entry& entry);
void delete_entry(FileWrapper::Line line);
uint32_t entry_count() const;
bool empty() const;
iterator begin() {
return {*this, 0};
}
iterator end() {
return {*this, entry_count()};
}
private:
std::unique_ptr<FileWrapper> wrapper_{};
};
#endif /* __FREQMAN_DB_H__ */
+31 -6
View File
@@ -25,16 +25,15 @@
* and fills it backwards towards the front.
* The return value 'q' is a pointer to the start.
* TODO: use std::array for all this. */
template <typename Int>
static char* to_string_dec_uint_internal(
char* p,
uint32_t n) {
Int n) {
*p = 0;
auto q = p;
do {
const uint32_t d = n % 10;
const char c = d + 48;
*(--q) = c;
*(--q) = n % 10 + '0';
n /= 10;
} while (n != 0);
@@ -60,13 +59,36 @@ static char* to_string_dec_uint_pad_internal(
return q;
}
char* to_string_dec_uint(const uint32_t n, StringFormatBuffer& buffer, size_t& length) {
template <typename Int>
char* to_string_dec_uint_internal(Int n, StringFormatBuffer& buffer, size_t& length) {
auto end = &buffer.back();
auto start = to_string_dec_uint_internal(end, n);
length = end - start;
return start;
}
char* to_string_dec_uint(uint32_t n, StringFormatBuffer& buffer, size_t& length) {
return to_string_dec_uint_internal(n, buffer, length);
}
char* to_string_dec_uint64(uint64_t n, StringFormatBuffer& buffer, size_t& length) {
return to_string_dec_uint_internal(n, buffer, length);
}
std::string to_string_dec_uint(uint32_t n) {
StringFormatBuffer b{};
size_t len{};
char* str = to_string_dec_uint(n, b, len);
return std::string(str, len);
}
std::string to_string_dec_uint64(uint64_t n) {
StringFormatBuffer b{};
size_t len{};
char* str = to_string_dec_uint(n, b, len);
return std::string(str, len);
}
std::string to_string_bin(
const uint32_t n,
const uint8_t l) {
@@ -177,7 +199,7 @@ static void to_string_hex_internal(char* p, const uint64_t n, const int32_t l) {
std::string to_string_hex(const uint64_t n, int32_t l) {
char p[32];
l = std::min(l, 31L);
l = std::min<int32_t>(l, 31);
to_string_hex_internal(p, n, l - 1);
p[l] = 0;
return p;
@@ -285,6 +307,9 @@ static const char* whitespace_str = " \t\r\n";
std::string trim(std::string_view str) {
auto first = str.find_first_not_of(whitespace_str);
if (first == std::string::npos)
return {};
auto last = str.find_last_not_of(whitespace_str);
return std::string{str.substr(first, last - first + 1)};
}
+9 -4
View File
@@ -41,20 +41,25 @@ enum TimeFormat {
const char unit_prefix[7]{'n', 'u', 'm', 0, 'k', 'M', 'G'};
using StringFormatBuffer = std::array<char, 16>;
using StringFormatBuffer = std::array<char, 24>;
/* uint32_t conversion without memory allocations. */
char* to_string_dec_uint(const uint32_t n, StringFormatBuffer& buffer, size_t& length);
/* uint conversion without memory allocations. */
char* to_string_dec_uint(uint32_t n, StringFormatBuffer& buffer, size_t& length);
char* to_string_dec_uint64(uint64_t n, StringFormatBuffer& buffer, size_t& length);
std::string to_string_dec_uint(uint32_t n);
std::string to_string_dec_uint64(uint64_t n);
// TODO: Allow l=0 to not fill/justify? Already using this way in ui_spectrum.hpp...
std::string to_string_bin(const uint32_t n, const uint8_t l = 0);
std::string to_string_dec_uint(const uint32_t n, const int32_t l = 0, const char fill = ' ');
std::string to_string_dec_uint(const uint32_t n, const int32_t l, const char fill = ' ');
std::string to_string_dec_int(const int32_t n, const int32_t l = 0, const char fill = 0);
std::string to_string_decimal(float decimal, int8_t precision);
std::string to_string_hex(const uint64_t n, const int32_t l = 0);
std::string to_string_hex_array(uint8_t* const array, const int32_t l = 0);
// NB: These pad-left and don't work correctly for values less than 1M.
std::string to_string_freq(const uint64_t f);
std::string to_string_short_freq(const uint64_t f);
std::string to_string_time_ms(const uint32_t ms);
+2 -21
View File
@@ -88,25 +88,6 @@ std::string fx100_string(uint32_t f) {
return to_string_dec_uint(f / 100) + "." + to_string_dec_uint((f / 10) % 10);
}
void tone_keys_populate(OptionsField& field) {
using option_t = std::pair<std::string, int32_t>;
using options_t = std::vector<option_t>;
options_t tone_key_options;
std::string tone_name;
for (size_t c = 0; c < tone_keys.size(); c++) {
auto f = tone_keys[c].second;
if ((c != 0) && (f < 1000 * 100))
tone_name = "CTCSS " + fx100_string(f) + " #" + tone_keys[c].first;
else
tone_name = tone_keys[c].first;
tone_key_options.emplace_back(tone_name, c);
}
field.set_options(tone_key_options);
}
float tone_key_frequency(tone_index index) {
return float(tone_keys[index].second) / 100.0;
}
@@ -169,7 +150,7 @@ std::string tone_key_string_by_value(uint32_t value, size_t max_length) {
// Value is in 0.01 Hz units
tone_index tone_key_index_by_value(uint32_t value) {
uint32_t diff;
uint32_t min_diff{value * 2};
uint32_t min_diff{UINT32_MAX};
tone_index min_idx{-1};
tone_index idx;
@@ -196,7 +177,7 @@ tone_index tone_key_index_by_string(char* str) {
if (!str)
return -1;
for (tone_index index = 0; (unsigned)index < tone_keys.size(); index++) {
if (tone_keys[index].first.compare(str) >= 0)
if (tone_keys[index].first.compare(str) >= 0) // TODO: why >=?
return index;
}
return -1;
+5 -7
View File
@@ -23,10 +23,9 @@
#ifndef __TONE_KEY_H_
#define __TONE_KEY_H_
#include "ui.hpp"
#include "ui_widget.hpp"
using namespace ui;
#include <cstdint>
#include <string>
#include <vector>
namespace tonekey {
@@ -34,15 +33,14 @@ namespace tonekey {
#define TONE_DISPLAY_TOGGLE_COUNTER 3
#define F2Ix100(x) (int32_t)(x * 100.0)
typedef int32_t tone_index;
using tone_index = int32_t;
using tone_key_t = std::vector<std::pair<std::string, uint32_t>>;
extern const tone_key_t tone_keys;
void tone_keys_populate(OptionsField& field);
float tone_key_frequency(tone_index index);
std::string fx100_string(uint32_t f);
std::string tone_key_string(tone_index index);
std::string tone_key_value_string(tone_index index);
std::string tone_key_string_by_value(uint32_t value, size_t max_length);
+118 -127
View File
@@ -1,5 +1,7 @@
/*
* Copyright (C) 2015 Jared Boone, ShareBrained Technology, Inc.
* Copyright (C) 2023 gullradriel, Nilorea Studio Inc.
* Copyright (C) 2023 Kyle Reed
*
* This file is part of PortaPack.
*
@@ -24,154 +26,143 @@
#include "baseband_api.hpp"
#include "utility.hpp"
#include <algorithm>
namespace ui {
FreqManUIList::FreqManUIList(
Rect parent_rect,
bool instant_exec)
FreqManUIList::FreqManUIList(Rect parent_rect)
: Widget{parent_rect},
instant_exec_{instant_exec} {
visible_lines_{(unsigned)parent_rect.height() / char_height} {
this->set_focusable(true);
}
void FreqManUIList::set_highlighted_index(int index) {
if (freqlist_db == nullptr || (unsigned)(current_index + index) >= freqlist_db->size())
return;
if (index < 0) {
index = 0;
if (current_index > 0)
current_index--;
}
if (index >= freqlist_nb_lines) {
index = freqlist_nb_lines - 1;
if ((unsigned)(current_index + index) < freqlist_db->size())
current_index++;
else
current_index = freqlist_db->size() - freqlist_nb_lines - 1;
}
highlighted_index = index;
}
uint8_t FreqManUIList::get_index() {
return current_index + highlighted_index;
}
void FreqManUIList::paint(Painter& painter) {
freqlist_nb_lines = 0;
const auto r = screen_rect();
uint8_t focused = has_focus();
const Rect r_widget_screen{r.left() + focused, r.top() + focused, r.width() - 2 * focused, r.height() - 2 * +focused};
painter.fill_rectangle(
r_widget_screen,
Color::black());
// only return after clearing the screen so previous entries are not shown anymore
if (freqlist_db == nullptr || freqlist_db->size() == 0)
auto rect = screen_rect();
if (!db_ || db_->empty()) {
auto line_position = rect.location() + Point{7 * 8, 6 * 16};
painter.fill_rectangle(rect, Color::black());
painter.draw_string(line_position, Styles::white, "Empty Category");
return;
// coloration if file is too big
auto text_color = &Styles::white;
if (freqlist_db->size() > FREQMAN_MAX_PER_FILE)
text_color = &Styles::yellow;
uint8_t nb_lines = 0;
for (uint8_t it = current_index; it < freqlist_db->size(); it++) {
uint8_t line_height = (int)nb_lines * char_height;
if (line_height < (r.height() - char_height)) { // line is within the widget
std::string description = freqman_item_string(freqlist_db->at(it), 30);
if (nb_lines == highlighted_index) {
const Rect r_highlighted_freq{0, r.location().y() + (int)nb_lines * char_height, 240, char_height};
painter.fill_rectangle(
r_highlighted_freq,
Color::white());
painter.draw_string(
{0, r.location().y() + (int)nb_lines * char_height},
Styles::bg_white, description);
} else {
painter.draw_string(
{0, r.location().y() + (int)nb_lines * char_height},
*text_color, description);
}
}
nb_lines++;
} else {
// rect is filled, we can break
break;
}
}
freqlist_nb_lines = nb_lines;
if (has_focus() || highlighted()) {
const Rect r_focus{r.left(), r.top(), r.width(), r.height()};
painter.draw_rectangle(
r_focus,
Color::white());
}
}
// Indicate when a file is too large by drawing in yellow.
auto over_max = db_->entry_count() > freqman_default_max_entries;
auto base_style = over_max ? &Styles::yellow : &Styles::white;
void FreqManUIList::set_db(freqman_db& db) {
freqlist_db = &db;
if (db.size() == 0) {
current_index = 0;
highlighted_index = 0;
} else {
if ((unsigned)(current_index + highlighted_index) >= db.size()) {
current_index = db.size() - 1 - highlighted_index;
}
if (current_index < 0) {
current_index = 0;
if (highlighted_index > 0)
highlighted_index--;
// TODO: could minimize redraw/re-read if necessary
// with better change tracking.
for (auto offset = 0u; offset < visible_lines_; ++offset) {
// The whole frame needs to be cleared so every line 'slot'
// is redrawn even when `text` just left empty.
auto text = std::string{};
auto index = start_index_ + offset;
auto line_position = rect.location() + Point{4, 1 + (int)offset * char_height};
// Highlight the selected item.
auto style = (offset == selected_index_) ? &Styles::bg_white : base_style;
if (index < db_->entry_count()) {
auto entry = (*db_)[index];
// db_ is directly backed by a file, so invalid lines cannot be
// pre-filtered. Just show an empty 'slot' in this case.
if (entry.type != freqman_type::Unknown)
text = pretty_string(entry, line_max_length);
}
// Pad right with ' ' so trailing chars are cleaned up.
// draw_glyph has less flicker than fill_rect when drawing.
if (text.length() < line_max_length)
text.resize(line_max_length, ' ');
painter.draw_string(line_position, *style, text);
}
// Draw a bounding rectangle when focused.
painter.draw_rectangle(rect, (has_focus() ? Color::white() : Color::black()));
}
void FreqManUIList::on_focus() {
if (on_highlight)
on_highlight(*this);
set_dirty();
}
void FreqManUIList::on_blur() {
set_dirty();
}
bool FreqManUIList::on_key(const KeyEvent key) {
if (key == KeyEvent::Select) {
if (on_select) {
on_select(*this);
return true;
}
} else {
if (on_dir) {
return on_dir(*this, key);
}
}
return false;
}
if (!db_ || db_->empty())
return false;
bool FreqManUIList::on_touch(const TouchEvent event) {
switch (event.type) {
case TouchEvent::Type::Start:
set_highlighted(true);
set_dirty();
if (on_touch_press) {
on_touch_press(*this);
}
if (on_select && instant_exec_) {
on_select(*this);
}
return true;
case TouchEvent::Type::End:
set_highlighted(false);
set_dirty();
if (on_touch_release) {
on_touch_release(*this);
}
if (on_select && !instant_exec_) {
on_select(*this);
}
return true;
default:
return false;
if (key == KeyEvent::Select && on_select) {
on_select(get_index());
return true;
} else if (key == KeyEvent::Right && on_leave) {
on_leave();
return true;
}
}
bool FreqManUIList::on_encoder(EncoderEvent delta) {
set_highlighted_index((int)highlighted_index + delta);
auto delta = 0;
if (key == KeyEvent::Up && get_index() > 0)
delta = -1;
else if (key == KeyEvent::Down && get_index() < db_->entry_count() - 1)
delta = 1;
else
return false;
adjust_selected_index(delta);
set_dirty();
return true;
}
bool FreqManUIList::on_encoder(EncoderEvent delta) {
if (!db_ || db_->empty())
return false;
adjust_selected_index(delta);
set_dirty();
return true;
}
void FreqManUIList::set_parent_rect(Rect new_parent_rect) {
visible_lines_ = new_parent_rect.height() / char_height;
Widget::set_parent_rect(new_parent_rect);
}
void FreqManUIList::set_index(size_t index) {
start_index_ = 0;
selected_index_ = 0;
adjust_selected_index(index);
}
size_t FreqManUIList::get_index() const {
return start_index_ + selected_index_;
}
void FreqManUIList::set_db(FreqmanDB& db) {
db_ = &db;
start_index_ = 0;
selected_index_ = 0;
set_dirty();
}
void FreqManUIList::adjust_selected_index(int delta) {
int32_t new_index = selected_index_ + delta;
// The selection went off the top of the screen, move up.
if (new_index < 0) {
start_index_ = std::max<int32_t>(start_index_ + new_index, 0);
selected_index_ = 0;
}
// Selection is off the bottom of the screen, move down.
else if (new_index >= (int32_t)visible_lines_) {
start_index_ = std::min<int32_t>(start_index_ + delta, db_->entry_count() - visible_lines_);
selected_index_ = visible_lines_ - 1;
}
// Otherwise, scroll within the screen, but not past the end.
else {
selected_index_ = std::min<int32_t>(new_index, db_->entry_count() - 1);
}
}
} /* namespace ui */
+22 -25
View File
@@ -1,5 +1,7 @@
/*
* Copyright (C) 2015 Jared Boone, ShareBrained Technology, Inc.
* Copyright (C) 2023 gullradriel, Nilorea Studio Inc.
* Copyright (C) 2023 Kyle Reed
*
* This file is part of PortaPack.
*
@@ -23,53 +25,48 @@
#define __UI_FREQLIST_H__
#include "ui.hpp"
#include "ui_widget.hpp"
#include "ui_painter.hpp"
#include "ui_styles.hpp"
#include "ui_widget.hpp"
#include "event_m0.hpp"
#include "message.hpp"
#include "freqman.hpp"
#include "freqman_db.hpp"
#include "message.hpp"
#include <cstdint>
namespace ui {
class FreqManUIList : public Widget {
public:
std::function<void(FreqManUIList&)> on_select{};
std::function<void(FreqManUIList&)> on_touch_release{}; // Executed when releasing touch, after on_select.
std::function<void(FreqManUIList&)> on_touch_press{}; // Executed when touching, before on_select.
std::function<bool(FreqManUIList&, KeyEvent)> on_dir{};
std::function<void(FreqManUIList&)> on_highlight{};
std::function<void(size_t)> on_select{};
std::function<void()> on_leave{}; // Called when Right is pressed.
FreqManUIList(Rect parent_rect, bool instant_exec); // instant_exec: Execute on_select when you touching instead of releasing
FreqManUIList(
Rect parent_rect)
: FreqManUIList{parent_rect, false} {
}
FreqManUIList()
: FreqManUIList{{}, {}} {
}
FreqManUIList(Rect parent_rect);
FreqManUIList(const FreqManUIList& other) = delete;
FreqManUIList& operator=(const FreqManUIList& other) = delete;
void paint(Painter& painter) override;
void on_focus() override;
void on_blur() override;
bool on_key(const KeyEvent key) override;
bool on_touch(const TouchEvent event) override;
bool on_encoder(EncoderEvent delta) override;
void set_parent_rect(Rect new_parent_rect) override;
void set_highlighted_index(int index); // internal set highlighted_index in list handler
uint8_t get_index(); // return highlighed + index
uint8_t set_index(uint8_t index); // try to set current_index + highlighed from index, return capped index
void set_db(freqman_db& db);
void set_index(size_t index);
size_t get_index() const;
void set_db(FreqmanDB& db);
private:
void adjust_selected_index(int index);
static constexpr int8_t char_height = 16;
bool instant_exec_{false};
freqman_db* freqlist_db{nullptr};
int current_index{0};
int highlighted_index{0};
int freqlist_nb_lines{0};
static constexpr int8_t char_width = 8;
static constexpr int8_t line_max_length = 29;
size_t visible_lines_{0};
FreqmanDB* db_{nullptr};
size_t start_index_{0};
size_t selected_index_{0};
};
} // namespace ui
+2 -2
View File
@@ -84,8 +84,8 @@ class GeoPos : public View {
Labels labels_position{
{{1 * 8, 0 * 16}, "Alt:", Color::light_grey()},
{{1 * 8, 1 * 16}, "Lat: * ' \"", Color::light_grey()}, // No ° symbol in 8x16 font
{{1 * 8, 2 * 16}, "Lon: * ' \"", Color::light_grey()},
{{1 * 8, 1 * 16}, "Lat: \x90 ' \"", Color::light_grey()}, // 0x90 is degree ° symbol in our 8x16 font
{{1 * 8, 2 * 16}, "Lon: \x90 ' \"", Color::light_grey()},
};
NumberField field_altitude{
+1 -1
View File
@@ -96,7 +96,7 @@ bool FrequencyField::on_encoder(const EncoderEvent delta) {
// To get these magic numbers, I graphed the function until the
// curve shape seemed about right then tested on device.
delta_ms = std::min(145ull, delta_ms) + 5; // Prevent DIV/0
int64_t scale = 200'000'000 / (0.001'55 * pow(delta_ms, 5.45)) + 8;
int64_t scale = 200'000'000 / (0.001'55 * std::pow(delta_ms, 5.45)) + 8;
set_value(value() + (delta * scale));
} else {
set_value(value() + (delta * step));
+47
View File
@@ -0,0 +1,47 @@
/*
* Copyright (C) 2015 Jared Boone, ShareBrained Technology, Inc.
* Copyright (C) 2017 Furrtek
*
* 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_tone_key.hpp"
#include <utility>
using namespace ui;
namespace tonekey {
void tone_keys_populate(OptionsField& field) {
OptionsField::options_t tone_key_options;
std::string tone_name;
for (size_t c = 0; c < tone_keys.size(); c++) {
auto f = tone_keys[c].second;
if ((c != 0) && (f < 1000 * 100))
tone_name = "CTCSS " + fx100_string(f) + " #" + tone_keys[c].first;
else
tone_name = tone_keys[c].first;
tone_key_options.emplace_back(tone_name, c);
}
field.set_options(std::move(tone_key_options));
}
} // namespace tonekey
+36
View File
@@ -0,0 +1,36 @@
/*
* Copyright (C) 2015 Jared Boone, ShareBrained Technology, Inc.
* Copyright (C) 2017 Furrtek
*
* 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_TONE_KEY_H_
#define __UI_TONE_KEY_H_
#include "tone_key.hpp"
#include "ui.hpp"
#include "ui_widget.hpp"
namespace tonekey {
void tone_keys_populate(ui::OptionsField& field);
} // namespace tonekey
#endif /*__UI_TONE_KEY_H_*/
+54 -35
View File
@@ -1112,11 +1112,13 @@ NewButton::NewButton(
Rect parent_rect,
std::string text,
const Bitmap* bitmap,
Color color)
Color color,
bool vertical_center)
: Widget{parent_rect},
text_{text},
bitmap_{bitmap},
color_{color} {
color_{color},
vertical_center_{vertical_center} {
set_focusable(true);
}
@@ -1143,6 +1145,11 @@ void NewButton::set_color(Color color) {
set_dirty();
}
void NewButton::set_vertical_center(bool value) {
vertical_center_ = value;
set_dirty();
}
ui::Color NewButton::color() {
return color_;
}
@@ -1164,28 +1171,34 @@ void NewButton::paint(Painter& painter) {
const Style paint_style = {style().font, bg, fg};
painter.draw_rectangle({r.location(), {r.size().width(), 1}}, Color::light_grey());
painter.draw_rectangle({r.location().x(), r.location().y() + r.size().height() - 1, r.size().width(), 1}, Color::dark_grey());
painter.draw_rectangle({r.location().x() + r.size().width() - 1, r.location().y(), 1, r.size().height()}, Color::dark_grey());
painter.draw_rectangle({r.location(), {r.width(), 1}}, Color::light_grey());
painter.draw_rectangle({r.left(), r.top() + r.height() - 1, r.width(), 1}, Color::dark_grey());
painter.draw_rectangle({r.left() + r.width() - 1, r.top(), 1, r.height()}, Color::dark_grey());
painter.fill_rectangle(
{r.location().x(), r.location().y() + 1, r.size().width() - 1, r.size().height() - 2},
{r.left(), r.top() + 1, r.width() - 1, r.height() - 2},
paint_style.background);
int y = r.location().y();
int y = r.top();
if (bitmap_) {
int offset_y = vertical_center_ ? (r.height() / 2) - (bitmap_->size.height() / 2) : 6;
Point bmp_pos = {r.left() + (r.width() / 2) - (bitmap_->size.width() / 2), r.top() + offset_y};
y += bitmap_->size.height() - offset_y;
painter.draw_bitmap(
{r.location().x() + (r.size().width() / 2) - 8, r.location().y() + 6},
bmp_pos,
*bitmap_,
color_, // Color::green(), //fg,
bg);
y += 10;
}
const auto label_r = paint_style.font.size_of(text_);
painter.draw_string(
{r.location().x() + (r.size().width() - label_r.width()) / 2, y + (r.size().height() - label_r.height()) / 2},
paint_style,
text_);
if (!text_.empty()) {
const auto label_r = paint_style.font.size_of(text_);
painter.draw_string(
{r.left() + (r.width() - label_r.width()) / 2, y + (r.height() - label_r.height()) / 2},
paint_style,
text_);
}
}
void NewButton::on_focus() {
@@ -1390,12 +1403,12 @@ void ImageToggle::set_value(bool b) {
/* ImageOptionsField *****************************************************/
ImageOptionsField::ImageOptionsField(
const Rect parent_rect,
const Color foreground,
const Color background,
const options_t options)
Rect parent_rect,
Color foreground,
Color background,
options_t options)
: Widget{parent_rect},
options{options},
options{std::move(options)},
foreground_{foreground},
background_{background} {
set_focusable(true);
@@ -1437,7 +1450,7 @@ void ImageOptionsField::set_by_value(value_t v) {
}
void ImageOptionsField::set_options(options_t new_options) {
options = new_options;
options = std::move(new_options);
// Set an invalid index to force on_change.
selected_index_ = (size_t)-1;
@@ -1482,11 +1495,11 @@ bool ImageOptionsField::on_touch(const TouchEvent event) {
OptionsField::OptionsField(
Point parent_pos,
int length,
size_t length,
options_t options)
: Widget{{parent_pos, {8 * length, 16}}},
: Widget{{parent_pos, {8 * (int)length, 16}}},
length_{length},
options{options} {
options_{std::move(options)} {
set_focusable(true);
}
@@ -1494,16 +1507,20 @@ size_t OptionsField::selected_index() const {
return selected_index_;
}
size_t OptionsField::selected_index_value() const {
return options[selected_index_].second;
const OptionsField::name_t& OptionsField::selected_index_name() const {
return options_[selected_index_].first;
}
const OptionsField::value_t& OptionsField::selected_index_value() const {
return options_[selected_index_].second;
}
void OptionsField::set_selected_index(const size_t new_index, bool trigger_change) {
if (new_index < options.size()) {
if (new_index < options_.size()) {
if (new_index != selected_index() || trigger_change) {
selected_index_ = new_index;
if (on_change) {
on_change(selected_index(), options[selected_index()].second);
on_change(selected_index(), options_[selected_index()].second);
}
set_dirty();
}
@@ -1512,7 +1529,7 @@ void OptionsField::set_selected_index(const size_t new_index, bool trigger_chang
void OptionsField::set_by_value(value_t v) {
size_t new_index = 0;
for (const auto& option : options) {
for (const auto& option : options_) {
if (option.second == v) {
set_selected_index(new_index);
return;
@@ -1529,7 +1546,7 @@ void OptionsField::set_by_nearest_value(value_t v) {
size_t curr_index = 0;
int32_t min_diff = INT32_MAX;
for (const auto& option : options) {
for (const auto& option : options_) {
auto diff = abs(v - option.second);
if (diff < min_diff) {
min_diff = diff;
@@ -1543,7 +1560,7 @@ void OptionsField::set_by_nearest_value(value_t v) {
}
void OptionsField::set_options(options_t new_options) {
options = new_options;
options_ = std::move(new_options);
// Set an invalid index to force on_change.
selected_index_ = (size_t)-1;
@@ -1556,12 +1573,14 @@ void OptionsField::paint(Painter& painter) {
painter.fill_rectangle({screen_rect().location(), {(int)length_ * 8, 16}}, ui::Color::black());
if (selected_index() < options.size()) {
const auto text = options[selected_index()].first;
if (selected_index() < options_.size()) {
std::string_view temp = selected_index_name();
if (temp.length() > length_)
temp = temp.substr(0, length_);
painter.draw_string(
screen_pos(),
paint_style,
text);
temp);
}
}
@@ -1574,8 +1593,8 @@ void OptionsField::on_focus() {
bool OptionsField::on_encoder(const EncoderEvent delta) {
int32_t new_value = selected_index() + delta;
if (new_value < 0)
new_value = options.size() - 1;
else if ((size_t)new_value >= options.size())
new_value = options_.size() - 1;
else if ((size_t)new_value >= options_.size())
new_value = 0;
set_selected_index(new_value);
+13 -9
View File
@@ -455,7 +455,7 @@ class NewButton : public Widget {
NewButton(const NewButton&) = delete;
NewButton& operator=(const NewButton&) = delete;
NewButton(Rect parent_rect, std::string text, const Bitmap* bitmap);
NewButton(Rect parent_rect, std::string text, const Bitmap* bitmap, Color color);
NewButton(Rect parent_rect, std::string text, const Bitmap* bitmap, Color color, bool vertical_center = false);
NewButton()
: NewButton{{}, {}, {}} {
}
@@ -463,6 +463,7 @@ class NewButton : public Widget {
void set_bitmap(const Bitmap* bitmap);
void set_text(const std::string value);
void set_color(Color value);
void set_vertical_center(bool value);
std::string text() const;
const Bitmap* bitmap();
ui::Color color();
@@ -477,6 +478,7 @@ class NewButton : public Widget {
std::string text_;
const Bitmap* bitmap_;
Color color_;
bool vertical_center_{false};
};
class Image : public Widget {
@@ -575,10 +577,10 @@ class ImageOptionsField : public Widget {
std::function<void(void)> on_show_options{};
ImageOptionsField(
const Rect parent_rect,
const Color foreground,
const Color background,
const options_t options);
Rect parent_rect,
Color foreground,
Color background,
options_t options);
ImageOptionsField()
: ImageOptionsField{{}, Color::white(), Color::black(), {}} {
@@ -615,12 +617,14 @@ class OptionsField : public Widget {
std::function<void(size_t, value_t)> on_change{};
std::function<void(void)> on_show_options{};
OptionsField(Point parent_pos, int length, options_t options);
OptionsField(Point parent_pos, size_t length, options_t options);
const options_t& options() const { return options_; }
void set_options(options_t new_options);
size_t selected_index() const;
size_t selected_index_value() const;
const name_t& selected_index_name() const;
const value_t& selected_index_value() const;
void set_selected_index(const size_t new_index, bool trigger_change = true);
void set_by_value(value_t v);
@@ -633,8 +637,8 @@ class OptionsField : public Widget {
bool on_touch(const TouchEvent event) override;
private:
const int length_;
options_t options;
const size_t length_;
options_t options_;
size_t selected_index_{0};
};
Binary file not shown.

After

Width:  |  Height:  |  Size: 234 B

+9
View File
@@ -39,11 +39,20 @@ add_executable(application_test EXCLUDE_FROM_ALL
${PROJECT_SOURCE_DIR}/test_convert.cpp
${PROJECT_SOURCE_DIR}/test_file_reader.cpp
${PROJECT_SOURCE_DIR}/test_file_wrapper.cpp
${PROJECT_SOURCE_DIR}/test_freqman_db.cpp
${PROJECT_SOURCE_DIR}/test_mock_file.cpp
${PROJECT_SOURCE_DIR}/test_optional.cpp
${PROJECT_SOURCE_DIR}/test_string_format.cpp
${PROJECT_SOURCE_DIR}/test_utility.cpp
${PROJECT_SOURCE_DIR}/../../application/file_reader.cpp
${PROJECT_SOURCE_DIR}/../../application/freqman_db.cpp
# Dependencies
${PROJECT_SOURCE_DIR}/../../application/file.cpp
${PROJECT_SOURCE_DIR}/../../application/string_format.cpp
${PROJECT_SOURCE_DIR}/../../application/tone_key.cpp
${PROJECT_SOURCE_DIR}/linker_stubs.cpp
)
target_include_directories(application_test PRIVATE
@@ -0,0 +1,80 @@
/*
* Copyright (C) 2023 Kyle Reed
*
* 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.
*/
/* This file contains stub functions necessary to enable linking.
* Try to minimize dependecies by breaking code into separate files
* or using templates and mock types. Because the test code is built
* and executed on the dev machine, a lot of core firmware code
* will not or cannot work (e.g. filesystem). We could build abstractions
* but that's just device overhead that only supports testing. */
#include <string>
/* FatFS stubs */
#include "ff.h"
FRESULT f_close(FIL*) {
return FR_OK;
}
FRESULT f_closedir(DIR*) {
return FR_OK;
}
FRESULT f_findfirst(DIR*, FILINFO*, const TCHAR*, const TCHAR*) {
return FR_OK;
}
FRESULT f_findnext(DIR*, FILINFO*) {
return FR_OK;
}
FRESULT f_getfree(const TCHAR*, DWORD*, FATFS**) {
return FR_OK;
}
FRESULT f_lseek(FIL*, FSIZE_t) {
return FR_OK;
}
FRESULT f_mkdir(const TCHAR*) {
return FR_OK;
}
FRESULT f_open(FIL*, const TCHAR*, BYTE) {
return FR_OK;
}
FRESULT f_read(FIL*, void*, UINT, UINT*) {
return FR_OK;
}
FRESULT f_rename(const TCHAR*, const TCHAR*) {
return FR_OK;
}
FRESULT f_stat(const TCHAR*, FILINFO*) {
return FR_OK;
}
FRESULT f_sync(FIL*) {
return FR_OK;
}
FRESULT f_truncate(FIL*) {
return FR_OK;
}
FRESULT f_unlink(const TCHAR*) {
return FR_OK;
}
FRESULT f_write(FIL*, const void*, UINT, UINT*) {
return FR_OK;
}
/* Debug */
void __debug_log(const std::string&) {}
@@ -38,6 +38,20 @@ TEST_CASE("It can iterate file lines.") {
CHECK_EQ(line_count, 3);
}
TEST_CASE("It can iterate multiple times.") {
MockFile f{"abc\ndef\nhij"};
BufferLineReader<MockFile> reader{f};
int line_count = 0;
int line_count2 = 0;
for (const auto& line : reader)
++line_count;
for (const auto& line : reader)
++line_count2;
CHECK_EQ(line_count, 3);
CHECK_EQ(line_count2, 3);
}
TEST_CASE("It can iterate file ending with newline.") {
MockFile f{"abc\ndef\nhij\n"};
BufferLineReader<MockFile> reader{f};
@@ -133,6 +147,18 @@ TEST_CASE("It will split only empty columns.") {
TEST_SUITE_END();
TEST_CASE("count_lines returns 1 for single line") {
MockFile f{"abs"};
BufferLineReader<MockFile> reader{f};
CHECK_EQ(count_lines(reader), 1);
}
TEST_CASE("count_lines returns 2 for 2 lines") {
MockFile f{"abs"};
BufferLineReader<MockFile> reader{f};
CHECK_EQ(count_lines(reader), 1);
}
/* Simple example of how to use this to read settings by lines. */
TEST_CASE("It can parse a settings file.") {
MockFile f{"100,File.txt,5\n200,File2.txt,7"};
@@ -0,0 +1,247 @@
/*
* Copyright (C) 2023 Kyle Reed
*
* 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 "doctest.h"
#include "freqman_db.hpp"
TEST_SUITE_BEGIN("Freqman Parsing");
TEST_CASE("It can parse basic single freq entry.") {
freqman_entry e;
REQUIRE(
parse_freqman_entry(
"f=123000000,d=This is the description.", e));
CHECK_EQ(e.frequency_a, 123'000'000);
CHECK_EQ(e.description, "This is the description.");
CHECK_EQ(e.type, freqman_type::Single);
}
TEST_CASE("It can parse basic range freq entry.") {
freqman_entry e;
REQUIRE(
parse_freqman_entry(
"a=123000000,b=423000000,d=This is the description.", e));
CHECK_EQ(e.frequency_a, 123'000'000);
CHECK_EQ(e.frequency_b, 423'000'000);
CHECK_EQ(e.description, "This is the description.");
CHECK_EQ(e.type, freqman_type::Range);
}
TEST_CASE("It can parse basic ham radio freq entry.") {
freqman_entry e;
REQUIRE(
parse_freqman_entry(
"r=123000000,t=423000000,d=This is the description.", e));
CHECK_EQ(e.frequency_a, 123'000'000);
CHECK_EQ(e.frequency_b, 423'000'000);
CHECK_EQ(e.description, "This is the description.");
CHECK_EQ(e.type, freqman_type::HamRadio);
}
TEST_CASE("It can parse modulation") {
freqman_entry e;
REQUIRE(
parse_freqman_entry(
"f=123000000,d=This is the description.,m=AM", e));
CHECK_EQ(e.modulation, 0);
REQUIRE(
parse_freqman_entry(
"f=123000000,d=This is the description.,m=NFM", e));
CHECK_EQ(e.modulation, 1);
REQUIRE(
parse_freqman_entry(
"f=123000000,d=This is the description.,m=WFM", e));
CHECK_EQ(e.modulation, 2);
REQUIRE(
parse_freqman_entry(
"f=123000000,d=This is the description.,m=SPEC", e));
CHECK_EQ(e.modulation, 3);
REQUIRE(
parse_freqman_entry(
"f=123000000,d=This is the description.,m=FOO", e));
CHECK_EQ(e.modulation, freqman_invalid_index);
}
TEST_CASE("It can parse bandwidth") {
freqman_entry e;
REQUIRE(
parse_freqman_entry(
"f=123000000,d=This is the description.,m=AM,bw=DSB 6k", e));
CHECK_EQ(e.bandwidth, 1);
REQUIRE(
parse_freqman_entry(
"f=123000000,d=This is the description.,bw=DSB 6k", e));
// Modulation wasn't set.
CHECK_EQ(e.bandwidth, freqman_invalid_index);
REQUIRE(
parse_freqman_entry(
"f=123000000,d=This is the description.,m=WFM,bw=50k", e));
// Invalid bandwidth value.
CHECK_EQ(e.bandwidth, freqman_invalid_index);
REQUIRE(
parse_freqman_entry(
"f=123000000,d=This is the description.,m=SPEC,bw=16k", e));
CHECK_EQ(e.modulation, 3);
REQUIRE(
parse_freqman_entry(
"f=123000000,m=NFM,bw=11k,d=This is the description.", e));
CHECK_EQ(e.modulation, 1);
}
TEST_CASE("It can parse frequency step") {
freqman_entry e;
REQUIRE(
parse_freqman_entry(
"f=123000000,d=This is the description.,s=0.1kHz", e));
CHECK_EQ(e.step, 0);
REQUIRE(
parse_freqman_entry(
"f=123000000,d=This is the description.,s=50kHz", e));
CHECK_EQ(e.step, 11);
REQUIRE(
parse_freqman_entry(
"f=123000000,d=This is the description.,s=FOO", e));
CHECK_EQ(e.step, freqman_invalid_index);
}
TEST_CASE("It can parse tone freq") {
freqman_entry e;
REQUIRE(
parse_freqman_entry(
"f=123000000,d=This is the description.,c=0.0", e));
CHECK_EQ(e.tone, 0);
REQUIRE(
parse_freqman_entry(
"f=123000000,c=67.0,d=This is the description.", e));
CHECK_EQ(e.tone, 1);
REQUIRE(
parse_freqman_entry(
"f=123000000,c=67,d=This is the description.", e));
// Fractional can be omitted.
CHECK_EQ(e.tone, 1);
REQUIRE(
parse_freqman_entry(
"f=123000000,c=69.33,d=This is the description.", e));
// Fractional extra digits can be omitted.
CHECK_EQ(e.tone, 2);
REQUIRE(
parse_freqman_entry(
"f=123000000,d=This is the description.,c=72", e));
// Should choose nearest.
CHECK_EQ(e.tone, 3);
}
TEST_CASE("It can serialize basic Single entry") {
auto str = to_freqman_string(freqman_entry{
.frequency_a = 123'456'000,
.description = "Foobar",
.type = freqman_type::Single,
});
CHECK(str == "f=123456000,d=Foobar");
}
TEST_CASE("It can serialize basic Range entry") {
auto str = to_freqman_string(freqman_entry{
.frequency_a = 123'456'000,
.frequency_b = 423'456'000,
.description = "Foobar",
.type = freqman_type::Range,
});
CHECK(str == "a=123456000,b=423456000,d=Foobar");
}
TEST_CASE("It can serialize basic HamRadio entry") {
auto str = to_freqman_string(freqman_entry{
.frequency_a = 123'456'000,
.frequency_b = 423'456'000,
.description = "Foobar",
.type = freqman_type::HamRadio,
});
CHECK(str == "r=123456000,t=423456000,d=Foobar");
}
// New tables for a future PR.
/*
TEST_CASE("It can parse modulation") {
freqman_entry e;
REQUIRE(
parse_freqman_entry(
"f=123000000,d=This is the description.,m=AM", e));
CHECK_EQ(e.modulation, freqman_modulation::AM);
REQUIRE(
parse_freqman_entry(
"f=123000000,d=This is the description.,m=NFM", e));
CHECK_EQ(e.modulation, freqman_modulation::NFM);
REQUIRE(
parse_freqman_entry(
"f=123000000,d=This is the description.,m=WFM", e));
CHECK_EQ(e.modulation, freqman_modulation::WFM);
REQUIRE(
parse_freqman_entry(
"f=123000000,d=This is the description.,m=SPEC", e));
CHECK_EQ(e.modulation, freqman_modulation::SPEC);
REQUIRE(
parse_freqman_entry(
"f=123000000,d=This is the description.,m=FOO", e));
CHECK_EQ(e.modulation, freqman_modulation::Unknown);
}
TEST_CASE("It can parse frequency step") {
freqman_entry e;
REQUIRE(
parse_freqman_entry(
"f=123000000,d=This is the description.,s=0.1kHz", e));
CHECK_EQ(e.step, freqman_step::_100Hz);
REQUIRE(
parse_freqman_entry(
"f=123000000,d=This is the description.,s=50kHz", e));
CHECK_EQ(e.step, freqman_step::_50kHz);
REQUIRE(
parse_freqman_entry(
"f=123000000,d=This is the description.,s=FOO", e));
CHECK_EQ(e.step, freqman_step::Unknown);
}
*/
TEST_SUITE_END();
@@ -0,0 +1,49 @@
/*
* Copyright (C) 2023 Kyle Reed
*
* 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 "doctest.h"
#include "string_format.hpp"
/* TODO: Tests for all string_format functions. */
TEST_CASE("to_string_dec_uint64 returns correct value.") {
CHECK_EQ(to_string_dec_uint64(0), "0");
CHECK_EQ(to_string_dec_uint64(1), "1");
CHECK_EQ(to_string_dec_uint64(1'000'000), "1000000");
CHECK_EQ(to_string_dec_uint64(1'234'567'890), "1234567890");
CHECK_EQ(to_string_dec_uint64(1'234'567'891), "1234567891");
}
/*TEST_CASE("to_string_freq returns correct value.") {
CHECK_EQ(to_string_freq(0), "0");
CHECK_EQ(to_string_freq(1), "1");
CHECK_EQ(to_string_freq(1'000'000), "1000000");
CHECK_EQ(to_string_freq(1'234'567'890), "1234567890");
CHECK_EQ(to_string_freq(1'234'567'891), "1234567891");
}*/
TEST_CASE("trim removes whitespace.") {
CHECK(trim(" foo\n") == "foo");
}
TEST_CASE("trim returns empty for only whitespace.") {
CHECK(trim(" \n").empty());
}