Compare commits

..

17 Commits

Author SHA1 Message Date
Totoo b3edaf3cd0 AIS name fix (#2793)
* AIS name fix
2025-09-17 22:40:46 +02:00
Totoo 9afd3a9dca Standalone imp (#2791)
* Moved ui elements to common folder in standalone, and add screen parameters to the api v4

* pass it as a pointer, so if standalone app supports it can change
2025-09-16 20:29:05 +02:00
Totoo d80f41e9d7 Beep on search find something.
Beep on search find something. Solves #2643
2025-09-14 07:22:48 +00:00
Totoo 46d87d95ae Set MIC app's freq step a bit lower to 12.5k. Solves #2728 (#2788) 2025-09-13 12:38:29 +02:00
Totoo 7626dff1ef Add blue bar (Channel) to sonde app. 2025-09-13 12:37:34 +02:00
Totoo f101680521 fixes osm marker display bug #2783 (#2786) 2025-09-13 12:37:01 +02:00
Totoo 325f295ee8 Log function for search app (#2782) 2025-09-07 22:44:04 +08:00
Totoo b15bb59678 Standalone app api v3 (#2772)
Added file io, and updated some ui elements.
Also added Digital Rain standalone app for an example.
2025-09-01 11:50:46 +02:00
Totoo 776c9bc7c9 Fix jammer ranges, clean code (#2776) 2025-08-30 22:22:14 +02:00
Totoo b9ebefc7c7 AIS map speed fix (#2777) 2025-08-30 15:01:29 +02:00
Netro 70ace4be8f BLE RX Lag Improvements (#2770)
* Better handling on filtering to improve lag.
2025-08-27 15:46:53 +02:00
Totoo fe826d5d98 Fix bmp16bit with nocomp header (#2769) 2025-08-27 19:07:08 +08:00
Totoo 4dda7cfaa5 Openstreetmap support (#2765) 2025-08-27 06:37:57 +00:00
zxkmm e6d4081c06 Add game 2048 (#2767)
* _

* rename my name

* fix warnings

* fix warnings
2025-08-26 07:26:53 -07:00
zxkmm 4f738adc70 rename my names in comments (#2768)
* rename my name

* remove wrong line
2025-08-26 19:56:39 +08:00
Totoo d516191ccb Update external.cmake (#2764)
Disable acars ext app, since not working
2025-08-22 10:44:55 +02:00
gullradriel 29bdc539a3 Fix BLE TX warning, regenerate bitmap.hpp (#2762)
* fix 'progress' variable warning, added a TODO
* up to date bitmap.hpp file, soundboard icon
2025-08-19 07:19:46 +02:00
137 changed files with 15799 additions and 360 deletions
+12 -9
View File
@@ -223,9 +223,9 @@ void AISRecentEntry::update(const ais::Packet& packet) {
break;
case 5:
call_sign = packet.text(70, 7);
name = packet.text(112, 20);
destination = packet.text(302, 20);
call_sign = trim(packet.text(70, 7));
name = trim(packet.text(112, 20));
destination = trim(packet.text(302, 20));
break;
case 18:
@@ -238,7 +238,7 @@ void AISRecentEntry::update(const ais::Packet& packet) {
break;
case 21:
name = packet.text(43, 20);
name = trim(packet.text(43, 20));
last_position.timestamp = packet.received_at();
last_position.latitude = packet.latitude(192);
last_position.longitude = packet.longitude(164);
@@ -247,11 +247,11 @@ void AISRecentEntry::update(const ais::Packet& packet) {
case 24:
switch (packet.read(38, 2)) {
case 0:
name = packet.text(40, 20);
name = trim(packet.text(40, 20));
break;
case 1:
call_sign = packet.text(90, 7);
call_sign = trim(packet.text(90, 7));
break;
default:
@@ -324,7 +324,7 @@ AISRecentEntryDetailView& AISRecentEntryDetailView::operator=(const AISRecentEnt
void AISRecentEntryDetailView::update_position() {
if (send_updates)
geomap_view->update_position(ais::format::latlon_float(entry_.last_position.latitude.normalized()), ais::format::latlon_float(entry_.last_position.longitude.normalized()), (float)entry_.last_position.true_heading, 0, entry_.last_position.speed_over_ground > 1022 ? 0 : entry_.last_position.speed_over_ground);
geomap_view->update_position(ais::format::latlon_float(entry_.last_position.latitude.normalized()), ais::format::latlon_float(entry_.last_position.longitude.normalized()), (float)entry_.last_position.true_heading, 0, entry_.last_position.speed_over_ground > 1022 ? 0 : entry_.last_position.speed_over_ground / 10);
}
bool AISRecentEntryDetailView::add_map_marker(const AISRecentEntry& entry) {
@@ -456,7 +456,10 @@ void AISAppView::on_tick_second() {
++timer_seconds;
if (timer_seconds % 10 == 0) {
if (recent_entry_detail_view.hidden()) return;
recent_entry_detail_view.update_map_markers(recent);
if (got_new_packet) {
got_new_packet = false;
recent_entry_detail_view.update_map_markers(recent);
}
}
}
@@ -475,7 +478,7 @@ void AISAppView::on_packet(const ais::Packet& packet) {
if (logger) {
logger->on_packet(packet);
}
got_new_packet = true;
auto& entry = ::on_packet(recent, packet.source_id());
entry.update(packet);
recent_entries_view.set_dirty();
+1
View File
@@ -221,6 +221,7 @@ class AISAppView : public View {
};
SignalToken signal_token_tick_second{};
uint8_t timer_seconds = 0;
bool got_new_packet{false}; // got any new packet since latest screen update?
MessageHandlerRegistration message_handler_packet{
Message::ID::AISPacket,
+49 -74
View File
@@ -36,6 +36,8 @@
#include "ui_textentry.hpp"
#include "usb_serial_asyncmsg.hpp"
#include <string_view>
using namespace portapack;
using namespace modems;
namespace fs = std::filesystem;
@@ -239,8 +241,7 @@ BleRecentEntryDetailView::BleRecentEntryDetailView(NavigationView& nav, const Bl
text_mac_address.set(to_string_mac_address(entry.packetData.macAddress, 6, false));
text_pdu_type.set(pdu_type_to_string(entry.pduType));
std::string vendor_name = lookup_mac_vendor(entry.packetData.macAddress);
text_vendor.set(vendor_name);
text_vendor.set(entry.vendor_name.empty() ? lookup_mac_vendor(entry.packetData.macAddress) : entry.vendor_name);
button_done.on_select = [&nav](const ui::Button&) {
nav.pop();
@@ -431,8 +432,7 @@ void BleRecentEntryDetailView::set_entry(const BleRecentEntry& entry) {
entry_ = entry;
text_mac_address.set(to_string_mac_address(entry.packetData.macAddress, 6, false));
text_pdu_type.set(pdu_type_to_string(entry.pduType));
std::string vendor_name = lookup_mac_vendor(entry.packetData.macAddress);
text_vendor.set(vendor_name);
text_vendor.set(entry.vendor_name.empty() ? lookup_mac_vendor(entry.packetData.macAddress) : entry.vendor_name);
set_dirty();
}
@@ -640,9 +640,9 @@ BLERxView::BLERxView(NavigationView& nav)
};
options_filter.on_change = [this](size_t index, int32_t v) {
(void)v;
filter_index = (uint8_t)index;
recent.clear();
handle_filter_options(v);
recent_entries_view.set_dirty();
};
@@ -818,31 +818,17 @@ void BLERxView::on_data(BlePacketData* packet) {
key |= ((uint64_t)packet->type) << 48;
}
bool packetExists = false;
// If found store into tempEntry to modify.
auto it = find(recent, key);
if (it != recent.end()) {
recent.push_front(*it);
recent.erase(it);
updateEntry(packet, recent.front(), (ADV_PDU_TYPE)packet->type);
packetExists = true;
} else {
recent.emplace_front(key);
truncate_entries(recent);
packetExists = updateEntry(packet, recent.front(), (ADV_PDU_TYPE)packet->type);
// If parsing failed, remove entry.
if (!packetExists) {
recent.erase(recent.begin());
}
}
if (packetExists) {
handle_filter_options(options_filter.selected_index());
handle_entries_sort(options_sort.selected_index());
if (updateEntry(packet, recent.front(), (ADV_PDU_TYPE)packet->type)) {
if (!searchList.empty()) {
auto it = searchList.begin();
@@ -860,24 +846,31 @@ void BLERxView::on_data(BlePacketData* packet) {
text_found_count.set(to_string_dec_uint(found_count) + "/" + to_string_dec_uint(total_count));
}
// Packets were updated, resort the list.
handle_entries_sort(options_sort.selected_index());
} else {
recent.pop_front();
}
log_ble_packet(packet);
}
void BLERxView::log_ble_packet(BlePacketData* packet) {
str_console = "";
str_console += pdu_type_to_string((ADV_PDU_TYPE)packet->type);
str_console += " Len:";
str_console += to_string_dec_uint(packet->size);
str_console += " Mac:";
str_console += to_string_mac_address(packet->macAddress, 6, false);
str_console += " Data:";
if ((logger && logging) || serial_logging) {
str_console = "";
str_console += pdu_type_to_string((ADV_PDU_TYPE)packet->type);
str_console += " Len:";
str_console += to_string_dec_uint(packet->size);
str_console += " Mac:";
str_console += to_string_mac_address(packet->macAddress, 6, false);
str_console += " Data:";
int i;
int i;
for (i = 0; i < packet->dataLen; i++) {
str_console += to_string_hex(packet->data[i], 2);
for (i = 0; i < packet->dataLen; i++) {
str_console += to_string_hex(packet->data[i], 2);
}
}
// Log at End of Packet.
@@ -894,7 +887,7 @@ void BLERxView::on_filter_change(std::string value) {
// New filter? Reset list from recent entries.
if (filter != value) {
filter = value;
handle_filter_options(options_filter.selected_index());
recent.clear();
}
}
@@ -999,48 +992,31 @@ void BLERxView::handle_entries_sort(uint8_t index) {
recent_entries_view.set_dirty();
}
void BLERxView::handle_filter_options(uint8_t index) {
auto value = filter;
bool BLERxView::handle_filter_options(uint8_t index, const BleRecentEntry& entry) {
const std::string& value = filter; // no copy
if (value.empty()) return true;
switch (index) {
// Data
case 0:
resetFilteredEntries(recent, [&value](const BleRecentEntry& entry) {
return (entry.dataString.find(value) == std::string::npos) && (entry.nameString.find(value) == std::string::npos);
});
break;
// MAC address (All caps: e.g. AA:BB:CC:DD:EE:FF)
case 1:
resetFilteredEntries(recent, [&value](const BleRecentEntry& entry) {
return (to_string_mac_address(entry.packetData.macAddress, 6, false).find(value) == std::string::npos);
});
break;
// Name
case 2:
resetFilteredEntries(recent, [&value](const BleRecentEntry& entry) {
return (entry.nameString.find(value) == std::string::npos);
});
break;
// Info
case 3:
resetFilteredEntries(recent, [&value](const BleRecentEntry& entry) {
return (entry.informationString.find(value) == std::string::npos);
});
break;
// Vendor
case 4:
resetFilteredEntries(recent, [&value](const BleRecentEntry& entry) {
std::string vendor_name = lookup_mac_vendor(entry.packetData.macAddress);
return (vendor_name.find(value) == std::string::npos);
});
break;
// Channel
case 5:
resetFilteredEntries(recent, [&value](const BleRecentEntry& entry) {
return (to_string_dec_int(entry.channelNumber).find(value) == std::string::npos);
});
break;
case 0: // Data OR Name
return std::string_view(entry.dataString).find(value) != std::string_view::npos || std::string_view(entry.nameString).find(value) != std::string_view::npos;
case 1: // MAC
return std::string_view(to_string_mac_address(entry.packetData.macAddress, 6, false)).find(value) != std::string_view::npos;
case 2: // Name
return std::string_view(entry.nameString).find(value) != std::string_view::npos;
case 3: // Info
return std::string_view(entry.informationString).find(value) != std::string_view::npos;
case 4: // Vendor
return std::string_view(entry.vendor_name).find(value) != std::string_view::npos;
case 5: // Channel (string once, not per call)
return std::string_view(to_string_dec_int(entry.channelNumber)).find(value) != std::string_view::npos;
default:
break;
return true;
}
}
@@ -1106,8 +1082,7 @@ bool BLERxView::updateEntry(const BlePacketData* packet, BleRecentEntry& entry,
entry.numHits++;
if (entry.vendor_status == MAC_VENDOR_UNKNOWN) {
std::string vendor_name;
entry.vendor_status = lookup_mac_vendor_status(entry.packetData.macAddress, vendor_name);
entry.vendor_status = lookup_mac_vendor_status(entry.packetData.macAddress, entry.vendor_name);
}
// Parse Data Section into buffer to be interpretted later.
@@ -1118,7 +1093,7 @@ bool BLERxView::updateEntry(const BlePacketData* packet, BleRecentEntry& entry,
entry.include_name = check_name.value();
}
return success;
return handle_filter_options(options_filter.selected_index(), entry) && success;
}
bool BLERxView::parse_tracking_beacon_data(const uint8_t* data, uint8_t length, std::string& nameString, std::string& informationString) {
+3 -1
View File
@@ -99,6 +99,7 @@ struct BleRecentEntry {
ADV_PDU_TYPE pduType;
uint8_t channelNumber;
MAC_VENDOR_STATUS vendor_status;
std::string vendor_name;
bool entryFound;
BleRecentEntry()
@@ -119,6 +120,7 @@ struct BleRecentEntry {
pduType{},
channelNumber{},
vendor_status{MAC_VENDOR_UNKNOWN},
vendor_name{},
entryFound{} {
}
@@ -225,7 +227,7 @@ class BLERxView : public View {
void file_error();
void on_timer();
void handle_entries_sort(uint8_t index);
void handle_filter_options(uint8_t index);
bool handle_filter_options(uint8_t index, const BleRecentEntry& entry);
bool updateEntry(const BlePacketData* packet, BleRecentEntry& entry, ADV_PDU_TYPE pdu_type);
bool parse_beacon_data(const uint8_t* data, uint8_t length, std::string& nameString, std::string& informationString);
bool parse_tracking_beacon_data(const uint8_t* data, uint8_t length, std::string& nameString, std::string& informationString);
+2
View File
@@ -293,6 +293,8 @@ void BLETxView::on_timer() {
}
void BLETxView::on_tx_progress(const bool done, uint32_t progress) {
// TODO: make use of progress variable
(void)progress;
if (done) {
if (is_active()) {
transmitter_model.disable();
+2 -2
View File
@@ -635,14 +635,14 @@ void ADSBRxView::refresh_ui() {
if (details_view->map_active()) {
// Is it time to clear and refresh the map's markers?
if (ticks_since_marker_refresh >= MARKER_UPDATE_SECONDS) {
if (ticks_since_marker_refresh >= (details_view->get_map_type() == MAP_TYPE_OSM ? MARKER_UPDATE_SECONDS_OSM : MARKER_UPDATE_SECONDS_BIN)) {
map_needs_update = true;
ticks_since_marker_refresh = 0;
details_view->clear_map_markers();
}
} else {
// Refresh map immediately once active.
ticks_since_marker_refresh = MARKER_UPDATE_SECONDS;
ticks_since_marker_refresh = MARKER_UPDATE_SECONDS_OSM; // this is the bigger, so set it to force
}
// Process the entries list.
+11 -3
View File
@@ -64,8 +64,9 @@ namespace ui {
#define VEL_AIR_SUBSONIC 3
#define VEL_AIR_SUPERSONIC 4
#define O_E_FRAME_TIMEOUT 20 // timeout between odd and even frames
#define MARKER_UPDATE_SECONDS 5 // "other" map marker redraw interval
#define O_E_FRAME_TIMEOUT 20 // timeout between odd and even frames
#define MARKER_UPDATE_SECONDS_OSM 10 // "other" map marker redraw interval for osm
#define MARKER_UPDATE_SECONDS_BIN 5 // "other" map marker redraw interval for bin map
/* Thresholds (in seconds) that define the transition between ages. */
struct ADSBAgeLimit {
@@ -277,6 +278,13 @@ class ADSBRxDetailsView : public View {
std::string title() const override { return "Details"; }
MapType get_map_type() {
if (geomap_view_)
return geomap_view_->get_map_type();
else
return MapType::MAP_TYPE_BIN; // default
}
private:
void refresh_ui();
void on_gps(const GPSPosDataMessage* msg);
@@ -388,7 +396,7 @@ class ADSBRxView : public View {
SignalToken signal_token_tick_second{};
uint8_t tick_count = 0;
uint16_t ticks_since_marker_refresh{MARKER_UPDATE_SECONDS};
uint16_t ticks_since_marker_refresh{MARKER_UPDATE_SECONDS_OSM};
/* Max number of entries that can be updated in a single pass.
* 16 is one screen of recent entries. */
+2
View File
@@ -446,6 +446,8 @@ MicTXView::MicTXView(
field_frequency.set_value(tx_frequency);
// TODO: would be nice if frequency step was configurable in this app
// but now, make it a bit lower
receiver_model.set_frequency_step(12'500); // set freq step to 12.5kHz
field_frequency.set_step(receiver_model.frequency_step());
// WARNING: transmitter_model.set_target_frequency() and receiver_model.set_target_frequency() both update the same tuning freq, but one has an offset!
+25 -1
View File
@@ -26,11 +26,17 @@
#include "binder.hpp"
#include "string_format.hpp"
#include "ui_freqman.hpp"
#include "audio.hpp"
using namespace portapack;
namespace pmem = portapack::persistent_memory;
namespace ui {
void SearchLogger::log_data(SearchRecentEntry& data) {
log_file.write_entry(";" + to_string_short_freq(data.frequency) + ";" + std::to_string(data.duration));
}
template <>
void RecentEntriesTable<SearchRecentEntries>::draw(
const Entry& entry,
@@ -75,6 +81,7 @@ SearchView::SearchView(
&check_snap,
&options_snap,
&big_display,
&check_log,
&recent_entries_view});
baseband::set_spectrum(SEARCH_SLICE_WIDTH, 31);
@@ -100,6 +107,14 @@ SearchView::SearchView(
on_range_changed();
});
check_log.on_select = [this](Checkbox&, bool v) {
logging = v;
if (logging) {
logger.append(logs_dir.string() + "/SEARCH_" + to_string_timestamp(rtc_time::now()) + ".CSV");
logger.write_header();
}
};
bind(field_threshold, settings_.power_threshold);
bind(check_snap, settings_.snap_search);
bind(options_snap, settings_.snap_step);
@@ -108,9 +123,15 @@ SearchView::SearchView(
on_range_changed();
receiver_model.enable();
if (pmem::beep_on_packets()) {
audio::set_rate(audio::Rate::Hz_24000);
audio::output::start();
}
}
SearchView::~SearchView() {
audio::output::stop();
receiver_model.disable();
baseband::shutdown();
}
@@ -192,7 +213,9 @@ void SearchView::do_detection() {
locked = true;
locked_bin = bin_max;
if (pmem::beep_on_packets()) {
baseband::request_audio_beep(1000, 24000, 60);
}
// TODO: open Audio.
} else
text_infos.set("Out of range");
@@ -210,6 +233,7 @@ void SearchView::do_detection() {
auto& entry = ::on_packet(recent, resolved_frequency);
entry.set_duration(duration);
if (logging) logger.log_data(entry);
recent_entries_view.set_dirty();
text_infos.set("Listening");
+25
View File
@@ -26,6 +26,8 @@
#include "radio_state.hpp"
#include "gradient.hpp"
#include "ui_receiver.hpp"
#include "log_file.hpp"
#include "file_path.hpp"
namespace ui {
@@ -70,6 +72,21 @@ struct SearchRecentEntry {
using SearchRecentEntries = RecentEntries<SearchRecentEntry>;
class SearchLogger {
public:
Optional<File::Error> append(const std::filesystem::path& filename) {
return log_file.append(filename);
}
void log_data(SearchRecentEntry& data);
void write_header() {
log_file.write_raw("Time;Freq;Duration;");
}
private:
LogFile log_file{};
};
class SearchView : public View {
public:
SearchView(NavigationView& nav);
@@ -148,6 +165,8 @@ class SearchView : public View {
uint16_t locked_bin = 0;
uint8_t search_counter = 0;
bool locked = false;
bool logging = false;
SearchLogger logger{};
void do_detection();
void do_timers();
@@ -169,6 +188,12 @@ class SearchView : public View {
{{1 * 8, 25 * 8}, "Accuracy +/-4.9kHz", Theme::getInstance()->fg_light->foreground},
{{26 * 8, 25 * 8}, "MHz", Theme::getInstance()->fg_light->foreground}};
Checkbox check_log{
{24 * 8, 10 * 8},
3,
"LOG",
true};
FrequencyField field_frequency_min{
{1 * 8, 1 * 16}};
FrequencyField field_frequency_max{
+1 -1
View File
@@ -6,7 +6,7 @@
* Copyright (C) 2024 Mark Thompson
* Copyright (C) 2024 u-foka
* Copyright (C) 2024 HTotoo
* Copyleft (ɔ) 2024 zxkmm under GPL license
* copyleft 2024 zxkmm AKA zix aka sommermorgentraum
*
* This file is part of PortaPack.
*
+1 -1
View File
@@ -5,7 +5,7 @@
* Copyright (C) 2023 Kyle Reed
* Copyright (C) 2024 Mark Thompson
* Copyright (C) 2024 u-foka
* Copyleft (ɔ) 2024 zxkmm under GPL license
* copyleft 2024 zxkmm AKA zix aka sommermorgentraum
*
* This file is part of PortaPack.
*
+1
View File
@@ -54,6 +54,7 @@ SondeView::SondeView(NavigationView& nav)
&field_lna,
&field_vga,
&rssi,
&channel,
&field_volume,
&check_log,
&check_crc,
+3
View File
@@ -118,6 +118,9 @@ class SondeView : public View {
RSSI rssi{
{21 * 8, 0, 6 * 8, 4}};
Channel channel{
{21 * 8, 5, 6 * 8, 4},
};
AudioVolumeField field_volume{
{screen_width - 2 * 8, 0 * 16}};
@@ -27,6 +27,9 @@
#include "ui_font_fixed_5x8.hpp"
#include "ui_font_fixed_8x16.hpp"
#include "portapack.hpp"
#include "file.hpp"
namespace ui {
@@ -93,8 +96,90 @@ bool i2c_read(uint8_t* cmd, size_t cmd_len, uint8_t* data, size_t data_len) {
return dev->i2c_read(cmd, cmd_len, data, data_len);
}
// v3
// Version 3
FRESULT ext_f_open(FIL* fp, const TCHAR* path, BYTE mode) {
return f_open(fp, path, mode);
}
FRESULT ext_f_close(FIL* fp) {
return f_close(fp);
}
FRESULT ext_f_read(FIL* fp, void* buff, UINT btr, UINT* br) {
return f_read(fp, buff, btr, br);
}
FRESULT ext_f_write(FIL* fp, const void* buff, UINT btw, UINT* bw) {
return f_write(fp, buff, btw, bw);
}
FRESULT ext_f_lseek(FIL* fp, FSIZE_t ofs) {
return f_lseek(fp, ofs);
}
FRESULT ext_f_truncate(FIL* fp) {
return f_truncate(fp);
}
FRESULT ext_f_sync(FIL* fp) {
return f_sync(fp);
}
FRESULT ext_f_opendir(DIR* dp, const TCHAR* path) {
return f_opendir(dp, path);
}
FRESULT ext_f_closedir(DIR* dp) {
return f_closedir(dp);
}
FRESULT ext_f_readdir(DIR* dp, FILINFO* fno) {
return f_readdir(dp, fno);
}
FRESULT ext_f_findfirst(DIR* dp, FILINFO* fno, const TCHAR* path, const TCHAR* pattern) {
return f_findfirst(dp, fno, path, pattern);
}
FRESULT ext_f_findnext(DIR* dp, FILINFO* fno) {
return f_findnext(dp, fno);
}
FRESULT ext_f_mkdir(const TCHAR* path) {
return f_mkdir(path);
}
FRESULT ext_f_unlink(const TCHAR* path) {
return f_unlink(path);
}
FRESULT ext_f_rename(const TCHAR* path_old, const TCHAR* path_new) {
return f_rename(path_old, path_new);
}
FRESULT ext_f_stat(const TCHAR* path, FILINFO* fno) {
return f_stat(path, fno);
}
FRESULT ext_f_utime(const TCHAR* path, const FILINFO* fno) {
return f_utime(path, fno);
}
FRESULT ext_f_getfree(const TCHAR* path, DWORD* nclst, FATFS** fatfs) {
return f_getfree(path, nclst, fatfs);
}
FRESULT ext_f_mount(FATFS* fs, const TCHAR* path, BYTE opt) {
return f_mount(fs, path, opt);
}
int ext_f_putc(TCHAR c, FIL* fp) {
return f_putc(c, fp);
}
int ext_f_puts(const TCHAR* str, FIL* cp) {
return f_puts(str, cp);
}
int ext_f_printf(FIL* fp, const TCHAR* str, ...) {
return f_printf(fp, str);
}
TCHAR* ext_f_gets(TCHAR* buff, int len, FIL* fp) {
return f_gets(buff, len, fp);
}
void ext_draw_pixels(const ui::Rect r, const ui::Color* const colors, const size_t count) {
portapack::display.draw_pixels(r, colors, count);
}
void ext_draw_pixel(const ui::Point p, const ui::Color color) {
portapack::display.draw_pixel(p, color);
}
StandaloneView* standaloneView = nullptr;
void exit_app() {
if (standaloneView) standaloneView->exit();
}
void set_dirty() {
if (standaloneView != nullptr)
standaloneView->set_dirty();
@@ -121,6 +206,36 @@ standalone_application_api_t api = {
/* .i2c_read = */ &i2c_read,
/* .panic = */ &chDbgPanic,
/* .set_dirty = */ &set_dirty,
// Version 3
.f_open = &ext_f_open,
.f_close = &ext_f_close,
.f_read = &ext_f_read,
.f_write = &ext_f_write,
.f_lseek = &ext_f_lseek,
.f_truncate = &ext_f_truncate,
.f_sync = &ext_f_sync,
.f_opendir = &ext_f_opendir,
.f_closedir = &ext_f_closedir,
.f_readdir = &ext_f_readdir,
.f_findfirst = &ext_f_findfirst,
.f_findnext = &ext_f_findnext,
.f_mkdir = &ext_f_mkdir,
.f_unlink = &ext_f_unlink,
.f_rename = &ext_f_rename,
.f_stat = &ext_f_stat,
.f_utime = &ext_f_utime,
.f_getfree = &ext_f_getfree,
.f_mount = &ext_f_mount,
.f_putc = &ext_f_putc,
.f_puts = &ext_f_puts,
.f_printf = &ext_f_printf,
.f_gets = &ext_f_gets,
.draw_pixels = &ext_draw_pixels,
.draw_pixel = &ext_draw_pixel,
.exit_app = &exit_app,
// version 4
.screen_height = &screen_height,
.screen_width = &screen_width,
};
StandaloneView::StandaloneView(NavigationView& nav, uint8_t* app_image)
@@ -158,22 +273,21 @@ bool StandaloneView::on_key(const KeyEvent key) {
if (get_application_information()->header_version > 1) {
return get_application_information()->OnKeyEvent((uint8_t)key);
}
return false;
return true;
}
bool StandaloneView::on_encoder(const EncoderEvent event) {
if (get_application_information()->header_version > 1) {
return get_application_information()->OnEncoder((int32_t)event);
}
return false;
return true;
}
bool StandaloneView::on_touch(const TouchEvent event) {
if (get_application_information()->header_version > 1) {
get_application_information()->OnTouchEvent(event.point.x(), event.point.y(), (uint32_t)event.type);
}
return true;
return false;
}
bool StandaloneView::on_keyboard(const KeyboardEvent event) {
@@ -202,4 +316,8 @@ void StandaloneView::on_before_detach() {
context().focus_manager().clearMirror();
}
void StandaloneView::exit() {
nav_.pop();
}
} // namespace ui
@@ -51,6 +51,8 @@ class StandaloneView : public View {
void frame_sync();
void exit();
private:
bool initialized = false;
NavigationView& nav_;
+38
View File
@@ -3689,6 +3689,44 @@ static constexpr Bitmap bitmap_icon_sonde{
{16, 16},
bitmap_icon_sonde_data};
static constexpr uint8_t bitmap_icon_soundboard_data[] = {
0xF0,
0x0F,
0x1C,
0x18,
0x17,
0x38,
0x15,
0x78,
0x15,
0xF8,
0x15,
0x82,
0x15,
0x8B,
0xD5,
0x83,
0xD5,
0xBB,
0xD5,
0x83,
0x15,
0x8B,
0x15,
0x92,
0x15,
0xA0,
0x17,
0x80,
0x1C,
0x80,
0xF0,
0xFF,
};
static constexpr Bitmap bitmap_icon_soundboard{
{16, 16},
bitmap_icon_soundboard_data};
static constexpr uint8_t bitmap_icon_speaker_data[] = {
0x00,
0x00,
@@ -1,5 +1,5 @@
/*
* copyleft spammingdramaqueen
* copyleft 2024 zxkmm AKA zix aka sommermorgentraum
*
* This file is part of PortaPack.
*
@@ -1,5 +1,5 @@
/*
* copyleft spammingdramaqueen
* copyleft 2024 zxkmm AKA zix aka sommermorgentraum
*
* This file is part of PortaPack.
*
+1 -1
View File
@@ -440,7 +440,7 @@ static void _sumlr(void);
#undef __O
#undef __P
#define PI 3.141592653589793
#include "mathdef.hpp"
static void _numinput(byte k);
static double dpush(double d);
+1 -1
View File
@@ -31,7 +31,7 @@ int HALF_HEIGHT = 0;
#define JOGGING_SPEED 1.2
#define MAX_ENTITY_DISTANCE 200
#define ITEM_COLLIDER_DIST 6
#define PI 3.14159265358979323846
#include "mathdef.hpp"
#define GUN_WIDTH 30
#define GUN_HEIGHT 40
#define GUN_TARGET_POS 24
+6 -1
View File
@@ -245,6 +245,10 @@ set(EXTCPPSRC
#soundboard
external/soundboard/main.cpp
external/soundboard/soundboard_app.cpp
#game2048
external/game2048/main.cpp
external/game2048/ui_game2048.cpp
)
set(EXTAPPLIST
@@ -273,7 +277,7 @@ set(EXTAPPLIST
morse_tx
sstvtx
random_password
acars_rx
# acars_rx
ookbrute
ook_editor
wefax_rx
@@ -307,4 +311,5 @@ set(EXTAPPLIST
ert
epirb_rx
soundboard
game2048
)
+8
View File
@@ -82,6 +82,8 @@ MEMORY
ram_external_app_ert (rwx) : org = 0xADE90000, len = 32k
ram_external_app_epirb_rx (rwx) : org = 0xADEA0000, len = 32k
ram_external_app_soundboard (rwx) : org = 0xADEB0000, len = 32k
ram_external_app_game2048 (rwx) : org = 0xADEC0000, len = 32k
}
@@ -441,5 +443,11 @@ SECTIONS
*(*ui*external_app*soundboard*);
} > ram_external_app_soundboard
.external_app_game2048 : ALIGN(4) SUBALIGN(4)
{
KEEP(*(.external_app.app_game2048.application_information));
*(*ui*external_app*game2048*);
} > ram_external_app_game2048
}
@@ -1,6 +1,6 @@
/*
* Copyright (C) 2023 Mark Thompson
* copyleft Whiterose of the Dark Army
* copyleft 2024 zxkmm AKA zix aka sommermorgentraum
*
* This file is part of PortaPack.
*
@@ -1,6 +1,6 @@
/*
* Copyright (C) 2023 Mark Thompson
* copyleft Whiterose of the Dark Army
* copyleft 2024 zxkmm AKA zix aka sommermorgentraum
*
* This file is part of PortaPack.
*
+83
View File
@@ -0,0 +1,83 @@
/*
* Copyright (C) 2023 Bernd Herzog
*
* This file is part of PortaPack.
*
* This program is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation; either version 2, or (at your option)
* any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program; see the file COPYING. If not, write to
* the Free Software Foundation, Inc., 51 Franklin Street,
* Boston, MA 02110-1301, USA.
*/
#include "ui.hpp"
#include "ui_game2048.hpp"
#include "ui_navigation.hpp"
#include "external_app.hpp"
namespace ui::external_app::game2048 {
void initialize_app(ui::NavigationView& nav) {
nav.push<Game2048View>();
}
} // namespace ui::external_app::game2048
extern "C" {
__attribute__((section(".external_app.app_game2048.application_information"), used)) application_information_t _application_information_game2048 = {
(uint8_t*)0x00000000,
ui::external_app::game2048::initialize_app,
CURRENT_HEADER_VERSION,
VERSION_MD5,
"2048",
{
0x8C,
0x31,
0x5A,
0x6B,
0xDE,
0x7B,
0x8C,
0x31,
0x00,
0x00,
0x8C,
0x31,
0x5A,
0x7B,
0xDE,
0x7B,
0x8C,
0x31,
0x00,
0x00,
0x8C,
0x31,
0xDA,
0x7B,
0xDE,
0x7B,
0x8C,
0x31,
0x00,
0x00,
0x00,
0x00,
},
ui::Color::green().v,
app_location_t::GAMES,
-1,
{0, 0, 0, 0},
0x00000000,
};
} // namespace ui::external_app::game2048
+422
View File
@@ -0,0 +1,422 @@
/*
* copyleft 2025 zxkmm AKA zix aka sommermorgentraum
*
* 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_game2048.hpp"
#include "string_format.hpp"
namespace ui::external_app::game2048 {
Game2048View::Game2048View(NavigationView& nav)
: nav_(nav), score(0), best_score(0), game_state(STATE_PLAYING) {
add_children({&dummy});
init_game();
}
void Game2048View::on_show() {
if (!initialized) {
std::srand(LPC_RTC->CTIME0);
reset_game();
initialized = true;
}
}
void Game2048View::init_game() {
for (int i = 0; i < GRID_SIZE; i++) {
for (int j = 0; j < GRID_SIZE; j++) {
grid[i][j] = 0;
}
}
}
void Game2048View::reset_game() {
need_repaint = true;
init_game();
score = 0;
game_state = STATE_PLAYING;
add_random_tile();
add_random_tile();
}
void Game2048View::add_random_tile() {
std::vector<std::pair<int, int>> empty_cells;
for (int i = 0; i < GRID_SIZE; i++) {
for (int j = 0; j < GRID_SIZE; j++) {
if (grid[i][j] == 0) {
empty_cells.push_back(std::make_pair(i, j));
}
}
}
if (!empty_cells.empty()) {
auto& random_cell = empty_cells[rand() % empty_cells.size()];
grid[random_cell.first][random_cell.second] = (rand() % 10 == 0) ? 4 : 2;
}
}
void Game2048View::compress_left(int row[]) {
int temp[GRID_SIZE];
int index = 0;
for (int i = 0; i < GRID_SIZE; i++) {
temp[i] = 0;
}
for (int i = 0; i < GRID_SIZE; i++) {
if (row[i] != 0) {
temp[index++] = row[i];
}
}
for (int i = 0; i < GRID_SIZE; i++) {
row[i] = temp[i];
}
}
void Game2048View::merge_left(int row[]) {
for (int i = 0; i < GRID_SIZE - 1; i++) {
if (row[i] != 0 && row[i] == row[i + 1]) {
row[i] *= 2;
score += row[i];
row[i + 1] = 0;
}
}
}
bool Game2048View::move_row_left(int row[]) {
int original[GRID_SIZE];
for (int i = 0; i < GRID_SIZE; i++) {
original[i] = row[i];
}
compress_left(row);
merge_left(row);
compress_left(row);
for (int i = 0; i < GRID_SIZE; i++) {
if (original[i] != row[i]) {
return true;
}
}
return false;
}
void Game2048View::rotate_grid_clockwise() {
int temp[GRID_SIZE][GRID_SIZE];
for (int i = 0; i < GRID_SIZE; i++) {
for (int j = 0; j < GRID_SIZE; j++) {
temp[j][GRID_SIZE - 1 - i] = grid[i][j];
}
}
for (int i = 0; i < GRID_SIZE; i++) {
for (int j = 0; j < GRID_SIZE; j++) {
grid[i][j] = temp[i][j];
}
}
}
void Game2048View::rotate_grid_counter_clockwise() {
int temp[GRID_SIZE][GRID_SIZE];
for (int i = 0; i < GRID_SIZE; i++) {
for (int j = 0; j < GRID_SIZE; j++) {
temp[GRID_SIZE - 1 - j][i] = grid[i][j];
}
}
for (int i = 0; i < GRID_SIZE; i++) {
for (int j = 0; j < GRID_SIZE; j++) {
grid[i][j] = temp[i][j];
}
}
}
bool Game2048View::move_left() {
bool moved = false;
for (int i = 0; i < GRID_SIZE; i++) {
if (move_row_left(grid[i])) {
moved = true;
}
}
return moved;
}
bool Game2048View::move_right() {
rotate_grid_clockwise();
rotate_grid_clockwise();
bool moved = move_left();
rotate_grid_clockwise();
rotate_grid_clockwise();
return moved;
}
bool Game2048View::move_up() {
rotate_grid_counter_clockwise();
bool moved = move_left();
rotate_grid_clockwise();
return moved;
}
bool Game2048View::move_down() {
rotate_grid_clockwise();
bool moved = move_left();
rotate_grid_counter_clockwise();
return moved;
}
bool Game2048View::can_move() {
for (int i = 0; i < GRID_SIZE; i++) {
for (int j = 0; j < GRID_SIZE; j++) {
if (grid[i][j] == 0) return true;
if (j < GRID_SIZE - 1 && grid[i][j] == grid[i][j + 1]) return true;
if (i < GRID_SIZE - 1 && grid[i][j] == grid[i + 1][j]) return true;
}
}
return false;
}
bool Game2048View::is_game_won() {
for (int i = 0; i < GRID_SIZE; i++) {
for (int j = 0; j < GRID_SIZE; j++) {
if (grid[i][j] == 2048) {
return true;
}
}
}
return false;
}
Color Game2048View::get_tile_color(int value) {
switch (value) {
case 0:
return Color::light_grey();
case 2:
return Color::white();
case 4:
return Color::yellow();
case 8:
return Color::orange();
case 16:
return Color::red();
case 32:
return Color::magenta();
case 64:
return Color::purple();
case 128:
return Color::blue();
case 256:
return Color::cyan();
case 512:
return Color::green();
case 1024:
return Color::dark_green();
case 2048:
return Color::dark_red();
default:
return Color::black();
}
}
Color Game2048View::get_text_color(int value) {
if (value <= 4) {
return Color::black();
}
return Color::white();
}
void Game2048View::draw_tile(int x, int y, int value) {
int tile_x = BOARD_START_X + x * (TILE_SIZE + TILE_MARGIN);
int tile_y = BOARD_START_Y + y * (TILE_SIZE + TILE_MARGIN);
Color tile_color = get_tile_color(value);
painter.fill_rectangle({tile_x, tile_y, TILE_SIZE, TILE_SIZE}, tile_color);
painter.draw_rectangle({tile_x, tile_y, TILE_SIZE, TILE_SIZE}, Color::dark_grey());
if (value > 0) {
std::string value_str = to_string_dec_uint(value);
int text_width = value_str.length() * 8;
int text_height = 16;
int text_x = tile_x + (TILE_SIZE - text_width) / 2;
int text_y = tile_y + (TILE_SIZE - text_height) / 2;
painter.draw_string({text_x, text_y}, *Theme::getInstance()->bg_darkest, value_str);
}
}
void Game2048View::draw_score() {
painter.fill_rectangle({0, 0, screen_width, INFO_HEIGHT}, Color::black());
std::string score_str = "Score: " + to_string_dec_uint(score);
painter.draw_string({10, 5}, *Theme::getInstance()->bg_darkest, score_str);
if (best_score > 0) {
std::string best_str = "Best: " + to_string_dec_uint(best_score);
painter.draw_string({150, 5}, *Theme::getInstance()->bg_darkest, best_str);
}
}
void Game2048View::draw_game() {
// the paint timing is always bad at app enterences,
// in all apps it behave like this.
// this is a buffer timer to let it flash a time until app loaded.
if (!need_repaint && load_wait_time_counter++ < 40000) {
return;
}
if (need_repaint_bg_frame) painter.fill_rectangle({0, INFO_HEIGHT, screen_width, screen_height - INFO_HEIGHT}, Color::dark_grey());
for (int i = 0; i < GRID_SIZE; i++) {
for (int j = 0; j < GRID_SIZE; j++) {
draw_tile(j, i, grid[i][j]);
}
}
draw_score();
need_repaint = false;
need_repaint_bg_frame = false;
}
void Game2048View::show_game_over() {
if (show_you_won_you_lost_slow_down_cunter++ % 1000 != 0) return;
// slow down otherwies it repaint even faster than it finished paint full stuff
painter.fill_rectangle({40, 100, 160, 100}, Color::black());
painter.draw_rectangle({40, 100, 160, 100}, Color::white());
painter.draw_string({80, 115}, *Theme::getInstance()->bg_darkest, "GAME OVER");
painter.draw_string({70, 135}, *Theme::getInstance()->bg_darkest, "Press SELECT");
painter.draw_string({75, 155}, *Theme::getInstance()->bg_darkest, "to restart");
need_repaint = false;
need_repaint_bg_frame = true;
}
void Game2048View::show_you_won() {
if (show_you_won_you_lost_slow_down_cunter++ % 1000 != 0) return;
// slow down otherwies it repaint even faster than it finished paint full stuff
painter.fill_rectangle({40, 100, 160, 100}, Color::black());
painter.draw_rectangle({40, 100, 160, 100}, Color::white());
painter.draw_string({80, 115}, *Theme::getInstance()->bg_darkest, "YOU WON!");
painter.draw_string({70, 135}, *Theme::getInstance()->bg_darkest, "Press SELECT");
painter.draw_string({75, 155}, *Theme::getInstance()->bg_darkest, "to restart");
need_repaint = false;
need_repaint_bg_frame = true;
}
void Game2048View::paint(Painter&) {
draw_game();
if (game_state == STATE_GAME_OVER) {
need_repaint = true;
need_repaint_bg_frame = true;
show_game_over();
} else if (game_state == STATE_WON) {
need_repaint = true;
need_repaint_bg_frame = true;
show_you_won();
}
}
void Game2048View::frame_sync() {
draw_game();
if (game_state == STATE_GAME_OVER) {
show_game_over();
} else if (game_state == STATE_WON) {
show_you_won();
}
}
bool Game2048View::on_key(KeyEvent key) {
if (key == KeyEvent::Select) {
if (game_state == STATE_GAME_OVER || game_state == STATE_WON) {
reset_game();
return true;
}
}
if (game_state != STATE_PLAYING) {
return false;
}
bool moved = false;
switch (key) {
case KeyEvent::Left:
moved = move_left();
break;
case KeyEvent::Right:
moved = move_right();
break;
case KeyEvent::Up:
moved = move_up();
break;
case KeyEvent::Down:
moved = move_down();
break;
default:
return false;
}
if (moved) {
need_repaint = true;
add_random_tile();
if (score > best_score) {
best_score = score;
}
if (is_game_won() && game_state == STATE_PLAYING) {
game_state = STATE_WON;
} else if (!can_move()) {
game_state = STATE_GAME_OVER;
}
}
return true;
}
bool Game2048View::on_encoder(const EncoderEvent) {
if (game_state != STATE_PLAYING) {
return false;
}
bool moved = false;
if (moved) {
add_random_tile();
if (score > best_score) {
best_score = score;
}
if (is_game_won() && game_state == STATE_PLAYING) {
game_state = STATE_WON;
} else if (!can_move()) {
game_state = STATE_GAME_OVER;
}
}
return true;
}
} // namespace ui::external_app::game2048
+113
View File
@@ -0,0 +1,113 @@
/*
* copyleft 2025 zxkmm AKA zix aka sommermorgentraum
*
* 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_GAME2048_H__
#define __UI_GAME2048_H__
#include "ui.hpp"
#include "ui_navigation.hpp"
#include "event_m0.hpp"
#include "message.hpp"
#include "irq_controls.hpp"
#include "lpc43xx_cpp.hpp"
#include "ui_widget.hpp"
namespace ui::external_app::game2048 {
#define GRID_SIZE 4
#define TILE_SIZE 50
#define TILE_MARGIN 5
#define BOARD_START_X 15
#define BOARD_START_Y 35
#define INFO_HEIGHT 30
enum GameState {
STATE_PLAYING,
STATE_GAME_OVER,
STATE_WON
};
class Game2048View : public View {
public:
Game2048View(NavigationView& nav);
void on_show() override;
std::string title() const override { return "2048"; }
void focus() override { dummy.focus(); }
void paint(Painter& painter) override;
void frame_sync();
bool on_key(KeyEvent key) override;
bool on_encoder(const EncoderEvent event) override;
private:
NavigationView& nav_;
Painter painter{};
int grid[GRID_SIZE][GRID_SIZE];
int score;
int best_score;
GameState game_state;
bool initialized = false;
bool need_repaint = true;
bool need_repaint_bg_frame = true; // drar bg frame is too flickering, so make it paint as less as possible
bool need_set_dirty = false;
uint32_t load_wait_time_counter = 0; // the paint timing is always bad at app enterences,
// in all apps it behave like this.
// this is a buffer timer to let it flash a time until app loaded.
uint32_t show_you_won_you_lost_slow_down_cunter = 0; // slow down otherwies it repaint even faster than it finished paint fulll stuff
void init_game();
void reset_game();
void add_random_tile();
bool move_left();
bool move_right();
bool move_up();
bool move_down();
bool can_move();
bool is_game_won();
void draw_game();
void draw_tile(int x, int y, int value);
void draw_score();
void show_game_over();
void show_you_won();
Color get_tile_color(int value);
Color get_text_color(int value);
void compress_left(int row[]);
void merge_left(int row[]);
bool move_row_left(int row[]);
void rotate_grid_clockwise();
void rotate_grid_counter_clockwise();
Button dummy{
{screen_width, 0, 0, 0},
""};
MessageHandlerRegistration message_handler_frame_sync{
Message::ID::DisplayFrameSync,
[this](const Message* const) {
this->frame_sync();
}};
};
} // namespace ui::external_app::game2048
#endif /* __UI_GAME2048_H__ */
+1 -1
View File
@@ -2,7 +2,7 @@
* Copyright (C) 2015 Jared Boone, ShareBrained Technology, Inc.
* Copyright (C) 2016 Furrtek
* Copyright (C) 2020 euquiq
* copyleft mr.r0b0t from the F society
* copyleft 2024 zxkmm AKA zix aka sommermorgentraum
*
* This file is part of PortaPack.
*
+1 -1
View File
@@ -2,7 +2,7 @@
* Copyright (C) 2015 Jared Boone, ShareBrained Technology, Inc.
* Copyright (C) 2016 Furrtek
* Copyright (C) 2020 euquiq
* copyleft mr.r0b0t from the F society
* copyleft 2024 zxkmm AKA zix aka sommermorgentraum
*
* This file is part of PortaPack.
*
+3 -45
View File
@@ -112,6 +112,7 @@ void RangeView::paint(Painter&) {
}
RangeView::RangeView(NavigationView& nav) {
(void)nav;
hidden(true);
add_children({&labels,
@@ -122,50 +123,6 @@ RangeView::RangeView(NavigationView& nav) {
&button_center,
&button_width});
check_enabled.on_select = [this](Checkbox&, bool v) {
frequency_range.enabled = v;
};
button_start.on_select = [this, &nav](Button& button) {
auto new_view = nav.push<FrequencyKeypadView>(frequency_range.min);
new_view->on_changed = [this, &button](rf::Frequency f) {
update_start(f);
};
};
button_stop.on_select = [this, &nav](Button& button) {
auto new_view = nav.push<FrequencyKeypadView>(frequency_range.max);
new_view->on_changed = [this, &button](rf::Frequency f) {
update_stop(f);
};
};
button_center.on_select = [this, &nav](Button& button) {
auto new_view = nav.push<FrequencyKeypadView>(center);
new_view->on_changed = [this, &button](rf::Frequency f) {
update_center(f);
};
};
button_width.on_select = [this, &nav](Button& button) {
auto new_view = nav.push<FrequencyKeypadView>(width);
new_view->on_changed = [this, &button](rf::Frequency f) {
update_width(f);
};
};
button_load_range.on_select = [this, &nav](Button&) {
auto load_view = nav.push<FrequencyLoadView>();
load_view->on_frequency_loaded = [this](rf::Frequency value) {
update_center(value);
update_width(100000);
};
load_view->on_range_loaded = [this](rf::Frequency start, rf::Frequency stop) {
update_start(start);
update_stop(stop);
};
};
check_enabled.set_value(false);
}
@@ -400,7 +357,8 @@ JammerView::JammerView(NavigationView& nav)
};
for (auto range_view : range_views) {
range_view->check_enabled.on_select = [this](Checkbox&, bool) {
range_view->check_enabled.on_select = [this, range_view](Checkbox&, bool v) {
range_view->frequency_range.enabled = v;
if (jamming) update_config();
};
range_view->button_start.on_select = [this, range_view](Button&) {
+1 -1
View File
@@ -1,5 +1,5 @@
/*
* copyleft 2024 sommermorgentraum
* copyleft 2024 zxkmm AKA zix aka sommermorgentraum
*
* This file is part of PortaPack.
*
+1 -1
View File
@@ -1,5 +1,5 @@
/*
* copyleft 2024 sommermorgentraum
* copyleft 2024 zxkmm AKA zix aka sommermorgentraum
*
* This file is part of PortaPack.
*
+2 -2
View File
@@ -1,7 +1,7 @@
/*
* Copyright (C) 2024 Samir Sánchez Garnica @sasaga92
* copyleft Elliot Alderson from F society
* copyleft Darlene Alderson from F society
* copyleft 2024 zxkmm AKA zix aka sommermorgentraum
*
* This file is part of PortaPack.
*
+1 -2
View File
@@ -1,7 +1,6 @@
/*
* Copyright (C) 2024 Samir Sánchez Garnica @sasaga92
* copyleft Elliot Alderson from F society
* copyleft Darlene Alderson from F society
* copyleft 2024 zxkmm AKA zix aka sommermorgentraum
*
* This file is part of PortaPack.
*
+1 -1
View File
@@ -1,6 +1,6 @@
/*
* Copyright (C) 2024 HTotoo
* copyleft Whiterose of the Dark Army
* copyleft 2024 zxkmm AKA zix aka sommermorgentraum
*
* This file is part of PortaPack.
*
+1 -1
View File
@@ -1,6 +1,6 @@
/*
* Copyright (C) 2024 HTotoo
* copyleft Whiterose of the Dark Army
* copyleft 2024 zxkmm AKA zix aka sommermorgentraum
*
* This file is part of PortaPack.
*
@@ -1,6 +1,5 @@
/*
* copyleft Elliot Alderson from F society
* copyleft Darlene Alderson from F society
* copyleft 2025 zxkmm AKA zix aka sommermorgentraum
*
* This file is part of PortaPack.
*
@@ -1,6 +1,5 @@
/*
* copyleft Elliot Alderson from F society
* copyleft Darlene Alderson from F society
* copyleft 2025 zxkmm AKA zix aka sommermorgentraum
*
* This file is part of PortaPack.
*
+1 -1
View File
@@ -1,6 +1,6 @@
/*
* Copyright 2025 Mark Thompson
* copyleft Mr. Robot of F.Society
* copyleft 2025 zxkmm AKA zix aka sommermorgentraum
*
* This file is part of PortaPack.
*
+1 -1
View File
@@ -1,6 +1,6 @@
/*
* Copyright 2025 Mark Thompson
* copyleft Mr. Robot of F.Society
* copyleft 2025 zxkmm AKA zix aka sommermorgentraum
*
* This file is part of PortaPack.
*
+1 -1
View File
@@ -1,5 +1,5 @@
/*
* copyleft 2024 sommermorgentraum
* copyleft 2024 zxkmm AKA zix aka sommermorgentraum
*
* This file is part of PortaPack.
*
+1 -1
View File
@@ -1,5 +1,5 @@
/*
* copyleft 2024 sommermorgentraum
* copyleft 2024 zxkmm AKA zix aka sommermorgentraum
*
* This file is part of PortaPack.
*
@@ -158,9 +158,10 @@ WardriveMapView::WardriveMapView(NavigationView& nav)
}
// never move this before the first load() bc that will mess load up
geomap.on_move = [this](float lon, float lat) {
geomap.on_move = [this](float lon, float lat, bool absolute) {
(void)lon;
(void)lat;
(void)absolute;
load_markers();
};
btn_back.on_select = [this, &nav](Button&) {
+1 -1
View File
@@ -113,7 +113,7 @@ File::Result<bool> File::eof() {
File::Result<File::Offset> File::seek(Offset new_position) {
/* NOTE: Returns *old* position, not new position */
const auto old_position = tell();
const auto result = f_lseek(&f, new_position);
const auto result = (old_position == new_position) ? FR_OK : f_lseek(&f, new_position);
if (result != FR_OK) {
return {static_cast<Error>(result)};
}
+1 -1
View File
@@ -1,6 +1,6 @@
/*
* Copyright (C) 2015 Jared Boone, ShareBrained Technology, Inc.
* copyleft (ɔ) 2023 zxkmm under GPL license
* copyleft 2023 zxkmm AKA zix aka sommermorgentraum
* Copyright (C) 2023 u-foka
*
* This file is part of PortaPack.
+1 -1
View File
@@ -1,6 +1,6 @@
/*
* Copyright (C) 2015 Jared Boone, ShareBrained Technology, Inc.
* copyleft (ɔ) 2023 zxkmm under GPL license
* copyleft 2023 zxkmm AKA zix aka sommermorgentraum
* Copyright (C) 2023 u-foka
*
* This file is part of PortaPack.
+6
View File
@@ -4,6 +4,12 @@ namespace ui {
ThemeTemplate* Theme::current = nullptr;
void Theme::destroy() {
if (current != nullptr)
delete current;
current = nullptr;
}
ThemeTemplate* Theme::getInstance() {
if (current == nullptr) SetTheme(DefaultGrey);
return Theme::current;
+1 -1
View File
@@ -113,7 +113,7 @@ class Theme {
static void SetTheme(ThemeId theme);
static ThemeTemplate* current;
static void destroy(); // used from standalone app, to prevent memleak
private:
};
+1 -1
View File
@@ -4,7 +4,7 @@
* Copyright (C) 2019 Elia Yehuda (z4ziggy)
* Copyright (C) 2023 Mark Thompson
* Copyright (C) 2024 u-foka
* Copyleft () 2024 zxkmm with the GPL license
* copyleft 2024 zxkmm AKA zix aka sommermorgentraum
*
* This file is part of PortaPack.
*
+327 -105
View File
@@ -22,21 +22,17 @@
*/
#include "ui_geomap.hpp"
#include "portapack.hpp"
#include <cstring>
#include <stdio.h>
#include <string_view>
using namespace portapack;
#include "string_format.hpp"
#include "complex.hpp"
#include "ui_font_fixed_5x8.hpp"
#include "file_path.hpp"
namespace ui {
GeoPos::GeoPos(
const Point pos,
const alt_unit altitude_unit,
@@ -188,6 +184,7 @@ int32_t GeoPos::speed() {
GeoMap::GeoMap(
Rect parent_rect)
: Widget{parent_rect}, markerListLen(0) {
has_osm = use_osm = find_osm_file_tile();
}
bool GeoMap::on_encoder(const EncoderEvent delta) {
@@ -201,6 +198,8 @@ bool GeoMap::on_encoder(const EncoderEvent delta) {
map_zoom += (map_zoom >= MAP_ZOOM_RESOLUTION_LIMIT) ? map_zoom : 1;
}
}
map_osm_zoom++;
if (has_osm) set_osm_max_zoom();
} else if (delta < 0) {
if (map_zoom > -MAX_MAP_ZOOM_OUT) {
if (map_zoom == 1) {
@@ -212,12 +211,18 @@ bool GeoMap::on_encoder(const EncoderEvent delta) {
map_zoom--;
}
}
if (map_osm_zoom > 0) map_osm_zoom--;
} else {
return false;
}
map_visible = map_opened && (map_zoom <= MAP_ZOOM_RESOLUTION_LIMIT);
zoom_pixel_offset = (map_visible && (map_zoom > 1)) ? (float)map_zoom / 2 : 0.0f;
if (use_osm) {
map_visible = true;
zoom_pixel_offset = 0;
} else {
zoom_pixel_offset = (map_visible && (map_zoom > 1)) ? (float)map_zoom / 2 : 0.0f;
}
// Trigger map redraw
redraw_map = true;
@@ -225,7 +230,7 @@ bool GeoMap::on_encoder(const EncoderEvent delta) {
return true;
}
void GeoMap::map_read_line(ui::Color* buffer, uint16_t pixels) {
void GeoMap::map_read_line_bin(ui::Color* buffer, uint16_t pixels) {
if (map_zoom == 1) {
map_file.read(buffer, pixels << 1);
} else if (map_zoom > 1) {
@@ -251,7 +256,7 @@ void GeoMap::map_read_line(ui::Color* buffer, uint16_t pixels) {
for (int i = 0; i < geomap_rect_width; i++) {
buffer[i] = zoom_out_buffer[i * (-map_zoom)];
}
delete zoom_out_buffer;
delete[] zoom_out_buffer;
}
}
@@ -264,39 +269,86 @@ void GeoMap::draw_markers(Painter& painter) {
void GeoMap::draw_marker_item(Painter& painter, GeoMarker& item, const Color color, const Color fontColor, const Color backColor) {
const auto r = screen_rect();
const ui::Point itemPoint = item_rect_pixel(item);
if ((itemPoint.x() >= 0) && (itemPoint.x() < r.width()) &&
(itemPoint.y() > 10) && (itemPoint.y() < r.height())) // Dont draw within symbol size of top
{
draw_marker(painter, {itemPoint.x(), itemPoint.y() + r.top()}, item.angle, item.tag, color, fontColor, backColor);
draw_marker(painter, {itemPoint.x() + r.left(), itemPoint.y() + r.top()}, item.angle, item.tag, color, fontColor, backColor);
}
}
// Calculate screen position of item, adjusted for zoom factor.
ui::Point GeoMap::item_rect_pixel(GeoMarker& item) {
const auto r = screen_rect();
const auto geomap_rect_half_width = r.width() / 2;
const auto geomap_rect_half_height = r.height() / 2;
GeoPoint mapPoint = lat_lon_to_map_pixel(item.lat, item.lon);
float x = mapPoint.x - x_pos;
float y = mapPoint.y - y_pos;
if (map_zoom > 1) {
x = x * map_zoom + zoom_pixel_offset;
y = y * map_zoom + zoom_pixel_offset;
} else if (map_zoom < 0) {
x = x / (-map_zoom);
y = y / (-map_zoom);
if (!use_osm) {
const auto r = screen_rect();
const auto geomap_rect_half_width = r.width() / 2;
const auto geomap_rect_half_height = r.height() / 2;
GeoPoint mapPoint = lat_lon_to_map_pixel(item.lat, item.lon);
float x = mapPoint.x - x_pos;
float y = mapPoint.y - y_pos;
if (map_zoom > 1) {
x = x * map_zoom + zoom_pixel_offset;
y = y * map_zoom + zoom_pixel_offset;
} else if (map_zoom < 0) {
x = x / (-map_zoom);
y = y / (-map_zoom);
}
x += geomap_rect_half_width;
y += geomap_rect_half_height;
return {(int16_t)x, (int16_t)y};
}
x += geomap_rect_half_width;
y += geomap_rect_half_height;
// osm calculation
double y = lat_to_pixel_y_tile(item.lat, map_osm_zoom) - viewport_top_left_py;
double x = lon_to_pixel_x_tile(item.lon, map_osm_zoom) - viewport_top_left_px;
return {(int16_t)x, (int16_t)y};
}
// Converts latitude/longitude to pixel coordinates in map file.
/**
* @brief Converts longitude to a map tile's X-coordinate.
* @param lon The longitude in degrees.
* @param zoom The zoom level.
* @return The X-coordinate of the tile.
*/
int GeoMap::lon2tile(double lon, int zoom) {
return (int)floor((lon + 180.0) / 360.0 * pow(2.0, zoom));
}
/**
* @brief Converts latitude to a map tile's Y-coordinate.
* @param lat The latitude in degrees.
* @param zoom The zoom level.
* @return The Y-coordinate of the tile.
*/
int GeoMap::lat2tile(double lat, int zoom) {
// Convert latitude from degrees to radians for trigonometric functions
double lat_rad = lat * M_PI / 180.0;
// Perform the Mercator projection calculation
return (int)floor((1.0 - log(tan(lat_rad) + 1.0 / cos(lat_rad)) / M_PI) / 2.0 * pow(2.0, zoom));
}
void GeoMap::set_osm_max_zoom() {
if (map_osm_zoom > 20) map_osm_zoom = 20;
for (uint8_t i = map_osm_zoom; i > 0; i--) {
int tile_x = lon2tile(lon_, i);
int tile_y = lat2tile(lat_, i);
std::string filename = "/OSM/" + to_string_dec_int(i) + "/" + to_string_dec_int(tile_x) + "/" + to_string_dec_int(tile_y) + ".bmp";
std::filesystem::path file_path(filename);
if (file_exists(file_path)) {
map_osm_zoom = i;
return;
}
}
map_osm_zoom = 0; // should not happen
}
// checks if the tile file presents or not. to determine if we got osm or not
uint8_t GeoMap::find_osm_file_tile() {
std::string filename = "/OSM/" + to_string_dec_int(0) + "/" + to_string_dec_int(0) + "/" + to_string_dec_int(0) + ".bmp";
std::filesystem::path file_path(filename);
if (file_exists(file_path)) return 1;
return 0; // not found
}
// Converts latitude/longitude to pixel coordinates in binary map file.
// (Note that when map_zoom==1, one pixel in map file corresponds to 1 pixel on screen)
GeoPoint GeoMap::lat_lon_to_map_pixel(float lat, float lon) {
// Using WGS 84/Pseudo-Mercator projection
@@ -310,9 +362,7 @@ GeoPoint GeoMap::lat_lon_to_map_pixel(float lat, float lon) {
}
// Draw grid in place of map (when zoom-in level is too high).
void GeoMap::draw_map_grid() {
const auto r = screen_rect();
void GeoMap::draw_map_grid(ui::Rect r) {
// Grid spacing is just based on zoom at the moment, and centered on screen.
// TODO: Maybe align with latitude/longitude seconds instead?
int grid_spacing = map_zoom * 2;
@@ -332,60 +382,184 @@ void GeoMap::draw_map_grid() {
}
}
double GeoMap::tile_pixel_x_to_lon(int x, int zoom) {
double map_width = pow(2.0, zoom) * TILE_SIZE;
return (x / map_width * 360.0) - 180.0;
}
double GeoMap::tile_pixel_y_to_lat(int y, int zoom) {
double map_height = pow(2.0, zoom) * TILE_SIZE;
double n = M_PI * (1.0 - 2.0 * y / map_height);
return atan(sinh(n)) * 180.0 / M_PI;
}
double GeoMap::lon_to_pixel_x_tile(double lon, int zoom) {
return ((lon + 180.0) / 360.0) * pow(2.0, zoom) * TILE_SIZE;
}
double GeoMap::lat_to_pixel_y_tile(double lat, int zoom) {
double lat_rad = lat * M_PI / 180.0;
double sin_lat = sin(lat_rad);
return ((1.0 - log((1.0 + sin_lat) / (1.0 - sin_lat)) / (2.0 * M_PI)) / 2.0) * pow(2.0, zoom) * TILE_SIZE;
}
bool GeoMap::draw_osm_file(int zoom, int tile_x, int tile_y, int relative_x, int relative_y) {
const auto r = screen_rect();
// Early exit if the tile is completely outside the viewport
if (relative_x >= r.width() || relative_y >= r.height() ||
relative_x + TILE_SIZE <= 0 || relative_y + TILE_SIZE <= 0) {
return true;
}
BMPFile bmp{};
bmp.open("/OSM/" + to_string_dec_int(zoom) + "/" + to_string_dec_int(tile_x) + "/" + to_string_dec_int(tile_y) + ".bmp", true);
// 1. Define the source and destination areas, starting with the full tile.
int src_x = 0;
int src_y = 0;
int dest_x = relative_x;
int dest_y = relative_y;
int clip_w = TILE_SIZE;
int clip_h = TILE_SIZE;
// 2. Clip left edge
if (dest_x < 0) {
src_x = -dest_x;
clip_w += dest_x;
dest_x = 0;
}
// 3. Clip top edge
if (dest_y < 0) {
src_y = -dest_y;
clip_h += dest_y;
dest_y = 0;
}
// 4. Clip right edge
if (dest_x + clip_w > r.width()) {
clip_w = r.width() - dest_x;
}
// 5. Clip bottom edge
if (dest_y + clip_h > r.height()) {
clip_h = r.height() - dest_y;
}
// 6. If clipping resulted in no visible area, we're done.
if (clip_w <= 0 || clip_h <= 0) {
return true;
}
if (!bmp.is_loaded()) {
// Draw an error rectangle using the calculated clipped dimensions
ui::Rect error_rect{{dest_x + r.left(), dest_y + r.top()}, {clip_w, clip_h}};
display.fill_rectangle(error_rect, Theme::getInstance()->bg_darkest->background);
return false;
}
std::vector<ui::Color> line(clip_w);
for (int y = 0; y < clip_h; ++y) {
int source_row = src_y + y;
int dest_row = dest_y + y;
bmp.seek(src_x, source_row);
for (int x = 0; x < clip_w; ++x) {
bmp.read_next_px(line[x], true);
}
display.draw_pixels({dest_x + r.left(), dest_row + r.top(), clip_w, 1}, line);
}
return true;
}
void GeoMap::paint(Painter& painter) {
const auto r = screen_rect();
std::array<ui::Color, geomap_rect_width> map_line_buffer;
int16_t zoom_seek_x, zoom_seek_y;
// Ony redraw map if it moved by at least 1 pixel or the markers list was updated
if (map_zoom <= 1) {
// Zooming out, or no zoom
const int min_diff = abs(map_zoom);
if ((int)abs(x_pos - prev_x_pos) >= min_diff)
redraw_map = true;
else if ((int)abs(y_pos - prev_y_pos) >= min_diff)
redraw_map = true;
if (!use_osm) {
// Ony redraw map if it moved by at least 1 pixel or the markers list was updated
if (map_zoom <= 1) {
// Zooming out, or no zoom
const int min_diff = abs(map_zoom);
if ((int)abs(x_pos - prev_x_pos) >= min_diff)
redraw_map = true;
else if ((int)abs(y_pos - prev_y_pos) >= min_diff)
redraw_map = true;
} else {
// Zooming in; magnify position differences (utilizing zoom_pixel_offset)
if ((int)(abs(x_pos - prev_x_pos) * map_zoom) >= 1)
redraw_map = true;
else if ((int)(abs(y_pos - prev_y_pos) * map_zoom) >= 1)
redraw_map = true;
}
} else {
// Zooming in; magnify position differences (utilizing zoom_pixel_offset)
if ((int)(abs(x_pos - prev_x_pos) * map_zoom) >= 1)
redraw_map = true;
else if ((int)(abs(y_pos - prev_y_pos) * map_zoom) >= 1)
redraw_map = true;
// using osm; needs to be stricter with the redraws, it'll be checked on move
}
if (redraw_map) {
prev_x_pos = x_pos; // Note x_pos/y_pos pixel position in map file now correspond to screen rect CENTER pixel
prev_y_pos = y_pos;
redraw_map = false;
// Adjust starting corner position of map per zoom setting;
// When zooming in the map should technically by shifted left & up by another map_zoom/2 pixels but
// the map_read_line() function doesn't handle that yet so we're adjusting markers instead (see zoom_pixel_offset).
if (map_zoom > 1) {
zoom_seek_x = x_pos - (float)r.width() / (2 * map_zoom);
zoom_seek_y = y_pos - (float)r.height() / (2 * map_zoom);
} else {
zoom_seek_x = x_pos - (r.width() * abs(map_zoom)) / 2;
zoom_seek_y = y_pos - (r.height() * abs(map_zoom)) / 2;
}
if (map_visible) {
// Read from map file and display to zoomed scale
int duplicate_lines = (map_zoom < 0) ? 1 : map_zoom;
for (uint16_t line = 0; line < (r.height() / duplicate_lines); line++) {
uint16_t seek_line = zoom_seek_y + ((map_zoom >= 0) ? line : line * (-map_zoom));
map_file.seek(4 + ((zoom_seek_x + (map_width * seek_line)) << 1));
map_read_line(map_line_buffer.data(), r.width());
if (!use_osm) {
prev_x_pos = x_pos; // Note x_pos/y_pos pixel position in map file now correspond to screen rect CENTER pixel
prev_y_pos = y_pos;
// Adjust starting corner position of map per zoom setting;
// When zooming in the map should technically by shifted left & up by another map_zoom/2 pixels but
// the map_read_line_bin() function doesn't handle that yet so we're adjusting markers instead (see zoom_pixel_offset).
if (map_zoom > 1) {
zoom_seek_x = x_pos - (float)r.width() / (2 * map_zoom);
zoom_seek_y = y_pos - (float)r.height() / (2 * map_zoom);
} else {
zoom_seek_x = x_pos - (r.width() * abs(map_zoom)) / 2;
zoom_seek_y = y_pos - (r.height() * abs(map_zoom)) / 2;
}
// Read from map file and display to zoomed scale
int duplicate_lines = (map_zoom < 0) ? 1 : map_zoom;
for (uint16_t line = 0; line < (r.height() / duplicate_lines); line++) {
uint16_t seek_line = zoom_seek_y + ((map_zoom >= 0) ? line : line * (-map_zoom));
map_file.seek(4 + ((zoom_seek_x + (map_width * seek_line)) << 1));
map_read_line_bin(map_line_buffer.data(), r.width());
for (uint16_t j = 0; j < duplicate_lines; j++) {
display.draw_pixels({0, r.top() + (line * duplicate_lines) + j, r.width(), 1}, map_line_buffer);
}
}
for (uint16_t j = 0; j < duplicate_lines; j++) {
display.draw_pixels({0, r.top() + (line * duplicate_lines) + j, r.width(), 1}, map_line_buffer);
} else {
// display osm tiles
// Convert center GPS to a global pixel coordinate
double global_center_px = lon_to_pixel_x_tile(lon_, map_osm_zoom);
double global_center_py = lat_to_pixel_y_tile(lat_, map_osm_zoom);
// Find the top-left corner of the screen (viewport) in global pixel coordinates
viewport_top_left_px = global_center_px - (r.width() / 2.0);
viewport_top_left_py = global_center_py - (r.height() / 2.0);
// Find the tile ID that contains the top-left corner of the viewport
int start_tile_x = floor(viewport_top_left_px / TILE_SIZE);
int start_tile_y = floor(viewport_top_left_py / TILE_SIZE);
// Calculate the crucial render offset.
// This determines how much the first tile is shifted to align the map correctly.
// This value will almost always be negative or zero.
double render_offset_x = -(viewport_top_left_px - (start_tile_x * TILE_SIZE));
double render_offset_y = -(viewport_top_left_py - (start_tile_y * TILE_SIZE));
// Determine how many tiles we need to draw to fill the screen
int tiles_needed_x = (r.width() / TILE_SIZE) + 2;
int tiles_needed_y = (r.height() / TILE_SIZE) + 2;
for (int y = 0; y < tiles_needed_y; ++y) {
for (int x = 0; x < tiles_needed_x; ++x) {
int current_tile_x = start_tile_x + x;
int current_tile_y = start_tile_y + y;
// Calculate the final on-screen drawing position for this tile.
// For the first tile (x=0, y=0), this will be the negative offset.
int draw_pos_x = round(render_offset_x + x * TILE_SIZE);
int draw_pos_y = round(render_offset_y + y * TILE_SIZE);
if (!draw_osm_file(map_osm_zoom, current_tile_x, current_tile_y, draw_pos_x, draw_pos_y)) {
// already blanked it.
}
}
}
}
} else {
// No map data or excessive zoom; just draw a grid
draw_map_grid();
draw_map_grid(r);
}
// Draw crosshairs in center in manual panning mode
if (manual_panning_) {
display.fill_rectangle({r.center() - Point(16, 1) + Point(zoom_pixel_offset, zoom_pixel_offset), {32, 2}}, Color::red());
@@ -394,8 +568,9 @@ void GeoMap::paint(Painter& painter) {
// Draw the other markers
draw_markers(painter);
draw_scale(painter);
if (!use_osm) draw_scale(painter);
draw_mypos(painter);
if (has_osm) draw_switcher(painter);
set_clean();
}
@@ -405,6 +580,12 @@ void GeoMap::paint(Painter& painter) {
}
}
void GeoMap::draw_switcher(Painter& painter) {
painter.fill_rectangle({screen_rect().left(), screen_rect().top(), 3 * 20, 20}, Theme::getInstance()->bg_darker->background);
std::string_view txt = (use_osm) ? "B I N" : "O S M";
painter.draw_string({screen_rect().left() + 5, screen_rect().top() + 2}, *Theme::getInstance()->fg_light, txt);
}
bool GeoMap::on_keyboard(KeyboardEvent key) {
if (key == '+' || key == ' ') return on_encoder(1);
if (key == '-') return on_encoder(-1);
@@ -413,11 +594,26 @@ bool GeoMap::on_keyboard(KeyboardEvent key) {
}
bool GeoMap::on_touch(const TouchEvent event) {
if (has_osm && event.type == TouchEvent::Type::Start && event.point.x() < screen_rect().left() + 3 * 20 && event.point.y() < screen_rect().top() + 20) {
use_osm = !use_osm;
move(lon_, lat_); // to re calculate the center for each map type
if (use_osm) set_osm_max_zoom();
redraw_map = true;
set_dirty();
return false; // false, because with true this hits 2 times
}
if ((event.type == TouchEvent::Type::Start) && (mode_ == PROMPT)) {
Point p;
set_highlighted(true);
if (on_move) {
Point p = event.point - screen_rect().center();
on_move(p.x() / 2.0 * lon_ratio, p.y() / 2.0 * lat_ratio);
if (!use_osm) {
p = event.point - screen_rect().center();
on_move(p.x() / 2.0 * lon_ratio, p.y() / 2.0 * lat_ratio, false);
} else {
p = event.point - screen_rect().location();
on_move(tile_pixel_x_to_lon(p.x() + viewport_top_left_px, map_osm_zoom), tile_pixel_y_to_lat(p.y() + viewport_top_left_py, map_osm_zoom), true);
}
return true;
}
}
@@ -426,25 +622,30 @@ bool GeoMap::on_touch(const TouchEvent event) {
void GeoMap::move(const float lon, const float lat) {
const auto r = screen_rect();
bool is_changed = (lon_ != lon || lat_ != lat);
lon_ = lon;
lat_ = lat;
if (!use_osm) {
// Calculate x_pos/y_pos in map file corresponding to CENTER pixel of screen rect
// (Note there is a 1:1 correspondence between map file pixels and screen pixels when map_zoom=1)
GeoPoint mapPoint = lat_lon_to_map_pixel(lat_, lon_);
x_pos = mapPoint.x;
y_pos = mapPoint.y;
// Cap position
if (x_pos > (map_width - r.width() / 2))
x_pos = map_width - r.width() / 2;
if (y_pos > (map_height + r.height() / 2))
y_pos = map_height - r.height() / 2;
// Calculate x_pos/y_pos in map file corresponding to CENTER pixel of screen rect
// (Note there is a 1:1 correspondence between map file pixels and screen pixels when map_zoom=1)
GeoPoint mapPoint = lat_lon_to_map_pixel(lat_, lon_);
x_pos = mapPoint.x;
y_pos = mapPoint.y;
// Cap position
if (x_pos > (map_width - r.width() / 2))
x_pos = map_width - r.width() / 2;
if (y_pos > (map_height + r.height() / 2))
y_pos = map_height - r.height() / 2;
// Scale calculation
float km_per_deg_lon = cos(lat * pi / 180) * 111.321; // 111.321 km/deg longitude at equator, and 0 km at poles
pixels_per_km = (r.width() / 2) / km_per_deg_lon;
// Scale calculation
float km_per_deg_lon = cos(lat * pi / 180) * 111.321; // 111.321 km/deg longitude at equator, and 0 km at poles
pixels_per_km = (r.width() / 2) / km_per_deg_lon;
} else {
if (is_changed) {
set_osm_max_zoom();
redraw_map = true;
}
}
}
bool GeoMap::init() {
@@ -459,7 +660,7 @@ bool GeoMap::init() {
map_height = 32768;
}
map_visible = map_opened;
map_visible = map_opened || has_osm;
map_center_x = map_width >> 1;
map_center_y = map_height >> 1;
@@ -469,7 +670,6 @@ bool GeoMap::init() {
map_bottom = sin(-85.05 * pi / 180); // Map bitmap only goes from about -85 to 85 lat
map_world_lon = map_width / (2 * pi);
map_offset = (map_world_lon / 2 * log((1 + map_bottom) / (1 - map_bottom)));
return map_opened;
}
@@ -573,15 +773,22 @@ void GeoMap::clear_markers() {
MapMarkerStored GeoMap::store_marker(GeoMarker& marker) {
const auto r = screen_rect();
MapMarkerStored ret;
// Check if it could be on screen
// (Shows more distant planes when zoomed out)
GeoPoint mapPoint = lat_lon_to_map_pixel(marker.lat, marker.lon);
int x_dist = abs((int)mapPoint.x - (int)x_pos);
int y_dist = abs((int)mapPoint.y - (int)y_pos);
int zoom_out = (map_zoom < 0) ? -map_zoom : 1;
bool can_see_on_map = true;
if (!use_osm) {
GeoPoint mapPoint = lat_lon_to_map_pixel(marker.lat, marker.lon);
int x_dist = abs((int)mapPoint.x - (int)x_pos);
int y_dist = abs((int)mapPoint.y - (int)y_pos);
int zoom_out = (map_zoom < 0) ? -map_zoom : 1;
if ((x_dist >= (zoom_out * r.width() / 2)) || (y_dist >= (zoom_out * r.height() / 2))) {
can_see_on_map = false;
}
} else {
// osm is so zoomable, we try to store all possible. maybe need to refine it a bit more later
}
if ((x_dist >= (zoom_out * r.width() / 2)) || (y_dist >= (zoom_out * r.height() / 2))) {
if (!can_see_on_map) {
ret = MARKER_NOT_STORED;
} else if (markerListLen < NumMarkerListElements) {
markerList[markerListLen] = marker;
@@ -595,24 +802,29 @@ MapMarkerStored GeoMap::store_marker(GeoMarker& marker) {
}
void GeoMap::update_my_position(float lat, float lon, int32_t altitude) {
bool is_changed = (my_pos.lat != lat) || (my_pos.lon != lon);
my_pos.lat = lat;
my_pos.lon = lon;
my_altitude = altitude;
redraw_map = true;
redraw_map = is_changed;
set_dirty();
}
void GeoMap::update_my_orientation(uint16_t angle, bool refresh) {
bool is_changed = (my_pos.angle != angle);
my_pos.angle = angle;
if (refresh) {
if (refresh && is_changed) {
redraw_map = true;
set_dirty();
}
}
MapType GeoMap::get_map_type() {
return use_osm ? MAP_TYPE_OSM : MAP_TYPE_BIN;
}
void GeoMapView::focus() {
geopos.focus();
if (!geomap.map_file_opened())
nav_.display_modal("No map", "No world_map.bin file in\n/" + adsb_dir.string() + "/ directory", ABORT);
}
@@ -629,7 +841,7 @@ void GeoMapView::update_position(float lat, float lon, uint16_t angle, int32_t a
geomap.set_dirty();
return;
}
bool is_changed = lat_ != lat || lon_ != lon || altitude_ != altitude || speed_ != speed || angle_ != angle;
lat_ = lat;
lon_ = lon;
altitude_ = altitude;
@@ -644,7 +856,7 @@ void GeoMapView::update_position(float lat, float lon, uint16_t angle, int32_t a
geopos.set_report_change(true);
geomap.set_angle(angle);
geomap.move(lon_, lat_);
if (is_changed) geomap.move(lon_, lat_);
geomap.set_dirty();
}
@@ -660,19 +872,25 @@ void GeoMapView::setup() {
geopos.set_lon(lon_);
geopos.on_change = [this](int32_t altitude, float lat, float lon, int32_t speed) {
bool is_changed = (altitude_ != altitude) || (lat_ != lat) || (lon_ != lon) || (speed_ != speed);
altitude_ = altitude;
lat_ = lat;
lon_ = lon;
speed_ = speed;
geopos.hide_altandspeed();
geomap.set_manual_panning(true);
geomap.move(lon_, lat_);
if (is_changed) geomap.move(lon_, lat_);
geomap.set_dirty();
};
geomap.on_move = [this](float move_x, float move_y) {
lon_ += move_x;
lat_ += move_y;
geomap.on_move = [this](float move_x, float move_y, bool absolute) {
if (absolute) {
lon_ = move_x;
lat_ = move_y;
} else {
lon_ += move_x;
lat_ += move_y;
}
// Stupid hack to avoid an event loop
geopos.set_report_change(false);
@@ -769,4 +987,8 @@ MapMarkerStored GeoMapView::store_marker(GeoMarker& marker) {
return geomap.store_marker(marker);
}
MapType GeoMapView::get_map_type() {
return geomap.get_map_type();
}
} /* namespace ui */
+33 -5
View File
@@ -27,6 +27,8 @@
#include "ui.hpp"
#include "file.hpp"
#include "ui_navigation.hpp"
#include "bmpfile.hpp"
#include "mathdef.hpp"
#include "portapack.hpp"
@@ -43,6 +45,8 @@ namespace ui {
#define GEOMAP_RECT_WIDTH 240
#define GEOMAP_RECT_HEIGHT (320 - 16 - GEOMAP_BANNER_HEIGHT)
#define TILE_SIZE 256
enum GeoMapMode {
DISPLAY,
PROMPT
@@ -193,9 +197,14 @@ enum MapMarkerStored {
MARKER_LIST_FULL
};
enum MapType {
MAP_TYPE_OSM,
MAP_TYPE_BIN
};
class GeoMap : public Widget {
public:
std::function<void(float, float)> on_move{};
std::function<void(float, float, bool)> on_move{};
GeoMap(Rect parent_rect);
@@ -216,6 +225,7 @@ class GeoMap : public Widget {
void set_tag(std::string new_tag) {
tag_ = new_tag;
}
MapType get_map_type();
void set_angle(uint16_t new_angle) {
angle_ = new_angle;
@@ -246,8 +256,22 @@ class GeoMap : public Widget {
void draw_markers(Painter& painter);
void draw_mypos(Painter& painter);
void draw_bearing(const Point origin, const uint16_t angle, uint32_t size, const Color color);
void draw_map_grid();
void map_read_line(ui::Color* buffer, uint16_t pixels);
void draw_map_grid(ui::Rect r);
void draw_switcher(Painter& painter);
void map_read_line_bin(ui::Color* buffer, uint16_t pixels);
// open street map related
uint8_t find_osm_file_tile();
void set_osm_max_zoom();
bool draw_osm_file(int zoom, int tile_x, int tile_y, int relative_x, int relative_y);
int lon2tile(double lon, int zoom);
int lat2tile(double lat, int zoom);
double lon_to_pixel_x_tile(double lon, int zoom);
double lat_to_pixel_y_tile(double lat, int zoom);
double tile_pixel_x_to_lon(int x, int zoom);
double tile_pixel_y_to_lat(int y, int zoom);
uint8_t map_osm_zoom{3};
double viewport_top_left_px = 0;
double viewport_top_left_py = 0;
bool manual_panning_{false};
bool hide_center_marker_{false};
@@ -258,11 +282,11 @@ class GeoMap : public Widget {
uint16_t map_width{}, map_height{};
int32_t map_center_x{}, map_center_y{};
int16_t map_zoom{1};
float lon_ratio{}, lat_ratio{};
double map_bottom{};
double map_world_lon{};
double map_offset{};
float x_pos{}, y_pos{};
float prev_x_pos{32767.0f}, prev_y_pos{32767.0f};
float lat_{};
@@ -278,7 +302,9 @@ class GeoMap : public Widget {
int markerListLen{0};
GeoMarker markerList[NumMarkerListElements];
bool redraw_map{false};
bool redraw_map{true};
bool use_osm{false};
bool has_osm{false};
};
class GeoMapView : public View {
@@ -313,6 +339,8 @@ class GeoMapView : public View {
void update_my_position(float lat, float lon, int32_t altitude);
void update_my_orientation(uint16_t angle, bool refresh = false);
MapType get_map_type();
std::string title() const override { return "Map view"; };
void clear_markers();
+1 -1
View File
@@ -1,6 +1,6 @@
/*
* Copyright (C) 2015 Jared Boone, ShareBrained Technology, Inc.
* Copyleft Mr. Robot 2025
* copyleft 2025 zxkmm AKA zix aka sommermorgentraum
*
* This file is part of PortaPack.
*
+1 -1
View File
@@ -1,6 +1,6 @@
/*
* Copyright (C) 2015 Jared Boone, ShareBrained Technology, Inc.
* Copyleft Mr. Robot 2025
* copyleft 2025 zxkmm AKA zix aka sommermorgentraum
*
* This file is part of PortaPack.
*
@@ -347,7 +347,7 @@ namespace ui {
return false;
// TODO: move this to m4 memory space
auto app_image = reinterpret_cast<uint8_t*>(portapack::memory::map::m4_code.end() - app.size());
auto app_image = reinterpret_cast<uint8_t*>(portapack::memory::map::local_sram_0.base());
// read file in 512 byte chunks
for (size_t file_read_index = 0; file_read_index < app.size(); file_read_index += std::filesystem::max_file_block_size) {
+1 -1
View File
@@ -2,7 +2,7 @@
* Copyright (C) 2015 Jared Boone, ShareBrained Technology, Inc.
* Copyright (C) 2016 Furrtek
* Copyright (C) 2024 u-foka
* Copyleft (ɔ) 2024 zxkmm under GPL license
* copyleft 2024 zxkmm AKA zix aka sommermorgentraum
*
* This file is part of PortaPack.
*
+1 -1
View File
@@ -2,7 +2,7 @@
* Copyright (C) 2015 Jared Boone, ShareBrained Technology, Inc.
* Copyright (C) 2016 Furrtek
* Copyright (C) 2024 u-foka
* Copyleft (ɔ) 2024 zxkmm under GPL license
* copyleft 2024 zxkmm AKA zix aka sommermorgentraum
*
* This file is part of PortaPack.
*
+1 -3
View File
@@ -1,7 +1,5 @@
/*
* Copyright (C) 2015 Jared Boone, ShareBrained Technology, Inc.
* Copyright (C) 2017 Furrtek
* Copyleft (ɔ) 2024 zxkmm with the GPL license
* copyleft 2024 zxkmm AKA zix aka sommermorgentraum
* Copyright (C) 2024 HTotoo
*
* This file is part of PortaPack.
+1 -3
View File
@@ -1,7 +1,5 @@
/*
* Copyright (C) 2015 Jared Boone, ShareBrained Technology, Inc.
* Copyright (C) 2017 Furrtek
* Copyleft (ɔ) 2024 zxkmm with the GPL license
* copyleft 2024 zxkmm AKA zix aka sommermorgentraum
* Copyright (C) 2024 HTotoo
*
* This file is part of PortaPack.
+1
View File
@@ -86,6 +86,7 @@ void EPIRBProcessor::payload_handler(const baseband::Packet& packet) {
}
void EPIRBProcessor::on_message(const Message* const message) {
(void)message; // Unused in this processor
}
int main() {
+1 -3
View File
@@ -33,9 +33,7 @@
#include <cstdint>
#include <cstddef>
#ifndef M_PI
#define M_PI 3.14159265358979323846
#endif
#include "mathdef.hpp"
float FSKRxProcessor::detect_peak_power(const buffer_c8_t& buffer, int N) {
int32_t power = 0;
+1 -3
View File
@@ -30,9 +30,7 @@
#include <random>
#include <cmath>
#ifndef M_PI
#define M_PI 3.14159265358979323846f
#endif
#include "mathdef.hpp"
void JammerProcessor::execute(const buffer_c8_t& buffer) {
if (!configured) return;
+1 -1
View File
@@ -91,7 +91,7 @@ const float adsb_lat_lut[58] = {
83.07199445, 83.99173563, 84.89166191, 85.75541621,
86.53536998, 87.00000000};
const float PI = 3.14159265358979323846;
#include "mathdef.hpp"
const float NZ = 15.0;
+57 -36
View File
@@ -52,10 +52,10 @@ void BMPFile::close() {
bmpimage.close();
}
// creates a new bmp file. for now, hardcoded to 3 byte colour depth
// creates a new bmp file. hardcoded to 3 byte (24-bit) colour depth
bool BMPFile::create(const std::filesystem::path& file, uint32_t x, uint32_t y) {
is_opened = false;
is_read_ony = true;
is_read_only = true;
bmpimage.close(); // if already open, close before open a new
if (file_exists(file)) {
delete_file(file); // overwrite
@@ -85,7 +85,7 @@ bool BMPFile::create(const std::filesystem::path& file, uint32_t x, uint32_t y)
bmpimage.write(&bmp_header, sizeof(bmp_header_t));
file_pos = bmp_header.image_data;
is_opened = true;
is_read_ony = false;
is_read_only = false;
if (!expand_y(y)) return false; // will fill with 0, and update header data
seek(0, 0);
return true;
@@ -94,30 +94,36 @@ bool BMPFile::create(const std::filesystem::path& file, uint32_t x, uint32_t y)
// opens the file and parses header data. return true on success
bool BMPFile::open(const std::filesystem::path& file, bool readonly) {
is_opened = false;
is_read_ony = true;
bmpimage.close(); // if already open, close before open a new
is_read_only = true;
bmpimage.close();
auto result = bmpimage.open(file, readonly, false);
if (!result.value().ok()) return false;
file_pos = 0;
bmpimage.seek(file_pos);
auto read_size = bmpimage.read(&bmp_header, sizeof(bmp_header_t));
if (!((bmp_header.signature == 0x4D42) && // "BM" Signature
(bmp_header.planes == 1) && // Seems always to be 1
(bmp_header.compression == 0 || bmp_header.compression == 3))) { // No compression
bmpimage.read(&bmp_header, sizeof(bmp_header_t));
if (!((bmp_header.signature == 0x4D42) &&
(bmp_header.planes == 1) &&
(bmp_header.compression == 0 || bmp_header.compression == 3))) {
return false;
}
char buffer[257];
switch (bmp_header.bpp) {
case 8:
type = 4;
byte_per_px = 1;
// bmpimage.seek(sizeof(bmp_header_t));
// bmpimage.read(color_palette, 1024);
return false; // niy
break;
case 16:
file_pos = 0x36;
memset(buffer, 0, 16);
bmpimage.read(buffer, 16);
byte_per_px = 2;
if (buffer[1] == 0x7C)
type = 3; // A1R5G5B5
else
type = 0; // R5G6B5
type = 5;
if (bmp_header.compression == 3) {
return false;
} // niy
break;
case 24:
type = 1;
@@ -130,12 +136,11 @@ bool BMPFile::open(const std::filesystem::path& file, bool readonly) {
default:
// not supported
return false;
break;
}
byte_per_row = (bmp_header.width * byte_per_px % 4 == 0) ? bmp_header.width * byte_per_px : (bmp_header.width * byte_per_px + (4 - ((bmp_header.width * byte_per_px) % 4)));
byte_per_row = (bmp_header.width * byte_per_px + 3) & ~3;
file_pos = bmp_header.image_data;
is_opened = true;
is_read_ony = readonly;
is_read_only = readonly;
currx = 0;
curry = 0;
return true;
@@ -161,20 +166,34 @@ bool BMPFile::read_next_px(ui::Color& px, bool seek = true) {
auto res = bmpimage.read(buffer, byte_per_px);
if (res.is_error()) return false;
switch (type) {
case 0: // R5G6B5
case 3: // A1R5G5B5
if (!type)
px = ui::Color((uint16_t)buffer[0] | ((uint16_t)buffer[1] << 8));
else
px = ui::Color(((uint16_t)buffer[0] & 0x1F) | ((uint16_t)buffer[0] & 0xE0) << 1 | ((uint16_t)buffer[1] & 0x7F) << 9);
case 5: {
// ARGB1555
uint16_t val = buffer[0] | (buffer[1] << 8);
// Extract components
//*a = (val >> 15) & 0x01; // 1-bit alpha
uint8_t r = (val >> 10) & 0x1F; // 5-bit red
uint8_t g = (val >> 5) & 0x1F; // 5-bit green
uint8_t b = (val)&0x1F; // 5-bit blue
// expand
r = (r << 3) | (r >> 2);
g = (g << 3) | (g >> 2);
b = (b << 3) | (b >> 2);
px = ui::Color(r, g, b);
break;
}
case 2: // 32
px = ui::Color(buffer[2], buffer[1], buffer[0]);
break;
case 4: { // 8-bit
// uint8_t index = buffer[0];
// px = ui::Color(color_palette[index][2], color_palette[index][1], color_palette[index][0]); // Palette is BGR
// px = ui::Color(buffer[0]); // niy, since needs a lot of ram for the palette
break;
}
case 1: // 24
default:
px = ui::Color(buffer[2], buffer[1], buffer[0]);
break;
case 2: // 32
px = ui::Color(buffer[2], buffer[1], buffer[0]);
break;
}
if (seek) advance_curr_px();
@@ -188,23 +207,23 @@ void BMPFile::set_bg_color(ui::Color background) {
}
// delete bg color. default. creation or expansion will be fast, but the file will contain random garbage. no problem if you write all pixels later.
void BMPFile::delete_db_color() {
void BMPFile::delete_bg_color() {
use_bg = false;
}
// writes a color data to the current position, and advances 1 px. true on success, false on error
bool BMPFile::write_next_px(ui::Color& px) {
if (!is_opened) return false;
if (is_read_ony) return false;
if (!is_opened || is_read_only) return false;
uint8_t buffer[4];
switch (type) {
case 5:
case 0: // R5G6B5
case 3: // A1R5G5B5
if (!type) {
buffer[0] = (px.r() << 3) | (px.g() >> 3); // todo test in future
buffer[0] = (px.r() << 3) | (px.g() >> 3);
buffer[1] = (px.g() << 5) | px.b();
} else {
buffer[0] = (1 << 7) | (px.r() << 2) | (px.g() >> 3); // todo test in future
buffer[0] = (1 << 7) | (px.r() << 2) | (px.g() >> 3);
buffer[1] = (px.g() << 5) | px.b();
}
break;
@@ -220,6 +239,8 @@ bool BMPFile::write_next_px(ui::Color& px) {
buffer[0] = px.b();
buffer[3] = 255;
break;
case 4: // 8-bit
return false;
}
auto res = bmpimage.write(buffer, byte_per_px);
if (res.is_error()) return false;
@@ -260,7 +281,7 @@ bool BMPFile::expand_y(uint32_t new_y) {
if (!is_opened) return false; // not yet opened
uint32_t old_height = get_real_height();
if (new_y < old_height) return true; // already bigger
if (is_read_ony) return false; // can't expand
if (is_read_only) return false; // can't expand
uint32_t delta = (new_y - old_height) * byte_per_row;
bmp_header.size += delta;
bmp_header.data_size += delta;
+3 -2
View File
@@ -47,12 +47,12 @@ class BMPFile {
uint32_t get_width();
bool is_bottomup();
void set_bg_color(ui::Color background);
void delete_db_color();
void delete_bg_color();
private:
bool advance_curr_px(uint32_t num);
bool is_opened = false;
bool is_read_ony = true;
bool is_read_only = true;
File bmpimage{};
size_t file_pos = 0;
@@ -64,6 +64,7 @@ class BMPFile {
uint32_t currx = 0;
uint32_t curry = 0;
ui::Color bg{};
// uint8_t color_palette[256][4];
bool use_bg = false;
};
+3 -1
View File
@@ -26,7 +26,9 @@
#include <complex>
#include <cmath>
constexpr float pi{3.141592653589793238462643383279502884f};
#include "mathdef.hpp"
constexpr float pi{M_PI};
namespace std {
+1 -1
View File
@@ -1,7 +1,7 @@
/*
* Copyright (C) 2014 Jared Boone, ShareBrained Technology, Inc.
* Copyright (C) 2016 Furrtek
* copyleft 2025 Whiterose of the Dark Army
* copyleft 2024 zxkmm AKA zix aka sommermorgentraum
*
* This file is part of PortaPack.
*
+4
View File
@@ -0,0 +1,4 @@
#pragma once
#define PI 3.1415926535897932384626433832795
#define M_PI PI
+1 -1
View File
@@ -1,6 +1,6 @@
/*
* Copyright (C) 2014 Jared Boone, ShareBrained Technology, Inc.
* Copyleft (ɔ) 2024 zxkmm under GPL license
* copyleft 2024 zxkmm AKA zix aka sommermorgentraum
*
* This file is part of PortaPack.
*
+1 -1
View File
@@ -1,6 +1,6 @@
/*
* Copyright (C) 2014 Jared Boone, ShareBrained Technology, Inc.
* Copyleft (ɔ) 2024 zxkmm under GPL license
* copyleft 2024 zxkmm AKA zix aka sommermorgentraum
* Copyright (C) 2024 u-foka
* Copyright (C) 2024 Mark Thompson
*
+1 -1
View File
@@ -47,7 +47,7 @@ static uint8_t calfrchk[51]; // so subframes are preserved while populate
#define pos_GPSecefY 0x114 // 0x118 // 4 byte (not actually used since Y and Z are following X, and grabbed in that same loop)
#define pos_GPSecefZ 0x118 // 0x11C // 4 byte (same as Y)
#define PI 3.1415926535897932384626433832795 // 3.1416 //(3.1415926535897932384626433832795)
#include "mathdef.hpp"
Packet::Packet(
const baseband::Packet& packet,
+53 -2
View File
@@ -27,8 +27,9 @@
#include <stddef.h>
#include "ui.hpp"
#include "file.hpp"
#define CURRENT_STANDALONE_APPLICATION_API_VERSION 2
#define CURRENT_STANDALONE_APPLICATION_API_VERSION 4
struct standalone_application_api_t {
// Version 1
@@ -58,7 +59,57 @@ struct standalone_application_api_t {
void (*panic)(const char* msg);
void (*set_dirty)();
// TODO: add filesystem access functions
// Version 3
FRESULT(*f_open)
(FIL* fp, const TCHAR* path, BYTE mode);
FRESULT(*f_close)
(FIL* fp);
FRESULT(*f_read)
(FIL* fp, void* buff, UINT btr, UINT* br);
FRESULT(*f_write)
(FIL* fp, const void* buff, UINT btw, UINT* bw);
FRESULT(*f_lseek)
(FIL* fp, FSIZE_t ofs);
FRESULT(*f_truncate)
(FIL* fp);
FRESULT(*f_sync)
(FIL* fp);
FRESULT(*f_opendir)
(DIR* dp, const TCHAR* path);
FRESULT(*f_closedir)
(DIR* dp);
FRESULT(*f_readdir)
(DIR* dp, FILINFO* fno);
FRESULT(*f_findfirst)
(DIR* dp, FILINFO* fno, const TCHAR* path, const TCHAR* pattern);
FRESULT(*f_findnext)
(DIR* dp, FILINFO* fno);
FRESULT(*f_mkdir)
(const TCHAR* path);
FRESULT(*f_unlink)
(const TCHAR* path);
FRESULT(*f_rename)
(const TCHAR* path_old, const TCHAR* path_new);
FRESULT(*f_stat)
(const TCHAR* path, FILINFO* fno);
FRESULT(*f_utime)
(const TCHAR* path, const FILINFO* fno);
FRESULT(*f_getfree)
(const TCHAR* path, DWORD* nclst, FATFS** fatfs);
FRESULT(*f_mount)
(FATFS* fs, const TCHAR* path, BYTE opt);
int (*f_putc)(TCHAR c, FIL* fp);
int (*f_puts)(const TCHAR* str, FIL* cp);
int (*f_printf)(FIL* fp, const TCHAR* str, ...);
TCHAR* (*f_gets)(TCHAR* buff, int len, FIL* fp);
void (*draw_pixels)(const ui::Rect r, const ui::Color* const colors, const size_t count);
void (*draw_pixel)(const ui::Point p, const ui::Color color);
void (*exit_app)();
// Version 4
uint16_t* screen_height;
uint16_t* screen_width;
// TODO: add baseband access functions
// HOW TO extend this interface:
+1 -1
View File
@@ -1,6 +1,6 @@
/*
* Copyright (C) 2014 Jared Boone, ShareBrained Technology, Inc.
* copyleft Mr. Robot
* copyleft 2024 zxkmm AKA zix aka sommermorgentraum
*
* This file is part of PortaPack.
*
+1 -1
View File
@@ -1,6 +1,6 @@
/*
* Copyright (C) 2014 Jared Boone, ShareBrained Technology, Inc.
* copyleft Mr. Robot
* copyleft 2024 zxkmm AKA zix aka sommermorgentraum
*
* This file is part of PortaPack.
*
+2 -1
View File
@@ -3,9 +3,10 @@ cmake_minimum_required(VERSION 3.16)
project(standalone_apps)
add_subdirectory(pacman)
add_subdirectory(digitalrain)
add_custom_target(
standalone_apps
DEPENDS pacman_app
DEPENDS pacman_app digitalrain_app
)
+53
View File
@@ -0,0 +1,53 @@
/*
* Copyright (C) 2014 Jared Boone, ShareBrained Technology, Inc.
* Copyright (C) 2016 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.
*/
#pragma pack(push, 1)
struct bmp_header_t {
uint16_t signature;
uint32_t size;
uint16_t reserved_1;
uint16_t reserved_2;
uint32_t image_data;
uint32_t BIH_size;
uint32_t width;
int32_t height; // can be negative, to signal the bottom-up or reserve status
uint16_t planes;
uint16_t bpp;
uint32_t compression;
uint32_t data_size;
uint32_t h_res;
uint32_t v_res;
uint32_t colors_count;
uint32_t icolors_count;
};
#pragma pack(pop)
#pragma pack(push, 1)
struct bmp_palette_t {
struct color_t {
uint8_t B;
uint8_t G;
uint8_t R;
uint8_t A;
} color[16];
};
#pragma pack(pop)
+306
View File
@@ -0,0 +1,306 @@
/*
* Copyright (C) 2024 HTotoo
*
* 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 "bmpfile.hpp"
bool BMPFile::is_loaded() {
return is_opened;
}
// fix height info
uint32_t BMPFile::get_real_height() {
if (!is_opened) return 0;
return bmp_header.height >= 0 ? (uint32_t)bmp_header.height : (uint32_t)(-1 * bmp_header.height);
}
// get bmp width
uint32_t BMPFile::get_width() {
if (!is_opened) return 0;
return bmp_header.width;
}
// get if the rows are bottom up (for most bmp), or up to bottom (negative height, we use it for write)
bool BMPFile::is_bottomup() {
return (bmp_header.height >= 0);
}
BMPFile::~BMPFile() {
close();
}
// closes file
void BMPFile::close() {
is_opened = false;
bmpimage.close();
}
// creates a new bmp file. hardcoded to 3 byte (24-bit) colour depth
bool BMPFile::create(const std::filesystem::path& file, uint32_t x, uint32_t y) {
is_opened = false;
is_read_only = true;
bmpimage.close(); // if already open, close before open a new
if (file_exists(file)) {
delete_file(file); // overwrite
}
auto result = bmpimage.open(file, false, true);
if (!result.value().ok()) return false;
file_pos = 0;
byte_per_row = (x * 3 % 4 == 0) ? x * 3 : (x * 3 + (4 - ((x * 3) % 4))); // with padding
bmpimage.seek(file_pos);
bmp_header.signature = 0x4D42;
bmp_header.planes = 1;
bmp_header.compression = 0;
bmp_header.bpp = 24; // 3 byte depth
bmp_header.width = x;
bmp_header.height = 0; // for now, will expand
bmp_header.image_data = 0x36;
bmp_header.BIH_size = 0x28;
bmp_header.h_res = 100;
bmp_header.v_res = 100;
byte_per_px = 3;
type = 1;
bmp_header.size = sizeof(bmp_header) + get_real_height() * byte_per_row; // with padding! --will update later with expand
bmp_header.data_size = bmp_header.size - sizeof(bmp_header_t);
bmp_header.colors_count = 0;
bmp_header.icolors_count = 0;
bmpimage.write(&bmp_header, sizeof(bmp_header_t));
file_pos = bmp_header.image_data;
is_opened = true;
is_read_only = false;
if (!expand_y(y)) return false; // will fill with 0, and update header data
seek(0, 0);
return true;
}
// opens the file and parses header data. return true on success
bool BMPFile::open(const std::filesystem::path& file, bool readonly) {
is_opened = false;
is_read_only = true;
bmpimage.close();
auto result = bmpimage.open(file, readonly, false);
if (!result.value().ok()) return false;
file_pos = 0;
bmpimage.seek(file_pos);
bmpimage.read(&bmp_header, sizeof(bmp_header_t));
if (!((bmp_header.signature == 0x4D42) &&
(bmp_header.planes == 1) &&
(bmp_header.compression == 0 || bmp_header.compression == 3))) {
return false;
}
switch (bmp_header.bpp) {
case 8:
type = 4;
byte_per_px = 1;
// bmpimage.seek(sizeof(bmp_header_t));
// bmpimage.read(color_palette, 1024);
return false; // niy
break;
case 16:
byte_per_px = 2;
type = 5;
if (bmp_header.compression == 3) {
return false;
} // niy
break;
case 24:
type = 1;
byte_per_px = 3;
break;
case 32:
type = 2;
byte_per_px = 4;
break;
default:
// not supported
return false;
}
byte_per_row = (bmp_header.width * byte_per_px + 3) & ~3;
file_pos = bmp_header.image_data;
is_opened = true;
is_read_only = readonly;
currx = 0;
curry = 0;
return true;
}
// jumps to next pixel. false on the end
bool BMPFile::advance_curr_px(uint32_t num = 1) {
if (curry >= get_real_height()) return false;
uint32_t rowsToAdvance = (currx + num) / bmp_header.width;
uint32_t nx = (currx + num) % bmp_header.width;
uint32_t ny = curry + rowsToAdvance;
if (ny >= get_real_height()) {
return false;
}
seek(nx, ny);
return true;
}
// reads next px, then advance the pos (and seek). return false on error
bool BMPFile::read_next_px(ui::Color& px, bool seek = true) {
if (!is_opened) return false;
uint8_t buffer[4];
auto res = bmpimage.read(buffer, byte_per_px);
if (res.is_error()) return false;
switch (type) {
case 5: {
// ARGB1555
uint16_t val = buffer[0] | (buffer[1] << 8);
// Extract components
//*a = (val >> 15) & 0x01; // 1-bit alpha
uint8_t r = (val >> 10) & 0x1F; // 5-bit red
uint8_t g = (val >> 5) & 0x1F; // 5-bit green
uint8_t b = (val)&0x1F; // 5-bit blue
// expand
r = (r << 3) | (r >> 2);
g = (g << 3) | (g >> 2);
b = (b << 3) | (b >> 2);
px = ui::Color(r, g, b);
break;
}
case 2: // 32
px = ui::Color(buffer[2], buffer[1], buffer[0]);
break;
case 4: { // 8-bit
// uint8_t index = buffer[0];
// px = ui::Color(color_palette[index][2], color_palette[index][1], color_palette[index][0]); // Palette is BGR
// px = ui::Color(buffer[0]); // niy, since needs a lot of ram for the palette
break;
}
case 1: // 24
default:
px = ui::Color(buffer[2], buffer[1], buffer[0]);
break;
}
if (seek) advance_curr_px();
return true;
}
// if you set this, then the expanded part (or the newly created) will be filled with this color. but the expansion or the creation will be slower.
void BMPFile::set_bg_color(ui::Color background) {
bg = background;
use_bg = true;
}
// delete bg color. default. creation or expansion will be fast, but the file will contain random garbage. no problem if you write all pixels later.
void BMPFile::delete_bg_color() {
use_bg = false;
}
// writes a color data to the current position, and advances 1 px. true on success, false on error
bool BMPFile::write_next_px(ui::Color& px) {
if (!is_opened || is_read_only) return false;
uint8_t buffer[4];
switch (type) {
case 5:
case 0: // R5G6B5
case 3: // A1R5G5B5
if (!type) {
buffer[0] = (px.r() << 3) | (px.g() >> 3);
buffer[1] = (px.g() << 5) | px.b();
} else {
buffer[0] = (1 << 7) | (px.r() << 2) | (px.g() >> 3);
buffer[1] = (px.g() << 5) | px.b();
}
break;
case 1: // 24
default:
buffer[2] = px.r();
buffer[1] = px.g();
buffer[0] = px.b();
break;
case 2: // 32
buffer[2] = px.r();
buffer[1] = px.g();
buffer[0] = px.b();
buffer[3] = 255;
break;
case 4: // 8-bit
return false;
}
auto res = bmpimage.write(buffer, byte_per_px);
if (res.is_error()) return false;
advance_curr_px();
return true;
}
// positions in the file to the given pixel. 0 based indexing
bool BMPFile::seek(uint32_t x, uint32_t y) {
if (!is_opened) return false;
if (x >= bmp_header.width) return false;
if (y >= get_real_height()) return false;
if (!BMPFile::is_bottomup()) {
file_pos = bmp_header.image_data; // nav to start pos.
file_pos += y * byte_per_row;
file_pos += x * byte_per_px;
bmpimage.seek(file_pos);
currx = x;
curry = y;
} else {
file_pos = bmp_header.image_data; // nav to start pos.
file_pos += (get_real_height() - y - 1) * byte_per_row;
file_pos += x * byte_per_px;
bmpimage.seek(file_pos);
currx = x;
curry = y;
}
return true;
}
// expands the image with a delta (y). also seek's t it's begining. in bottumup format, it should be used carefully!
bool BMPFile::expand_y_delta(uint32_t delta_y) {
return expand_y(get_real_height() + delta_y);
}
// expands the image to a new y size. also seek's t it's begining. in bottumup format, it should be used carefully!
bool BMPFile::expand_y(uint32_t new_y) {
if (!is_opened) return false; // not yet opened
uint32_t old_height = get_real_height();
if (new_y < old_height) return true; // already bigger
if (is_read_only) return false; // can't expand
uint32_t delta = (new_y - old_height) * byte_per_row;
bmp_header.size += delta;
bmp_header.data_size += delta;
bmp_header.height = -1 * new_y; //-1*, so no bottom-up structure needed. easier to expand.
bmpimage.seek(0);
bmpimage.write(&bmp_header, sizeof(bmp_header)); // overwrite header
bmpimage.seek(bmp_header.size); // seek to new end to expand
// fill with bg color if needed
if (use_bg) {
seek(0, old_height); // to the new begin
size_t newpxcount = ((new_y - old_height) * bmp_header.width);
for (size_t i = 0; i < newpxcount; ++i)
write_next_px(bg);
}
if (is_bottomup()) {
seek(0, new_y - old_height); // seek to the new chunk begin
} else {
seek(0, curry + 1); // seek to the begin of the new chunk
}
return true;
}
+71
View File
@@ -0,0 +1,71 @@
/*
* Copyright (C) 2024 HTotoo
*
* 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 __BMPFILE__H
#define __BMPFILE__H
#include <cstring>
#include <string>
#include "file.hpp"
#include "bmp.hpp"
#include "ui.hpp"
class BMPFile {
public:
~BMPFile();
bool open(const std::filesystem::path& file, bool readonly);
bool create(const std::filesystem::path& file, uint32_t x, uint32_t y);
void close();
bool is_loaded();
bool seek(uint32_t x, uint32_t y);
bool expand_y(uint32_t new_y);
bool expand_y_delta(uint32_t delta_y);
uint32_t getbpr() { return byte_per_row; };
bool read_next_px(ui::Color& px, bool seek);
bool write_next_px(ui::Color& px);
uint32_t get_real_height();
uint32_t get_width();
bool is_bottomup();
void set_bg_color(ui::Color background);
void delete_bg_color();
private:
bool advance_curr_px(uint32_t num);
bool is_opened = false;
bool is_read_only = true;
File bmpimage{};
size_t file_pos = 0;
bmp_header_t bmp_header{};
uint8_t type = 0;
uint8_t byte_per_px = 1;
uint32_t byte_per_row = 0;
uint32_t currx = 0;
uint32_t curry = 0;
ui::Color bg{};
// uint8_t color_palette[256][4];
bool use_bg = false;
};
#endif
@@ -0,0 +1,120 @@
/*
* 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 __CIRCULAR_BUFFER_H__
#define __CIRCULAR_BUFFER_H__
#include <stddef.h> // For size_t
#include <memory>
/* Implements a fixed-size, circular buffer.
* NB: Holds Capacity - 1 items.
* There are no bounds checks on accessors so ensure there are
* items in the buffer before accessing front/back/operator[]. */
template <typename T, size_t Capacity>
class CircularBuffer {
public:
CircularBuffer() = default;
CircularBuffer(const CircularBuffer&) = delete;
CircularBuffer(CircularBuffer&&) = delete;
CircularBuffer& operator=(const CircularBuffer&) = delete;
CircularBuffer& operator=(CircularBuffer&&) = delete;
void push_front(T val) {
head_ = head_ > 0 ? head_ - 1 : last_index;
if (head_ == end_)
pop_back_internal();
data_[head_] = std::move(val);
}
void pop_front() {
if (!empty())
pop_front_internal();
}
void push_back(T val) {
data_[end_] = std::move(val);
end_ = end_ < last_index ? end_ + 1 : 0;
if (head_ == end_)
pop_front_internal();
}
void pop_back() {
if (!empty())
pop_back_internal();
}
T& operator[](size_t ix) & {
ix += head_;
if (ix >= Capacity)
ix -= Capacity;
return data_[ix];
}
const T& operator[](size_t ix) const& {
return const_cast<CircularBuffer*>(this)->operator[](ix);
}
const T& front() const& {
return data_[head_];
}
const T& back() const& {
auto end = end_ > 0 ? end_ - 1 : last_index;
return data_[end];
}
size_t size() const& {
auto end = end_;
if (end < head_)
end += Capacity;
return end - head_;
}
bool empty() const {
return head_ == end_;
}
void clear() {
head_ = 0;
end_ = 0;
}
private:
void pop_front_internal() {
head_ = head_ < last_index ? head_ + 1 : 0;
}
void pop_back_internal() {
end_ = end_ > 0 ? end_ - 1 : last_index;
}
static constexpr size_t last_index = Capacity - 1;
size_t head_{0};
size_t end_{0};
T data_[Capacity]{};
};
#endif /*__CIRCULAR_BUFFER_H__*/
+114
View File
@@ -0,0 +1,114 @@
/*
* Copyright (C) 2014 Jared Boone, ShareBrained Technology, Inc.
*
* 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 __COMPLEX_H__
#define __COMPLEX_H__
#include <cstdint>
#include <complex>
#include <cmath>
constexpr float pi{3.141592653589793238462643383279502884f};
namespace std {
template <>
struct complex<int8_t> {
public:
typedef int8_t value_type;
typedef uint16_t rep_type;
constexpr complex(
int8_t re = 0,
int8_t im = 0)
: _v{re, im} {
}
constexpr int8_t real() const { return _v[0]; }
constexpr int8_t imag() const { return _v[1]; }
void real(int8_t v) { _v[0] = v; }
void imag(int8_t v) { _v[1] = v; }
constexpr uint16_t __rep() const {
return _rep;
}
private:
union {
value_type _v[2];
rep_type _rep;
};
};
template <>
struct complex<int16_t> {
public:
typedef int16_t value_type;
typedef uint32_t rep_type;
constexpr complex(
int16_t re = 0,
int16_t im = 0)
: _v{re, im} {
}
constexpr int16_t real() const { return _v[0]; }
constexpr int16_t imag() const { return _v[1]; }
void real(int16_t v) { _v[0] = v; }
void imag(int16_t v) { _v[1] = v; }
template <class X>
complex<int16_t>& operator+=(const complex<X>& other) {
_v[0] += other.real();
_v[1] += other.imag();
return *this;
}
constexpr uint32_t __rep() const {
return _rep;
}
constexpr operator std::complex<float>() const {
return {
static_cast<float>(_v[0]),
static_cast<float>(_v[1])};
}
private:
union {
value_type _v[2];
rep_type _rep;
};
};
} /* namespace std */
using complex8_t = std::complex<int8_t>;
using complex16_t = std::complex<int16_t>;
using complex32_t = std::complex<int32_t>;
static_assert(sizeof(complex8_t) == 2, "complex8_t size wrong");
static_assert(sizeof(complex16_t) == 4, "complex16_t size wrong");
static_assert(sizeof(complex32_t) == 8, "complex32_t size wrong");
#endif /*__COMPLEX_H__*/
+641
View File
@@ -0,0 +1,641 @@
/*
* Copyright (C) 2015 Jared Boone, ShareBrained Technology, Inc.
* Copyright (C) 2016 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 "file.hpp"
#include "complex.hpp"
#include <algorithm>
#include <codecvt>
#include <cstring>
#include <locale>
namespace fs = std::filesystem;
static const fs::path c8_ext{u".C8"};
static const fs::path c16_ext{u".C16"};
Optional<File::Error> File::open_fatfs(const std::filesystem::path& filename, BYTE mode) {
auto result = f_open(&f, reinterpret_cast<const TCHAR*>(filename.c_str()), mode);
if (result == FR_OK) {
if (mode & FA_OPEN_ALWAYS) {
const auto result = f_lseek(&f, f_size(&f));
if (result != FR_OK) {
f_close(&f);
}
}
}
if (result == FR_OK) {
return {};
} else {
return {result};
}
}
/*
* @param read_only: open in readonly mode
* @param create: create if it doesnt exist
*/
Optional<File::Error> File::open(const std::filesystem::path& filename, bool read_only, bool create) {
BYTE mode = read_only ? FA_READ : FA_READ | FA_WRITE;
if (create)
mode |= FA_OPEN_ALWAYS;
return open_fatfs(filename, mode);
}
Optional<File::Error> File::append(const std::filesystem::path& filename) {
return open_fatfs(filename, FA_WRITE | FA_OPEN_ALWAYS);
}
Optional<File::Error> File::create(const std::filesystem::path& filename) {
return open_fatfs(filename, FA_WRITE | FA_CREATE_ALWAYS);
}
File::~File() {
f_close(&f);
}
void File::close() {
f_close(&f);
}
File::Result<File::Size> File::read(void* data, Size bytes_to_read) {
UINT bytes_read = 0;
const auto result = f_read(&f, data, bytes_to_read, &bytes_read);
if (result == FR_OK) {
return {static_cast<size_t>(bytes_read)};
} else {
return {static_cast<Error>(result)};
}
}
File::Result<File::Size> File::write(const void* data, Size bytes_to_write) {
UINT bytes_written = 0;
const auto result = f_write(&f, data, bytes_to_write, &bytes_written);
if (result == FR_OK) {
if (bytes_to_write == bytes_written) {
return {static_cast<File::Size>(bytes_written)};
} else {
return Error{FR_DISK_FULL};
}
} else {
return {static_cast<Error>(result)};
}
}
File::Offset File::tell() const {
return f_tell(&f);
}
File::Result<bool> File::eof() {
return f_eof(&f);
}
File::Result<File::Offset> File::seek(Offset new_position) {
/* NOTE: Returns *old* position, not new position */
const auto old_position = tell();
const auto result = (old_position == new_position) ? FR_OK : f_lseek(&f, new_position);
if (result != FR_OK) {
return {static_cast<Error>(result)};
}
if (f_tell(&f) != new_position) {
return {static_cast<Error>(FR_BAD_SEEK)};
}
return {static_cast<File::Offset>(old_position)};
}
File::Result<File::Offset> File::truncate() {
const auto position = f_tell(&f);
const auto result = f_truncate(&f);
if (result != FR_OK) {
return {static_cast<Error>(result)};
}
return {static_cast<File::Offset>(position)};
}
File::Size File::size() const {
return f_size(&f);
}
Optional<File::Error> File::write_line(const std::string& s) {
const auto result_s = write(s.c_str(), s.size());
if (result_s.is_error()) {
return {result_s.error()};
}
const auto result_crlf = write("\r\n", 2);
if (result_crlf.is_error()) {
return {result_crlf.error()};
}
return {};
}
Optional<File::Error> File::sync() {
const auto result = f_sync(&f);
if (result == FR_OK) {
return {};
} else {
return {result};
}
}
File::Result<std::string> File::read_file(const std::filesystem::path& filename) {
constexpr size_t buffer_size = 0x80;
char* buffer[buffer_size];
File f;
auto error = f.open(filename);
if (error)
return *error;
std::string content;
content.resize(f.size());
auto str = &content[0];
auto total_read = 0u;
while (true) {
auto read = f.read(buffer, buffer_size);
if (!read)
return read.error();
memcpy(str, buffer, *read);
str += *read;
total_read += *read;
if (*read < buffer_size)
break;
}
content.resize(total_read);
return content;
}
/* Range used for filename matching.
* Start and end are inclusive positions of "???" */
struct pattern_range {
size_t start;
size_t end;
};
/* Finds the last file matching the specified pattern that
* can be automatically incremented (digits in pattern).
* NB: assumes a patten with contiguous '?' like "FOO_???.txt". */
static std::filesystem::path find_last_ordinal_match(
const std::filesystem::path& folder,
const std::filesystem::path& pattern,
pattern_range range) {
auto last_match = std::filesystem::path();
auto can_increment = [range](const auto& path) {
for (auto i = range.start; i <= range.end; ++i)
if (!isdigit(path.native()[i]))
return false;
return true;
};
for (const auto& entry : std::filesystem::directory_iterator(folder, pattern)) {
if (std::filesystem::is_regular_file(entry.status()) && can_increment(entry.path())) {
const auto& match = entry.path();
if (match > last_match) {
last_match = match;
}
}
}
return last_match;
}
/* Given a file path like "FOO_0001.txt" increment it to "FOO_0002.txt". */
static std::filesystem::path increment_filename_ordinal(
const std::filesystem::path& path,
pattern_range range) {
auto name = path.filename().native();
for (auto i = range.end; i >= range.start; --i) {
auto& c = name[i];
// Not a digit or would overflow the counter.
if (c < u'0' || c > u'9' || (c == u'9' && i == range.start))
return {};
if (c == u'9')
c = '0';
else {
c++;
break;
}
}
return {name};
}
std::filesystem::path next_filename_matching_pattern(const std::filesystem::path& filename_pattern) {
auto path = filename_pattern.parent_path();
auto pattern = filename_pattern.filename();
auto range = pattern_range{
pattern.native().find_first_of(u'?'),
pattern.native().find_last_of(u'?')};
const auto match = find_last_ordinal_match(path, pattern, range);
if (match.empty()) {
auto pattern_str = pattern.native();
for (auto i = range.start; i <= range.end; ++i)
pattern_str[i] = u'0';
return path / pattern_str;
}
auto next_name = increment_filename_ordinal(match, range);
return next_name.empty() ? next_name : path / next_name;
}
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{};
scan_root_files(directory, extension, [&file_list](const std::filesystem::path& p) {
file_list.push_back(p);
});
return file_list;
}
std::vector<std::filesystem::path> scan_root_directories(const std::filesystem::path& directory) {
std::vector<std::filesystem::path> directory_list{};
for (const auto& entry : std::filesystem::directory_iterator(directory, "*")) {
if (std::filesystem::is_directory(entry.status())) {
directory_list.push_back(entry.path());
}
}
return directory_list;
}
std::filesystem::filesystem_error delete_file(const std::filesystem::path& file_path) {
return {f_unlink(reinterpret_cast<const TCHAR*>(file_path.c_str()))};
}
std::filesystem::filesystem_error rename_file(
const std::filesystem::path& file_path,
const std::filesystem::path& new_name) {
return {f_rename(reinterpret_cast<const TCHAR*>(file_path.c_str()), reinterpret_cast<const TCHAR*>(new_name.c_str()))};
}
std::filesystem::filesystem_error copy_file(
const std::filesystem::path& file_path,
const std::filesystem::path& dest_path) {
constexpr size_t buffer_size = std::filesystem::max_file_block_size;
uint8_t buffer[buffer_size];
File src;
File dst;
auto error = src.open(file_path);
if (error) return error.value();
error = dst.create(dest_path);
if (error) return error.value();
while (true) {
auto result = src.read(buffer, buffer_size);
if (result.is_error()) return result.error();
result = dst.write(buffer, *result);
if (result.is_error()) return result.error();
if (*result < buffer_size)
break;
}
return {};
}
FATTimestamp file_created_date(const std::filesystem::path& file_path) {
FILINFO filinfo;
f_stat(reinterpret_cast<const TCHAR*>(file_path.c_str()), &filinfo);
return {filinfo.fdate, filinfo.ftime};
}
std::filesystem::filesystem_error file_update_date(const std::filesystem::path& file_path, FATTimestamp timestamp) {
FILINFO filinfo{};
filinfo.fdate = timestamp.FAT_date;
filinfo.ftime = timestamp.FAT_time;
return f_utime(reinterpret_cast<const TCHAR*>(file_path.c_str()), &filinfo);
}
std::filesystem::filesystem_error make_new_file(
const std::filesystem::path& file_path) {
File f;
auto error = f.create(file_path);
if (error)
return *error;
return {};
}
std::filesystem::filesystem_error make_new_directory(
const std::filesystem::path& dir_path) {
return {f_mkdir(reinterpret_cast<const TCHAR*>(dir_path.c_str()))};
}
std::filesystem::filesystem_error ensure_directory(
const std::filesystem::path& dir_path) {
if (dir_path.empty() || std::filesystem::file_exists(dir_path))
return {};
auto result = ensure_directory(dir_path.parent_path());
if (result.code())
return result;
return make_new_directory(dir_path);
}
namespace std {
namespace filesystem {
std::string filesystem_error::what() const {
switch (err) {
case FR_OK:
return "ok";
case FR_DISK_ERR:
return "disk error";
case FR_INT_ERR:
return "insanity detected";
case FR_NOT_READY:
return "SD card not ready";
case FR_NO_FILE:
return "no file";
case FR_NO_PATH:
return "no path";
case FR_INVALID_NAME:
return "invalid name";
case FR_DENIED:
return "denied";
case FR_EXIST:
return "exists";
case FR_INVALID_OBJECT:
return "invalid object";
case FR_WRITE_PROTECTED:
return "write protected";
case FR_INVALID_DRIVE:
return "invalid drive";
case FR_NOT_ENABLED:
return "not enabled";
case FR_NO_FILESYSTEM:
return "no filesystem";
case FR_MKFS_ABORTED:
return "mkfs aborted";
case FR_TIMEOUT:
return "timeout";
case FR_LOCKED:
return "locked";
case FR_NOT_ENOUGH_CORE:
return "not enough core";
case FR_TOO_MANY_OPEN_FILES:
return "too many open files";
case FR_INVALID_PARAMETER:
return "invalid parameter";
case FR_EOF:
return "end of file";
case FR_DISK_FULL:
return "disk full";
case FR_BAD_SEEK:
return "bad seek";
case FR_UNEXPECTED:
return "unexpected";
default:
return "unknown";
}
}
path path::parent_path() const {
const auto index = _s.find_last_of(preferred_separator);
if (index == _s.npos) {
return {}; // NB: Deviation from STL.
} else {
return _s.substr(0, index);
}
}
path path::extension() const {
const auto t = filename().native();
const auto index = t.find_last_of(u'.');
if (index == t.npos) {
return {};
} else {
return t.substr(index);
}
}
path path::filename() const {
const auto index = _s.find_last_of(preferred_separator);
if (index == _s.npos) {
return _s;
} else {
return _s.substr(index + 1);
}
}
path path::stem() const {
const auto t = filename().native();
const auto index = t.find_last_of(u'.');
if (index == t.npos) {
return t;
} else {
return t.substr(0, index);
}
}
std::string path::string() const {
std::wstring_convert<std::codecvt_utf8_utf16<path::value_type>, path::value_type> conv;
return conv.to_bytes(native());
}
// appends a string to the end of filename, but leaves the extension asd.txt + "fg" -> asdfg.txt
path& path::append_filename(const string_type& str) {
const auto t = extension().native();
_s.erase(_s.size() - t.size()); // remove extension
_s += str; // append string
_s += t; // add back extension
return *this;
}
path& path::replace_extension(const path& replacement) {
const auto t = extension().native();
_s.erase(_s.size() - t.size());
if (!replacement._s.empty()) {
if (replacement._s.front() != u'.') {
_s += u'.';
}
_s += replacement._s;
}
return *this;
}
bool operator==(const path& lhs, const path& rhs) {
return lhs.native() == rhs.native();
}
bool operator!=(const path& lhs, const path& rhs) {
return !(lhs == rhs);
}
bool operator<(const path& lhs, const path& rhs) {
return lhs.native() < rhs.native();
}
bool operator>(const path& lhs, const path& rhs) {
return lhs.native() > rhs.native();
}
path operator+(const path& lhs, const path& rhs) {
path result = lhs;
result += rhs;
return result;
}
path operator/(const path& lhs, const path& rhs) {
path result = lhs;
result /= rhs;
return result;
}
bool path_iequal(
const path& lhs,
const path& rhs) {
const auto& lhs_str = lhs.native();
const auto& rhs_str = rhs.native();
// NB: Not correct for Unicode/locales.
if (lhs_str.length() == rhs_str.length()) {
for (size_t i = 0; i < lhs_str.length(); ++i)
if (towupper(lhs_str[i]) != towupper(rhs_str[i]))
return false;
return true;
}
return false;
}
bool is_cxx_capture_file(const path& filename) {
auto ext = filename.extension();
return path_iequal(c8_ext, ext) || path_iequal(c16_ext, ext);
}
uint8_t capture_file_sample_size(const path& filename) {
if (path_iequal(filename.extension(), c8_ext))
return sizeof(complex8_t);
if (path_iequal(filename.extension(), c16_ext))
return sizeof(complex16_t);
return 0;
}
directory_iterator::directory_iterator(
const std::filesystem::path& path,
const std::filesystem::path& wild)
: path_{path}, wild_{wild} {
impl = std::make_shared<Impl>();
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...
}
}
directory_iterator& directory_iterator::operator++() {
const auto result = f_findnext(&impl->dir, &impl->filinfo);
if ((result != FR_OK) || (impl->filinfo.fname[0] == 0)) {
impl.reset();
}
return *this;
}
bool is_directory(const file_status s) {
return (s & AM_DIR);
}
bool is_regular_file(const file_status s) {
return !(s & AM_DIR);
}
bool file_exists(const path& file_path) {
FILINFO filinfo;
auto fr = f_stat(reinterpret_cast<const TCHAR*>(file_path.c_str()), &filinfo);
return fr == FR_OK;
}
bool is_directory(const path& file_path) {
FILINFO filinfo;
auto fr = f_stat(reinterpret_cast<const TCHAR*>(file_path.c_str()), &filinfo);
return fr == FR_OK && is_directory(static_cast<file_status>(filinfo.fattrib));
}
bool is_empty_directory(const path& file_path) {
DIR dir;
FILINFO filinfo;
if (!is_directory(file_path))
return false;
auto result = f_findfirst(&dir, &filinfo, reinterpret_cast<const TCHAR*>(file_path.c_str()), (const TCHAR*)u"*");
return !((result == FR_OK) && (filinfo.fname[0] != (TCHAR)'\0'));
}
int file_count(const path& directory) {
int count{0};
for (auto& entry : std::filesystem::directory_iterator(directory, (const TCHAR*)u"*")) {
(void)entry; // avoid unused warning
++count;
}
return count;
}
space_info space(const path& p) {
DWORD free_clusters{0};
FATFS* fs;
if (f_getfree(reinterpret_cast<const TCHAR*>(p.c_str()), &free_clusters, &fs) == FR_OK) {
#if _MAX_SS != _MIN_SS
static_assert(false, "FatFs not configured for fixed sector size");
#else
const std::uintmax_t cluster_bytes = fs->csize * _MIN_SS;
return {
(fs->n_fatent - 2) * cluster_bytes,
free_clusters * cluster_bytes,
free_clusters * cluster_bytes,
};
#endif
} else {
return {0, 0, 0};
}
}
} /* namespace filesystem */
} /* namespace std */
+368
View File
@@ -0,0 +1,368 @@
/*
* Copyright (C) 2015 Jared Boone, ShareBrained Technology, Inc.
* Copyright (C) 2016 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 __FILE_H__
#define __FILE_H__
#include "ff.h"
#include "optional.hpp"
#include "result.hpp"
#include <cstddef>
#include <cstdint>
#include <string>
#include <array>
#include <memory>
#include <iterator>
#include <vector>
namespace std {
namespace filesystem {
struct filesystem_error {
constexpr filesystem_error() = default;
constexpr filesystem_error(
FRESULT fatfs_error)
: err{fatfs_error} {
}
constexpr filesystem_error(
unsigned int other_error)
: err{other_error} {
}
uint32_t code() const {
return err;
}
std::string what() const;
bool ok() const {
return err == FR_OK;
}
private:
uint32_t err{FR_OK};
};
struct path {
using string_type = std::u16string;
using value_type = string_type::value_type;
static constexpr value_type preferred_separator = u'/';
path()
: _s{} {
}
path(const path& p)
: _s{p._s} {
}
path(path&& p)
: _s{std::move(p._s)} {
}
template <class Source>
path(const Source& source)
: path{std::begin(source), std::end(source)} {
}
template <class InputIt>
path(InputIt first,
InputIt last)
: _s{first, last} {
}
path(const char16_t* const s)
: _s{s} {
}
path(const TCHAR* const s)
: _s{reinterpret_cast<const std::filesystem::path::value_type*>(s)} {
}
path& operator=(const path& p) {
_s = p._s;
return *this;
}
path& operator=(path&& p) {
_s = std::move(p._s);
return *this;
}
path parent_path() const;
path extension() const;
path filename() const;
path stem() const;
bool empty() const {
return _s.empty();
}
const value_type* c_str() const {
return native().c_str();
}
const TCHAR* tchar() const {
return reinterpret_cast<const TCHAR*>(native().c_str());
}
const string_type& native() const {
return _s;
}
std::string string() const;
path& operator+=(const path& p) {
_s += p._s;
return *this;
}
path& operator+=(const string_type& str) {
_s += str;
return *this;
}
path& operator/=(const path& p) {
if (_s.back() != preferred_separator && p._s.front() != preferred_separator)
_s += preferred_separator;
_s += p._s;
return *this;
}
path& replace_extension(const path& replacement = path());
path& append_filename(const string_type& str);
private:
string_type _s;
};
bool operator==(const path& lhs, const path& rhs);
bool operator!=(const path& lhs, const path& rhs);
bool operator<(const path& lhs, const path& rhs);
bool operator>(const path& lhs, const path& rhs);
path operator+(const path& lhs, const path& rhs);
path operator/(const path& lhs, const path& rhs);
/* Case insensitive path equality on underlying "native" string. */
bool path_iequal(const path& lhs, const path& rhs);
bool is_cxx_capture_file(const path& filename);
uint8_t capture_file_sample_size(const path& filename);
using file_status = BYTE;
/* The largest block that can be read/written to a file. */
constexpr uint16_t max_file_block_size = 512;
static_assert(sizeof(path::value_type) == 2, "sizeof(std::filesystem::path::value_type) != 2");
static_assert(sizeof(path::value_type) == sizeof(TCHAR), "FatFs TCHAR size != std::filesystem::path::value_type");
struct space_info {
static_assert(sizeof(std::uintmax_t) >= 8, "std::uintmax_t too small (<uint64_t)");
std::uintmax_t capacity;
std::uintmax_t free;
std::uintmax_t available;
};
struct directory_entry : public FILINFO {
file_status status() const {
return fattrib;
}
std::uintmax_t size() const {
return fsize;
};
const std::filesystem::path path() const noexcept { return {fname}; };
};
class directory_iterator {
struct Impl {
DIR dir;
directory_entry filinfo;
~Impl() {
f_closedir(&dir);
}
};
std::shared_ptr<Impl> impl{};
std::filesystem::path path_{};
std::filesystem::path wild_{};
friend bool operator!=(const directory_iterator& lhs, const directory_iterator& rhs);
public:
using difference_type = std::ptrdiff_t;
using value_type = directory_entry;
using pointer = const directory_entry*;
using reference = const directory_entry&;
using iterator_category = std::input_iterator_tag;
directory_iterator() noexcept {};
directory_iterator(const std::filesystem::path& path,
const std::filesystem::path& wild);
~directory_iterator() {}
directory_iterator& operator++();
reference operator*() const {
// TODO: Exception or assert if impl == nullptr.
return impl->filinfo;
}
};
inline const directory_iterator& begin(const directory_iterator& iter) noexcept {
return iter;
};
inline directory_iterator end(const directory_iterator&) noexcept {
return {};
};
inline bool operator!=(const directory_iterator& lhs, const directory_iterator& rhs) {
return lhs.impl != rhs.impl;
};
bool is_directory(const file_status s);
bool is_regular_file(const file_status s);
bool file_exists(const path& file_path);
bool is_directory(const path& file_path);
bool is_empty_directory(const path& file_path);
int file_count(const path& dir_path);
space_info space(const path& p);
} /* namespace filesystem */
} /* namespace std */
struct FATTimestamp {
uint16_t FAT_date;
uint16_t FAT_time;
};
std::filesystem::filesystem_error delete_file(const std::filesystem::path& file_path);
std::filesystem::filesystem_error rename_file(const std::filesystem::path& file_path, const std::filesystem::path& new_name);
std::filesystem::filesystem_error copy_file(const std::filesystem::path& file_path, const std::filesystem::path& dest_path);
FATTimestamp file_created_date(const std::filesystem::path& file_path);
std::filesystem::filesystem_error file_update_date(const std::filesystem::path& file_path, FATTimestamp timestamp);
std::filesystem::filesystem_error make_new_file(const std::filesystem::path& file_path);
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);
/* Gets an auto incrementing filename stem.
* Pattern should be like "FOO_???.txt" where ??? will be replaced by digits.
* Pattern may also contain a folder path like "LOGS/FOO_???.txt".
* Pattern '?' must be contiguous (bad: "FOO?_??")
* Returns empty path if a filename could not be created. */
std::filesystem::path next_filename_matching_pattern(const std::filesystem::path& pattern);
/* Values added to FatFs FRESULT enum, values outside the FRESULT data type */
static_assert(sizeof(FIL::err) == 1, "FatFs FIL::err size not expected.");
/* Dangerous to expose these, as FatFs native error values are byte-sized. However,
* my filesystem_error implementation is fine with it. */
#define FR_DISK_FULL (0x100)
#define FR_EOF (0x101)
#define FR_BAD_SEEK (0x102)
#define FR_UNEXPECTED (0x103)
/* NOTE: sizeof(File) == 556 bytes because of the FIL's buf member. */
class File {
public:
using Size = uint64_t;
using Offset = uint64_t;
using Timestamp = uint32_t;
using Error = std::filesystem::filesystem_error;
template <typename T>
using Result = Result<T, Error>;
File() {};
~File();
File(File&& other) {
std::swap(f, other.f);
}
File& operator=(File&& other) {
std::swap(f, other.f);
return *this;
}
/* Prevent copies */
File(const File&) = delete;
File& operator=(const File&) = delete;
// TODO: Return Result<>.
Optional<Error> open(const std::filesystem::path& filename, bool read_only = true, bool create = false);
void close();
Optional<Error> append(const std::filesystem::path& filename);
Optional<Error> create(const std::filesystem::path& filename);
Result<Size> read(void* data, const Size bytes_to_read);
Result<Size> write(const void* data, Size bytes_to_write);
Offset tell() const;
Result<Offset> seek(uint64_t Offset);
Result<Offset> truncate();
Size size() const;
Result<bool> eof();
template <size_t N>
Result<Size> write(const std::array<uint8_t, N>& data) {
return write(data.data(), N);
}
Optional<Error> write_line(const std::string& s);
// TODO: Return Result<>.
Optional<Error> sync();
/* Reads the entire file contents to a string.
* NB: This will likely fail for files larger than ~10kB. */
static Result<std::string> read_file(const std::filesystem::path& filename);
private:
FIL f{};
Optional<Error> open_fatfs(const std::filesystem::path& filename, BYTE mode);
};
#endif /*__FILE_H__*/
@@ -0,0 +1,55 @@
/*
* Copyright (C) 2024 Mark Thompson
*
* 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 "file_path.hpp"
#include "file.hpp"
const std::filesystem::path adsb_dir = u"ADSB";
const std::filesystem::path ais_dir = u"AIS";
const std::filesystem::path apps_dir = u"APPS";
const std::filesystem::path aprs_dir = u"APRS";
const std::filesystem::path audio_dir = u"AUDIO";
const std::filesystem::path blerx_dir = u"BLERX";
const std::filesystem::path bletx_dir = u"BLETX";
const std::filesystem::path captures_dir = u"CAPTURES";
const std::filesystem::path cvsfiles_dir = u"CVSFILES";
const std::filesystem::path debug_dir = u"DEBUG";
const std::filesystem::path firmware_dir = u"FIRMWARE";
const std::filesystem::path freqman_dir = u"FREQMAN";
const std::filesystem::path gps_dir = u"GPS";
const std::filesystem::path logs_dir = u"LOGS";
const std::filesystem::path looking_glass_dir = u"LOOKINGGLASS";
const std::filesystem::path playlist_dir = u"PLAYLIST";
const std::filesystem::path remotes_dir = u"REMOTES";
const std::filesystem::path repeat_rec_path = u"CAPTURES";
const std::filesystem::path samples_dir = u"SAMPLES";
const std::filesystem::path screenshots_dir = u"SCREENSHOTS";
const std::filesystem::path settings_dir = u"SETTINGS";
const std::filesystem::path spectrum_dir = u"SPECTRUM";
const std::filesystem::path splash_dir = u"SPLASH";
const std::filesystem::path sstv_dir = u"SSTV";
const std::filesystem::path wav_dir = u"WAV";
const std::filesystem::path whipcalc_dir = u"WHIPCALC";
const std::filesystem::path ook_editor_dir = u"OOKFILES";
const std::filesystem::path hopper_dir = u"HOPPER";
const std::filesystem::path subghz_dir = u"SUBGHZ";
const std::filesystem::path waterfalls_dir = u"WATERFALLS";
const std::filesystem::path macaddress_dir = u"MACADDRESS";
@@ -0,0 +1,59 @@
/*
* Copyright (C) 2024 Mark Thompson
*
* 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 __FILE_PATH_H__
#define __FILE_PATH_H__
#include "file.hpp"
extern const std::filesystem::path adsb_dir;
extern const std::filesystem::path ais_dir;
extern const std::filesystem::path apps_dir;
extern const std::filesystem::path aprs_dir;
extern const std::filesystem::path audio_dir;
extern const std::filesystem::path blerx_dir;
extern const std::filesystem::path bletx_dir;
extern const std::filesystem::path captures_dir;
extern const std::filesystem::path cvsfiles_dir;
extern const std::filesystem::path debug_dir;
extern const std::filesystem::path firmware_dir;
extern const std::filesystem::path freqman_dir;
extern const std::filesystem::path gps_dir;
extern const std::filesystem::path logs_dir;
extern const std::filesystem::path looking_glass_dir;
extern const std::filesystem::path playlist_dir;
extern const std::filesystem::path remotes_dir;
extern const std::filesystem::path repeat_rec_path;
extern const std::filesystem::path samples_dir;
extern const std::filesystem::path screenshots_dir;
extern const std::filesystem::path settings_dir;
extern const std::filesystem::path spectrum_dir;
extern const std::filesystem::path splash_dir;
extern const std::filesystem::path sstv_dir;
extern const std::filesystem::path wav_dir;
extern const std::filesystem::path whipcalc_dir;
extern const std::filesystem::path ook_editor_dir;
extern const std::filesystem::path hopper_dir;
extern const std::filesystem::path subghz_dir;
extern const std::filesystem::path waterfalls_dir;
extern const std::filesystem::path macaddress_dir;
#endif /* __FILE_PATH_H__ */
@@ -0,0 +1,48 @@
/*
* 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 "file_reader.hpp"
#include <string_view>
#include <vector>
/* Splits the string on the specified char and returns
* a vector of string_views. NB: the lifetime of the
* string to split must be maintained while the views
* are used or they will dangle. */
std::vector<std::string_view> split_string(std::string_view str, char c) {
std::vector<std::string_view> cols;
size_t start = 0;
while (start < str.length()) {
auto it = str.find(c, start);
if (it == str.npos)
break;
cols.emplace_back(&str[start], it - start);
start = it + 1;
}
if (start <= str.length() && !str.empty())
cols.emplace_back(&str[start], str.length() - start);
return cols;
}
@@ -0,0 +1,151 @@
/*
* 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 __FILE_READER_HPP__
#define __FILE_READER_HPP__
#include "file.hpp"
#include <cstring>
#include <cstdlib>
#include <string>
#include <string_view>
#include <vector>
/* BufferType requires the following members
* Size size()
* Result<Size> read(void* data, Size bytes_to_read)
* Result<Offset> seek(uint32_t offset)
*/
/* Iterates lines in buffer split on '\n'.
* 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:
struct iterator {
bool operator!=(const iterator& other) {
return this->pos_ != other.pos_ || this->reader_ != other.reader_;
}
const std::string& operator*() {
if (!cached_) {
bool ok = reader_->read_line(*this);
cached_ = true;
if (!ok) *this = reader_->end();
}
return line_data_;
}
iterator& operator++() {
const auto size = reader_->size();
if (pos_ < size) {
cached_ = false;
pos_ += line_data_.length();
}
if (pos_ >= size)
*this = reader_->end();
return *this;
}
typename BufferType::Size pos_{};
BufferLineReader* reader_{};
bool cached_ = false;
std::string line_data_{};
};
BufferLineReader(BufferType& buffer)
: buffer_{buffer} {}
iterator begin() { return {0, this}; }
iterator end() { return {size(), this}; }
typename BufferType::Size size() const { return buffer_.size(); }
private:
BufferType& buffer_;
bool read_line(iterator& it) {
constexpr size_t buf_size = 0x80;
char buf[buf_size];
uint32_t offset = 0;
it.line_data_.resize(buf_size);
buffer_.seek(it.pos_);
while (true) {
auto read = buffer_.read(buf, buf_size);
if (!read)
return false;
// Find newline.
auto len = 0u;
for (; len < *read; ++len) {
if (buf[len] == '\n') {
++len;
break;
}
}
// Reallocate if needed.
if (offset + len >= it.line_data_.length())
it.line_data_.resize(offset + len);
std::strncpy(&it.line_data_[offset], buf, len);
offset += len;
if (len < buf_size)
break;
}
it.line_data_.resize(offset);
return true;
}
};
using FileLineReader = BufferLineReader<File>;
/* Splits the string on the specified char and returns
* a vector of string_views. NB: the lifetime of the
* string to split must be maintained while the views
* 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
@@ -0,0 +1,543 @@
/*
* 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 __FILE_WRAPPER_HPP__
#define __FILE_WRAPPER_HPP__
#include "circular_buffer.hpp"
#include "file.hpp"
#include "optional.hpp"
#include <functional>
#include <memory>
#include <string_view>
enum class LineEnding : uint8_t {
LF,
CRLF
};
/* TODO:
* - CRLF handling.
* - Avoid full re-read on edits.
* - Would need to read old/new text when editing to track newlines.
* - How to surface errors? Exceptions?
*/
/* FatFs docs http://elm-chan.org/fsw/ff/00index_e.html */
/* BufferType requires the following members
* Size size()
* Result<Size> read(void* data, Size bytes_to_read)
* Result<Size> write(const void* data, Size bytes_to_write)
* Result<Offset> seek(uint32_t offset)
* Result<Offset> truncate()
* Optional<Error> sync()
*/
/* Wraps a buffer and provides an API for accessing lines efficiently. */
template <typename BufferType, uint32_t CacheSize>
class BufferWrapper {
public:
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;
// Offset of the end, exclusive.
Offset end;
Offset length() const { return end - start; }
};
BufferWrapper(BufferType* buffer)
: wrapped_{buffer} {
initialize();
}
virtual ~BufferWrapper() {}
std::function<void(Size, Size)> on_read_progress{};
/* Prevent copies */
BufferWrapper(const BufferWrapper&) = delete;
BufferWrapper& operator=(const BufferWrapper&) = delete;
Optional<std::string> get_text(Line line, Column col, Offset length) {
std::string buffer;
buffer.resize(length);
auto result = get_text(line, col, &buffer[0], length);
if (!result)
return {};
buffer.resize(*result);
return buffer;
}
Optional<Offset> get_text(Line line, Column col, char* output, Offset length) {
auto range = line_range(line);
int32_t to_read = length;
if (!range)
return {};
// Don't read past end of line.
if (range->start + col + to_read >= range->end)
to_read = range->end - col - range->start;
if (to_read <= 0)
return {};
return read(range->start + col, output, to_read);
}
/* Gets the size of the buffer in bytes. */
Size size() const { return wrapped_->size(); }
/* Get the count of the lines in the buffer. */
uint32_t line_count() const { return line_count_; }
/* Gets the range of the line if valid. */
Optional<Range> line_range(Line line) {
ensure_cached(line);
auto index = index_for_line(line);
if (!index)
return {};
auto start = *index == 0 ? start_offset_ : (newlines_[*index - 1] + 1);
auto end = newlines_[*index] + 1;
return Range{start, end};
}
/* Gets the length of the line, or 0 if invalid. */
Offset line_length(Line line) {
auto range = line_range(line);
return range ? range->length() : 0;
}
/* Gets the buffer offset of the line & col if valid. */
Optional<Offset> get_offset(Line line, Column col) {
auto range = line_range(line);
if (range)
return range->start + col;
return {};
}
/* Gets the index of the first line in the cache.
* Only really useful for unit testing or diagnostics. */
Offset start_line() { return start_line_; };
/* Inserts a line before the specified line or at the
* end of the buffer if line >= line_count. */
void insert_line(Line line) {
auto range = line_range(line);
if (range)
replace_range({range->start, range->start}, "\n");
else if (line >= line_count_)
replace_range({(Offset)size(), (Offset)size()}, "\n");
}
/* Deletes the specified line. */
void delete_line(Line line) {
auto range = line_range(line);
if (range)
replace_range(*range, {});
}
/* Replace the specified range with the string contents.
* A range with start/end set to the same value will insert.
* A range with an empty string will delete. */
void replace_range(Range range, std::string_view value) {
if (range.start > size() || range.end > size() || range.start > range.end)
return;
/* If delta_length == 0, it's an overwrite. Could still have
* added or removed newlines so caches will need to be rebuilt.
* If delta_length > 0, the file needs to grow and content needs
* to be shifted forward until the end of the range.
* If delta_length < 0, the file needs to be truncated and the
* content after the value needs to be shifted backward. */
int32_t delta_length = value.length() - range.length();
if (delta_length > 0)
expand(range.end, delta_length);
else if (delta_length < 0)
shrink(range.end, delta_length);
write(range.start, value);
wrapped_->sync();
rebuild_cache();
}
protected:
BufferWrapper() {}
void set_buffer(BufferType* buffer) {
wrapped_ = buffer;
initialize();
}
private:
/* Number of newline offsets to cache. */
static constexpr Offset max_newlines = CacheSize;
/* Size of stack buffer used for reading/writing. */
static constexpr Offset buffer_size = 512;
void initialize() {
start_offset_ = 0;
start_line_ = 0;
line_count_ = 0;
rebuild_cache();
}
void rebuild_cache() {
newlines_.clear();
// Special case for empty files to keep them consistent.
if (size() == 0) {
line_count_ = 1;
newlines_.push_back(0);
return;
}
// TODO: think through this for edit cases.
// E.g. don't read to end, maybe could specify
// a range to re-read because it should be possible
// to tell where the dirty regions are. After the
// dirty region, it should be possible to fixup
// the line_count data.
// TODO: seems like shrink/expand could do this while
// they are running.
line_count_ = start_line_;
Offset offset = start_offset_;
// Report progress every N lines.
constexpr auto report_interval = 100u;
auto result = next_newline(offset);
auto next_report = report_interval;
while (result) {
++line_count_;
if (newlines_.size() < max_newlines)
newlines_.push_back(*result);
offset = *result + 1;
if (on_read_progress && line_count_ > next_report) {
on_read_progress(offset, size());
next_report = line_count_ + report_interval;
}
result = next_newline(offset);
}
}
Optional<Offset> read(Offset offset, char* buffer, Offset length) {
if (offset + length > size())
return {};
wrapped_->seek(offset);
auto result = wrapped_->read(buffer, length);
if (result.is_error())
return {};
return *result;
}
bool write(Offset offset, std::string_view value) {
wrapped_->seek(offset);
auto result = wrapped_->write(value.data(), value.length());
return result.is_ok();
}
/* Returns the index of the line in the newline cache if valid. */
Optional<Offset> index_for_line(Line line) const {
if (line >= line_count_)
return {};
Offset actual = line - start_line_;
if (actual >= newlines_.size()) // NB: underflow wrap.
return {};
return actual;
}
/* Ensure specified line is in the newline cache. */
void ensure_cached(Line line) {
if (line >= line_count_)
return;
auto index = index_for_line(line);
if (index)
return;
if (line < start_line_) {
while (line < start_line_ && start_offset_ >= 2) {
// start_offset_ - 1 should be a newline. Need to
// find the new value for start_offset_. start_line_
// has to be > 0 to get into this block so there should
// always be one newline before start_offset_.
auto offset = previous_newline(start_offset_ - 2);
newlines_.push_front(start_offset_ - 1);
if (!offset) {
// Must be at beginning.
start_line_ = 0;
start_offset_ = 0;
} else {
// Found an previous newline, the new start_line_
// starts at the newline offset + 1.
start_line_--;
start_offset_ = *offset + 1;
}
}
} else {
while (line >= start_line_ + newlines_.size()) {
auto offset = next_newline(newlines_.back() + 1);
if (offset) {
start_line_++;
start_offset_ = newlines_.front() + 1;
newlines_.push_back(*offset);
} /* else at the EOF. */
}
}
}
/* Finding the first newline backward from offset. */
Optional<Offset> previous_newline(Offset offset) {
char buffer[buffer_size];
auto to_read = buffer_size;
do {
if (offset < to_read) {
// NB: Char at 'offset' was read in the previous iteration.
to_read = offset;
offset = 0;
} else
offset -= to_read;
wrapped_->seek(offset);
auto result = wrapped_->read(buffer, to_read);
if (result.is_error())
break;
// Find newlines in the buffer backwards.
for (int32_t i = *result - 1; i >= 0; --i) {
switch (buffer[i]) {
case '\n':
return offset + i;
}
}
if (offset == 0)
break;
} while (true);
return {}; // Didn't find one.
}
/* Finding the first newline forward from offset. */
Optional<Offset> next_newline(Offset offset) {
// EOF, no more newlines to find.
if (offset >= size())
return {};
char buffer[buffer_size];
wrapped_->seek(offset);
while (true) {
auto result = wrapped_->read(buffer, buffer_size);
if (result.is_error())
return {};
// Find newlines in the buffer.
for (Offset i = 0; i < *result; ++i) {
switch (buffer[i]) {
case '\n':
return offset + i;
}
}
offset += *result;
if (*result < buffer_size)
break;
}
// For consistency, treat the end of the file as a "newline".
return size() - 1;
}
/* Grow the file and move file content so that the
* content at src is shifted forward by 'delta'. */
void expand(Offset src, int32_t delta) {
if (delta <= 0) // Not an expand.
return;
char buffer[buffer_size];
auto to_read = buffer_size;
// Number of bytes left to shift.
Offset remaining = size() - src;
Offset offset = size();
Size report_total = remaining;
Size report_interval = report_total / 8;
Size next_report = remaining - report_interval;
while (remaining > 0) {
offset -= std::min(remaining, buffer_size);
to_read = std::min(remaining, buffer_size);
wrapped_->seek(offset);
auto result = wrapped_->read(buffer, to_read);
if (result.is_error())
break;
wrapped_->seek(offset + delta);
result = wrapped_->write(buffer, *result);
if (result.is_error())
break;
remaining -= *result;
if (on_read_progress && remaining <= next_report) {
on_read_progress(report_total - remaining, report_total);
next_report = remaining > report_interval ? remaining - report_interval : 0;
}
}
}
/* Shrink the file and move file content so that the
* content at src is shifted backward by 'delta'. */
void shrink(Offset src, int32_t delta) {
if (delta >= 0) // Not a shrink.
return;
char buffer[buffer_size];
auto offset = src;
Size report_total = size();
Size report_interval = report_total / 8;
Size next_report = offset + report_interval;
while (true) {
wrapped_->seek(offset);
auto result = wrapped_->read(buffer, buffer_size);
if (result.is_error())
break;
wrapped_->seek(offset + delta);
result = wrapped_->write(buffer, *result);
if (result.is_error() || *result < buffer_size)
break;
offset += *result;
if (on_read_progress && offset >= next_report) {
on_read_progress(offset, report_total);
next_report = offset + report_interval;
}
}
// Delete the extra bytes at the end of the file.
wrapped_->truncate();
}
BufferType* wrapped_{};
/* Total number of lines in the buffer. */
Offset line_count_{0};
/* The offset and line of the newlines cache. */
Offset start_offset_{0};
Offset start_line_{0};
LineEnding line_ending_{LineEnding::LF};
CircularBuffer<Offset, max_newlines + 1> newlines_{};
};
/* A BufferWrapper over a file. */
class FileWrapper : public BufferWrapper<File, 64> {
public:
template <typename T>
using Result = File::Result<T>;
using Error = File::Error;
static Result<std::unique_ptr<FileWrapper>> open(
const std::filesystem::path& path,
bool create = false,
std::function<void(Size, Size)> on_read_progress = nullptr) {
auto fw = std::unique_ptr<FileWrapper>(new FileWrapper());
auto error = fw->file_.open(path, /*read_only*/ false, create);
if (error)
return *error;
if (on_read_progress)
fw->on_read_progress = on_read_progress;
fw->initialize();
return fw;
}
/* Underlying file. */
File& file() { return file_; }
/* Swaps out the underlying file for the specified file.
* The swapped file is expected have the same contents.
* For copy-on-write scenario with a temp file. */
bool assume_file(const std::filesystem::path& path) {
File file;
auto error = file.open(path, /*read_only*/ false);
if (error)
return false;
file_ = std::move(file);
return true;
}
private:
FileWrapper() {}
void initialize() {
set_buffer(&file_);
}
File file_{};
};
template <uint32_t CacheSize = 64, typename T>
BufferWrapper<T, CacheSize> wrap_buffer(T& buffer) {
return {&buffer};
}
#endif // __FILE_WRAPPER_HPP__
@@ -0,0 +1,4 @@
#pragma once
#define PI 3.1415926535897932384626433832795
#define M_PI PI
@@ -0,0 +1,68 @@
/*
* Copyright (C) 2016 Jared Boone, ShareBrained Technology, Inc.
*
* 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 __OPTIONAL_H__
#define __OPTIONAL_H__
#include <utility>
template <typename T>
class Optional {
public:
constexpr Optional()
: value_{}, valid_{false} {}
constexpr Optional(const T& value)
: value_{value}, valid_{true} {}
constexpr Optional(T&& value)
: value_{std::move(value)}, valid_{true} {}
constexpr Optional& operator=(const T& value) {
value_ = value;
valid_ = true;
return *this;
}
constexpr Optional& operator=(T&& value) {
value_ = std::move(value);
valid_ = true;
return *this;
}
bool is_valid() const { return valid_; }
operator bool() const { return valid_; }
// TODO: Throw if not valid?
T& value() & { return value_; }
T& operator*() & { return value_; }
const T& value() const& { return value_; }
const T& operator*() const& { return value_; }
T&& value() && { return std::move(value_); }
T&& operator*() && { return std::move(value_); }
T* operator->() { return &value_; }
const T* operator->() const { return &value_; }
private:
T value_;
bool valid_;
};
#endif /*__OPTIONAL_H__*/
+84
View File
@@ -0,0 +1,84 @@
/*
* Copyright (C) 2023 Kyle Reed
* Copyright (C) 2015 Jared Boone, ShareBrained Technology, Inc.
* Copyright (C) 2016 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 <utility>
template <typename TValue, typename TError>
struct Result {
enum class Type {
Success,
Error,
} type;
union {
TValue value_;
TError error_;
};
bool is_ok() const {
return type == Type::Success;
}
operator bool() const {
return is_ok();
}
bool is_error() const {
return type == Type::Error;
}
const TValue& value() const& {
return value_;
}
const TValue& operator*() const& {
return value_;
}
TValue&& operator*() && {
return std::move(value_);
}
const TError& error() const& {
return error_;
}
Result() = delete;
constexpr Result(TValue&& value)
: type{Type::Success},
value_{std::forward<TValue>(value)} {
}
constexpr Result(TError error)
: type{Type::Error},
error_{error} {
}
~Result() {
if (is_ok())
value_.~TValue();
else
error_.~TError();
}
};
@@ -0,0 +1,147 @@
/*
* Copyright (C) 2015 Jared Boone, ShareBrained Technology, Inc.
*
* 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 __SINE_TABLE_H__
#define __SINE_TABLE_H__
// TODO: Including only for pi. Need separate math.hpp...
#include "complex.hpp"
#include <array>
#include <cmath>
/*
import numpy
length = 256
w = numpy.arange(length, dtype=numpy.float64) * (2 * numpy.pi / length)
v = numpy.sin(w)
print(v)
*/
constexpr uint16_t sine_table_f32_period = 256;
// periode is 256 . means sine_table_f32[0]= sine_table_f32[0+256], sine_table_f32[1]=sine_table_f32[1+256] (those two added manualy)
// Then table has 258 values ,256:[0,..255] + [256] and [257], those two are used when we interpolate[255] with [255+1], and [256] with [256+1]
// [256] index is needed in the function sin_f32() when we are inputing very small radian values , example , sin_f32((-1e-14) in radians)
static constexpr std::array<float, sine_table_f32_period + 2> sine_table_f32{
0.00000000e+00, 2.45412285e-02, 4.90676743e-02,
7.35645636e-02, 9.80171403e-02, 1.22410675e-01,
1.46730474e-01, 1.70961889e-01, 1.95090322e-01,
2.19101240e-01, 2.42980180e-01, 2.66712757e-01,
2.90284677e-01, 3.13681740e-01, 3.36889853e-01,
3.59895037e-01, 3.82683432e-01, 4.05241314e-01,
4.27555093e-01, 4.49611330e-01, 4.71396737e-01,
4.92898192e-01, 5.14102744e-01, 5.34997620e-01,
5.55570233e-01, 5.75808191e-01, 5.95699304e-01,
6.15231591e-01, 6.34393284e-01, 6.53172843e-01,
6.71558955e-01, 6.89540545e-01, 7.07106781e-01,
7.24247083e-01, 7.40951125e-01, 7.57208847e-01,
7.73010453e-01, 7.88346428e-01, 8.03207531e-01,
8.17584813e-01, 8.31469612e-01, 8.44853565e-01,
8.57728610e-01, 8.70086991e-01, 8.81921264e-01,
8.93224301e-01, 9.03989293e-01, 9.14209756e-01,
9.23879533e-01, 9.32992799e-01, 9.41544065e-01,
9.49528181e-01, 9.56940336e-01, 9.63776066e-01,
9.70031253e-01, 9.75702130e-01, 9.80785280e-01,
9.85277642e-01, 9.89176510e-01, 9.92479535e-01,
9.95184727e-01, 9.97290457e-01, 9.98795456e-01,
9.99698819e-01, 1.00000000e+00, 9.99698819e-01,
9.98795456e-01, 9.97290457e-01, 9.95184727e-01,
9.92479535e-01, 9.89176510e-01, 9.85277642e-01,
9.80785280e-01, 9.75702130e-01, 9.70031253e-01,
9.63776066e-01, 9.56940336e-01, 9.49528181e-01,
9.41544065e-01, 9.32992799e-01, 9.23879533e-01,
9.14209756e-01, 9.03989293e-01, 8.93224301e-01,
8.81921264e-01, 8.70086991e-01, 8.57728610e-01,
8.44853565e-01, 8.31469612e-01, 8.17584813e-01,
8.03207531e-01, 7.88346428e-01, 7.73010453e-01,
7.57208847e-01, 7.40951125e-01, 7.24247083e-01,
7.07106781e-01, 6.89540545e-01, 6.71558955e-01,
6.53172843e-01, 6.34393284e-01, 6.15231591e-01,
5.95699304e-01, 5.75808191e-01, 5.55570233e-01,
5.34997620e-01, 5.14102744e-01, 4.92898192e-01,
4.71396737e-01, 4.49611330e-01, 4.27555093e-01,
4.05241314e-01, 3.82683432e-01, 3.59895037e-01,
3.36889853e-01, 3.13681740e-01, 2.90284677e-01,
2.66712757e-01, 2.42980180e-01, 2.19101240e-01,
1.95090322e-01, 1.70961889e-01, 1.46730474e-01,
1.22410675e-01, 9.80171403e-02, 7.35645636e-02,
4.90676743e-02, 2.45412285e-02, 1.22464680e-16,
-2.45412285e-02, -4.90676743e-02, -7.35645636e-02,
-9.80171403e-02, -1.22410675e-01, -1.46730474e-01,
-1.70961889e-01, -1.95090322e-01, -2.19101240e-01,
-2.42980180e-01, -2.66712757e-01, -2.90284677e-01,
-3.13681740e-01, -3.36889853e-01, -3.59895037e-01,
-3.82683432e-01, -4.05241314e-01, -4.27555093e-01,
-4.49611330e-01, -4.71396737e-01, -4.92898192e-01,
-5.14102744e-01, -5.34997620e-01, -5.55570233e-01,
-5.75808191e-01, -5.95699304e-01, -6.15231591e-01,
-6.34393284e-01, -6.53172843e-01, -6.71558955e-01,
-6.89540545e-01, -7.07106781e-01, -7.24247083e-01,
-7.40951125e-01, -7.57208847e-01, -7.73010453e-01,
-7.88346428e-01, -8.03207531e-01, -8.17584813e-01,
-8.31469612e-01, -8.44853565e-01, -8.57728610e-01,
-8.70086991e-01, -8.81921264e-01, -8.93224301e-01,
-9.03989293e-01, -9.14209756e-01, -9.23879533e-01,
-9.32992799e-01, -9.41544065e-01, -9.49528181e-01,
-9.56940336e-01, -9.63776066e-01, -9.70031253e-01,
-9.75702130e-01, -9.80785280e-01, -9.85277642e-01,
-9.89176510e-01, -9.92479535e-01, -9.95184727e-01,
-9.97290457e-01, -9.98795456e-01, -9.99698819e-01,
-1.00000000e+00, -9.99698819e-01, -9.98795456e-01,
-9.97290457e-01, -9.95184727e-01, -9.92479535e-01,
-9.89176510e-01, -9.85277642e-01, -9.80785280e-01,
-9.75702130e-01, -9.70031253e-01, -9.63776066e-01,
-9.56940336e-01, -9.49528181e-01, -9.41544065e-01,
-9.32992799e-01, -9.23879533e-01, -9.14209756e-01,
-9.03989293e-01, -8.93224301e-01, -8.81921264e-01,
-8.70086991e-01, -8.57728610e-01, -8.44853565e-01,
-8.31469612e-01, -8.17584813e-01, -8.03207531e-01,
-7.88346428e-01, -7.73010453e-01, -7.57208847e-01,
-7.40951125e-01, -7.24247083e-01, -7.07106781e-01,
-6.89540545e-01, -6.71558955e-01, -6.53172843e-01,
-6.34393284e-01, -6.15231591e-01, -5.95699304e-01,
-5.75808191e-01, -5.55570233e-01, -5.34997620e-01,
-5.14102744e-01, -4.92898192e-01, -4.71396737e-01,
-4.49611330e-01, -4.27555093e-01, -4.05241314e-01,
-3.82683432e-01, -3.59895037e-01, -3.36889853e-01,
-3.13681740e-01, -2.90284677e-01, -2.66712757e-01,
-2.42980180e-01, -2.19101240e-01, -1.95090322e-01,
-1.70961889e-01, -1.46730474e-01, -1.22410675e-01,
-9.80171403e-02, -7.35645636e-02, -4.90676743e-02,
-2.45412285e-02, 0.00000000e+00, 2.45412285e-02};
inline float sin_f32(const float w) {
const float x = w / (2 * pi); // normalization
const float x_frac = x - std::floor(x); // [0, 1]
const float n = x_frac * sine_table_f32_period;
const uint16_t n_int = static_cast<uint16_t>(n);
const float n_frac = n - n_int;
const float p0 = sine_table_f32[n_int];
const float p1 = sine_table_f32[n_int + 1];
const float diff = p1 - p0;
const float result = p0 + n_frac * diff; // linear interpolation
return result;
}
#endif /*__SINE_TABLE_H__*/
@@ -0,0 +1 @@
#include "standalone_app.hpp"
@@ -0,0 +1,569 @@
/*
* Copyright (C) 2014 Jared Boone, ShareBrained Technology, Inc.
*
* 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 "string_format.hpp"
#include <algorithm>
#include <array>
#include <complex>
#include <cstdint>
#include <cstddef>
#include <initializer_list>
#include <memory>
#include <string_view>
#include <type_traits>
using namespace std::literals;
/* This takes a pointer to the end of a buffer
* 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,
Int n)
{
*p = 0;
auto q = p;
do
{
*(--q) = n % 10 + '0';
n /= 10;
} while (n != 0);
return q;
}
static char *to_string_dec_uint_pad_internal(
char *const term,
const uint32_t n,
const int32_t l,
const char fill)
{
auto q = to_string_dec_uint_internal(term, n);
// Fill with padding if needed.
// TODO: use std::array instead. There's no
// bounds checks on any of this!
if (fill)
{
while ((term - q) < l)
{
*(--q) = fill;
}
}
return q;
}
static char *to_string_dec_uint_internal(uint64_t 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(uint64_t n, StringFormatBuffer &buffer, size_t &length)
{
return to_string_dec_uint_internal(n, buffer, length);
}
char *to_string_dec_int(int64_t n, StringFormatBuffer &buffer, size_t &length)
{
bool negative = n < 0;
auto start = to_string_dec_uint(negative ? -n : n, buffer, length);
if (negative)
{
*(--start) = '-';
++length;
}
return start;
}
std::string to_string_dec_int(int64_t n)
{
StringFormatBuffer b{};
size_t len{};
char *str = to_string_dec_int(n, b, len);
return std::string(str, len);
}
std::string to_string_dec_uint(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)
{
char p[33];
for (uint8_t c = 0; c < l; c++)
{
if (n & (1 << (l - 1 - c)))
p[c] = '1';
else
p[c] = '0';
}
p[l] = 0;
return p;
}
std::string to_string_dec_uint(
const uint32_t n,
const int32_t l,
const char fill)
{
char p[16];
auto term = p + sizeof(p) - 1;
auto q = to_string_dec_uint_pad_internal(term, n, l, fill);
// Right justify.
// (This code is redundant and won't do anything if a fill character was specified)
while ((term - q) < l)
{
*(--q) = ' ';
}
return q;
}
std::string to_string_dec_int(
const int32_t n,
const int32_t l,
const char fill)
{
const size_t negative = (n < 0) ? 1 : 0;
uint32_t n_abs = negative ? -n : n;
char p[16];
auto term = p + sizeof(p) - 1;
auto q = to_string_dec_uint_pad_internal(term, n_abs, l - negative, fill);
// Add sign.
if (negative)
{
*(--q) = '-';
}
// Right justify.
// (This code is redundant and won't do anything if a fill character was specified)
while ((term - q) < l)
{
*(--q) = ' ';
}
return q;
}
std::string to_string_decimal(float decimal, int8_t precision)
{
double integer_part;
double fractional_part;
std::string result;
if (precision > 9)
precision = 9; // we will convert to uin32_t, and that is the max it can hold.
fractional_part = modf(decimal, &integer_part) * pow(10, precision);
if (fractional_part < 0)
{
fractional_part = -fractional_part;
}
result = to_string_dec_int(integer_part) + "." + to_string_dec_uint(fractional_part, precision, '0');
return result;
}
std::string to_string_decimal_padding(float decimal, int8_t precision, const int32_t l)
{
double integer_part;
double fractional_part;
std::string result;
if (precision > 9)
precision = 9; // we will convert to uin32_t, and that is the max it can hold.
fractional_part = modf(decimal, &integer_part) * pow(10, precision);
if (fractional_part < 0)
{
fractional_part = -fractional_part;
}
result = to_string_dec_int(integer_part) + "." + to_string_dec_uint(fractional_part, precision, '0');
// Add padding with spaces to meet the length requirement
if (result.length() < (uint32_t)l)
{
int padding_length = l - result.length();
std::string padding(padding_length, ' ');
result = padding + result;
}
return result;
}
// right-justified frequency in Hz, always 10 characters
std::string to_string_freq(const uint64_t f)
{
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 + 50) / 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_uint(f / 1000000);
}
else
{
if (precision > 6)
precision = 6;
uint32_t divisor = pow10[6 - precision];
final_str = to_string_dec_uint((f + (divisor / 2)) / 1000000) + "." + to_string_dec_int(((f + (divisor / 2)) / divisor) % pow10[precision], precision, '0');
}
return final_str;
}
std::string to_string_time_ms(const uint32_t ms)
{
std::string final_str{""};
if (ms < 1000)
{
final_str = to_string_dec_uint(ms) + "ms";
}
else
{
auto seconds = ms / 1000;
if (seconds >= 60)
final_str = to_string_dec_uint(seconds / 60) + "m";
return final_str + to_string_dec_uint(seconds % 60) + "s";
}
return final_str;
}
static char *to_string_hex_internal(char *ptr, uint64_t value, uint8_t length)
{
if (length == 0)
return ptr;
*(--ptr) = uint_to_char(value & 0xF, 16);
return to_string_hex_internal(ptr, value >> 4, length - 1);
}
std::string to_string_hex(uint64_t value, int32_t length)
{
constexpr uint8_t buffer_length = 33;
char buffer[buffer_length];
char *ptr = &buffer[buffer_length - 1];
*ptr = '\0';
length = std::min<uint8_t>(buffer_length - 1, length);
return to_string_hex_internal(ptr, value, length);
}
std::string to_string_hex_array(uint8_t *array, int32_t length)
{
std::string str_return;
str_return.reserve(length * 2);
for (uint8_t i = 0; i < length; i++)
str_return += to_string_hex(array[i], 2);
return str_return;
}
// TODO: wire standalone api:
/*
std::string to_string_datetime(const rtc::RTC &value, const TimeFormat format)
{
std::string string{""};
if (format == YMDHMS)
{
string += to_string_dec_uint(value.year(), 4) + "-" +
to_string_dec_uint(value.month(), 2, '0') + "-" +
to_string_dec_uint(value.day(), 2, '0') + " ";
}
string += to_string_dec_uint(value.hour(), 2, '0') + ":" +
to_string_dec_uint(value.minute(), 2, '0');
if ((format == YMDHMS) || (format == HMS))
string += ":" + to_string_dec_uint(value.second(), 2, '0');
return string;
}
std::string to_string_timestamp(const rtc::RTC &value)
{
return to_string_dec_uint(value.year(), 4, '0') +
to_string_dec_uint(value.month(), 2, '0') +
to_string_dec_uint(value.day(), 2, '0') +
to_string_dec_uint(value.hour(), 2, '0') +
to_string_dec_uint(value.minute(), 2, '0') +
to_string_dec_uint(value.second(), 2, '0');
}
std::string to_string_FAT_timestamp(const FATTimestamp &timestamp)
{
return to_string_dec_uint((timestamp.FAT_date >> 9) + 1980) + "-" +
to_string_dec_uint((timestamp.FAT_date >> 5) & 0xF, 2, '0') + "-" +
to_string_dec_uint((timestamp.FAT_date & 0x1F), 2, '0') + " " +
to_string_dec_uint((timestamp.FAT_time >> 11), 2, '0') + ":" +
to_string_dec_uint((timestamp.FAT_time >> 5) & 0x3F, 2, '0');
}
*/
std::string to_string_file_size(uint32_t file_size)
{
static const std::string suffix[5] = {"B", "kB", "MB", "GB", "??"};
size_t suffix_index = 0;
while (file_size >= 1024)
{
file_size /= 1024;
suffix_index++;
}
if (suffix_index > 4)
suffix_index = 4;
return to_string_dec_uint(file_size) + suffix[suffix_index];
}
std::string to_string_mac_address(const uint8_t *macAddress, uint8_t length, bool noColon)
{
std::string string;
string += to_string_hex(macAddress[0], 2);
for (int i = 1; i < length; i++)
{
string += noColon ? to_string_hex(macAddress[i], 2) : ":" + to_string_hex(macAddress[i], 2);
}
return string;
}
std::string to_string_formatted_mac_address(const char *macAddress)
{
std::string formattedAddress;
for (int i = 0; i < 12; i += 2)
{
if (i > 0)
{
formattedAddress += ':';
}
formattedAddress += macAddress[i];
formattedAddress += macAddress[i + 1];
}
return formattedAddress;
}
void generateRandomMacAddress(char *macAddress)
{
const char hexDigits[] = "0123456789ABCDEF";
// Generate 12 random hexadecimal characters
for (int i = 0; i < 12; i++)
{
int randomIndex = rand() % 16;
macAddress[i] = hexDigits[randomIndex];
}
macAddress[12] = '\0'; // Null-terminate the string
}
// TODO: wire standalone api:
/*
uint64_t readUntil(File &file, char *result, std::size_t maxBufferSize, char delimiter)
{
std::size_t bytesRead = 0;
while (true)
{
char ch;
File::Result<File::Size> readResult = file.read(&ch, 1);
if (readResult.is_ok() && readResult.value() > 0)
{
if (ch == delimiter)
{
// Found a space character, stop reading
break;
}
else if (bytesRead < maxBufferSize)
{
// Append the character to the result if there's space
result[bytesRead++] = ch;
}
else
{
// Buffer is full, break to prevent overflow
break;
}
}
else
{
break; // End of file or error
}
}
// Null-terminate the result string
result[bytesRead] = '\0';
return bytesRead;
}
*/
std::string unit_auto_scale(double n, const uint32_t base_unit, uint32_t precision)
{
const uint32_t powers_of_ten[5] = {1, 10, 100, 1000, 10000};
std::string string{""};
uint32_t prefix_index = base_unit;
double integer_part;
double fractional_part;
precision = std::min((uint32_t)4, precision);
while (n > 1000)
{
n /= 1000.0;
prefix_index++;
}
fractional_part = modf(n, &integer_part) * powers_of_ten[precision];
if (fractional_part < 0)
fractional_part = -fractional_part;
string = to_string_dec_int(integer_part);
if (precision)
string += '.' + to_string_dec_uint(fractional_part, precision, '0');
if (prefix_index != 3)
string += unit_prefix[prefix_index];
return string;
}
double get_decimals(double num, int16_t mult, bool round)
{
num -= int(num); // keep decimals only
num *= mult; // Shift decimals into integers
if (!round)
return num;
int16_t intnum = int(num); // Round it up if necessary
num -= intnum; // Get decimal part
if (num > .5)
intnum++; // Round up
return intnum;
}
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)};
}
std::string trimr(std::string_view str)
{
size_t last = str.find_last_not_of(whitespace_str);
return std::string{last != std::string::npos ? str.substr(0, last + 1) : ""};
}
std::string truncate(std::string_view str, size_t length)
{
return std::string{str.length() <= length ? str : str.substr(0, length)};
}
uint8_t char_to_uint(char c, uint8_t radix)
{
uint8_t v = 0;
if (c >= '0' && c <= '9')
v = c - '0';
else if (c >= 'A' && c <= 'F')
v = c - 'A' + 10; // A is dec: 10
else if (c >= 'a' && c <= 'f')
v = c - 'a' + 10; // A is dec: 10
return v < radix ? v : 0;
}
char uint_to_char(uint8_t val, uint8_t radix)
{
if (val >= radix)
return 0;
if (val < 10)
return '0' + val;
else
return 'A' + val - 10; // A is dec: 10
}
@@ -0,0 +1,106 @@
/*
* Copyright (C) 2014 Jared Boone, ShareBrained Technology, Inc.
*
* 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 __STRING_FORMAT_H__
#define __STRING_FORMAT_H__
#include <cstdint>
#include <array>
#include <string>
#include <string_view>
// #include "file.hpp"
// BARF! rtc::RTC is leaking everywhere.
// #include "lpc43xx_cpp.hpp"
// using namespace lpc43xx;
enum TimeFormat
{
YMDHMS = 0,
HMS = 1,
HM = 2
};
const char unit_prefix[7]{'n', 'u', 'm', 0, 'k', 'M', 'G'};
using StringFormatBuffer = std::array<char, 24>;
/* Integer conversion without memory allocations. */
char *to_string_dec_int(int64_t n, StringFormatBuffer &buffer, size_t &length);
char *to_string_dec_uint(uint64_t n, StringFormatBuffer &buffer, size_t &length);
std::string to_string_dec_int(int64_t n);
std::string to_string_dec_uint(uint64_t n);
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, const char fill = ' ');
std::string to_string_dec_int(const int32_t n, const int32_t l, const char fill = 0);
std::string to_string_decimal(float decimal, int8_t precision);
std::string to_string_decimal_padding(float decimal, int8_t precision, const int32_t l);
std::string to_string_hex(uint64_t n, int32_t length);
std::string to_string_hex_array(uint8_t *array, int32_t length);
/* Helper to select length based on type size. */
template <typename T>
std::string to_string_hex(T n)
{
return to_string_hex(n, sizeof(T) * 2); // Two digits/byte.
}
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);
// TODO: wire standalone api: std::string to_string_datetime(const rtc::RTC &value, const TimeFormat format = YMDHMS);
// TODO: wire standalone api: std::string to_string_timestamp(const rtc::RTC &value);
// TODO: wire standalone api: std::string to_string_FAT_timestamp(const FATTimestamp &timestamp);
// Gets a human readable file size string.
std::string to_string_file_size(uint32_t file_size);
// Converts Mac Address to string.
std::string to_string_mac_address(const uint8_t *macAddress, uint8_t length, bool noColon);
std::string to_string_formatted_mac_address(const char *macAddress);
void generateRandomMacAddress(char *macAddress);
// TODO: wire standalone api: uint64_t readUntil(File &file, char *result, std::size_t maxBufferSize, char delimiter);
/* Scales 'n' to be a value less than 1000. 'base_unit' is the index of the unit from
* 'unit_prefix' that 'n' is in initially. 3 is the index of the '1s' unit. */
std::string unit_auto_scale(double n, const uint32_t base_unit, uint32_t precision);
double get_decimals(double num, int16_t mult, bool round = false);
std::string trim(std::string_view str); // Remove whitespace at ends.
std::string trimr(std::string_view str); // Remove trailing spaces
std::string truncate(std::string_view, size_t length);
/* Gets the int value for a character given the radix.
* e.g. '5' => 5, 'D' => 13. Out of bounds => 0. */
uint8_t char_to_uint(char c, uint8_t radix = 10);
/* Gets the int value for a character given the radix.
* e.g. 5 => '5', 13 => 'D'. Out of bounds => '\0'. */
char uint_to_char(uint8_t val, uint8_t radix = 10);
#endif /*__STRING_FORMAT_H__*/
+870
View File
@@ -0,0 +1,870 @@
#include "theme.hpp"
namespace ui {
ThemeTemplate* Theme::current = nullptr;
ThemeTemplate* Theme::getInstance() {
if (current == nullptr) SetTheme(DefaultGrey);
return Theme::current;
}
void Theme::destroy() {
if (current != nullptr) {
delete current;
current = nullptr;
}
}
void Theme::SetTheme(ThemeId theme) {
if (current != nullptr) delete current;
switch (theme) {
case Yellow:
current = new ThemeYellow();
break;
case Aqua:
current = new ThemeAqua();
break;
case Green:
current = new ThemeGreen();
break;
case Red:
current = new ThemeRed();
break;
case Dark:
current = new ThemeDark();
break;
case DefaultGrey:
default:
current = new ThemeDefault();
break;
}
}
ThemeTemplate::~ThemeTemplate() {
delete bg_lightest;
delete bg_lightest_small;
delete bg_light;
delete bg_medium;
delete bg_dark;
delete bg_darker;
delete bg_darkest;
delete bg_darkest_small;
delete bg_important_small;
delete error_dark;
delete warning_dark;
delete ok_dark;
delete fg_dark;
delete fg_medium;
delete fg_light;
delete fg_red;
delete fg_green;
delete fg_yellow;
delete fg_orange;
delete fg_blue;
delete fg_cyan;
delete fg_darkcyan;
delete fg_magenta;
delete option_active;
delete status_active; // green, the status bar icons when active
delete bg_table_header;
}
ThemeYellow::ThemeYellow() {
bg_lightest = new Style{
.font = font::fixed_8x16(),
.background = {255, 255, 204},
.foreground = Color::black(),
};
bg_lightest_small = new Style{
.font = font::fixed_5x8(),
.background = {255, 255, 204},
.foreground = Color::black(),
};
bg_light = new Style{
.font = font::fixed_8x16(),
.background = {255, 255, 102},
.foreground = Color::white(),
};
bg_medium = new Style{
.font = font::fixed_8x16(),
.background = {204, 204, 0},
.foreground = Color::white(),
};
bg_dark = new Style{
.font = font::fixed_8x16(),
.background = {153, 153, 0},
.foreground = Color::white(),
};
bg_darker = new Style{
.font = font::fixed_8x16(),
.background = {102, 102, 0},
.foreground = Color::white(),
};
bg_darkest = new Style{
.font = font::fixed_8x16(),
.background = {31, 31, 0},
.foreground = Color::white(),
};
bg_darkest_small = new Style{
.font = font::fixed_5x8(),
.background = {31, 31, 0},
.foreground = Color::white(),
};
bg_important_small = new Style{
.font = font::fixed_5x8(),
.background = Color::yellow(),
.foreground = {31, 31, 0},
};
error_dark = new Style{
.font = font::fixed_8x16(),
.background = {31, 31, 0},
.foreground = Color::red(),
};
warning_dark = new Style{
.font = font::fixed_8x16(),
.background = {31, 31, 0},
.foreground = Color::yellow(),
};
ok_dark = new Style{
.font = font::fixed_8x16(),
.background = {31, 31, 0},
.foreground = Color::green(),
};
fg_dark = new Style{
.font = font::fixed_8x16(),
.background = {31, 31, 0},
.foreground = {153, 153, 0},
};
fg_medium = new Style{
.font = font::fixed_8x16(),
.background = {31, 31, 0},
.foreground = {204, 204, 0},
};
fg_light = new Style{
.font = font::fixed_8x16(),
.background = {31, 31, 0},
.foreground = Color::light_grey(),
};
fg_red = new Style{
.font = font::fixed_8x16(),
.background = {31, 31, 0},
.foreground = Color::red(),
};
fg_green = new Style{
.font = font::fixed_8x16(),
.background = {31, 31, 0},
.foreground = Color::green(),
};
fg_yellow = new Style{
.font = font::fixed_8x16(),
.background = {31, 31, 0},
.foreground = Color::yellow(),
};
fg_orange = new Style{
.font = font::fixed_8x16(),
.background = {31, 31, 0},
.foreground = Color::orange(),
};
fg_blue = new Style{
.font = font::fixed_8x16(),
.background = {31, 31, 0},
.foreground = Color::blue(),
};
fg_cyan = new Style{
.font = font::fixed_8x16(),
.background = {31, 31, 0},
.foreground = Color::cyan(),
};
fg_darkcyan = new Style{
.font = font::fixed_8x16(),
.background = {31, 31, 0},
.foreground = Color::dark_cyan(),
};
fg_magenta = new Style{
.font = font::fixed_8x16(),
.background = {31, 31, 0},
.foreground = Color::magenta(),
};
option_active = new Style{
.font = font::fixed_8x16(),
.background = Color::orange(),
.foreground = Color::white(),
};
status_active = new Color{0, 255, 0}; // green, the status bar icons when active
bg_table_header = new Color{205, 205, 0};
}
ThemeAqua::ThemeAqua() {
bg_lightest = new Style{
.font = font::fixed_8x16(),
.background = {204, 255, 255},
.foreground = Color::black(),
};
bg_lightest_small = new Style{
.font = font::fixed_5x8(),
.background = {204, 255, 255},
.foreground = Color::black(),
};
bg_light = new Style{
.font = font::fixed_8x16(),
.background = {102, 255, 255},
.foreground = Color::white(),
};
bg_medium = new Style{
.font = font::fixed_8x16(),
.background = {0, 144, 200},
.foreground = Color::white(),
};
bg_dark = new Style{
.font = font::fixed_8x16(),
.background = {0, 153, 153},
.foreground = Color::white(),
};
bg_darker = new Style{
.font = font::fixed_8x16(),
.background = {0, 102, 102},
.foreground = Color::white(),
};
bg_darkest = new Style{
.font = font::fixed_8x16(),
.background = {0, 31, 31},
.foreground = Color::white(),
};
bg_darkest_small = new Style{
.font = font::fixed_5x8(),
.background = {0, 31, 31},
.foreground = Color::white(),
};
bg_important_small = new Style{
.font = font::fixed_5x8(),
.background = Color::yellow(),
.foreground = {0, 31, 31},
};
error_dark = new Style{
.font = font::fixed_8x16(),
.background = {0, 31, 31},
.foreground = Color::red(),
};
warning_dark = new Style{
.font = font::fixed_8x16(),
.background = {0, 31, 31},
.foreground = Color::yellow(),
};
ok_dark = new Style{
.font = font::fixed_8x16(),
.background = {0, 31, 31},
.foreground = Color::green(),
};
fg_dark = new Style{
.font = font::fixed_8x16(),
.background = {0, 31, 31},
.foreground = {0, 153, 153},
};
fg_medium = new Style{
.font = font::fixed_8x16(),
.background = {0, 31, 31},
.foreground = {0, 204, 204},
};
fg_light = new Style{
.font = font::fixed_8x16(),
.background = {0, 31, 31},
.foreground = Color::light_grey(),
};
fg_red = new Style{
.font = font::fixed_8x16(),
.background = {0, 31, 31},
.foreground = Color::red(),
};
fg_green = new Style{
.font = font::fixed_8x16(),
.background = {31, 31, 0},
.foreground = Color::green(),
};
fg_yellow = new Style{
.font = font::fixed_8x16(),
.background = {0, 31, 31},
.foreground = Color::yellow(),
};
fg_orange = new Style{
.font = font::fixed_8x16(),
.background = {0, 31, 31},
.foreground = Color::orange(),
};
fg_blue = new Style{
.font = font::fixed_8x16(),
.background = {0, 31, 31},
.foreground = Color::blue(),
};
fg_cyan = new Style{
.font = font::fixed_8x16(),
.background = {0, 31, 31},
.foreground = Color::cyan(),
};
fg_darkcyan = new Style{
.font = font::fixed_8x16(),
.background = {0, 31, 31},
.foreground = Color::dark_cyan(),
};
fg_magenta = new Style{
.font = font::fixed_8x16(),
.background = {0, 31, 31},
.foreground = Color::magenta(),
};
option_active = new Style{
.font = font::fixed_8x16(),
.background = Color::blue(),
.foreground = Color::white(),
};
status_active = new Color{0, 255, 0}; // green, the status bar icons when active
bg_table_header = new Color{0, 205, 205};
}
ThemeDefault::ThemeDefault() {
bg_lightest = new Style{
.font = font::fixed_8x16(),
.background = Color::white(),
.foreground = Color::black(),
};
bg_lightest_small = new Style{
.font = font::fixed_5x8(),
.background = Color::white(),
.foreground = Color::black(),
};
bg_light = new Style{
.font = font::fixed_8x16(),
.background = Color::light_grey(),
.foreground = Color::white(),
};
bg_medium = new Style{
.font = font::fixed_8x16(),
.background = Color::grey(),
.foreground = Color::white(),
};
bg_dark = new Style{
.font = font::fixed_8x16(),
.background = Color::dark_grey(),
.foreground = Color::white(),
};
bg_darker = new Style{
.font = font::fixed_8x16(),
.background = Color::darker_grey(),
.foreground = Color::white(),
};
bg_darkest = new Style{
.font = font::fixed_8x16(),
.background = Color::black(),
.foreground = Color::white(),
};
bg_darkest_small = new Style{
.font = font::fixed_5x8(),
.background = Color::black(),
.foreground = Color::white(),
};
bg_important_small = new Style{
.font = font::fixed_5x8(),
.background = Color::yellow(),
.foreground = Color::black(),
};
error_dark = new Style{
.font = font::fixed_8x16(),
.background = Color::black(),
.foreground = Color::red(),
};
warning_dark = new Style{
.font = font::fixed_8x16(),
.background = Color::black(),
.foreground = Color::yellow(),
};
ok_dark = new Style{
.font = font::fixed_8x16(),
.background = Color::black(),
.foreground = Color::green(),
};
fg_dark = new Style{
.font = font::fixed_8x16(),
.background = Color::black(),
.foreground = Color::dark_grey(),
};
fg_medium = new Style{
.font = font::fixed_8x16(),
.background = Color::black(),
.foreground = Color::grey(),
};
fg_light = new Style{
.font = font::fixed_8x16(),
.background = Color::black(),
.foreground = Color::light_grey(),
};
fg_red = new Style{
.font = font::fixed_8x16(),
.background = Color::black(),
.foreground = Color::red(),
};
fg_green = new Style{
.font = font::fixed_8x16(),
.background = Color::black(),
.foreground = Color::green(),
};
fg_yellow = new Style{
.font = font::fixed_8x16(),
.background = Color::black(),
.foreground = Color::yellow(),
};
fg_orange = new Style{
.font = font::fixed_8x16(),
.background = Color::black(),
.foreground = Color::orange(),
};
fg_blue = new Style{
.font = font::fixed_8x16(),
.background = Color::black(),
.foreground = Color::blue(),
};
fg_cyan = new Style{
.font = font::fixed_8x16(),
.background = Color::black(),
.foreground = Color::cyan(),
};
fg_darkcyan = new Style{
.font = font::fixed_8x16(),
.background = Color::black(),
.foreground = Color::dark_cyan(),
};
fg_magenta = new Style{
.font = font::fixed_8x16(),
.background = Color::black(),
.foreground = Color::magenta(),
};
option_active = new Style{
.font = font::fixed_8x16(),
.background = Color::blue(),
.foreground = Color::white(),
};
status_active = new Color{0, 255, 0}; // green, the status bar icons when active
bg_table_header = new Color{0, 0, 255};
}
ThemeGreen::ThemeGreen() {
bg_lightest = new Style{
.font = font::fixed_8x16(),
.background = {0, 245, 29},
.foreground = Color::black(),
};
bg_lightest_small = new Style{
.font = font::fixed_5x8(),
.background = {0, 245, 29},
.foreground = Color::black(),
};
bg_light = new Style{
.font = font::fixed_8x16(),
.background = {0, 212, 25},
.foreground = Color::white(),
};
bg_medium = new Style{
.font = font::fixed_8x16(),
.background = {0, 143, 17},
.foreground = Color::white(),
};
bg_dark = new Style{
.font = font::fixed_8x16(),
.background = {0, 99, 12},
.foreground = Color::white(),
};
bg_darker = new Style{
.font = font::fixed_8x16(),
.background = {0, 79, 9},
.foreground = Color::white(),
};
bg_darkest = new Style{
.font = font::fixed_8x16(),
.background = {0, 33, 4},
.foreground = Color::white(),
};
bg_darkest_small = new Style{
.font = font::fixed_5x8(),
.background = {0, 33, 4},
.foreground = Color::white(),
};
bg_important_small = new Style{
.font = font::fixed_5x8(),
.background = Color::yellow(),
.foreground = {0, 33, 4},
};
error_dark = new Style{
.font = font::fixed_8x16(),
.background = {0, 33, 4},
.foreground = Color::red(),
};
warning_dark = new Style{
.font = font::fixed_8x16(),
.background = {0, 33, 4},
.foreground = Color::yellow(),
};
ok_dark = new Style{
.font = font::fixed_8x16(),
.background = {0, 33, 4},
.foreground = Color::green(),
};
fg_dark = new Style{
.font = font::fixed_8x16(),
.background = {0, 33, 4},
.foreground = {0, 99, 12},
};
fg_medium = new Style{
.font = font::fixed_8x16(),
.background = {0, 33, 4},
.foreground = {0, 143, 17},
};
fg_light = new Style{
.font = font::fixed_8x16(),
.background = {0, 33, 4},
.foreground = Color::light_grey(),
};
fg_red = new Style{
.font = font::fixed_8x16(),
.background = {0, 33, 4},
.foreground = Color::red(),
};
fg_green = new Style{
.font = font::fixed_8x16(),
.background = {0, 33, 4},
.foreground = Color::green(),
};
fg_yellow = new Style{
.font = font::fixed_8x16(),
.background = {0, 33, 4},
.foreground = Color::yellow(),
};
fg_orange = new Style{
.font = font::fixed_8x16(),
.background = {0, 33, 4},
.foreground = Color::orange(),
};
fg_blue = new Style{
.font = font::fixed_8x16(),
.background = {0, 33, 4},
.foreground = Color::blue(),
};
fg_cyan = new Style{
.font = font::fixed_8x16(),
.background = {0, 33, 4},
.foreground = Color::cyan(),
};
fg_darkcyan = new Style{
.font = font::fixed_8x16(),
.background = {0, 33, 4},
.foreground = Color::dark_cyan(),
};
fg_magenta = new Style{
.font = font::fixed_8x16(),
.background = {0, 33, 4},
.foreground = Color::magenta(),
};
option_active = new Style{
.font = font::fixed_8x16(),
.background = Color::orange(),
.foreground = Color::white(),
};
status_active = new Color{0, 255, 0}; // green, the status bar icons when active
bg_table_header = new Color{0, 205, 30};
}
ThemeRed::ThemeRed() {
bg_lightest = new Style{
.font = font::fixed_8x16(),
.background = {245, 29, 0},
.foreground = Color::black(),
};
bg_lightest_small = new Style{
.font = font::fixed_5x8(),
.background = {245, 29, 0},
.foreground = Color::black(),
};
bg_light = new Style{
.font = font::fixed_8x16(),
.background = {212, 25, 0},
.foreground = Color::white(),
};
bg_medium = new Style{
.font = font::fixed_8x16(),
.background = {143, 17, 0},
.foreground = Color::white(),
};
bg_dark = new Style{
.font = font::fixed_8x16(),
.background = {99, 12, 0},
.foreground = Color::white(),
};
bg_darker = new Style{
.font = font::fixed_8x16(),
.background = {79, 9, 0},
.foreground = Color::white(),
};
bg_darkest = new Style{
.font = font::fixed_8x16(),
.background = {33, 4, 0},
.foreground = Color::white(),
};
bg_darkest_small = new Style{
.font = font::fixed_5x8(),
.background = {33, 4, 0},
.foreground = Color::white(),
};
bg_important_small = new Style{
.font = font::fixed_5x8(),
.background = Color::yellow(),
.foreground = {33, 4, 0},
};
error_dark = new Style{
.font = font::fixed_8x16(),
.background = {33, 4, 0},
.foreground = Color::red(),
};
warning_dark = new Style{
.font = font::fixed_8x16(),
.background = {33, 4, 0},
.foreground = Color::yellow(),
};
ok_dark = new Style{
.font = font::fixed_8x16(),
.background = {33, 4, 0},
.foreground = Color::green(),
};
fg_dark = new Style{
.font = font::fixed_8x16(),
.background = {33, 4, 0},
.foreground = {99, 12, 0},
};
fg_medium = new Style{
.font = font::fixed_8x16(),
.background = {33, 4, 0},
.foreground = {143, 17, 0},
};
fg_light = new Style{
.font = font::fixed_8x16(),
.background = {33, 4, 0},
.foreground = Color::light_grey(),
};
fg_red = new Style{
.font = font::fixed_8x16(),
.background = {33, 4, 0},
.foreground = Color::red(),
};
fg_green = new Style{
.font = font::fixed_8x16(),
.background = {33, 4, 0},
.foreground = Color::green(),
};
fg_yellow = new Style{
.font = font::fixed_8x16(),
.background = {33, 4, 0},
.foreground = Color::yellow(),
};
fg_orange = new Style{
.font = font::fixed_8x16(),
.background = {33, 4, 0},
.foreground = Color::orange(),
};
fg_blue = new Style{
.font = font::fixed_8x16(),
.background = {33, 4, 0},
.foreground = Color::blue(),
};
fg_cyan = new Style{
.font = font::fixed_8x16(),
.background = {33, 4, 0},
.foreground = Color::cyan(),
};
fg_darkcyan = new Style{
.font = font::fixed_8x16(),
.background = {33, 4, 0},
.foreground = Color::dark_cyan(),
};
fg_magenta = new Style{
.font = font::fixed_8x16(),
.background = {33, 4, 0},
.foreground = Color::magenta(),
};
option_active = new Style{
.font = font::fixed_8x16(),
.background = Color::orange(),
.foreground = Color::white(),
};
status_active = new Color{0, 255, 0}; // green, the status bar icons when active
bg_table_header = new Color{205, 30, 0};
}
ThemeDark::ThemeDark() {
bg_lightest = new Style{
.font = font::fixed_8x16(),
.background = {32, 32, 32},
.foreground = Color::white(),
};
bg_lightest_small = new Style{
.font = font::fixed_5x8(),
.background = {32, 32, 32},
.foreground = Color::white(),
};
bg_light = new Style{
.font = font::fixed_8x16(),
.background = {24, 24, 24},
.foreground = Color::white(),
};
bg_medium = new Style{
.font = font::fixed_8x16(),
.background = {16, 16, 16},
.foreground = Color::white(),
};
bg_dark = new Style{
.font = font::fixed_8x16(),
.background = {8, 8, 8},
.foreground = Color::white(),
};
bg_darker = new Style{
.font = font::fixed_8x16(),
.background = {4, 4, 4},
.foreground = Color::white(),
};
bg_darkest = new Style{
.font = font::fixed_8x16(),
.background = Color::black(),
.foreground = Color::white(),
};
bg_darkest_small = new Style{
.font = font::fixed_5x8(),
.background = Color::black(),
.foreground = Color::white(),
};
bg_important_small = new Style{
.font = font::fixed_5x8(),
.background = {64, 64, 64},
.foreground = Color::white(),
};
error_dark = new Style{
.font = font::fixed_8x16(),
.background = Color::black(),
.foreground = Color::red(),
};
warning_dark = new Style{
.font = font::fixed_8x16(),
.background = Color::black(),
.foreground = Color::yellow(),
};
ok_dark = new Style{
.font = font::fixed_8x16(),
.background = Color::black(),
.foreground = Color::green(),
};
fg_dark = new Style{
.font = font::fixed_8x16(),
.background = Color::black(),
.foreground = {96, 96, 96},
};
fg_medium = new Style{
.font = font::fixed_8x16(),
.background = Color::black(),
.foreground = {128, 128, 128},
};
fg_light = new Style{
.font = font::fixed_8x16(),
.background = Color::black(),
.foreground = Color::white(),
};
fg_red = new Style{
.font = font::fixed_8x16(),
.background = Color::black(),
.foreground = Color::red(),
};
fg_green = new Style{
.font = font::fixed_8x16(),
.background = Color::black(),
.foreground = Color::green(),
};
fg_yellow = new Style{
.font = font::fixed_8x16(),
.background = Color::black(),
.foreground = Color::yellow(),
};
fg_orange = new Style{
.font = font::fixed_8x16(),
.background = Color::black(),
.foreground = Color::orange(),
};
fg_blue = new Style{
.font = font::fixed_8x16(),
.background = Color::black(),
.foreground = Color::blue(),
};
fg_cyan = new Style{
.font = font::fixed_8x16(),
.background = Color::black(),
.foreground = Color::cyan(),
};
fg_darkcyan = new Style{
.font = font::fixed_8x16(),
.background = Color::black(),
.foreground = Color::dark_cyan(),
};
fg_magenta = new Style{
.font = font::fixed_8x16(),
.background = Color::black(),
.foreground = Color::magenta(),
};
option_active = new Style{
.font = font::fixed_8x16(),
.background = {64, 64, 64},
.foreground = Color::white(),
};
status_active = new Color{0, 255, 0};
bg_table_header = new Color{48, 48, 48};
}
} // namespace ui
+122
View File
@@ -0,0 +1,122 @@
/*
* Copyright (C) 2024 HTotoo
*
* 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 __THEME_H__
#define __THEME_H__
#include <cstdint>
#include <cstddef>
#include <memory>
#include "ui_painter.hpp"
#include "ui_font_fixed_5x8.hpp"
#include "ui_font_fixed_8x16.hpp"
namespace ui {
class ThemeTemplate {
public:
~ThemeTemplate();
Style* bg_lightest;
Style* bg_lightest_small;
Style* bg_light;
Style* bg_medium;
Style* bg_dark;
Style* bg_darker;
Style* bg_darkest;
Style* bg_darkest_small;
Style* bg_important_small;
Style* error_dark;
Style* warning_dark;
Style* ok_dark;
Style* fg_dark;
Style* fg_medium;
Style* fg_light;
Style* fg_red;
Style* fg_green;
Style* fg_yellow;
Style* fg_orange;
Style* fg_blue;
Style* fg_cyan;
Style* fg_darkcyan;
Style* fg_magenta;
Style* option_active;
Color* status_active; // green, the status bar icons when active
Color* bg_table_header;
};
class ThemeDefault : public ThemeTemplate {
public:
ThemeDefault();
};
class ThemeYellow : public ThemeTemplate {
public:
ThemeYellow();
};
class ThemeAqua : public ThemeTemplate {
public:
ThemeAqua();
};
class ThemeGreen : public ThemeTemplate {
public:
ThemeGreen();
};
class ThemeRed : public ThemeTemplate {
public:
ThemeRed();
};
class ThemeDark : public ThemeTemplate {
public:
ThemeDark();
};
class Theme {
public:
enum ThemeId {
DefaultGrey = 0,
Yellow = 1,
Aqua = 2,
Green = 3,
Red = 4,
Dark = 5,
MAX
};
static ThemeTemplate* getInstance();
static void SetTheme(ThemeId theme);
static ThemeTemplate* current;
static void destroy();
private:
};
} // namespace ui
#endif /*__THEME_H__*/
+132
View File
@@ -0,0 +1,132 @@
/*
* Copyright (C) 2014 Jared Boone, ShareBrained Technology, Inc.
*
* This file is part of PortaPack.
*
* This program is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation; either version 2, or (at your option)
* any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program; see the file COPYING. If not, write to
* the Free Software Foundation, Inc., 51 Franklin Street,
* Boston, MA 02110-1301, USA.
*/
#include "ui.hpp"
// #include "irq_controls.hpp"
#include "sine_table.hpp"
#include "utility.hpp"
#include <algorithm>
namespace ui
{
// CGA palette
// Index into this table should match STR_COLOR_ escape string in ui.hpp
Color term_colors[16] = {
Color::black(),
Color::dark_blue(),
Color::dark_green(),
Color::dark_cyan(),
Color::dark_red(),
Color::dark_magenta(),
Color::dark_yellow(),
Color::light_grey(),
Color::dark_grey(),
Color::blue(),
Color::green(),
Color::cyan(),
Color::red(),
Color::magenta(),
Color::yellow(),
Color::white()};
bool Rect::contains(const Point p) const
{
return (p.x() >= left()) && (p.y() >= top()) &&
(p.x() < right()) && (p.y() < bottom());
}
Rect Rect::intersect(const Rect &o) const
{
const auto x1 = std::max(left(), o.left());
const auto x2 = std::min(right(), o.right());
const auto y1 = std::max(top(), o.top());
const auto y2 = std::min(bottom(), o.bottom());
if ((x2 >= x1) && (y2 > y1))
{
return {x1, y1, x2 - x1, y2 - y1};
}
else
{
return {};
}
}
// TODO: This violates the principle of least surprise!
// This does a union, but that might not be obvious from "+=" syntax.
Rect &Rect::operator+=(const Rect &p)
{
if (is_empty())
{
*this = p;
}
if (!p.is_empty())
{
const auto x1 = std::min(left(), p.left());
const auto y1 = std::min(top(), p.top());
_pos = {x1, y1};
const auto x2 = std::max(right(), p.right());
const auto y2 = std::max(bottom(), p.bottom());
_size = {x2 - x1, y2 - y1};
}
return *this;
}
Rect &Rect::operator+=(const Point &p)
{
_pos += p;
return *this;
}
Rect &Rect::operator-=(const Point &p)
{
_pos -= p;
return *this;
}
Point polar_to_point(float angle, uint32_t distance)
{
// polar to compass with y negated for screen drawing
return Point(sin_f32(DEG_TO_RAD(-angle) + pi) * distance,
sin_f32(DEG_TO_RAD(-angle) - (pi / 2)) * distance);
}
Point fast_polar_to_point(int32_t angle, uint32_t distance)
{
// polar to compass with y negated for screen drawing
return Point((int16_sin_s4(((1 << 16) * (-angle + 180)) / 360) * distance) / (1 << 16),
(int16_sin_s4(((1 << 16) * (-angle - 90)) / 360) * distance) / (1 << 16));
}
bool key_is_long_pressed(KeyEvent key)
{
if (key < KeyEvent::Back)
{
// TODO: this would make more sense as a flag on KeyEvent
// passed up to the UI via event dispatch.
return false; // TODO: wire standalone api: switch_is_long_pressed(static_cast<Switch>(key));
}
return false;
}
} /* namespace ui */
+410
View File
@@ -0,0 +1,410 @@
/*
* Copyright (C) 2014 Jared Boone, ShareBrained Technology, Inc.
*
* 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_H__
#define __UI_H__
#include <cstdint>
namespace ui {
// Escape sequences for colored text; second character is index into term_colors[]
#define STR_COLOR_BLACK "\x1B\x00"
#define STR_COLOR_DARK_BLUE "\x1B\x01"
#define STR_COLOR_DARK_GREEN "\x1B\x02"
#define STR_COLOR_DARK_CYAN "\x1B\x03"
#define STR_COLOR_DARK_RED "\x1B\x04"
#define STR_COLOR_DARK_MAGENTA "\x1B\x05"
#define STR_COLOR_DARK_YELLOW "\x1B\x06"
#define STR_COLOR_LIGHT_GREY "\x1B\x07"
#define STR_COLOR_DARK_GREY "\x1B\x08"
#define STR_COLOR_BLUE "\x1B\x09"
#define STR_COLOR_GREEN "\x1B\x0A"
#define STR_COLOR_CYAN "\x1B\x0B"
#define STR_COLOR_RED "\x1B\x0C"
#define STR_COLOR_MAGENTA "\x1B\x0D"
#define STR_COLOR_YELLOW "\x1B\x0E"
#define STR_COLOR_WHITE "\x1B\x0F"
#define STR_COLOR_FOREGROUND "\x1B\x10"
#define DEG_TO_RAD(d) (d * (2 * pi) / 360.0)
using Coord = int16_t;
using Dim = int16_t;
constexpr uint16_t screen_width = 240;
constexpr uint16_t screen_height = 320;
/* Dimensions for the default font character. */
constexpr uint16_t char_width = 8;
constexpr uint16_t char_height = 16;
struct Color {
uint16_t v; // rrrrrGGGGGGbbbbb
constexpr Color()
: v{0} {
}
constexpr Color(
uint16_t v)
: v{v} {
}
constexpr Color(
uint8_t r,
uint8_t g,
uint8_t b)
: v{
static_cast<uint16_t>(
((r & 0xf8) << 8) | ((g & 0xfc) << 3) | ((b & 0xf8) >> 3))} {
}
uint8_t r() {
return (uint8_t)((v >> 8) & 0xf8);
}
uint8_t g() {
return (uint8_t)((v >> 3) & 0xfc);
}
uint8_t b() {
return (uint8_t)((v << 3) & 0xf8);
}
uint8_t to_greyscale() {
uint32_t r = (v >> 8) & 0xf8;
uint32_t g = (v >> 3) & 0xfc;
uint32_t b = (v << 3) & 0xf8;
uint32_t grey = ((r * 306) + (g * 601) + (b * 117)) >> 10;
return (uint8_t)grey;
}
uint16_t dark() {
// stripping bits 4 & 5 from each of the colors R/G/B
return (v & ((0xc8 << 8) | (0xcc << 3) | (0xc8 >> 3)));
}
Color operator-() const {
return (v ^ 0xffff);
}
/* Converts a 32-bit color into a 16-bit color.
* High byte is ignored. */
static constexpr Color RGB(uint32_t rgb) {
return {static_cast<uint8_t>((rgb >> 16) & 0xff),
static_cast<uint8_t>((rgb >> 8) & 0xff),
static_cast<uint8_t>(rgb & 0xff)};
}
static constexpr Color black() {
return {0, 0, 0};
}
static constexpr Color red() {
return {255, 0, 0};
}
static constexpr Color dark_red() {
return {159, 0, 0};
}
static constexpr Color orange() {
return {255, 175, 0};
}
static constexpr Color dark_orange() {
return {191, 95, 0};
}
static constexpr Color yellow() {
return {255, 255, 0};
}
static constexpr Color dark_yellow() {
return {191, 191, 0};
}
static constexpr Color green() {
return {0, 255, 0};
}
static constexpr Color dark_green() {
return {0, 159, 0};
}
static constexpr Color blue() {
return {0, 0, 255};
}
static constexpr Color dark_blue() {
return {0, 0, 191};
}
static constexpr Color cyan() {
return {0, 255, 255};
}
static constexpr Color dark_cyan() {
return {0, 191, 191};
}
static constexpr Color magenta() {
return {255, 0, 255};
}
static constexpr Color dark_magenta() {
return {191, 0, 191};
}
static constexpr Color white() {
return {255, 255, 255};
}
static constexpr Color light_grey() {
return {191, 191, 191};
}
static constexpr Color grey() {
return {127, 127, 127};
}
static constexpr Color dark_grey() {
return {63, 63, 63};
}
static constexpr Color darker_grey() {
return {31, 31, 31};
}
static constexpr Color purple() {
return {204, 0, 102};
}
};
extern Color term_colors[16];
struct ColorRGB888 {
uint8_t r;
uint8_t g;
uint8_t b;
};
struct Point {
private:
Coord _x;
Coord _y;
public:
constexpr Point()
: _x{0},
_y{0} {
}
constexpr Point(
int x,
int y)
: _x{static_cast<Coord>(x)},
_y{static_cast<Coord>(y)} {
}
constexpr int x() const {
return _x;
}
constexpr int y() const {
return _y;
}
constexpr Point operator-() const {
return {-_x, -_y};
}
constexpr Point operator+(const Point& p) const {
return {_x + p._x, _y + p._y};
}
constexpr Point operator-(const Point& p) const {
return {_x - p._x, _y - p._y};
}
Point& operator+=(const Point& p) {
_x += p._x;
_y += p._y;
return *this;
}
Point& operator-=(const Point& p) {
_x -= p._x;
_y -= p._y;
return *this;
}
};
struct Size {
private:
Dim _w;
Dim _h;
public:
constexpr Size()
: _w{0},
_h{0} {
}
constexpr Size(
int w,
int h)
: _w{static_cast<Dim>(w)},
_h{static_cast<Dim>(h)} {
}
int width() const {
return _w;
}
int height() const {
return _h;
}
bool is_empty() const {
return (_w < 1) || (_h < 1);
}
};
struct Rect {
private:
Point _pos;
Size _size;
public:
constexpr Rect()
: _pos{},
_size{} {
}
constexpr Rect(
int x,
int y,
int w,
int h)
: _pos{x, y},
_size{w, h} {
}
constexpr Rect(
Point pos,
Size size)
: _pos(pos),
_size(size) {
}
Point location() const {
return _pos;
}
Size size() const {
return _size;
}
int top() const {
return _pos.y();
}
int bottom() const {
return _pos.y() + _size.height();
}
int left() const {
return _pos.x();
}
int right() const {
return _pos.x() + _size.width();
}
int width() const {
return _size.width();
}
int height() const {
return _size.height();
}
Point center() const {
return {_pos.x() + _size.width() / 2, _pos.y() + _size.height() / 2};
}
bool is_empty() const {
return _size.is_empty();
}
bool contains(const Point p) const;
Rect intersect(const Rect& o) const;
Rect operator+(const Point& p) const {
return {_pos + p, _size};
}
Rect& operator+=(const Rect& p);
Rect& operator+=(const Point& p);
Rect& operator-=(const Point& p);
operator bool() const {
return !_size.is_empty();
}
};
struct Bitmap {
const Size size;
const uint8_t* const data;
};
enum class KeyEvent : uint8_t {
/* Ordinals map to bit positions reported by CPLD */
Right = 0,
Left = 1,
Down = 2,
Up = 3,
Select = 4,
Dfu = 5,
Back = 6, /* Left and Up together */
};
using EncoderEvent = int32_t;
using KeyboardEvent = uint8_t;
struct TouchEvent {
enum class Type : uint32_t {
Start = 0,
Move = 1,
End = 2,
};
Point point;
Type type;
};
Point polar_to_point(float angle, uint32_t distance);
Point fast_polar_to_point(int32_t angle, uint32_t distance);
/* Default font glyph size. */
constexpr Size char_size{char_width, char_height};
bool key_is_long_pressed(KeyEvent key);
} /* namespace ui */
#endif /*__UI_H__*/
+211
View File
@@ -0,0 +1,211 @@
/*
* Copyright (C) 2015 Jared Boone, ShareBrained Technology, Inc.
* Copyright (C) 2016 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_focus.hpp"
#include "ui_widget.hpp"
#include <cstdint>
#include <algorithm>
#include <functional>
#include <utility>
#include <vector>
#define pow2(x) ((x) * (x))
namespace ui {
Widget* FocusManager::focus_widget() const {
return focus_widget_;
}
void FocusManager::set_focus_widget(Widget* const new_focus_widget) {
// Widget already has focus.
if (new_focus_widget == focus_widget()) {
return;
}
if (new_focus_widget) {
if (!new_focus_widget->focusable())
return;
}
// Blur old widget.
// NB: This will crash if the focus_widget is a destroyed instance.
if (focus_widget()) {
focus_widget()->on_blur();
focus_widget()->set_dirty();
}
focus_widget_ = new_focus_widget;
if (focus_widget()) {
focus_widget()->on_focus();
focus_widget()->set_dirty();
}
}
using test_result_t = std::pair<Widget* const, const uint32_t>;
using test_fn = std::function<test_result_t(Widget* const)>;
using test_collection_t = std::vector<test_result_t>;
/* Walk all visible widgets in hierarchy, collecting those that pass test */
template <typename TestFn>
static void widget_collect_visible(Widget* const w, TestFn test, test_collection_t& collection) {
for (auto child : w->children()) {
if (!child->hidden()) {
const auto result = test(child);
if (result.first) {
collection.push_back(result);
}
widget_collect_visible(child, test, collection);
}
}
}
static int32_t rect_distances(
const KeyEvent direction,
const Rect& rect_start,
const Rect& rect_end) {
Coord on_axis_max, on_axis_min;
switch (direction) {
case KeyEvent::Right:
on_axis_max = rect_end.left();
on_axis_min = rect_start.right();
break;
case KeyEvent::Left:
on_axis_max = rect_start.left();
on_axis_min = rect_end.right();
break;
case KeyEvent::Down:
on_axis_max = rect_end.top();
on_axis_min = rect_start.bottom();
break;
case KeyEvent::Up:
on_axis_max = rect_start.top();
on_axis_min = rect_end.bottom();
break;
default:
return -1;
}
Coord on_axis_distance = on_axis_max - on_axis_min;
if (on_axis_distance < 0) {
return -1;
}
Coord perpendicular_axis_start, perpendicular_axis_end;
switch (direction) {
case KeyEvent::Right:
case KeyEvent::Left:
perpendicular_axis_start = rect_start.center().y();
perpendicular_axis_end = rect_end.center().y();
break;
case KeyEvent::Up:
case KeyEvent::Down:
perpendicular_axis_start = rect_start.center().x();
perpendicular_axis_end = rect_end.center().x();
break;
default:
return -1;
}
switch (direction) {
case KeyEvent::Right:
case KeyEvent::Left:
return pow2(std::abs(perpendicular_axis_end - perpendicular_axis_start) + 1) * (on_axis_distance + 1);
break;
case KeyEvent::Up:
case KeyEvent::Down:
return (std::abs(perpendicular_axis_end - perpendicular_axis_start) + 1) * pow2(on_axis_distance + 1);
break;
default:
return 0;
}
}
void FocusManager::update(
Widget* const top_widget,
const KeyEvent event) {
if (focus_widget()) {
const auto focus_screen_rect = focus_widget()->screen_rect();
const auto test_fn = [&focus_screen_rect, event](ui::Widget* const w) -> test_result_t {
// if( w->visible() && w->focusable() ) {
if (w->focusable()) {
const auto distance = rect_distances(event, focus_screen_rect, w->screen_rect());
if (distance >= 0) {
return {w, distance};
}
}
return {nullptr, 0};
};
const auto find_back_fn = [](ui::Widget* const w) -> test_result_t {
if (w->focusable() && (w->id == -1))
return {w, 0};
else
return {nullptr, 0};
};
test_collection_t collection;
widget_collect_visible(top_widget, test_fn, collection);
const auto compare_fn = [](const test_result_t& a, const test_result_t& b) {
return a.second < b.second;
};
const auto nearest = std::min_element(collection.cbegin(), collection.cend(), compare_fn);
// Up and left to indicate back
if (event == KeyEvent::Back) {
collection.clear();
widget_collect_visible(top_widget, find_back_fn, collection);
if (!collection.empty())
set_focus_widget(collection[0].first);
} else if (nearest != collection.cend()) {
// focus->blur();
const auto new_focus = (*nearest).first;
set_focus_widget(new_focus);
} else {
if ((focus_widget()->id >= 0) && (event == KeyEvent::Left)) {
// Stuck left, move to back button
collection.clear();
widget_collect_visible(top_widget, find_back_fn, collection);
if (!collection.empty())
set_focus_widget(collection[0].first);
}
}
}
}
} /* namespace ui */
@@ -0,0 +1,45 @@
/*
* Copyright (C) 2015 Jared Boone, ShareBrained Technology, Inc.
*
* 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_FOCUS_H__
#define __UI_FOCUS_H__
#include "ui.hpp"
namespace ui {
class Widget;
class FocusManager {
public:
Widget* focus_widget() const;
void set_focus_widget(Widget* const new_focus_widget);
void update(Widget* const top_widget, const KeyEvent event);
// void update(Widget* const top_widget, const TouchEvent event);
private:
Widget* focus_widget_{nullptr};
};
} /* namespace ui */
#endif /*__UI_FOCUS_H__*/

Some files were not shown because too many files have changed in this diff Show More