mirror of
https://github.com/portapack-mayhem/mayhem-firmware.git
synced 2026-09-15 19:13:00 +00:00
Compare commits
17 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| b3edaf3cd0 | |||
| 9afd3a9dca | |||
| d80f41e9d7 | |||
| 46d87d95ae | |||
| 7626dff1ef | |||
| f101680521 | |||
| 325f295ee8 | |||
| b15bb59678 | |||
| 776c9bc7c9 | |||
| b9ebefc7c7 | |||
| 70ace4be8f | |||
| fe826d5d98 | |||
| 4dda7cfaa5 | |||
| e6d4081c06 | |||
| 4f738adc70 | |||
| d516191ccb | |||
| 29bdc539a3 |
@@ -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();
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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. */
|
||||
|
||||
@@ -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!
|
||||
|
||||
@@ -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");
|
||||
|
||||
@@ -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{
|
||||
|
||||
@@ -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.
|
||||
*
|
||||
|
||||
@@ -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.
|
||||
*
|
||||
|
||||
@@ -54,6 +54,7 @@ SondeView::SondeView(NavigationView& nav)
|
||||
&field_lna,
|
||||
&field_vga,
|
||||
&rssi,
|
||||
&channel,
|
||||
&field_volume,
|
||||
&check_log,
|
||||
&check_crc,
|
||||
|
||||
@@ -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_;
|
||||
|
||||
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
|
||||
@@ -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
|
||||
@@ -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
@@ -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
@@ -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
@@ -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,5 +1,5 @@
|
||||
/*
|
||||
* copyleft 2024 sommermorgentraum
|
||||
* copyleft 2024 zxkmm AKA zix aka sommermorgentraum
|
||||
*
|
||||
* This file is part of PortaPack.
|
||||
*
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* copyleft 2024 sommermorgentraum
|
||||
* copyleft 2024 zxkmm AKA zix aka sommermorgentraum
|
||||
*
|
||||
* This file is part of PortaPack.
|
||||
*
|
||||
|
||||
@@ -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,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
@@ -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
@@ -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,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,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
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* copyleft 2024 sommermorgentraum
|
||||
* copyleft 2024 zxkmm AKA zix aka sommermorgentraum
|
||||
*
|
||||
* This file is part of PortaPack.
|
||||
*
|
||||
|
||||
+1
-1
@@ -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&) {
|
||||
|
||||
@@ -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,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,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.
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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:
|
||||
};
|
||||
|
||||
|
||||
@@ -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.
|
||||
*
|
||||
|
||||
@@ -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 */
|
||||
|
||||
@@ -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,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,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) {
|
||||
|
||||
@@ -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.
|
||||
*
|
||||
|
||||
@@ -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,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,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.
|
||||
|
||||
@@ -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() {
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
};
|
||||
|
||||
|
||||
@@ -26,7 +26,9 @@
|
||||
#include <complex>
|
||||
#include <cmath>
|
||||
|
||||
constexpr float pi{3.141592653589793238462643383279502884f};
|
||||
#include "mathdef.hpp"
|
||||
|
||||
constexpr float pi{M_PI};
|
||||
|
||||
namespace std {
|
||||
|
||||
|
||||
@@ -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.
|
||||
*
|
||||
|
||||
@@ -0,0 +1,4 @@
|
||||
#pragma once
|
||||
|
||||
#define PI 3.1415926535897932384626433832795
|
||||
#define M_PI PI
|
||||
@@ -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,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
|
||||
*
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,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,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.
|
||||
*
|
||||
|
||||
@@ -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
|
||||
)
|
||||
|
||||
|
||||
@@ -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)
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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__*/
|
||||
@@ -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__*/
|
||||
@@ -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 */
|
||||
@@ -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__*/
|
||||
@@ -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 ×tamp)
|
||||
{
|
||||
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 ×tamp);
|
||||
|
||||
// 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__*/
|
||||
@@ -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
|
||||
@@ -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__*/
|
||||
@@ -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 */
|
||||
@@ -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__*/
|
||||
@@ -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
Reference in New Issue
Block a user