Compare commits

...

18 Commits

Author SHA1 Message Date
Mark Thompson 2498861003 Increased number of encoder dial sensitivity setting levels (#1275)
* Encoder dial sensitivity 0-4

* Workaround for worn encoders & more sensitivity levels

* Workaround for worn encoders & more sensitivity levels

* Encoder sensitivity levels 0-4

* Encoder sensitivity levels 0-4

* Fix comment

* Clang

* Clang

* Change default encoder sensitivity in pmem

* Set default encoder sensitivity

* Set default encoder sensitivity

* Revert widget type for encoder sensitivity

* Revert widget type for encoder dial sensitivity

* Revert worn encoder workaround (didn't work)

* 5 levels of encoder dial sensitivity

* 5 levels of encoder dial sensitivity
2023-07-16 17:05:27 -05:00
Kyle Reed 25923e82a4 New Freqman edit UI (#1272)
* WIP new edit UI

* Fix textfield highlight

* WIP new edit

* Wrap up first pass of freqman edit

* Fix indexing of options

---------

Co-authored-by: kallanreed <kallanreed@noreply.github.com>
2023-07-14 18:46:39 -07:00
Mark Thompson 61cb57e48d Added to_string_rounded_freq() function (#1271)
* Added to_string_rounded_freq function

* Added to_string_rounded_freq function

* Clang

* Clang try #2

* Use constexpr for pow10 array

* Additional string_format test cases

* Specify std=c++17 for g++ versions that default to an older standard

* Corrected string_format test cases
2023-07-14 13:38:02 -05:00
Kyle Reed 4985d836ea Support save prompt on back arrow (#1270)
* Support save prompt on back arrow
* Add lifetime warning comment
2023-07-13 18:38:40 +02:00
eried e1a79a0ff1 Fonts for pcb 2023-07-13 15:21:05 +02:00
Kyle Reed d72d935607 Support showing invalid entries in Freqman and allow minor edits (#1269)
* Support showing invalid entries in Freqman and allow minor edits
* Use light_grey instead of grey
* Fix comment
* Fix null in description bug
* Fix spacing in delete entry dialog
* trim in delete modal
2023-07-13 08:28:27 +02:00
Mark Thompson 4ed06b9eff Correct HamRadio frequency validation (#1266) 2023-07-12 20:27:02 +02:00
Mark Thompson fa4623db7c Print leading zeroes after decimal point (#1265) 2023-07-12 13:00:45 -05:00
Mark Thompson a44e8b9921 Use unicode standard 0xA0-0xFF for Latin 1 characters (#1263)
* Update chars 0xA0-0xFF to match unicode

* Update ui_text.hpp

Update chars 0xA0-0xFF to match unicode

* Use unicode 0xB0 for degree character

* Clang
2023-07-12 07:14:00 -05:00
jLynx 6b4f1a552e Changelog refactoring (#1264)
* Delete stable_changelog.py

* Update changelog.py

* Update create_stable_release.yml
2023-07-12 18:50:39 +12:00
pathetic nihilist 3d32682a67 time counting fix (#1261) 2023-07-12 13:10:52 +12:00
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
88 changed files with 3742 additions and 1462 deletions
+44 -17
View File
@@ -1,28 +1,55 @@
import os
import re
import sys
raw_git = os.popen('git log next --since="24 hours" --pretty=format:"- %h - {USERNAME}*+%al-%an*: %s"').read()
import requests
from datetime import datetime, timedelta, timezone
# Set up your personal access token and the repository details
token = os.environ.get('GH_TOKEN')
repo_owner = "eried"
repo_name = "portapack-mayhem"
def compute_username(line):
stripped = re.search(r'(?<=\*)(.*?)(?=\*)', line).group(0)
def print_stable_changelog(previous_sha):
url = f"compare/{previous_sha}...next"
commits = handle_get_request(url)
for commit in commits["commits"]:
# Print the commit details
print(format_output(commit))
pattern = re.compile("[$@+&?].*[$@+&?]")
if pattern.match(stripped):
stripped = re.sub("[$@+&?].*[$@+&?]", "", stripped)
stripped = re.match(r'.+?(?=-)', stripped).group(0)
def print_nightly_changelog():
# Calculate the date and time 24 hours ago
since_time = (datetime.now(timezone.utc) - timedelta(hours=24)).isoformat() # Check that this is UTC
url = "commits"
commits = handle_get_request(url, since_time)
for commit in commits:
# Print the commit details
print(format_output(commit))
def handle_get_request(path, offset=None):
headers = {"Authorization": f"Bearer {token}"}
params = {"since": offset}
url_base = f"https://api.github.com/repos/{repo_owner}/{repo_name}/"
response = requests.get(url_base + path, headers=headers, params=params)
# Check if the request was successful (status code 200)
if response.status_code == 200:
return response.json()
else:
stripped = re.sub(r'^.*?-', "", stripped)
return "@" + stripped
print(f"Request failed with status code: {response.status_code}")
return None
def compile_line(line):
username = compute_username(line)
line = re.sub(r'[*].*[*]', "", line)
line = line.replace("{USERNAME}", username)
return line
def format_output(commit):
message_lines = commit["commit"]["message"].split("\n")
author = commit["author"]["login"] if commit["author"] and "login" in commit["author"] else commit["commit"]["author"]["name"]
return '- ' + commit["sha"][:8] + ' - @' + author + ': ' + message_lines[0]
for row in raw_git.splitlines():
print(compile_line(row))
if len(sys.argv) < 2:
print_nightly_changelog()
else:
past_version = sys.argv[1]
print_stable_changelog(past_version)
@@ -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
+16 -1
View File
@@ -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,8 +47,10 @@ 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=$(python3 .github/workflows/changelog.py ${{ steps.past_version.outputs.past_version }})
CHANGELOG="${CHANGELOG//'%'/'%25'}"
CHANGELOG="${CHANGELOG//$'\n'/'%0A'}"
CHANGELOG="${CHANGELOG//$'\r'/'%0D'}"
@@ -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
-30
View File
@@ -1,30 +0,0 @@
import os
import re
import sys
past_version = sys.argv[1]
raw_git = os.popen('git log ' + past_version + '..next --pretty=format:"- %h - {USERNAME}*+%al-%an*: %s"').read()
def compute_username(line):
stripped = re.search(r'(?<=\*)(.*?)(?=\*)', line).group(0)
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
def compile_line(line):
username = compute_username(line)
line = re.sub(r'[*].*[*]', "", line)
line = line.replace("{USERNAME}", username)
return line
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);
+393 -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,281 +23,480 @@
#include "ui_freqman.hpp"
#include "portapack.hpp"
#include "event_m0.hpp"
#include "freqman.hpp"
#include "portapack.hpp"
#include "rtc_time.hpp"
#include "tone_key.hpp"
#include "ui_receiver.hpp"
#include "ui_styles.hpp"
#include "utility.hpp"
#include <memory>
using namespace portapack;
namespace fs = std::filesystem;
// TODO: Clean up after moving to better lookup tables.
extern ui::OptionsField::options_t freqman_bandwidths[4];
extern ui::OptionsField::options_t freqman_steps;
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_, desc_edit_max, [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_entry() {
auto edit_view = nav_.push<FrequencyEditView>(current_entry());
edit_view->on_save = [this](const freqman_entry& entry) {
db_.replace_entry(current_index(), entry);
freqlist_view.set_dirty();
};
}
void FrequencyManagerView::on_edit_freq() {
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_, desc_edit_max, [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_, 20, [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 " + trim(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,
&button_add_category,
&button_del_category,
&button_edit_entry,
&rect_padding,
&button_edit_freq,
&button_edit_desc,
&button_add_entry,
&button_del_entry});
freqlist_view.on_select = [this](size_t) {
button_edit_entry.focus();
};
// Allows for quickly exiting control.
freqlist_view.on_leave = [this]() {
button_edit_entry.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&) {
button_edit_freq.focus();
button_add_category.on_select = [this]() {
on_add_category();
};
button_new_category.on_select = [this, &nav](Button&) {
desc_buffer = "";
on_new_category(nav);
button_del_category.on_select = [this]() {
on_del_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_edit_entry.on_select = [this](Button&) {
on_edit_entry();
};
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();
};
}
/* FrequencyEditView *************************************/
FrequencyEditView::FrequencyEditView(
NavigationView& nav,
freqman_entry entry)
: nav_{nav},
entry_{std::move(entry)} {
add_children({&labels,
&field_type,
&field_freq_a,
&field_freq_b,
&field_modulation,
&field_bandwidth,
&field_step,
&field_tone,
&field_description,
&text_validation,
&button_save,
&button_cancel});
freqman_set_modulation_option(field_modulation);
populate_bandwidth_options();
populate_step_options();
populate_tone_options();
// Add the "invalid/unset" option. Kind of hacky, but...
field_modulation.options().insert(
field_modulation.options().begin(), {"None", -1});
field_step.options().insert(
field_step.options().begin(), {"None", -1});
field_type.set_by_value((int32_t)entry_.type);
field_type.on_change = [this](size_t, auto value) {
entry_.type = static_cast<freqman_type>(value);
refresh_ui();
};
// TODO: this pattern should be able to be wrapped up.
field_freq_a.set_value(entry_.frequency_a);
field_freq_a.on_change = [this](rf::Frequency f) {
entry_.frequency_a = f;
refresh_ui();
};
field_freq_a.on_edit = [this]() {
auto freq_view = nav_.push<FrequencyKeypadView>(field_freq_a.value());
freq_view->on_changed = [this](rf::Frequency f) {
field_freq_a.set_value(f);
};
};
field_freq_b.set_value(entry_.frequency_b);
field_freq_b.on_change = [this](rf::Frequency f) {
entry_.frequency_b = f;
refresh_ui();
};
field_freq_b.on_edit = [this]() {
auto freq_view = nav_.push<FrequencyKeypadView>(field_freq_b.value());
freq_view->on_changed = [this](rf::Frequency f) {
field_freq_b.set_value(f);
};
};
field_modulation.set_by_value((int32_t)entry_.modulation);
field_modulation.on_change = [this](size_t, auto value) {
entry_.modulation = static_cast<freqman_index_t>(value);
populate_bandwidth_options();
};
field_bandwidth.set_by_value((int32_t)entry_.bandwidth);
field_bandwidth.on_change = [this](size_t, auto value) {
entry_.bandwidth = static_cast<freqman_index_t>(value);
};
field_step.set_by_value((int32_t)entry_.step);
field_step.on_change = [this](size_t, auto value) {
entry_.step = static_cast<freqman_index_t>(value);
};
field_tone.set_by_value((int32_t)entry_.tone);
field_tone.on_change = [this](size_t, auto value) {
entry_.tone = static_cast<freqman_index_t>(value);
};
field_description.set_text(entry_.description);
field_description.on_change = [this](TextField& tf) {
entry_.description = tf.get_text();
};
field_description.on_select = [this](TextField& tf) {
temp_buffer_ = tf.get_text();
text_prompt(nav_, temp_buffer_, FreqManBaseView::desc_edit_max,
[this, &tf](std::string& new_desc) {
tf.set_text(new_desc);
});
};
button_save.on_select = [this](Button&) {
if (on_save)
on_save(std::move(entry_));
nav_.pop();
};
button_cancel.on_select = [this](Button&) {
nav_.pop();
};
refresh_ui();
}
void FrequencyEditView::focus() {
button_cancel.focus();
}
void FrequencyEditView::refresh_ui() {
// This needs to be called whenever a UI option is changed
// in a way that causes fields to be unused or would make the
// entry fail validation.
auto is_range = entry_.type == freqman_type::Range;
auto is_ham = entry_.type == freqman_type::HamRadio;
auto has_freq_b = is_range || is_ham;
field_freq_b.set_style(has_freq_b ? &Styles::white : &Styles::grey);
field_step.set_style(is_range ? &Styles::white : &Styles::grey);
field_tone.set_style(is_ham ? &Styles::white : &Styles::grey);
if (is_valid(entry_)) {
text_validation.set("Valid");
text_validation.set_style(&Styles::green);
} else {
text_validation.set("Error");
text_validation.set_style(&Styles::red);
}
}
// TODO: The following functions shouldn't be needed once
// freqman_db lookup tables are complete.
void FrequencyEditView::populate_bandwidth_options() {
OptionsField::options_t options;
options.push_back({"None", -1});
if (entry_.modulation < std::size(freqman_bandwidths)) {
auto& bandwidths = freqman_bandwidths[entry_.modulation];
for (auto i = 1u; i < bandwidths.size(); ++i) {
auto& item = bandwidths[i];
options.push_back({item.first, (OptionsField::value_t)i});
}
}
field_bandwidth.set_options(std::move(options));
}
void FrequencyEditView::populate_step_options() {
OptionsField::options_t options;
options.push_back({"None", -1});
for (auto i = 1u; i < freqman_steps.size(); ++i) {
auto& item = freqman_steps[i];
options.push_back({item.first, (OptionsField::value_t)i});
}
field_step.set_options(std::move(options));
}
void FrequencyEditView::populate_tone_options() {
OptionsField::options_t options;
options.push_back({"None", -1});
for (auto i = 1u; i < tonekey::tone_keys.size(); ++i) {
auto& item = tonekey::tone_keys[i];
options.push_back({item.first, (OptionsField::value_t)i});
}
field_tone.set_options(std::move(options));
}
} // namespace ui
+165 -69
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,94 +21,99 @@
* 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 {
class FreqManBaseView : public View {
public:
FreqManBaseView(
NavigationView& nav);
FreqManBaseView(NavigationView& nav);
void focus() override;
static constexpr size_t desc_edit_max = 0x80;
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};
std::function<void(void)> on_select_frequency{};
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 +122,137 @@ 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"; };
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_entry();
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()}};
NewButton button_add_category{
{23 * 8, 0 * 16, 7 * 4, 20},
{},
&bitmap_icon_new_file,
Color::white(),
true};
Button button_new_category{
{23 * 8, 2, 7 * 8, 20},
"New"};
NewButton button_del_category{
{26 * 8 + 4, 0 * 16, 7 * 4, 20},
{},
&bitmap_icon_trash,
Color::red(),
true};
Button button_edit_entry{
{0 * 8, 14 * 16 - 4, 15 * 8, 1 * 16 + 4},
"Edit"};
Rectangle rect_padding{
{15 * 8, 14 * 16 - 4, 15 * 8, 1 * 16 + 4},
Color::grey()};
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};
};
class FrequencyEditView : public View {
public:
std::function<void(const freqman_entry&)> on_save{};
FrequencyEditView(
NavigationView& nav,
freqman_entry entry);
std::string title() const override { return "Freqman Edit"; }
void focus() override;
private:
NavigationView& nav_;
freqman_entry entry_;
std::string temp_buffer_{};
void refresh_ui();
void populate_bandwidth_options();
void populate_step_options();
void populate_tone_options();
Labels labels{
{{5 * 8, 1 * 16}, "Edit Frequency Entry", Color::white()},
{{0 * 8, 3 * 16}, "Entry Type :", Color::light_grey()},
{{0 * 8, 4 * 16}, "Frequency A:", Color::light_grey()},
{{0 * 8, 5 * 16}, "Frequency B:", Color::light_grey()},
{{0 * 8, 6 * 16}, "Modulation :", Color::light_grey()},
{{0 * 8, 7 * 16}, "Bandwidth :", Color::light_grey()},
{{0 * 8, 8 * 16}, "Step :", Color::light_grey()},
{{0 * 8, 9 * 16}, "Tone Freq :", Color::light_grey()},
{{0 * 8, 10 * 16}, "Description:", Color::light_grey()},
};
OptionsField field_type{{13 * 8, 3 * 16}, 8, {
{"Single", 0},
{"Range", 1},
{"HamRadio", 2},
{"Raw", 3},
}};
FrequencyField field_freq_a{{13 * 8, 4 * 16}};
FrequencyField field_freq_b{{13 * 8, 5 * 16}};
OptionsField field_modulation{{13 * 8, 6 * 16}, 5, {}};
OptionsField field_bandwidth{{13 * 8, 7 * 16}, 7, {}};
OptionsField field_step{{13 * 8, 8 * 16}, 12, {}};
OptionsField field_tone{{13 * 8, 9 * 16}, 13, {}};
TextField field_description{
{13 * 8, 10 * 16, 17 * 8, 1 * 16},
{}};
Text text_validation{
{12 * 8, 12 * 16, 5 * 8, 1 * 16},
{}};
Button button_save{
{0 * 8, 17 * 16, 15 * 8, 2 * 16},
"Save"};
Button button_cancel{
{15 * 8, 17 * 16, 15 * 8, 2 * 16},
"Cancel"};
};
} /* 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;
+6 -4
View File
@@ -480,10 +480,12 @@ class SetEncoderDialView : public View {
OptionsField field_encoder_dial_sensitivity{
{20 * 8, 3 * 16},
6,
{{"LOW", encoder_dial_sensitivity::DIAL_SENSITIVITY_LOW},
{"NORMAL", encoder_dial_sensitivity::DIAL_SENSITIVITY_MEDIUM},
{"HIGH", encoder_dial_sensitivity::DIAL_SENSITIVITY_HIGH}}};
7,
{{"LOWEST", encoder_dial_sensitivity::DIAL_SENSITIVITY_LOWEST},
{"LOW", encoder_dial_sensitivity::DIAL_SENSITIVITY_LOW},
{"NORMAL", encoder_dial_sensitivity::DIAL_SENSITIVITY_NORMAL},
{"HIGH", encoder_dial_sensitivity::DIAL_SENSITIVITY_HIGH},
{"HIGHEST", encoder_dial_sensitivity::DIAL_SENSITIVITY_HIGHEST}}};
Button button_save{
{2 * 8, 16 * 16, 12 * 8, 32},
+51 -31
View File
@@ -301,6 +301,38 @@ void TextEditorMenu::hide_children(bool hidden) {
/* TextEditorView ***************************************************/
static fs::path get_temp_path(const fs::path& path) {
if (!path.empty())
return path + "~";
return {};
}
static void delete_temp_file(const fs::path& path) {
auto temp_path = get_temp_path(path);
if (!temp_path.empty()) {
delete_file(temp_path);
}
}
static void save_temp_file(const fs::path& path) {
delete_file(path);
copy_file(get_temp_path(path), path);
}
static void show_save_prompt(
NavigationView& nav,
std::function<void()> on_save,
std::function<void()> continuation) {
nav.display_modal(
"Save?", "Save changes?", YESNO,
[on_save](bool choice) {
if (choice && on_save)
on_save();
});
nav.set_on_pop(continuation);
}
TextEditorView::TextEditorView(NavigationView& nav)
: nav_{nav} {
add_children(
@@ -381,9 +413,7 @@ TextEditorView::TextEditorView(NavigationView& nav)
};
menu.on_exit() = [this]() {
show_save_prompt([this]() {
nav_.pop();
});
nav_.pop();
};
button_menu.on_select = [this]() {
@@ -402,7 +432,15 @@ TextEditorView::TextEditorView(NavigationView& nav, const fs::path& path)
}
TextEditorView::~TextEditorView() {
delete_temp_file();
// NB: Be careful here. The UI will render after this instance
// has been destroyed. Everything needed to render the UI
// and perform the save actions must be value captured.
if (file_dirty_) {
ui::show_save_prompt(
nav_,
[p = path_]() { ui::save_temp_file(p); },
[p = std::move(path_)]() { delete_temp_file(p); });
}
}
void TextEditorView::on_show() {
@@ -415,7 +453,7 @@ void TextEditorView::on_show() {
void TextEditorView::open_file(const fs::path& path) {
file_.reset();
viewer.clear_file();
delete_temp_file();
delete_temp_file(path_);
path_ = {};
file_dirty_ = false;
@@ -516,13 +554,10 @@ void TextEditorView::show_save_prompt(std::function<void()> continuation) {
return;
}
nav_.display_modal(
"Save?", "Save changes?", YESNO,
[this](bool choice) {
if (choice)
save_temp_file();
});
nav_.set_on_pop(continuation);
ui::show_save_prompt(
nav_,
[this]() { save_temp_file(); },
continuation);
}
void TextEditorView::prepare_for_write() {
@@ -533,31 +568,16 @@ void TextEditorView::prepare_for_write() {
// Copy to temp file on write.
has_temp_file_ = true;
delete_temp_file();
copy_file(path_, get_temp_path());
file_->assume_file(get_temp_path());
}
void TextEditorView::delete_temp_file() const {
auto temp_path = get_temp_path();
if (!temp_path.empty()) {
delete_file(temp_path);
}
delete_temp_file(path_);
copy_file(path_, get_temp_path(path_));
file_->assume_file(get_temp_path(path_));
}
void TextEditorView::save_temp_file() {
if (file_dirty_) {
delete_file(path_);
copy_file(get_temp_path(), path_);
ui::save_temp_file(path_);
file_dirty_ = false;
}
}
fs::path TextEditorView::get_temp_path() const {
if (!path_.empty())
return path_ + "~";
return {};
}
} // namespace ui
@@ -229,10 +229,7 @@ class TextEditorView : public View {
void show_save_prompt(std::function<void()> continuation);
void prepare_for_write();
void create_temp_file() const;
void delete_temp_file() const;
void save_temp_file();
std::filesystem::path get_temp_path() const;
NavigationView& nav_;
std::unique_ptr<FileWrapper> file_{};
+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,
+43
View File
@@ -0,0 +1,43 @@
/*
* 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 __FIELD_BINDER_H__
#define __FIELD_BINDER_H__
/* There's a common pattern that appears in applications that use
* field widgets. The field value is set, on_change and on_select
* handlers are registered. on_select will read the value and
* set the new value once changed. on_change will be called which
* will write the new value to the backing variable. */
struct binding_traits {
};
template <typename T>
constexpr auto get_traits() {
return {};
}
template <typename TField, typename TTraits = get_traits<TField>()>
class FieldBinding {
};
#endif /*__FIELD_BINDER_H__*/
+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__*/
+483
View File
@@ -0,0 +1,483 @@
/*
* 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)
},
};
// TODO: these should be indexes.
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},
};
// TODO: these should be indexes.
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 = "R:" + to_string_dec_uint(entry.frequency_a / 1'000'000) + "M,T:" +
to_string_dec_uint(entry.frequency_b / 1'000'000) + "M: " + entry.description;
break;
case freqman_type::Raw:
str = 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;
case freqman_type::Raw:
return entry.description;
default:
return {};
};
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);
}
}
return is_valid(entry);
}
// 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;
}
bool is_valid(const freqman_entry& entry) {
// No valid frequency combination was set.
if (entry.type == freqman_type::Unknown)
return false;
// Frequency A must be set for all types
if (entry.frequency_a == 0)
return false;
// Frequency B must be set for type Range or Ham Radio
if (entry.type == freqman_type::Range || entry.type == freqman_type::HamRadio) {
if (entry.frequency_b == 0)
return false;
}
// Ranges should have frequencies A <= B.
if (entry.type == freqman_type::Range) {
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;
}
/* 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;
else {
entry.type = freqman_type::Raw;
entry.description = trim(*line_text);
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;
// 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;
}
+238
View File
@@ -0,0 +1,238 @@
/*
* 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=
Raw, // line content in description
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{}; // '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);
/* Returns true if the entry is well-formed. */
bool is_valid(const freqman_entry& entry);
/* 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__ */
+27 -62
View File
@@ -32,66 +32,24 @@
// 12 degrees of rotation.
//
// For each encoder "pulse" there are 4 state transitions, and we can choose
// between looking at all of them (high sensitivity), half of them (medium/default),
// or one quarter of them (low sensitivity).
static const int8_t transition_map[][16] = {
// Normal (Medium) Sensitivity -- default
{
0, // 0000: noop
0, // 0001: ccw start
0, // 0010: cw start
0, // 0011: rate
1, // 0100: cw end
0, // 0101: noop
0, // 0110: rate
-1, // 0111: ccw end
-1, // 1000: ccw end
0, // 1001: rate
0, // 1010: noop
1, // 1011: cw end
0, // 1100: rate
0, // 1101: cw start
0, // 1110: ccw start
0, // 1111: noop
},
// Low Sensitivity
{
0, // 0000: noop
0, // 0001: ccw start
0, // 0010: cw start
0, // 0011: rate
1, // 0100: cw end
0, // 0101: noop
0, // 0110: rate
0, // 0111: ccw end
-1, // 1000: ccw end
0, // 1001: rate
0, // 1010: noop
0, // 1011: cw end
0, // 1100: rate
0, // 1101: cw start
0, // 1110: ccw start
0, // 1111: noop
},
// High Sensitivity
{
0, // 0000: noop
-1, // 0001: ccw start
1, // 0010: cw start
0, // 0011: rate
1, // 0100: cw end
0, // 0101: noop
0, // 0110: rate
-1, // 0111: ccw end
-1, // 1000: ccw end
0, // 1001: rate
0, // 1010: noop
1, // 1011: cw end
0, // 1100: rate
1, // 1101: cw start
-1, // 1110: ccw start
0, // 1111: noop
},
// how many transitions are needed before movement is registered
static const int8_t transition_map[16] = {
0, // 0000: noop
-1, // 0001: ccw start
1, // 0010: cw start
0, // 0011: rate
1, // 0100: cw end
0, // 0101: noop
0, // 0110: rate
-1, // 0111: ccw end
-1, // 1000: ccw end
0, // 1001: rate
0, // 1010: noop
1, // 1011: cw end
0, // 1100: rate
1, // 1101: cw start
-1, // 1110: ccw start
0, // 1111: noop
};
int_fast8_t Encoder::update(
@@ -102,6 +60,13 @@ int_fast8_t Encoder::update(
state <<= 1;
state |= phase_1;
// dial sensitivity setting is stored in pmem
return transition_map[portapack::persistent_memory::config_encoder_dial_sensitivity()][state & 0xf];
int_fast8_t retval = transition_map[state & 0xf];
transition_count += retval;
if (abs(transition_count) > portapack::persistent_memory::config_encoder_dial_sensitivity())
transition_count = 0;
else
retval = 0;
return retval;
}
+1
View File
@@ -32,6 +32,7 @@ class Encoder {
private:
uint_fast8_t state{0};
int_fast8_t transition_count{0};
};
#endif /*__ENCODER_H__*/
+70 -10
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) {
@@ -89,8 +111,8 @@ std::string to_string_dec_uint(
auto term = p + sizeof(p) - 1;
auto q = to_string_dec_uint_pad_internal(term, n, l, fill);
// TODO: is this needed, seems like pad already does this.
// Right justify.
// (This code is redundant and won't do anything if a fill character was specified)
while ((term - q) < l) {
*(--q) = ' ';
}
@@ -115,6 +137,7 @@ std::string to_string_dec_int(
}
// Right justify.
// (This code is redundant and won't do anything if a fill character was specified)
while ((term - q) < l) {
*(--q) = ' ';
}
@@ -139,13 +162,47 @@ std::string to_string_decimal(float decimal, int8_t precision) {
return result;
}
// right-justified frequency in Hz, always 10 characters
std::string to_string_freq(const uint64_t f) {
auto final_str = to_string_dec_int(f / 1000000, 4) + to_string_dec_int(f % 1000000, 6, '0');
std::string final_str{""};
if (f < 1000000)
final_str = to_string_dec_int(f, 10, ' ');
else
final_str = to_string_dec_int(f / 1000000, 4) + to_string_dec_int(f % 1000000, 6, '0');
return final_str;
}
// right-justified frequency in MHz, rounded to 4 decimal places, always 9 characters
std::string to_string_short_freq(const uint64_t f) {
auto final_str = to_string_dec_int(f / 1000000, 4) + "." + to_string_dec_int((f / 100) % 10000, 4, '0');
auto final_str = to_string_dec_int(f / 1000000, 4) + "." + to_string_dec_int(((f + 50) / 100) % 10000, 4, '0');
return final_str;
}
// non-justified non-padded frequency in MHz, rounded to specified number of decimal places
std::string to_string_rounded_freq(const uint64_t f, int8_t precision) {
std::string final_str{""};
static constexpr uint32_t pow10[7] = {
1,
10,
100,
1000,
10000,
100000,
1000000,
};
if (precision < 1) {
final_str = to_string_dec_uint64(f / 1000000);
} else {
if (precision > 6)
precision = 6;
uint32_t divisor = pow10[6 - precision];
final_str = to_string_dec_uint64(f / 1000000) + "." + to_string_dec_int(((f + (divisor / 2)) / divisor) % pow10[precision], precision, '0');
}
return final_str;
}
@@ -177,7 +234,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;
@@ -263,7 +320,7 @@ std::string unit_auto_scale(double n, const uint32_t base_nano, uint32_t precisi
string = to_string_dec_int(integer_part);
if (precision)
string += '.' + to_string_dec_uint(fractional_part, precision);
string += '.' + to_string_dec_uint(fractional_part, precision, '0');
if (prefix_index != 3)
string += unit_prefix[prefix_index];
@@ -285,6 +342,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,14 +41,18 @@ 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);
@@ -57,6 +61,7 @@ std::string to_string_hex_array(uint8_t* const array, const int32_t l = 0);
std::string to_string_freq(const uint64_t f);
std::string to_string_short_freq(const uint64_t f);
std::string to_string_rounded_freq(const uint64_t f, int8_t precision);
std::string to_string_time_ms(const uint32_t ms);
std::string to_string_datetime(const rtc::RTC& value, const TimeFormat format = YMDHMS);
+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);
+122 -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,147 @@
#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};
auto is_selected = offset == selected_index_;
auto style = 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. Show an empty entry if 'Unknown'.
if (entry.type != freqman_type::Unknown)
text = pretty_string(entry, line_max_length);
// Otherwise, if 'Raw' indicate an invalid entry by color.
if (entry.type == freqman_type::Raw)
style = &Styles::light_grey;
}
// 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, (is_selected ? style->invert() : *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: \xB0 ' \"", Color::light_grey()}, // 0xB0 is degree ° symbol in our 8x16 font
{{1 * 8, 2 * 16}, "Lon: \xB0 ' \"", 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));
+1 -1
View File
@@ -48,7 +48,7 @@ class TextEntryView : public View {
void char_add(const char c);
void char_delete();
TextField text_input;
TextEdit text_input;
Button button_ok{
{22 * 8, 33 * 8, 7 * 8, 32},
"OK"};
+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_*/
@@ -363,6 +363,7 @@ void defaults() {
set_config_backlight_timer(backlight_config_t{});
set_config_splash(true);
set_encoder_dial_sensitivity(DIAL_SENSITIVITY_NORMAL);
// Default values for recon app.
set_recon_autosave_freqs(false);
@@ -107,9 +107,11 @@ struct backlight_config_t {
};
enum encoder_dial_sensitivity {
DIAL_SENSITIVITY_MEDIUM = 0,
DIAL_SENSITIVITY_LOW = 1,
DIAL_SENSITIVITY_HIGH = 2,
DIAL_SENSITIVITY_HIGHEST = 0,
DIAL_SENSITIVITY_HIGH = 1,
DIAL_SENSITIVITY_NORMAL = 2,
DIAL_SENSITIVITY_LOW = 3,
DIAL_SENSITIVITY_LOWEST = 4,
NUM_DIAL_SENSITIVITY
};
+17 -2
View File
@@ -24,11 +24,26 @@
namespace ui {
Glyph Font::glyph(const char c) const {
size_t index;
if (c < c_start) {
// Non-display C0 Control characters - map to blank (index 0)
return {w, h, data};
}
const size_t index = c - c_start;
if (index >= c_count) {
// Handle gap in glyphs table for C1 Control characters 0x80-0x9F
if (c < C1_CONTROL_CHARS_START) {
// ASCII chars <0x80:
index = c - c_start;
} else if (c >= C1_CONTROL_CHARS_START + C1_CONTROL_CHARS_COUNT) {
// Latin 1 chars 0xA0-0xFF
index = c - c_start - C1_CONTROL_CHARS_COUNT;
} else {
// C1 Control characters - map to blank
return {w, h, data};
}
if (index >= c_count) { // Latin Extended characters > 0xFF - not supported
return {w, h, data};
} else {
return {w, h, &data[index * data_stride]};
+4
View File
@@ -28,6 +28,10 @@
#include "ui.hpp"
// C1 Control Characters are an unprintable range of glyphs missing from the font files
#define C1_CONTROL_CHARS_START 0x80
#define C1_CONTROL_CHARS_COUNT 32
namespace ui {
class Glyph {
+96 -51
View File
@@ -349,7 +349,7 @@ Text::Text(
Rect parent_rect,
std::string text)
: Widget{parent_rect},
text{text} {
text{std::move(text)} {
}
Text::Text(
@@ -357,14 +357,14 @@ Text::Text(
: Text{parent_rect, {}} {
}
void Text::set(const std::string value) {
text = value;
void Text::set(std::string_view value) {
text = std::string{value};
set_dirty();
}
void Text::paint(Painter& painter) {
const auto rect = screen_rect();
const auto s = style();
auto s = has_focus() ? style().invert() : style();
painter.fill_rectangle(rect, s.background);
@@ -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);
@@ -1589,9 +1608,9 @@ bool OptionsField::on_touch(const TouchEvent event) {
return true;
}
/* TextField ***********************************************************/
/* TextEdit ***********************************************************/
TextField::TextField(
TextEdit::TextEdit(
std::string& str,
size_t max_length,
Point position,
@@ -1605,24 +1624,24 @@ TextField::TextField(
set_focusable(true);
}
const std::string& TextField::value() const {
const std::string& TextEdit::value() const {
return text_;
}
void TextField::set_cursor(uint32_t pos) {
void TextEdit::set_cursor(uint32_t pos) {
cursor_pos_ = std::min<size_t>(pos, text_.length());
set_dirty();
}
void TextField::set_insert_mode() {
void TextEdit::set_insert_mode() {
insert_mode_ = true;
}
void TextField::set_overwrite_mode() {
void TextEdit::set_overwrite_mode() {
insert_mode_ = false;
}
void TextField::char_add(char c) {
void TextEdit::char_add(char c) {
// Don't add if inserting and at max_length and
// don't overwrite if past the end of the text.
if ((text_.length() >= max_length_ && insert_mode_) ||
@@ -1638,7 +1657,7 @@ void TextField::char_add(char c) {
set_dirty();
}
void TextField::char_delete() {
void TextEdit::char_delete() {
if (cursor_pos_ == 0)
return;
@@ -1647,7 +1666,7 @@ void TextField::char_delete() {
set_dirty();
}
void TextField::paint(Painter& painter) {
void TextEdit::paint(Painter& painter) {
constexpr int char_width = 8;
auto rect = screen_rect();
@@ -1683,7 +1702,7 @@ void TextField::paint(Painter& painter) {
painter.draw_rectangle(cursor_box, cursor_style.background);
}
bool TextField::on_key(const KeyEvent key) {
bool TextEdit::on_key(const KeyEvent key) {
if (key == KeyEvent::Left && cursor_pos_ > 0)
cursor_pos_--;
else if (key == KeyEvent::Right && cursor_pos_ < text_.length())
@@ -1697,7 +1716,7 @@ bool TextField::on_key(const KeyEvent key) {
return true;
}
bool TextField::on_encoder(const EncoderEvent delta) {
bool TextEdit::on_encoder(const EncoderEvent delta) {
int32_t new_pos = cursor_pos_ + delta;
// Let the encoder wrap around the ends of the text.
@@ -1710,7 +1729,7 @@ bool TextField::on_encoder(const EncoderEvent delta) {
return true;
}
bool TextField::on_touch(const TouchEvent event) {
bool TextEdit::on_touch(const TouchEvent event) {
if (event.type == TouchEvent::Type::Start)
focus();
@@ -1718,6 +1737,32 @@ bool TextField::on_touch(const TouchEvent event) {
return true;
}
/* TextField *************************************************************/
TextField::TextField(Rect parent_rect, std::string text)
: Text(parent_rect, std::move(text)) {
set_focusable(true);
}
const std::string& TextField::get_text() const {
return text;
}
void TextField::set_text(std::string_view value) {
set(value);
if (on_change)
on_change(*this);
}
bool TextField::on_key(KeyEvent key) {
if (key == KeyEvent::Select && on_select) {
on_select(*this);
return true;
}
return false;
}
/* NumberField ***********************************************************/
NumberField::NumberField(
+45 -23
View File
@@ -33,10 +33,11 @@
#include "portapack.hpp"
#include "utility.hpp"
#include <memory>
#include <vector>
#include <string>
#include <functional>
#include <memory>
#include <string>
#include <string_view>
#include <vector>
namespace ui {
@@ -202,11 +203,11 @@ class Text : public Widget {
Text(Rect parent_rect, std::string text);
Text(Rect parent_rect);
void set(const std::string value);
void set(std::string_view value);
void paint(Painter& painter) override;
private:
protected:
std::string text;
};
@@ -455,7 +456,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 +464,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 +479,7 @@ class NewButton : public Widget {
std::string text_;
const Bitmap* bitmap_;
Color color_;
bool vertical_center_{false};
};
class Image : public Widget {
@@ -575,10 +578,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 +618,15 @@ 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);
options_t& options() { return 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,19 +639,19 @@ 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};
};
// A TextField is bound to a string reference and allows the string
// A TextEdit is bound to a string reference and allows the string
// to be manipulated. The field itself does not provide the UI for
// setting the value. It provides the UI of rendering the text,
// a cursor, and an API to edit the string content.
class TextField : public Widget {
class TextEdit : public Widget {
public:
TextField(std::string& str, Point position, uint32_t length = 30)
: TextField{str, 64, position, length} {}
TextEdit(std::string& str, Point position, uint32_t length = 30)
: TextEdit{str, 64, position, length} {}
// Str: the string containing the content to edit.
// Max_length: max length the string is allowed to use.
@@ -654,12 +660,12 @@ class TextField : public Widget {
// - Characters are 8 pixels wide.
// - The screen can show 30 characters max.
// - The control is 16 pixels tall.
TextField(std::string& str, size_t max_length, Point position, uint32_t length = 30);
TextEdit(std::string& str, size_t max_length, Point position, uint32_t length = 30);
TextField(const TextField&) = delete;
TextField(TextField&&) = delete;
TextField& operator=(const TextField&) = delete;
TextField& operator=(TextField&&) = delete;
TextEdit(const TextEdit&) = delete;
TextEdit(TextEdit&&) = delete;
TextEdit& operator=(const TextEdit&) = delete;
TextEdit& operator=(TextEdit&&) = delete;
const std::string& value() const;
@@ -684,6 +690,22 @@ class TextField : public Widget {
bool insert_mode_;
};
class TextField : public Text {
public:
std::function<void(TextField&)> on_select{};
std::function<void(TextField&)> on_change{};
TextField(Rect parent_rect, std::string text);
const std::string& get_text() const;
void set_text(std::string_view value);
bool on_key(KeyEvent key) override;
private:
using Text::set;
};
class NumberField : public Widget {
public:
std::function<void(NumberField&)> on_select{};
Binary file not shown.

After

Width:  |  Height:  |  Size: 234 B

+10
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
@@ -62,6 +71,7 @@ target_include_directories(application_test PRIVATE
)
target_compile_options(application_test PRIVATE
-std=c++17
-DLPC43XX
-DLPC43XX_M0
-D__NEWLIB__
@@ -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,81 @@
/*
* 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("to_string_short_freq returns correct value.") {
CHECK_EQ(to_string_short_freq(0), " 0.0000");
CHECK_EQ(to_string_short_freq(49), " 0.0000");
CHECK_EQ(to_string_short_freq(50), " 0.0001");
CHECK_EQ(to_string_short_freq(1'000'049), " 1.0000");
CHECK_EQ(to_string_short_freq(1'000'050), " 1.0001");
CHECK_EQ(to_string_short_freq(1'234'567'850), "1234.5679");
CHECK_EQ(to_string_short_freq(1'234'567'849), "1234.5678");
}
TEST_CASE("to_string_rounded_freq returns correct value.") {
CHECK_EQ(to_string_rounded_freq(0, 0), "0");
CHECK_EQ(to_string_rounded_freq(0, 2), "0.00");
CHECK_EQ(to_string_rounded_freq(49, 4), "0.0000");
CHECK_EQ(to_string_rounded_freq(50, 4), "0.0001");
CHECK_EQ(to_string_rounded_freq(23'456, 4), "0.0235");
CHECK_EQ(to_string_rounded_freq(1'000'000, 4), "1.0000");
CHECK_EQ(to_string_rounded_freq(1'000'567'849, 4), "1000.5678");
CHECK_EQ(to_string_rounded_freq(1'234'567'891, 0), "1234");
CHECK_EQ(to_string_rounded_freq(1'234'567'891, 1), "1234.6");
CHECK_EQ(to_string_rounded_freq(1'234'567'891, 2), "1234.57");
CHECK_EQ(to_string_rounded_freq(1'234'567'891, 3), "1234.568");
CHECK_EQ(to_string_rounded_freq(1'234'567'891, 4), "1234.5679");
CHECK_EQ(to_string_rounded_freq(1'234'567'891, 5), "1234.56789");
CHECK_EQ(to_string_rounded_freq(1'234'567'891, 6), "1234.567891");
CHECK_EQ(to_string_rounded_freq(1'234'567'891, 9), "1234.567891");
}
TEST_CASE("trim removes whitespace.") {
CHECK(trim(" foo\n") == "foo");
}
TEST_CASE("trim returns empty for only whitespace.") {
CHECK(trim(" \n").empty());
}
TEST_CASE("trim empty returns empty.") {
CHECK(trim("").empty());
}
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -0,0 +1 @@
These fonts are for free and can be use for commercial and non-commercial purposes. These aren't professionally made--only beta test. Let me know if there are problems with spaces and kernings.
+93
View File
@@ -0,0 +1,93 @@
Copyright 2017 The Sedgwick Ave Project Authors (https://github.com/googlefonts/sedgwickave)
This Font Software is licensed under the SIL Open Font License, Version 1.1.
This license is copied below, and is also available with a FAQ at:
http://scripts.sil.org/OFL
-----------------------------------------------------------
SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007
-----------------------------------------------------------
PREAMBLE
The goals of the Open Font License (OFL) are to stimulate worldwide
development of collaborative font projects, to support the font creation
efforts of academic and linguistic communities, and to provide a free and
open framework in which fonts may be shared and improved in partnership
with others.
The OFL allows the licensed fonts to be used, studied, modified and
redistributed freely as long as they are not sold by themselves. The
fonts, including any derivative works, can be bundled, embedded,
redistributed and/or sold with any software provided that any reserved
names are not used by derivative works. The fonts and derivatives,
however, cannot be released under any other type of license. The
requirement for fonts to remain under this license does not apply
to any document created using the fonts or their derivatives.
DEFINITIONS
"Font Software" refers to the set of files released by the Copyright
Holder(s) under this license and clearly marked as such. This may
include source files, build scripts and documentation.
"Reserved Font Name" refers to any names specified as such after the
copyright statement(s).
"Original Version" refers to the collection of Font Software components as
distributed by the Copyright Holder(s).
"Modified Version" refers to any derivative made by adding to, deleting,
or substituting -- in part or in whole -- any of the components of the
Original Version, by changing formats or by porting the Font Software to a
new environment.
"Author" refers to any designer, engineer, programmer, technical
writer or other person who contributed to the Font Software.
PERMISSION & CONDITIONS
Permission is hereby granted, free of charge, to any person obtaining
a copy of the Font Software, to use, study, copy, merge, embed, modify,
redistribute, and sell modified and unmodified copies of the Font
Software, subject to the following conditions:
1) Neither the Font Software nor any of its individual components,
in Original or Modified Versions, may be sold by itself.
2) Original or Modified Versions of the Font Software may be bundled,
redistributed and/or sold with any software, provided that each copy
contains the above copyright notice and this license. These can be
included either as stand-alone text files, human-readable headers or
in the appropriate machine-readable metadata fields within text or
binary files as long as those fields can be easily viewed by the user.
3) No Modified Version of the Font Software may use the Reserved Font
Name(s) unless explicit written permission is granted by the corresponding
Copyright Holder. This restriction only applies to the primary font name as
presented to the users.
4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font
Software shall not be used to promote, endorse or advertise any
Modified Version, except to acknowledge the contribution(s) of the
Copyright Holder(s) and the Author(s) or with their explicit written
permission.
5) The Font Software, modified or unmodified, in part or in whole,
must be distributed entirely under this license, and must not be
distributed under any other license. The requirement for fonts to
remain under this license does not apply to any document created
using the Font Software.
TERMINATION
This license becomes null and void if any of the above conditions are
not met.
DISCLAIMER
THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT
OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE
COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL
DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM
OTHER DEALINGS IN THE FONT SOFTWARE.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -0,0 +1 @@
,dannybernecker,dannys-mbp.speedport_w_724v_typ_a_05011603_00_007,16.12.2015 04:43,file:///Users/dannybernecker/Library/Application%20Support/LibreOffice/4;
Binary file not shown.

After

Width:  |  Height:  |  Size: 62 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 119 KiB

@@ -0,0 +1,94 @@
Copyright (c) <2006>, <Punkto Design> (<http://charaktertypen.com | info@charaktertypen.com>),
with Reserved Font Name <„Beatstreet“>.
This Font Software is licensed under the SIL Open Font License, Version 1.1.
This license is copied below, and is also available with a FAQ at:
http://scripts.sil.org/OFL
-----------------------------------------------------------
SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007
-----------------------------------------------------------
PREAMBLE
The goals of the Open Font License (OFL) are to stimulate worldwide
development of collaborative font projects, to support the font creation
efforts of academic and linguistic communities, and to provide a free and
open framework in which fonts may be shared and improved in partnership
with others.
The OFL allows the licensed fonts to be used, studied, modified and
redistributed freely as long as they are not sold by themselves. The
fonts, including any derivative works, can be bundled, embedded,
redistributed and/or sold with any software provided that any reserved
names are not used by derivative works. The fonts and derivatives,
however, cannot be released under any other type of license. The
requirement for fonts to remain under this license does not apply
to any document created using the fonts or their derivatives.
DEFINITIONS
"Font Software" refers to the set of files released by the Copyright
Holder(s) under this license and clearly marked as such. This may
include source files, build scripts and documentation.
"Reserved Font Name" refers to any names specified as such after the
copyright statement(s).
"Original Version" refers to the collection of Font Software components as
distributed by the Copyright Holder(s).
"Modified Version" refers to any derivative made by adding to, deleting,
or substituting -- in part or in whole -- any of the components of the
Original Version, by changing formats or by porting the Font Software to a
new environment.
"Author" refers to any designer, engineer, programmer, technical
writer or other person who contributed to the Font Software.
PERMISSION & CONDITIONS
Permission is hereby granted, free of charge, to any person obtaining
a copy of the Font Software, to use, study, copy, merge, embed, modify,
redistribute, and sell modified and unmodified copies of the Font
Software, subject to the following conditions:
1) Neither the Font Software nor any of its individual components,
in Original or Modified Versions, may be sold by itself.
2) Original or Modified Versions of the Font Software may be bundled,
redistributed and/or sold with any software, provided that each copy
contains the above copyright notice and this license. These can be
included either as stand-alone text files, human-readable headers or
in the appropriate machine-readable metadata fields within text or
binary files as long as those fields can be easily viewed by the user.
3) No Modified Version of the Font Software may use the Reserved Font
Name(s) unless explicit written permission is granted by the corresponding
Copyright Holder. This restriction only applies to the primary font name as
presented to the users.
4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font
Software shall not be used to promote, endorse or advertise any
Modified Version, except to acknowledge the contribution(s) of the
Copyright Holder(s) and the Author(s) or with their explicit written
permission.
5) The Font Software, modified or unmodified, in part or in whole,
must be distributed entirely under this license, and must not be
distributed under any other license. The requirement for fonts to
remain under this license does not apply to any document created
using the Font Software.
TERMINATION
This license becomes null and void if any of the above conditions are
not met.
DISCLAIMER
THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT
OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE
COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL
DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM
OTHER DEALINGS IN THE FONT SOFTWARE.
Binary file not shown.
Binary file not shown.
+93
View File
@@ -0,0 +1,93 @@
Copyright (c) <2006>, <Punkto Design, D. Bernecker> (<http://charaktertypen.com | info@charaktertypen.com>), with Reserved Font Name <„Tagster“>.
This Font Software is licensed under the SIL Open Font License, Version 1.1.
This license is copied below, and is also available with a FAQ at:
http://scripts.sil.org/OFL
-----------------------------------------------------------
SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007
-----------------------------------------------------------
PREAMBLE
The goals of the Open Font License (OFL) are to stimulate worldwide
development of collaborative font projects, to support the font creation
efforts of academic and linguistic communities, and to provide a free and
open framework in which fonts may be shared and improved in partnership
with others.
The OFL allows the licensed fonts to be used, studied, modified and
redistributed freely as long as they are not sold by themselves. The
fonts, including any derivative works, can be bundled, embedded,
redistributed and/or sold with any software provided that any reserved
names are not used by derivative works. The fonts and derivatives,
however, cannot be released under any other type of license. The
requirement for fonts to remain under this license does not apply
to any document created using the fonts or their derivatives.
DEFINITIONS
"Font Software" refers to the set of files released by the Copyright
Holder(s) under this license and clearly marked as such. This may
include source files, build scripts and documentation.
"Reserved Font Name" refers to any names specified as such after the
copyright statement(s).
"Original Version" refers to the collection of Font Software components as
distributed by the Copyright Holder(s).
"Modified Version" refers to any derivative made by adding to, deleting,
or substituting -- in part or in whole -- any of the components of the
Original Version, by changing formats or by porting the Font Software to a
new environment.
"Author" refers to any designer, engineer, programmer, technical
writer or other person who contributed to the Font Software.
PERMISSION & CONDITIONS
Permission is hereby granted, free of charge, to any person obtaining
a copy of the Font Software, to use, study, copy, merge, embed, modify,
redistribute, and sell modified and unmodified copies of the Font
Software, subject to the following conditions:
1) Neither the Font Software nor any of its individual components,
in Original or Modified Versions, may be sold by itself.
2) Original or Modified Versions of the Font Software may be bundled,
redistributed and/or sold with any software, provided that each copy
contains the above copyright notice and this license. These can be
included either as stand-alone text files, human-readable headers or
in the appropriate machine-readable metadata fields within text or
binary files as long as those fields can be easily viewed by the user.
3) No Modified Version of the Font Software may use the Reserved Font
Name(s) unless explicit written permission is granted by the corresponding
Copyright Holder. This restriction only applies to the primary font name as
presented to the users.
4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font
Software shall not be used to promote, endorse or advertise any
Modified Version, except to acknowledge the contribution(s) of the
Copyright Holder(s) and the Author(s) or with their explicit written
permission.
5) The Font Software, modified or unmodified, in part or in whole,
must be distributed entirely under this license, and must not be
distributed under any other license. The requirement for fonts to
remain under this license does not apply to any document created
using the Font Software.
TERMINATION
This license becomes null and void if any of the above conditions are
not met.
DISCLAIMER
THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT
OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE
COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL
DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM
OTHER DEALINGS IN THE FONT SOFTWARE.
+143
View File
@@ -0,0 +1,143 @@
<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.0 Transitional//EN">
<HTML>
<HEAD>
<META HTTP-EQUIV="CONTENT-TYPE" CONTENT="text/html; charset=shift_jis">
<TITLE>LARABIE FONTS &ldquo;README</TITLE>
<META NAME="GENERATOR" CONTENT="StarOffice 8 (Win32)">
<META NAME="AUTHOR" CONTENT="Rina Larabie">
<META NAME="CREATED" CONTENT="20061130;11580000">
<META NAME="CHANGEDBY" CONTENT="Rina Larabie">
<META NAME="CHANGED" CONTENT="20061207;15120000">
<STYLE>
<!--
@page { size: 8.5in 11in; margin-right: 1.25in; margin-top: 1in; margin-bottom: 1in }
P { margin-bottom: 0.08in; direction: ltr; color: #000000; widows: 2; orphans: 2 }
P.western { font-family: "Times New Roman", serif; font-size: 12pt; so-language: en-US }
P.cjk { font-family: "Times New Roman", serif; font-size: 12pt }
P.ctl { font-family: "Times New Roman", serif; font-size: 12pt; so-language: ar-SA }
H3 { margin-top: 0.19in; margin-bottom: 0.19in; direction: ltr; color: #000000; widows: 2; orphans: 2; page-break-after: auto }
H3.western { font-size: 13pt; so-language: en-US }
H3.cjk { font-family: "Times New Roman", serif; font-size: 13pt }
H3.ctl { font-family: "Times New Roman", serif; font-size: 13pt; so-language: ar-SA }
A:link { color: #0000ff }
-->
</STYLE>
</HEAD>
<BODY LANG="en-US" TEXT="#000000" LINK="#0000ff" DIR="LTR">
<H3 CLASS="western" STYLE="margin-top: 0in"><FONT FACE="Tahoma, sans-serif">LARABIE
FONTS FREEWARE FONTS EULA (End User License Agreement) and SOFTWARE
INCLUSION AGREEMENT</FONT></H3>
<P CLASS="western" STYLE="margin-bottom: 0in"><B><FONT FACE="Tahoma, sans-serif">LARABIE
FONTS FREEWARE FONTS EULA</FONT></B></P>
<P CLASS="western" STYLE="margin-bottom: 0in"><BR>
</P>
<P CLASS="western" STYLE="margin-bottom: 0in"><FONT SIZE=2><FONT FACE="Tahoma, sans-serif">Larabie
Fonts Freeware Fonts in TrueType format are free to use for personal
and commercial purposes. No payment is necessary to use Larabie Fonts
Freeware Fonts for personal or commercial use. If you wish to include
Larabie Fonts Freeware Fonts in software see SOFTWARE INCLUSION
AGREEMENT below. </FONT></FONT>
</P>
<P CLASS="western" STYLE="margin-bottom: 0in"><BR>
</P>
<P CLASS="western" STYLE="margin-bottom: 0in"><FONT FACE="Tahoma, sans-serif"><FONT SIZE=2><B>USAGE</B></FONT></FONT></P>
<P CLASS="western" STYLE="margin-bottom: 0in"><FONT SIZE=2><FONT FACE="Tahoma, sans-serif">You
can offer individual Larabie Fonts Freeware Fonts for download on a
website but do not combine fonts into a single archive or alter them
in any way. We appreciate inclusion of the font name and trademark or
site URL in the credits or documentation but it is not mandatory. </FONT></FONT>
</P>
<P CLASS="western" STYLE="margin-bottom: 0in"><BR>
</P>
<P CLASS="western" STYLE="margin-bottom: 0in"><FONT SIZE=2><FONT FACE="Tahoma, sans-serif">Some
Larabie Fonts Freeware Fonts may have enhanced and/or expanded
families available for sale at </FONT></FONT><FONT COLOR="#0000ff"><U><A HREF="http://www.typodermic.com/"><FONT SIZE=2><FONT FACE="Tahoma, sans-serif">www.typodermic.com</FONT></FONT></A></U></FONT><FONT SIZE=2><FONT FACE="Tahoma, sans-serif">.</FONT></FONT></P>
<P CLASS="western" STYLE="margin-bottom: 0in"><BR>
</P>
<P CLASS="western" STYLE="margin-bottom: 0in"><FONT FACE="Tahoma, sans-serif"><FONT SIZE=2><B>PAYMENT</B></FONT></FONT></P>
<P CLASS="western" STYLE="margin-bottom: 0in"><FONT SIZE=2><FONT FACE="Tahoma, sans-serif">Payment
is not required for the use of Larabie Fonts Freeware Fonts unless
they are intended to be <I>included</I> with software. More details
follow.</FONT></FONT></P>
<P CLASS="western" STYLE="margin-bottom: 0in"><FONT SIZE=2><FONT FACE="Tahoma, sans-serif">Donations
to Larabie Fonts are welcome via check or electronic payment via
Paypal. We can accept any amount in US dollars, Canadian dollars, GBP
or euros. If you require an invoice for your donation, please refer
to </FONT></FONT><FONT COLOR="#0000ff"><U><A HREF="http://www.larabiefonts.com/donation.html"><FONT SIZE=2><FONT FACE="Tahoma, sans-serif">www.larabiefonts.com/donation.html</FONT></FONT></A></U></FONT><FONT SIZE=2><FONT FACE="Tahoma, sans-serif">
for instructions. </FONT></FONT>
</P>
<P CLASS="western" STYLE="margin-bottom: 0in"><FONT SIZE=2><FONT FACE="Tahoma, sans-serif">Donations
can include CDs, magazines, t-shirts, a sample of your merchandise or
anything featuring Larabie Fonts. Mailing information is available at
the link above.</FONT></FONT></P>
<P CLASS="western" STYLE="margin-bottom: 0in"><BR>
</P>
<P CLASS="western" STYLE="margin-bottom: 0in"><FONT FACE="Tahoma, sans-serif"><FONT SIZE=2><B>SUPPORT</B></FONT></FONT></P>
<P CLASS="western" STYLE="margin-bottom: 0in"><FONT SIZE=2><FONT FACE="Tahoma, sans-serif">Font
installation help is available at </FONT></FONT><FONT COLOR="#0000ff"><U><A HREF="http://www.myfonts.com/support"><FONT SIZE=2><FONT FACE="Tahoma, sans-serif">www.myfonts.com/support</FONT></FONT></A></U></FONT><FONT SIZE=2><FONT FACE="Tahoma, sans-serif">.
If you experience problems with any Larabie Font, please visit
</FONT></FONT><FONT COLOR="#0000ff"><U><A HREF="http://www.larabiefonts.com/"><FONT SIZE=2><FONT FACE="Tahoma, sans-serif">www.larabiefonts.com</FONT></FONT></A></U></FONT><FONT SIZE=2><FONT FACE="Tahoma, sans-serif">
to verify you have the latest version. If you download Larabie Fonts
Freeware Fonts from other websites you may get older versions that
have spacing issues, incomplete character sets or technical problems.
</FONT></FONT>
</P>
<P CLASS="western" STYLE="margin-bottom: 0in"><BR>
</P>
<P CLASS="western" STYLE="margin-bottom: 0in"><B><FONT FACE="Tahoma, sans-serif">SOFTWARE
INCLUSION AGREEMENT </FONT></B>
</P>
<P CLASS="western" STYLE="margin-bottom: 0in"><BR>
</P>
<P CLASS="western" STYLE="margin-bottom: 0in"><FONT SIZE=2><FONT FACE="Tahoma, sans-serif">The
Larabie Fonts SOFTWARE PRODUCT is protected by copyright laws and
International copyright treaties, as well as other intellectual
property laws and treaties. The SOFTWARE PRODUCT is licensed, not
sold.</FONT></FONT></P>
<P CLASS="western" STYLE="margin-bottom: 0in"><BR>
</P>
<P CLASS="western" STYLE="margin-bottom: 0in"><FONT SIZE=2><FONT FACE="Tahoma, sans-serif">1.
GRANT OF LICENSE. This document grants you the following rights:</FONT></FONT></P>
<P CLASS="western" STYLE="margin-bottom: 0in"><FONT FACE="Tahoma, sans-serif"><FONT SIZE=2>-
Installation and Use. You may install and use an unlimited number of
copies of the SOFTWARE PRODUCT. You may copy and distribute unlimited
copies of the SOFTWARE PRODUCT as you receive them, in any medium,
provided that you publish on each copy an appropriate copyright
notice. Keep intact all the notices that refer to this License and
give any other recipients of the fonts a copy of this License along
with the fonts.</FONT></FONT></P>
<P CLASS="western" STYLE="margin-bottom: 0in"><BR>
</P>
<P CLASS="western" STYLE="margin-bottom: 0in"><FONT SIZE=2><FONT FACE="Tahoma, sans-serif">2.
LIMITED WARRANTY NO WARRANTIES. Larabie Fonts expressly disclaims any
warranty for the SOFTWARE PRODUCT. The SOFTWARE PRODUCT and any
related documentation is provided &quot;as is&quot; without warranty
of any kind, either express or implied, including, without
limitation, the implied warranties or merchantability, fitness for a
particular purpose, or non-infringement. The entire risk arising out
of use or performance of the SOFTWARE PRODUCT remains with you.</FONT></FONT></P>
<P CLASS="western" STYLE="margin-bottom: 0in"><FONT SIZE=2><FONT FACE="Tahoma, sans-serif">NO
LIABILITY FOR CONSEQUENTIAL DAMAGES. In no event shall Ray Larabie or
Larabie Fonts be liable for any damages whatsoever (including,
without limitation, damages for loss of business profits, business
interruption, loss of business information, or any other pecuniary
loss) arising out of the use of or inability to use this product,
even if Larabie Fonts has been advised of the possibility of such
damages.</FONT></FONT></P>
<P CLASS="western" STYLE="margin-bottom: 0in"><BR>
</P>
<P CLASS="western" STYLE="margin-bottom: 0in"><FONT FACE="Tahoma, sans-serif"><FONT SIZE=2>3.
MISCELLANEOUS</FONT></FONT></P>
<P CLASS="western" STYLE="margin-bottom: 0in"><FONT SIZE=2><FONT FACE="Tahoma, sans-serif">Should
you have any questions concerning this document or you desire to
contact Larabie Fonts for any reason, please email
</FONT></FONT><FONT COLOR="#0000ff"><U><A HREF="http://www.larabiefonts.com/email.html"><FONT SIZE=2><FONT FACE="Tahoma, sans-serif">www.larabiefonts.com/email.html</FONT></FONT></A></U></FONT><FONT SIZE=2><FONT FACE="Tahoma, sans-serif">.</FONT></FONT></P>
<P CLASS="western" STYLE="margin-bottom: 0in"><BR>
</P>
<P CLASS="western" STYLE="margin-bottom: 0in"><FONT SIZE=2><FONT FACE="Tahoma, sans-serif">4.
GOVERNING LAW</FONT></FONT></P>
<P CLASS="western" STYLE="margin-bottom: 0in"><FONT FACE="Tahoma, sans-serif"><FONT SIZE=2>This
agreement is governed by the laws of Canada and the province of
British Columbia.</FONT></FONT></P>
</BODY>
</HTML>
Binary file not shown.
Binary file not shown.

After

Width:  |  Height:  |  Size: 2.6 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 723 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 721 KiB

+456
View File
@@ -0,0 +1,456 @@
<?xml version="1.0" encoding="utf-8"?>
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
<svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
width="2320px" height="2320px" viewBox="0 0 2320 2320" enable-background="new 0 0 2320 2320" xml:space="preserve">
<rect x="0" y="0" width="2320" height="2320" fill="rgb(255,255,255)" /><g transform="translate(160,160)"><g transform="translate(640,0) scale(0.8,0.8)"><g transform="" style="fill: rgb(0, 0, 0);">
<rect width="100" height="100"/>
</g></g><g transform="translate(800,0) scale(0.8,0.8)"><g transform="" style="fill: rgb(0, 0, 0);">
<rect width="100" height="100"/>
</g></g><g transform="translate(880,0) scale(0.8,0.8)"><g transform="" style="fill: rgb(0, 0, 0);">
<rect width="100" height="100"/>
</g></g><g transform="translate(1120,0) scale(0.8,0.8)"><g transform="" style="fill: rgb(0, 0, 0);">
<rect width="100" height="100"/>
</g></g><g transform="translate(1280,0) scale(0.8,0.8)"><g transform="" style="fill: rgb(0, 0, 0);">
<rect width="100" height="100"/>
</g></g><g transform="translate(720,80) scale(0.8,0.8)"><g transform="" style="fill: rgb(0, 0, 0);">
<rect width="100" height="100"/>
</g></g><g transform="translate(880,80) scale(0.8,0.8)"><g transform="" style="fill: rgb(0, 0, 0);">
<rect width="100" height="100"/>
</g></g><g transform="translate(1040,80) scale(0.8,0.8)"><g transform="" style="fill: rgb(0, 0, 0);">
<rect width="100" height="100"/>
</g></g><g transform="translate(1120,80) scale(0.8,0.8)"><g transform="" style="fill: rgb(0, 0, 0);">
<rect width="100" height="100"/>
</g></g><g transform="translate(1280,80) scale(0.8,0.8)"><g transform="" style="fill: rgb(0, 0, 0);">
<rect width="100" height="100"/>
</g></g><g transform="translate(640,160) scale(0.8,0.8)"><g transform="" style="fill: rgb(0, 0, 0);">
<rect width="100" height="100"/>
</g></g><g transform="translate(800,160) scale(0.8,0.8)"><g transform="" style="fill: rgb(0, 0, 0);">
<rect width="100" height="100"/>
</g></g><g transform="translate(1040,160) scale(0.8,0.8)"><g transform="" style="fill: rgb(0, 0, 0);">
<rect width="100" height="100"/>
</g></g><g transform="translate(1120,160) scale(0.8,0.8)"><g transform="" style="fill: rgb(0, 0, 0);">
<rect width="100" height="100"/>
</g></g><g transform="translate(1280,160) scale(0.8,0.8)"><g transform="" style="fill: rgb(0, 0, 0);">
<rect width="100" height="100"/>
</g></g><g transform="translate(800,240) scale(0.8,0.8)"><g transform="" style="fill: rgb(0, 0, 0);">
<rect width="100" height="100"/>
</g></g><g transform="translate(960,240) scale(0.8,0.8)"><g transform="" style="fill: rgb(0, 0, 0);">
<rect width="100" height="100"/>
</g></g><g transform="translate(1040,240) scale(0.8,0.8)"><g transform="" style="fill: rgb(0, 0, 0);">
<rect width="100" height="100"/>
</g></g><g transform="translate(1120,240) scale(0.8,0.8)"><g transform="" style="fill: rgb(0, 0, 0);">
<rect width="100" height="100"/>
</g></g><g transform="translate(1200,240) scale(0.8,0.8)"><g transform="" style="fill: rgb(0, 0, 0);">
<rect width="100" height="100"/>
</g></g><g transform="translate(720,320) scale(0.8,0.8)"><g transform="" style="fill: rgb(0, 0, 0);">
<rect width="100" height="100"/>
</g></g><g transform="translate(880,320) scale(0.8,0.8)"><g transform="" style="fill: rgb(0, 0, 0);">
<rect width="100" height="100"/>
</g></g><g transform="translate(1040,320) scale(0.8,0.8)"><g transform="" style="fill: rgb(0, 0, 0);">
<rect width="100" height="100"/>
</g></g><g transform="translate(640,400) scale(0.8,0.8)"><g transform="" style="fill: rgb(0, 0, 0);">
<rect width="100" height="100"/>
</g></g><g transform="translate(880,400) scale(0.8,0.8)"><g transform="" style="fill: rgb(0, 0, 0);">
<rect width="100" height="100"/>
</g></g><g transform="translate(960,400) scale(0.8,0.8)"><g transform="" style="fill: rgb(0, 0, 0);">
<rect width="100" height="100"/>
</g></g><g transform="translate(1040,400) scale(0.8,0.8)"><g transform="" style="fill: rgb(0, 0, 0);">
<rect width="100" height="100"/>
</g></g><g transform="translate(1280,400) scale(0.8,0.8)"><g transform="" style="fill: rgb(0, 0, 0);">
<rect width="100" height="100"/>
</g></g><g transform="translate(640,480) scale(0.8,0.8)"><g transform="" style="fill: rgb(0, 0, 0);">
<rect width="100" height="100"/>
</g></g><g transform="translate(800,480) scale(0.8,0.8)"><g transform="" style="fill: rgb(0, 0, 0);">
<rect width="100" height="100"/>
</g></g><g transform="translate(960,480) scale(0.8,0.8)"><g transform="" style="fill: rgb(0, 0, 0);">
<rect width="100" height="100"/>
</g></g><g transform="translate(1120,480) scale(0.8,0.8)"><g transform="" style="fill: rgb(0, 0, 0);">
<rect width="100" height="100"/>
</g></g><g transform="translate(1280,480) scale(0.8,0.8)"><g transform="" style="fill: rgb(0, 0, 0);">
<rect width="100" height="100"/>
</g></g><g transform="translate(960,560) scale(0.8,0.8)"><g transform="" style="fill: rgb(0, 0, 0);">
<rect width="100" height="100"/>
</g></g><g transform="translate(1200,560) scale(0.8,0.8)"><g transform="" style="fill: rgb(0, 0, 0);">
<rect width="100" height="100"/>
</g></g><g transform="translate(0,640) scale(0.8,0.8)"><g transform="" style="fill: rgb(0, 0, 0);">
<rect width="100" height="100"/>
</g></g><g transform="translate(160,640) scale(0.8,0.8)"><g transform="" style="fill: rgb(0, 0, 0);">
<rect width="100" height="100"/>
</g></g><g transform="translate(480,640) scale(0.8,0.8)"><g transform="" style="fill: rgb(0, 0, 0);">
<rect width="100" height="100"/>
</g></g><g transform="translate(560,640) scale(0.8,0.8)"><g transform="" style="fill: rgb(0, 0, 0);">
<rect width="100" height="100"/>
</g></g><g transform="translate(720,640) scale(0.8,0.8)"><g transform="" style="fill: rgb(0, 0, 0);">
<rect width="100" height="100"/>
</g></g><g transform="translate(800,640) scale(0.8,0.8)"><g transform="" style="fill: rgb(0, 0, 0);">
<rect width="100" height="100"/>
</g></g><g transform="translate(880,640) scale(0.8,0.8)"><g transform="" style="fill: rgb(0, 0, 0);">
<rect width="100" height="100"/>
</g></g><g transform="translate(1200,640) scale(0.8,0.8)"><g transform="" style="fill: rgb(0, 0, 0);">
<rect width="100" height="100"/>
</g></g><g transform="translate(1280,640) scale(0.8,0.8)"><g transform="" style="fill: rgb(0, 0, 0);">
<rect width="100" height="100"/>
</g></g><g transform="translate(1520,640) scale(0.8,0.8)"><g transform="" style="fill: rgb(0, 0, 0);">
<rect width="100" height="100"/>
</g></g><g transform="translate(1760,640) scale(0.8,0.8)"><g transform="" style="fill: rgb(0, 0, 0);">
<rect width="100" height="100"/>
</g></g><g transform="translate(1920,640) scale(0.8,0.8)"><g transform="" style="fill: rgb(0, 0, 0);">
<rect width="100" height="100"/>
</g></g><g transform="translate(0,720) scale(0.8,0.8)"><g transform="" style="fill: rgb(0, 0, 0);">
<rect width="100" height="100"/>
</g></g><g transform="translate(80,720) scale(0.8,0.8)"><g transform="" style="fill: rgb(0, 0, 0);">
<rect width="100" height="100"/>
</g></g><g transform="translate(240,720) scale(0.8,0.8)"><g transform="" style="fill: rgb(0, 0, 0);">
<rect width="100" height="100"/>
</g></g><g transform="translate(400,720) scale(0.8,0.8)"><g transform="" style="fill: rgb(0, 0, 0);">
<rect width="100" height="100"/>
</g></g><g transform="translate(560,720) scale(0.8,0.8)"><g transform="" style="fill: rgb(0, 0, 0);">
<rect width="100" height="100"/>
</g></g><g transform="translate(720,720) scale(0.8,0.8)"><g transform="" style="fill: rgb(0, 0, 0);">
<rect width="100" height="100"/>
</g></g><g transform="translate(800,720) scale(0.8,0.8)"><g transform="" style="fill: rgb(0, 0, 0);">
<rect width="100" height="100"/>
</g></g><g transform="translate(1040,720) scale(0.8,0.8)"><g transform="" style="fill: rgb(0, 0, 0);">
<rect width="100" height="100"/>
</g></g><g transform="translate(1200,720) scale(0.8,0.8)"><g transform="" style="fill: rgb(0, 0, 0);">
<rect width="100" height="100"/>
</g></g><g transform="translate(1280,720) scale(0.8,0.8)"><g transform="" style="fill: rgb(0, 0, 0);">
<rect width="100" height="100"/>
</g></g><g transform="translate(1440,720) scale(0.8,0.8)"><g transform="" style="fill: rgb(0, 0, 0);">
<rect width="100" height="100"/>
</g></g><g transform="translate(1520,720) scale(0.8,0.8)"><g transform="" style="fill: rgb(0, 0, 0);">
<rect width="100" height="100"/>
</g></g><g transform="translate(1680,720) scale(0.8,0.8)"><g transform="" style="fill: rgb(0, 0, 0);">
<rect width="100" height="100"/>
</g></g><g transform="translate(1840,720) scale(0.8,0.8)"><g transform="" style="fill: rgb(0, 0, 0);">
<rect width="100" height="100"/>
</g></g><g transform="translate(1920,720) scale(0.8,0.8)"><g transform="" style="fill: rgb(0, 0, 0);">
<rect width="100" height="100"/>
</g></g><g transform="translate(80,800) scale(0.8,0.8)"><g transform="" style="fill: rgb(0, 0, 0);">
<rect width="100" height="100"/>
</g></g><g transform="translate(160,800) scale(0.8,0.8)"><g transform="" style="fill: rgb(0, 0, 0);">
<rect width="100" height="100"/>
</g></g><g transform="translate(400,800) scale(0.8,0.8)"><g transform="" style="fill: rgb(0, 0, 0);">
<rect width="100" height="100"/>
</g></g><g transform="translate(480,800) scale(0.8,0.8)"><g transform="" style="fill: rgb(0, 0, 0);">
<rect width="100" height="100"/>
</g></g><g transform="translate(640,800) scale(0.8,0.8)"><g transform="" style="fill: rgb(0, 0, 0);">
<rect width="100" height="100"/>
</g></g><g transform="translate(720,800) scale(0.8,0.8)"><g transform="" style="fill: rgb(0, 0, 0);">
<rect width="100" height="100"/>
</g></g><g transform="translate(800,800) scale(0.8,0.8)"><g transform="" style="fill: rgb(0, 0, 0);">
<rect width="100" height="100"/>
</g></g><g transform="translate(1040,800) scale(0.8,0.8)"><g transform="" style="fill: rgb(0, 0, 0);">
<rect width="100" height="100"/>
</g></g><g transform="translate(1120,800) scale(0.8,0.8)"><g transform="" style="fill: rgb(0, 0, 0);">
<rect width="100" height="100"/>
</g></g><g transform="translate(1200,800) scale(0.8,0.8)"><g transform="" style="fill: rgb(0, 0, 0);">
<rect width="100" height="100"/>
</g></g><g transform="translate(1520,800) scale(0.8,0.8)"><g transform="" style="fill: rgb(0, 0, 0);">
<rect width="100" height="100"/>
</g></g><g transform="translate(1600,800) scale(0.8,0.8)"><g transform="" style="fill: rgb(0, 0, 0);">
<rect width="100" height="100"/>
</g></g><g transform="translate(1680,800) scale(0.8,0.8)"><g transform="" style="fill: rgb(0, 0, 0);">
<rect width="100" height="100"/>
</g></g><g transform="translate(1760,800) scale(0.8,0.8)"><g transform="" style="fill: rgb(0, 0, 0);">
<rect width="100" height="100"/>
</g></g><g transform="translate(1920,800) scale(0.8,0.8)"><g transform="" style="fill: rgb(0, 0, 0);">
<rect width="100" height="100"/>
</g></g><g transform="translate(80,880) scale(0.8,0.8)"><g transform="" style="fill: rgb(0, 0, 0);">
<rect width="100" height="100"/>
</g></g><g transform="translate(400,880) scale(0.8,0.8)"><g transform="" style="fill: rgb(0, 0, 0);">
<rect width="100" height="100"/>
</g></g><g transform="translate(560,880) scale(0.8,0.8)"><g transform="" style="fill: rgb(0, 0, 0);">
<rect width="100" height="100"/>
</g></g><g transform="translate(800,880) scale(0.8,0.8)"><g transform="" style="fill: rgb(0, 0, 0);">
<rect width="100" height="100"/>
</g></g><g transform="translate(880,880) scale(0.8,0.8)"><g transform="" style="fill: rgb(0, 0, 0);">
<rect width="100" height="100"/>
</g></g><g transform="translate(960,880) scale(0.8,0.8)"><g transform="" style="fill: rgb(0, 0, 0);">
<rect width="100" height="100"/>
</g></g><g transform="translate(1120,880) scale(0.8,0.8)"><g transform="" style="fill: rgb(0, 0, 0);">
<rect width="100" height="100"/>
</g></g><g transform="translate(1360,880) scale(0.8,0.8)"><g transform="" style="fill: rgb(0, 0, 0);">
<rect width="100" height="100"/>
</g></g><g transform="translate(1440,880) scale(0.8,0.8)"><g transform="" style="fill: rgb(0, 0, 0);">
<rect width="100" height="100"/>
</g></g><g transform="translate(1680,880) scale(0.8,0.8)"><g transform="" style="fill: rgb(0, 0, 0);">
<rect width="100" height="100"/>
</g></g><g transform="translate(0,960) scale(0.8,0.8)"><g transform="" style="fill: rgb(0, 0, 0);">
<rect width="100" height="100"/>
</g></g><g transform="translate(80,960) scale(0.8,0.8)"><g transform="" style="fill: rgb(0, 0, 0);">
<rect width="100" height="100"/>
</g></g><g transform="translate(480,960) scale(0.8,0.8)"><g transform="" style="fill: rgb(0, 0, 0);">
<rect width="100" height="100"/>
</g></g><g transform="translate(720,960) scale(0.8,0.8)"><g transform="" style="fill: rgb(0, 0, 0);">
<rect width="100" height="100"/>
</g></g><g transform="translate(800,960) scale(0.8,0.8)"><g transform="" style="fill: rgb(0, 0, 0);">
<rect width="100" height="100"/>
</g></g><g transform="translate(880,960) scale(0.8,0.8)"><g transform="" style="fill: rgb(0, 0, 0);">
<rect width="100" height="100"/>
</g></g><g transform="translate(960,960) scale(0.8,0.8)"><g transform="" style="fill: rgb(0, 0, 0);">
<rect width="100" height="100"/>
</g></g><g transform="translate(1200,960) scale(0.8,0.8)"><g transform="" style="fill: rgb(0, 0, 0);">
<rect width="100" height="100"/>
</g></g><g transform="translate(1280,960) scale(0.8,0.8)"><g transform="" style="fill: rgb(0, 0, 0);">
<rect width="100" height="100"/>
</g></g><g transform="translate(1440,960) scale(0.8,0.8)"><g transform="" style="fill: rgb(0, 0, 0);">
<rect width="100" height="100"/>
</g></g><g transform="translate(1920,960) scale(0.8,0.8)"><g transform="" style="fill: rgb(0, 0, 0);">
<rect width="100" height="100"/>
</g></g><g transform="translate(80,1040) scale(0.8,0.8)"><g transform="" style="fill: rgb(0, 0, 0);">
<rect width="100" height="100"/>
</g></g><g transform="translate(320,1040) scale(0.8,0.8)"><g transform="" style="fill: rgb(0, 0, 0);">
<rect width="100" height="100"/>
</g></g><g transform="translate(400,1040) scale(0.8,0.8)"><g transform="" style="fill: rgb(0, 0, 0);">
<rect width="100" height="100"/>
</g></g><g transform="translate(560,1040) scale(0.8,0.8)"><g transform="" style="fill: rgb(0, 0, 0);">
<rect width="100" height="100"/>
</g></g><g transform="translate(800,1040) scale(0.8,0.8)"><g transform="" style="fill: rgb(0, 0, 0);">
<rect width="100" height="100"/>
</g></g><g transform="translate(960,1040) scale(0.8,0.8)"><g transform="" style="fill: rgb(0, 0, 0);">
<rect width="100" height="100"/>
</g></g><g transform="translate(1200,1040) scale(0.8,0.8)"><g transform="" style="fill: rgb(0, 0, 0);">
<rect width="100" height="100"/>
</g></g><g transform="translate(1280,1040) scale(0.8,0.8)"><g transform="" style="fill: rgb(0, 0, 0);">
<rect width="100" height="100"/>
</g></g><g transform="translate(1360,1040) scale(0.8,0.8)"><g transform="" style="fill: rgb(0, 0, 0);">
<rect width="100" height="100"/>
</g></g><g transform="translate(1440,1040) scale(0.8,0.8)"><g transform="" style="fill: rgb(0, 0, 0);">
<rect width="100" height="100"/>
</g></g><g transform="translate(1520,1040) scale(0.8,0.8)"><g transform="" style="fill: rgb(0, 0, 0);">
<rect width="100" height="100"/>
</g></g><g transform="translate(1840,1040) scale(0.8,0.8)"><g transform="" style="fill: rgb(0, 0, 0);">
<rect width="100" height="100"/>
</g></g><g transform="translate(1920,1040) scale(0.8,0.8)"><g transform="" style="fill: rgb(0, 0, 0);">
<rect width="100" height="100"/>
</g></g><g transform="translate(0,1120) scale(0.8,0.8)"><g transform="" style="fill: rgb(0, 0, 0);">
<rect width="100" height="100"/>
</g></g><g transform="translate(80,1120) scale(0.8,0.8)"><g transform="" style="fill: rgb(0, 0, 0);">
<rect width="100" height="100"/>
</g></g><g transform="translate(160,1120) scale(0.8,0.8)"><g transform="" style="fill: rgb(0, 0, 0);">
<rect width="100" height="100"/>
</g></g><g transform="translate(240,1120) scale(0.8,0.8)"><g transform="" style="fill: rgb(0, 0, 0);">
<rect width="100" height="100"/>
</g></g><g transform="translate(400,1120) scale(0.8,0.8)"><g transform="" style="fill: rgb(0, 0, 0);">
<rect width="100" height="100"/>
</g></g><g transform="translate(480,1120) scale(0.8,0.8)"><g transform="" style="fill: rgb(0, 0, 0);">
<rect width="100" height="100"/>
</g></g><g transform="translate(560,1120) scale(0.8,0.8)"><g transform="" style="fill: rgb(0, 0, 0);">
<rect width="100" height="100"/>
</g></g><g transform="translate(640,1120) scale(0.8,0.8)"><g transform="" style="fill: rgb(0, 0, 0);">
<rect width="100" height="100"/>
</g></g><g transform="translate(720,1120) scale(0.8,0.8)"><g transform="" style="fill: rgb(0, 0, 0);">
<rect width="100" height="100"/>
</g></g><g transform="translate(800,1120) scale(0.8,0.8)"><g transform="" style="fill: rgb(0, 0, 0);">
<rect width="100" height="100"/>
</g></g><g transform="translate(960,1120) scale(0.8,0.8)"><g transform="" style="fill: rgb(0, 0, 0);">
<rect width="100" height="100"/>
</g></g><g transform="translate(1040,1120) scale(0.8,0.8)"><g transform="" style="fill: rgb(0, 0, 0);">
<rect width="100" height="100"/>
</g></g><g transform="translate(1200,1120) scale(0.8,0.8)"><g transform="" style="fill: rgb(0, 0, 0);">
<rect width="100" height="100"/>
</g></g><g transform="translate(1280,1120) scale(0.8,0.8)"><g transform="" style="fill: rgb(0, 0, 0);">
<rect width="100" height="100"/>
</g></g><g transform="translate(1360,1120) scale(0.8,0.8)"><g transform="" style="fill: rgb(0, 0, 0);">
<rect width="100" height="100"/>
</g></g><g transform="translate(1440,1120) scale(0.8,0.8)"><g transform="" style="fill: rgb(0, 0, 0);">
<rect width="100" height="100"/>
</g></g><g transform="translate(1680,1120) scale(0.8,0.8)"><g transform="" style="fill: rgb(0, 0, 0);">
<rect width="100" height="100"/>
</g></g><g transform="translate(1760,1120) scale(0.8,0.8)"><g transform="" style="fill: rgb(0, 0, 0);">
<rect width="100" height="100"/>
</g></g><g transform="translate(1920,1120) scale(0.8,0.8)"><g transform="" style="fill: rgb(0, 0, 0);">
<rect width="100" height="100"/>
</g></g><g transform="translate(160,1200) scale(0.8,0.8)"><g transform="" style="fill: rgb(0, 0, 0);">
<rect width="100" height="100"/>
</g></g><g transform="translate(560,1200) scale(0.8,0.8)"><g transform="" style="fill: rgb(0, 0, 0);">
<rect width="100" height="100"/>
</g></g><g transform="translate(880,1200) scale(0.8,0.8)"><g transform="" style="fill: rgb(0, 0, 0);">
<rect width="100" height="100"/>
</g></g><g transform="translate(960,1200) scale(0.8,0.8)"><g transform="" style="fill: rgb(0, 0, 0);">
<rect width="100" height="100"/>
</g></g><g transform="translate(1120,1200) scale(0.8,0.8)"><g transform="" style="fill: rgb(0, 0, 0);">
<rect width="100" height="100"/>
</g></g><g transform="translate(1440,1200) scale(0.8,0.8)"><g transform="" style="fill: rgb(0, 0, 0);">
<rect width="100" height="100"/>
</g></g><g transform="translate(1520,1200) scale(0.8,0.8)"><g transform="" style="fill: rgb(0, 0, 0);">
<rect width="100" height="100"/>
</g></g><g transform="translate(1600,1200) scale(0.8,0.8)"><g transform="" style="fill: rgb(0, 0, 0);">
<rect width="100" height="100"/>
</g></g><g transform="translate(1680,1200) scale(0.8,0.8)"><g transform="" style="fill: rgb(0, 0, 0);">
<rect width="100" height="100"/>
</g></g><g transform="translate(0,1280) scale(0.8,0.8)"><g transform="" style="fill: rgb(0, 0, 0);">
<rect width="100" height="100"/>
</g></g><g transform="translate(80,1280) scale(0.8,0.8)"><g transform="" style="fill: rgb(0, 0, 0);">
<rect width="100" height="100"/>
</g></g><g transform="translate(160,1280) scale(0.8,0.8)"><g transform="" style="fill: rgb(0, 0, 0);">
<rect width="100" height="100"/>
</g></g><g transform="translate(400,1280) scale(0.8,0.8)"><g transform="" style="fill: rgb(0, 0, 0);">
<rect width="100" height="100"/>
</g></g><g transform="translate(480,1280) scale(0.8,0.8)"><g transform="" style="fill: rgb(0, 0, 0);">
<rect width="100" height="100"/>
</g></g><g transform="translate(640,1280) scale(0.8,0.8)"><g transform="" style="fill: rgb(0, 0, 0);">
<rect width="100" height="100"/>
</g></g><g transform="translate(800,1280) scale(0.8,0.8)"><g transform="" style="fill: rgb(0, 0, 0);">
<rect width="100" height="100"/>
</g></g><g transform="translate(960,1280) scale(0.8,0.8)"><g transform="" style="fill: rgb(0, 0, 0);">
<rect width="100" height="100"/>
</g></g><g transform="translate(1200,1280) scale(0.8,0.8)"><g transform="" style="fill: rgb(0, 0, 0);">
<rect width="100" height="100"/>
</g></g><g transform="translate(1280,1280) scale(0.8,0.8)"><g transform="" style="fill: rgb(0, 0, 0);">
<rect width="100" height="100"/>
</g></g><g transform="translate(1360,1280) scale(0.8,0.8)"><g transform="" style="fill: rgb(0, 0, 0);">
<rect width="100" height="100"/>
</g></g><g transform="translate(1440,1280) scale(0.8,0.8)"><g transform="" style="fill: rgb(0, 0, 0);">
<rect width="100" height="100"/>
</g></g><g transform="translate(1520,1280) scale(0.8,0.8)"><g transform="" style="fill: rgb(0, 0, 0);">
<rect width="100" height="100"/>
</g></g><g transform="translate(1600,1280) scale(0.8,0.8)"><g transform="" style="fill: rgb(0, 0, 0);">
<rect width="100" height="100"/>
</g></g><g transform="translate(1840,1280) scale(0.8,0.8)"><g transform="" style="fill: rgb(0, 0, 0);">
<rect width="100" height="100"/>
</g></g><g transform="translate(640,1360) scale(0.8,0.8)"><g transform="" style="fill: rgb(0, 0, 0);">
<rect width="100" height="100"/>
</g></g><g transform="translate(720,1360) scale(0.8,0.8)"><g transform="" style="fill: rgb(0, 0, 0);">
<rect width="100" height="100"/>
</g></g><g transform="translate(1200,1360) scale(0.8,0.8)"><g transform="" style="fill: rgb(0, 0, 0);">
<rect width="100" height="100"/>
</g></g><g transform="translate(1280,1360) scale(0.8,0.8)"><g transform="" style="fill: rgb(0, 0, 0);">
<rect width="100" height="100"/>
</g></g><g transform="translate(1600,1360) scale(0.8,0.8)"><g transform="" style="fill: rgb(0, 0, 0);">
<rect width="100" height="100"/>
</g></g><g transform="translate(1920,1360) scale(0.8,0.8)"><g transform="" style="fill: rgb(0, 0, 0);">
<rect width="100" height="100"/>
</g></g><g transform="translate(640,1440) scale(0.8,0.8)"><g transform="" style="fill: rgb(0, 0, 0);">
<rect width="100" height="100"/>
</g></g><g transform="translate(880,1440) scale(0.8,0.8)"><g transform="" style="fill: rgb(0, 0, 0);">
<rect width="100" height="100"/>
</g></g><g transform="translate(960,1440) scale(0.8,0.8)"><g transform="" style="fill: rgb(0, 0, 0);">
<rect width="100" height="100"/>
</g></g><g transform="translate(1040,1440) scale(0.8,0.8)"><g transform="" style="fill: rgb(0, 0, 0);">
<rect width="100" height="100"/>
</g></g><g transform="translate(1280,1440) scale(0.8,0.8)"><g transform="" style="fill: rgb(0, 0, 0);">
<rect width="100" height="100"/>
</g></g><g transform="translate(1440,1440) scale(0.8,0.8)"><g transform="" style="fill: rgb(0, 0, 0);">
<rect width="100" height="100"/>
</g></g><g transform="translate(1600,1440) scale(0.8,0.8)"><g transform="" style="fill: rgb(0, 0, 0);">
<rect width="100" height="100"/>
</g></g><g transform="translate(1920,1440) scale(0.8,0.8)"><g transform="" style="fill: rgb(0, 0, 0);">
<rect width="100" height="100"/>
</g></g><g transform="translate(960,1520) scale(0.8,0.8)"><g transform="" style="fill: rgb(0, 0, 0);">
<rect width="100" height="100"/>
</g></g><g transform="translate(1200,1520) scale(0.8,0.8)"><g transform="" style="fill: rgb(0, 0, 0);">
<rect width="100" height="100"/>
</g></g><g transform="translate(1280,1520) scale(0.8,0.8)"><g transform="" style="fill: rgb(0, 0, 0);">
<rect width="100" height="100"/>
</g></g><g transform="translate(1600,1520) scale(0.8,0.8)"><g transform="" style="fill: rgb(0, 0, 0);">
<rect width="100" height="100"/>
</g></g><g transform="translate(1840,1520) scale(0.8,0.8)"><g transform="" style="fill: rgb(0, 0, 0);">
<rect width="100" height="100"/>
</g></g><g transform="translate(720,1600) scale(0.8,0.8)"><g transform="" style="fill: rgb(0, 0, 0);">
<rect width="100" height="100"/>
</g></g><g transform="translate(960,1600) scale(0.8,0.8)"><g transform="" style="fill: rgb(0, 0, 0);">
<rect width="100" height="100"/>
</g></g><g transform="translate(1120,1600) scale(0.8,0.8)"><g transform="" style="fill: rgb(0, 0, 0);">
<rect width="100" height="100"/>
</g></g><g transform="translate(1200,1600) scale(0.8,0.8)"><g transform="" style="fill: rgb(0, 0, 0);">
<rect width="100" height="100"/>
</g></g><g transform="translate(1280,1600) scale(0.8,0.8)"><g transform="" style="fill: rgb(0, 0, 0);">
<rect width="100" height="100"/>
</g></g><g transform="translate(1360,1600) scale(0.8,0.8)"><g transform="" style="fill: rgb(0, 0, 0);">
<rect width="100" height="100"/>
</g></g><g transform="translate(1440,1600) scale(0.8,0.8)"><g transform="" style="fill: rgb(0, 0, 0);">
<rect width="100" height="100"/>
</g></g><g transform="translate(1520,1600) scale(0.8,0.8)"><g transform="" style="fill: rgb(0, 0, 0);">
<rect width="100" height="100"/>
</g></g><g transform="translate(1600,1600) scale(0.8,0.8)"><g transform="" style="fill: rgb(0, 0, 0);">
<rect width="100" height="100"/>
</g></g><g transform="translate(1840,1600) scale(0.8,0.8)"><g transform="" style="fill: rgb(0, 0, 0);">
<rect width="100" height="100"/>
</g></g><g transform="translate(800,1680) scale(0.8,0.8)"><g transform="" style="fill: rgb(0, 0, 0);">
<rect width="100" height="100"/>
</g></g><g transform="translate(880,1680) scale(0.8,0.8)"><g transform="" style="fill: rgb(0, 0, 0);">
<rect width="100" height="100"/>
</g></g><g transform="translate(960,1680) scale(0.8,0.8)"><g transform="" style="fill: rgb(0, 0, 0);">
<rect width="100" height="100"/>
</g></g><g transform="translate(1040,1680) scale(0.8,0.8)"><g transform="" style="fill: rgb(0, 0, 0);">
<rect width="100" height="100"/>
</g></g><g transform="translate(1200,1680) scale(0.8,0.8)"><g transform="" style="fill: rgb(0, 0, 0);">
<rect width="100" height="100"/>
</g></g><g transform="translate(1360,1680) scale(0.8,0.8)"><g transform="" style="fill: rgb(0, 0, 0);">
<rect width="100" height="100"/>
</g></g><g transform="translate(1600,1680) scale(0.8,0.8)"><g transform="" style="fill: rgb(0, 0, 0);">
<rect width="100" height="100"/>
</g></g><g transform="translate(1760,1680) scale(0.8,0.8)"><g transform="" style="fill: rgb(0, 0, 0);">
<rect width="100" height="100"/>
</g></g><g transform="translate(1840,1680) scale(0.8,0.8)"><g transform="" style="fill: rgb(0, 0, 0);">
<rect width="100" height="100"/>
</g></g><g transform="translate(640,1760) scale(0.8,0.8)"><g transform="" style="fill: rgb(0, 0, 0);">
<rect width="100" height="100"/>
</g></g><g transform="translate(880,1760) scale(0.8,0.8)"><g transform="" style="fill: rgb(0, 0, 0);">
<rect width="100" height="100"/>
</g></g><g transform="translate(1200,1760) scale(0.8,0.8)"><g transform="" style="fill: rgb(0, 0, 0);">
<rect width="100" height="100"/>
</g></g><g transform="translate(1280,1760) scale(0.8,0.8)"><g transform="" style="fill: rgb(0, 0, 0);">
<rect width="100" height="100"/>
</g></g><g transform="translate(1520,1760) scale(0.8,0.8)"><g transform="" style="fill: rgb(0, 0, 0);">
<rect width="100" height="100"/>
</g></g><g transform="translate(1600,1760) scale(0.8,0.8)"><g transform="" style="fill: rgb(0, 0, 0);">
<rect width="100" height="100"/>
</g></g><g transform="translate(1680,1760) scale(0.8,0.8)"><g transform="" style="fill: rgb(0, 0, 0);">
<rect width="100" height="100"/>
</g></g><g transform="translate(1840,1760) scale(0.8,0.8)"><g transform="" style="fill: rgb(0, 0, 0);">
<rect width="100" height="100"/>
</g></g><g transform="translate(1920,1760) scale(0.8,0.8)"><g transform="" style="fill: rgb(0, 0, 0);">
<rect width="100" height="100"/>
</g></g><g transform="translate(880,1840) scale(0.8,0.8)"><g transform="" style="fill: rgb(0, 0, 0);">
<rect width="100" height="100"/>
</g></g><g transform="translate(1280,1840) scale(0.8,0.8)"><g transform="" style="fill: rgb(0, 0, 0);">
<rect width="100" height="100"/>
</g></g><g transform="translate(1360,1840) scale(0.8,0.8)"><g transform="" style="fill: rgb(0, 0, 0);">
<rect width="100" height="100"/>
</g></g><g transform="translate(1520,1840) scale(0.8,0.8)"><g transform="" style="fill: rgb(0, 0, 0);">
<rect width="100" height="100"/>
</g></g><g transform="translate(1600,1840) scale(0.8,0.8)"><g transform="" style="fill: rgb(0, 0, 0);">
<rect width="100" height="100"/>
</g></g><g transform="translate(640,1920) scale(0.8,0.8)"><g transform="" style="fill: rgb(0, 0, 0);">
<rect width="100" height="100"/>
</g></g><g transform="translate(720,1920) scale(0.8,0.8)"><g transform="" style="fill: rgb(0, 0, 0);">
<rect width="100" height="100"/>
</g></g><g transform="translate(880,1920) scale(0.8,0.8)"><g transform="" style="fill: rgb(0, 0, 0);">
<rect width="100" height="100"/>
</g></g><g transform="translate(960,1920) scale(0.8,0.8)"><g transform="" style="fill: rgb(0, 0, 0);">
<rect width="100" height="100"/>
</g></g><g transform="translate(1280,1920) scale(0.8,0.8)"><g transform="" style="fill: rgb(0, 0, 0);">
<rect width="100" height="100"/>
</g></g><g transform="translate(1360,1920) scale(0.8,0.8)"><g transform="" style="fill: rgb(0, 0, 0);">
<rect width="100" height="100"/>
</g></g><g transform="translate(1440,1920) scale(0.8,0.8)"><g transform="" style="fill: rgb(0, 0, 0);">
<rect width="100" height="100"/>
</g></g><g transform="translate(1680,1920) scale(0.8,0.8)"><g transform="" style="fill: rgb(0, 0, 0);">
<rect width="100" height="100"/>
</g></g><g transform="translate(1920,1920) scale(0.8,0.8)"><g transform="" style="fill: rgb(0, 0, 0);">
<rect width="100" height="100"/>
</g></g><g transform="translate(0,0) scale(5.6, 5.6)"><g transform="" style="fill: rgb(0, 0, 0);">
<g>
<rect x="15" y="15" style="fill:none;" width="70" height="70"/>
<path d="M85,0H15H0v15v70v15h15h70h15V85V15V0H85z M85,85H15V15h70V85z"/>
</g>
</g></g><g transform="translate(1440,0) scale(5.6, 5.6)"><g transform="" style="fill: rgb(0, 0, 0);">
<g>
<rect x="15" y="15" style="fill:none;" width="70" height="70"/>
<path d="M85,0H15H0v15v70v15h15h70h15V85V15V0H85z M85,85H15V15h70V85z"/>
</g>
</g></g><g transform="translate(0,1440) scale(5.6, 5.6)"><g transform="" style="fill: rgb(0, 0, 0);">
<g>
<rect x="15" y="15" style="fill:none;" width="70" height="70"/>
<path d="M85,0H15H0v15v70v15h15h70h15V85V15V0H85z M85,85H15V15h70V85z"/>
</g>
</g></g><g transform="translate(160,160) scale(2.4, 2.4)"><g transform="" style="fill: rgb(0, 0, 0);">
<rect width="100" height="100"/>
</g></g><g transform="translate(1600,160) scale(2.4, 2.4)"><g transform="" style="fill: rgb(0, 0, 0);">
<rect width="100" height="100"/>
</g></g><g transform="translate(160,1600) scale(2.4, 2.4)"><g transform="" style="fill: rgb(0, 0, 0);">
<rect width="100" height="100"/>
</g></g></g></svg>

After

Width:  |  Height:  |  Size: 30 KiB