Compare commits

...

27 Commits

Author SHA1 Message Date
RocketGod 23cabb8b8a Make dynamic screen for PortaRF (#2800) 2025-10-02 11:31:33 -07:00
Totoo b3edaf3cd0 AIS name fix (#2793)
* AIS name fix
2025-09-17 22:40:46 +02:00
Totoo 9afd3a9dca Standalone imp (#2791)
* Moved ui elements to common folder in standalone, and add screen parameters to the api v4

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

* rename my name

* fix warnings

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

* remove wrong line
2025-08-26 19:56:39 +08:00
Totoo d516191ccb Update external.cmake (#2764)
Disable acars ext app, since not working
2025-08-22 10:44:55 +02:00
gullradriel 29bdc539a3 Fix BLE TX warning, regenerate bitmap.hpp (#2762)
* fix 'progress' variable warning, added a TODO
* up to date bitmap.hpp file, soundboard icon
2025-08-19 07:19:46 +02:00
Arne Luehrs 29bba4d0ea Epirb 406 v2 (#2761)
* Adding channel selection to EPIRB receiver

* UI enhancement, Packet error check and color display of error

* code formating
2025-08-18 09:09:37 +02:00
zxkmm a6f886ad0a - (#2759) 2025-08-16 10:50:52 +02:00
Totoo 2d2d9223f2 better rs41 (#2757) 2025-08-15 16:24:07 +02:00
zxkmm 6e33007cb6 move soundbrd app external (#2756) 2025-08-15 13:31:18 +02:00
zxkmm 3f7b59f27e frequency keypad opt (#2755)
* init

* revert changes that caused by batch replace

* revert changes that caused by batch replace

* revert changes that caused by batch replace

* revert submodu;e
2025-08-13 14:25:21 +02:00
Arne Luehrs 375d1ad54e Implementation of EPIRB receiver (#2754)
* Implementation of EPIRB receiver
* Baseband processing of EPIRB signal
* UI to ddecode and display EPIRB message with display on a map
* External application
* External proc element
* Delete CLAUDE.md
2025-08-13 14:24:18 +02:00
Netro 6b05878532 BLE RX/TX Changes (#2752)
* Work on BLE Rx Tx improvements.
* Working on compile size.
* cleanup
* Formatting
* Fixes
* More Improvements + Custom Parsing for Tags
* Moving ERT to external apps.
* Fix Icon.
2025-08-11 07:42:58 +02:00
RocketGod 3983749f11 Update ui_jammer.cpp (#2747)
Update warning to reflect actual range limit of bandwidth.
2025-07-30 20:08:00 +02:00
Totoo 28e2f770e5 fix miss (#2738) 2025-07-12 13:33:22 +02:00
167 changed files with 18717 additions and 1590 deletions
+1
View File
@@ -62,6 +62,7 @@ cmake-build-debug/
.vscode
.idea
*.swp
.claude
# VSCodium extensions
.history
-3
View File
@@ -270,13 +270,10 @@ set(CPPSRC
ui/ui_bmpview.cpp
apps/ais_app.cpp
apps/analog_audio_app.cpp
apps/ble_comm_app.cpp
apps/ble_rx_app.cpp
apps/ble_tx_app.cpp
apps/capture_app.cpp
apps/ert_app.cpp
apps/pocsag_app.cpp
apps/soundboard_app.cpp
apps/ui_about_simple.cpp
apps/ui_adsb_rx.cpp
apps/ui_aprs_rx.cpp
+12 -9
View File
@@ -223,9 +223,9 @@ void AISRecentEntry::update(const ais::Packet& packet) {
break;
case 5:
call_sign = packet.text(70, 7);
name = packet.text(112, 20);
destination = packet.text(302, 20);
call_sign = trim(packet.text(70, 7));
name = trim(packet.text(112, 20));
destination = trim(packet.text(302, 20));
break;
case 18:
@@ -238,7 +238,7 @@ void AISRecentEntry::update(const ais::Packet& packet) {
break;
case 21:
name = packet.text(43, 20);
name = trim(packet.text(43, 20));
last_position.timestamp = packet.received_at();
last_position.latitude = packet.latitude(192);
last_position.longitude = packet.longitude(164);
@@ -247,11 +247,11 @@ void AISRecentEntry::update(const ais::Packet& packet) {
case 24:
switch (packet.read(38, 2)) {
case 0:
name = packet.text(40, 20);
name = trim(packet.text(40, 20));
break;
case 1:
call_sign = packet.text(90, 7);
call_sign = trim(packet.text(90, 7));
break;
default:
@@ -324,7 +324,7 @@ AISRecentEntryDetailView& AISRecentEntryDetailView::operator=(const AISRecentEnt
void AISRecentEntryDetailView::update_position() {
if (send_updates)
geomap_view->update_position(ais::format::latlon_float(entry_.last_position.latitude.normalized()), ais::format::latlon_float(entry_.last_position.longitude.normalized()), (float)entry_.last_position.true_heading, 0, entry_.last_position.speed_over_ground > 1022 ? 0 : entry_.last_position.speed_over_ground);
geomap_view->update_position(ais::format::latlon_float(entry_.last_position.latitude.normalized()), ais::format::latlon_float(entry_.last_position.longitude.normalized()), (float)entry_.last_position.true_heading, 0, entry_.last_position.speed_over_ground > 1022 ? 0 : entry_.last_position.speed_over_ground / 10);
}
bool AISRecentEntryDetailView::add_map_marker(const AISRecentEntry& entry) {
@@ -456,7 +456,10 @@ void AISAppView::on_tick_second() {
++timer_seconds;
if (timer_seconds % 10 == 0) {
if (recent_entry_detail_view.hidden()) return;
recent_entry_detail_view.update_map_markers(recent);
if (got_new_packet) {
got_new_packet = false;
recent_entry_detail_view.update_map_markers(recent);
}
}
}
@@ -475,7 +478,7 @@ void AISAppView::on_packet(const ais::Packet& packet) {
if (logger) {
logger->on_packet(packet);
}
got_new_packet = true;
auto& entry = ::on_packet(recent, packet.source_id());
entry.update(packet);
recent_entries_view.set_dirty();
+1
View File
@@ -221,6 +221,7 @@ class AISAppView : public View {
};
SignalToken signal_token_tick_second{};
uint8_t timer_seconds = 0;
bool got_new_packet{false}; // got any new packet since latest screen update?
MessageHandlerRegistration message_handler_packet{
Message::ID::AISPacket,
+63 -105
View File
@@ -69,7 +69,7 @@ static std::uint64_t get_freq_by_channel_number(uint8_t channel_number) {
}
void BLECommView::focus() {
options_channel.focus();
field_frequency.focus();
}
BLECommView::BLECommView(NavigationView& nav)
@@ -79,10 +79,7 @@ BLECommView::BLECommView(NavigationView& nav)
&field_rf_amp,
&field_lna,
&field_vga,
&options_channel,
&field_frequency,
&label_send_adv,
&button_send_adv,
&check_log,
&label_packets_sent,
&text_packets_sent,
@@ -90,10 +87,6 @@ BLECommView::BLECommView(NavigationView& nav)
field_frequency.set_step(0);
button_send_adv.on_select = [this](ImageButton&) {
this->toggle();
};
check_log.set_value(logging);
check_log.on_select = [this](Checkbox&, bool v) {
@@ -104,27 +97,8 @@ BLECommView::BLECommView(NavigationView& nav)
logger->append(logs_dir.string() + "/BLELOG_" + to_string_timestamp(rtc_time::now()) + ".TXT");
};
options_channel.on_change = [this](size_t, int32_t i) {
// If we selected Auto don't do anything and Auto will handle changing.
if (i == 40) {
auto_channel = true;
return;
} else {
auto_channel = false;
}
field_frequency.set_value(get_freq_by_channel_number(i));
channel_number_rx = i;
channel_number_tx = i;
};
options_channel.set_selected_index(3, true);
logger = std::make_unique<BLECommLogger>();
// Generate new random Mac Address upon each new startup.
generateRandomMacAddress(randomMac);
// Setup Initial Advertise Packet.
advertisePacket = build_adv_packet();
}
@@ -145,86 +119,66 @@ bool BLECommView::in_tx_mode() const {
return (bool)is_running_tx;
}
void BLECommView::toggle() {
if (in_tx_mode()) {
sendAdvertisement(false);
} else {
sendAdvertisement(true);
}
}
BLETxPacket BLECommView::build_adv_packet() {
BLETxPacket bleTxPacket;
memset(&bleTxPacket, 0, sizeof(BLETxPacket));
std::string dataString = "11094861636b524620506f7274617061636b";
strncpy(bleTxPacket.macAddress, randomMac, 12);
std::string dataString = "02010603030F1807084861636B5246";
strncpy(bleTxPacket.advertisementData, dataString.c_str(), dataString.length());
// Duty cycle of 40% per 100ms advertisment periods.
strncpy(bleTxPacket.packetCount, "80", 3);
bleTxPacket.packet_count = 80;
strncpy(bleTxPacket.packetCount, "1", 2);
bleTxPacket.packet_count = 1;
bleTxPacket.packetType = PKT_TYPE_DISCOVERY;
bleTxPacket.pduType = PKT_TYPE_ADV_IND;
return bleTxPacket;
}
void BLECommView::sendAdvertisement(bool enable) {
if (enable) {
startTx(advertisePacket);
is_adv = true;
} else {
is_adv = false;
stopTx();
}
void BLECommView::sendAdvertisement(void) {
startTx(advertisePacket);
ble_state = Ble_State_Advertising;
}
void BLECommView::startTx(BLETxPacket packetToSend) {
int randomChannel = channel_number_tx;
switch_rx_tx(false);
if (auto_channel) {
int min = 37;
int max = 39;
currentPacket = packetToSend;
packet_counter = currentPacket.packet_count;
randomChannel = min + std::rand() % (max - min + 1);
field_frequency.set_value(get_freq_by_channel_number(randomChannel));
switch (advCount) {
case 0:
channel_number_tx = 37;
break;
case 1:
channel_number_tx = 38;
break;
case 2:
channel_number_tx = 39;
break;
}
if (!in_tx_mode()) {
switch_rx_tx(false);
field_frequency.set_value(get_freq_by_channel_number(37));
currentPacket = packetToSend;
packet_counter = currentPacket.packet_count;
advCount++;
button_send_adv.set_bitmap(&bitmap_stop);
baseband::set_btletx(randomChannel, currentPacket.macAddress, currentPacket.advertisementData, currentPacket.packetType);
transmitter_model.set_tx_gain(47);
transmitter_model.enable();
is_running_tx = true;
} else {
baseband::set_btletx(randomChannel, currentPacket.macAddress, currentPacket.advertisementData, currentPacket.packetType);
if (advCount == 3) {
advCount = 0;
}
if ((packet_counter % 10) == 0) {
text_packets_sent.set(to_string_dec_uint(packet_counter));
}
baseband::set_btletx(37, deviceMAC, currentPacket.advertisementData, currentPacket.pduType);
transmitter_model.set_tx_gain(47);
transmitter_model.enable();
is_sending = true;
packet_counter--;
is_running_tx = true;
}
void BLECommView::stopTx() {
button_send_adv.set_bitmap(&bitmap_play);
text_packets_sent.set(to_string_dec_uint(packet_counter));
switch_rx_tx(true);
baseband::set_btlerx(channel_number_rx);
field_frequency.set_value(get_freq_by_channel_number(channel_number_rx));
receiver_model.enable();
is_running_tx = false;
@@ -248,14 +202,12 @@ void BLECommView::on_data(BlePacketData* packet) {
parse_received_packet(packet, (ADV_PDU_TYPE)packet->type);
}
// called each 1/60th of second, so 6 = 100ms
void BLECommView::on_timer() {
// Send advertise burst only once every 100ms
if (++timer_counter == timer_period) {
timer_counter = 0;
if (!is_adv) {
sendAdvertisement(true);
if (ble_state == Ble_State_Receiving || ble_state == Ble_State_Idle) {
sendAdvertisement();
}
}
}
@@ -263,17 +215,18 @@ void BLECommView::on_timer() {
void BLECommView::on_tx_progress(const bool done) {
if (done) {
if (in_tx_mode()) {
is_sending = false;
ble_state = Ble_State_Receiving;
if (packet_counter == 0) {
if (is_adv) {
sendAdvertisement(false);
} else {
stopTx();
}
} else {
startTx(currentPacket);
}
timer_counter = 0;
timer_period = 12;
stopTx();
// else
// {
// startTx(advertisePacket);
// advCount++;
// }
}
}
}
@@ -309,23 +262,28 @@ void BLECommView::parse_received_packet(const BlePacketData* packet, ADV_PDU_TYP
}
if (pdu_type == SCAN_REQ || pdu_type == CONNECT_REQ) {
ADV_PDU_PAYLOAD_TYPE_1_3* directed_mac_data = (ADV_PDU_PAYLOAD_TYPE_1_3*)packet->data;
std::reverse(directed_mac_data->A1, directed_mac_data->A1 + 6);
console.clear(true);
std::string str_console = "";
std::string pduTypeStr = "";
if (pdu_type == SCAN_REQ) {
pduTypeStr += "SCAN_REQ";
} else if (pdu_type == CONNECT_REQ) {
pduTypeStr += "CONNECT_REQ";
}
ADV_PDU_PAYLOAD_TYPE_1_3* directed_mac_data = (ADV_PDU_PAYLOAD_TYPE_1_3*)packet->data;
str_console += "PACKET TYPE:" + pduTypeStr + "\n";
str_console += "MY MAC:" + to_string_formatted_mac_address(randomMac) + "\n";
str_console += "SCAN MAC:" + to_string_mac_address(directed_mac_data->A1, 6, false) + "\n";
console.write(str_console);
std::reverse(directed_mac_data->A1, directed_mac_data->A1 + 6);
std::string directedMAC = to_string_mac_address(directed_mac_data->A1, 6, false);
// Compare directed MAC Hex with the device MAC.
if (1) {
// std::string str_console = "Received SCAN_REQ from directed MAC: " + directedMAC + "\n";
// console.clear(true);
// console.writeln(str_console);
}
} else if (pdu_type == CONNECT_REQ) {
ADV_PDU_PAYLOAD_TYPE_5* connectReq = (ADV_PDU_PAYLOAD_TYPE_5*)packet->data;
std::reverse(connectReq->AdvA, connectReq->AdvA + 6);
std::string directedMAC = to_string_mac_address(connectReq->AdvA, 6, false);
std::string str_console = "Received CONNECT_REQ from directed MAC: " + directedMAC + "\n";
console.clear(true);
console.writeln(str_console);
}
}
}
+13 -12
View File
@@ -76,7 +76,16 @@ class BLECommView : public View {
void on_data(BlePacketData* packetData);
void on_tx_progress(const bool done);
void parse_received_packet(const BlePacketData* packet, ADV_PDU_TYPE pdu_type);
void sendAdvertisement(bool enable);
void sendAdvertisement(void);
typedef enum {
Ble_State_Idle = 0,
Ble_State_Advertising = 1,
Ble_State_Receiving = 2,
Ble_State_Sending = 3
} BleState;
BleState ble_state{Ble_State_Idle};
NavigationView& nav_;
@@ -101,8 +110,9 @@ class BLECommView : public View {
uint8_t channel_number_tx = 37;
uint8_t channel_number_rx = 37;
bool auto_channel = false;
uint8_t advCount = 0;
char randomMac[13] = "010203040506";
char deviceMAC[13] = "C23456789ABC";
bool is_running_tx = false;
bool is_sending = false;
@@ -111,7 +121,7 @@ class BLECommView : public View {
int16_t timer_period{6}; // Delay each packet by 16ms.
int16_t timer_counter = 0;
int16_t timer_rx_counter = 0;
int16_t timer_rx_period{12}; // Poll Rx for at least 200ms. (TBD)
int16_t timer_rx_period{12}; // Poll Rx for at least 150ms. (TBD)
uint32_t packet_counter{0};
BLETxPacket advertisePacket{};
@@ -147,15 +157,6 @@ class BLECommView : public View {
Channel channel{
{24 * 8, 5, 6 * 8, 4}};
Labels label_send_adv{
{{0 * 8, 2 * 8}, "Send Advertisement:", Theme::getInstance()->fg_light->foreground}};
ImageButton button_send_adv{
{21 * 8, 1 * 16, 10 * 8, 2 * 16},
&bitmap_play,
Theme::getInstance()->fg_green->foreground,
Theme::getInstance()->fg_green->background};
Checkbox check_log{
{24 * 8, 2 * 8},
3,
+282 -137
View File
@@ -36,6 +36,8 @@
#include "ui_textentry.hpp"
#include "usb_serial_asyncmsg.hpp"
#include <string_view>
using namespace portapack;
using namespace modems;
namespace fs = std::filesystem;
@@ -239,15 +241,14 @@ 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();
};
button_send.on_select = [this, &nav](const ui::Button&) {
auto packetToSend = build_packet();
auto packetToSend = build_packet(entry_);
nav.set_on_pop([packetToSend, &nav]() {
nav.replace<BLETxView>(packetToSend);
});
@@ -255,7 +256,7 @@ BleRecentEntryDetailView::BleRecentEntryDetailView(NavigationView& nav, const Bl
};
button_save.on_select = [this, &nav](const ui::Button&) {
auto packetToSave = build_packet();
auto packetToSave = build_packet(entry_);
packetFileBuffer = "";
text_prompt(
@@ -431,12 +432,11 @@ 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();
}
BLETxPacket BleRecentEntryDetailView::build_packet() {
BLETxPacket BleRecentEntryDetailView::build_packet(BleRecentEntry entry_) {
BLETxPacket bleTxPacket;
memset(&bleTxPacket, 0, sizeof(BLETxPacket));
@@ -444,8 +444,8 @@ BLETxPacket BleRecentEntryDetailView::build_packet() {
strncpy(bleTxPacket.macAddress, macAddressStr.c_str(), 12);
strncpy(bleTxPacket.advertisementData, entry_.dataString.c_str(), entry_.packetData.dataLen * 2);
strncpy(bleTxPacket.packetCount, "50", 3);
bleTxPacket.packet_count = 50;
strncpy(bleTxPacket.packetCount, "10", 3);
bleTxPacket.packet_count = 10;
return bleTxPacket;
}
@@ -495,16 +495,18 @@ BLERxView::BLERxView(NavigationView& nav)
&field_vga,
&options_channel,
&field_frequency,
&check_log,
&button_find,
&check_name,
&label_sort,
&options_sort,
&label_found,
&text_found_count,
&check_serial_log,
&button_filter,
&options_filter,
&check_name,
&check_log,
&check_serial_log,
&check_unique,
&check_duplicate_packets,
&button_find,
&label_found,
&text_found_count,
&button_save_list,
&button_clear_list,
&button_switch,
@@ -516,6 +518,29 @@ BLERxView::BLERxView(NavigationView& nav)
nav_.push<BleRecentEntryDetailView>(entry);
};
ensure_directory(find_packet_path);
ensure_directory(log_packets_path);
ensure_directory(packet_save_path);
// ------------------------------------------------------------------------------
// Handle Check Boxes
// ------------------------------------------------------------------------------
logger = std::make_unique<BLELogger>();
check_name.on_select = [this](Checkbox&, bool v) {
name_enable = v;
// update the include_name instance variable value of each entry in recent entries
setAllMembersToValue(recent, &BleRecentEntry::include_name, v);
recent_entries_view.set_dirty();
};
check_log.on_select = [this](Checkbox&, bool v) {
logging = v;
if (logger && logging)
logger->append(bletx_dir.string() + "/Packets/BLETx_" + to_string_timestamp(rtc_time::now()) + ".TXT");
};
check_serial_log.on_select = [this](Checkbox&, bool v) {
serial_logging = v;
if (v) {
@@ -524,12 +549,28 @@ BLERxView::BLERxView(NavigationView& nav)
portapack::async_tx_enabled = false;
}
};
check_unique.on_select = [this](Checkbox&, bool v) {
uniqueParsing = v;
recent.clear();
recent_entries_view.set_dirty();
};
check_duplicate_packets.on_select = [this](Checkbox&, bool v) {
duplicatePackets = v;
recent.clear();
recent_entries_view.set_dirty();
};
check_name.set_value(name_enable);
check_log.set_value(logging);
check_serial_log.set_value(serial_logging);
check_unique.set_value(uniqueParsing);
check_duplicate_packets.set_value(duplicatePackets);
ensure_directory(find_packet_path);
ensure_directory(log_packets_path);
ensure_directory(packet_save_path);
// ------------------------------------------------------------------------------
// Handle Buttons
// ------------------------------------------------------------------------------
filterBuffer = filter;
button_filter.on_select = [this](Button&) {
@@ -543,16 +584,6 @@ BLERxView::BLERxView(NavigationView& nav)
});
};
logger = std::make_unique<BLELogger>();
check_log.on_select = [this](Checkbox&, bool v) {
logging = v;
if (logger && logging)
logger->append(blerx_dir.string() + "/Logs/BLELOG_" + to_string_timestamp(rtc_time::now()) + ".TXT");
};
check_log.set_value(logging);
button_save_list.on_select = [this, &nav](const ui::Button&) {
listFileBuffer = "";
text_prompt(
@@ -574,17 +605,18 @@ BLERxView::BLERxView(NavigationView& nav)
nav.replace<BLETxView>();
};
field_frequency.set_step(0);
check_name.set_value(name_enable);
check_name.on_select = [this](Checkbox&, bool v) {
name_enable = v;
// update the include_name instance variable value of each entry in recent entries
setAllMembersToValue(recent, &BleRecentEntry::include_name, v);
recent_entries_view.set_dirty();
button_find.on_select = [this](Button&) {
auto open_view = nav_.push<FileLoadView>(".TXT");
open_view->on_changed = [this](std::filesystem::path new_file_path) {
on_file_changed(new_file_path);
};
};
// ------------------------------------------------------------------------------
// Handle Options
// ------------------------------------------------------------------------------
field_frequency.set_step(0);
options_channel.on_change = [this](size_t index, int32_t v) {
channel_index = (uint8_t)index;
@@ -608,10 +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);
uniqueParsing = filter_index == 2 ? true : false;
recent_entries_view.set_dirty();
};
@@ -619,15 +650,6 @@ BLERxView::BLERxView(NavigationView& nav)
options_sort.set_selected_index(sort_index, true);
options_filter.set_selected_index(filter_index, true);
button_find.on_select = [this](Button&) {
auto open_view = nav_.push<FileLoadView>(".TXT");
open_view->on_changed = [this](std::filesystem::path new_file_path) {
on_file_changed(new_file_path);
// nav_.set_on_pop([this]() { button_play.focus(); });
};
};
// Auto-configure modem for LCR RX (will be removed later)
baseband::set_btlerx(channel_number);
@@ -785,37 +807,28 @@ bool BLERxView::saveFile(const std::filesystem::path& path) {
}
void BLERxView::on_data(BlePacketData* packet) {
uint64_t macAddressEncoded = copy_mac_address_to_uint64(packet->macAddress);
uint64_t uniqueKeyEncoded = copy_mac_address_to_uint64(packet->macAddress);
// Start of Packet stuffing.
// Masking off the top 2 bytes to avoid invalid keys.
uint64_t key = macAddressEncoded & 0xFFFFFFFFFFFF;
bool packetExists = false;
uint64_t key = (uniqueKeyEncoded & 0xFFFFFFFFFFFF);
if (duplicatePackets) {
key |= ((uint64_t)packet->type) << 48;
}
// 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();
@@ -833,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.
@@ -866,12 +886,8 @@ void BLERxView::log_ble_packet(BlePacketData* packet) {
void BLERxView::on_filter_change(std::string value) {
// New filter? Reset list from recent entries.
if (filter != value) {
// resetFilteredEntries(recent, [&value](const BleRecentEntry& entry) {
// // return (entry.dataString.find(value) == std::string::npos) && (entry.nameString.find(value) == std::string::npos);
// return (entry.dataString.find(value) == std::string::npos) && (entry.nameString.find(value) == std::string::npos) && (to_string_mac_address(entry.packetData.macAddress, 6, false).find(value) == std::string::npos);
// });
filter = value;
handle_filter_options(options_filter.selected_index());
recent.clear();
}
}
@@ -947,7 +963,7 @@ void BLERxView::handle_entries_sort(uint8_t index) {
switch (index) {
case 0:
sortEntriesBy(
recent, [](const BleRecentEntry& entry) { return entry.macAddress; }, true);
recent, [](const BleRecentEntry& entry) { return entry.uniqueKey & 0xFFFFFFFFFFFF; }, true);
break;
case 1:
sortEntriesBy(
@@ -965,6 +981,10 @@ void BLERxView::handle_entries_sort(uint8_t index) {
sortEntriesBy(
recent, [](const BleRecentEntry& entry) { return entry.nameString; }, true);
break;
case 5:
sortEntriesBy(
recent, [](const BleRecentEntry& entry) { return entry.informationString; }, true);
break;
default:
break;
}
@@ -972,21 +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) {
case 0: // filter by Data
resetFilteredEntries(recent, [&value](const BleRecentEntry& entry) {
return (entry.dataString.find(value) == std::string::npos) && (entry.nameString.find(value) == std::string::npos);
});
break;
case 1: // filter by MAC address (All caps: e.g. AA:BB:CC:DD:EE:FF)
resetFilteredEntries(recent, [&value](const BleRecentEntry& entry) {
return (to_string_mac_address(entry.packetData.macAddress, 6, false).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;
}
}
@@ -1007,60 +1037,175 @@ bool BLERxView::updateEntry(const BlePacketData* packet, BleRecentEntry& entry,
bool success = false;
int i;
for (i = 0; i < packet->dataLen; i++) {
data_string += to_string_hex(packet->data[i], 2);
}
entry.dbValue = packet->max_dB - (receiver_model.lna() + receiver_model.vga() + (receiver_model.rf_amp() ? 14 : 0));
entry.timestamp = to_string_timestamp(rtc_time::now());
entry.dataString = data_string;
entry.packetData.type = packet->type;
entry.packetData.size = packet->size;
entry.packetData.dataLen = packet->dataLen;
// Mac Address of sender.
entry.packetData.macAddress[0] = packet->macAddress[0];
entry.packetData.macAddress[1] = packet->macAddress[1];
entry.packetData.macAddress[2] = packet->macAddress[2];
entry.packetData.macAddress[3] = packet->macAddress[3];
entry.packetData.macAddress[4] = packet->macAddress[4];
entry.packetData.macAddress[5] = packet->macAddress[5];
entry.pduType = pdu_type;
entry.channelNumber = channel_number;
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);
}
// Parse Data Section into buffer to be interpretted later.
for (int i = 0; i < packet->dataLen; i++) {
entry.packetData.data[i] = packet->data[i];
}
entry.include_name = check_name.value();
// Only parse name for advertisment packets and empty name entries
if (pdu_type == ADV_IND || pdu_type == ADV_NONCONN_IND || pdu_type == SCAN_RSP || pdu_type == ADV_SCAN_IND) {
if (uniqueParsing) {
// Add your unique beacon parsing function here.
success = parse_tracking_beacon_data(packet->data, packet->dataLen, entry.nameString, entry.informationString);
}
if (!success && !uniqueParsing) {
success = parse_beacon_data(packet->data, packet->dataLen, entry.nameString, entry.informationString);
}
} else if (pdu_type == ADV_DIRECT_IND || pdu_type == SCAN_REQ) {
ADV_PDU_PAYLOAD_TYPE_1_3* directed_mac_data = (ADV_PDU_PAYLOAD_TYPE_1_3*)entry.packetData.data;
reverse_byte_array(directed_mac_data->A1, 6);
if (!uniqueParsing) {
ADV_PDU_PAYLOAD_TYPE_1_3* directed_mac_data = (ADV_PDU_PAYLOAD_TYPE_1_3*)packet->data;
reverse_byte_array(directed_mac_data->A1, 6);
success = true;
}
}
return success;
if (success) {
int i;
for (i = 0; i < packet->dataLen; i++) {
data_string += to_string_hex(packet->data[i], 2);
}
entry.dbValue = packet->max_dB - (receiver_model.lna() + receiver_model.vga() + (receiver_model.rf_amp() ? 14 : 0));
entry.timestamp = to_string_timestamp(rtc_time::now());
entry.dataString = data_string;
entry.packetData.type = packet->type;
entry.packetData.size = packet->size;
entry.packetData.dataLen = packet->dataLen;
// Mac Address of sender.
entry.packetData.macAddress[0] = packet->macAddress[0];
entry.packetData.macAddress[1] = packet->macAddress[1];
entry.packetData.macAddress[2] = packet->macAddress[2];
entry.packetData.macAddress[3] = packet->macAddress[3];
entry.packetData.macAddress[4] = packet->macAddress[4];
entry.packetData.macAddress[5] = packet->macAddress[5];
entry.pduType = pdu_type;
entry.channelNumber = channel_number;
entry.numHits++;
if (entry.vendor_status == MAC_VENDOR_UNKNOWN) {
entry.vendor_status = lookup_mac_vendor_status(entry.packetData.macAddress, entry.vendor_name);
}
// Parse Data Section into buffer to be interpretted later.
for (int i = 0; i < packet->dataLen; i++) {
entry.packetData.data[i] = packet->data[i];
}
entry.include_name = check_name.value();
}
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) {
uint8_t currentByte, currentLength, currentType = 0;
for (currentByte = 0; currentByte < length;) {
currentLength = data[currentByte++];
currentType = data[currentByte++];
// Manufacturer Specific Data (0xFF)
if (currentType == 0xFF && currentLength >= 4) {
uint16_t companyID = data[currentByte] | (data[currentByte + 1] << 8);
// Apple AirTag / Find My
if (companyID == 0x004C && currentLength >= 6) {
uint8_t appleType = data[currentByte + 2];
uint8_t appleLen = data[currentByte + 3];
if (appleType == 0x12 && appleLen == 0x19 && currentLength >= 4 + appleLen) {
nameString.assign("Apple AirTag");
informationString.assign("Find My");
return true;
} else if (appleType == 0x02 && appleLen == 0x15) {
uint16_t major = (data[currentByte + 20] << 8) | data[currentByte + 21];
uint16_t minor = (data[currentByte + 22] << 8) | data[currentByte + 23];
nameString.assign("iBeacon");
informationString.assign(to_string_hex(major) + to_string_hex(minor));
return true;
}
}
}
// Services
else if ((currentType == 0x02 || currentType == 0x03) && currentLength >= 3) {
for (int u = 0; u < currentLength - 1; u += 2) {
uint16_t uuid16 = data[currentByte + u] | (data[currentByte + u + 1] << 8);
if (uuid16 == 0x1802) { // Immediate Alert Service = Find Me Profile
nameString.assign("FindMe");
informationString.assign("IAS");
return true;
}
}
}
// Service Data
else if (currentType == 0x16 && currentLength >= 3) {
uint16_t uuid16 = data[currentByte] | (data[currentByte + 1] << 8);
switch (uuid16) {
case 0xFD59: { // Samsung SmartTag - Unregistered
nameString.assign("Samsung SmartTag");
informationString.assign("Unreg");
return true;
}
case 0xFD5A: { // Samsung SmartTag - Registered
nameString.assign("Samsung SmartTag");
informationString.assign("Reg");
return true;
}
case 0xFD84: {
if (currentByte + 2 < length) {
uint8_t model = data[currentByte + 2];
switch (model) {
case 0x01:
informationString.assign("Mate");
break;
case 0x02:
informationString.assign("Pro");
break;
case 0x03:
informationString.assign("Slim");
break;
case 0x04:
informationString.assign("Sticker");
break;
default:
informationString.assign("Unknown");
break;
}
}
nameString.assign("Tile");
return true;
}
case 0xFEAA: {
if (currentByte + 2 < length) {
uint8_t frameType = data[currentByte + 2];
switch (frameType) {
case 0x00:
informationString.assign("UID");
break;
case 0x10:
informationString.assign("URL");
break;
case 0x20:
informationString.assign("TLM");
break;
case 0x30:
informationString.assign("EID");
break;
default:
informationString.assign("Unknown");
break;
}
}
nameString.assign("Eddystone");
return true;
}
}
}
currentByte += (currentLength - 1);
}
return false;
}
bool BLERxView::parse_beacon_data(const uint8_t* data, uint8_t length, std::string& nameString, std::string& informationString) {
+48 -28
View File
@@ -71,7 +71,8 @@ typedef enum {
RESERVED5 = 12,
RESERVED6 = 13,
RESERVED7 = 14,
RESERVED8 = 15
RESERVED8 = 15,
UNKNOWN = 16
} ADV_PDU_TYPE;
typedef enum {
@@ -84,9 +85,9 @@ typedef enum {
struct BleRecentEntry {
using Key = uint64_t;
static constexpr Key invalid_key = 0xFFFFFFFFFFFF;
static constexpr Key invalid_key = 0xFFFFFFFFFFFFF;
uint64_t macAddress;
uint64_t uniqueKey;
int dbValue;
BlePacketData packetData;
std::string timestamp;
@@ -98,6 +99,7 @@ struct BleRecentEntry {
ADV_PDU_TYPE pduType;
uint8_t channelNumber;
MAC_VENDOR_STATUS vendor_status;
std::string vendor_name;
bool entryFound;
BleRecentEntry()
@@ -105,8 +107,8 @@ struct BleRecentEntry {
}
BleRecentEntry(
const uint64_t macAddress)
: macAddress{macAddress},
const uint64_t uniqueKey)
: uniqueKey{uniqueKey},
dbValue{},
packetData{},
timestamp{},
@@ -118,11 +120,12 @@ struct BleRecentEntry {
pduType{},
channelNumber{},
vendor_status{MAC_VENDOR_UNKNOWN},
vendor_name{},
entryFound{} {
}
Key key() const {
return macAddress;
return uniqueKey;
}
};
@@ -139,11 +142,11 @@ class BleRecentEntryDetailView : public View {
void update_data();
void focus() override;
void paint(Painter&) override;
static BLETxPacket build_packet(BleRecentEntry entry_);
private:
NavigationView& nav_;
BleRecentEntry entry_{};
BLETxPacket build_packet();
void on_save_file(const std::string value, BLETxPacket packetToSave);
bool saveFile(const std::filesystem::path& path, BLETxPacket packetToSave);
std::string packetFileBuffer{};
@@ -224,9 +227,10 @@ 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);
NavigationView& nav_;
@@ -240,6 +244,7 @@ class BLERxView : public View {
uint8_t sort_index{0};
uint8_t filter_index{0};
bool uniqueParsing = false;
bool duplicatePackets = false;
std::string filter{};
bool logging{false};
bool serial_logging{false};
@@ -258,6 +263,8 @@ class BLERxView : public View {
// disabled to always start without USB serial activated until we can make it non blocking if not connected
// {"serial_log"sv, &serial_logging},
{"name"sv, &name_enable},
{"unique_parsing"sv, &uniqueParsing},
{"duplicate_packets"sv, &duplicatePackets},
}};
std::string str_console = "";
@@ -267,7 +274,7 @@ class BLERxView : public View {
bool auto_channel = false;
int16_t timer_count{0};
int16_t timer_period{2}; // 25ms
int16_t timer_period{1}; // 25ms
std::string filterBuffer{};
std::string listFileBuffer{};
@@ -283,7 +290,7 @@ class BLERxView : public View {
std::filesystem::path log_packets_path{blerx_dir / u"Logs/????.TXT"};
std::filesystem::path packet_save_path{blerx_dir / u"Lists/????.csv"};
static constexpr auto header_height = 9 * 8;
static constexpr auto header_height = 12 * 8;
static constexpr auto switch_button_height = 3 * 16;
OptionsField options_channel{
@@ -323,7 +330,8 @@ class BLERxView : public View {
{"Hits", 1},
{"dB", 2},
{"Time", 3},
{"Name", 4}}};
{"Name", 4},
{"Info", 5}}};
Button button_filter{
{11 * 8, 2 * 8, 7 * 8, 16},
@@ -334,7 +342,10 @@ class BLERxView : public View {
7,
{{"Data", 0},
{"MAC", 1},
{"Unique", 2}}};
{"Name", 2},
{"Info", 3},
{"Vendor", 4},
{"Channel", 5}}};
Checkbox check_log{
{10 * 8, 4 * 8 + 2},
@@ -348,36 +359,45 @@ class BLERxView : public View {
"Name",
true};
Button button_find{
{0 * 8, 7 * 8 - 2, 4 * 8, 16},
"Find"};
Labels label_found{
{{5 * 8, 7 * 8 - 2}, "Found:", Theme::getInstance()->fg_light->foreground}};
Text text_found_count{
{11 * 8, 7 * 8 - 2, 20 * 8, 16},
"0/0"};
Checkbox check_serial_log{
{18 * 8 + 2, 4 * 8 + 2},
7,
"USB Log",
true};
// Console console{
// {0, 10 * 8, screen_height, screen_height-80}};
Checkbox check_unique{
{0 * 8 + 2, 7 * 8 + 2},
7,
"Unique",
true};
Checkbox check_duplicate_packets{
{10 * 8 + 2, 7 * 8 + 2},
7,
"Duplicate",
true};
Button button_find{
{0 * 8, 10 * 8 - 2, 4 * 8, 16},
"Find"};
Labels label_found{
{{5 * 8, 10 * 8 - 2}, "Found:", Theme::getInstance()->fg_light->foreground}};
Text text_found_count{
{11 * 8, 10 * 8 - 2, 20 * 8, 16},
"0/0"};
Button button_clear_list{
{2 * 8, screen_height - (16 + 32), 7 * 8, 32},
{2 * 8, 320 - (16 + 32), 7 * 8, 32},
"Clear"};
Button button_save_list{
{11 * 8, screen_height - (16 + 32), 11 * 8, 32},
{11 * 8, 320 - (16 + 32), 11 * 8, 32},
"Export CSV"};
Button button_switch{
{screen_width - 6 * 8, screen_height - (16 + 32), 4 * 8, 32},
{240 - 6 * 8, 320 - (16 + 32), 4 * 8, 32},
"Tx"};
std::string str_log{""};
+117 -80
View File
@@ -35,6 +35,7 @@
#include "rtc_time.hpp"
#include "string_format.hpp"
#include "file_path.hpp"
#include "usb_serial_asyncmsg.hpp"
using namespace portapack;
using namespace modems;
@@ -123,6 +124,31 @@ static std::uint64_t get_freq_by_channel_number(uint8_t channel_number) {
namespace ui {
PKT_TYPE get_pkt_type_from_string(const std::string& type_str) {
if (type_str == "RAW") {
return PKT_TYPE_RAW;
} else if (type_str == "DISCOVERY") {
return PKT_TYPE_DISCOVERY;
} else if (type_str == "IBEACON") {
return PKT_TYPE_IBEACON;
} else if (type_str == "ADV_IND") {
return PKT_TYPE_ADV_IND;
} else if (type_str == "ADV_DIRECT_IND") {
return PKT_TYPE_ADV_DIRECT_IND;
} else if (type_str == "ADV_NONCONN_IND") {
return PKT_TYPE_ADV_NONCONN_IND;
} else if (type_str == "ADV_SCAN_IND") {
return PKT_TYPE_ADV_SCAN_IND;
} else if (type_str == "SCAN_REQ") {
return PKT_TYPE_SCAN_REQ;
} else if (type_str == "SCAN_RSP") {
return PKT_TYPE_SCAN_RSP;
} else if (type_str == "CONNECT_REQ") {
return PKT_TYPE_CONNECT_REQ;
}
return PKT_TYPE_INVALID_TYPE;
}
void BLETxView::focus() {
button_open.focus();
}
@@ -167,28 +193,11 @@ void BLETxView::toggle() {
}
}
void BLETxView::start() {
baseband::run_image(portapack::spi_flash::image_tag_btle_tx);
transmitter_model.enable();
void BLETxView::send_packet() {
// Generate new random Mac Address.
transmitter_model.enable();
generateRandomMacAddress(randomMac);
// If this is our first run, check file.
if (!is_active()) {
File data_file;
auto error = data_file.open(file_path);
if (error && !file_override) {
file_error();
check_loop.set_value(false);
return;
}
button_play.set_bitmap(&bitmap_stop);
is_running = true;
}
char advertisementData[63] = {0};
strcpy(advertisementData, packets[current_packet].advertisementData);
@@ -235,9 +244,28 @@ void BLETxView::start() {
}
}
// Setup next packet configuration.
progressbar.set_max(packets[current_packet].packet_count);
baseband::set_btletx(channel_number, random_mac ? randomMac : packets[current_packet].macAddress, advertisementData, pduType);
progressbar.set_value(packets[current_packet].packet_count - packet_counter);
text_packets_sent.set(to_string_dec_uint(packet_counter));
baseband::set_btletx(channel_number, random_mac ? randomMac : packets[current_packet].macAddress, advertisementData, packets[current_packet].pduType);
packetDone = false;
}
void BLETxView::start() {
if (file_path.empty()) {
file_error();
check_loop.set_value(false);
return;
}
baseband::run_image(portapack::spi_flash::image_tag_btle_tx);
transmitter_model.enable();
button_play.set_bitmap(&bitmap_stop);
is_running = true;
send_packet();
}
void BLETxView::stop() {
@@ -253,66 +281,69 @@ void BLETxView::stop() {
is_running = false;
}
void BLETxView::reset() {
transmitter_model.disable();
baseband::shutdown();
start();
}
// called each 1/60th of second, so 6 = 100ms
void BLETxView::on_timer() {
if (++timer_count == timer_period) {
timer_count = 0;
if (is_active()) {
// Reached end of current packet repeats.
if (packet_counter == 0) {
// Done sending all packets.
if (current_packet == (num_packets - 1)) {
current_packet = 0;
// If looping, restart from beginning.
if (check_loop.value()) {
update_current_packet(packets[current_packet], current_packet);
reset();
} else {
stop();
}
} else {
current_packet++;
update_current_packet(packets[current_packet], current_packet);
reset();
}
} else {
reset();
}
}
}
if (++auto_channel_counter == auto_channel_period) {
auto_channel_counter = 0;
if (auto_channel) {
int min = 37;
int max = 39;
channel_number = min + std::rand() % (max - min + 1);
field_frequency.set_value(get_freq_by_channel_number(channel_number));
if (is_active() && packetDone) {
send_packet();
}
}
}
void BLETxView::on_tx_progress(const bool done) {
void BLETxView::on_tx_progress(const bool done, uint32_t progress) {
// TODO: make use of progress variable
(void)progress;
if (done) {
if (is_active()) {
if ((packet_counter % 10) == 0) {
text_packets_sent.set(to_string_dec_uint(packet_counter));
}
transmitter_model.disable();
if (auto_channel) {
switch (advCount) {
case 0:
channel_number = 37;
break;
case 1:
channel_number = 38;
break;
case 2:
channel_number = 39;
break;
}
packet_counter--;
progressbar.set_value(packets[current_packet].packet_count - packet_counter);
field_frequency.set_value(get_freq_by_channel_number(channel_number));
if (advCount == 3) {
channel_number = 37;
packet_counter--;
packetDone = true;
advCount = 0;
} else {
send_packet();
advCount++;
}
} else {
packet_counter--;
packetDone = true;
}
}
// Reached end of current packet repeats.
if (packet_counter == 0) {
// Done sending all packets.
if (current_packet == (num_packets - 1)) {
current_packet = 0;
// If looping, restart from beginning.
if (check_loop.value()) {
update_current_packet(packets[current_packet], current_packet);
} else {
stop();
}
} else {
current_packet++;
update_current_packet(packets[current_packet], current_packet);
}
}
}
}
@@ -331,7 +362,6 @@ BLETxView::BLETxView(NavigationView& nav)
&label_speed,
&options_speed,
&options_channel,
&options_adv_type,
&label_marked_data,
&marked_data_sequence,
&label_packet_index,
@@ -371,13 +401,8 @@ BLETxView::BLETxView(NavigationView& nav)
timer_count = 0;
};
options_adv_type.on_change = [this](size_t, int32_t i) {
pduType = (PKT_TYPE)i;
};
options_speed.set_selected_index(0);
options_channel.set_selected_index(0);
options_adv_type.set_selected_index(0);
options_channel.set_selected_index(3);
check_rand_mac.set_value(false);
check_rand_mac.on_select = [this](Checkbox&, bool v) {
@@ -482,17 +507,25 @@ void BLETxView::on_file_changed(const fs::path& new_file_path) {
do {
readUntil(data_file, packets[num_packets].macAddress, mac_address_size_str, ' ');
readUntil(data_file, packets[num_packets].advertisementData, max_packet_size_str, ' ');
readUntil(data_file, packets[num_packets].packetType, max_packet_type_str, ' ');
readUntil(data_file, packets[num_packets].packetCount, max_packet_repeat_str, '\n');
uint64_t macAddressSize = strlen(packets[num_packets].macAddress);
uint64_t advertisementDataSize = strlen(packets[num_packets].advertisementData);
uint64_t packetCountSize = strlen(packets[num_packets].packetCount);
uint64_t packetTypeSize = strlen(packets[num_packets].packetType);
packets[num_packets].packet_count = stringToUint32(packets[num_packets].packetCount);
packets[num_packets].pduType = (PKT_TYPE)get_pkt_type_from_string(packets[num_packets].packetType);
// Verify Data.
if ((macAddressSize == mac_address_size_str) && (advertisementDataSize < max_packet_size_str) && (packetCountSize < max_packet_repeat_str) &&
hasValidHexPairs(packets[num_packets].macAddress, macAddressSize / 2) && hasValidHexPairs(packets[num_packets].advertisementData, advertisementDataSize / 2) && (packets[num_packets].packet_count >= 1) && (packets[num_packets].packet_count < max_packet_repeat_count)) {
if ((macAddressSize == mac_address_size_str) &&
(advertisementDataSize < max_packet_size_str) &&
(packetCountSize < max_packet_repeat_str) &&
hasValidHexPairs(packets[num_packets].macAddress, macAddressSize / 2) &&
hasValidHexPairs(packets[num_packets].advertisementData, advertisementDataSize / 2) &&
(packets[num_packets].packet_count >= 1) && (packets[num_packets].packet_count < max_packet_repeat_count) &&
(packetTypeSize <= max_packet_type_str)) {
text_filename.set(truncate(file_path.filename().string(), 12));
} else {
@@ -510,6 +543,8 @@ void BLETxView::on_file_changed(const fs::path& new_file_path) {
} while (num_packets < max_num_packets);
update_current_packet(packets[0], 0);
data_file.close();
}
}
@@ -540,6 +575,8 @@ void BLETxView::update_current_packet(BLETxPacket packet, uint32_t currentIndex)
text_mac_address.set(formattedMacAddress);
progressbar.set_max(packet.packet_count);
packet_counter = packet.packet_count;
current_packet = currentIndex;
@@ -550,7 +587,7 @@ void BLETxView::update_current_packet(BLETxPacket packet, uint32_t currentIndex)
dataFile.write("\n", 1);
}
dataFile.~File();
dataFile.close();
auto result = FileWrapper::open(dataTempFilePath);
+18 -13
View File
@@ -38,6 +38,8 @@
#include "utility.hpp"
#include "file_path.hpp"
#include "ble_tx_app.hpp"
#include "recent_entries.hpp"
#include <string>
@@ -91,8 +93,9 @@ struct BLETxPacket {
char macAddress[13];
char advertisementData[63];
char packetCount[11];
char packetType[17];
uint32_t packet_count;
PKT_TYPE packetType;
PKT_TYPE pduType;
};
class BLETxView : public View {
@@ -109,8 +112,8 @@ class BLETxView : public View {
bool is_active() const;
void toggle();
void start();
void send_packet();
void stop();
void reset();
void handle_replay_thread_done(const uint32_t return_code);
void file_error();
bool saveFile(const std::filesystem::path& path);
@@ -122,7 +125,7 @@ class BLETxView : public View {
void on_data(uint32_t value, bool is_data);
void on_file_changed(const std::filesystem::path& new_file_path);
void on_save_file(const std::string value);
void on_tx_progress(const bool done);
void on_tx_progress(const bool done, uint32_t progress);
void on_random_data_change(std::string value);
void update_current_packet(BLETxPacket packet, uint32_t currentIndex);
@@ -156,6 +159,8 @@ class BLETxView : public View {
uint32_t packet_counter{0};
uint32_t num_packets{0};
uint32_t current_packet{0};
uint8_t packetTxCount{0};
bool packetDone = false;
bool random_mac = false;
bool file_override = false;
@@ -170,17 +175,17 @@ class BLETxView : public View {
std::vector<uint16_t> markedBytes{};
CursorPos cursor_pos{};
uint8_t marked_counter = 0;
uint8_t advCount = 0;
static constexpr uint8_t mac_address_size_str{12};
static constexpr uint8_t max_packet_size_str{62};
static constexpr uint8_t max_packet_repeat_str{10};
static constexpr uint8_t max_packet_type_str{16};
static constexpr uint32_t max_packet_repeat_count{UINT32_MAX};
static constexpr uint32_t max_num_packets{32};
BLETxPacket packets[max_num_packets];
PKT_TYPE pduType = {PKT_TYPE_DISCOVERY};
static constexpr auto header_height = 10 * 16;
static constexpr auto switch_button_height = 6 * 16;
@@ -216,7 +221,7 @@ class BLETxView : public View {
true};
ImageButton button_play{
{screen_width - 2 * 8, 2 * 16, 2 * 8, 1 * 16},
{28 * 8, 2 * 16, 2 * 8, 1 * 16},
&bitmap_play,
Theme::getInstance()->fg_green->foreground,
Theme::getInstance()->fg_green->background};
@@ -227,11 +232,11 @@ class BLETxView : public View {
OptionsField options_speed{
{7 * 8, 6 * 8},
3,
{{"1 ", 1}, // 16ms
{"2 ", 2}, // 32ms
{"3 ", 3}, // 48ms
{"4 ", 6}, // 100ms
{"5 ", 12}}}; // 200ms
{{"1 ", 2}, // 25ms
{"2 ", 4}, // 50ms
{"3 ", 6}, // 75ms
{"4 ", 8}, // 100ms
{"5 ", 12}}}; // 150ms
OptionsField options_channel{
{11 * 8, 6 * 8},
@@ -288,7 +293,7 @@ class BLETxView : public View {
{{0 * 8, 9 * 16}, "Packet Data:", Theme::getInstance()->fg_light->foreground}};
TextViewer dataEditView{
{0, 9 * 18, screen_width, screen_height - 80}};
{0, 9 * 18, 240, 240}};
Button button_clear_marked{
{1 * 8, 14 * 16, 13 * 8, 3 * 8},
@@ -320,7 +325,7 @@ class BLETxView : public View {
Message::ID::TXProgress,
[this](const Message* const p) {
const auto message = *reinterpret_cast<const TXProgressMessage*>(p);
this->on_tx_progress(message.done);
this->on_tx_progress(message.done, message.progress);
}};
MessageHandlerRegistration message_handler_frame_sync{
+2 -2
View File
@@ -635,14 +635,14 @@ void ADSBRxView::refresh_ui() {
if (details_view->map_active()) {
// Is it time to clear and refresh the map's markers?
if (ticks_since_marker_refresh >= MARKER_UPDATE_SECONDS) {
if (ticks_since_marker_refresh >= (details_view->get_map_type() == MAP_TYPE_OSM ? MARKER_UPDATE_SECONDS_OSM : MARKER_UPDATE_SECONDS_BIN)) {
map_needs_update = true;
ticks_since_marker_refresh = 0;
details_view->clear_map_markers();
}
} else {
// Refresh map immediately once active.
ticks_since_marker_refresh = MARKER_UPDATE_SECONDS;
ticks_since_marker_refresh = MARKER_UPDATE_SECONDS_OSM; // this is the bigger, so set it to force
}
// Process the entries list.
+11 -3
View File
@@ -64,8 +64,9 @@ namespace ui {
#define VEL_AIR_SUBSONIC 3
#define VEL_AIR_SUPERSONIC 4
#define O_E_FRAME_TIMEOUT 20 // timeout between odd and even frames
#define MARKER_UPDATE_SECONDS 5 // "other" map marker redraw interval
#define O_E_FRAME_TIMEOUT 20 // timeout between odd and even frames
#define MARKER_UPDATE_SECONDS_OSM 10 // "other" map marker redraw interval for osm
#define MARKER_UPDATE_SECONDS_BIN 5 // "other" map marker redraw interval for bin map
/* Thresholds (in seconds) that define the transition between ages. */
struct ADSBAgeLimit {
@@ -277,6 +278,13 @@ class ADSBRxDetailsView : public View {
std::string title() const override { return "Details"; }
MapType get_map_type() {
if (geomap_view_)
return geomap_view_->get_map_type();
else
return MapType::MAP_TYPE_BIN; // default
}
private:
void refresh_ui();
void on_gps(const GPSPosDataMessage* msg);
@@ -388,7 +396,7 @@ class ADSBRxView : public View {
SignalToken signal_token_tick_second{};
uint8_t tick_count = 0;
uint16_t ticks_since_marker_refresh{MARKER_UPDATE_SECONDS};
uint16_t ticks_since_marker_refresh{MARKER_UPDATE_SECONDS_OSM};
/* Max number of entries that can be updated in a single pass.
* 16 is one screen of recent entries. */
+2 -2
View File
@@ -247,8 +247,8 @@ APRSTableView::APRSTableView(NavigationView& nav, Rect parent_rec)
details_view.hidden(true);
recent_entries_view.set_parent_rect({0, 0, screen_width, screen_width - 40});
details_view.set_parent_rect({0, 0, screen_width, screen_width - 40});
recent_entries_view.set_parent_rect({0, 0, screen_width, screen_height - 40});
details_view.set_parent_rect({0, 0, screen_width, screen_height - 40});
recent_entries_view.on_select = [this](const APRSRecentEntry& entry) {
this->on_show_detail(entry);
+2
View File
@@ -446,6 +446,8 @@ MicTXView::MicTXView(
field_frequency.set_value(tx_frequency);
// TODO: would be nice if frequency step was configurable in this app
// but now, make it a bit lower
receiver_model.set_frequency_step(12'500); // set freq step to 12.5kHz
field_frequency.set_step(receiver_model.frequency_step());
// WARNING: transmitter_model.set_target_frequency() and receiver_model.set_target_frequency() both update the same tuning freq, but one has an offset!
+25 -1
View File
@@ -26,11 +26,17 @@
#include "binder.hpp"
#include "string_format.hpp"
#include "ui_freqman.hpp"
#include "audio.hpp"
using namespace portapack;
namespace pmem = portapack::persistent_memory;
namespace ui {
void SearchLogger::log_data(SearchRecentEntry& data) {
log_file.write_entry(";" + to_string_short_freq(data.frequency) + ";" + std::to_string(data.duration));
}
template <>
void RecentEntriesTable<SearchRecentEntries>::draw(
const Entry& entry,
@@ -75,6 +81,7 @@ SearchView::SearchView(
&check_snap,
&options_snap,
&big_display,
&check_log,
&recent_entries_view});
baseband::set_spectrum(SEARCH_SLICE_WIDTH, 31);
@@ -100,6 +107,14 @@ SearchView::SearchView(
on_range_changed();
});
check_log.on_select = [this](Checkbox&, bool v) {
logging = v;
if (logging) {
logger.append(logs_dir.string() + "/SEARCH_" + to_string_timestamp(rtc_time::now()) + ".CSV");
logger.write_header();
}
};
bind(field_threshold, settings_.power_threshold);
bind(check_snap, settings_.snap_search);
bind(options_snap, settings_.snap_step);
@@ -108,9 +123,15 @@ SearchView::SearchView(
on_range_changed();
receiver_model.enable();
if (pmem::beep_on_packets()) {
audio::set_rate(audio::Rate::Hz_24000);
audio::output::start();
}
}
SearchView::~SearchView() {
audio::output::stop();
receiver_model.disable();
baseband::shutdown();
}
@@ -192,7 +213,9 @@ void SearchView::do_detection() {
locked = true;
locked_bin = bin_max;
if (pmem::beep_on_packets()) {
baseband::request_audio_beep(1000, 24000, 60);
}
// TODO: open Audio.
} else
text_infos.set("Out of range");
@@ -210,6 +233,7 @@ void SearchView::do_detection() {
auto& entry = ::on_packet(recent, resolved_frequency);
entry.set_duration(duration);
if (logging) logger.log_data(entry);
recent_entries_view.set_dirty();
text_infos.set("Listening");
+25
View File
@@ -26,6 +26,8 @@
#include "radio_state.hpp"
#include "gradient.hpp"
#include "ui_receiver.hpp"
#include "log_file.hpp"
#include "file_path.hpp"
namespace ui {
@@ -70,6 +72,21 @@ struct SearchRecentEntry {
using SearchRecentEntries = RecentEntries<SearchRecentEntry>;
class SearchLogger {
public:
Optional<File::Error> append(const std::filesystem::path& filename) {
return log_file.append(filename);
}
void log_data(SearchRecentEntry& data);
void write_header() {
log_file.write_raw("Time;Freq;Duration;");
}
private:
LogFile log_file{};
};
class SearchView : public View {
public:
SearchView(NavigationView& nav);
@@ -148,6 +165,8 @@ class SearchView : public View {
uint16_t locked_bin = 0;
uint8_t search_counter = 0;
bool locked = false;
bool logging = false;
SearchLogger logger{};
void do_detection();
void do_timers();
@@ -169,6 +188,12 @@ class SearchView : public View {
{{1 * 8, 25 * 8}, "Accuracy +/-4.9kHz", Theme::getInstance()->fg_light->foreground},
{{26 * 8, 25 * 8}, "MHz", Theme::getInstance()->fg_light->foreground}};
Checkbox check_log{
{24 * 8, 10 * 8},
3,
"LOG",
true};
FrequencyField field_frequency_min{
{1 * 8, 1 * 16}};
FrequencyField field_frequency_max{
+1 -1
View File
@@ -6,7 +6,7 @@
* Copyright (C) 2024 Mark Thompson
* Copyright (C) 2024 u-foka
* Copyright (C) 2024 HTotoo
* Copyleft (ɔ) 2024 zxkmm under GPL license
* copyleft 2024 zxkmm AKA zix aka sommermorgentraum
*
* This file is part of PortaPack.
*
+1 -1
View File
@@ -5,7 +5,7 @@
* Copyright (C) 2023 Kyle Reed
* Copyright (C) 2024 Mark Thompson
* Copyright (C) 2024 u-foka
* Copyleft (ɔ) 2024 zxkmm under GPL license
* copyleft 2024 zxkmm AKA zix aka sommermorgentraum
*
* This file is part of PortaPack.
*
+1
View File
@@ -54,6 +54,7 @@ SondeView::SondeView(NavigationView& nav)
&field_lna,
&field_vga,
&rssi,
&channel,
&field_volume,
&check_log,
&check_crc,
+3
View File
@@ -118,6 +118,9 @@ class SondeView : public View {
RSSI rssi{
{21 * 8, 0, 6 * 8, 4}};
Channel channel{
{21 * 8, 5, 6 * 8, 4},
};
AudioVolumeField field_volume{
{screen_width - 2 * 8, 0 * 16}};
@@ -27,6 +27,9 @@
#include "ui_font_fixed_5x8.hpp"
#include "ui_font_fixed_8x16.hpp"
#include "portapack.hpp"
#include "file.hpp"
namespace ui {
@@ -93,8 +96,90 @@ bool i2c_read(uint8_t* cmd, size_t cmd_len, uint8_t* data, size_t data_len) {
return dev->i2c_read(cmd, cmd_len, data, data_len);
}
// v3
// Version 3
FRESULT ext_f_open(FIL* fp, const TCHAR* path, BYTE mode) {
return f_open(fp, path, mode);
}
FRESULT ext_f_close(FIL* fp) {
return f_close(fp);
}
FRESULT ext_f_read(FIL* fp, void* buff, UINT btr, UINT* br) {
return f_read(fp, buff, btr, br);
}
FRESULT ext_f_write(FIL* fp, const void* buff, UINT btw, UINT* bw) {
return f_write(fp, buff, btw, bw);
}
FRESULT ext_f_lseek(FIL* fp, FSIZE_t ofs) {
return f_lseek(fp, ofs);
}
FRESULT ext_f_truncate(FIL* fp) {
return f_truncate(fp);
}
FRESULT ext_f_sync(FIL* fp) {
return f_sync(fp);
}
FRESULT ext_f_opendir(DIR* dp, const TCHAR* path) {
return f_opendir(dp, path);
}
FRESULT ext_f_closedir(DIR* dp) {
return f_closedir(dp);
}
FRESULT ext_f_readdir(DIR* dp, FILINFO* fno) {
return f_readdir(dp, fno);
}
FRESULT ext_f_findfirst(DIR* dp, FILINFO* fno, const TCHAR* path, const TCHAR* pattern) {
return f_findfirst(dp, fno, path, pattern);
}
FRESULT ext_f_findnext(DIR* dp, FILINFO* fno) {
return f_findnext(dp, fno);
}
FRESULT ext_f_mkdir(const TCHAR* path) {
return f_mkdir(path);
}
FRESULT ext_f_unlink(const TCHAR* path) {
return f_unlink(path);
}
FRESULT ext_f_rename(const TCHAR* path_old, const TCHAR* path_new) {
return f_rename(path_old, path_new);
}
FRESULT ext_f_stat(const TCHAR* path, FILINFO* fno) {
return f_stat(path, fno);
}
FRESULT ext_f_utime(const TCHAR* path, const FILINFO* fno) {
return f_utime(path, fno);
}
FRESULT ext_f_getfree(const TCHAR* path, DWORD* nclst, FATFS** fatfs) {
return f_getfree(path, nclst, fatfs);
}
FRESULT ext_f_mount(FATFS* fs, const TCHAR* path, BYTE opt) {
return f_mount(fs, path, opt);
}
int ext_f_putc(TCHAR c, FIL* fp) {
return f_putc(c, fp);
}
int ext_f_puts(const TCHAR* str, FIL* cp) {
return f_puts(str, cp);
}
int ext_f_printf(FIL* fp, const TCHAR* str, ...) {
return f_printf(fp, str);
}
TCHAR* ext_f_gets(TCHAR* buff, int len, FIL* fp) {
return f_gets(buff, len, fp);
}
void ext_draw_pixels(const ui::Rect r, const ui::Color* const colors, const size_t count) {
portapack::display.draw_pixels(r, colors, count);
}
void ext_draw_pixel(const ui::Point p, const ui::Color color) {
portapack::display.draw_pixel(p, color);
}
StandaloneView* standaloneView = nullptr;
void exit_app() {
if (standaloneView) standaloneView->exit();
}
void set_dirty() {
if (standaloneView != nullptr)
standaloneView->set_dirty();
@@ -121,6 +206,36 @@ standalone_application_api_t api = {
/* .i2c_read = */ &i2c_read,
/* .panic = */ &chDbgPanic,
/* .set_dirty = */ &set_dirty,
// Version 3
.f_open = &ext_f_open,
.f_close = &ext_f_close,
.f_read = &ext_f_read,
.f_write = &ext_f_write,
.f_lseek = &ext_f_lseek,
.f_truncate = &ext_f_truncate,
.f_sync = &ext_f_sync,
.f_opendir = &ext_f_opendir,
.f_closedir = &ext_f_closedir,
.f_readdir = &ext_f_readdir,
.f_findfirst = &ext_f_findfirst,
.f_findnext = &ext_f_findnext,
.f_mkdir = &ext_f_mkdir,
.f_unlink = &ext_f_unlink,
.f_rename = &ext_f_rename,
.f_stat = &ext_f_stat,
.f_utime = &ext_f_utime,
.f_getfree = &ext_f_getfree,
.f_mount = &ext_f_mount,
.f_putc = &ext_f_putc,
.f_puts = &ext_f_puts,
.f_printf = &ext_f_printf,
.f_gets = &ext_f_gets,
.draw_pixels = &ext_draw_pixels,
.draw_pixel = &ext_draw_pixel,
.exit_app = &exit_app,
// version 4
.screen_height = &screen_height,
.screen_width = &screen_width,
};
StandaloneView::StandaloneView(NavigationView& nav, uint8_t* app_image)
@@ -158,22 +273,21 @@ bool StandaloneView::on_key(const KeyEvent key) {
if (get_application_information()->header_version > 1) {
return get_application_information()->OnKeyEvent((uint8_t)key);
}
return false;
return true;
}
bool StandaloneView::on_encoder(const EncoderEvent event) {
if (get_application_information()->header_version > 1) {
return get_application_information()->OnEncoder((int32_t)event);
}
return false;
return true;
}
bool StandaloneView::on_touch(const TouchEvent event) {
if (get_application_information()->header_version > 1) {
get_application_information()->OnTouchEvent(event.point.x(), event.point.y(), (uint32_t)event.type);
}
return true;
return false;
}
bool StandaloneView::on_keyboard(const KeyboardEvent event) {
@@ -202,4 +316,8 @@ void StandaloneView::on_before_detach() {
context().focus_manager().clearMirror();
}
void StandaloneView::exit() {
nav_.pop();
}
} // namespace ui
@@ -51,6 +51,8 @@ class StandaloneView : public View {
void frame_sync();
void exit();
private:
bool initialized = false;
NavigationView& nav_;
@@ -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.
*
+141 -245
View File
@@ -10,91 +10,41 @@
namespace ui::external_app::breakout {
Ticker game_timer;
int paddle_x = 0;
float ball_x = 0;
float ball_y = GAME_AREA_BOTTOM - PADDLE_HEIGHT - BALL_SIZE - 1;
float ball_dx = 1.5f;
float ball_dy = -2.0f;
int score = 0;
int lives = 3;
int level = 1;
int game_state = STATE_MENU;
bool initialized = false;
bool ball_attached = true;
unsigned int brick_count = 0;
bool menu_initialized = false;
bool blink_state = true;
uint32_t blink_counter = 0;
int16_t prompt_x = 0;
bool gameover_initialized = false;
bool gameover_blink_state = true;
uint32_t gameover_blink_counter = 0;
int16_t restart_x = 0;
bool bricks[BRICK_ROWS][BRICK_COLS];
int brick_colors[BRICK_ROWS];
const Color pp_colors[] = {
Color::white(),
Color::blue(),
Color::yellow(),
Color::purple(),
Color::green(),
Color::red(),
Color::magenta(),
Color::orange(),
Color::black(),
};
Painter painter;
bool but_RIGHT = false;
bool but_LEFT = false;
bool but_SELECT = false;
static Callback game_update_callback = nullptr;
static uint32_t game_update_timeout = 0;
static uint32_t game_update_counter = 0;
void cls() {
void BreakoutView::cls() {
painter.fill_rectangle({0, 0, portapack::display.width(), portapack::display.height()}, Color::black());
}
void background(int color) {
void BreakoutView::background(int color) {
(void)color;
}
void fillrect(int x1, int y1, int x2, int y2, int color) {
void BreakoutView::fillrect(int x1, int y1, int x2, int y2, int color) {
painter.fill_rectangle({x1, y1, x2 - x1, y2 - y1}, pp_colors[color]);
}
void rect(int x1, int y1, int x2, int y2, int color) {
void BreakoutView::rect(int x1, int y1, int x2, int y2, int color) {
painter.draw_rectangle({x1, y1, x2 - x1, y2 - y1}, pp_colors[color]);
}
void check_game_timer() {
void BreakoutView::check_game_timer() {
if (game_update_callback) {
if (++game_update_counter >= game_update_timeout) {
game_update_counter = 0;
game_update_callback();
game_timer_check();
}
}
}
void Ticker::attach(Callback func, double delay_sec) {
game_update_callback = func;
void BreakoutView::attach(double delay_sec) {
game_update_callback = true;
game_update_timeout = delay_sec * 60;
}
void Ticker::detach() {
game_update_callback = nullptr;
void BreakoutView::detach() {
game_update_callback = false;
}
void game_timer_check() {
void BreakoutView::game_timer_check() {
if (game_state == STATE_PLAYING) {
update_game();
} else if (game_state == STATE_MENU) {
@@ -104,36 +54,57 @@ void game_timer_check() {
}
}
void init_game() {
paddle_x = (screen_width - PADDLE_WIDTH) / 2;
void BreakoutView::init_game() {
SCREEN_WIDTH = screen_width;
SCREEN_HEIGHT = screen_height;
GAME_AREA_BOTTOM = SCREEN_HEIGHT - 10;
PADDLE_Y = GAME_AREA_BOTTOM - PADDLE_HEIGHT;
PADDLE_SPEED = 10;
// Calculate brick layout
int available_width = SCREEN_WIDTH - 4;
BRICK_COLS = available_width / 22;
if (BRICK_COLS > 12) BRICK_COLS = 12;
if (BRICK_COLS < 5) BRICK_COLS = 5;
BRICK_WIDTH = (available_width - (BRICK_COLS - 1) * BRICK_GAP) / BRICK_COLS;
// Initialize paddle position
paddle_x = (SCREEN_WIDTH - PADDLE_WIDTH) / 2;
score = 0;
lives = 3;
level = 1;
// Set brick colors
brick_colors[0] = Red;
brick_colors[1] = Orange;
brick_colors[2] = Yellow;
brick_colors[3] = Green;
brick_colors[4] = Purple;
// Initialize bricks array
bricks.resize(BRICK_ROWS);
for (int i = 0; i < BRICK_ROWS; i++) {
bricks[i].resize(BRICK_COLS);
}
init_level();
game_state = STATE_MENU;
menu_initialized = false;
blink_state = true;
blink_counter = 0;
menu_blink_state = true;
menu_blink_counter = 0;
}
void init_level() {
void BreakoutView::init_level() {
ball_x = paddle_x + (PADDLE_WIDTH / 2) - (BALL_SIZE / 2);
ball_y = GAME_AREA_BOTTOM - PADDLE_HEIGHT - BALL_SIZE - 1;
float speed_multiplier = (level == 1) ? 1.0f : 1.0f + ((level - 1) * BALL_SPEED_INCREASE);
ball_dx = (ball_dx > 0 ? 1.5f : -1.5f) * speed_multiplier;
float speed_multiplier = 1.0f + ((level - 1) * BALL_SPEED_INCREASE);
ball_dx = 1.5f * speed_multiplier;
ball_dy = -2.0f * speed_multiplier;
ball_attached = true;
// Fill all bricks
brick_count = 0;
for (int row = 0; row < BRICK_ROWS; row++) {
for (int col = 0; col < BRICK_COLS; col++) {
@@ -143,10 +114,9 @@ void init_level() {
}
}
void draw_screen() {
void BreakoutView::draw_screen() {
cls();
background(COLOR_BACKGROUND);
draw_borders();
draw_bricks();
draw_paddle();
@@ -156,11 +126,11 @@ void draw_screen() {
draw_level();
}
void draw_borders() {
rect(0, GAME_AREA_TOP - 1, screen_width, GAME_AREA_TOP, COLOR_BORDER);
void BreakoutView::draw_borders() {
rect(0, GAME_AREA_TOP - 1, SCREEN_WIDTH, GAME_AREA_TOP, COLOR_BORDER);
}
void draw_bricks() {
void BreakoutView::draw_bricks() {
for (int row = 0; row < BRICK_ROWS; row++) {
for (int col = 0; col < BRICK_COLS; col++) {
if (bricks[row][col]) {
@@ -173,37 +143,37 @@ void draw_bricks() {
}
}
void draw_paddle() {
void BreakoutView::draw_paddle() {
fillrect(paddle_x, PADDLE_Y, paddle_x + PADDLE_WIDTH, PADDLE_Y + PADDLE_HEIGHT, COLOR_PADDLE);
}
void draw_ball() {
void BreakoutView::draw_ball() {
fillrect(ball_x, ball_y, ball_x + BALL_SIZE, ball_y + BALL_SIZE, COLOR_BALL);
}
void draw_score() {
void BreakoutView::draw_score() {
auto style = *ui::Theme::getInstance()->fg_green;
painter.draw_string({5, 10}, style, "Score: " + std::to_string(score));
}
void draw_lives() {
void BreakoutView::draw_lives() {
auto style = *ui::Theme::getInstance()->fg_red;
painter.draw_string({5, 30}, style, "Lives: " + std::to_string(lives));
}
void draw_level() {
void BreakoutView::draw_level() {
auto style = *ui::Theme::getInstance()->fg_yellow;
painter.draw_string({80, 30}, style, "Level: " + std::to_string(level));
}
void move_paddle_left() {
void BreakoutView::move_paddle_left() {
if (paddle_x > 0) {
fillrect(paddle_x, PADDLE_Y, paddle_x + PADDLE_WIDTH, PADDLE_Y + PADDLE_HEIGHT, COLOR_BACKGROUND);
if (ball_attached) {
fillrect(ball_x, ball_y, ball_x + BALL_SIZE, ball_y + BALL_SIZE, COLOR_BACKGROUND);
}
paddle_x -= 10;
paddle_x -= PADDLE_SPEED;
if (paddle_x < 0) paddle_x = 0;
if (ball_attached) {
@@ -217,15 +187,15 @@ void move_paddle_left() {
}
}
void move_paddle_right() {
if (paddle_x < screen_width - PADDLE_WIDTH) {
void BreakoutView::move_paddle_right() {
if (paddle_x < SCREEN_WIDTH - PADDLE_WIDTH) {
fillrect(paddle_x, PADDLE_Y, paddle_x + PADDLE_WIDTH, PADDLE_Y + PADDLE_HEIGHT, COLOR_BACKGROUND);
if (ball_attached) {
fillrect(ball_x, ball_y, ball_x + BALL_SIZE, ball_y + BALL_SIZE, COLOR_BACKGROUND);
}
paddle_x += 10;
if (paddle_x > screen_width - PADDLE_WIDTH) paddle_x = screen_width - PADDLE_WIDTH;
paddle_x += PADDLE_SPEED;
if (paddle_x > SCREEN_WIDTH - PADDLE_WIDTH) paddle_x = SCREEN_WIDTH - PADDLE_WIDTH;
if (ball_attached) {
ball_x = paddle_x + (PADDLE_WIDTH / 2) - (BALL_SIZE / 2);
@@ -238,26 +208,41 @@ void move_paddle_right() {
}
}
void launch_ball() {
void BreakoutView::launch_ball() {
if (ball_attached) {
ball_attached = false;
ball_x = paddle_x + (PADDLE_WIDTH / 2) - (BALL_SIZE / 2);
ball_y = GAME_AREA_BOTTOM - PADDLE_HEIGHT - BALL_SIZE - 1;
float speed_multiplier = (level == 1) ? 1.0f : 1.0f + ((level - 1) * BALL_SPEED_INCREASE);
float speed_multiplier = 1.0f + ((level - 1) * BALL_SPEED_INCREASE);
ball_dx = 1.5f * speed_multiplier;
ball_dy = -2.0f * speed_multiplier;
}
}
void update_game() {
void BreakoutView::update_game() {
if (ball_attached) {
return;
}
fillrect(ball_x, ball_y, ball_x + BALL_SIZE, ball_y + BALL_SIZE, COLOR_BACKGROUND);
float next_ball_y = ball_y + ball_dy;
if (next_ball_y > GAME_AREA_BOTTOM) {
ball_x += ball_dx;
ball_y += ball_dy;
// Wall collisions
if (ball_x < 0) {
ball_x = 0;
ball_dx = -ball_dx;
} else if (ball_x > SCREEN_WIDTH - BALL_SIZE) {
ball_x = SCREEN_WIDTH - BALL_SIZE;
ball_dx = -ball_dx;
}
if (ball_y < GAME_AREA_TOP) {
ball_y = GAME_AREA_TOP;
ball_dy = -ball_dy;
}
// Bottom edge - lose life
if (ball_y > GAME_AREA_BOTTOM) {
lives--;
draw_lives();
if (lives <= 0) {
@@ -271,37 +256,20 @@ void update_game() {
return;
}
ball_x += ball_dx;
ball_y = next_ball_y;
if (ball_x < 0) {
ball_x = 0;
ball_dx = -ball_dx;
} else if (ball_x > screen_width - BALL_SIZE) {
ball_x = screen_width - BALL_SIZE;
ball_dx = -ball_dx;
}
if (ball_y < GAME_AREA_TOP) {
ball_y = GAME_AREA_TOP;
ball_dy = -ball_dy;
}
// Paddle collision
if (ball_y + BALL_SIZE >= PADDLE_Y && ball_y <= PADDLE_Y + PADDLE_HEIGHT) {
if (ball_x + BALL_SIZE >= paddle_x && ball_x <= paddle_x + PADDLE_WIDTH) {
ball_y = PADDLE_Y - BALL_SIZE;
ball_dy = -ball_dy;
// Add some angle based on hit position
float hit_position = (ball_x + (BALL_SIZE / 2)) - paddle_x;
float angle = (hit_position / PADDLE_WIDTH) - 0.5f;
ball_dx = angle * 4.0f;
if (ball_dx > -0.5f && ball_dx < 0.5f) {
ball_dx = (ball_dx > 0) ? 0.5f : -0.5f;
}
ball_dy = -ball_dy;
}
}
check_collisions();
draw_ball();
if (check_level_complete()) {
@@ -309,151 +277,93 @@ void update_game() {
}
}
void check_collisions() {
void BreakoutView::check_collisions() {
int grid_x = ball_x / (BRICK_WIDTH + BRICK_GAP);
int grid_y = (ball_y - GAME_AREA_TOP - 5) / (BRICK_HEIGHT + BRICK_GAP);
for (int row = grid_y - 1; row <= grid_y + 1; row++) {
for (int col = grid_x - 1; col <= grid_x + 1; col++) {
if (row >= 0 && row < BRICK_ROWS && col >= 0 && col < BRICK_COLS) {
if (bricks[row][col] && check_brick_collision(row, col)) {
return;
}
}
}
if (grid_x >= 0 && grid_x < BRICK_COLS && grid_y >= 0 && grid_y < BRICK_ROWS) {
check_brick_collision(grid_y, grid_x);
}
}
bool check_brick_collision(int row, int col) {
bool BreakoutView::check_brick_collision(int row, int col) {
if (!bricks[row][col]) return false;
int brick_x = col * (BRICK_WIDTH + BRICK_GAP);
int brick_y = GAME_AREA_TOP + row * (BRICK_HEIGHT + BRICK_GAP) + 5;
if (ball_x + BALL_SIZE >= brick_x && ball_x <= brick_x + BRICK_WIDTH &&
ball_y + BALL_SIZE >= brick_y && ball_y <= brick_y + BRICK_HEIGHT) {
fillrect(brick_x, brick_y, brick_x + BRICK_WIDTH, brick_y + BRICK_HEIGHT, COLOR_BACKGROUND);
bricks[row][col] = false;
brick_count--;
score += (5 - row) * 10;
draw_score();
float center_x = brick_x + BRICK_WIDTH / 2;
float center_y = brick_y + BRICK_HEIGHT / 2;
float ball_center_x = ball_x + BALL_SIZE / 2;
float ball_center_y = ball_y + BALL_SIZE / 2;
float dx = std::abs(ball_center_x - center_x);
float dy = std::abs(ball_center_y - center_y);
if (dx * BRICK_HEIGHT > dy * BRICK_WIDTH) {
ball_dx = -ball_dx;
} else {
ball_dy = -ball_dy;
}
ball_dy = -ball_dy;
return true;
}
return false;
}
bool check_level_complete() {
bool BreakoutView::check_level_complete() {
return brick_count == 0;
}
void next_level() {
void BreakoutView::next_level() {
level++;
init_level();
draw_screen();
}
void handle_game_over() {
void BreakoutView::handle_game_over() {
game_state = STATE_GAME_OVER;
gameover_initialized = false;
show_game_over();
}
void init_menu() {
cls();
background(COLOR_BACKGROUND);
auto style_yellow = *ui::Theme::getInstance()->fg_yellow;
auto style_blue = *ui::Theme::getInstance()->fg_blue;
auto style_cyan = *ui::Theme::getInstance()->fg_cyan;
int16_t screen_width = ui::screen_width;
int16_t title_x = (screen_width - 17 * 8) / 2;
int16_t divider_width = 24 * 8;
int16_t divider_x = (screen_width - divider_width) / 2;
int16_t instruction_width = 22 * 8;
int16_t instruction_x = (screen_width - instruction_width) / 2;
int16_t prompt_width = 16 * 8;
prompt_x = (screen_width - prompt_width) / 2;
painter.fill_rectangle({0, 30, screen_width, 30}, Color::black());
painter.draw_string({title_x + 2, 42}, style_yellow, "*** BREAKOUT ***");
painter.draw_string({divider_x, 70}, style_blue, "========================");
painter.fill_rectangle({instruction_x - 5, 110, instruction_width + 10, 70}, Color::black());
painter.draw_rectangle({instruction_x - 5, 110, instruction_width + 10, 70}, Color::white());
painter.draw_string({instruction_x, 120}, style_cyan, " ROTARY: MOVE PADDLE");
painter.draw_string({instruction_x, 150}, style_cyan, " SELECT: START/LAUNCH");
painter.draw_string({divider_x, 190}, style_blue, "========================");
menu_initialized = true;
}
void show_menu() {
if (!menu_initialized) {
init_menu();
}
auto style_red = *ui::Theme::getInstance()->fg_red;
if (++blink_counter >= 30) {
blink_counter = 0;
blink_state = !blink_state;
painter.fill_rectangle({prompt_x - 2, 228, 16 * 8 + 4, 20}, Color::black());
if (blink_state) {
painter.draw_string({prompt_x, 230}, style_red, "* PRESS SELECT *");
}
}
}
void init_game_over() {
cls();
background(COLOR_BACKGROUND);
auto style_red = *ui::Theme::getInstance()->fg_red;
auto style_yellow = *ui::Theme::getInstance()->fg_yellow;
// int16_t screen_width = screen_width;
int16_t title_width = 9 * 8;
int16_t title_x = (screen_width - title_width) / 2;
int16_t score_text_width = (16 + std::to_string(score).length()) * 8;
int16_t score_x = (screen_width - score_text_width) / 2;
painter.draw_rectangle({20, 80, screen_width - 40, 160}, Color::red());
painter.draw_rectangle({22, 82, screen_width - 44, 156}, Color::white());
painter.draw_string({title_x, 100}, style_red, "GAME OVER");
painter.fill_rectangle({40, 140, screen_width - 80, 30}, Color::black());
painter.draw_rectangle({40, 140, screen_width - 80, 30}, Color::yellow());
painter.draw_string({score_x, 150}, style_yellow, " FINAL SCORE: " + std::to_string(score));
int16_t restart_width = 12 * 8;
restart_x = (screen_width - restart_width) / 2;
gameover_initialized = true;
gameover_blink_state = true;
gameover_blink_counter = 0;
}
void show_game_over() {
if (!gameover_initialized) {
init_game_over();
void BreakoutView::show_menu() {
if (menu_blink_counter == 0) {
cls();
background(COLOR_BACKGROUND);
auto style_yellow = *ui::Theme::getInstance()->fg_yellow;
auto style_blue = *ui::Theme::getInstance()->fg_blue;
auto style_cyan = *ui::Theme::getInstance()->fg_cyan;
painter.draw_string({50, 40}, style_yellow, "*** BREAKOUT ***");
painter.draw_string({20, 100}, style_blue, "========================");
painter.draw_string({30, 130}, style_cyan, "ROTARY: MOVE PADDLE");
painter.draw_string({30, 160}, style_cyan, "SELECT: START/LAUNCH");
painter.draw_string({20, 190}, style_blue, "========================");
}
auto style_red = *ui::Theme::getInstance()->fg_red;
if (++menu_blink_counter >= 30) {
menu_blink_counter = 0;
menu_blink_state = !menu_blink_state;
painter.fill_rectangle({56, 228, 128, 20}, Color::black());
if (menu_blink_state) {
painter.draw_string({56, 230}, style_red, "* PRESS SELECT *");
}
}
}
void BreakoutView::show_game_over() {
if (gameover_blink_counter == 0) {
cls();
background(COLOR_BACKGROUND);
auto style_red = *ui::Theme::getInstance()->fg_red;
auto style_yellow = *ui::Theme::getInstance()->fg_yellow;
painter.draw_string({90, 100}, style_red, "GAME OVER");
painter.draw_string({50, 150}, style_yellow, "SCORE: " + std::to_string(score));
}
auto style_green = *ui::Theme::getInstance()->fg_green;
@@ -462,32 +372,27 @@ void show_game_over() {
gameover_blink_counter = 0;
gameover_blink_state = !gameover_blink_state;
painter.fill_rectangle({restart_x - 2, 198, 12 * 8 + 4, 20}, Color::black());
painter.fill_rectangle({72, 198, 96, 20}, Color::black());
if (gameover_blink_state) {
painter.draw_string({restart_x, 200}, style_green, "PRESS SELECT");
painter.draw_string({72, 200}, style_green, "PRESS SELECT");
}
}
}
void reset_game() {
void BreakoutView::reset_game() {
level = 1;
score = 0;
lives = 3;
game_state = STATE_PLAYING;
init_level();
draw_screen();
gameover_initialized = false;
gameover_blink_state = true;
gameover_blink_counter = 0;
}
BreakoutView::BreakoutView(NavigationView& nav)
: nav_{nav} {
paddle_x = (screen_width - PADDLE_WIDTH) / 2;
ball_x = screen_width / 2;
: nav_{nav}, bricks{} { // Add bricks{} here
add_children({&dummy});
game_timer.attach(&game_timer_check, 1.0 / 60.0);
attach(1.0 / 60.0);
}
void BreakoutView::on_show() {
@@ -512,20 +417,15 @@ bool BreakoutView::on_encoder(const EncoderEvent delta) {
if (game_state == STATE_PLAYING) {
if (delta > 0) {
move_paddle_right();
set_dirty();
} else if (delta < 0) {
move_paddle_left();
set_dirty();
}
set_dirty();
}
return true;
}
bool BreakoutView::on_key(const KeyEvent key) {
but_SELECT = (key == KeyEvent::Select);
but_LEFT = (key == KeyEvent::Left);
but_RIGHT = (key == KeyEvent::Right);
if (key == KeyEvent::Select) {
if (game_state == STATE_MENU) {
game_state = STATE_PLAYING;
@@ -535,14 +435,10 @@ bool BreakoutView::on_key(const KeyEvent key) {
} else if (game_state == STATE_GAME_OVER) {
reset_game();
}
} else if (key == KeyEvent::Left) {
if (game_state == STATE_PLAYING) {
move_paddle_left();
}
} else if (key == KeyEvent::Right) {
if (game_state == STATE_PLAYING) {
move_paddle_right();
}
} else if (key == KeyEvent::Left && game_state == STATE_PLAYING) {
move_paddle_left();
} else if (key == KeyEvent::Right && game_state == STATE_PLAYING) {
move_paddle_right();
}
set_dirty();
+79 -70
View File
@@ -17,6 +17,7 @@
#include "random.hpp"
#include "lpc43xx_cpp.hpp"
#include "ui_widget.hpp"
#include <vector>
namespace ui::external_app::breakout {
@@ -32,41 +33,15 @@ enum {
Black,
};
extern const Color pp_colors[];
extern Painter painter;
extern bool but_RIGHT;
extern bool but_LEFT;
extern bool but_SELECT;
void cls();
void background(int color);
void fillrect(int x1, int y1, int x2, int y2, int color);
void rect(int x1, int y1, int x2, int y2, int color);
#define wait(x) chThdSleepMilliseconds(x * 1000)
using Callback = void (*)(void);
void check_game_timer();
class Ticker {
public:
Ticker() = default;
void attach(Callback func, double delay_sec);
void detach();
};
#define PADDLE_WIDTH 40
#define PADDLE_HEIGHT 10
#define BALL_SIZE 8
#define BRICK_WIDTH 20
#define BRICK_HEIGHT 10
#define BRICK_ROWS 5
#define BRICK_COLS 10
#define BRICK_GAP 2
#define GAME_AREA_TOP 50
#define GAME_AREA_BOTTOM 310
#define PADDLE_Y (GAME_AREA_BOTTOM - PADDLE_HEIGHT)
#define BALL_SPEED_INCREASE 0.1f
#define STATE_MENU 0
@@ -77,49 +52,6 @@ class Ticker {
#define COLOR_PADDLE Blue
#define COLOR_BALL White
#define COLOR_BORDER White
#define COLOR_BRICK_COLORS \
{ Red, Orange, Yellow, Green, Purple }
extern Ticker game_timer;
extern int paddle_x;
extern float ball_x;
extern float ball_y;
extern float ball_dx;
extern float ball_dy;
extern int score;
extern int lives;
extern int level;
extern int game_state;
extern bool initialized;
extern bool ball_attached;
extern unsigned int brick_count;
extern bool bricks[BRICK_ROWS][BRICK_COLS];
extern int brick_colors[BRICK_ROWS];
void game_timer_check();
void init_game();
void init_level();
void draw_screen();
void draw_bricks();
void draw_paddle();
void draw_ball();
void draw_score();
void draw_lives();
void draw_level();
void draw_borders();
void move_paddle_left();
void move_paddle_right();
void launch_ball();
void update_game();
void check_collisions();
bool check_brick_collision(int row, int col);
void handle_game_over();
void show_menu();
void show_game_over();
bool check_level_complete();
void next_level();
void reset_game();
class BreakoutView : public View {
public:
@@ -134,9 +66,86 @@ class BreakoutView : public View {
bool on_encoder(const EncoderEvent event) override;
bool on_key(KeyEvent key) override;
void cls();
void background(int color);
void fillrect(int x1, int y1, int x2, int y2, int color);
void rect(int x1, int y1, int x2, int y2, int color);
void game_timer_check();
void init_game();
void init_level();
void draw_screen();
void draw_bricks();
void draw_paddle();
void draw_ball();
void draw_score();
void draw_lives();
void draw_level();
void draw_borders();
void move_paddle_left();
void move_paddle_right();
void launch_ball();
void update_game();
void check_collisions();
bool check_brick_collision(int row, int col);
void handle_game_over();
void show_menu();
void show_game_over();
bool check_level_complete();
void next_level();
void reset_game();
void check_game_timer();
void attach(double delay_sec);
void detach();
private:
bool initialized = false;
const Color pp_colors[9] = {
Color::white(),
Color::blue(),
Color::yellow(),
Color::purple(),
Color::green(),
Color::red(),
Color::magenta(),
Color::orange(),
Color::black(),
};
NavigationView& nav_;
Painter painter{};
// Dynamic screen-dependent values
int SCREEN_WIDTH = 0;
int SCREEN_HEIGHT = 0;
int GAME_AREA_BOTTOM = 0;
int PADDLE_Y = 0;
int BRICK_WIDTH = 0;
int BRICK_COLS = 0;
int PADDLE_SPEED = 0;
int paddle_x = 0;
float ball_x = 0;
float ball_y = 0;
float ball_dx = 1.5f;
float ball_dy = -2.0f;
int score = 0;
int lives = 3;
int level = 1;
int game_state = STATE_MENU;
bool initialized = false;
bool ball_attached = true;
unsigned int brick_count = 0;
bool menu_blink_state = true;
uint32_t menu_blink_counter = 0;
bool gameover_blink_state = true;
uint32_t gameover_blink_counter = 0;
std::vector<std::vector<bool>> bricks;
int brick_colors[BRICK_ROWS];
bool game_update_callback = false;
double game_update_timeout = 0;
uint32_t game_update_counter = 0;
Button dummy{
{screen_width, 0, 0, 0},
+1 -1
View File
@@ -440,7 +440,7 @@ static void _sumlr(void);
#undef __O
#undef __P
#define PI 3.141592653589793
#include "mathdef.hpp"
static void _numinput(byte k);
static double dpush(double d);
+1 -1
View File
@@ -31,7 +31,7 @@ int HALF_HEIGHT = 0;
#define JOGGING_SPEED 1.2
#define MAX_ENTITY_DISTANCE 200
#define ITEM_COLLIDER_DIST 6
#define PI 3.14159265358979323846
#include "mathdef.hpp"
#define GUN_WIDTH 30
#define GUN_HEIGHT 40
#define GUN_TARGET_POS 24
+83
View File
@@ -0,0 +1,83 @@
/*
* Copyright (C) 2024 EPIRB Decoder Implementation
*
* 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_epirb_rx.hpp"
#include "ui_navigation.hpp"
#include "external_app.hpp"
namespace ui::external_app::epirb_rx {
void initialize_app(ui::NavigationView& nav) {
nav.push<EPIRBAppView>();
}
} // namespace ui::external_app::epirb_rx
extern "C" {
__attribute__((section(".external_app.app_epirb_rx.application_information"), used)) application_information_t _application_information_epirb_rx = {
/*.memory_location = */ (uint8_t*)0x00000000,
/*.externalAppEntry = */ ui::external_app::epirb_rx::initialize_app,
/*.header_version = */ CURRENT_HEADER_VERSION,
/*.app_version = */ VERSION_MD5,
/*.app_name = */ "EPIRB RX",
/*.bitmap_data = */ {
0x00,
0x00,
0x00,
0x00,
0x7C,
0x3E,
0xFE,
0x7F,
0xFF,
0xFF,
0xF7,
0xEF,
0xE3,
0xC7,
0xE3,
0xC7,
0xE3,
0xC7,
0xE3,
0xC7,
0xF7,
0xEF,
0xFF,
0xFF,
0xFE,
0x7F,
0x7C,
0x3E,
0x00,
0x00,
0x00,
0x00,
},
/*.icon_color = */ ui::Color::red().v,
/*.menu_location = */ app_location_t::RX,
/*.desired_menu_position = */ -1,
/*.m4_app_tag = portapack::spi_flash::image_tag_epirb_rx */ {'P', 'E', 'P', 'I'},
/*.m4_app_offset = */ 0x00000000, // will be filled at compile time
};
}
+707
View File
@@ -0,0 +1,707 @@
/*
* Copyright (C) 2024 EPIRB Decoder Implementation
*
* 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 "baseband_api.hpp"
#include "portapack_persistent_memory.hpp"
#include "file_path.hpp"
#include "ui_epirb_rx.hpp"
using namespace portapack;
#include "rtc_time.hpp"
#include "string_format.hpp"
#include "ui.hpp"
#include "message.hpp"
namespace ui::external_app::epirb_rx {
EPIRBBeacon EPIRBDecoder::decode_packet(const baseband::Packet& packet) {
EPIRBBeacon beacon;
if (packet.size() < 112) {
return beacon; // Invalid packet
}
// Convert packet bits to byte array for easier processing
std::array<uint8_t, 16> data{};
for (size_t i = 0; i < std::min(packet.size() / 8, data.size()); i++) {
uint8_t byte_val = 0;
for (int bit = 0; bit < 8 && (i * 8 + bit) < packet.size(); bit++) {
if (packet[i * 8 + bit]) {
byte_val |= (1 << (7 - bit));
}
}
data[i] = byte_val;
}
// Perform BCH error detection and correction
uint8_t error_count = 0;
beacon.packet_status = perform_bch_check(data, error_count);
beacon.error_count = error_count;
// Extract beacon ID (bits 26-85, 15 hex digits)
beacon.beacon_id = 0;
for (int i = 3; i < 11; i++) {
beacon.beacon_id = (beacon.beacon_id << 8) | data[i];
}
// Extract beacon type (bits 86-88)
uint8_t type_bits = (data[10] >> 5) & 0x07;
beacon.beacon_type = decode_beacon_type(type_bits);
// Extract emergency type (bits 91-94 for some beacon types)
uint8_t emergency_bits = (data[11] >> 4) & 0x0F;
beacon.emergency_type = decode_emergency_type(emergency_bits);
// Extract location if encoded (depends on beacon type and protocol)
beacon.location = decode_location(data);
// Extract country code (bits 1-10)
beacon.country_code = decode_country_code(data);
// Set timestamp
rtc::RTC datetime;
rtcGetTime(&RTCD1, &datetime);
beacon.timestamp = datetime;
return beacon;
}
EPIRBLocation EPIRBDecoder::decode_location(const std::array<uint8_t, 16>& data) {
// EPIRB location encoding varies by protocol version
// This is a simplified decoder for the most common format
// Check for location data presence (bit patterns vary)
if ((data[12] & 0x80) == 0) {
return EPIRBLocation(); // No location data
}
// Extract latitude (simplified - actual encoding is more complex)
int32_t lat_raw = ((data[12] & 0x7F) << 10) | (data[13] << 2) | ((data[14] >> 6) & 0x03);
if (lat_raw & 0x10000) lat_raw |= 0xFFFE0000; // Sign extend
float latitude = lat_raw * (180.0f / 131072.0f);
// Extract longitude (simplified - actual encoding is more complex)
int32_t lon_raw = ((data[14] & 0x3F) << 12) | (data[15] << 4) | ((data[0] >> 4) & 0x0F);
if (lon_raw & 0x20000) lon_raw |= 0xFFFC0000; // Sign extend
float longitude = lon_raw * (360.0f / 262144.0f);
// Validate coordinates
if (latitude < -90.0f || latitude > 90.0f || longitude < -180.0f || longitude > 180.0f) {
return EPIRBLocation(); // Invalid coordinates
}
return EPIRBLocation(latitude, longitude);
}
BeaconType EPIRBDecoder::decode_beacon_type(uint8_t type_bits) {
switch (type_bits) {
case 0:
return BeaconType::OrbitingLocationBeacon;
case 1:
return BeaconType::PersonalLocatorBeacon;
case 2:
return BeaconType::EmergencyLocatorTransmitter;
case 3:
return BeaconType::SerialELT;
case 4:
return BeaconType::NationalELT;
default:
return BeaconType::Other;
}
}
EmergencyType EPIRBDecoder::decode_emergency_type(uint8_t emergency_bits) {
switch (emergency_bits) {
case 0:
return EmergencyType::Fire;
case 1:
return EmergencyType::Flooding;
case 2:
return EmergencyType::Collision;
case 3:
return EmergencyType::Grounding;
case 4:
return EmergencyType::Sinking;
case 5:
return EmergencyType::Disabled;
case 6:
return EmergencyType::Abandoning;
case 7:
return EmergencyType::Piracy;
case 8:
return EmergencyType::Man_Overboard;
default:
return EmergencyType::Other;
}
}
uint32_t EPIRBDecoder::decode_country_code(const std::array<uint8_t, 16>& data) {
// Country code is in bits 1-10 (ITU country code)
return ((data[0] & 0x03) << 8) | data[1];
}
std::string EPIRBDecoder::decode_vessel_name(const std::array<uint8_t, 16>& /* data */) {
// Vessel name extraction depends on beacon type and protocol
// This is a placeholder - actual implementation would be more complex
return "";
}
PacketStatus EPIRBDecoder::perform_bch_check(std::array<uint8_t, 16>& data, uint8_t& error_count) {
// Make a copy to detect changes
std::array<uint8_t, 16> original_data = data;
// Calculate BCH syndrome
uint32_t syndrome = calculate_bch_syndrome(data);
if (syndrome == 0) {
// No errors detected
error_count = 0;
return PacketStatus::Valid;
}
// Try to correct single-bit error
if (correct_single_error(data, syndrome)) {
// Successfully corrected
error_count = count_bit_errors(original_data, data);
return PacketStatus::Corrected;
}
// Multiple errors or uncorrectable
error_count = 255; // Indicate unknown error count
return PacketStatus::Error;
}
uint32_t EPIRBDecoder::calculate_bch_syndrome(const std::array<uint8_t, 16>& data) {
// BCH(127,92,5) polynomial for EPIRB: x^35 + x^2 + x + 1
// This is a simplified implementation - actual EPIRB uses BCH(63,21,6)
uint32_t syndrome = 0;
uint32_t polynomial = 0x80000007; // x^31 + x^2 + x + 1 (simplified)
// Process each byte of the data
for (int i = 0; i < 14; i++) { // Only data bits, not parity
uint32_t byte_val = data[i];
for (int bit = 7; bit >= 0; bit--) {
syndrome <<= 1;
if (byte_val & (1 << bit)) {
syndrome |= 1;
}
// XOR with polynomial if MSB is set
if (syndrome & 0x80000000) {
syndrome ^= polynomial;
}
}
}
// XOR with parity bits
syndrome ^= (data[14] << 8) | data[15];
return syndrome & 0xFFFF; // 16-bit syndrome
}
bool EPIRBDecoder::correct_single_error(std::array<uint8_t, 16>& data, uint32_t syndrome) {
// Simplified single-error correction
// This is a basic implementation - real BCH correction is more complex
if (syndrome == 0) return true; // No error
// Look up table for single-bit error patterns (simplified)
// In a real implementation, this would be a proper BCH syndrome table
for (int byte_idx = 0; byte_idx < 14; byte_idx++) {
for (int bit_idx = 0; bit_idx < 8; bit_idx++) {
// Create test error pattern
std::array<uint8_t, 16> test_data = data;
test_data[byte_idx] ^= (1 << bit_idx);
// Check if this correction produces zero syndrome
if (calculate_bch_syndrome(test_data) == 0) {
// Found the error location, apply correction
data[byte_idx] ^= (1 << bit_idx);
return true;
}
}
}
return false; // Could not correct
}
uint8_t EPIRBDecoder::count_bit_errors(const std::array<uint8_t, 16>& original, const std::array<uint8_t, 16>& corrected) {
uint8_t count = 0;
for (size_t i = 0; i < 16; i++) {
uint8_t diff = original[i] ^ corrected[i];
// Count set bits in diff
while (diff) {
count += diff & 1;
diff >>= 1;
}
}
return count;
}
void EPIRBLogger::on_packet(const EPIRBBeacon& beacon) {
std::string entry = "EPIRB," +
to_string_dec_uint(beacon.beacon_id, 15, '0') + "," +
to_string_dec_uint(static_cast<uint8_t>(beacon.beacon_type)) + "," +
to_string_dec_uint(static_cast<uint8_t>(beacon.emergency_type)) + ",";
if (beacon.location.valid) {
entry += to_string_decimal(beacon.location.latitude, 6) + "," +
to_string_decimal(beacon.location.longitude, 6);
} else {
entry += ",";
}
entry += "," + to_string_dec_uint(beacon.country_code) + "," +
format_packet_status(beacon.packet_status) + "," +
to_string_dec_uint(beacon.error_count) + "\n";
log_file.write_entry(beacon.timestamp, entry);
}
std::string format_beacon_type(BeaconType type) {
switch (type) {
case BeaconType::OrbitingLocationBeacon:
return "OLB";
case BeaconType::PersonalLocatorBeacon:
return "PLB";
case BeaconType::EmergencyLocatorTransmitter:
return "ELT";
case BeaconType::SerialELT:
return "S-ELT";
case BeaconType::NationalELT:
return "N-ELT";
default:
return "Other";
}
}
std::string format_emergency_type(EmergencyType type) {
switch (type) {
case EmergencyType::Fire:
return "Fire";
case EmergencyType::Flooding:
return "Flooding";
case EmergencyType::Collision:
return "Collision";
case EmergencyType::Grounding:
return "Grounding";
case EmergencyType::Sinking:
return "Sinking";
case EmergencyType::Disabled:
return "Disabled";
case EmergencyType::Abandoning:
return "Abandoning";
case EmergencyType::Piracy:
return "Piracy";
case EmergencyType::Man_Overboard:
return "MOB";
default:
return "Other";
}
}
std::string format_packet_status(PacketStatus status) {
switch (status) {
case PacketStatus::Valid:
return "OK";
case PacketStatus::Corrected:
return "CORR";
case PacketStatus::Error:
return "ERR";
default:
return "UNK";
}
}
ui::Color get_packet_status_color(PacketStatus status) {
switch (status) {
case PacketStatus::Valid:
return ui::Color::green();
case PacketStatus::Corrected:
return ui::Color::yellow();
case PacketStatus::Error:
return ui::Color::red();
default:
return ui::Color::white();
}
}
EPIRBBeaconDetailView::EPIRBBeaconDetailView(ui::NavigationView& nav) {
add_children({&button_done,
&button_see_map});
button_done.on_select = [this](Button&) {
if (on_close) on_close();
};
button_see_map.on_select = [this, &nav](Button&) {
if (beacon_.location.valid) {
nav.push<GeoMapView>(
to_string_hex(beacon_.beacon_id, 8), // tag as string
0, // altitude
GeoPos::alt_unit::METERS,
GeoPos::spd_unit::NONE,
beacon_.location.latitude,
beacon_.location.longitude,
0, // angle
[this]() {
if (on_close) on_close();
});
}
};
}
void EPIRBBeaconDetailView::set_beacon(const EPIRBBeacon& beacon) {
beacon_ = beacon;
set_dirty();
}
void EPIRBBeaconDetailView::focus() {
button_see_map.focus();
}
void EPIRBBeaconDetailView::paint(ui::Painter& painter) {
View::paint(painter);
const auto rect = screen_rect();
const auto s = style();
auto draw_cursor = rect.location();
draw_cursor += {8, 8};
draw_cursor = draw_field(painter, {draw_cursor, {200, 16}}, s,
"Beacon ID", to_string_hex(beacon_.beacon_id, 15))
.location();
draw_cursor = draw_field(painter, {draw_cursor, {200, 16}}, s,
"Type", format_beacon_type(beacon_.beacon_type))
.location();
draw_cursor = draw_field(painter, {draw_cursor, {200, 16}}, s,
"Emergency", format_emergency_type(beacon_.emergency_type))
.location();
if (beacon_.location.valid) {
draw_cursor = draw_field(painter, {draw_cursor, {200, 16}}, s,
"Latitude", to_string_decimal(beacon_.location.latitude, 6) + "°")
.location();
draw_cursor = draw_field(painter, {draw_cursor, {200, 16}}, s,
"Longitude", to_string_decimal(beacon_.location.longitude, 6) + "°")
.location();
} else {
draw_cursor = draw_field(painter, {draw_cursor, {200, 16}}, s,
"Location", "Unknown")
.location();
}
draw_cursor = draw_field(painter, {draw_cursor, {200, 16}}, s,
"Country", to_string_dec_uint(beacon_.country_code))
.location();
draw_cursor = draw_field(painter, {draw_cursor, {200, 16}}, s,
"Time", to_string_datetime(beacon_.timestamp, HMS))
.location();
// Show packet status with appropriate color
std::string status_text = format_packet_status(beacon_.packet_status);
if (beacon_.error_count > 0 && beacon_.packet_status == PacketStatus::Corrected) {
status_text += " (" + to_string_dec_uint(beacon_.error_count) + " err)";
}
draw_cursor = draw_field(painter, {draw_cursor, {200, 16}}, s,
"Status", status_text)
.location();
}
ui::Rect EPIRBBeaconDetailView::draw_field(
ui::Painter& painter,
const ui::Rect& draw_rect,
const ui::Style& style,
const std::string& label,
const std::string& value) {
const auto label_width = 8 * 8;
painter.draw_string({draw_rect.location()}, style, label + ":");
painter.draw_string({draw_rect.location() + ui::Point{label_width, 0}}, style, value);
return {draw_rect.location() + ui::Point{0, draw_rect.height()}, draw_rect.size()};
}
EPIRBAppView::EPIRBAppView(ui::NavigationView& nav)
: nav_(nav) {
baseband::run_prepared_image(portapack::memory::map::m4_code.base());
add_children({&label_frequency,
&options_frequency,
&field_rf_amp,
&field_lna,
&field_vga,
&rssi,
&field_volume,
&channel,
&label_status,
&label_beacons_count,
&label_latest,
&text_latest_info,
&label_packet_stats,
&console,
&button_map,
&button_clear,
&button_log});
button_map.on_select = [this](Button&) {
this->on_show_map();
};
button_clear.on_select = [this](Button&) {
this->on_clear_beacons();
};
button_log.on_select = [this](Button&) {
this->on_toggle_log();
};
options_frequency.on_change = [this](size_t, ui::OptionsField::value_t v) {
receiver_model.set_target_frequency(v);
};
options_frequency.set_by_value(receiver_model.target_frequency());
signal_token_tick_second = rtc_time::signal_tick_second += [this]() {
this->on_tick_second();
};
// Configure receiver for default EPIRB frequency (406.028 MHz)
receiver_model.set_target_frequency(406028000);
receiver_model.set_rf_amp(true);
receiver_model.set_lna(32);
receiver_model.set_vga(32);
receiver_model.set_sampling_rate(2457600);
receiver_model.enable();
logger = std::make_unique<EPIRBLogger>();
if (logger) {
logger->append(logs_dir / "epirb_rx.txt");
}
}
EPIRBAppView::~EPIRBAppView() {
rtc_time::signal_tick_second -= signal_token_tick_second;
receiver_model.disable();
baseband::shutdown();
}
void EPIRBAppView::set_parent_rect(const ui::Rect new_parent_rect) {
View::set_parent_rect(new_parent_rect);
const auto console_rect = ui::Rect{
new_parent_rect.left(),
new_parent_rect.top() + header_height,
new_parent_rect.width(),
new_parent_rect.height() - header_height - 32};
console.set_parent_rect(console_rect);
}
void EPIRBAppView::paint(ui::Painter& /* painter */) {
// Custom painting if needed
}
void EPIRBAppView::focus() {
options_frequency.focus();
}
void EPIRBAppView::on_packet(const baseband::Packet& packet) {
// Decode the EPIRB packet
auto beacon = EPIRBDecoder::decode_packet(packet);
if (beacon.beacon_id != 0) { // Valid beacon decoded
on_beacon_decoded(beacon);
}
}
void EPIRBAppView::on_beacon_decoded(const EPIRBBeacon& beacon) {
beacons_received++;
// Track packet statistics
switch (beacon.packet_status) {
case PacketStatus::Valid:
packets_valid++;
break;
case PacketStatus::Corrected:
packets_corrected++;
break;
case PacketStatus::Error:
packets_error++;
break;
}
recent_beacons.push_back(beacon);
// Keep only last 50 beacons
if (recent_beacons.size() > 50) {
recent_beacons.erase(recent_beacons.begin());
}
// Update display
update_display();
// Log the beacon
if (logger) {
logger->on_packet(beacon);
}
// Display in console with full details and colored status
std::string beacon_info = format_beacon_summary(beacon);
if (beacon.emergency_type != EmergencyType::Other) {
beacon_info += " [" + format_emergency_type(beacon.emergency_type) + "]";
}
// Add colored status indicator
std::string status_color;
switch (beacon.packet_status) {
case PacketStatus::Valid:
status_color = STR_COLOR_GREEN;
break;
case PacketStatus::Corrected:
status_color = STR_COLOR_YELLOW;
break;
case PacketStatus::Error:
status_color = STR_COLOR_RED;
break;
default:
status_color = STR_COLOR_WHITE;
break;
}
beacon_info += " [" + status_color + format_packet_status(beacon.packet_status) + STR_COLOR_WHITE + "]";
if (beacon.error_count > 0 && beacon.packet_status == PacketStatus::Corrected) {
beacon_info += " (" + to_string_dec_uint(beacon.error_count) + "e)";
}
console.write(beacon_info + "\n");
}
void EPIRBAppView::on_show_map() {
if (!recent_beacons.empty()) {
// Find latest beacon with valid location
for (auto it = recent_beacons.rbegin(); it != recent_beacons.rend(); ++it) {
if (it->location.valid) {
// Create a GeoMapView with all beacon locations
auto map_view = nav_.push<ui::GeoMapView>(
"EPIRB", // tag
0, // altitude
ui::GeoPos::alt_unit::METERS,
ui::GeoPos::spd_unit::NONE,
it->location.latitude,
it->location.longitude,
0 // angle
);
// Add all beacons with valid locations as markers
for (const auto& beacon : recent_beacons) {
if (beacon.location.valid) {
ui::GeoMarker marker;
marker.lat = beacon.location.latitude;
marker.lon = beacon.location.longitude;
marker.angle = 0;
marker.tag = to_string_hex(beacon.beacon_id, 8) + " " +
format_beacon_type(beacon.beacon_type);
map_view->store_marker(marker);
}
}
return;
}
}
}
// No valid location found
nav_.display_modal("No Location", "No beacons with valid\nlocation data found.");
}
void EPIRBAppView::on_clear_beacons() {
recent_beacons.clear();
beacons_received = 0;
packets_valid = 0;
packets_corrected = 0;
packets_error = 0;
console.clear(true);
update_display();
}
void EPIRBAppView::on_toggle_log() {
// Toggle logging functionality
if (logger) {
logger.reset();
button_log.set_text("Log");
} else {
logger = std::make_unique<EPIRBLogger>();
logger->append("epirb_rx.txt");
button_log.set_text("Stop");
}
}
void EPIRBAppView::on_tick_second() {
// Update status display every second
rtc::RTC datetime;
rtcGetTime(&RTCD1, &datetime);
label_status.set("Listening... " + to_string_datetime(datetime, HM));
}
void EPIRBAppView::update_display() {
label_beacons_count.set("Beacons: " + to_string_dec_uint(beacons_received));
// Update packet statistics display
std::string stats = std::string("Stats: ") +
STR_COLOR_GREEN + to_string_dec_uint(packets_valid) + "OK " +
STR_COLOR_YELLOW + to_string_dec_uint(packets_corrected) + "CORR " +
STR_COLOR_RED + to_string_dec_uint(packets_error) + "ERR" + STR_COLOR_WHITE;
label_packet_stats.set(stats);
if (!recent_beacons.empty()) {
const auto& latest = recent_beacons.back();
text_latest_info.set(format_beacon_summary(latest));
}
}
std::string EPIRBAppView::format_beacon_summary(const EPIRBBeacon& beacon) {
std::string summary = to_string_hex(beacon.beacon_id, 8) + " " +
format_beacon_type(beacon.beacon_type);
if (beacon.location.valid) {
summary += " " + format_location(beacon.location);
}
// Add status indicator for summary display
summary += " " + format_packet_status(beacon.packet_status);
return summary;
}
std::string EPIRBAppView::format_location(const EPIRBLocation& location) {
return to_string_decimal(location.latitude, 4) + "°," +
to_string_decimal(location.longitude, 4) + "°";
}
} // namespace ui::external_app::epirb_rx
+297
View File
@@ -0,0 +1,297 @@
/*
* Copyright (C) 2024 EPIRB Decoder Implementation
*
* 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_EPIRB_RX_H__
#define __UI_EPIRB_RX_H__
#include "app_settings.hpp"
#include "radio_state.hpp"
#include "ui_widget.hpp"
#include "ui_navigation.hpp"
#include "ui_receiver.hpp"
#include "ui_geomap.hpp"
#include "event_m0.hpp"
#include "signal.hpp"
#include "message.hpp"
#include "log_file.hpp"
#include "baseband_packet.hpp"
/* #include <cstdint>
#include <cstddef>
#include <string>
#include <array> */
namespace ui::external_app::epirb_rx {
// EPIRB 406 MHz beacon types
enum class BeaconType : uint8_t {
OrbitingLocationBeacon = 0,
PersonalLocatorBeacon = 1,
EmergencyLocatorTransmitter = 2,
SerialELT = 3,
NationalELT = 4,
Other = 15
};
// EPIRB distress and emergency types
enum class EmergencyType : uint8_t {
Fire = 0,
Flooding = 1,
Collision = 2,
Grounding = 3,
Sinking = 4,
Disabled = 5,
Abandoning = 6,
Piracy = 7,
Man_Overboard = 8,
Other = 15
};
struct EPIRBLocation {
float latitude; // degrees, -90 to +90
float longitude; // degrees, -180 to +180
bool valid;
EPIRBLocation()
: latitude(0.0f), longitude(0.0f), valid(false) {}
EPIRBLocation(float lat, float lon)
: latitude(lat), longitude(lon), valid(true) {}
};
enum class PacketStatus : uint8_t {
Valid = 0,
Corrected = 1,
Error = 2
};
struct EPIRBBeacon {
uint32_t beacon_id;
BeaconType beacon_type;
EmergencyType emergency_type;
EPIRBLocation location;
uint32_t country_code;
std::string vessel_name;
rtc::RTC timestamp;
uint32_t sequence_number;
PacketStatus packet_status;
uint8_t error_count;
EPIRBBeacon()
: beacon_id(0), beacon_type(BeaconType::Other), emergency_type(EmergencyType::Other), location(), country_code(0), vessel_name(), timestamp(), sequence_number(0), packet_status(PacketStatus::Error), error_count(0) {}
};
class EPIRBDecoder {
public:
static EPIRBBeacon decode_packet(const baseband::Packet& packet);
private:
static EPIRBLocation decode_location(const std::array<uint8_t, 16>& data);
static BeaconType decode_beacon_type(uint8_t type_bits);
static EmergencyType decode_emergency_type(uint8_t emergency_bits);
static uint32_t decode_country_code(const std::array<uint8_t, 16>& data);
static std::string decode_vessel_name(const std::array<uint8_t, 16>& data);
// BCH error correction methods
static PacketStatus perform_bch_check(std::array<uint8_t, 16>& data, uint8_t& error_count);
static uint32_t calculate_bch_syndrome(const std::array<uint8_t, 16>& data);
static bool correct_single_error(std::array<uint8_t, 16>& data, uint32_t syndrome);
static uint8_t count_bit_errors(const std::array<uint8_t, 16>& original, const std::array<uint8_t, 16>& corrected);
};
class EPIRBLogger {
public:
Optional<File::Error> append(const std::filesystem::path& filename) {
return log_file.append(filename);
}
void on_packet(const EPIRBBeacon& beacon);
private:
LogFile log_file{};
};
// Forward declarations of formatting functions
std::string format_beacon_type(BeaconType type);
std::string format_emergency_type(EmergencyType type);
std::string format_packet_status(PacketStatus status);
ui::Color get_packet_status_color(PacketStatus status);
class EPIRBBeaconDetailView : public ui::View {
public:
std::function<void(void)> on_close{};
EPIRBBeaconDetailView(ui::NavigationView& nav);
EPIRBBeaconDetailView(const EPIRBBeaconDetailView&) = delete;
EPIRBBeaconDetailView& operator=(const EPIRBBeaconDetailView&) = delete;
void set_beacon(const EPIRBBeacon& beacon);
const EPIRBBeacon& beacon() const { return beacon_; }
void focus() override;
void paint(ui::Painter&) override;
ui::GeoMapView* get_geomap_view() { return geomap_view; }
private:
EPIRBBeacon beacon_{};
ui::Button button_done{
{125, 224, 96, 24},
"Done"};
ui::Button button_see_map{
{19, 224, 96, 24},
"See on map"};
ui::GeoMapView* geomap_view{nullptr};
ui::Rect draw_field(
ui::Painter& painter,
const ui::Rect& draw_rect,
const ui::Style& style,
const std::string& label,
const std::string& value);
};
class EPIRBAppView : public ui::View {
public:
EPIRBAppView(ui::NavigationView& nav);
~EPIRBAppView();
void set_parent_rect(const ui::Rect new_parent_rect) override;
void paint(ui::Painter&) override;
void focus() override;
std::string title() const override { return "EPIRB RX"; }
private:
app_settings::SettingsManager settings_{
"rx_epirb", app_settings::Mode::RX};
ui::NavigationView& nav_;
std::vector<EPIRBBeacon> recent_beacons{};
std::unique_ptr<EPIRBLogger> logger{};
EPIRBBeaconDetailView beacon_detail_view{nav_};
static constexpr auto header_height = 4 * 16;
ui::Text label_frequency{
{0 * 8, 0 * 16, 4 * 8, 1 * 16},
"Freq"};
ui::OptionsField options_frequency{
{5 * 8, 0 * 16},
7,
{
{"406.028", 406028000},
{"406.025", 406025000},
{"406.037", 406037000},
{"433.025", 433025000},
{"144.875", 144875000},
}};
ui::RFAmpField field_rf_amp{
{13 * 8, 0 * 16}};
ui::LNAGainField field_lna{
{15 * 8, 0 * 16}};
ui::VGAGainField field_vga{
{18 * 8, 0 * 16}};
ui::RSSI rssi{
{21 * 8, 0, 6 * 8, 4}};
ui::AudioVolumeField field_volume{
{screen_width - 2 * 8, 0 * 16}};
ui::Channel channel{
{21 * 8, 5, 6 * 8, 4}};
// Status display
ui::Text label_status{
{0 * 8, 1 * 16, 15 * 8, 1 * 16},
"Listening..."};
ui::Text label_beacons_count{
{16 * 8, 1 * 16, 14 * 8, 1 * 16},
"Beacons: 0"};
ui::Text label_packet_stats{
{0 * 8, 3 * 16, 29 * 8, 1 * 16},
""};
// Latest beacon info display
ui::Text label_latest{
{0 * 8, 2 * 16, 8 * 8, 1 * 16},
"Latest:"};
ui::Text text_latest_info{
{8 * 8, 2 * 16, 22 * 8, 1 * 16},
""};
// Beacon list
ui::Console console{
{0, 4 * 16, 240, 152}};
ui::Button button_map{
{0, 224, 60, 24},
"Map"};
ui::Button button_clear{
{64, 224, 60, 24},
"Clear"};
ui::Button button_log{
{128, 224, 60, 24},
"Log"};
SignalToken signal_token_tick_second{};
uint32_t beacons_received = 0;
uint32_t packets_valid = 0;
uint32_t packets_corrected = 0;
uint32_t packets_error = 0;
MessageHandlerRegistration message_handler_packet{
Message::ID::EPIRBPacket,
[this](Message* const p) {
const auto message = static_cast<const EPIRBPacketMessage*>(p);
this->on_packet(message->packet);
}};
void on_packet(const baseband::Packet& packet);
void on_beacon_decoded(const EPIRBBeacon& beacon);
void on_show_map();
void on_clear_beacons();
void on_toggle_log();
void on_tick_second();
void update_display();
std::string format_beacon_summary(const EPIRBBeacon& beacon);
std::string format_location(const EPIRBLocation& location);
};
} // namespace ui::external_app::epirb_rx
#endif // __UI_EPIRB_RX_H__
@@ -96,22 +96,7 @@ void ERTRecentEntry::update(const ert::Packet& packet) {
packet_type = packet.type();
}
namespace ui {
template <>
void RecentEntriesTable<ERTRecentEntries>::draw(
const Entry& entry,
const Rect& target_rect,
Painter& painter,
const Style& style) {
std::string line = ert::format::id(entry.id) + " " + ert::format::commodity_type(entry.commodity_type) + " " + ert::format::consumption(entry.last_consumption) + " ";
line += (entry.packet_type == ert::Packet::Type::SCM) ? ert::format::tamper_flags_scm(entry.last_tamper_flags) : ert::format::tamper_flags(entry.last_tamper_flags);
line += (entry.received_count > 99) ? " ++" : to_string_dec_uint(entry.received_count, 3);
line.resize(target_rect.width() / 8, ' ');
painter.draw_string(target_rect.location(), style, line);
}
namespace ui::external_app::ert_app {
ERTAppView::ERTAppView(NavigationView& nav)
: nav_{nav} {
@@ -182,4 +167,21 @@ void ERTAppView::on_freqchg(int64_t freq) {
field_frequency.set_value(freq);
}
} /* namespace ui */
} /* namespace ui::external_app::ert_app */
namespace ui {
template <>
void RecentEntriesTable<ui::external_app::ert_app::ERTRecentEntries>::draw(
const Entry& entry,
const Rect& target_rect,
Painter& painter,
const Style& style) {
std::string line = ert::format::id(entry.id) + " " + ert::format::commodity_type(entry.commodity_type) + " " + ert::format::consumption(entry.last_consumption) + " ";
line += (entry.packet_type == ert::Packet::Type::SCM) ? ert::format::tamper_flags_scm(entry.last_tamper_flags) : ert::format::tamper_flags(entry.last_tamper_flags);
line += (entry.received_count > 99) ? " ++" : to_string_dec_uint(entry.received_count, 3);
line.resize(target_rect.width() / 8, ' ');
painter.draw_string(target_rect.location(), style, line);
}
} // namespace ui
@@ -104,10 +104,9 @@ class ERTLogger {
LogFile log_file{};
};
namespace ui::external_app::ert_app {
using ERTRecentEntries = RecentEntries<ERTRecentEntry>;
namespace ui {
using ERTRecentEntriesView = RecentEntriesView<ERTRecentEntries>;
class ERTAppView : public View {
@@ -188,6 +187,6 @@ class ERTAppView : public View {
void on_show_list();
};
} /* namespace ui */
} /* namespace ui::external_app::ert_app */
#endif /*__ERT_APP_H__*/
+83
View File
@@ -0,0 +1,83 @@
/*
* Copyright (C) 2023 Bernd Herzog
*
* This file is part of PortaPack.
*
* This program is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation; either version 2, or (at your option)
* any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program; see the file COPYING. If not, write to
* the Free Software Foundation, Inc., 51 Franklin Street,
* Boston, MA 02110-1301, USA.
*/
#include "ui.hpp"
#include "ert_app.hpp"
#include "ui_navigation.hpp"
#include "external_app.hpp"
namespace ui::external_app::ert_app {
void initialize_app(ui::NavigationView& nav) {
nav.push<ERTAppView>();
}
} // namespace ui::external_app::ert_app
extern "C" {
__attribute__((section(".external_app.app_ert.application_information"), used)) application_information_t _application_information_ert = {
/*.memory_location = */ (uint8_t*)0x00000000,
/*.externalAppEntry = */ ui::external_app::ert_app::initialize_app,
/*.header_version = */ CURRENT_HEADER_VERSION,
/*.app_version = */ VERSION_MD5,
/*.app_name = */ "ERT Meter",
/*.bitmap_data = */ {
0x00,
0x00,
0x00,
0x0F,
0x80,
0x7F,
0xC0,
0x0F,
0xFC,
0x0F,
0xC2,
0x0F,
0x82,
0x7F,
0x01,
0x0F,
0x01,
0x00,
0x21,
0x05,
0x53,
0x09,
0x56,
0x09,
0x50,
0x05,
0x50,
0x05,
0x20,
0xAD,
0x00,
0x00,
},
/*.icon_color = */ ui::Color::green().v,
/*.menu_location = */ app_location_t::RX,
/*.desired_menu_position = */ -1,
/*.m4_app_tag = portapack::spi_flash::image_tag_none */ {'P', 'E', 'R', 'T'},
/*.m4_app_offset = */ 0x00000000, // will be filled at compile time
};
}
+29 -8
View File
@@ -110,12 +110,13 @@ set(EXTCPPSRC
#wefax_rx
external/wefax_rx/main.cpp
external/wefax_rx/ui_wefax_rx.cpp
#noaaapt_rx
external/noaaapt_rx/main.cpp
external/noaaapt_rx/ui_noaaapt_rx.cpp
#shoppingcart_lock
external/shoppingcart_lock/main.cpp
@@ -215,15 +216,15 @@ set(EXTCPPSRC
#gfxEQ
external/gfxeq/main.cpp
external/gfxeq/ui_gfxeq.cpp
external/gfxeq/ui_gfxeq.cpp
#detector_rx
external/detector_rx/main.cpp
external/detector_rx/ui_detector_rx.cpp
external/detector_rx/ui_detector_rx.cpp
#space_invaders
external/spaceinv/main.cpp
external/spaceinv/ui_spaceinv.cpp
external/spaceinv/ui_spaceinv.cpp
#blackjack
external/blackjack/main.cpp
@@ -231,7 +232,23 @@ set(EXTCPPSRC
#battleship
external/battleship/main.cpp
external/battleship/ui_battleship.cpp
external/battleship/ui_battleship.cpp
#ert
external/ert/main.cpp
external/ert/ert_app.cpp
#epirb_rx
external/epirb_rx/main.cpp
external/epirb_rx/ui_epirb_rx.cpp
#soundboard
external/soundboard/main.cpp
external/soundboard/soundboard_app.cpp
#game2048
external/game2048/main.cpp
external/game2048/ui_game2048.cpp
)
set(EXTAPPLIST
@@ -260,7 +277,7 @@ set(EXTAPPLIST
morse_tx
sstvtx
random_password
#acars_rx
# acars_rx
ookbrute
ook_editor
wefax_rx
@@ -291,4 +308,8 @@ set(EXTAPPLIST
spaceinv
blackjack
battleship
ert
epirb_rx
soundboard
game2048
)
+37 -7
View File
@@ -79,6 +79,12 @@ MEMORY
ram_external_app_spaceinv (rwx) : org = 0xADE60000, len = 32k
ram_external_app_blackjack (rwx) : org = 0xADE70000, len = 32k
ram_external_app_battleship (rwx) : org = 0xADE80000, len = 32k
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
}
SECTIONS
@@ -340,7 +346,7 @@ SECTIONS
KEEP(*(.external_app.app_stopwatch.application_information));
*(*ui*external_app*stopwatch*);
} > ram_external_app_stopwatch
.external_app_wefax_rx : ALIGN(4) SUBALIGN(4)
{
KEEP(*(.external_app.app_wefax_rx.application_information));
@@ -363,19 +369,19 @@ SECTIONS
{
KEEP(*(.external_app.app_doom.application_information));
*(*ui*external_app*doom*);
} > ram_external_app_doom
} > ram_external_app_doom
.external_app_debug_pmem : ALIGN(4) SUBALIGN(4)
{
KEEP(*(.external_app.app_debug_pmem.application_information));
*(*ui*external_app*debug_pmem*);
} > ram_external_app_debug_pmem
} > ram_external_app_debug_pmem
.external_app_scanner : ALIGN(4) SUBALIGN(4)
{
KEEP(*(.external_app.app_scanner.application_information));
*(*ui*external_app*scanner*);
} > ram_external_app_scanner
} > ram_external_app_scanner
.external_app_level : ALIGN(4) SUBALIGN(4)
{
@@ -407,17 +413,41 @@ SECTIONS
*(*ui*external_app*spaceinv*);
} > ram_external_app_spaceinv
.external_app_blackjack : ALIGN(4) SUBALIGN(4)
.external_app_blackjack : ALIGN(4) SUBALIGN(4)
{
KEEP(*(.external_app.app_blackjack.application_information));
*(*ui*external_app*blackjack*);
} > ram_external_app_blackjack
.external_app_battleship : ALIGN(4) SUBALIGN(4)
.external_app_battleship : ALIGN(4) SUBALIGN(4)
{
KEEP(*(.external_app.app_battleship.application_information));
*(*ui*external_app*battleship*);
} > ram_external_app_battleship
.external_app_ert : ALIGN(4) SUBALIGN(4)
{
KEEP(*(.external_app.app_ert.application_information));
*(*ui*external_app*ert*);
} > ram_external_app_ert
.external_app_epirb_rx : ALIGN(4) SUBALIGN(4)
{
KEEP(*(.external_app.app_epirb_rx.application_information));
*(*ui*external_app*epirb_rx*);
} > ram_external_app_epirb_rx
.external_app_soundboard : ALIGN(4) SUBALIGN(4)
{
KEEP(*(.external_app.app_soundboard.application_information));
*(*ui*external_app*soundboard*);
} > ram_external_app_soundboard
.external_app_game2048 : ALIGN(4) SUBALIGN(4)
{
KEEP(*(.external_app.app_game2048.application_information));
*(*ui*external_app*game2048*);
} > ram_external_app_game2048
}
@@ -1,6 +1,6 @@
/*
* Copyright (C) 2023 Mark Thompson
* copyleft Whiterose of the Dark Army
* copyleft 2024 zxkmm AKA zix aka sommermorgentraum
*
* This file is part of PortaPack.
*
@@ -1,6 +1,6 @@
/*
* Copyright (C) 2023 Mark Thompson
* copyleft Whiterose of the Dark Army
* copyleft 2024 zxkmm AKA zix aka sommermorgentraum
*
* This file is part of PortaPack.
*
+83
View File
@@ -0,0 +1,83 @@
/*
* Copyright (C) 2023 Bernd Herzog
*
* This file is part of PortaPack.
*
* This program is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation; either version 2, or (at your option)
* any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program; see the file COPYING. If not, write to
* the Free Software Foundation, Inc., 51 Franklin Street,
* Boston, MA 02110-1301, USA.
*/
#include "ui.hpp"
#include "ui_game2048.hpp"
#include "ui_navigation.hpp"
#include "external_app.hpp"
namespace ui::external_app::game2048 {
void initialize_app(ui::NavigationView& nav) {
nav.push<Game2048View>();
}
} // namespace ui::external_app::game2048
extern "C" {
__attribute__((section(".external_app.app_game2048.application_information"), used)) application_information_t _application_information_game2048 = {
(uint8_t*)0x00000000,
ui::external_app::game2048::initialize_app,
CURRENT_HEADER_VERSION,
VERSION_MD5,
"2048",
{
0x8C,
0x31,
0x5A,
0x6B,
0xDE,
0x7B,
0x8C,
0x31,
0x00,
0x00,
0x8C,
0x31,
0x5A,
0x7B,
0xDE,
0x7B,
0x8C,
0x31,
0x00,
0x00,
0x8C,
0x31,
0xDA,
0x7B,
0xDE,
0x7B,
0x8C,
0x31,
0x00,
0x00,
0x00,
0x00,
},
ui::Color::green().v,
app_location_t::GAMES,
-1,
{0, 0, 0, 0},
0x00000000,
};
} // namespace ui::external_app::game2048
+422
View File
@@ -0,0 +1,422 @@
/*
* copyleft 2025 zxkmm AKA zix aka sommermorgentraum
*
* This file is part of PortaPack.
*
* This program is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation; either version 2, or (at your option)
* any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program; see the file COPYING. If not, write to
* the Free Software Foundation, Inc., 51 Franklin Street,
* Boston, MA 02110-1301, USA.
*/
#include "ui_game2048.hpp"
#include "string_format.hpp"
namespace ui::external_app::game2048 {
Game2048View::Game2048View(NavigationView& nav)
: nav_(nav), score(0), best_score(0), game_state(STATE_PLAYING) {
add_children({&dummy});
init_game();
}
void Game2048View::on_show() {
if (!initialized) {
std::srand(LPC_RTC->CTIME0);
reset_game();
initialized = true;
}
}
void Game2048View::init_game() {
for (int i = 0; i < GRID_SIZE; i++) {
for (int j = 0; j < GRID_SIZE; j++) {
grid[i][j] = 0;
}
}
}
void Game2048View::reset_game() {
need_repaint = true;
init_game();
score = 0;
game_state = STATE_PLAYING;
add_random_tile();
add_random_tile();
}
void Game2048View::add_random_tile() {
std::vector<std::pair<int, int>> empty_cells;
for (int i = 0; i < GRID_SIZE; i++) {
for (int j = 0; j < GRID_SIZE; j++) {
if (grid[i][j] == 0) {
empty_cells.push_back(std::make_pair(i, j));
}
}
}
if (!empty_cells.empty()) {
auto& random_cell = empty_cells[rand() % empty_cells.size()];
grid[random_cell.first][random_cell.second] = (rand() % 10 == 0) ? 4 : 2;
}
}
void Game2048View::compress_left(int row[]) {
int temp[GRID_SIZE];
int index = 0;
for (int i = 0; i < GRID_SIZE; i++) {
temp[i] = 0;
}
for (int i = 0; i < GRID_SIZE; i++) {
if (row[i] != 0) {
temp[index++] = row[i];
}
}
for (int i = 0; i < GRID_SIZE; i++) {
row[i] = temp[i];
}
}
void Game2048View::merge_left(int row[]) {
for (int i = 0; i < GRID_SIZE - 1; i++) {
if (row[i] != 0 && row[i] == row[i + 1]) {
row[i] *= 2;
score += row[i];
row[i + 1] = 0;
}
}
}
bool Game2048View::move_row_left(int row[]) {
int original[GRID_SIZE];
for (int i = 0; i < GRID_SIZE; i++) {
original[i] = row[i];
}
compress_left(row);
merge_left(row);
compress_left(row);
for (int i = 0; i < GRID_SIZE; i++) {
if (original[i] != row[i]) {
return true;
}
}
return false;
}
void Game2048View::rotate_grid_clockwise() {
int temp[GRID_SIZE][GRID_SIZE];
for (int i = 0; i < GRID_SIZE; i++) {
for (int j = 0; j < GRID_SIZE; j++) {
temp[j][GRID_SIZE - 1 - i] = grid[i][j];
}
}
for (int i = 0; i < GRID_SIZE; i++) {
for (int j = 0; j < GRID_SIZE; j++) {
grid[i][j] = temp[i][j];
}
}
}
void Game2048View::rotate_grid_counter_clockwise() {
int temp[GRID_SIZE][GRID_SIZE];
for (int i = 0; i < GRID_SIZE; i++) {
for (int j = 0; j < GRID_SIZE; j++) {
temp[GRID_SIZE - 1 - j][i] = grid[i][j];
}
}
for (int i = 0; i < GRID_SIZE; i++) {
for (int j = 0; j < GRID_SIZE; j++) {
grid[i][j] = temp[i][j];
}
}
}
bool Game2048View::move_left() {
bool moved = false;
for (int i = 0; i < GRID_SIZE; i++) {
if (move_row_left(grid[i])) {
moved = true;
}
}
return moved;
}
bool Game2048View::move_right() {
rotate_grid_clockwise();
rotate_grid_clockwise();
bool moved = move_left();
rotate_grid_clockwise();
rotate_grid_clockwise();
return moved;
}
bool Game2048View::move_up() {
rotate_grid_counter_clockwise();
bool moved = move_left();
rotate_grid_clockwise();
return moved;
}
bool Game2048View::move_down() {
rotate_grid_clockwise();
bool moved = move_left();
rotate_grid_counter_clockwise();
return moved;
}
bool Game2048View::can_move() {
for (int i = 0; i < GRID_SIZE; i++) {
for (int j = 0; j < GRID_SIZE; j++) {
if (grid[i][j] == 0) return true;
if (j < GRID_SIZE - 1 && grid[i][j] == grid[i][j + 1]) return true;
if (i < GRID_SIZE - 1 && grid[i][j] == grid[i + 1][j]) return true;
}
}
return false;
}
bool Game2048View::is_game_won() {
for (int i = 0; i < GRID_SIZE; i++) {
for (int j = 0; j < GRID_SIZE; j++) {
if (grid[i][j] == 2048) {
return true;
}
}
}
return false;
}
Color Game2048View::get_tile_color(int value) {
switch (value) {
case 0:
return Color::light_grey();
case 2:
return Color::white();
case 4:
return Color::yellow();
case 8:
return Color::orange();
case 16:
return Color::red();
case 32:
return Color::magenta();
case 64:
return Color::purple();
case 128:
return Color::blue();
case 256:
return Color::cyan();
case 512:
return Color::green();
case 1024:
return Color::dark_green();
case 2048:
return Color::dark_red();
default:
return Color::black();
}
}
Color Game2048View::get_text_color(int value) {
if (value <= 4) {
return Color::black();
}
return Color::white();
}
void Game2048View::draw_tile(int x, int y, int value) {
int tile_x = BOARD_START_X + x * (TILE_SIZE + TILE_MARGIN);
int tile_y = BOARD_START_Y + y * (TILE_SIZE + TILE_MARGIN);
Color tile_color = get_tile_color(value);
painter.fill_rectangle({tile_x, tile_y, TILE_SIZE, TILE_SIZE}, tile_color);
painter.draw_rectangle({tile_x, tile_y, TILE_SIZE, TILE_SIZE}, Color::dark_grey());
if (value > 0) {
std::string value_str = to_string_dec_uint(value);
int text_width = value_str.length() * 8;
int text_height = 16;
int text_x = tile_x + (TILE_SIZE - text_width) / 2;
int text_y = tile_y + (TILE_SIZE - text_height) / 2;
painter.draw_string({text_x, text_y}, *Theme::getInstance()->bg_darkest, value_str);
}
}
void Game2048View::draw_score() {
painter.fill_rectangle({0, 0, screen_width, INFO_HEIGHT}, Color::black());
std::string score_str = "Score: " + to_string_dec_uint(score);
painter.draw_string({10, 5}, *Theme::getInstance()->bg_darkest, score_str);
if (best_score > 0) {
std::string best_str = "Best: " + to_string_dec_uint(best_score);
painter.draw_string({150, 5}, *Theme::getInstance()->bg_darkest, best_str);
}
}
void Game2048View::draw_game() {
// the paint timing is always bad at app enterences,
// in all apps it behave like this.
// this is a buffer timer to let it flash a time until app loaded.
if (!need_repaint && load_wait_time_counter++ < 40000) {
return;
}
if (need_repaint_bg_frame) painter.fill_rectangle({0, INFO_HEIGHT, screen_width, screen_height - INFO_HEIGHT}, Color::dark_grey());
for (int i = 0; i < GRID_SIZE; i++) {
for (int j = 0; j < GRID_SIZE; j++) {
draw_tile(j, i, grid[i][j]);
}
}
draw_score();
need_repaint = false;
need_repaint_bg_frame = false;
}
void Game2048View::show_game_over() {
if (show_you_won_you_lost_slow_down_cunter++ % 1000 != 0) return;
// slow down otherwies it repaint even faster than it finished paint full stuff
painter.fill_rectangle({40, 100, 160, 100}, Color::black());
painter.draw_rectangle({40, 100, 160, 100}, Color::white());
painter.draw_string({80, 115}, *Theme::getInstance()->bg_darkest, "GAME OVER");
painter.draw_string({70, 135}, *Theme::getInstance()->bg_darkest, "Press SELECT");
painter.draw_string({75, 155}, *Theme::getInstance()->bg_darkest, "to restart");
need_repaint = false;
need_repaint_bg_frame = true;
}
void Game2048View::show_you_won() {
if (show_you_won_you_lost_slow_down_cunter++ % 1000 != 0) return;
// slow down otherwies it repaint even faster than it finished paint full stuff
painter.fill_rectangle({40, 100, 160, 100}, Color::black());
painter.draw_rectangle({40, 100, 160, 100}, Color::white());
painter.draw_string({80, 115}, *Theme::getInstance()->bg_darkest, "YOU WON!");
painter.draw_string({70, 135}, *Theme::getInstance()->bg_darkest, "Press SELECT");
painter.draw_string({75, 155}, *Theme::getInstance()->bg_darkest, "to restart");
need_repaint = false;
need_repaint_bg_frame = true;
}
void Game2048View::paint(Painter&) {
draw_game();
if (game_state == STATE_GAME_OVER) {
need_repaint = true;
need_repaint_bg_frame = true;
show_game_over();
} else if (game_state == STATE_WON) {
need_repaint = true;
need_repaint_bg_frame = true;
show_you_won();
}
}
void Game2048View::frame_sync() {
draw_game();
if (game_state == STATE_GAME_OVER) {
show_game_over();
} else if (game_state == STATE_WON) {
show_you_won();
}
}
bool Game2048View::on_key(KeyEvent key) {
if (key == KeyEvent::Select) {
if (game_state == STATE_GAME_OVER || game_state == STATE_WON) {
reset_game();
return true;
}
}
if (game_state != STATE_PLAYING) {
return false;
}
bool moved = false;
switch (key) {
case KeyEvent::Left:
moved = move_left();
break;
case KeyEvent::Right:
moved = move_right();
break;
case KeyEvent::Up:
moved = move_up();
break;
case KeyEvent::Down:
moved = move_down();
break;
default:
return false;
}
if (moved) {
need_repaint = true;
add_random_tile();
if (score > best_score) {
best_score = score;
}
if (is_game_won() && game_state == STATE_PLAYING) {
game_state = STATE_WON;
} else if (!can_move()) {
game_state = STATE_GAME_OVER;
}
}
return true;
}
bool Game2048View::on_encoder(const EncoderEvent) {
if (game_state != STATE_PLAYING) {
return false;
}
bool moved = false;
if (moved) {
add_random_tile();
if (score > best_score) {
best_score = score;
}
if (is_game_won() && game_state == STATE_PLAYING) {
game_state = STATE_WON;
} else if (!can_move()) {
game_state = STATE_GAME_OVER;
}
}
return true;
}
} // namespace ui::external_app::game2048
+113
View File
@@ -0,0 +1,113 @@
/*
* copyleft 2025 zxkmm AKA zix aka sommermorgentraum
*
* This file is part of PortaPack.
*
* This program is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation; either version 2, or (at your option)
* any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program; see the file COPYING. If not, write to
* the Free Software Foundation, Inc., 51 Franklin Street,
* Boston, MA 02110-1301, USA.
*/
#ifndef __UI_GAME2048_H__
#define __UI_GAME2048_H__
#include "ui.hpp"
#include "ui_navigation.hpp"
#include "event_m0.hpp"
#include "message.hpp"
#include "irq_controls.hpp"
#include "lpc43xx_cpp.hpp"
#include "ui_widget.hpp"
namespace ui::external_app::game2048 {
#define GRID_SIZE 4
#define TILE_SIZE 50
#define TILE_MARGIN 5
#define BOARD_START_X 15
#define BOARD_START_Y 35
#define INFO_HEIGHT 30
enum GameState {
STATE_PLAYING,
STATE_GAME_OVER,
STATE_WON
};
class Game2048View : public View {
public:
Game2048View(NavigationView& nav);
void on_show() override;
std::string title() const override { return "2048"; }
void focus() override { dummy.focus(); }
void paint(Painter& painter) override;
void frame_sync();
bool on_key(KeyEvent key) override;
bool on_encoder(const EncoderEvent event) override;
private:
NavigationView& nav_;
Painter painter{};
int grid[GRID_SIZE][GRID_SIZE];
int score;
int best_score;
GameState game_state;
bool initialized = false;
bool need_repaint = true;
bool need_repaint_bg_frame = true; // drar bg frame is too flickering, so make it paint as less as possible
bool need_set_dirty = false;
uint32_t load_wait_time_counter = 0; // the paint timing is always bad at app enterences,
// in all apps it behave like this.
// this is a buffer timer to let it flash a time until app loaded.
uint32_t show_you_won_you_lost_slow_down_cunter = 0; // slow down otherwies it repaint even faster than it finished paint fulll stuff
void init_game();
void reset_game();
void add_random_tile();
bool move_left();
bool move_right();
bool move_up();
bool move_down();
bool can_move();
bool is_game_won();
void draw_game();
void draw_tile(int x, int y, int value);
void draw_score();
void show_game_over();
void show_you_won();
Color get_tile_color(int value);
Color get_text_color(int value);
void compress_left(int row[]);
void merge_left(int row[]);
bool move_row_left(int row[]);
void rotate_grid_clockwise();
void rotate_grid_counter_clockwise();
Button dummy{
{screen_width, 0, 0, 0},
""};
MessageHandlerRegistration message_handler_frame_sync{
Message::ID::DisplayFrameSync,
[this](const Message* const) {
this->frame_sync();
}};
};
} // namespace ui::external_app::game2048
#endif /* __UI_GAME2048_H__ */
+1 -1
View File
@@ -2,7 +2,7 @@
* Copyright (C) 2015 Jared Boone, ShareBrained Technology, Inc.
* Copyright (C) 2016 Furrtek
* Copyright (C) 2020 euquiq
* copyleft mr.r0b0t from the F society
* copyleft 2024 zxkmm AKA zix aka sommermorgentraum
*
* This file is part of PortaPack.
*
+1 -1
View File
@@ -2,7 +2,7 @@
* Copyright (C) 2015 Jared Boone, ShareBrained Technology, Inc.
* Copyright (C) 2016 Furrtek
* Copyright (C) 2020 euquiq
* copyleft mr.r0b0t from the F society
* copyleft 2024 zxkmm AKA zix aka sommermorgentraum
*
* This file is part of PortaPack.
*
+4 -46
View File
@@ -112,6 +112,7 @@ void RangeView::paint(Painter&) {
}
RangeView::RangeView(NavigationView& nav) {
(void)nav;
hidden(true);
add_children({&labels,
@@ -122,50 +123,6 @@ RangeView::RangeView(NavigationView& nav) {
&button_center,
&button_width});
check_enabled.on_select = [this](Checkbox&, bool v) {
frequency_range.enabled = v;
};
button_start.on_select = [this, &nav](Button& button) {
auto new_view = nav.push<FrequencyKeypadView>(frequency_range.min);
new_view->on_changed = [this, &button](rf::Frequency f) {
update_start(f);
};
};
button_stop.on_select = [this, &nav](Button& button) {
auto new_view = nav.push<FrequencyKeypadView>(frequency_range.max);
new_view->on_changed = [this, &button](rf::Frequency f) {
update_stop(f);
};
};
button_center.on_select = [this, &nav](Button& button) {
auto new_view = nav.push<FrequencyKeypadView>(center);
new_view->on_changed = [this, &button](rf::Frequency f) {
update_center(f);
};
};
button_width.on_select = [this, &nav](Button& button) {
auto new_view = nav.push<FrequencyKeypadView>(width);
new_view->on_changed = [this, &button](rf::Frequency f) {
update_width(f);
};
};
button_load_range.on_select = [this, &nav](Button&) {
auto load_view = nav.push<FrequencyLoadView>();
load_view->on_frequency_loaded = [this](rf::Frequency value) {
update_center(value);
update_width(100000);
};
load_view->on_range_loaded = [this](rf::Frequency start, rf::Frequency stop) {
update_start(start);
update_stop(stop);
};
};
check_enabled.set_value(false);
}
@@ -274,7 +231,7 @@ bool JammerView::update_config() {
return true;
} else {
if (out_of_ranges)
nav_.display_modal("Error", "Jamming bandwidth too large.\nMust be less than 24MHz.");
nav_.display_modal("Error", "Jamming bandwidth too large.\nMust be 80MHz or less.");
else
nav_.display_modal("Error", "No range enabled.");
return false;
@@ -400,7 +357,8 @@ JammerView::JammerView(NavigationView& nav)
};
for (auto range_view : range_views) {
range_view->check_enabled.on_select = [this](Checkbox&, bool) {
range_view->check_enabled.on_select = [this, range_view](Checkbox&, bool v) {
range_view->frequency_range.enabled = v;
if (jamming) update_config();
};
range_view->button_start.on_select = [this, range_view](Button&) {
+1 -1
View File
@@ -1,5 +1,5 @@
/*
* copyleft 2024 sommermorgentraum
* copyleft 2024 zxkmm AKA zix aka sommermorgentraum
*
* This file is part of PortaPack.
*
+1 -1
View File
@@ -1,5 +1,5 @@
/*
* copyleft 2024 sommermorgentraum
* copyleft 2024 zxkmm AKA zix aka sommermorgentraum
*
* This file is part of PortaPack.
*
+2 -2
View File
@@ -1,7 +1,7 @@
/*
* Copyright (C) 2024 Samir Sánchez Garnica @sasaga92
* copyleft Elliot Alderson from F society
* copyleft Darlene Alderson from F society
* copyleft 2024 zxkmm AKA zix aka sommermorgentraum
*
* This file is part of PortaPack.
*
+1 -2
View File
@@ -1,7 +1,6 @@
/*
* Copyright (C) 2024 Samir Sánchez Garnica @sasaga92
* copyleft Elliot Alderson from F society
* copyleft Darlene Alderson from F society
* copyleft 2024 zxkmm AKA zix aka sommermorgentraum
*
* This file is part of PortaPack.
*
+1 -1
View File
@@ -1,6 +1,6 @@
/*
* Copyright (C) 2024 HTotoo
* copyleft Whiterose of the Dark Army
* copyleft 2024 zxkmm AKA zix aka sommermorgentraum
*
* This file is part of PortaPack.
*
+1 -1
View File
@@ -1,6 +1,6 @@
/*
* Copyright (C) 2024 HTotoo
* copyleft Whiterose of the Dark Army
* copyleft 2024 zxkmm AKA zix aka sommermorgentraum
*
* This file is part of PortaPack.
*
@@ -1,6 +1,5 @@
/*
* copyleft Elliot Alderson from F society
* copyleft Darlene Alderson from F society
* copyleft 2025 zxkmm AKA zix aka sommermorgentraum
*
* This file is part of PortaPack.
*
@@ -1,6 +1,5 @@
/*
* copyleft Elliot Alderson from F society
* copyleft Darlene Alderson from F society
* copyleft 2025 zxkmm AKA zix aka sommermorgentraum
*
* This file is part of PortaPack.
*
+1 -1
View File
@@ -10,7 +10,7 @@
namespace ui::external_app::shoppingcart_lock {
void initialize_app(NavigationView& nav) {
baseband::run_image(portapack::spi_flash::image_tag_audio_tx);
baseband::run_prepared_image(portapack::memory::map::m4_code.base());
nav.push<ShoppingCartLock>();
}
} // namespace ui::external_app::shoppingcart_lock
+83
View File
@@ -0,0 +1,83 @@
/*
* Copyright (C) 2023 Bernd Herzog
*
* This file is part of PortaPack.
*
* This program is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation; either version 2, or (at your option)
* any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program; see the file COPYING. If not, write to
* the Free Software Foundation, Inc., 51 Franklin Street,
* Boston, MA 02110-1301, USA.
*/
#include "ui.hpp"
#include "soundboard_app.hpp"
#include "ui_navigation.hpp"
#include "external_app.hpp"
namespace ui::external_app::soundboard {
void initialize_app(ui::NavigationView& nav) {
nav.push<SoundBoardView>();
}
} // namespace ui::external_app::soundboard
extern "C" {
__attribute__((section(".external_app.app_soundboard.application_information"), used)) application_information_t _application_information_soundboard = {
/*.memory_location = */ (uint8_t*)0x00000000,
/*.externalAppEntry = */ ui::external_app::soundboard::initialize_app,
/*.header_version = */ CURRENT_HEADER_VERSION,
/*.app_version = */ VERSION_MD5,
/*.app_name = */ "Soundbrd",
/*.bitmap_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,
},
/*.icon_color = */ ui::Color::orange().v,
/*.menu_location = */ app_location_t::TX,
/*.desired_menu_position = */ -1,
/*.m4_app_tag = portapack::spi_flash::image_tag_soundboard */ {'P', 'A', 'T', 'X'},
/*.m4_app_offset = */ 0x00000000, // will be filled at compile time
};
}
@@ -33,7 +33,7 @@
using namespace tonekey;
using namespace portapack;
namespace ui {
namespace ui::external_app::soundboard {
#define FILE_PER_PAGE 50
@@ -251,7 +251,7 @@ void SoundBoardView::refresh_list() {
SoundBoardView::SoundBoardView(
NavigationView& nav)
: nav_(nav) {
baseband::run_image(portapack::spi_flash::image_tag_audio_tx);
baseband::run_prepared_image(portapack::memory::map::m4_code.base());
add_children({&labels,
&menu_view,
@@ -323,4 +323,4 @@ SoundBoardView::~SoundBoardView() {
baseband::shutdown();
}
} // namespace ui
} // namespace ui::external_app::soundboard
@@ -33,7 +33,7 @@
#include "app_settings.hpp"
#include "radio_state.hpp"
namespace ui {
namespace ui::external_app::soundboard {
class SoundBoardView : public View {
public:
@@ -178,6 +178,6 @@ class SoundBoardView : public View {
}};
};
} /* namespace ui */
} /* namespace ui::external_app::soundboard */
#endif /*__UI_SOUNDBOARD_H__*/
+1 -1
View File
@@ -1,6 +1,6 @@
/*
* Copyright 2025 Mark Thompson
* copyleft Mr. Robot of F.Society
* copyleft 2025 zxkmm AKA zix aka sommermorgentraum
*
* This file is part of PortaPack.
*
+1 -1
View File
@@ -1,6 +1,6 @@
/*
* Copyright 2025 Mark Thompson
* copyleft Mr. Robot of F.Society
* copyleft 2025 zxkmm AKA zix aka sommermorgentraum
*
* This file is part of PortaPack.
*
+1 -1
View File
@@ -1,5 +1,5 @@
/*
* copyleft 2024 sommermorgentraum
* copyleft 2024 zxkmm AKA zix aka sommermorgentraum
*
* This file is part of PortaPack.
*
+1 -1
View File
@@ -1,5 +1,5 @@
/*
* copyleft 2024 sommermorgentraum
* copyleft 2024 zxkmm AKA zix aka sommermorgentraum
*
* This file is part of PortaPack.
*
@@ -158,9 +158,10 @@ WardriveMapView::WardriveMapView(NavigationView& nav)
}
// never move this before the first load() bc that will mess load up
geomap.on_move = [this](float lon, float lat) {
geomap.on_move = [this](float lon, float lat, bool absolute) {
(void)lon;
(void)lat;
(void)absolute;
load_markers();
};
btn_back.on_select = [this, &nav](Button&) {
+1 -1
View File
@@ -262,7 +262,7 @@ void ViewWavView::on_playback_progress(const uint32_t progress) {
ViewWavView::ViewWavView(
NavigationView& nav)
: nav_(nav) {
baseband::run_image(portapack::spi_flash::image_tag_audio_tx);
baseband::run_prepared_image(portapack::memory::map::m4_code.base());
wav_reader = std::make_unique<WAVFileReader>();
add_children({&labels,
+1 -1
View File
@@ -113,7 +113,7 @@ File::Result<bool> File::eof() {
File::Result<File::Offset> File::seek(Offset new_position) {
/* NOTE: Returns *old* position, not new position */
const auto old_position = tell();
const auto result = f_lseek(&f, new_position);
const auto result = (old_position == new_position) ? FR_OK : f_lseek(&f, new_position);
if (result != FR_OK) {
return {static_cast<Error>(result)};
}
+1 -1
View File
@@ -1,6 +1,6 @@
/*
* Copyright (C) 2015 Jared Boone, ShareBrained Technology, Inc.
* copyleft (ɔ) 2023 zxkmm under GPL license
* copyleft 2023 zxkmm AKA zix aka sommermorgentraum
* Copyright (C) 2023 u-foka
*
* This file is part of PortaPack.
+1 -1
View File
@@ -1,6 +1,6 @@
/*
* Copyright (C) 2015 Jared Boone, ShareBrained Technology, Inc.
* copyleft (ɔ) 2023 zxkmm under GPL license
* copyleft 2023 zxkmm AKA zix aka sommermorgentraum
* Copyright (C) 2023 u-foka
*
* This file is part of PortaPack.
+6
View File
@@ -4,6 +4,12 @@ namespace ui {
ThemeTemplate* Theme::current = nullptr;
void Theme::destroy() {
if (current != nullptr)
delete current;
current = nullptr;
}
ThemeTemplate* Theme::getInstance() {
if (current == nullptr) SetTheme(DefaultGrey);
return Theme::current;
+1 -1
View File
@@ -113,7 +113,7 @@ class Theme {
static void SetTheme(ThemeId theme);
static ThemeTemplate* current;
static void destroy(); // used from standalone app, to prevent memleak
private:
};
+1 -1
View File
@@ -4,7 +4,7 @@
* Copyright (C) 2019 Elia Yehuda (z4ziggy)
* Copyright (C) 2023 Mark Thompson
* Copyright (C) 2024 u-foka
* Copyleft () 2024 zxkmm with the GPL license
* copyleft 2024 zxkmm AKA zix aka sommermorgentraum
*
* This file is part of PortaPack.
*
+327 -105
View File
@@ -22,21 +22,17 @@
*/
#include "ui_geomap.hpp"
#include "portapack.hpp"
#include <cstring>
#include <stdio.h>
#include <string_view>
using namespace portapack;
#include "string_format.hpp"
#include "complex.hpp"
#include "ui_font_fixed_5x8.hpp"
#include "file_path.hpp"
namespace ui {
GeoPos::GeoPos(
const Point pos,
const alt_unit altitude_unit,
@@ -188,6 +184,7 @@ int32_t GeoPos::speed() {
GeoMap::GeoMap(
Rect parent_rect)
: Widget{parent_rect}, markerListLen(0) {
has_osm = use_osm = find_osm_file_tile();
}
bool GeoMap::on_encoder(const EncoderEvent delta) {
@@ -201,6 +198,8 @@ bool GeoMap::on_encoder(const EncoderEvent delta) {
map_zoom += (map_zoom >= MAP_ZOOM_RESOLUTION_LIMIT) ? map_zoom : 1;
}
}
map_osm_zoom++;
if (has_osm) set_osm_max_zoom();
} else if (delta < 0) {
if (map_zoom > -MAX_MAP_ZOOM_OUT) {
if (map_zoom == 1) {
@@ -212,12 +211,18 @@ bool GeoMap::on_encoder(const EncoderEvent delta) {
map_zoom--;
}
}
if (map_osm_zoom > 0) map_osm_zoom--;
} else {
return false;
}
map_visible = map_opened && (map_zoom <= MAP_ZOOM_RESOLUTION_LIMIT);
zoom_pixel_offset = (map_visible && (map_zoom > 1)) ? (float)map_zoom / 2 : 0.0f;
if (use_osm) {
map_visible = true;
zoom_pixel_offset = 0;
} else {
zoom_pixel_offset = (map_visible && (map_zoom > 1)) ? (float)map_zoom / 2 : 0.0f;
}
// Trigger map redraw
redraw_map = true;
@@ -225,7 +230,7 @@ bool GeoMap::on_encoder(const EncoderEvent delta) {
return true;
}
void GeoMap::map_read_line(ui::Color* buffer, uint16_t pixels) {
void GeoMap::map_read_line_bin(ui::Color* buffer, uint16_t pixels) {
if (map_zoom == 1) {
map_file.read(buffer, pixels << 1);
} else if (map_zoom > 1) {
@@ -251,7 +256,7 @@ void GeoMap::map_read_line(ui::Color* buffer, uint16_t pixels) {
for (int i = 0; i < geomap_rect_width; i++) {
buffer[i] = zoom_out_buffer[i * (-map_zoom)];
}
delete zoom_out_buffer;
delete[] zoom_out_buffer;
}
}
@@ -264,39 +269,86 @@ void GeoMap::draw_markers(Painter& painter) {
void GeoMap::draw_marker_item(Painter& painter, GeoMarker& item, const Color color, const Color fontColor, const Color backColor) {
const auto r = screen_rect();
const ui::Point itemPoint = item_rect_pixel(item);
if ((itemPoint.x() >= 0) && (itemPoint.x() < r.width()) &&
(itemPoint.y() > 10) && (itemPoint.y() < r.height())) // Dont draw within symbol size of top
{
draw_marker(painter, {itemPoint.x(), itemPoint.y() + r.top()}, item.angle, item.tag, color, fontColor, backColor);
draw_marker(painter, {itemPoint.x() + r.left(), itemPoint.y() + r.top()}, item.angle, item.tag, color, fontColor, backColor);
}
}
// Calculate screen position of item, adjusted for zoom factor.
ui::Point GeoMap::item_rect_pixel(GeoMarker& item) {
const auto r = screen_rect();
const auto geomap_rect_half_width = r.width() / 2;
const auto geomap_rect_half_height = r.height() / 2;
GeoPoint mapPoint = lat_lon_to_map_pixel(item.lat, item.lon);
float x = mapPoint.x - x_pos;
float y = mapPoint.y - y_pos;
if (map_zoom > 1) {
x = x * map_zoom + zoom_pixel_offset;
y = y * map_zoom + zoom_pixel_offset;
} else if (map_zoom < 0) {
x = x / (-map_zoom);
y = y / (-map_zoom);
if (!use_osm) {
const auto r = screen_rect();
const auto geomap_rect_half_width = r.width() / 2;
const auto geomap_rect_half_height = r.height() / 2;
GeoPoint mapPoint = lat_lon_to_map_pixel(item.lat, item.lon);
float x = mapPoint.x - x_pos;
float y = mapPoint.y - y_pos;
if (map_zoom > 1) {
x = x * map_zoom + zoom_pixel_offset;
y = y * map_zoom + zoom_pixel_offset;
} else if (map_zoom < 0) {
x = x / (-map_zoom);
y = y / (-map_zoom);
}
x += geomap_rect_half_width;
y += geomap_rect_half_height;
return {(int16_t)x, (int16_t)y};
}
x += geomap_rect_half_width;
y += geomap_rect_half_height;
// osm calculation
double y = lat_to_pixel_y_tile(item.lat, map_osm_zoom) - viewport_top_left_py;
double x = lon_to_pixel_x_tile(item.lon, map_osm_zoom) - viewport_top_left_px;
return {(int16_t)x, (int16_t)y};
}
// Converts latitude/longitude to pixel coordinates in map file.
/**
* @brief Converts longitude to a map tile's X-coordinate.
* @param lon The longitude in degrees.
* @param zoom The zoom level.
* @return The X-coordinate of the tile.
*/
int GeoMap::lon2tile(double lon, int zoom) {
return (int)floor((lon + 180.0) / 360.0 * pow(2.0, zoom));
}
/**
* @brief Converts latitude to a map tile's Y-coordinate.
* @param lat The latitude in degrees.
* @param zoom The zoom level.
* @return The Y-coordinate of the tile.
*/
int GeoMap::lat2tile(double lat, int zoom) {
// Convert latitude from degrees to radians for trigonometric functions
double lat_rad = lat * M_PI / 180.0;
// Perform the Mercator projection calculation
return (int)floor((1.0 - log(tan(lat_rad) + 1.0 / cos(lat_rad)) / M_PI) / 2.0 * pow(2.0, zoom));
}
void GeoMap::set_osm_max_zoom() {
if (map_osm_zoom > 20) map_osm_zoom = 20;
for (uint8_t i = map_osm_zoom; i > 0; i--) {
int tile_x = lon2tile(lon_, i);
int tile_y = lat2tile(lat_, i);
std::string filename = "/OSM/" + to_string_dec_int(i) + "/" + to_string_dec_int(tile_x) + "/" + to_string_dec_int(tile_y) + ".bmp";
std::filesystem::path file_path(filename);
if (file_exists(file_path)) {
map_osm_zoom = i;
return;
}
}
map_osm_zoom = 0; // should not happen
}
// checks if the tile file presents or not. to determine if we got osm or not
uint8_t GeoMap::find_osm_file_tile() {
std::string filename = "/OSM/" + to_string_dec_int(0) + "/" + to_string_dec_int(0) + "/" + to_string_dec_int(0) + ".bmp";
std::filesystem::path file_path(filename);
if (file_exists(file_path)) return 1;
return 0; // not found
}
// Converts latitude/longitude to pixel coordinates in binary map file.
// (Note that when map_zoom==1, one pixel in map file corresponds to 1 pixel on screen)
GeoPoint GeoMap::lat_lon_to_map_pixel(float lat, float lon) {
// Using WGS 84/Pseudo-Mercator projection
@@ -310,9 +362,7 @@ GeoPoint GeoMap::lat_lon_to_map_pixel(float lat, float lon) {
}
// Draw grid in place of map (when zoom-in level is too high).
void GeoMap::draw_map_grid() {
const auto r = screen_rect();
void GeoMap::draw_map_grid(ui::Rect r) {
// Grid spacing is just based on zoom at the moment, and centered on screen.
// TODO: Maybe align with latitude/longitude seconds instead?
int grid_spacing = map_zoom * 2;
@@ -332,60 +382,184 @@ void GeoMap::draw_map_grid() {
}
}
double GeoMap::tile_pixel_x_to_lon(int x, int zoom) {
double map_width = pow(2.0, zoom) * TILE_SIZE;
return (x / map_width * 360.0) - 180.0;
}
double GeoMap::tile_pixel_y_to_lat(int y, int zoom) {
double map_height = pow(2.0, zoom) * TILE_SIZE;
double n = M_PI * (1.0 - 2.0 * y / map_height);
return atan(sinh(n)) * 180.0 / M_PI;
}
double GeoMap::lon_to_pixel_x_tile(double lon, int zoom) {
return ((lon + 180.0) / 360.0) * pow(2.0, zoom) * TILE_SIZE;
}
double GeoMap::lat_to_pixel_y_tile(double lat, int zoom) {
double lat_rad = lat * M_PI / 180.0;
double sin_lat = sin(lat_rad);
return ((1.0 - log((1.0 + sin_lat) / (1.0 - sin_lat)) / (2.0 * M_PI)) / 2.0) * pow(2.0, zoom) * TILE_SIZE;
}
bool GeoMap::draw_osm_file(int zoom, int tile_x, int tile_y, int relative_x, int relative_y) {
const auto r = screen_rect();
// Early exit if the tile is completely outside the viewport
if (relative_x >= r.width() || relative_y >= r.height() ||
relative_x + TILE_SIZE <= 0 || relative_y + TILE_SIZE <= 0) {
return true;
}
BMPFile bmp{};
bmp.open("/OSM/" + to_string_dec_int(zoom) + "/" + to_string_dec_int(tile_x) + "/" + to_string_dec_int(tile_y) + ".bmp", true);
// 1. Define the source and destination areas, starting with the full tile.
int src_x = 0;
int src_y = 0;
int dest_x = relative_x;
int dest_y = relative_y;
int clip_w = TILE_SIZE;
int clip_h = TILE_SIZE;
// 2. Clip left edge
if (dest_x < 0) {
src_x = -dest_x;
clip_w += dest_x;
dest_x = 0;
}
// 3. Clip top edge
if (dest_y < 0) {
src_y = -dest_y;
clip_h += dest_y;
dest_y = 0;
}
// 4. Clip right edge
if (dest_x + clip_w > r.width()) {
clip_w = r.width() - dest_x;
}
// 5. Clip bottom edge
if (dest_y + clip_h > r.height()) {
clip_h = r.height() - dest_y;
}
// 6. If clipping resulted in no visible area, we're done.
if (clip_w <= 0 || clip_h <= 0) {
return true;
}
if (!bmp.is_loaded()) {
// Draw an error rectangle using the calculated clipped dimensions
ui::Rect error_rect{{dest_x + r.left(), dest_y + r.top()}, {clip_w, clip_h}};
display.fill_rectangle(error_rect, Theme::getInstance()->bg_darkest->background);
return false;
}
std::vector<ui::Color> line(clip_w);
for (int y = 0; y < clip_h; ++y) {
int source_row = src_y + y;
int dest_row = dest_y + y;
bmp.seek(src_x, source_row);
for (int x = 0; x < clip_w; ++x) {
bmp.read_next_px(line[x], true);
}
display.draw_pixels({dest_x + r.left(), dest_row + r.top(), clip_w, 1}, line);
}
return true;
}
void GeoMap::paint(Painter& painter) {
const auto r = screen_rect();
std::array<ui::Color, geomap_rect_width> map_line_buffer;
int16_t zoom_seek_x, zoom_seek_y;
// Ony redraw map if it moved by at least 1 pixel or the markers list was updated
if (map_zoom <= 1) {
// Zooming out, or no zoom
const int min_diff = abs(map_zoom);
if ((int)abs(x_pos - prev_x_pos) >= min_diff)
redraw_map = true;
else if ((int)abs(y_pos - prev_y_pos) >= min_diff)
redraw_map = true;
if (!use_osm) {
// Ony redraw map if it moved by at least 1 pixel or the markers list was updated
if (map_zoom <= 1) {
// Zooming out, or no zoom
const int min_diff = abs(map_zoom);
if ((int)abs(x_pos - prev_x_pos) >= min_diff)
redraw_map = true;
else if ((int)abs(y_pos - prev_y_pos) >= min_diff)
redraw_map = true;
} else {
// Zooming in; magnify position differences (utilizing zoom_pixel_offset)
if ((int)(abs(x_pos - prev_x_pos) * map_zoom) >= 1)
redraw_map = true;
else if ((int)(abs(y_pos - prev_y_pos) * map_zoom) >= 1)
redraw_map = true;
}
} else {
// Zooming in; magnify position differences (utilizing zoom_pixel_offset)
if ((int)(abs(x_pos - prev_x_pos) * map_zoom) >= 1)
redraw_map = true;
else if ((int)(abs(y_pos - prev_y_pos) * map_zoom) >= 1)
redraw_map = true;
// using osm; needs to be stricter with the redraws, it'll be checked on move
}
if (redraw_map) {
prev_x_pos = x_pos; // Note x_pos/y_pos pixel position in map file now correspond to screen rect CENTER pixel
prev_y_pos = y_pos;
redraw_map = false;
// Adjust starting corner position of map per zoom setting;
// When zooming in the map should technically by shifted left & up by another map_zoom/2 pixels but
// the map_read_line() function doesn't handle that yet so we're adjusting markers instead (see zoom_pixel_offset).
if (map_zoom > 1) {
zoom_seek_x = x_pos - (float)r.width() / (2 * map_zoom);
zoom_seek_y = y_pos - (float)r.height() / (2 * map_zoom);
} else {
zoom_seek_x = x_pos - (r.width() * abs(map_zoom)) / 2;
zoom_seek_y = y_pos - (r.height() * abs(map_zoom)) / 2;
}
if (map_visible) {
// Read from map file and display to zoomed scale
int duplicate_lines = (map_zoom < 0) ? 1 : map_zoom;
for (uint16_t line = 0; line < (r.height() / duplicate_lines); line++) {
uint16_t seek_line = zoom_seek_y + ((map_zoom >= 0) ? line : line * (-map_zoom));
map_file.seek(4 + ((zoom_seek_x + (map_width * seek_line)) << 1));
map_read_line(map_line_buffer.data(), r.width());
if (!use_osm) {
prev_x_pos = x_pos; // Note x_pos/y_pos pixel position in map file now correspond to screen rect CENTER pixel
prev_y_pos = y_pos;
// Adjust starting corner position of map per zoom setting;
// When zooming in the map should technically by shifted left & up by another map_zoom/2 pixels but
// the map_read_line_bin() function doesn't handle that yet so we're adjusting markers instead (see zoom_pixel_offset).
if (map_zoom > 1) {
zoom_seek_x = x_pos - (float)r.width() / (2 * map_zoom);
zoom_seek_y = y_pos - (float)r.height() / (2 * map_zoom);
} else {
zoom_seek_x = x_pos - (r.width() * abs(map_zoom)) / 2;
zoom_seek_y = y_pos - (r.height() * abs(map_zoom)) / 2;
}
// Read from map file and display to zoomed scale
int duplicate_lines = (map_zoom < 0) ? 1 : map_zoom;
for (uint16_t line = 0; line < (r.height() / duplicate_lines); line++) {
uint16_t seek_line = zoom_seek_y + ((map_zoom >= 0) ? line : line * (-map_zoom));
map_file.seek(4 + ((zoom_seek_x + (map_width * seek_line)) << 1));
map_read_line_bin(map_line_buffer.data(), r.width());
for (uint16_t j = 0; j < duplicate_lines; j++) {
display.draw_pixels({0, r.top() + (line * duplicate_lines) + j, r.width(), 1}, map_line_buffer);
}
}
for (uint16_t j = 0; j < duplicate_lines; j++) {
display.draw_pixels({0, r.top() + (line * duplicate_lines) + j, r.width(), 1}, map_line_buffer);
} else {
// display osm tiles
// Convert center GPS to a global pixel coordinate
double global_center_px = lon_to_pixel_x_tile(lon_, map_osm_zoom);
double global_center_py = lat_to_pixel_y_tile(lat_, map_osm_zoom);
// Find the top-left corner of the screen (viewport) in global pixel coordinates
viewport_top_left_px = global_center_px - (r.width() / 2.0);
viewport_top_left_py = global_center_py - (r.height() / 2.0);
// Find the tile ID that contains the top-left corner of the viewport
int start_tile_x = floor(viewport_top_left_px / TILE_SIZE);
int start_tile_y = floor(viewport_top_left_py / TILE_SIZE);
// Calculate the crucial render offset.
// This determines how much the first tile is shifted to align the map correctly.
// This value will almost always be negative or zero.
double render_offset_x = -(viewport_top_left_px - (start_tile_x * TILE_SIZE));
double render_offset_y = -(viewport_top_left_py - (start_tile_y * TILE_SIZE));
// Determine how many tiles we need to draw to fill the screen
int tiles_needed_x = (r.width() / TILE_SIZE) + 2;
int tiles_needed_y = (r.height() / TILE_SIZE) + 2;
for (int y = 0; y < tiles_needed_y; ++y) {
for (int x = 0; x < tiles_needed_x; ++x) {
int current_tile_x = start_tile_x + x;
int current_tile_y = start_tile_y + y;
// Calculate the final on-screen drawing position for this tile.
// For the first tile (x=0, y=0), this will be the negative offset.
int draw_pos_x = round(render_offset_x + x * TILE_SIZE);
int draw_pos_y = round(render_offset_y + y * TILE_SIZE);
if (!draw_osm_file(map_osm_zoom, current_tile_x, current_tile_y, draw_pos_x, draw_pos_y)) {
// already blanked it.
}
}
}
}
} else {
// No map data or excessive zoom; just draw a grid
draw_map_grid();
draw_map_grid(r);
}
// Draw crosshairs in center in manual panning mode
if (manual_panning_) {
display.fill_rectangle({r.center() - Point(16, 1) + Point(zoom_pixel_offset, zoom_pixel_offset), {32, 2}}, Color::red());
@@ -394,8 +568,9 @@ void GeoMap::paint(Painter& painter) {
// Draw the other markers
draw_markers(painter);
draw_scale(painter);
if (!use_osm) draw_scale(painter);
draw_mypos(painter);
if (has_osm) draw_switcher(painter);
set_clean();
}
@@ -405,6 +580,12 @@ void GeoMap::paint(Painter& painter) {
}
}
void GeoMap::draw_switcher(Painter& painter) {
painter.fill_rectangle({screen_rect().left(), screen_rect().top(), 3 * 20, 20}, Theme::getInstance()->bg_darker->background);
std::string_view txt = (use_osm) ? "B I N" : "O S M";
painter.draw_string({screen_rect().left() + 5, screen_rect().top() + 2}, *Theme::getInstance()->fg_light, txt);
}
bool GeoMap::on_keyboard(KeyboardEvent key) {
if (key == '+' || key == ' ') return on_encoder(1);
if (key == '-') return on_encoder(-1);
@@ -413,11 +594,26 @@ bool GeoMap::on_keyboard(KeyboardEvent key) {
}
bool GeoMap::on_touch(const TouchEvent event) {
if (has_osm && event.type == TouchEvent::Type::Start && event.point.x() < screen_rect().left() + 3 * 20 && event.point.y() < screen_rect().top() + 20) {
use_osm = !use_osm;
move(lon_, lat_); // to re calculate the center for each map type
if (use_osm) set_osm_max_zoom();
redraw_map = true;
set_dirty();
return false; // false, because with true this hits 2 times
}
if ((event.type == TouchEvent::Type::Start) && (mode_ == PROMPT)) {
Point p;
set_highlighted(true);
if (on_move) {
Point p = event.point - screen_rect().center();
on_move(p.x() / 2.0 * lon_ratio, p.y() / 2.0 * lat_ratio);
if (!use_osm) {
p = event.point - screen_rect().center();
on_move(p.x() / 2.0 * lon_ratio, p.y() / 2.0 * lat_ratio, false);
} else {
p = event.point - screen_rect().location();
on_move(tile_pixel_x_to_lon(p.x() + viewport_top_left_px, map_osm_zoom), tile_pixel_y_to_lat(p.y() + viewport_top_left_py, map_osm_zoom), true);
}
return true;
}
}
@@ -426,25 +622,30 @@ bool GeoMap::on_touch(const TouchEvent event) {
void GeoMap::move(const float lon, const float lat) {
const auto r = screen_rect();
bool is_changed = (lon_ != lon || lat_ != lat);
lon_ = lon;
lat_ = lat;
if (!use_osm) {
// Calculate x_pos/y_pos in map file corresponding to CENTER pixel of screen rect
// (Note there is a 1:1 correspondence between map file pixels and screen pixels when map_zoom=1)
GeoPoint mapPoint = lat_lon_to_map_pixel(lat_, lon_);
x_pos = mapPoint.x;
y_pos = mapPoint.y;
// Cap position
if (x_pos > (map_width - r.width() / 2))
x_pos = map_width - r.width() / 2;
if (y_pos > (map_height + r.height() / 2))
y_pos = map_height - r.height() / 2;
// Calculate x_pos/y_pos in map file corresponding to CENTER pixel of screen rect
// (Note there is a 1:1 correspondence between map file pixels and screen pixels when map_zoom=1)
GeoPoint mapPoint = lat_lon_to_map_pixel(lat_, lon_);
x_pos = mapPoint.x;
y_pos = mapPoint.y;
// Cap position
if (x_pos > (map_width - r.width() / 2))
x_pos = map_width - r.width() / 2;
if (y_pos > (map_height + r.height() / 2))
y_pos = map_height - r.height() / 2;
// Scale calculation
float km_per_deg_lon = cos(lat * pi / 180) * 111.321; // 111.321 km/deg longitude at equator, and 0 km at poles
pixels_per_km = (r.width() / 2) / km_per_deg_lon;
// Scale calculation
float km_per_deg_lon = cos(lat * pi / 180) * 111.321; // 111.321 km/deg longitude at equator, and 0 km at poles
pixels_per_km = (r.width() / 2) / km_per_deg_lon;
} else {
if (is_changed) {
set_osm_max_zoom();
redraw_map = true;
}
}
}
bool GeoMap::init() {
@@ -459,7 +660,7 @@ bool GeoMap::init() {
map_height = 32768;
}
map_visible = map_opened;
map_visible = map_opened || has_osm;
map_center_x = map_width >> 1;
map_center_y = map_height >> 1;
@@ -469,7 +670,6 @@ bool GeoMap::init() {
map_bottom = sin(-85.05 * pi / 180); // Map bitmap only goes from about -85 to 85 lat
map_world_lon = map_width / (2 * pi);
map_offset = (map_world_lon / 2 * log((1 + map_bottom) / (1 - map_bottom)));
return map_opened;
}
@@ -573,15 +773,22 @@ void GeoMap::clear_markers() {
MapMarkerStored GeoMap::store_marker(GeoMarker& marker) {
const auto r = screen_rect();
MapMarkerStored ret;
// Check if it could be on screen
// (Shows more distant planes when zoomed out)
GeoPoint mapPoint = lat_lon_to_map_pixel(marker.lat, marker.lon);
int x_dist = abs((int)mapPoint.x - (int)x_pos);
int y_dist = abs((int)mapPoint.y - (int)y_pos);
int zoom_out = (map_zoom < 0) ? -map_zoom : 1;
bool can_see_on_map = true;
if (!use_osm) {
GeoPoint mapPoint = lat_lon_to_map_pixel(marker.lat, marker.lon);
int x_dist = abs((int)mapPoint.x - (int)x_pos);
int y_dist = abs((int)mapPoint.y - (int)y_pos);
int zoom_out = (map_zoom < 0) ? -map_zoom : 1;
if ((x_dist >= (zoom_out * r.width() / 2)) || (y_dist >= (zoom_out * r.height() / 2))) {
can_see_on_map = false;
}
} else {
// osm is so zoomable, we try to store all possible. maybe need to refine it a bit more later
}
if ((x_dist >= (zoom_out * r.width() / 2)) || (y_dist >= (zoom_out * r.height() / 2))) {
if (!can_see_on_map) {
ret = MARKER_NOT_STORED;
} else if (markerListLen < NumMarkerListElements) {
markerList[markerListLen] = marker;
@@ -595,24 +802,29 @@ MapMarkerStored GeoMap::store_marker(GeoMarker& marker) {
}
void GeoMap::update_my_position(float lat, float lon, int32_t altitude) {
bool is_changed = (my_pos.lat != lat) || (my_pos.lon != lon);
my_pos.lat = lat;
my_pos.lon = lon;
my_altitude = altitude;
redraw_map = true;
redraw_map = is_changed;
set_dirty();
}
void GeoMap::update_my_orientation(uint16_t angle, bool refresh) {
bool is_changed = (my_pos.angle != angle);
my_pos.angle = angle;
if (refresh) {
if (refresh && is_changed) {
redraw_map = true;
set_dirty();
}
}
MapType GeoMap::get_map_type() {
return use_osm ? MAP_TYPE_OSM : MAP_TYPE_BIN;
}
void GeoMapView::focus() {
geopos.focus();
if (!geomap.map_file_opened())
nav_.display_modal("No map", "No world_map.bin file in\n/" + adsb_dir.string() + "/ directory", ABORT);
}
@@ -629,7 +841,7 @@ void GeoMapView::update_position(float lat, float lon, uint16_t angle, int32_t a
geomap.set_dirty();
return;
}
bool is_changed = lat_ != lat || lon_ != lon || altitude_ != altitude || speed_ != speed || angle_ != angle;
lat_ = lat;
lon_ = lon;
altitude_ = altitude;
@@ -644,7 +856,7 @@ void GeoMapView::update_position(float lat, float lon, uint16_t angle, int32_t a
geopos.set_report_change(true);
geomap.set_angle(angle);
geomap.move(lon_, lat_);
if (is_changed) geomap.move(lon_, lat_);
geomap.set_dirty();
}
@@ -660,19 +872,25 @@ void GeoMapView::setup() {
geopos.set_lon(lon_);
geopos.on_change = [this](int32_t altitude, float lat, float lon, int32_t speed) {
bool is_changed = (altitude_ != altitude) || (lat_ != lat) || (lon_ != lon) || (speed_ != speed);
altitude_ = altitude;
lat_ = lat;
lon_ = lon;
speed_ = speed;
geopos.hide_altandspeed();
geomap.set_manual_panning(true);
geomap.move(lon_, lat_);
if (is_changed) geomap.move(lon_, lat_);
geomap.set_dirty();
};
geomap.on_move = [this](float move_x, float move_y) {
lon_ += move_x;
lat_ += move_y;
geomap.on_move = [this](float move_x, float move_y, bool absolute) {
if (absolute) {
lon_ = move_x;
lat_ = move_y;
} else {
lon_ += move_x;
lat_ += move_y;
}
// Stupid hack to avoid an event loop
geopos.set_report_change(false);
@@ -769,4 +987,8 @@ MapMarkerStored GeoMapView::store_marker(GeoMarker& marker) {
return geomap.store_marker(marker);
}
MapType GeoMapView::get_map_type() {
return geomap.get_map_type();
}
} /* namespace ui */
+33 -5
View File
@@ -27,6 +27,8 @@
#include "ui.hpp"
#include "file.hpp"
#include "ui_navigation.hpp"
#include "bmpfile.hpp"
#include "mathdef.hpp"
#include "portapack.hpp"
@@ -43,6 +45,8 @@ namespace ui {
#define GEOMAP_RECT_WIDTH 240
#define GEOMAP_RECT_HEIGHT (320 - 16 - GEOMAP_BANNER_HEIGHT)
#define TILE_SIZE 256
enum GeoMapMode {
DISPLAY,
PROMPT
@@ -193,9 +197,14 @@ enum MapMarkerStored {
MARKER_LIST_FULL
};
enum MapType {
MAP_TYPE_OSM,
MAP_TYPE_BIN
};
class GeoMap : public Widget {
public:
std::function<void(float, float)> on_move{};
std::function<void(float, float, bool)> on_move{};
GeoMap(Rect parent_rect);
@@ -216,6 +225,7 @@ class GeoMap : public Widget {
void set_tag(std::string new_tag) {
tag_ = new_tag;
}
MapType get_map_type();
void set_angle(uint16_t new_angle) {
angle_ = new_angle;
@@ -246,8 +256,22 @@ class GeoMap : public Widget {
void draw_markers(Painter& painter);
void draw_mypos(Painter& painter);
void draw_bearing(const Point origin, const uint16_t angle, uint32_t size, const Color color);
void draw_map_grid();
void map_read_line(ui::Color* buffer, uint16_t pixels);
void draw_map_grid(ui::Rect r);
void draw_switcher(Painter& painter);
void map_read_line_bin(ui::Color* buffer, uint16_t pixels);
// open street map related
uint8_t find_osm_file_tile();
void set_osm_max_zoom();
bool draw_osm_file(int zoom, int tile_x, int tile_y, int relative_x, int relative_y);
int lon2tile(double lon, int zoom);
int lat2tile(double lat, int zoom);
double lon_to_pixel_x_tile(double lon, int zoom);
double lat_to_pixel_y_tile(double lat, int zoom);
double tile_pixel_x_to_lon(int x, int zoom);
double tile_pixel_y_to_lat(int y, int zoom);
uint8_t map_osm_zoom{3};
double viewport_top_left_px = 0;
double viewport_top_left_py = 0;
bool manual_panning_{false};
bool hide_center_marker_{false};
@@ -258,11 +282,11 @@ class GeoMap : public Widget {
uint16_t map_width{}, map_height{};
int32_t map_center_x{}, map_center_y{};
int16_t map_zoom{1};
float lon_ratio{}, lat_ratio{};
double map_bottom{};
double map_world_lon{};
double map_offset{};
float x_pos{}, y_pos{};
float prev_x_pos{32767.0f}, prev_y_pos{32767.0f};
float lat_{};
@@ -278,7 +302,9 @@ class GeoMap : public Widget {
int markerListLen{0};
GeoMarker markerList[NumMarkerListElements];
bool redraw_map{false};
bool redraw_map{true};
bool use_osm{false};
bool has_osm{false};
};
class GeoMapView : public View {
@@ -313,6 +339,8 @@ class GeoMapView : public View {
void update_my_position(float lat, float lon, int32_t altitude);
void update_my_orientation(uint16_t angle, bool refresh = false);
MapType get_map_type();
std::string title() const override { return "Map view"; };
void clear_markers();
+72 -10
View File
@@ -275,29 +275,63 @@ FrequencyKeypadView::FrequencyKeypadView(
};
button.on_select = button_fn;
button.set_parent_rect({(n % 3) * button_w,
(n / 3) * button_h + 24,
(n / 3) * button_h + 25,
button_w, button_h});
button.set_text(label);
n++;
}
add_children({&button_save,
add_children({&button_save_ghz,
&button_save_khz,
&button_save_mhz,
&button_load,
&button_close});
&button_clear,
&button_done_ghz,
&button_done_mhz,
&button_done_khz});
button_save.on_select = [this, &nav](Button&) {
nav.push<FrequencySaveView>(this->value());
button_save_mhz.on_select = [this, &nav](Button&) {
nav.push<FrequencySaveView>(this->value(FrequencyUnit::MHZ)); // pass as mhz and handle unit convert logic there, cuz idk if pass others would loss something
};
button_save_ghz.on_select = [this, &nav](Button&) {
nav.push<FrequencySaveView>(this->value(FrequencyUnit::GHZ));
};
button_save_khz.on_select = [this, &nav](Button&) {
nav.push<FrequencySaveView>(this->value(FrequencyUnit::KHZ));
};
button_load.on_select = [this, &nav](Button&) {
auto load_view = nav.push<FrequencyLoadView>();
load_view->on_frequency_loaded = [this](rf::Frequency value) {
set_value(value);
};
};
button_clear.on_select = [this](Button&) {
mhz.clear();
submhz.clear();
clear_field_if_digits_entered = true;
if (state == State::DigitSubMHz) {
state = State::DigitMHz;
}
draw_input_hint();
update_text();
};
button_close.on_select = [this, &nav](Button&) {
button_done_ghz.on_select = [this, &nav](Button&) {
if (on_changed)
on_changed(this->value());
on_changed(this->value(FrequencyUnit::GHZ));
nav.pop();
};
button_done_mhz.on_select = [this, &nav](Button&) {
if (on_changed)
on_changed(this->value(FrequencyUnit::MHZ));
nav.pop();
};
button_done_khz.on_select = [this, &nav](Button&) {
if (on_changed)
on_changed(this->value(FrequencyUnit::KHZ));
nav.pop();
};
@@ -305,11 +339,20 @@ FrequencyKeypadView::FrequencyKeypadView(
}
void FrequencyKeypadView::focus() {
button_close.focus();
button_done_mhz.focus();
}
rf::Frequency FrequencyKeypadView::value() const {
return mhz.as_int() * 1000000ULL + submhz.as_int() * submhz_base;
rf::Frequency FrequencyKeypadView::value(FrequencyUnit frequency_uni) const {
switch (frequency_uni) {
case FrequencyUnit::GHZ:
return mhz.as_int() * 1000000000ULL + submhz.as_int() * submhz_base * 1000;
case FrequencyUnit::MHZ:
return mhz.as_int() * 1000000ULL + submhz.as_int() * submhz_base;
case FrequencyUnit::KHZ:
return mhz.as_int() * 1000ULL + submhz.as_int() * submhz_base / 1000;
default:
return mhz.as_int() * 1000000ULL + submhz.as_int() * submhz_base;
}
}
void FrequencyKeypadView::set_value(const rf::Frequency new_value) {
@@ -381,6 +424,25 @@ void FrequencyKeypadView::field_toggle() {
state = State::DigitMHz;
clear_field_if_digits_entered = true;
}
draw_input_hint();
}
void FrequencyKeypadView::draw_input_hint() {
set_dirty();
}
void FrequencyKeypadView::paint(Painter& painter) {
View::paint(painter);
const bool s = state == State::DigitMHz;
painter.draw_hline(
{0, 36},
8 * 4,
s ? Color::white() : Color::black());
painter.draw_hline(
{5 * 8, 36},
8 * 4,
s ? Color::black() : Color::white());
}
void FrequencyKeypadView::update_text() {
+32 -9
View File
@@ -44,6 +44,12 @@ extern options_db_t freqman_steps;
namespace ui {
enum FrequencyUnit {
GHZ = 0,
MHZ,
KHZ,
};
class FrequencyField : public Widget {
public:
std::function<void(rf::Frequency)> on_change{};
@@ -207,8 +213,9 @@ class FrequencyKeypadView : public View {
const rf::Frequency value);
void focus() override;
void paint(Painter& painter) override;
rf::Frequency value() const;
rf::Frequency value(FrequencyUnit frequency_uni) const;
void set_value(const rf::Frequency new_value);
bool on_encoder(const EncoderEvent delta) override;
bool on_keyboard(const KeyboardEvent key) override;
@@ -220,7 +227,6 @@ class FrequencyKeypadView : public View {
static constexpr int mhz_digits = 4;
static constexpr int submhz_digits = 4;
static constexpr int mhz_mod = pow(10, mhz_digits);
static constexpr int submhz_base = pow(10, 6 - submhz_digits);
static constexpr int text_digits = mhz_digits + 1 + submhz_digits;
@@ -230,15 +236,30 @@ class FrequencyKeypadView : public View {
std::array<Button, 12> buttons{};
Button button_save{
{0, button_h * 5, 60, button_h},
"Save"};
Button button_save_ghz{
{0, 14 * 16, 80 / 2 - 1, 2 * 16},
"GHz"};
Button button_save_khz{
{0 + 40 - 1, 14 * 16, 80 / 2, 2 * 16},
"kHz"};
Button button_save_mhz{
{0, 16 * 16, 80 - 1, 3 * 16},
"Save MHz"};
Button button_load{
{60, button_h * 5, 60, button_h},
{80 + 1, 14 * 16, 80 - 2, 40},
"Load"};
Button button_close{
{128, button_h * 5, 112, button_h},
"Done"};
Button button_clear{
{80 + 1, 264, 80 - 2, 40},
"Clear"};
Button button_done_ghz{
{160 + 1, 14 * 16, 80 / 2 - 1, 2 * 16},
"GHz"};
Button button_done_khz{
{160 + 40, 14 * 16, 80 / 2 - 1, 2 * 16},
"kHz"};
Button button_done_mhz{
{160 + 1, 16 * 16, 80 - 2, 3 * 16},
"Done MHz"};
/* TODO: Template arg required in enum?! */
FieldString<mhz_digits> mhz{FieldString<4>::Justify::Right};
@@ -254,6 +275,8 @@ class FrequencyKeypadView : public View {
void on_button(Button& button);
void draw_input_hint();
void digit_add(const char c);
void digit_delete();
+1 -1
View File
@@ -1,6 +1,6 @@
/*
* Copyright (C) 2015 Jared Boone, ShareBrained Technology, Inc.
* Copyleft Mr. Robot 2025
* copyleft 2025 zxkmm AKA zix aka sommermorgentraum
*
* This file is part of PortaPack.
*
+1 -1
View File
@@ -1,6 +1,6 @@
/*
* Copyright (C) 2015 Jared Boone, ShareBrained Technology, Inc.
* Copyleft Mr. Robot 2025
* copyleft 2025 zxkmm AKA zix aka sommermorgentraum
*
* This file is part of PortaPack.
*
@@ -347,7 +347,7 @@ namespace ui {
return false;
// TODO: move this to m4 memory space
auto app_image = reinterpret_cast<uint8_t*>(portapack::memory::map::m4_code.end() - app.size());
auto app_image = reinterpret_cast<uint8_t*>(portapack::memory::map::local_sram_0.base());
// read file in 512 byte chunks
for (size_t file_read_index = 0; file_read_index < app.size(); file_read_index += std::filesystem::max_file_block_size) {
+5 -9
View File
@@ -2,7 +2,7 @@
* Copyright (C) 2015 Jared Boone, ShareBrained Technology, Inc.
* Copyright (C) 2016 Furrtek
* Copyright (C) 2024 u-foka
* Copyleft (ɔ) 2024 zxkmm under GPL license
* copyleft 2024 zxkmm AKA zix aka sommermorgentraum
*
* This file is part of PortaPack.
*
@@ -70,9 +70,7 @@
#include "ble_rx_app.hpp"
#include "ble_tx_app.hpp"
#include "capture_app.hpp"
#include "ert_app.hpp"
#include "pocsag_app.hpp"
#include "soundboard_app.hpp"
#include "core_control.hpp"
#include "file.hpp"
@@ -131,7 +129,6 @@ const NavigationView::AppList NavigationView::appList = {
{"aprsrx", "APRS", RX, Color::green(), &bitmap_icon_aprs, new ViewFactory<APRSRXView>()},
{"audio", "Audio", RX, Color::green(), &bitmap_icon_speaker, new ViewFactory<AnalogAudioView>()},
{"blerx", "BLE Rx", RX, Color::green(), &bitmap_icon_btle, new ViewFactory<BLERxView>()},
{"ert", "ERT Meter", RX, Color::green(), &bitmap_icon_ert, new ViewFactory<ERTAppView>()},
{"pocsag", "POCSAG", RX, Color::green(), &bitmap_icon_pocsag, new ViewFactory<POCSAGAppView>()},
{"radiosonde", "Radiosnde", RX, Color::green(), &bitmap_icon_sonde, new ViewFactory<SondeView>()},
{"search", "Search", RX, Color::yellow(), &bitmap_icon_search, new ViewFactory<SearchView>()},
@@ -144,7 +141,6 @@ const NavigationView::AppList NavigationView::appList = {
{"ooktx", "OOK", TX, ui::Color::yellow(), &bitmap_icon_remote, new ViewFactory<EncodersView>()},
{"pocsagtx", "POCSAG TX", TX, ui::Color::green(), &bitmap_icon_pocsag, new ViewFactory<POCSAGTXView>()},
{"rdstx", "RDS", TX, ui::Color::green(), &bitmap_icon_rds, new ViewFactory<RDSView>()},
{"soundbrd", "Soundbrd", TX, ui::Color::green(), &bitmap_icon_soundboard, new ViewFactory<SoundBoardView>()},
{"touchtune", "TouchTune", TX, ui::Color::green(), &bitmap_icon_touchtunes, new ViewFactory<TouchTunesView>()},
{"signalgen", "SignalGen", TX, Color::green(), &bitmap_icon_cwgen, new ViewFactory<SigGenView>()},
/* TRX ********************************************************************/
@@ -764,7 +760,7 @@ void add_apps(NavigationView& nav, BtnGridView& grid, app_location_t loc) {
for (auto& app : NavigationView::appList) {
if (app.menuLocation == loc) {
grid.add_item({app.displayName, app.iconColor, app.icon,
[&nav, &app]() {
[&nav, &app]() {
i2cdev::I2CDevManager::set_autoscan_interval(0); //if i navigate away from any menu, turn off autoscan
nav.push_view(std::unique_ptr<View>(app.viewFactory->produce(nav))); }},
true);
@@ -791,8 +787,8 @@ void add_external_items(NavigationView& nav, app_location_t location, BtnGridVie
error_tile_pos);
} else {
std::sort(externalItems.begin(), externalItems.end(), [](const auto &a, const auto &b)
{
return a.desired_position < b.desired_position;
{
return a.desired_position < b.desired_position;
});
for (auto const& gridItem : externalItems) {
@@ -801,7 +797,7 @@ void add_external_items(NavigationView& nav, app_location_t location, BtnGridVie
} else {
grid.insert_item(gridItem, gridItem.desired_position, true);
}
}
grid.update_items();
+1 -1
View File
@@ -2,7 +2,7 @@
* Copyright (C) 2015 Jared Boone, ShareBrained Technology, Inc.
* Copyright (C) 2016 Furrtek
* Copyright (C) 2024 u-foka
* Copyleft (ɔ) 2024 zxkmm under GPL license
* copyleft 2024 zxkmm AKA zix aka sommermorgentraum
*
* This file is part of PortaPack.
*
+1 -3
View File
@@ -1,7 +1,5 @@
/*
* Copyright (C) 2015 Jared Boone, ShareBrained Technology, Inc.
* Copyright (C) 2017 Furrtek
* Copyleft (ɔ) 2024 zxkmm with the GPL license
* copyleft 2024 zxkmm AKA zix aka sommermorgentraum
* Copyright (C) 2024 HTotoo
*
* This file is part of PortaPack.
+1 -3
View File
@@ -1,7 +1,5 @@
/*
* Copyright (C) 2015 Jared Boone, ShareBrained Technology, Inc.
* Copyright (C) 2017 Furrtek
* Copyleft (ɔ) 2024 zxkmm with the GPL license
* copyleft 2024 zxkmm AKA zix aka sommermorgentraum
* Copyright (C) 2024 HTotoo
*
* This file is part of PortaPack.
+16 -8
View File
@@ -279,7 +279,7 @@ macro(DeclareTargets chunk_tag name)
include_directories(. ${INCDIR} ${MODE_INCDIR})
link_directories(${LLIBDIR})
target_link_libraries(${PROJECT_NAME}.elf ${LIBS})
target_link_libraries(${PROJECT_NAME}.elf -Wl,-Map=${PROJECT_NAME}.map)
target_link_libraries(${PROJECT_NAME}.elf -Wl,--print-memory-usage)
@@ -369,13 +369,6 @@ set(MODE_CPPSRC
DeclareTargets(PAMA am_audio)
### Audio transmit
set(MODE_CPPSRC
proc_audiotx.cpp
)
DeclareTargets(PATX audio_tx)
### Capture
set(MODE_CPPSRC
@@ -578,6 +571,14 @@ set(MODE_CPPSRC
)
DeclareTargets(PAFR afskrx)
### EPIRB
set(MODE_CPPSRC
proc_epirb.cpp
)
DeclareTargets(PEPI epirb_rx)
### NRF RX
set(MODE_CPPSRC
@@ -682,6 +683,13 @@ set(MODE_CPPSRC
)
DeclareTargets(PNOA noaaapt_rx)
### Audio transmit
set(MODE_CPPSRC
proc_audiotx.cpp
)
DeclareTargets(PATX audio_tx)
### HackRF "factory" firmware
+58 -40
View File
@@ -30,10 +30,10 @@
#define new_way
int BTLETxProcessor::gen_sample_from_phy_bit(char* bit, char* sample, int num_bit) {
int BTLETxProcessor::gen_sample_from_phy_bit(char* bit, int8_t* sample, int num_bit) {
int num_sample = (num_bit * SAMPLE_PER_SYMBOL) + (LEN_GAUSS_FILTER * SAMPLE_PER_SYMBOL);
int8_t* tmp_phy_bit_over_sampling_int8 = (int8_t*)tmp_phy_bit_over_sampling;
memset(tmp_phy_bit_over_sampling_int8, 0, sizeof(tmp_phy_bit_over_sampling_int8));
int i, j;
@@ -242,6 +242,7 @@ void BTLETxProcessor::fill_adv_pdu_header(PKT_INFO* pkt, int txadd, int rxadd, i
bit_out[2] = 1;
bit_out[1] = 0;
bit_out[0] = 0;
rxadd = 1; // Scan response assumed to be with random address.
} else if (pkt->pkt_type == CONNECT_REQ) {
bit_out[3] = 0;
bit_out[2] = 1;
@@ -279,14 +280,12 @@ void BTLETxProcessor::fill_adv_pdu_header(PKT_INFO* pkt, int txadd, int rxadd, i
int BTLETxProcessor::calculate_sample_for_ADV(PKT_INFO* pkt) {
pkt->num_info_bit = 0;
// gen preamble and access address
const char* AA = "AA";
const char* AAValue = "D6BE898E";
pkt->num_info_bit = pkt->num_info_bit + convert_hex_to_bit((char*)AA, pkt->info_bit, 0, 1);
pkt->num_info_bit = pkt->num_info_bit + convert_hex_to_bit((char*)AAValue, pkt->info_bit + pkt->num_info_bit, 0, 4);
pkt->num_info_bit = pkt->num_info_bit + convert_hex_to_bit(AA, pkt->info_bit, 0, 1);
pkt->num_info_bit = pkt->num_info_bit + convert_hex_to_bit(AAValue, pkt->info_bit + pkt->num_info_bit, 0, 4);
// get txadd and rxadd
int txadd = 0, rxadd = 0;
// Tx assumed to be random address.
int txadd = 1, rxadd = 0;
pkt->num_info_bit = pkt->num_info_bit + 16; // 16 is header length
@@ -332,39 +331,56 @@ int BTLETxProcessor::calculate_pkt_info(PKT_INFO* pkt) {
void BTLETxProcessor::execute(const buffer_c8_t& buffer) {
int8_t re, im;
// This is called at 4M/2048 = 1953Hz
for (size_t i = 0; i < buffer.count; i++) {
if (configured) {
// This is going to loop through each sample bit and push it to the output buffer.
if (sample_count > length) {
configured = false;
sample_count = 0;
txprogress_message.done = true;
shared_memory.application_queue.push(txprogress_message);
} else {
// Real and imaginary was already calculated in gen_sample_from_phy_bit.
// It was processed from each data bit, run through a Gaussian Filter, and then ran through sin and cos table to get each IQ bit.
re = (int8_t)packets.phy_sample[sample_count++];
im = (int8_t)packets.phy_sample[sample_count++];
buffer.p[i] = {re, im};
if (progress_count >= progress_notice) {
progress_count = 0;
txprogress_message.progress++;
txprogress_message.done = false;
shared_memory.application_queue.push(txprogress_message);
} else {
progress_count++;
}
}
} else {
re = 0;
im = 0;
buffer.p[i] = {re, im};
if (!configured) {
for (size_t i = 0; i < buffer.count; i++) {
buffer.p[i] = {0, 0}; // Fill the buffer with zeros if not configured
}
return;
}
if (configured) {
switch (txprogress_message.progress) {
case 1:
case 2: {
for (size_t i = 0; i < buffer.count; i++) {
buffer.p[i] = {0, 0}; // Pre and Post pad BLE Packet.
}
} break;
case 0: {
size_t i = 0;
for (i = 0; (i < buffer.count) && (sample_count < length); i++) {
re = packets.phy_sample[sample_count++];
im = packets.phy_sample[sample_count++];
buffer.p[i] = {re, im};
}
if (sample_count >= length && i < buffer.count) {
// Fill the rest of the buffer with zeros if we reach the end of the packet
for (; i < buffer.count; i++) {
buffer.p[i] = {0, 0};
}
}
} break;
case 3: {
for (size_t i = 0; i < buffer.count; i++) {
buffer.p[i] = {0, 0}; // Pre and Post pad BLE Packet.
}
if (sample_count >= length) {
sample_count = 0;
configured = false;
doneSending = true;
txprogress_message.done = true;
shared_memory.application_queue.push(txprogress_message);
}
} break;
}
txprogress_message.progress++;
}
}
@@ -397,11 +413,13 @@ void BTLETxProcessor::configure(const BTLETxConfigureMessage& message) {
// Starting at sample_count 0 since packets.num_phy_sample contains every sample needed to be sent out.
sample_count = 0;
progress_count = 0;
repeatCount = 0;
progress_notice = 64;
txprogress_message.progress = 0;
txprogress_message.done = false;
configured = true;
doneSending = false;
}
int main() {
+8 -10
View File
@@ -80,18 +80,11 @@ class BTLETxProcessor : public BasebandProcessor {
int num_info_bit;
char info_bit[MAX_NUM_PHY_BYTE * 8]; // without CRC and whitening
int num_info_byte;
uint8_t info_byte[MAX_NUM_PHY_BYTE];
int num_phy_bit;
char phy_bit[MAX_NUM_PHY_BYTE * 8]; // all bits which will be fed to GFSK modulator
int num_phy_byte;
uint8_t phy_byte[MAX_NUM_PHY_BYTE];
int num_phy_sample;
char phy_sample[2 * MAX_NUM_PHY_SAMPLE]; // GFSK output to D/A (hackrf board)
int8_t phy_sample1[2 * MAX_NUM_PHY_SAMPLE]; // GFSK output to D/A (hackrf board)
int8_t phy_sample[2 * MAX_NUM_PHY_SAMPLE]; // GFSK output to D/A (hackrf board)
int space; // how many millisecond null signal shouwl be padded after this packet
};
@@ -108,22 +101,27 @@ class BTLETxProcessor : public BasebandProcessor {
void crc24(char* bit_in, int num_bit, char* init_hex, char* crc_result);
int convert_hex_to_bit(char* hex, char* bit, int stream_flip, int octet_limit);
void octet_hex_to_bit(char* hex, char* bit);
int gen_sample_from_phy_bit(char* bit, char* sample, int num_bit);
int gen_sample_from_phy_bit(char* bit, int8_t* sample, int num_bit);
bool configured = false;
float tmp_phy_bit_over_sampling[MAX_NUM_PHY_SAMPLE + 2 * LEN_GAUSS_FILTER * SAMPLE_PER_SYMBOL];
int8_t tmp_phy_bit_over_sampling_int8[MAX_NUM_PHY_SAMPLE + 2 * LEN_GAUSS_FILTER * SAMPLE_PER_SYMBOL];
float gauss_coef[LEN_GAUSS_FILTER * SAMPLE_PER_SYMBOL] = {7.561773e-09, 1.197935e-06, 8.050684e-05, 2.326833e-03, 2.959908e-02, 1.727474e-01, 4.999195e-01, 8.249246e-01, 9.408018e-01, 8.249246e-01, 4.999195e-01, 1.727474e-01, 2.959908e-02, 2.326833e-03, 8.050684e-05, 1.197935e-06};
uint32_t samples_per_bit{4};
uint32_t channel_number{37};
char macAddress[13] = "FFFFFFFFFF";
char AA[3] = "AA";
char AAValue[9] = "D6BE898E";
char advertisementData[63] = {0};
uint32_t length{0};
uint32_t shift_zero{}, shift_one{};
uint32_t progress_notice{}, progress_count{0};
uint32_t sample_count{0};
uint32_t paddingCount{0};
uint32_t repeatCount{0};
uint32_t phase{0}, sphase{0};
bool doneSending{false};
uint8_t cur_bit{0};
uint16_t bit_pos{0};
+96
View File
@@ -0,0 +1,96 @@
/*
* Copyright (C) 2024 EPIRB Receiver Implementation
*
* 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 "proc_epirb.hpp"
#include "portapack_shared_memory.hpp"
#include "dsp_fir_taps.hpp"
#include "event_m4.hpp"
#include <ch.h>
EPIRBProcessor::EPIRBProcessor() {
// Configure the decimation filters for narrowband EPIRB signal
// Target: Reduce 2.457600 MHz to ~38.4 kHz for 400 bps processing
decim_0.configure(taps_11k0_decim_0.taps);
decim_1.configure(taps_11k0_decim_1.taps);
baseband_thread.start();
}
void EPIRBProcessor::execute(const buffer_c8_t& buffer) {
/* 2.4576MHz, 2048 samples */
// First decimation stage: 2.4576 MHz -> 307.2 kHz
const auto decim_0_out = decim_0.execute(buffer, dst_buffer);
// Second decimation stage: 307.2 kHz -> 38.4 kHz
const auto decim_1_out = decim_1.execute(decim_0_out, dst_buffer);
const auto decimator_out = decim_1_out;
/* 38.4kHz, 32 samples (approximately) */
feed_channel_stats(decimator_out);
// Process each decimated sample through the matched filter
for (size_t i = 0; i < decimator_out.count; i++) {
// Apply matched filter for BPSK demodulation
if (mf.execute_once(decimator_out.p[i])) {
// Feed symbol to clock recovery when matched filter triggers
clock_recovery(mf.get_output());
}
}
}
void EPIRBProcessor::consume_symbol(const float raw_symbol) {
// BPSK demodulation: positive = 1, negative = 0
const uint_fast8_t sliced_symbol = (raw_symbol >= 0.0f) ? 1 : 0;
// Decode bi-phase L encoding manually
// In bi-phase L: 0 = no transition, 1 = transition
// This is a simple edge detector
const auto decoded_symbol = sliced_symbol ^ last_symbol;
last_symbol = sliced_symbol;
// Build packet from decoded symbols
packet_builder.execute(decoded_symbol);
}
void EPIRBProcessor::payload_handler(const baseband::Packet& packet) {
// EPIRB packet received - validate and process
if (packet.size() >= 112) { // Minimum EPIRB data payload size (112 bits)
packets_received++;
last_packet_timestamp = Timestamp::now();
// Create and send EPIRB packet message to application layer
const EPIRBPacketMessage message{packet};
shared_memory.application_queue.push(message);
}
}
void EPIRBProcessor::on_message(const Message* const message) {
(void)message; // Unused in this processor
}
int main() {
EventDispatcher event_dispatcher{std::make_unique<EPIRBProcessor>()};
event_dispatcher.run();
return 0;
}
+135
View File
@@ -0,0 +1,135 @@
/*
* Copyright (C) 2024 EPIRB Receiver Implementation
*
* 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 __PROC_EPIRB_H__
#define __PROC_EPIRB_H__
#include <cstdint>
#include <cstddef>
#include <array>
#include <complex>
#include "baseband_processor.hpp"
#include "baseband_thread.hpp"
#include "rssi_thread.hpp"
#include "channel_decimator.hpp"
#include "matched_filter.hpp"
#include "clock_recovery.hpp"
#include "symbol_coding.hpp"
#include "packet_builder.hpp"
#include "baseband_packet.hpp"
#include "message.hpp"
#include "buffer.hpp"
// Forward declarations for types only used as pointers/references
class Message;
namespace baseband {
class Packet;
}
// EPIRB 406 MHz Emergency Position Indicating Radio Beacon
// Signal characteristics:
// - Frequency: 406.025 - 406.028 MHz (typically 406.028 MHz)
// - Modulation: BPSK (Binary Phase Shift Keying)
// - Data rate: 400 bps
// - Encoding: Bi-phase L (Manchester)
// - Transmission: Every 50 seconds ± 2.5 seconds
// - Power: 5W ± 2dB
// - Message length: 144 bits (including sync pattern)
// Matched filter for BPSK demodulation at 400 bps
// Using raised cosine filter taps optimized for 400 bps BPSK
static constexpr std::array<std::complex<float>, 64> bpsk_taps = {{// Raised cosine filter coefficients for BPSK 400 bps
-5, -8, -12, -15, -17, -17, -15, -11,
-5, 2, 11, 20, 29, 37, 43, 47,
48, 46, 42, 35, 26, 16, 4, -8,
-21, -33, -44, -53, -59, -62, -62, -58,
-51, -41, -28, -13, 3, 19, 36, 51,
64, 74, 80, 82, 80, 74, 64, 51,
36, 19, 3, -13, -28, -41, -51, -58,
-62, -62, -59, -53, -44, -33, -21, -8}};
class EPIRBProcessor : public BasebandProcessor {
public:
EPIRBProcessor();
void execute(const buffer_c8_t& buffer) override;
void on_message(const Message* const message) override;
private:
// EPIRB operates at 406 MHz with narrow bandwidth
static constexpr size_t baseband_fs = 2457600;
static constexpr uint32_t epirb_center_freq = 406028000; // 406.028 MHz
static constexpr uint32_t symbol_rate = 400; // 400 bps
static constexpr size_t decimation_factor = 64; // Decimate to ~38.4kHz
std::array<complex16_t, 512> dst{};
const buffer_c16_t dst_buffer{
dst.data(),
dst.size()};
// Decimation chain for 406 MHz EPIRB signal processing
dsp::decimate::FIRC8xR16x24FS4Decim8 decim_0{};
dsp::decimate::FIRC16xR16x32Decim8 decim_1{};
dsp::matched_filter::MatchedFilter mf{bpsk_taps, 2};
// Clock recovery for 400 bps symbol rate
// Sampling rate after decimation: ~38.4kHz
// Symbols per sample: 38400 / 400 = 96 samples per symbol
clock_recovery::ClockRecovery<clock_recovery::FixedErrorFilter> clock_recovery{
38400, // sampling_rate
400, // symbol_rate (400 bps)
{0.0555f}, // error_filter coefficient
[this](const float symbol) { this->consume_symbol(symbol); }};
// Simple bi-phase L decoder state
uint_fast8_t last_symbol = 0;
// EPIRB packet structure:
// - Sync pattern: 000101010101... (15 bits)
// - Frame sync: 0111110 (7 bits)
// - Data: 112 bits
// - BCH error correction: 10 bits
// Total: 144 bits
PacketBuilder<BitPattern, BitPattern, BitPattern> packet_builder{
{0b010101010101010, 15, 1}, // Preamble pattern
{0b0111110, 7}, // Frame sync pattern
{0b0111110, 7}, // End pattern (same as sync for simplicity)
[this](const baseband::Packet& packet) {
this->payload_handler(packet);
}};
void consume_symbol(const float symbol);
void payload_handler(const baseband::Packet& packet);
// Statistics
uint32_t packets_received = 0;
Timestamp last_packet_timestamp{};
/* NB: Threads should be the last members in the class definition. */
BasebandThread baseband_thread{
baseband_fs, this, baseband::Direction::Receive, /*auto_start*/ false};
RSSIThread rssi_thread{};
};
#endif /*__PROC_EPIRB_H__*/
+1 -3
View File
@@ -33,9 +33,7 @@
#include <cstdint>
#include <cstddef>
#ifndef M_PI
#define M_PI 3.14159265358979323846
#endif
#include "mathdef.hpp"
float FSKRxProcessor::detect_peak_power(const buffer_c8_t& buffer, int N) {
int32_t power = 0;
+1 -3
View File
@@ -30,9 +30,7 @@
#include <random>
#include <cmath>
#ifndef M_PI
#define M_PI 3.14159265358979323846f
#endif
#include "mathdef.hpp"
void JammerProcessor::execute(const buffer_c8_t& buffer) {
if (!configured) return;
+1 -1
View File
@@ -91,7 +91,7 @@ const float adsb_lat_lut[58] = {
83.07199445, 83.99173563, 84.89166191, 85.75541621,
86.53536998, 87.00000000};
const float PI = 3.14159265358979323846;
#include "mathdef.hpp"
const float NZ = 15.0;
+57 -36
View File
@@ -52,10 +52,10 @@ void BMPFile::close() {
bmpimage.close();
}
// creates a new bmp file. for now, hardcoded to 3 byte colour depth
// creates a new bmp file. hardcoded to 3 byte (24-bit) colour depth
bool BMPFile::create(const std::filesystem::path& file, uint32_t x, uint32_t y) {
is_opened = false;
is_read_ony = true;
is_read_only = true;
bmpimage.close(); // if already open, close before open a new
if (file_exists(file)) {
delete_file(file); // overwrite
@@ -85,7 +85,7 @@ bool BMPFile::create(const std::filesystem::path& file, uint32_t x, uint32_t y)
bmpimage.write(&bmp_header, sizeof(bmp_header_t));
file_pos = bmp_header.image_data;
is_opened = true;
is_read_ony = false;
is_read_only = false;
if (!expand_y(y)) return false; // will fill with 0, and update header data
seek(0, 0);
return true;
@@ -94,30 +94,36 @@ bool BMPFile::create(const std::filesystem::path& file, uint32_t x, uint32_t y)
// opens the file and parses header data. return true on success
bool BMPFile::open(const std::filesystem::path& file, bool readonly) {
is_opened = false;
is_read_ony = true;
bmpimage.close(); // if already open, close before open a new
is_read_only = true;
bmpimage.close();
auto result = bmpimage.open(file, readonly, false);
if (!result.value().ok()) return false;
file_pos = 0;
bmpimage.seek(file_pos);
auto read_size = bmpimage.read(&bmp_header, sizeof(bmp_header_t));
if (!((bmp_header.signature == 0x4D42) && // "BM" Signature
(bmp_header.planes == 1) && // Seems always to be 1
(bmp_header.compression == 0 || bmp_header.compression == 3))) { // No compression
bmpimage.read(&bmp_header, sizeof(bmp_header_t));
if (!((bmp_header.signature == 0x4D42) &&
(bmp_header.planes == 1) &&
(bmp_header.compression == 0 || bmp_header.compression == 3))) {
return false;
}
char buffer[257];
switch (bmp_header.bpp) {
case 8:
type = 4;
byte_per_px = 1;
// bmpimage.seek(sizeof(bmp_header_t));
// bmpimage.read(color_palette, 1024);
return false; // niy
break;
case 16:
file_pos = 0x36;
memset(buffer, 0, 16);
bmpimage.read(buffer, 16);
byte_per_px = 2;
if (buffer[1] == 0x7C)
type = 3; // A1R5G5B5
else
type = 0; // R5G6B5
type = 5;
if (bmp_header.compression == 3) {
return false;
} // niy
break;
case 24:
type = 1;
@@ -130,12 +136,11 @@ bool BMPFile::open(const std::filesystem::path& file, bool readonly) {
default:
// not supported
return false;
break;
}
byte_per_row = (bmp_header.width * byte_per_px % 4 == 0) ? bmp_header.width * byte_per_px : (bmp_header.width * byte_per_px + (4 - ((bmp_header.width * byte_per_px) % 4)));
byte_per_row = (bmp_header.width * byte_per_px + 3) & ~3;
file_pos = bmp_header.image_data;
is_opened = true;
is_read_ony = readonly;
is_read_only = readonly;
currx = 0;
curry = 0;
return true;
@@ -161,20 +166,34 @@ bool BMPFile::read_next_px(ui::Color& px, bool seek = true) {
auto res = bmpimage.read(buffer, byte_per_px);
if (res.is_error()) return false;
switch (type) {
case 0: // R5G6B5
case 3: // A1R5G5B5
if (!type)
px = ui::Color((uint16_t)buffer[0] | ((uint16_t)buffer[1] << 8));
else
px = ui::Color(((uint16_t)buffer[0] & 0x1F) | ((uint16_t)buffer[0] & 0xE0) << 1 | ((uint16_t)buffer[1] & 0x7F) << 9);
case 5: {
// ARGB1555
uint16_t val = buffer[0] | (buffer[1] << 8);
// Extract components
//*a = (val >> 15) & 0x01; // 1-bit alpha
uint8_t r = (val >> 10) & 0x1F; // 5-bit red
uint8_t g = (val >> 5) & 0x1F; // 5-bit green
uint8_t b = (val)&0x1F; // 5-bit blue
// expand
r = (r << 3) | (r >> 2);
g = (g << 3) | (g >> 2);
b = (b << 3) | (b >> 2);
px = ui::Color(r, g, b);
break;
}
case 2: // 32
px = ui::Color(buffer[2], buffer[1], buffer[0]);
break;
case 4: { // 8-bit
// uint8_t index = buffer[0];
// px = ui::Color(color_palette[index][2], color_palette[index][1], color_palette[index][0]); // Palette is BGR
// px = ui::Color(buffer[0]); // niy, since needs a lot of ram for the palette
break;
}
case 1: // 24
default:
px = ui::Color(buffer[2], buffer[1], buffer[0]);
break;
case 2: // 32
px = ui::Color(buffer[2], buffer[1], buffer[0]);
break;
}
if (seek) advance_curr_px();
@@ -188,23 +207,23 @@ void BMPFile::set_bg_color(ui::Color background) {
}
// delete bg color. default. creation or expansion will be fast, but the file will contain random garbage. no problem if you write all pixels later.
void BMPFile::delete_db_color() {
void BMPFile::delete_bg_color() {
use_bg = false;
}
// writes a color data to the current position, and advances 1 px. true on success, false on error
bool BMPFile::write_next_px(ui::Color& px) {
if (!is_opened) return false;
if (is_read_ony) return false;
if (!is_opened || is_read_only) return false;
uint8_t buffer[4];
switch (type) {
case 5:
case 0: // R5G6B5
case 3: // A1R5G5B5
if (!type) {
buffer[0] = (px.r() << 3) | (px.g() >> 3); // todo test in future
buffer[0] = (px.r() << 3) | (px.g() >> 3);
buffer[1] = (px.g() << 5) | px.b();
} else {
buffer[0] = (1 << 7) | (px.r() << 2) | (px.g() >> 3); // todo test in future
buffer[0] = (1 << 7) | (px.r() << 2) | (px.g() >> 3);
buffer[1] = (px.g() << 5) | px.b();
}
break;
@@ -220,6 +239,8 @@ bool BMPFile::write_next_px(ui::Color& px) {
buffer[0] = px.b();
buffer[3] = 255;
break;
case 4: // 8-bit
return false;
}
auto res = bmpimage.write(buffer, byte_per_px);
if (res.is_error()) return false;
@@ -260,7 +281,7 @@ bool BMPFile::expand_y(uint32_t new_y) {
if (!is_opened) return false; // not yet opened
uint32_t old_height = get_real_height();
if (new_y < old_height) return true; // already bigger
if (is_read_ony) return false; // can't expand
if (is_read_only) return false; // can't expand
uint32_t delta = (new_y - old_height) * byte_per_row;
bmp_header.size += delta;
bmp_header.data_size += delta;
+3 -2
View File
@@ -47,12 +47,12 @@ class BMPFile {
uint32_t get_width();
bool is_bottomup();
void set_bg_color(ui::Color background);
void delete_db_color();
void delete_bg_color();
private:
bool advance_curr_px(uint32_t num);
bool is_opened = false;
bool is_read_ony = true;
bool is_read_only = true;
File bmpimage{};
size_t file_pos = 0;
@@ -64,6 +64,7 @@ class BMPFile {
uint32_t currx = 0;
uint32_t curry = 0;
ui::Color bg{};
// uint8_t color_palette[256][4];
bool use_bg = false;
};
+3 -1
View File
@@ -26,7 +26,9 @@
#include <complex>
#include <cmath>
constexpr float pi{3.141592653589793238462643383279502884f};
#include "mathdef.hpp"
constexpr float pi{M_PI};
namespace std {
+1 -1
View File
@@ -1,7 +1,7 @@
/*
* Copyright (C) 2014 Jared Boone, ShareBrained Technology, Inc.
* Copyright (C) 2016 Furrtek
* copyleft 2025 Whiterose of the Dark Army
* copyleft 2024 zxkmm AKA zix aka sommermorgentraum
*
* This file is part of PortaPack.
*
+4
View File
@@ -0,0 +1,4 @@
#pragma once
#define PI 3.1415926535897932384626433832795
#define M_PI PI
+12
View File
@@ -135,6 +135,7 @@ class Message {
NoaaAptRxStatusData = 78,
NoaaAptRxImageData = 79,
FSKPacket = 80,
EPIRBPacket = 81,
MAX
};
@@ -339,6 +340,17 @@ class AISPacketMessage : public Message {
baseband::Packet packet;
};
class EPIRBPacketMessage : public Message {
public:
constexpr EPIRBPacketMessage(
const baseband::Packet& packet)
: Message{ID::EPIRBPacket},
packet{packet} {
}
baseband::Packet packet;
};
class TPMSPacketMessage : public Message {
public:
constexpr TPMSPacketMessage(
+1 -1
View File
@@ -1,6 +1,6 @@
/*
* Copyright (C) 2014 Jared Boone, ShareBrained Technology, Inc.
* Copyleft (ɔ) 2024 zxkmm under GPL license
* copyleft 2024 zxkmm AKA zix aka sommermorgentraum
*
* This file is part of PortaPack.
*
+1 -1
View File
@@ -1,6 +1,6 @@
/*
* Copyright (C) 2014 Jared Boone, ShareBrained Technology, Inc.
* Copyleft (ɔ) 2024 zxkmm under GPL license
* copyleft 2024 zxkmm AKA zix aka sommermorgentraum
* Copyright (C) 2024 u-foka
* Copyright (C) 2024 Mark Thompson
*
+12 -17
View File
@@ -47,7 +47,7 @@ static uint8_t calfrchk[51]; // so subframes are preserved while populate
#define pos_GPSecefY 0x114 // 0x118 // 4 byte (not actually used since Y and Z are following X, and grabbed in that same loop)
#define pos_GPSecefZ 0x118 // 0x11C // 4 byte (same as Y)
#define PI 3.1415926535897932384626433832795 // 3.1416 //(3.1415926535897932384626433832795)
#include "mathdef.hpp"
Packet::Packet(
const baseband::Packet& packet,
@@ -110,7 +110,7 @@ GPS_data Packet::get_GPS_data() const {
result.alt = reader_bi_m.read(8 * 8, 24) / 100.0; // <|
result.lat = reader_bi_m.read(28 * 8, 32) / 1000000.0; // <| Inspired by https://raw.githubusercontent.com/projecthorus/radiosonde_auto_rx/master/demod/mod/m20mod.c
result.lon = reader_bi_m.read(32 * 8, 32) / 1000000.0; // <|
} else if (type_ == Type::Vaisala_RS41_SG) {
} else if (type_ == Type::Vaisala_RS41_SG && crc16rs41(block_gpspos)) {
uint8_t XYZ_bytes[4];
int32_t XYZ; // 32bit
double_t X[3];
@@ -245,7 +245,7 @@ temp_humid Packet::get_temp_humid() const {
}
}
if (type_ == Type::Vaisala_RS41_SG && crc_ok_RS41()) // Only process if packet is healthy
if (type_ == Type::Vaisala_RS41_SG && crc16rs41(block_meas)) // Only process if packet is healthy
{
// memset(calfrchk, 0, 51); // is this necessary ? only if the sondeID changes (new sonde)
// original code from https://github.com/rs1729/RS/blob/master/rs41/rs41ptu.c
@@ -371,9 +371,11 @@ std::string Packet::serial_number() const {
uint8_t achar;
for (uint8_t i = 0; i < 8; i++) { // euquiq: Serial ID is 8 bytes long, each byte a char
achar = vaisala_descramble(pos_SondeID + i);
if (achar < 32 || achar > 126)
return "?"; // Maybe there are ids with less than 8 bytes and this is not OK.
serial_id += (char)achar;
if (achar < 32 || achar > 126) {
serial_id += "?"; // Maybe there are ids with less than 8 bytes and this is not OK.
} else {
serial_id += (char)achar;
}
}
return serial_id;
} else {
@@ -414,15 +416,9 @@ bool Packet::crc_ok() const {
// 2 bytes CRC16 over the data.
bool Packet::crc_ok_RS41() const // check CRC for the data blocks we need
{
if (!crc16rs41(block_status))
return false;
if (!crc16rs41(block_gpspos))
return false;
if (!crc16rs41(block_meas))
return false;
if (!crc16rs41(block_status)) return false;
if (!crc16rs41(block_gpspos)) return false;
if (!crc16rs41(block_meas)) return false;
return true;
}
@@ -451,8 +447,7 @@ bool Packet::crc16rs41(uint32_t field_start) const {
}
}
// Check calculated CRC against packet's one
pos++;
int crcok = vaisala_descramble(pos) | (vaisala_descramble(pos + 1) << 8);
int crcok = vaisala_descramble(field_start + 2 + length) | (vaisala_descramble(field_start + 2 + length + 1) << 8);
if (crcok != rem)
return false;
return true;
+1
View File
@@ -87,6 +87,7 @@ constexpr image_tag_t image_tag_am_audio{'P', 'A', 'M', 'A'};
constexpr image_tag_t image_tag_am_tv{'P', 'A', 'M', 'T'};
constexpr image_tag_t image_tag_capture{'P', 'C', 'A', 'P'};
constexpr image_tag_t image_tag_ert{'P', 'E', 'R', 'T'};
constexpr image_tag_t image_tag_epirb_rx{'P', 'E', 'P', 'I'};
constexpr image_tag_t image_tag_nfm_audio{'P', 'N', 'F', 'M'};
constexpr image_tag_t image_tag_pocsag{'P', 'P', 'O', 'C'};
constexpr image_tag_t image_tag_pocsag2{'P', 'P', 'O', '2'};
+53 -2
View File
@@ -27,8 +27,9 @@
#include <stddef.h>
#include "ui.hpp"
#include "file.hpp"
#define CURRENT_STANDALONE_APPLICATION_API_VERSION 2
#define CURRENT_STANDALONE_APPLICATION_API_VERSION 4
struct standalone_application_api_t {
// Version 1
@@ -58,7 +59,57 @@ struct standalone_application_api_t {
void (*panic)(const char* msg);
void (*set_dirty)();
// TODO: add filesystem access functions
// Version 3
FRESULT(*f_open)
(FIL* fp, const TCHAR* path, BYTE mode);
FRESULT(*f_close)
(FIL* fp);
FRESULT(*f_read)
(FIL* fp, void* buff, UINT btr, UINT* br);
FRESULT(*f_write)
(FIL* fp, const void* buff, UINT btw, UINT* bw);
FRESULT(*f_lseek)
(FIL* fp, FSIZE_t ofs);
FRESULT(*f_truncate)
(FIL* fp);
FRESULT(*f_sync)
(FIL* fp);
FRESULT(*f_opendir)
(DIR* dp, const TCHAR* path);
FRESULT(*f_closedir)
(DIR* dp);
FRESULT(*f_readdir)
(DIR* dp, FILINFO* fno);
FRESULT(*f_findfirst)
(DIR* dp, FILINFO* fno, const TCHAR* path, const TCHAR* pattern);
FRESULT(*f_findnext)
(DIR* dp, FILINFO* fno);
FRESULT(*f_mkdir)
(const TCHAR* path);
FRESULT(*f_unlink)
(const TCHAR* path);
FRESULT(*f_rename)
(const TCHAR* path_old, const TCHAR* path_new);
FRESULT(*f_stat)
(const TCHAR* path, FILINFO* fno);
FRESULT(*f_utime)
(const TCHAR* path, const FILINFO* fno);
FRESULT(*f_getfree)
(const TCHAR* path, DWORD* nclst, FATFS** fatfs);
FRESULT(*f_mount)
(FATFS* fs, const TCHAR* path, BYTE opt);
int (*f_putc)(TCHAR c, FIL* fp);
int (*f_puts)(const TCHAR* str, FIL* cp);
int (*f_printf)(FIL* fp, const TCHAR* str, ...);
TCHAR* (*f_gets)(TCHAR* buff, int len, FIL* fp);
void (*draw_pixels)(const ui::Rect r, const ui::Color* const colors, const size_t count);
void (*draw_pixel)(const ui::Point p, const ui::Color color);
void (*exit_app)();
// Version 4
uint16_t* screen_height;
uint16_t* screen_width;
// TODO: add baseband access functions
// HOW TO extend this interface:
+1 -1
View File
@@ -1,6 +1,6 @@
/*
* Copyright (C) 2014 Jared Boone, ShareBrained Technology, Inc.
* copyleft Mr. Robot
* copyleft 2024 zxkmm AKA zix aka sommermorgentraum
*
* This file is part of PortaPack.
*
+1 -1
View File
@@ -1,6 +1,6 @@
/*
* Copyright (C) 2014 Jared Boone, ShareBrained Technology, Inc.
* copyleft Mr. Robot
* copyleft 2024 zxkmm AKA zix aka sommermorgentraum
*
* This file is part of PortaPack.
*

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