Compare commits

...

31 Commits

Author SHA1 Message Date
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
gullradriel 4c5275247e update version (#2735)
Co-authored-by: gullradriel <gullradriel@no-mail.com>
2025-07-11 15:57:04 +02:00
gullradriel a0185d28eb update submodule (#2734)
Co-authored-by: gullradriel <gullradriel@no-mail.com>
2025-07-09 21:33:38 +02:00
RocketGod 2500df310f Add radio settings, new app icon, and other UI improvements (#2732) 2025-07-03 11:03:51 -07:00
Totoo 54f9ff116b AIS map improv (#2725)
* AIS map improv

* format code mismatch with vc
2025-06-29 22:47:18 +02:00
Totoo 47f231ad63 Add blue bar to subghzd+weather (#2724) 2025-06-29 19:31:05 +08:00
Netro f90d3fabce Adding simple FSK Rx Processor. Can be used with New Apps. (#2716)
* Work to allow for unique beacon parsing functions.

* Fixing pull.

* Changes.

* Formatting.

* Fix Copyright

* Update firmware/application/apps/ble_rx_app.cpp

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>

* Update firmware/baseband/proc_btlerx.cpp

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>

* PR suggestions.

* Fix String.

* FSK Rx Improvements. Works for my custom protocol.

* Fix buffer size.

* Refactor

* Formatting.

* Formatting.

* Fixing compiling, and BLE Rx UI/Performance.

* More improvements.

* Fixing stuck state.

* More stuck parsing fix.

* Combining PR changes.

* Improvements from previous PR.

* Fix dbM calculation relative to device RSSI.

* Formatting.

---------

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
Co-authored-by: TJ <tj.baginski@cognosos.com>
2025-06-29 01:02:12 +02:00
RocketGod 4e276cdc71 Battleship (#2720)
* Made the Battleship 2P 2PP game - FSK is wip
* Using POCSAG
2025-06-28 20:02:29 +02:00
plomek 0eb03373b1 Added 3d printed cases for the H4M (#2715) 2025-06-25 19:16:43 +02:00
Netro d5ea0f0369 BLE Rx Improvements (#2710)
* Work to allow for unique beacon parsing functions.
* Fix Copyright
* Update firmware/application/apps/ble_rx_app.cpp
* Update firmware/baseband/proc_btlerx.cpp
* PR suggestions.
* Fix String.
* Refactor
2025-06-25 19:14:04 +02:00
RocketGod 22cc311447 Update app icons for Space Invaders and Dino Game (#2713) 2025-06-25 16:08:48 +02:00
RocketGod 4fbba205ad Made the Blackjack game (#2712)
* Made the Blackjack game
* Format Blackjack main.cpp
* Changed spade to diamond for dark mode visibility
* Format code
2025-06-25 16:07:56 +02:00
RocketGod 7b91103118 Made the Space Invaders game. Argh matey! (#2709)
* Made the Space Invaders game. Argh matey!
* Format code, sigh.
2025-06-21 08:35:52 +02:00
Tommaso Ventafridda c0aa4a1738 ADSB database update (tools, db) (#2701)
* enhance make_airlines_db tool
* enhance make_icao24_db tool
* update airlinescode (.txt, .db), aircraftdatabase/icao24 (.csv, .db)
2025-06-19 17:28:49 +02:00
Tommaso Ventafridda e26f77ee77 Filemanager: go to parent directory keep track of the right selected … (#2702)
* Filemanager: go to parent directory keep track of the right selected item and page number
* review: avoid unnecessary copies in get_extension
2025-06-19 17:24:38 +02:00
plomek f53262783a Improved FPV_ANALOG.txt FREQMAN file (#2700)
* Improved FPV_ANALOG.txt FREQMAN file
Removed unused or super rare analog fpv bands: U, O, H, D
Added 1.2GHz -1.3GHz channels sometimes used for long range analog fpv


* Corrected and updated the labels to be more consistent.

Corrected the labels to be more consistent.

I also somehow messed up the correct channels because 1.3GHz FPV is not fully standardized, but these channels seem to be the most common.
It should be all correct now.

Example transmitters using those channels:
https://greenchip.com.ua/0-0-1615-2.html
https://flymod.net/en/item/walksnail_vtx_9ch
https://pl.aliexpress.com/item/1005006505365351.html
2025-06-19 05:26:02 +00:00
Netro b456c18008 Modified Text Editor to handle long presses. (#2698) 2025-06-19 07:21:54 +02:00
Tommaso Ventafridda fa4b74fd6f Add vendor name in bluetooth rx app (#2696)
* add macaddress db, add vendor name in bluetooth rx app

* show "missing macaddress.db" instead of unknown if db not found

* bluetooth rx list with colors based on mac vendor

* bug fix
2025-06-16 10:57:58 -07:00
RocketGod 18bc2cf11c Made the Dino Game (#2697) 2025-06-15 09:25:10 -07:00
Tommaso Ventafridda 20f28c8331 add dark theme (#2695) 2025-06-13 18:34:20 +02:00
Tommaso Ventafridda ea38a0fe48 Add 1ms hop option to hopper app + 0ms (freeze UI) (#2692) 2025-06-12 11:54:37 +02:00
RocketGod fb2e576b34 Super secret dont look (#2690)
* Add new jammer modes

Overview

This PR enhances the PortaPack Jammer app by introducing eight new signal types, ported from my Flipper Zero RF Jammer app (https://github.com/RocketGod-git/flipper-zero-rf-jammer). These modes expand the app's capability to disrupt a wide range of RF communication protocols, from analog radios to modern digital systems. The implementation preserves the original app structure, resolves namespace conflicts, and ensures compatibility with the Mayhem firmware.

New Modes

The following modes have been added to the options_type in ui_jammer.hpp, with corresponding signal generation in proc_jammer.cpp:

Noise: Generates broadband white noise to interfere with analog and digital signals (e.g., Wi-Fi, Bluetooth, key fobs). Highly effective for overwhelming receivers across a frequency range.

Sine: Produces a continuous, unmodulated sine wave to jam narrowband receivers, ideal for analog FM/AM radios or telemetry systems.

Square: Emits a harmonic-rich square wave, disrupting digital protocols (e.g., OOK, ASK) and systems sensitive to sharp transitions, such as remote keyless entry.

Sawtooth (Experimental): Generates a sawtooth wave with a unique harmonic profile, useful for testing interference against PWM-based or niche analog systems.

Triangle (Experimental): Creates a triangle wave with minimal harmonics, suitable for exploratory jamming of narrowband systems or receiver linearity testing.

Chirp: Outputs a rapid frequency-sweeping chirp signal, effective against frequency-hopping and spread-spectrum systems (e.g., some Wi-Fi, Bluetooth, or military radios).

Gauss: Generates Gaussian noise to mimic natural interference, targeting digital systems like GPS or data links by degrading signal-to-noise ratios.

Brute (Experimental): Transmits a constant maximum-amplitude signal to saturate simple receiver front-ends, useful for brute-force jamming of basic analog devices.

* Super secret

* You gotta get (Get) that (That) dirt off your shoulder
2025-06-10 13:36:26 -07:00
Tommaso Ventafridda 1d79e30d4b Add all jammer modes in hopper app (#2691)
Added all modes that jammer app supports in hopper app.
2025-06-10 06:54:59 -07:00
110 changed files with 10037 additions and 1970 deletions
+1 -1
View File
@@ -1 +1 @@
v2.0.2
v2.1.0
+1 -1
View File
@@ -1 +1 @@
v2.1.0
v2.2.0
+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
+40 -2
View File
@@ -301,7 +301,7 @@ AISRecentEntryDetailView::AISRecentEntryDetailView(NavigationView& nav) {
ais::format::text(entry_.name),
0,
GeoPos::alt_unit::METERS,
GeoPos::spd_unit::NONE,
GeoPos::spd_unit::KNOTS,
ais::format::latlon_float(entry_.last_position.latitude.normalized()),
ais::format::latlon_float(entry_.last_position.longitude.normalized()),
entry_.last_position.true_heading,
@@ -324,7 +324,31 @@ 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);
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);
}
bool AISRecentEntryDetailView::add_map_marker(const AISRecentEntry& entry) {
if (geomap_view && send_updates) {
GeoMarker marker{};
marker.lon = ais::format::latlon_float(entry.last_position.longitude.normalized());
marker.lat = ais::format::latlon_float(entry.last_position.latitude.normalized());
marker.angle = entry.last_position.true_heading;
marker.tag = entry.call_sign.empty() ? to_string_dec_uint(entry.mmsi) : entry.call_sign;
auto markerStored = geomap_view->store_marker(marker);
return markerStored == MARKER_STORED;
}
return false;
}
void AISRecentEntryDetailView::update_map_markers(AISRecentEntries& entries) {
if (geomap_view && send_updates) {
geomap_view->clear_markers();
for (const auto& entry : entries) {
// if (entry.last_position.latitude.is_valid() && entry.last_position.longitude.is_valid()) {
add_map_marker(entry);
// }
}
update_position(); // to update view
}
}
void AISRecentEntryDetailView::focus() {
@@ -415,14 +439,27 @@ AISAppView::AISAppView(NavigationView& nav)
audio::set_rate(audio::Rate::Hz_24000);
audio::output::start();
}
signal_token_tick_second = rtc_time::signal_tick_second += [this]() {
on_tick_second();
};
}
AISAppView::~AISAppView() {
rtc_time::signal_tick_second -= signal_token_tick_second;
audio::output::stop();
receiver_model.disable();
baseband::shutdown();
}
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);
}
}
void AISAppView::focus() {
options_channel.focus();
}
@@ -461,6 +498,7 @@ void AISAppView::on_show_detail(const AISRecentEntry& entry) {
recent_entry_detail_view.hidden(false);
recent_entry_detail_view.set_entry(entry);
recent_entry_detail_view.focus();
recent_entry_detail_view.update_map_markers(recent);
}
} /* namespace ui */
+7
View File
@@ -125,10 +125,14 @@ class AISRecentEntryDetailView : public View {
void update_position();
void focus() override;
void paint(Painter&) override;
bool add_map_marker(const AISRecentEntry& entry);
void update_map_markers(AISRecentEntries& entries);
AISRecentEntryDetailView(const AISRecentEntryDetailView& Entry);
AISRecentEntryDetailView& operator=(const AISRecentEntryDetailView& Entry);
GeoMapView* get_geomap_view() { return geomap_view; }
private:
AISRecentEntry entry_{};
@@ -215,6 +219,8 @@ class AISAppView : public View {
Channel channel{
{21 * 8, 5, 6 * 8, 4},
};
SignalToken signal_token_tick_second{};
uint8_t timer_seconds = 0;
MessageHandlerRegistration message_handler_packet{
Message::ID::AISPacket,
@@ -229,6 +235,7 @@ class AISAppView : public View {
void on_packet(const ais::Packet& packet);
void on_show_list();
void on_show_detail(const AISRecentEntry& entry);
void on_tick_second();
};
} /* namespace ui */
+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,
+431 -155
View File
@@ -2,6 +2,7 @@
* Copyright (C) 2014 Jared Boone, ShareBrained Technology, Inc.
* Copyright (C) 2017 Furrtek
* Copyright (C) 2023 TJ Baginski
* Copyright (C) 2025 Tommaso Ventafridda
*
* This file is part of PortaPack.
*
@@ -67,6 +68,50 @@ uint64_t copy_mac_address_to_uint64(const uint8_t* macAddress) {
return result;
}
MAC_VENDOR_STATUS lookup_mac_vendor_status(const uint8_t* mac_address, std::string& vendor_name) {
static bool db_checked = false;
static bool db_exists = false;
if (!db_checked) {
database db;
database::MacAddressDBRecord dummy_record;
int test_result = db.retrieve_macaddress_record(&dummy_record, "000000");
db_exists = (test_result != DATABASE_NOT_FOUND);
db_checked = true;
}
if (!db_exists) {
vendor_name = "macaddress.db not found";
return MAC_DB_NOT_FOUND;
}
database db;
database::MacAddressDBRecord record;
// Convert MAC address to hex string
std::string mac_hex = "";
for (int i = 0; i < 3; i++) {
// Only need first 3 bytes for OUI
mac_hex += to_string_hex(mac_address[i], 2);
}
int result = db.retrieve_macaddress_record(&record, mac_hex);
if (result == DATABASE_RECORD_FOUND) {
vendor_name = std::string(record.vendor_name);
return MAC_VENDOR_FOUND;
} else {
vendor_name = "Unknown";
return MAC_VENDOR_NOT_FOUND;
}
}
std::string lookup_mac_vendor(const uint8_t* mac_address) {
std::string vendor_name;
lookup_mac_vendor_status(mac_address, vendor_name);
return vendor_name;
}
void reverse_byte_array(uint8_t* arr, int length) {
int start = 0;
int end = length - 1;
@@ -150,8 +195,15 @@ void RecentEntriesTable<BleRecentEntries>::draw(
line = to_string_mac_address(entry.packetData.macAddress, 6, false);
}
std::string hitsStr;
if (!entry.informationString.empty()) {
hitsStr = entry.informationString;
} else {
hitsStr = to_string_dec_int(entry.numHits);
}
// Pushing single digit values down right justified.
std::string hitsStr = to_string_dec_int(entry.numHits);
int hitsDigits = hitsStr.length();
uint8_t hits_spacing = 8 - hitsDigits;
@@ -165,7 +217,10 @@ void RecentEntriesTable<BleRecentEntries>::draw(
line += pad_string_with_spaces(db_spacing) + dbStr;
line.resize(target_rect.width() / 8, ' ');
painter.draw_string(target_rect.location(), style, line);
Style row_style = (entry.vendor_status == MAC_VENDOR_FOUND) ? style : Style{style.font, style.background, Color::grey()};
painter.draw_string(target_rect.location(), row_style, line);
}
BleRecentEntryDetailView::BleRecentEntryDetailView(NavigationView& nav, const BleRecentEntry& entry)
@@ -178,17 +233,21 @@ BleRecentEntryDetailView::BleRecentEntryDetailView(NavigationView& nav, const Bl
&text_mac_address,
&label_pdu_type,
&text_pdu_type,
&label_vendor,
&text_vendor,
&labels});
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);
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);
});
@@ -196,7 +255,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(
@@ -370,10 +429,14 @@ void BleRecentEntryDetailView::paint(Painter& painter) {
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);
set_dirty();
}
BLETxPacket BleRecentEntryDetailView::build_packet() {
BLETxPacket BleRecentEntryDetailView::build_packet(BleRecentEntry entry_) {
BLETxPacket bleTxPacket;
memset(&bleTxPacket, 0, sizeof(BLETxPacket));
@@ -381,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;
}
@@ -432,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,
@@ -453,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) {
@@ -461,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&) {
@@ -480,17 +584,6 @@ BLERxView::BLERxView(NavigationView& nav)
});
};
logger = std::make_unique<BLELogger>();
check_log.on_select = [this](Checkbox&, bool v) {
str_log = "";
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(
@@ -505,23 +598,25 @@ BLERxView::BLERxView(NavigationView& nav)
button_clear_list.on_select = [this](Button&) {
recent.clear();
recent_entries_view.set_dirty();
};
button_switch.on_select = [&nav](Button&) {
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;
@@ -546,22 +641,15 @@ BLERxView::BLERxView(NavigationView& nav)
options_filter.on_change = [this](size_t index, int32_t v) {
filter_index = (uint8_t)index;
recent.clear();
handle_filter_options(v);
recent_entries_view.set_dirty();
};
options_channel.set_selected_index(channel_index, true);
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);
@@ -719,10 +807,66 @@ bool BLERxView::saveFile(const std::filesystem::path& path) {
}
void BLERxView::on_data(BlePacketData* packet) {
if (!logging) {
str_log = "";
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 = (uniqueKeyEncoded & 0xFFFFFFFFFFFF);
if (duplicatePackets) {
key |= ((uint64_t)packet->type) << 48;
}
bool packetExists = false;
// If found store into tempEntry to modify.
auto it = find(recent, key);
if (it != recent.end()) {
recent.push_front(*it);
recent.erase(it);
updateEntry(packet, recent.front(), (ADV_PDU_TYPE)packet->type);
packetExists = true;
} else {
recent.emplace_front(key);
truncate_entries(recent);
packetExists = updateEntry(packet, recent.front(), (ADV_PDU_TYPE)packet->type);
// If parsing failed, remove entry.
if (!packetExists) {
recent.erase(recent.begin());
}
}
if (packetExists) {
handle_filter_options(options_filter.selected_index());
handle_entries_sort(options_sort.selected_index());
if (!searchList.empty()) {
auto it = searchList.begin();
while (it != searchList.end()) {
std::string searchStr = (std::string)*it;
if (recent.front().dataString.find(searchStr) != std::string::npos) {
searchList.erase(it);
found_count++;
break;
}
it++;
}
text_found_count.set(to_string_dec_uint(found_count) + "/" + to_string_dec_uint(total_count));
}
}
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);
@@ -736,58 +880,19 @@ void BLERxView::on_data(BlePacketData* packet) {
str_console += to_string_hex(packet->data[i], 2);
}
uint64_t macAddressEncoded = copy_mac_address_to_uint64(packet->macAddress);
// Start of Packet stuffing.
// Masking off the top 2 bytes to avoid invalid keys.
auto& entry = ::on_packet(recent, macAddressEncoded & 0xFFFFFFFFFFFF);
updateEntry(packet, entry, (ADV_PDU_TYPE)packet->type);
// Add entries if they meet the criteria.
// auto value = filter;
// resetFilteredEntries(recent, [&value](const BleRecentEntry& entry) {
// return (entry.dataString.find(value) == std::string::npos) && (entry.nameString.find(value) == std::string::npos);
// });
handle_filter_options(options_filter.selected_index());
handle_entries_sort(options_sort.selected_index());
// Log at End of Packet.
if (logger && logging) {
logger->log_raw_data(str_console + "\r\n");
logger->log_raw_data(str_console);
}
if (serial_logging) {
UsbSerialAsyncmsg::asyncmsg(str_console); // new line handled there, no need here.
}
str_console = "";
if (!searchList.empty()) {
auto it = searchList.begin();
while (it != searchList.end()) {
std::string searchStr = (std::string)*it;
if (entry.dataString.find(searchStr) != std::string::npos) {
searchList.erase(it);
found_count++;
break;
}
it++;
}
text_found_count.set(to_string_dec_uint(found_count) + "/" + to_string_dec_uint(total_count));
}
}
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());
}
@@ -841,13 +946,10 @@ void BLERxView::on_timer() {
timer_count = 0;
if (auto_channel) {
int min = 37;
int max = 39;
field_frequency.set_value(get_freq_by_channel_number(channel_number));
baseband::set_btlerx(channel_number);
int randomChannel = min + std::rand() % (max - min + 1);
field_frequency.set_value(get_freq_by_channel_number(randomChannel));
baseband::set_btlerx(randomChannel);
channel_number = (channel_number < 39) ? channel_number + 1 : 37;
}
}
if (ble_rx_error != BLE_RX_NO_ERROR) {
@@ -868,7 +970,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(
@@ -886,6 +988,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;
}
@@ -896,16 +1002,43 @@ void BLERxView::handle_entries_sort(uint8_t index) {
void BLERxView::handle_filter_options(uint8_t index) {
auto value = filter;
switch (index) {
case 0: // filter by Data
// Data
case 0:
resetFilteredEntries(recent, [&value](const BleRecentEntry& entry) {
return (entry.dataString.find(value) == std::string::npos) && (entry.nameString.find(value) == std::string::npos);
});
break;
case 1: // filter by MAC address (All caps: e.g. AA:BB:CC:DD:EE:FF)
// MAC address (All caps: e.g. AA:BB:CC:DD:EE:FF)
case 1:
resetFilteredEntries(recent, [&value](const BleRecentEntry& entry) {
return (to_string_mac_address(entry.packetData.macAddress, 6, false).find(value) == std::string::npos);
});
break;
// Name
case 2:
resetFilteredEntries(recent, [&value](const BleRecentEntry& entry) {
return (entry.nameString.find(value) == std::string::npos);
});
break;
// Info
case 3:
resetFilteredEntries(recent, [&value](const BleRecentEntry& entry) {
return (entry.informationString.find(value) == std::string::npos);
});
break;
// Vendor
case 4:
resetFilteredEntries(recent, [&value](const BleRecentEntry& entry) {
std::string vendor_name = lookup_mac_vendor(entry.packetData.macAddress);
return (vendor_name.find(value) == std::string::npos);
});
break;
// Channel
case 5:
resetFilteredEntries(recent, [&value](const BleRecentEntry& entry) {
return (to_string_dec_int(entry.channelNumber).find(value) == std::string::npos);
});
break;
default:
break;
}
@@ -923,72 +1056,215 @@ BLERxView::~BLERxView() {
baseband::shutdown();
}
void BLERxView::updateEntry(const BlePacketData* packet, BleRecentEntry& entry, ADV_PDU_TYPE pdu_type) {
bool BLERxView::updateEntry(const BlePacketData* packet, BleRecentEntry& entry, ADV_PDU_TYPE pdu_type) {
std::string data_string;
int i;
for (i = 0; i < packet->dataLen; i++) {
data_string += to_string_hex(packet->data[i], 2);
}
entry.dbValue = packet->max_dB;
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.numHits++;
entry.pduType = pdu_type;
entry.channelNumber = channel_number;
// 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();
bool success = false;
// 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) && entry.nameString.empty()) {
ADV_PDU_PAYLOAD_TYPE_0_2_4_6* advertiseData = (ADV_PDU_PAYLOAD_TYPE_0_2_4_6*)entry.packetData.data;
if (pdu_type == ADV_IND || pdu_type == ADV_NONCONN_IND || pdu_type == SCAN_RSP || pdu_type == ADV_SCAN_IND) {
if (uniqueParsing) {
success = parse_tracking_beacon_data(packet->data, packet->dataLen, entry.nameString, entry.informationString);
}
uint8_t currentByte = 0;
uint8_t length = 0;
uint8_t type = 0;
std::string decoded_data;
for (currentByte = 0; (currentByte < entry.packetData.dataLen);) {
length = advertiseData->Data[currentByte++];
type = advertiseData->Data[currentByte++];
// Subtract 1 because type is part of the length.
for (int i = 0; i < length - 1; i++) {
// parse the name of bluetooth device: 0x08->Shortened Local Name; 0x09->Complete Local Name
if (type == 0x08 || type == 0x09) {
decoded_data += (char)advertiseData->Data[currentByte];
}
currentByte++;
}
if (!decoded_data.empty()) {
entry.nameString = std::move(decoded_data);
break;
}
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;
}
}
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) {
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();
}
return 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) {
uint8_t currentByte, currentLength, currentType = 0;
std::string tempName = "";
for (currentByte = 0; currentByte < length;) {
currentLength = data[currentByte++];
currentType = data[currentByte++];
// Subtract 1 because type is part of the length.
for (int i = 0; ((i < currentLength - 1) && (currentByte < length)); i++) {
// parse the name of bluetooth device: 0x08->Shortened Local Name; 0x09->Complete Local Name
if (currentType == 0x08 || currentType == 0x09) {
tempName += (char)data[currentByte];
}
currentByte++;
}
if (!tempName.empty()) {
nameString = tempName;
break;
}
}
informationString = "";
if (!informationString.empty()) {
// Option to change title of Hits Column.
// Setting to default for now.
columns.set(1, "Hits", 7);
}
return true;
}
} /* namespace ui */
+75 -32
View File
@@ -2,6 +2,7 @@
* Copyright (C) 2014 Jared Boone, ShareBrained Technology, Inc.
* Copyright (C) 2017 Furrtek
* Copyright (C) 2023 TJ Baginski
* Copyright (C) 2025 Tommaso Ventafridda
*
* This file is part of PortaPack.
*
@@ -33,6 +34,7 @@
#include "ui_record_view.hpp"
#include "app_settings.hpp"
#include "radio_state.hpp"
#include "database.hpp"
#include "log_file.hpp"
#include "utility.hpp"
#include "usb_serial_thread.hpp"
@@ -69,24 +71,34 @@ typedef enum {
RESERVED5 = 12,
RESERVED6 = 13,
RESERVED7 = 14,
RESERVED8 = 15
RESERVED8 = 15,
UNKNOWN = 16
} ADV_PDU_TYPE;
typedef enum {
MAC_VENDOR_UNKNOWN = 0,
MAC_VENDOR_FOUND = 1,
MAC_VENDOR_NOT_FOUND = 2,
MAC_DB_NOT_FOUND = 3
} MAC_VENDOR_STATUS;
struct BleRecentEntry {
using Key = uint64_t;
static constexpr Key invalid_key = 0xffffffff;
static constexpr Key invalid_key = 0xFFFFFFFFFFFFF;
uint64_t macAddress;
uint64_t uniqueKey;
int dbValue;
BlePacketData packetData;
std::string timestamp;
std::string dataString;
std::string nameString;
std::string informationString;
bool include_name;
uint16_t numHits;
ADV_PDU_TYPE pduType;
uint8_t channelNumber;
MAC_VENDOR_STATUS vendor_status;
bool entryFound;
BleRecentEntry()
@@ -94,22 +106,24 @@ struct BleRecentEntry {
}
BleRecentEntry(
const uint64_t macAddress)
: macAddress{macAddress},
const uint64_t uniqueKey)
: uniqueKey{uniqueKey},
dbValue{},
packetData{},
timestamp{},
dataString{},
nameString{},
informationString{},
include_name{},
numHits{},
pduType{},
channelNumber{},
vendor_status{MAC_VENDOR_UNKNOWN},
entryFound{} {
}
Key key() const {
return macAddress;
return uniqueKey;
}
};
@@ -126,11 +140,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{};
@@ -152,6 +166,13 @@ class BleRecentEntryDetailView : public View {
{9 * 8, 1 * 16, 17 * 8, 16},
"-"};
Labels label_vendor{
{{0 * 8, 2 * 16}, "Vendor:", Theme::getInstance()->fg_light->foreground}};
Text text_vendor{
{7 * 8, 2 * 16, 23 * 8, 16},
"-"};
Labels labels{
{{0 * 8, 3 * 16}, "Len", Theme::getInstance()->fg_light->foreground},
{{5 * 8, 3 * 16}, "Type", Theme::getInstance()->fg_light->foreground},
@@ -198,15 +219,19 @@ class BLERxView : public View {
bool saveFile(const std::filesystem::path& path);
std::unique_ptr<UsbSerialThread> usb_serial_thread{};
void on_data(BlePacketData* packetData);
void log_ble_packet(BlePacketData* packet);
void on_filter_change(std::string value);
void on_file_changed(const std::filesystem::path& new_file_path);
void file_error();
void on_timer();
void handle_entries_sort(uint8_t index);
void handle_filter_options(uint8_t index);
void updateEntry(const BlePacketData* packet, BleRecentEntry& entry, ADV_PDU_TYPE pdu_type);
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_;
RxRadioState radio_state_{
2402000000 /* frequency */,
4000000 /* bandwidth */,
@@ -216,6 +241,8 @@ class BLERxView : public View {
uint8_t channel_index{0};
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};
@@ -234,6 +261,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 = "";
@@ -243,7 +272,7 @@ class BLERxView : public View {
bool auto_channel = false;
int16_t timer_count{0};
int16_t timer_period{6}; // 100ms
int16_t timer_period{1}; // 25ms
std::string filterBuffer{};
std::string listFileBuffer{};
@@ -259,7 +288,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{
@@ -299,7 +328,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},
@@ -307,9 +337,13 @@ class BLERxView : public View {
OptionsField options_filter{
{18 * 8 + 2, 2 * 8},
4,
7,
{{"Data", 0},
{"MAC", 1}}};
{"MAC", 1},
{"Name", 2},
{"Info", 3},
{"Vendor", 4},
{"Channel", 5}}};
Checkbox check_log{
{10 * 8, 4 * 8 + 2},
@@ -323,36 +357,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{""};
@@ -361,10 +404,10 @@ class BLERxView : public View {
BleRecentEntries recent{};
BleRecentEntries tempList{};
const RecentEntriesColumns columns{{
{"Mac Address", 17},
RecentEntriesColumns columns{{
{"Name", 17},
{"Hits", 7},
{"dB", 4},
{"dBm", 4},
}};
BleRecentEntriesView recent_entries_view{columns, recent};
+135 -83
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,73 +281,75 @@ 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) {
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);
}
}
}
}
BLETxView::BLETxView(NavigationView& nav)
: nav_{nav} {
add_children({&button_open,
add_children({&dataEditView,
&button_open,
&text_filename,
&progressbar,
&check_rand_mac,
@@ -330,7 +360,6 @@ BLETxView::BLETxView(NavigationView& nav)
&label_speed,
&options_speed,
&options_channel,
&options_adv_type,
&label_marked_data,
&marked_data_sequence,
&label_packet_index,
@@ -340,7 +369,6 @@ BLETxView::BLETxView(NavigationView& nav)
&label_mac_address,
&text_mac_address,
&label_data_packet,
&dataEditView,
&button_clear_marked,
&button_save_packet,
&button_switch});
@@ -371,13 +399,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) {
@@ -430,6 +453,23 @@ BLETxView::BLETxView(NavigationView& nav)
}
};
dataEditView.on_change = [this](uint8_t value) {
// Reject setting newline at index 29.
if (cursor_pos.col != 29) {
uint16_t dataBytePos = (cursor_pos.line * 29) + cursor_pos.col;
packets[current_packet].advertisementData[dataBytePos] = uint_to_char(value, 16);
update_current_packet(packets[current_packet], current_packet);
}
};
dataEditView.on_cursor_moved = [this]() {
// Save last selected cursor.
cursor_pos.line = dataEditView.line();
cursor_pos.col = dataEditView.col();
};
button_clear_marked.on_select = [this](Button&) {
marked_counter = 0;
markedBytes.clear();
@@ -465,17 +505,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 {
@@ -493,6 +541,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();
}
}
@@ -523,6 +573,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;
@@ -531,10 +583,9 @@ void BLETxView::update_current_packet(BLETxPacket packet, uint32_t currentIndex)
for (const std::string& str : strings) {
dataFile.write(str.c_str(), str.size());
dataFile.write("\n", 1);
;
}
dataFile.~File();
dataFile.close();
auto result = FileWrapper::open(dataTempFilePath);
@@ -546,6 +597,7 @@ void BLETxView::update_current_packet(BLETxPacket packet, uint32_t currentIndex)
dataEditView.set_font_zoom(true);
dataEditView.set_file(*dataFileWrapper);
dataEditView.redraw(true, true);
dataEditView.cursor_set(cursor_pos.line, cursor_pos.col);
}
void BLETxView::set_parent_rect(const Rect new_parent_rect) {
+18 -16
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},
@@ -287,11 +292,8 @@ class BLETxView : public View {
Labels label_data_packet{
{{0 * 8, 9 * 16}, "Packet Data:", Theme::getInstance()->fg_light->foreground}};
Console console{
{0, 9 * 18, screen_width, screen_height - 80}};
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},
@@ -323,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{
@@ -34,6 +34,7 @@ constexpr std::string_view mayhem_information_list[] = {
"RedFox-Fr,nemanjan00,",
"MichalLeonBorsuk,",
"MatiasFernandez,Giorgiofox",
"TommasoVentafridda",
" ",
"#Havoc:",
"jboone,furrtek,eried,argilo,",
+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);
+64 -90
View File
@@ -228,6 +228,8 @@ void FileManBaseView::load_directory_contents(const fs::path& dir_path) {
text_current.set(dir_path.empty() ? "(sd root)" : truncate(dir_path, 24));
// Collect all entries first
std::list<fileman_entry> all_entries;
for (const auto& entry : fs::directory_iterator(dir_path, u"*")) {
// Hide files starting with '.' (hidden / tmp).
if (!show_hidden_files && is_hidden_file(entry.path()))
@@ -235,36 +237,40 @@ void FileManBaseView::load_directory_contents(const fs::path& dir_path) {
if (fs::is_regular_file(entry.status())) {
if (!filtering || path_iequal(entry.path().extension(), extension_filter) || (cxx_file && is_cxx_capture_file(entry.path())))
insert_sorted(entry_list, {entry.path().string(), (uint32_t)entry.size(), false});
insert_sorted(all_entries, {entry.path().string(), (uint32_t)entry.size(), false});
} else if (fs::is_directory(entry.status())) {
insert_sorted(entry_list, {entry.path().string(), 0, true});
insert_sorted(all_entries, {entry.path().string(), 0, true});
}
}
// paginating
auto list_size = entry_list.size();
nb_pages = 1 + (list_size / items_per_page);
size_t start = pagination * items_per_page;
size_t stop = start + items_per_page;
if (list_size > start) {
if (list_size < stop)
stop = list_size;
entry_list.erase(std::next(entry_list.begin(), stop), entry_list.end());
entry_list.erase(entry_list.begin(), std::next(entry_list.begin(), start));
// Calculate pagination
nb_pages = (all_entries.size() + items_per_page - 1) / items_per_page;
if (nb_pages == 0) nb_pages = 1;
size_t start_idx = pagination * items_per_page;
size_t end_idx = std::min(start_idx + items_per_page, all_entries.size());
// Add "parent" directory if not at the root and on first page
if (!dir_path.empty() && pagination == 0) {
entry_list.push_back({parent_dir_path.string(), 0, true});
}
// Add "parent" directory if not at the root.
if (!dir_path.empty() && pagination == 0)
entry_list.insert(entry_list.begin(), {parent_dir_path.string(), 0, true});
// add next page
if (list_size > start + items_per_page) {
entry_list.push_back({str_next, (uint32_t)pagination + 1, true});
}
// add prev page
// Add prev page navigation if not on first page
if (pagination > 0) {
entry_list.insert(entry_list.begin(), {str_back, (uint32_t)pagination - 1, true});
entry_list.push_back({str_back, (uint32_t)pagination - 1, true});
}
// Add entries for current page
auto it = all_entries.begin();
std::advance(it, start_idx);
for (size_t i = start_idx; i < end_idx && it != all_entries.end(); i++, ++it) {
entry_list.push_back(*it);
}
// Add next page navigation if not on last page
if (end_idx < all_entries.size()) {
entry_list.push_back({str_next, (uint32_t)pagination + 1, true});
}
}
@@ -320,30 +326,49 @@ void FileManBaseView::focus() {
} else {
menu_view.focus();
}
// Set menu to the correct page and select the correct item
menu_view.set_highlighted(prev_highlight % items_per_page);
}
// Push directory - store the global index (page * items_per_page + local_index)
void FileManBaseView::push_dir(const fs::path& path) {
if (path == parent_dir_path) {
pop_dir();
} else {
// Save global index (combines page number and item position)
saved_index_stack.push_back(menu_view.highlighted_index() + (pagination * items_per_page));
current_path /= path;
saved_index_stack.push_back(menu_view.highlighted_index());
menu_view.set_highlighted(0);
reload_current(true);
reload_current(true); // Reset pagination when entering new directory
}
}
void FileManBaseView::pop_dir() {
if (saved_index_stack.empty())
if (current_path.empty()) {
return;
}
// Move to parent directory
current_path = current_path.parent_path();
reload_current(true);
menu_view.set_highlighted(saved_index_stack.back());
saved_index_stack.pop_back();
// Restore the previous global index if available
if (!saved_index_stack.empty()) {
uint32_t global_index = saved_index_stack.back();
saved_index_stack.pop_back();
// Calculate pagination from global index
pagination = global_index / items_per_page;
// Calculate local index within the page
prev_highlight = global_index % items_per_page;
restoring_navigation = true;
}
reload_current(false); // Important: don't reset pagination
}
std::string get_extension(std::string t) {
std::string FileManBaseView::get_extension(const std::string& t) const {
const auto index = t.find_last_of(u'.');
if (index == t.npos) {
return {};
@@ -372,7 +397,9 @@ void FileManBaseView::refresh_list() {
if (on_refresh_widgets)
on_refresh_widgets(false);
prev_highlight = menu_view.highlighted_index();
if (!restoring_navigation) {
prev_highlight = menu_view.highlighted_index();
}
menu_view.clear();
for (const auto& entry : entry_list) {
@@ -411,10 +438,14 @@ void FileManBaseView::refresh_list() {
}
menu_view.set_highlighted(prev_highlight);
restoring_navigation = false;
}
void FileManBaseView::reload_current(bool reset_pagination) {
if (reset_pagination) pagination = 0;
// Only reset pagination if explicitly requested
if (reset_pagination) {
pagination = 0;
}
load_directory_contents(current_path);
refresh_list();
}
@@ -489,63 +520,6 @@ void FileLoadView::refresh_widgets(const bool) {
set_dirty();
}
/* FileSaveView **************************************************************/
/*
FileSaveView::FileSaveView(
NavigationView& nav,
const fs::path& path,
const fs::path& file
) : nav_{ nav },
path_{ path },
file_{ file }
{
add_children({
&text_path,
&button_edit_path,
&text_name,
&button_edit_name,
&button_save,
&button_cancel,
});
button_edit_path.on_select = [this](Button&) {
buffer_ = path_.string();
text_prompt(nav_, buffer_, max_filename_length,ENTER_KEYBOARD_MODE_ALPHA,
[this](std::string&) {
path_ = buffer_;
refresh_widgets();
});
};
button_edit_name.on_select = [this](Button&) {
buffer_ = file_.string();
text_prompt(nav_, buffer_, max_filename_length,ENTER_KEYBOARD_MODE_ALPHA,
[this](std::string&) {
file_ = buffer_;
refresh_widgets();
});
};
button_save.on_select = [this](Button&) {
if (on_save)
on_save(path_ / file_);
else
nav_.pop();
};
button_cancel.on_select = [this](Button&) {
nav_.pop();
};
refresh_widgets();
}
void FileSaveView::refresh_widgets() {
text_path.set(truncate(path_, 30));
text_name.set(truncate(file_, 30));
set_dirty();
}
*/
/* FileManagerView ***********************************************************/
void FileManagerView::refresh_widgets(const bool v) {
+2
View File
@@ -64,6 +64,7 @@ class FileManBaseView : public View {
uint32_t prev_highlight = 0;
uint8_t pagination = 0;
uint8_t nb_pages = 1;
bool restoring_navigation = false;
static constexpr size_t max_filename_length = 20;
static constexpr size_t max_items_loaded = 75; // too memory hungry, so won't sort it
static constexpr size_t items_per_page = 20;
@@ -96,6 +97,7 @@ class FileManBaseView : public View {
void load_directory_contents(const std::filesystem::path& dir_path);
void load_directory_contents_unordered(const std::filesystem::path& dir_path, size_t file_cnt);
const file_assoc_t& get_assoc(const std::filesystem::path& ext) const;
std::string get_extension(const std::string& t) const;
void copy_waterfall(std::filesystem::path path);
NavigationView& nav_;
@@ -954,6 +954,7 @@ class SetThemeView : public View {
{"Aqua", 2},
{"Green", 3},
{"Red", 4},
{"Dark", 5},
},
true};
+1
View File
@@ -84,6 +84,7 @@ void SubGhzDView::focus() {
SubGhzDView::SubGhzDView(NavigationView& nav)
: nav_{nav} {
add_children({&rssi,
&channel,
&field_rf_amp,
&field_lna,
&field_vga,
+3
View File
@@ -129,6 +129,9 @@ class SubGhzDView : public View {
{18 * 8, 0 * 16}};
RSSI rssi{
{21 * 8, 0, 6 * 8, 4}};
Channel channel{
{21 * 8, 5, 6 * 8, 4},
};
RxFrequencyField field_frequency{
{0 * 8, 0 * 16},
nav_};
+97 -19
View File
@@ -26,6 +26,7 @@
#include "log_file.hpp"
#include "string_format.hpp"
#include "irq_controls.hpp"
using namespace portapack;
namespace fs = std::filesystem;
@@ -81,21 +82,75 @@ void TextViewer::paint(Painter& painter) {
paint_cursor(painter);
}
void TextViewer::enable_long_press() {
// Enable long press on "Select".
SwitchesState config;
config[toUType(Switch::Sel)] = true;
set_switches_long_press_config(config);
}
void TextViewer::on_focus() {
enable_long_press();
}
bool TextViewer::on_key(const KeyEvent key) {
int16_t delta_col = 0;
int16_t delta_line = 0;
if (key == KeyEvent::Left)
delta_col = -1;
else if (key == KeyEvent::Right)
delta_col = 1;
else if (key == KeyEvent::Up)
delta_line = -1;
else if (key == KeyEvent::Down)
delta_line = 1;
else if (key == KeyEvent::Select && on_select) {
on_select();
return true;
if (key == KeyEvent::Select) {
// Toggle 'digit' mode with long-press.
if (digit_mode_ || key_is_long_pressed(key)) {
digit_mode_ = !digit_mode_;
set_dirty();
return true;
}
}
if (digit_mode_) {
switch (key) {
case KeyEvent::Left:
delta_col = -1;
break;
case KeyEvent::Right:
delta_col = 1;
break;
case KeyEvent::Up:
set_value(value_ == 0x0F ? 0x00 : value_ + 1);
break;
case KeyEvent::Down:
set_value(value_ == 0x00 ? 0x0F : value_ - 1);
break;
default:
return false;
}
if (delta_col == 0 && delta_line == 0) {
return true;
}
} else {
switch (key) {
case KeyEvent::Left:
delta_col = -1;
break;
case KeyEvent::Right:
delta_col = 1;
break;
case KeyEvent::Up:
delta_line = -1;
break;
case KeyEvent::Down:
delta_line = 1;
break;
case KeyEvent::Select: {
if (on_select) {
on_select();
return true;
}
break;
}
default:
return false;
}
}
// Always allow cursor direction to be updated.
@@ -111,19 +166,42 @@ bool TextViewer::on_key(const KeyEvent key) {
bool TextViewer::on_encoder(EncoderEvent delta) {
bool updated = false;
if (cursor_.dir == ScrollDirection::Horizontal)
updated = apply_scrolling_constraints(0, delta);
else {
delta *= 16;
updated = apply_scrolling_constraints(delta, 0);
if (digit_mode_) {
if (delta > 0) {
set_value(value_ == 0x0F ? 0x00 : value_ + 1);
} else if (delta < 0) {
set_value(value_ == 0x00 ? 0x0F : value_ - 1);
}
updated = true;
} else {
if (cursor_.dir == ScrollDirection::Horizontal) {
updated = apply_scrolling_constraints(0, delta);
} else {
delta *= 16;
updated = apply_scrolling_constraints(delta, 0);
}
if (updated) {
redraw();
}
}
if (updated)
redraw();
return updated;
}
void TextViewer::set_value(uint8_t new_value) {
if (new_value != value_) {
value_ = new_value;
if (on_change) {
on_change(value_);
}
set_dirty();
}
}
void TextViewer::redraw(bool redraw_text, bool redraw_marked) {
paint_state_.redraw_text = redraw_text;
paint_state_.redraw_marked = redraw_marked;
@@ -54,10 +54,13 @@ class TextViewer : public Widget {
std::function<void()> on_select{};
std::function<void()> on_cursor_moved{};
std::function<void(uint8_t)> on_change{};
void paint(Painter& painter) override;
bool on_key(KeyEvent key) override;
bool on_encoder(EncoderEvent delta) override;
void set_value(uint8_t new_value);
void on_focus() override;
void redraw(bool redraw_text = false, bool redraw_marked = false);
@@ -74,6 +77,7 @@ class TextViewer : public Widget {
void cursor_set(uint16_t line, uint16_t col);
void cursor_mark_selected();
void cursor_clear_marked();
void enable_long_press();
typedef std::pair<uint16_t, uint16_t> LineColPair;
std::vector<LineColPair> lineColPair{};
@@ -95,6 +99,9 @@ class TextViewer : public Widget {
int8_t char_height{};
uint8_t max_line{};
uint8_t max_col{};
bool digit_mode_{false};
uint8_t value_{0};
bool allow_digit_mode_{true};
/* Returns true if the cursor was updated. */
bool apply_scrolling_constraints(
@@ -107,6 +107,7 @@ void WeatherView::focus() {
WeatherView::WeatherView(NavigationView& nav)
: nav_{nav} {
add_children({&rssi,
&channel,
&field_rf_amp,
&field_lna,
&field_vga,
@@ -147,6 +147,9 @@ class WeatherView : public View {
{18 * 8, 0 * 16}};
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}};
+7 -6
View File
@@ -186,13 +186,14 @@ void set_nrf(const uint32_t baudrate, const uint32_t word_length, const uint32_t
send_message(&message);
}
void set_fsk(const size_t deviation) {
void set_fsk(const uint8_t samplesPerSymbol, const uint32_t syncWord, const uint8_t syncWordLength, const uint32_t preamble, const uint8_t preambleLength, uint16_t numDataBytes) {
const FSKRxConfigureMessage message{
taps_200k_decim_0,
taps_16k0_decim_1,
taps_11k0_channel,
2,
deviation};
samplesPerSymbol,
syncWord,
syncWordLength,
preamble,
preambleLength,
numDataBytes};
send_message(&message);
}
+1 -1
View File
@@ -80,7 +80,7 @@ void set_pitch_rssi(int32_t avg, bool enabled);
void set_afsk_data(const uint32_t afsk_samples_per_bit, const uint32_t afsk_phase_inc_mark, const uint32_t afsk_phase_inc_space, const uint8_t afsk_repeat, const uint32_t afsk_bw, const uint8_t symbol_count);
void kill_afsk();
void set_afsk(const uint32_t baudrate, const uint32_t word_length, const uint32_t trigger_value, const bool trigger_word);
void set_fsk(const size_t deviation);
void set_fsk(const uint32_t samplesPerSymbol, const uint32_t syncWord, const uint8_t syncWordLength, const uint32_t preamble, const uint8_t preambleLength, uint16_t numDataBytes);
void set_aprs(const uint32_t baudrate);
void set_btlerx(uint8_t channel_number);
-38
View File
@@ -3689,44 +3689,6 @@ static constexpr Bitmap bitmap_icon_sonde{
{16, 16},
bitmap_icon_sonde_data};
static constexpr uint8_t bitmap_icon_soundboard_data[] = {
0xF0,
0x0F,
0x1C,
0x18,
0x17,
0x38,
0x15,
0x78,
0x15,
0xF8,
0x15,
0x82,
0x15,
0x8B,
0xD5,
0x83,
0xD5,
0xBB,
0xD5,
0x83,
0x15,
0x8B,
0x15,
0x92,
0x15,
0xA0,
0x17,
0x80,
0x1C,
0x80,
0xF0,
0xFF,
};
static constexpr Bitmap bitmap_icon_soundboard{
{16, 16},
bitmap_icon_soundboard_data};
static constexpr uint8_t bitmap_icon_speaker_data[] = {
0x00,
0x00,
+11
View File
@@ -2,6 +2,7 @@
* Copyright (C) 2015 Jared Boone, ShareBrained Technology, Inc.
* Copyright (C) 2016 Furrtek
* Copyright (C) 2022 Arjan Onwezen
* Copyright (C) 2025 Tommaso Ventafridda
*
* This file is part of PortaPack.
*
@@ -56,6 +57,16 @@ int database::retrieve_aircraft_record(AircraftDBRecord* record, std::string sea
return (result);
}
int database::retrieve_macaddress_record(MacAddressDBRecord* record, std::string search_term) {
file_path = macaddress_dir / u"macaddress.db";
index_item_length = 7;
record_length = 64;
result = retrieve_record(file_path, index_item_length, record_length, record, search_term);
return (result);
}
int database::retrieve_record(std::filesystem::path file_path, int index_item_length, int record_length, void* record, std::string search_term) {
if (search_term.empty())
return DATABASE_RECORD_NOT_FOUND;
+8 -1
View File
@@ -2,6 +2,7 @@
* Copyright (C) 2015 Jared Boone, ShareBrained Technology, Inc.
* Copyright (C) 2016 Furrtek
* Copyright (C) 2022 Arjan Onwezen
* Copyright (C) 2025 Tommaso Ventafridda
*
* This file is part of PortaPack.
*
@@ -60,6 +61,12 @@ class database {
int retrieve_aircraft_record(AircraftDBRecord* record, std::string search_term);
struct MacAddressDBRecord {
char vendor_name[64]; // vendor/manufacturer name
};
int retrieve_macaddress_record(MacAddressDBRecord* record, std::string search_term);
private:
std::filesystem::path file_path = ""; // path including filename
int index_item_length = 0; // length of index item
@@ -77,4 +84,4 @@ class database {
int retrieve_record(std::filesystem::path file_path, int index_item_length, int record_length, void* record, std::string search_term);
};
#endif /*__DATABASE_H__*/
#endif /*__DATABASE_H__*/
+55
View File
@@ -0,0 +1,55 @@
/*
* ------------------------------------------------------------
* | Made by RocketGod |
* | Find me at https://betaskynet.com |
* | Argh matey! |
* ------------------------------------------------------------
*/
#include "ui.hpp"
#include "ui_battleship.hpp"
#include "ui_navigation.hpp"
#include "external_app.hpp"
namespace ui::external_app::battleship {
void initialize_app(ui::NavigationView& nav) {
nav.push<BattleshipView>();
}
} // namespace ui::external_app::battleship
extern "C" {
__attribute__((section(".external_app.app_battleship.application_information"), used)) application_information_t _application_information_battleship = {
/*.memory_location = */ (uint8_t*)0x00000000,
/*.externalAppEntry = */ ui::external_app::battleship::initialize_app,
/*.header_version = */ CURRENT_HEADER_VERSION,
/*.app_version = */ VERSION_MD5,
/*.app_name = */ "Battleship",
/*.bitmap_data = */ {
// Pirate galleon - 16x16
0x80, 0x00, // ........#.......
0x80, 0x00, // ........#.......
0x80, 0x00, // ........#.......
0xC0, 0x01, // .......###......
0xE0, 0x03, // ......#####.....
0xF0, 0x07, // .....#######....
0xF8, 0x0F, // ....#########...
0xFC, 0x1F, // ...###########..
0xFE, 0x3F, // ..#############.
0x00, 0x01, // .......#........
0x00, 0x01, // .......#........
0x00, 0x01, // .......#........
0xFC, 0x3F, // ..############..
0xFE, 0x7F, // .##############.
0xFF, 0xFF, // ################
0xFC, 0x3F, // ..############..
},
/*.icon_color = */ ui::Color::blue().v,
/*.menu_location = */ app_location_t::GAMES,
/*.desired_menu_position = */ -1,
/*.m4_app_tag = */ {'P', 'P', 'O', '2'}, // Use POCSAG2 baseband (larger than FSKTX)
/*.m4_app_offset = */ 0x00000000, // will be filled at compile time
};
}
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,250 @@
/*
* ------------------------------------------------------------
* | Made by RocketGod |
* | Find me at https://betaskynet.com |
* | Argh matey! |
* ------------------------------------------------------------
*/
#ifndef __UI_BATTLESHIP_H__
#define __UI_BATTLESHIP_H__
#include "ui.hpp"
#include "ui_navigation.hpp"
#include "ui_receiver.hpp"
#include "ui_transmitter.hpp"
#include "app_settings.hpp"
#include "radio_state.hpp"
#include "baseband_api.hpp"
#include "string_format.hpp"
#include "audio.hpp"
#include "message.hpp"
#include "pocsag.hpp"
#include <array>
namespace ui::external_app::battleship {
constexpr uint8_t GRID_SIZE = 10;
constexpr uint8_t CELL_SIZE = 24;
constexpr uint8_t GRID_OFFSET_X = 0;
constexpr uint8_t GRID_OFFSET_Y = 32;
enum class ShipType : uint8_t {
CARRIER = 5,
BATTLESHIP = 4,
CRUISER = 3,
SUBMARINE = 3,
DESTROYER = 2
};
enum class GameState : uint8_t {
MENU,
PLACING_SHIPS,
WAITING_FOR_OPPONENT,
MY_TURN,
OPPONENT_TURN,
GAME_OVER
};
enum class CellState : uint8_t {
EMPTY,
SHIP,
HIT,
MISS,
SUNK
};
enum class MessageType : uint8_t {
READY,
SHOT,
HIT,
MISS,
SUNK,
WIN
};
struct Ship {
ShipType type;
uint8_t x;
uint8_t y;
bool horizontal;
uint8_t hits;
bool placed;
uint8_t size() const {
return static_cast<uint8_t>(type);
}
bool is_sunk() const {
return hits >= size();
}
};
struct GameMessage {
MessageType type;
uint8_t x;
uint8_t y;
};
class BattleshipView : public View {
public:
BattleshipView(NavigationView& nav);
~BattleshipView();
void focus() override;
void paint(Painter& painter) override;
bool on_touch(const TouchEvent event) override;
bool on_encoder(const EncoderEvent delta) override;
bool on_key(const KeyEvent key) override;
std::string title() const override { return "Battleship"; }
private:
NavigationView& nav_;
RxRadioState rx_radio_state_{
433920000 /* frequency */,
1750000 /* bandwidth */,
2280000 /* sampling rate */
};
TxRadioState tx_radio_state_{
433920000 /* frequency */,
1750000 /* bandwidth */,
2280000 /* sampling rate */
};
// Settings
bool sound_enabled{true};
bool rf_amp_enabled{false};
uint8_t lna_gain{24};
uint8_t vga_gain{24};
uint8_t tx_gain{35};
app_settings::SettingsManager settings_{
"battleship",
app_settings::Mode::RX_TX,
{{"rx_freq"sv, &rx_frequency},
{"tx_freq"sv, &tx_frequency},
{"rf_amp"sv, &rf_amp_enabled}}};
GameState game_state{GameState::MENU};
bool is_red_team{false};
bool opponent_ready{false};
uint8_t wins{0};
uint8_t losses{0};
std::array<std::array<CellState, GRID_SIZE>, GRID_SIZE> my_grid{};
std::array<std::array<CellState, GRID_SIZE>, GRID_SIZE> enemy_grid{};
std::string current_status{"Choose your team!"};
std::string current_score{"W:0 L:0"};
std::array<Ship, 5> my_ships{};
uint8_t current_ship_index{0};
bool placing_horizontal{true};
uint8_t ships_remaining{5};
uint8_t enemy_ships_remaining{5};
uint8_t cursor_x{0};
uint8_t cursor_y{0};
uint8_t target_x{0};
uint8_t target_y{0};
bool touch_enabled{true};
uint32_t tx_frequency{433920000};
uint32_t rx_frequency{433920000};
bool is_transmitting{false};
// POCSAG decoding state
pocsag::EccContainer ecc{};
pocsag::POCSAGState pocsag_state{&ecc};
uint32_t last_address{0};
// UI Elements - Menu/Settings Screen
Text text_title{{60, 2, 120, 24}, "BATTLESHIP"};
Text text_subtitle{{40, 20, 160, 16}, "Naval Combat Game"};
Rectangle rect_radio_settings{{12, 40, 216, 118}, Color::dark_grey()};
Text label_radio{{17, 45, 100, 16}, "RADIO SETUP"};
ButtonWithEncoder button_frequency{{17, 65, 11 * 8, 20}, ""};
// Radio controls
Text label_rf_amp{{17, 90, 35, 16}, "AMP:"};
Checkbox checkbox_rf_amp{{55, 90}, 3, "", false};
Text label_lna{{17, 118, 30, 16}, "LNA:"};
NumberField field_lna{{50, 118}, 2, {0, 40}, 8, ' '};
Text label_vga{{90, 118, 30, 16}, "VGA:"};
NumberField field_vga{{125, 118}, 2, {0, 62}, 8, ' '};
Text label_tx_gain{{155, 118, 25, 16}, "TX:"};
NumberField field_tx_gain{{185, 118}, 2, {0, 47}, 8, ' '};
Rectangle rect_audio_settings{{12, 164, 216, 45}, Color::dark_grey()};
Text label_audio{{17, 169, 80, 16}, "AUDIO"};
Checkbox checkbox_sound{{17, 187}, 8, "Sound On", true};
Text label_volume{{110, 187, 50, 16}, "Volume:"};
AudioVolumeField field_volume{{165, 187}};
Rectangle rect_team_selection{{12, 217, 216, 75}, Color::dark_grey()};
Text label_team{{17, 222, 110, 16}, "SELECT TEAM"};
Button button_red_team{{25, 242, 85, 45}, "RED\nTEAM"};
Button button_blue_team{{130, 242, 85, 45}, "BLUE\nTEAM"};
// In-game UI elements
RSSI rssi{{21 * 8, 0, 6 * 8, 4}};
Text text_status{{10, 16, 220, 16}, ""};
Text text_score{{170, 16, 60, 16}, ""};
Button button_rotate{{10, 265, 65, 32}, "Rotate"};
Button button_place{{82, 265, 65, 32}, "Place"};
Button button_fire{{82, 265, 65, 32}, "Fire!"};
Button button_menu{{155, 265, 65, 32}, "Menu"};
// Methods
void init_game();
void reset_game();
void start_team(bool red);
void setup_ships();
void draw_menu_screen(Painter& painter);
void draw_grid(Painter& painter, uint8_t grid_x, uint8_t grid_y, const std::array<std::array<CellState, GRID_SIZE>, GRID_SIZE>& grid, bool show_ships, bool is_offense_grid = false);
void draw_cell(Painter& painter, uint8_t cell_x, uint8_t cell_y, CellState state, bool show_ships, bool is_offense_grid, bool is_cursor);
void draw_ship_preview(Painter& painter);
void place_ship();
bool can_place_ship(uint8_t x, uint8_t y, uint8_t size, bool horizontal);
void fire_at_position();
void process_shot(uint8_t x, uint8_t y);
void update_score();
bool is_cursor_at(uint8_t x, uint8_t y, bool is_offense_grid);
void send_message(const GameMessage& msg);
void process_message(const std::string& message);
bool parse_coords(const std::string& coords, uint8_t& x, uint8_t& y);
void mark_ship_sunk(uint8_t x, uint8_t y, std::array<std::array<CellState, GRID_SIZE>, GRID_SIZE>& grid);
void configure_radio_tx();
void configure_radio_rx();
MessageHandlerRegistration message_handler_packet{
Message::ID::POCSAGPacket,
[this](Message* const p) {
const auto message = static_cast<const POCSAGPacketMessage*>(p);
on_pocsag_packet(message);
}};
MessageHandlerRegistration message_handler_tx_progress{
Message::ID::TXProgress,
[this](const Message* const p) {
const auto message = static_cast<const TXProgressMessage*>(p);
on_tx_progress(message->progress, message->done);
}};
void on_pocsag_packet(const POCSAGPacketMessage* message);
void on_tx_progress(const uint32_t progress, const bool done);
};
} // namespace ui::external_app::battleship
#endif
+53
View File
@@ -0,0 +1,53 @@
/*
* Blackjack Game for Portapack Mayhem
* Ported / Enhanced / Graphically made awesome by RocketGod (https://betaskynet.com)
* Based on BlackJack 83 for TI Calculator by Harper Maddox (was written in Assembly)
*/
#include "ui.hpp"
#include "ui_blackjack.hpp"
#include "ui_navigation.hpp"
#include "external_app.hpp"
namespace ui::external_app::blackjack {
void initialize_app(ui::NavigationView& nav) {
nav.push<BlackjackView>();
}
} // namespace ui::external_app::blackjack
extern "C" {
__attribute__((section(".external_app.app_blackjack.application_information"), used)) application_information_t _application_information_blackjack = {
(uint8_t*)0x00000000,
ui::external_app::blackjack::initialize_app,
CURRENT_HEADER_VERSION,
VERSION_MD5,
"Blackjack",
{
// Diamond icon 16x16
0x00, 0x00, // ................
0x80, 0x01, // .......##.......
0xC0, 0x03, // ......####......
0xE0, 0x07, // .....######.....
0xF0, 0x0F, // ....########....
0xF8, 0x1F, // ...##########...
0xFC, 0x3F, // ..############..
0xFE, 0x7F, // .##############.
0xFE, 0x7F, // .##############.
0xFC, 0x3F, // ..############..
0xF8, 0x1F, // ...##########...
0xF0, 0x0F, // ....########....
0xE0, 0x07, // .....######.....
0xC0, 0x03, // ......####......
0x80, 0x01, // .......##.......
0x00, 0x00, // ................
},
ui::Color::red().v, // Red color for diamonds
app_location_t::GAMES,
-1,
{0, 0, 0, 0},
0x00000000,
};
}
+727
View File
@@ -0,0 +1,727 @@
/*
* Blackjack Game for Portapack Mayhem
* Ported / Enhanced / Graphically made awesome by RocketGod (https://betaskynet.com)
* Based on BlackJack 83 for TI Calculator by Harper Maddox (was written in Assembly)
*/
#include "ui_blackjack.hpp"
namespace ui::external_app::blackjack {
// Global variables
static BlackjackView* current_instance = nullptr;
static Callback game_update_callback = nullptr;
static uint32_t game_update_timeout = 0;
static uint32_t game_update_counter = 0;
static Painter painter;
void check_game_timer() {
if (game_update_callback) {
if (++game_update_counter >= game_update_timeout) {
game_update_counter = 0;
game_update_callback();
}
}
}
void Ticker::attach(Callback func, double delay_sec) {
game_update_callback = func;
game_update_timeout = delay_sec * 60;
}
void Ticker::detach() {
game_update_callback = nullptr;
}
void game_timer_check() {
if (current_instance) {
current_instance->update_game();
}
}
BlackjackView::BlackjackView(NavigationView& nav)
: nav_{nav}, game_timer{} {
add_children({&dummy});
current_instance = this;
game_timer.attach(&game_timer_check, 1.0 / 60.0);
}
BlackjackView::~BlackjackView() {
current_instance = nullptr;
}
void BlackjackView::on_show() {
draw_menu_static();
}
void BlackjackView::paint(Painter& painter) {
(void)painter;
if (!initialized) {
initialized = true;
std::srand(LPC_RTC->CTIME0);
init_deck();
}
}
void BlackjackView::frame_sync() {
check_game_timer();
set_dirty();
}
void BlackjackView::clear_screen() {
painter.fill_rectangle({0, 0, 240, 320}, Color::black());
}
void BlackjackView::init_deck() {
// Initialize deck with card values 0-51
// 0-12 = Spades (A-K), 13-25 = Hearts, 26-38 = Diamonds, 39-51 = Clubs
for (int i = 0; i < 52; i++) {
deck[i] = i;
}
shuffle_deck();
}
void BlackjackView::shuffle_deck() {
// Simple shuffle using random swaps
for (int i = 51; i > 0; i--) {
int j = rand() % (i + 1);
uint8_t temp = deck[i];
deck[i] = deck[j];
deck[j] = temp;
}
deck_position = 0;
}
uint8_t BlackjackView::draw_card() {
if (deck_position >= 52) {
shuffle_deck();
}
return deck[deck_position++];
}
int BlackjackView::get_card_value(uint8_t card) {
int value = (card % 13) + 1; // 1-13
if (value > 10) value = 10; // Face cards worth 10
return value;
}
uint8_t BlackjackView::get_card_suit(uint8_t card) {
return card / 13; // 0=Spades, 1=Hearts, 2=Diamonds, 3=Clubs
}
std::string BlackjackView::get_card_string(uint8_t card) {
int value = (card % 13) + 1;
std::string result;
if (value == 1)
result = "A";
else if (value == 11)
result = "J";
else if (value == 12)
result = "Q";
else if (value == 13)
result = "K";
else if (value == 10)
result = "10"; // Special case for 10
else
result = std::to_string(value);
return result;
}
int BlackjackView::calculate_hand_value(uint8_t* cards, uint8_t count) {
int value = 0;
int aces = 0;
for (uint8_t i = 0; i < count; i++) {
int card_val = get_card_value(cards[i]);
if (card_val == 1) {
aces++;
value += 1;
} else {
value += card_val;
}
}
// Convert aces from 1 to 11 if beneficial
while (aces > 0 && value + 10 <= 21) {
value += 10;
aces--;
}
return value;
}
void BlackjackView::deal_cards() {
// Clear hands
player_card_count = 0;
dealer_card_count = 0;
// Deal initial cards
player_cards[player_card_count++] = draw_card();
dealer_cards[dealer_card_count++] = draw_card();
player_cards[player_card_count++] = draw_card();
dealer_cards[dealer_card_count++] = draw_card();
dealer_hidden = true;
game_state = GameState::PLAYING;
}
void BlackjackView::player_hit() {
if (player_card_count < MAX_CARDS_IN_HAND) {
player_cards[player_card_count++] = draw_card();
int value = calculate_hand_value(player_cards, player_card_count);
if (value > 21) {
// Bust!
cash = (cash >= bet) ? cash - bet : 0;
losses++;
game_state = GameState::GAME_OVER;
}
}
}
void BlackjackView::player_stay() {
dealer_hidden = false;
game_state = GameState::DEALER_TURN;
dealer_timer = 0;
}
void BlackjackView::dealer_turn() {
int dealer_value = calculate_hand_value(dealer_cards, dealer_card_count);
if (dealer_value < 17 && dealer_card_count < MAX_CARDS_IN_HAND) {
dealer_cards[dealer_card_count++] = draw_card();
} else {
check_game_over();
}
}
void BlackjackView::check_game_over() {
int player_value = calculate_hand_value(player_cards, player_card_count);
int dealer_value = calculate_hand_value(dealer_cards, dealer_card_count);
if (player_value > 21) {
// Player bust (already handled)
} else if (dealer_value > 21) {
// Dealer bust - player wins
cash += bet;
wins++;
} else if (player_value > dealer_value) {
// Player wins
cash += bet;
wins++;
} else if (player_value < dealer_value) {
// Dealer wins
cash = (cash >= bet) ? cash - bet : 0;
losses++;
}
// Else it's a tie - no money changes hands
if (cash > high_score) {
high_score = cash;
}
game_state = GameState::GAME_OVER;
}
void BlackjackView::update_game() {
switch (game_state) {
case GameState::MENU:
draw_menu();
break;
case GameState::BETTING:
draw_betting();
break;
case GameState::PLAYING:
draw_game();
break;
case GameState::DEALER_TURN:
if (++dealer_timer >= 60) { // 1 second delay
dealer_timer = 0;
dealer_turn();
}
draw_game();
break;
case GameState::GAME_OVER:
draw_game();
break;
case GameState::STATS:
draw_stats();
break;
}
}
void BlackjackView::draw_menu_static() {
clear_screen();
auto style_title = *ui::Theme::getInstance()->fg_green;
auto style_text = *ui::Theme::getInstance()->fg_light;
auto style_rules = *ui::Theme::getInstance()->fg_cyan;
painter.draw_string({84, 20}, style_title, "BLACKJACK");
// Draw rules
painter.draw_string({70, 55}, style_rules, "-- RULES --");
painter.draw_string({61, 75}, style_text, "Get close to 21");
painter.draw_string({61, 90}, style_text, "without going over");
painter.draw_string({61, 110}, style_text, "Dealer hits on 16");
painter.draw_string({61, 125}, style_text, "Dealer stays on 17");
painter.draw_string({61, 145}, style_text, "Blackjack pays 1:1");
// Controls
painter.draw_string({65, 175}, style_rules, "-- CONTROLS --");
painter.draw_string({61, 195}, style_text, "SELECT: Start/Hit");
painter.draw_string({61, 210}, style_text, "LEFT: Stats");
painter.draw_string({61, 225}, style_text, "RIGHT: Exit/Stay");
// Draw high score
painter.draw_string({61, 250}, style_text, "High Score: $" + std::to_string(high_score));
}
void BlackjackView::draw_menu() {
// Only handle the flashing text animation
if (++blink_counter >= 30) {
blink_counter = 0;
blink_state = !blink_state;
if (blink_state) {
auto style = *ui::Theme::getInstance()->fg_yellow;
painter.draw_string({55, 280}, style, "* PRESS SELECT *");
} else {
// Clear just the text area
painter.fill_rectangle({55, 278, 130, 20}, Color::black());
}
}
}
void BlackjackView::draw_stats() {
clear_screen();
auto style_title = *ui::Theme::getInstance()->fg_green;
auto style_text = *ui::Theme::getInstance()->fg_light;
auto style_value = *ui::Theme::getInstance()->fg_yellow;
painter.draw_string({75, 30}, style_title, "STATISTICS");
painter.draw_string({30, 80}, style_text, "Wins:");
painter.draw_string({150, 80}, style_value, std::to_string(wins));
painter.draw_string({30, 100}, style_text, "Losses:");
painter.draw_string({150, 100}, style_value, std::to_string(losses));
// Win percentage
uint32_t total = wins + losses;
if (total > 0) {
uint32_t win_pct = (wins * 100) / total;
painter.draw_string({30, 120}, style_text, "Win %:");
painter.draw_string({150, 120}, style_value, std::to_string(win_pct) + "%");
}
painter.draw_string({30, 160}, style_text, "High Score:");
painter.draw_string({150, 160}, style_value, "$" + std::to_string(high_score));
painter.draw_string({30, 180}, style_text, "Cash:");
painter.draw_string({150, 180}, style_value, "$" + std::to_string(cash));
painter.draw_string({40, 250}, style_text, "SELECT: Back");
}
void BlackjackView::draw_betting() {
static uint32_t last_bet = 0;
static bool betting_drawn = false;
if (!betting_drawn) {
clear_screen();
auto style_title = *ui::Theme::getInstance()->fg_green;
auto style_text = *ui::Theme::getInstance()->fg_light;
painter.draw_string({70, 40}, style_title, "PLACE BET");
painter.draw_string({30, 80}, style_text, "Cash: $" + std::to_string(cash));
painter.draw_string({30, 140}, style_text, "ENCODER: +/- $10");
painter.draw_string({30, 160}, style_text, "SELECT: Deal");
betting_drawn = true;
last_bet = 0;
}
// Update bet display
if (bet != last_bet) {
painter.fill_rectangle({30, 110, 150, 20}, Color::black());
painter.draw_string({30, 110}, *ui::Theme::getInstance()->fg_yellow, "Bet: $" + std::to_string(bet));
last_bet = bet;
}
}
void BlackjackView::draw_game() {
static int last_player_count = -1;
static int last_dealer_count = -1;
static GameState last_game_state = GameState::MENU;
static uint32_t last_bet = 0;
// Clear and redraw if hands changed or game state changed
if (player_card_count != last_player_count || dealer_card_count != last_dealer_count || game_state != last_game_state) {
clear_screen();
auto style = *ui::Theme::getInstance()->fg_green;
painter.draw_string({10, 10}, style, "Cash: $" + std::to_string(cash));
painter.draw_string({140, 10}, style, "Bet: $" + std::to_string(bet));
// Draw dealer hand with value next to label
auto style_value = *ui::Theme::getInstance()->fg_yellow;
painter.draw_string({10, 45}, *ui::Theme::getInstance()->fg_light, "Dealer:");
if (!dealer_hidden || game_state == GameState::GAME_OVER) {
int dealer_value = calculate_hand_value(dealer_cards, dealer_card_count);
painter.draw_string({70, 45}, style_value, "(" + std::to_string(dealer_value) + ")");
}
draw_hand(10, 65, dealer_cards, dealer_card_count, true);
// Draw player hand with value next to label
painter.draw_string({10, 165}, *ui::Theme::getInstance()->fg_light, "You:");
int player_value = calculate_hand_value(player_cards, player_card_count);
painter.draw_string({50, 165}, style_value, "(" + std::to_string(player_value) + ")");
draw_hand(10, 185, player_cards, player_card_count, false);
// Draw controls or result
if (game_state == GameState::PLAYING) {
auto style_text = *ui::Theme::getInstance()->fg_light;
painter.draw_string({30, 290}, style_text, "SELECT: Hit");
painter.draw_string({130, 290}, style_text, "RIGHT: Stay");
} else if (game_state == GameState::GAME_OVER) {
const Style* style_result = ui::Theme::getInstance()->fg_yellow;
std::string result;
if (player_value > 21) {
result = "BUST! You Lose";
style_result = ui::Theme::getInstance()->fg_red;
} else if (calculate_hand_value(dealer_cards, dealer_card_count) > 21) {
result = "Dealer Bust! Win!";
style_result = ui::Theme::getInstance()->fg_green;
} else if (player_value > calculate_hand_value(dealer_cards, dealer_card_count)) {
result = "You Win!";
style_result = ui::Theme::getInstance()->fg_green;
} else if (player_value < calculate_hand_value(dealer_cards, dealer_card_count)) {
result = "You Lose";
style_result = ui::Theme::getInstance()->fg_red;
} else {
result = "Push (Tie)";
}
// Draw result
painter.draw_string({60, 270}, *style_result, result);
// Draw compact bet selector in top right area
auto style_bet = *ui::Theme::getInstance()->fg_cyan;
painter.draw_string({140, 25}, style_bet, "Next: $" + std::to_string(bet));
// Show controls
painter.draw_string({10, 290}, *ui::Theme::getInstance()->fg_light, "SELECT: Deal ENC: +/-");
}
last_player_count = player_card_count;
last_dealer_count = dealer_card_count;
last_game_state = game_state;
last_bet = bet;
} else if (game_state == GameState::GAME_OVER && bet != last_bet) {
// Only update the bet display when it changes
painter.fill_rectangle({140, 25, 90, 16}, Color::black());
painter.draw_string({140, 25}, *ui::Theme::getInstance()->fg_cyan, "Next: $" + std::to_string(bet));
last_bet = bet;
}
}
void BlackjackView::draw_suit_symbol(int x, int y, uint8_t suit, Color color, bool large) {
if (large) {
// Large suit symbols for center of card
switch (suit) {
case 0: // Spades
// Top curve
painter.fill_rectangle({x + 9, y + 4, 2, 2}, color);
painter.fill_rectangle({x + 8, y + 5, 4, 2}, color);
painter.fill_rectangle({x + 7, y + 6, 6, 2}, color);
painter.fill_rectangle({x + 6, y + 7, 8, 2}, color);
painter.fill_rectangle({x + 5, y + 8, 10, 2}, color);
painter.fill_rectangle({x + 4, y + 9, 12, 2}, color);
painter.fill_rectangle({x + 3, y + 10, 14, 2}, color);
painter.fill_rectangle({x + 2, y + 11, 16, 2}, color);
painter.fill_rectangle({x + 1, y + 12, 18, 2}, color);
painter.fill_rectangle({x + 0, y + 13, 20, 3}, color);
painter.fill_rectangle({x + 1, y + 16, 18, 2}, color);
painter.fill_rectangle({x + 2, y + 17, 16, 1}, color);
painter.fill_rectangle({x + 3, y + 18, 14, 1}, color);
// Stem
painter.fill_rectangle({x + 9, y + 19, 2, 4}, color);
painter.fill_rectangle({x + 8, y + 22, 4, 1}, color);
painter.fill_rectangle({x + 7, y + 23, 6, 1}, color);
painter.fill_rectangle({x + 6, y + 24, 8, 1}, color);
break;
case 1: // Hearts
// Left bump
painter.fill_rectangle({x + 3, y + 5, 4, 2}, color);
painter.fill_rectangle({x + 2, y + 6, 6, 2}, color);
painter.fill_rectangle({x + 1, y + 7, 8, 3}, color);
painter.fill_rectangle({x + 0, y + 9, 9, 3}, color);
// Right bump
painter.fill_rectangle({x + 13, y + 5, 4, 2}, color);
painter.fill_rectangle({x + 12, y + 6, 6, 2}, color);
painter.fill_rectangle({x + 11, y + 7, 8, 3}, color);
painter.fill_rectangle({x + 11, y + 9, 9, 3}, color);
// Body
painter.fill_rectangle({x + 1, y + 11, 18, 3}, color);
painter.fill_rectangle({x + 2, y + 14, 16, 2}, color);
painter.fill_rectangle({x + 3, y + 16, 14, 2}, color);
painter.fill_rectangle({x + 4, y + 18, 12, 1}, color);
painter.fill_rectangle({x + 5, y + 19, 10, 1}, color);
painter.fill_rectangle({x + 6, y + 20, 8, 1}, color);
painter.fill_rectangle({x + 7, y + 21, 6, 1}, color);
painter.fill_rectangle({x + 8, y + 22, 4, 1}, color);
painter.fill_rectangle({x + 9, y + 23, 2, 1}, color);
break;
case 2: // Diamonds
painter.fill_rectangle({x + 10, y + 3, 1, 1}, color);
painter.fill_rectangle({x + 9, y + 4, 3, 1}, color);
painter.fill_rectangle({x + 8, y + 5, 5, 1}, color);
painter.fill_rectangle({x + 7, y + 6, 7, 1}, color);
painter.fill_rectangle({x + 6, y + 7, 9, 1}, color);
painter.fill_rectangle({x + 5, y + 8, 11, 1}, color);
painter.fill_rectangle({x + 4, y + 9, 13, 1}, color);
painter.fill_rectangle({x + 3, y + 10, 15, 1}, color);
painter.fill_rectangle({x + 2, y + 11, 17, 1}, color);
painter.fill_rectangle({x + 1, y + 12, 19, 1}, color);
painter.fill_rectangle({x + 0, y + 13, 21, 1}, color);
painter.fill_rectangle({x + 1, y + 14, 19, 1}, color);
painter.fill_rectangle({x + 2, y + 15, 17, 1}, color);
painter.fill_rectangle({x + 3, y + 16, 15, 1}, color);
painter.fill_rectangle({x + 4, y + 17, 13, 1}, color);
painter.fill_rectangle({x + 5, y + 18, 11, 1}, color);
painter.fill_rectangle({x + 6, y + 19, 9, 1}, color);
painter.fill_rectangle({x + 7, y + 20, 7, 1}, color);
painter.fill_rectangle({x + 8, y + 21, 5, 1}, color);
painter.fill_rectangle({x + 9, y + 22, 3, 1}, color);
painter.fill_rectangle({x + 10, y + 23, 1, 1}, color);
break;
case 3: // Clubs
// Center circle
painter.fill_rectangle({x + 8, y + 8, 5, 1}, color);
painter.fill_rectangle({x + 7, y + 9, 7, 3}, color);
painter.fill_rectangle({x + 8, y + 12, 5, 1}, color);
// Left circle
painter.fill_rectangle({x + 3, y + 11, 4, 1}, color);
painter.fill_rectangle({x + 2, y + 12, 6, 3}, color);
painter.fill_rectangle({x + 3, y + 15, 4, 1}, color);
// Right circle
painter.fill_rectangle({x + 14, y + 11, 4, 1}, color);
painter.fill_rectangle({x + 13, y + 12, 6, 3}, color);
painter.fill_rectangle({x + 14, y + 15, 4, 1}, color);
// Connect circles
painter.fill_rectangle({x + 6, y + 13, 9, 2}, color);
// Stem
painter.fill_rectangle({x + 9, y + 16, 3, 4}, color);
painter.fill_rectangle({x + 8, y + 19, 5, 1}, color);
painter.fill_rectangle({x + 7, y + 20, 7, 1}, color);
painter.fill_rectangle({x + 6, y + 21, 9, 1}, color);
break;
}
} else {
// Small suit symbols
switch (suit) {
case 0: // Spades - small
painter.fill_rectangle({x + 2, y + 1, 3, 3}, color);
painter.fill_rectangle({x + 1, y + 3, 5, 2}, color);
painter.fill_rectangle({x + 3, y + 5, 1, 2}, color);
break;
case 1: // Hearts - small
painter.fill_rectangle({x + 1, y + 1, 2, 2}, color);
painter.fill_rectangle({x + 4, y + 1, 2, 2}, color);
painter.fill_rectangle({x + 1, y + 2, 5, 2}, color);
painter.fill_rectangle({x + 2, y + 4, 3, 1}, color);
painter.fill_rectangle({x + 3, y + 5, 1, 1}, color);
break;
case 2: // Diamonds - small
painter.fill_rectangle({x + 3, y + 1, 1, 1}, color);
painter.fill_rectangle({x + 2, y + 2, 3, 1}, color);
painter.fill_rectangle({x + 1, y + 3, 5, 1}, color);
painter.fill_rectangle({x + 2, y + 4, 3, 1}, color);
painter.fill_rectangle({x + 3, y + 5, 1, 1}, color);
break;
case 3: // Clubs - small
painter.fill_rectangle({x + 3, y + 1, 2, 2}, color);
painter.fill_rectangle({x + 1, y + 3, 2, 2}, color);
painter.fill_rectangle({x + 4, y + 3, 2, 2}, color);
painter.fill_rectangle({x + 3, y + 5, 2, 2}, color);
break;
}
}
}
void BlackjackView::draw_card(int x, int y, uint8_t card, bool hidden) {
const int width = 60;
const int height = 80;
// Draw card background
painter.fill_rectangle({x, y, width, height}, Color::white());
painter.draw_rectangle({x, y, width, height}, Color::black());
painter.draw_rectangle({x + 1, y + 1, width - 2, height - 2}, Color::grey()); // Inner border
if (hidden) {
// Draw card back pattern - diagonal lines
for (int i = 4; i < width - 4; i += 6) {
for (int j = 4; j < height - 4; j += 6) {
painter.fill_rectangle({x + i, y + j, 3, 3}, Color::blue());
painter.fill_rectangle({x + i + 3, y + j + 3, 3, 3}, Color::red());
}
}
} else {
// Draw card value
uint8_t suit = get_card_suit(card);
Color suit_color = (suit == 1 || suit == 2) ? Color::red() : Color::black();
const auto* base_style = ui::Theme::getInstance()->fg_light;
Style card_style{
.font = base_style->font,
.background = Color::white(),
.foreground = suit_color};
std::string value_str = get_card_string(card);
// Draw value in top-left corner
painter.draw_string({x + 4, y + 4}, card_style, value_str);
// Draw small suit symbol next to value
int suit_x = (value_str == "10") ? x + 20 : x + 12;
draw_suit_symbol(suit_x, y + 4, suit, suit_color, false);
// Draw value in bottom-right corner
int bottom_x = (value_str == "10") ? x + width - 24 : x + width - 16;
painter.draw_string({bottom_x, y + height - 18}, card_style, value_str);
// Draw small suit symbol in bottom-right
draw_suit_symbol(x + width - 10, y + height - 16, suit, suit_color, false);
// Draw large suit symbol in center
draw_suit_symbol(x + 20, y + 28, suit, suit_color, true);
}
}
void BlackjackView::draw_hand(int x, int y, uint8_t* cards, uint8_t count, bool is_dealer) {
// Calculate total width needed
const int card_width = 60;
const int overlap = 40; // Amount of overlap when cards need to fit
const int max_width = 230 - x; // Available width on screen
int spacing;
if (count == 1) {
spacing = 0;
} else if (count == 2) {
spacing = card_width + 5; // Small gap for 2 cards
} else {
// Calculate spacing to fit all cards
int total_overlap_width = card_width + (count - 1) * overlap;
if (total_overlap_width <= max_width) {
spacing = overlap;
} else {
// Need more overlap to fit
spacing = (max_width - card_width) / (count - 1);
}
}
for (uint8_t i = 0; i < count; i++) {
bool hide = is_dealer && dealer_hidden && i == 1;
int card_x = x + (i * spacing);
draw_card(card_x, y, cards[i], hide);
}
}
bool BlackjackView::on_key(const KeyEvent key) {
if (key == KeyEvent::Select) {
switch (game_state) {
case GameState::MENU:
if (cash < 10) {
cash = 100; // Reset if broke - maybe should provide https://gamblersanonymous.org/ link if they also lost their wife and house
wins = 0;
losses = 0;
}
game_state = GameState::BETTING;
break;
case GameState::BETTING:
deal_cards();
break;
case GameState::PLAYING:
player_hit();
break;
case GameState::GAME_OVER:
// Deal new hand with current bet
if (cash >= bet) {
deal_cards();
} else if (cash >= 10) {
// Not enough for current bet, reduce to what they can afford
bet = 10;
deal_cards();
} else {
// Broke, reset game
cash = 100;
wins = 0;
losses = 0;
game_state = GameState::MENU;
draw_menu_static();
}
break;
case GameState::STATS:
game_state = GameState::MENU;
draw_menu_static();
break;
default:
break;
}
return true;
} else if (key == KeyEvent::Left) {
if (game_state == GameState::MENU) {
game_state = GameState::STATS;
}
return true;
} else if (key == KeyEvent::Right) {
if (game_state == GameState::MENU) {
nav_.pop();
return true;
} else if (game_state == GameState::PLAYING) {
player_stay();
return true;
}
}
return false;
}
bool BlackjackView::on_encoder(const EncoderEvent delta) {
if (game_state == GameState::BETTING || game_state == GameState::GAME_OVER) {
if (delta > 0 && bet + 10 <= cash) {
bet += 10;
} else if (delta < 0 && bet >= 20) {
bet -= 10;
}
return true;
}
return false;
}
} // namespace ui::external_app::blackjack
+156
View File
@@ -0,0 +1,156 @@
/*
* Blackjack Game for Portapack Mayhem
* Ported / Enhanced / Graphically made awesome by RocketGod (https://betaskynet.com)
* Based on BlackJack 83 for TI Calculator by Harper Maddox (was written in Assembly)
*/
#ifndef __UI_BLACKJACK_H__
#define __UI_BLACKJACK_H__
#include "ui.hpp"
#include "ui_navigation.hpp"
#include "event_m0.hpp"
#include "message.hpp"
#include "irq_controls.hpp"
#include "random.hpp"
#include "lpc43xx_cpp.hpp"
#include "ui_widget.hpp"
#include "app_settings.hpp"
#include <string>
#include <vector>
namespace ui::external_app::blackjack {
// Game states
enum class GameState {
MENU,
BETTING,
PLAYING,
DEALER_TURN,
GAME_OVER,
STATS
};
// Card structure
struct Card {
uint8_t value; // 1-13 (Ace=1, Jack=11, Queen=12, King=13)
uint8_t suit; // 0=Spades, 1=Hearts, 2=Diamonds, 3=Clubs
Card()
: value(0), suit(0) {}
Card(uint8_t v, uint8_t s)
: value(v), suit(s) {}
};
// Timer class
using Callback = void (*)(void);
class Ticker {
public:
Ticker() = default;
void attach(Callback func, double delay_sec);
void detach();
};
// Function declarations
void check_game_timer();
void game_timer_check();
class BlackjackView : public View {
public:
BlackjackView(NavigationView& nav);
~BlackjackView();
void on_show() override;
std::string title() const override { return "Blackjack"; }
void focus() override { dummy.focus(); }
void paint(Painter& painter) override;
void frame_sync();
bool on_encoder(const EncoderEvent event) override;
bool on_key(KeyEvent key) override;
// Public for timer callback
GameState game_state = GameState::MENU;
void update_game();
private:
NavigationView& nav_;
bool initialized = false;
// Game variables
static constexpr uint8_t MAX_CARDS_IN_HAND = 11; // Maximum possible cards before bust
uint8_t deck[52];
uint8_t deck_position = 0;
uint8_t player_cards[MAX_CARDS_IN_HAND];
uint8_t player_card_count = 0;
uint8_t dealer_cards[MAX_CARDS_IN_HAND];
uint8_t dealer_card_count = 0;
// Game state variables
uint32_t cash = 100;
uint32_t bet = 10;
uint32_t wins = 0;
uint32_t losses = 0;
uint32_t high_score = 100;
bool dealer_hidden = true;
// UI state
uint8_t menu_selection = 0;
bool blink_state = true;
uint32_t blink_counter = 0;
uint32_t dealer_timer = 0;
// Timer
Ticker game_timer;
// Methods
void init_deck();
void shuffle_deck();
uint8_t draw_card();
void deal_cards();
int calculate_hand_value(uint8_t* cards, uint8_t count);
int get_card_value(uint8_t card);
uint8_t get_card_suit(uint8_t card);
std::string get_card_string(uint8_t card);
void player_hit();
void player_stay();
void dealer_turn();
void check_game_over();
void reset_game();
// Drawing methods
void draw_menu();
void draw_menu_static();
void draw_stats();
void draw_game();
void draw_betting();
void draw_card(int x, int y, uint8_t card, bool hidden = false);
void draw_hand(int x, int y, uint8_t* cards, uint8_t count, bool is_dealer = false);
void draw_suit_symbol(int x, int y, uint8_t suit, Color color, bool large);
void clear_screen();
// Settings
app_settings::SettingsManager settings_{
"blackjack",
app_settings::Mode::NO_RF,
{{"cash"sv, &cash},
{"wins"sv, &wins},
{"losses"sv, &losses},
{"highscore"sv, &high_score}}};
Button dummy{
{240, 0, 0, 0},
""};
MessageHandlerRegistration message_handler_frame_sync{
Message::ID::DisplayFrameSync,
[this](const Message* const) {
this->frame_sync();
}};
};
} // namespace ui::external_app::blackjack
#endif /* __UI_BLACKJACK_H__ */
+58
View File
@@ -0,0 +1,58 @@
/*
* ------------------------------------------------------------
* | Made by RocketGod |
* | Find me at https://betaskynet.com |
* | Argh matey! |
* ------------------------------------------------------------
*
* Chrome Dino Game for Portapack Mayhem
* Based on the original DinoGame by various contributors
*/
#include "ui.hpp"
#include "ui_dinogame.hpp"
#include "ui_navigation.hpp"
#include "external_app.hpp"
namespace ui::external_app::dinogame {
void initialize_app(ui::NavigationView& nav) {
nav.push<DinoGameView>();
}
} // namespace ui::external_app::dinogame
extern "C" {
__attribute__((section(".external_app.app_dinogame.application_information"), used)) application_information_t _application_information_dinogame = {
(uint8_t*)0x00000000,
ui::external_app::dinogame::initialize_app,
CURRENT_HEADER_VERSION,
VERSION_MD5,
"Dino Game",
{
// Cactus icon 16x16
0x00, 0x00, // ................
0x80, 0x01, // .......##.......
0x80, 0x01, // .......##.......
0x80, 0x01, // .......##.......
0x98, 0x19, // ...##..##..##...
0x98, 0x19, // ...##..##..##...
0x98, 0x19, // ...##..##..##...
0x98, 0x19, // ...##..##..##...
0xF8, 0x1F, // ...##########...
0xF0, 0x0F, // ....########....
0x80, 0x01, // .......##.......
0x80, 0x01, // .......##.......
0x80, 0x01, // .......##.......
0x80, 0x01, // .......##.......
0x80, 0x01, // .......##.......
0x00, 0x00, // ................
},
ui::Color::green().v,
app_location_t::GAMES,
-1,
{0, 0, 0, 0},
0x00000000,
};
}
+447
View File
@@ -0,0 +1,447 @@
/*
standing - 34x36
ducking - 46x24
*/
const uint16_t dino_default[] PROGMEM = {
0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, // 0x0010 (16)
0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, // 0x0020 (32)
0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, // 0x0030 (48)
0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, // 0x0040 (64)
0x528A, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, // 0x0050 (80)
0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, // 0x0060 (96)
0x528A, 0x528A, 0x528A, 0x528A, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, // 0x0070 (112)
0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0x528A, 0x528A, 0x528A, 0xFFFF, 0xFFFF, 0x528A, 0x528A, 0x528A, 0x528A, // 0x0080 (128)
0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, // 0x0090 (144)
0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0x528A, 0x528A, 0x528A, 0xFFFF, 0xFFFF, 0x528A, 0x528A, // 0x00A0 (160)
0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, // 0x00B0 (176)
0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, // 0x00C0 (192)
0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, // 0x00D0 (208)
0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0x528A, 0x528A, 0x528A, // 0x00E0 (224)
0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0xFFFF, 0xFFFF, 0xFFFF, // 0x00F0 (240)
0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0x528A, // 0x0100 (256)
0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0xFFFF, // 0x0110 (272)
0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, // 0x0120 (288)
0xFFFF, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, // 0x0130 (304)
0x528A, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, // 0x0140 (320)
0xFFFF, 0xFFFF, 0xFFFF, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, // 0x0150 (336)
0x528A, 0x528A, 0x528A, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, // 0x0160 (352)
0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0xFFFF, 0xFFFF, 0xFFFF, // 0x0170 (368)
0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, // 0x0180 (384)
0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, // 0x0190 (400)
0x528A, 0x528A, 0x528A, 0x528A, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, // 0x01A0 (416)
0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, // 0x01B0 (432)
0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0x528A, 0x528A, 0xFFFF, 0xFFFF, 0xFFFF, // 0x01C0 (448)
0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, // 0x01D0 (464)
0x528A, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0x528A, 0x528A, 0xFFFF, // 0x01E0 (480)
0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, // 0x01F0 (496)
0x528A, 0x528A, 0x528A, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0x528A, // 0x0200 (512)
0x528A, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, // 0x0210 (528)
0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, // 0x0220 (544)
0xFFFF, 0x528A, 0x528A, 0x528A, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, // 0x0230 (560)
0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, // 0x0240 (576)
0xFFFF, 0xFFFF, 0xFFFF, 0x528A, 0x528A, 0x528A, 0x528A, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0x528A, 0x528A, 0x528A, 0x528A, // 0x0250 (592)
0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0xFFFF, 0xFFFF, 0xFFFF, // 0x0260 (608)
0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0xFFFF, 0xFFFF, 0xFFFF, 0x528A, 0x528A, 0x528A, // 0x0270 (624)
0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0xFFFF, 0xFFFF, 0x528A, 0x528A, 0xFFFF, // 0x0280 (640)
0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, // 0x0290 (656)
0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0xFFFF, 0xFFFF, 0xFFFF, // 0x02A0 (672)
0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, // 0x02B0 (688)
0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0xFFFF, // 0x02C0 (704)
0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, // 0x02D0 (720)
0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, // 0x02E0 (736)
0x528A, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0x528A, // 0x02F0 (752)
0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, // 0x0300 (768)
0x528A, 0x528A, 0x528A, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, // 0x0310 (784)
0xFFFF, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, // 0x0320 (800)
0x528A, 0x528A, 0x528A, 0x528A, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, // 0x0330 (816)
0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, // 0x0340 (832)
0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, // 0x0350 (848)
0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, // 0x0360 (864)
0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, // 0x0370 (880)
0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, // 0x0380 (896)
0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, // 0x0390 (912)
0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0x528A, 0x528A, 0x528A, // 0x03A0 (928)
0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, // 0x03B0 (944)
0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, // 0x03C0 (960)
0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, // 0x03D0 (976)
0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, // 0x03E0 (992)
0xFFFF, 0xFFFF, 0xFFFF, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0xFFFF, 0x528A, 0x528A, 0x528A, 0x528A, 0xFFFF, 0xFFFF, 0xFFFF, // 0x03F0 (1008)
0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, // 0x0400 (1024)
0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0x528A, 0x528A, 0x528A, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0x528A, 0x528A, 0xFFFF, // 0x0410 (1040)
0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, // 0x0420 (1056)
0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0x528A, 0x528A, 0x528A, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0x528A, // 0x0430 (1072)
0x528A, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, // 0x0440 (1088)
0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0x528A, 0x528A, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, // 0x0450 (1104)
0xFFFF, 0x528A, 0x528A, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, // 0x0460 (1120)
0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0x528A, 0x528A, 0x528A, 0xFFFF, 0xFFFF, // 0x0470 (1136)
0xFFFF, 0xFFFF, 0xFFFF, 0x528A, 0x528A, 0x528A, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, // 0x0480 (1152)
0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0x528A, 0x528A, 0x528A, // 0x0490 (1168)
0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0x528A, 0x528A, 0x528A, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, // 0x04A0 (1184)
0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, // 0x04B0 (1200)
0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, // 0x04C0 (1216)
0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF};
const uint16_t dino_leftstep[] PROGMEM = {
0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, // 0x0010 (16)
0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, // 0x0020 (32)
0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, // 0x0030 (48)
0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, // 0x0040 (64)
0x528A, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, // 0x0050 (80)
0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, // 0x0060 (96)
0x528A, 0x528A, 0x528A, 0x528A, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, // 0x0070 (112)
0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0x528A, 0x528A, 0x528A, 0xFFFF, 0xFFFF, 0x528A, 0x528A, 0x528A, 0x528A, // 0x0080 (128)
0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, // 0x0090 (144)
0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0x528A, 0x528A, 0x528A, 0xFFFF, 0xFFFF, 0x528A, 0x528A, // 0x00A0 (160)
0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, // 0x00B0 (176)
0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, // 0x00C0 (192)
0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, // 0x00D0 (208)
0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0x528A, 0x528A, 0x528A, // 0x00E0 (224)
0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0xFFFF, 0xFFFF, 0xFFFF, // 0x00F0 (240)
0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0x528A, // 0x0100 (256)
0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0xFFFF, // 0x0110 (272)
0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, // 0x0120 (288)
0xFFFF, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, // 0x0130 (304)
0x528A, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, // 0x0140 (320)
0xFFFF, 0xFFFF, 0xFFFF, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, // 0x0150 (336)
0x528A, 0x528A, 0x528A, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, // 0x0160 (352)
0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0xFFFF, 0xFFFF, 0xFFFF, // 0x0170 (368)
0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, // 0x0180 (384)
0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, // 0x0190 (400)
0x528A, 0x528A, 0x528A, 0x528A, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, // 0x01A0 (416)
0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, // 0x01B0 (432)
0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0x528A, 0x528A, 0xFFFF, 0xFFFF, 0xFFFF, // 0x01C0 (448)
0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, // 0x01D0 (464)
0x528A, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0x528A, 0x528A, 0xFFFF, // 0x01E0 (480)
0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, // 0x01F0 (496)
0x528A, 0x528A, 0x528A, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0x528A, // 0x0200 (512)
0x528A, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, // 0x0210 (528)
0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, // 0x0220 (544)
0xFFFF, 0x528A, 0x528A, 0x528A, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, // 0x0230 (560)
0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, // 0x0240 (576)
0xFFFF, 0xFFFF, 0xFFFF, 0x528A, 0x528A, 0x528A, 0x528A, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0x528A, 0x528A, 0x528A, 0x528A, // 0x0250 (592)
0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0xFFFF, 0xFFFF, 0xFFFF, // 0x0260 (608)
0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0xFFFF, 0xFFFF, 0xFFFF, 0x528A, 0x528A, 0x528A, // 0x0270 (624)
0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0xFFFF, 0xFFFF, 0x528A, 0x528A, 0xFFFF, // 0x0280 (640)
0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, // 0x0290 (656)
0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0xFFFF, 0xFFFF, 0xFFFF, // 0x02A0 (672)
0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, // 0x02B0 (688)
0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0xFFFF, // 0x02C0 (704)
0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, // 0x02D0 (720)
0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, // 0x02E0 (736)
0x528A, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0x528A, // 0x02F0 (752)
0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, // 0x0300 (768)
0x528A, 0x528A, 0x528A, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, // 0x0310 (784)
0xFFFF, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, // 0x0320 (800)
0x528A, 0x528A, 0x528A, 0x528A, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, // 0x0330 (816)
0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, // 0x0340 (832)
0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, // 0x0350 (848)
0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, // 0x0360 (864)
0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, // 0x0370 (880)
0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, // 0x0380 (896)
0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, // 0x0390 (912)
0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0x528A, 0x528A, 0x528A, // 0x03A0 (928)
0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, // 0x03B0 (944)
0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, // 0x03C0 (960)
0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, // 0x03D0 (976)
0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, // 0x03E0 (992)
0xFFFF, 0xFFFF, 0xFFFF, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0xFFFF, 0x528A, 0x528A, 0x528A, 0x528A, 0xFFFF, 0xFFFF, 0xFFFF, // 0x03F0 (1008)
0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, // 0x0400 (1024)
0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0x528A, 0x528A, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0x528A, 0x528A, 0xFFFF, // 0x0410 (1040)
0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, // 0x0420 (1056)
0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0x528A, 0x528A, 0x528A, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0x528A, // 0x0430 (1072)
0x528A, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, // 0x0440 (1088)
0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0x528A, 0x528A, 0x528A, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, // 0x0450 (1104)
0xFFFF, 0x528A, 0x528A, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, // 0x0460 (1120)
0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, // 0x0470 (1136)
0xFFFF, 0xFFFF, 0xFFFF, 0x528A, 0x528A, 0x528A, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, // 0x0480 (1152)
0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, // 0x0490 (1168)
0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0x528A, 0x528A, 0x528A, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, // 0x04A0 (1184)
0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, // 0x04B0 (1200)
0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, // 0x04C0 (1216)
0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF};
const uint16_t dino_rightstep[] PROGMEM = {
0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, // 0x0010 (16)
0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, // 0x0020 (32)
0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, // 0x0030 (48)
0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, // 0x0040 (64)
0x528A, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, // 0x0050 (80)
0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, // 0x0060 (96)
0x528A, 0x528A, 0x528A, 0x528A, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, // 0x0070 (112)
0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0x528A, 0x528A, 0x528A, 0xFFFF, 0xFFFF, 0x528A, 0x528A, 0x528A, 0x528A, // 0x0080 (128)
0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, // 0x0090 (144)
0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0x528A, 0x528A, 0x528A, 0xFFFF, 0xFFFF, 0x528A, 0x528A, // 0x00A0 (160)
0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, // 0x00B0 (176)
0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, // 0x00C0 (192)
0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, // 0x00D0 (208)
0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0x528A, 0x528A, 0x528A, // 0x00E0 (224)
0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0xFFFF, 0xFFFF, 0xFFFF, // 0x00F0 (240)
0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0x528A, // 0x0100 (256)
0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0xFFFF, // 0x0110 (272)
0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, // 0x0120 (288)
0xFFFF, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, // 0x0130 (304)
0x528A, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, // 0x0140 (320)
0xFFFF, 0xFFFF, 0xFFFF, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, // 0x0150 (336)
0x528A, 0x528A, 0x528A, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, // 0x0160 (352)
0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0xFFFF, 0xFFFF, 0xFFFF, // 0x0170 (368)
0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, // 0x0180 (384)
0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, // 0x0190 (400)
0x528A, 0x528A, 0x528A, 0x528A, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, // 0x01A0 (416)
0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, // 0x01B0 (432)
0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0x528A, 0x528A, 0xFFFF, 0xFFFF, 0xFFFF, // 0x01C0 (448)
0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, // 0x01D0 (464)
0x528A, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0x528A, 0x528A, 0xFFFF, // 0x01E0 (480)
0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, // 0x01F0 (496)
0x528A, 0x528A, 0x528A, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0x528A, // 0x0200 (512)
0x528A, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, // 0x0210 (528)
0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, // 0x0220 (544)
0xFFFF, 0x528A, 0x528A, 0x528A, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, // 0x0230 (560)
0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, // 0x0240 (576)
0xFFFF, 0xFFFF, 0xFFFF, 0x528A, 0x528A, 0x528A, 0x528A, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0x528A, 0x528A, 0x528A, 0x528A, // 0x0250 (592)
0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0xFFFF, 0xFFFF, 0xFFFF, // 0x0260 (608)
0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0xFFFF, 0xFFFF, 0xFFFF, 0x528A, 0x528A, 0x528A, // 0x0270 (624)
0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0xFFFF, 0xFFFF, 0x528A, 0x528A, 0xFFFF, // 0x0280 (640)
0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, // 0x0290 (656)
0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0xFFFF, 0xFFFF, 0xFFFF, // 0x02A0 (672)
0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, // 0x02B0 (688)
0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0xFFFF, // 0x02C0 (704)
0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, // 0x02D0 (720)
0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, // 0x02E0 (736)
0x528A, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0x528A, // 0x02F0 (752)
0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, // 0x0300 (768)
0x528A, 0x528A, 0x528A, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, // 0x0310 (784)
0xFFFF, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, // 0x0320 (800)
0x528A, 0x528A, 0x528A, 0x528A, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, // 0x0330 (816)
0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, // 0x0340 (832)
0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, // 0x0350 (848)
0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, // 0x0360 (864)
0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, // 0x0370 (880)
0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, // 0x0380 (896)
0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, // 0x0390 (912)
0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0x528A, 0x528A, 0x528A, // 0x03A0 (928)
0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, // 0x03B0 (944)
0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, // 0x03C0 (960)
0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, // 0x03D0 (976)
0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, // 0x03E0 (992)
0xFFFF, 0xFFFF, 0xFFFF, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0xFFFF, 0x528A, 0x528A, 0x528A, 0x528A, 0xFFFF, 0xFFFF, 0xFFFF, // 0x03F0 (1008)
0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, // 0x0400 (1024)
0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0x528A, 0x528A, 0x528A, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0x528A, 0x528A, 0xFFFF, // 0x0410 (1040)
0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, // 0x0420 (1056)
0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0x528A, 0x528A, 0x528A, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0x528A, // 0x0430 (1072)
0x528A, 0x528A, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, // 0x0440 (1088)
0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0x528A, 0x528A, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, // 0x0450 (1104)
0xFFFF, 0x528A, 0x528A, 0x528A, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, // 0x0460 (1120)
0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0x528A, 0x528A, 0x528A, 0xFFFF, 0xFFFF, // 0x0470 (1136)
0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, // 0x0480 (1152)
0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0x528A, 0x528A, 0x528A, // 0x0490 (1168)
0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, // 0x04A0 (1184)
0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, // 0x04B0 (1200)
0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, // 0x04C0 (1216)
0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF};
const uint16_t dino_ducking_leftstep[] PROGMEM = {
0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, // 0x0010 (16)
0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, // 0x0020 (32)
0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0x528A, 0x528A, 0xFFFF, // 0x0030 (48)
0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, // 0x0040 (64)
0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0x528A, 0x528A, 0x528A, 0x528A, // 0x0050 (80)
0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0xFFFF, 0xFFFF, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0xFFFF, // 0x0060 (96)
0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, // 0x0070 (112)
0x528A, 0x528A, 0x528A, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, // 0x0080 (128)
0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0xFFFF, 0xFFFF, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, // 0x0090 (144)
0xFFFF, 0xFFFF, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, // 0x00A0 (160)
0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, // 0x00B0 (176)
0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, // 0x00C0 (192)
0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, // 0x00D0 (208)
0x528A, 0x528A, 0x528A, 0x528A, 0xFFFF, 0xFFFF, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, // 0x00E0 (224)
0x528A, 0xFFFF, 0xFFFF, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, // 0x00F0 (240)
0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, // 0x0100 (256)
0x528A, 0xFFFF, 0xFFFF, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0xFFFF, 0xFFFF, // 0x0110 (272)
0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, // 0x0120 (288)
0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, // 0x0130 (304)
0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0xFFFF, 0xFFFF, 0xFFFF, 0x528A, 0x528A, // 0x0140 (320)
0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, // 0x0150 (336)
0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, // 0x0160 (352)
0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0x528A, 0x528A, 0x528A, 0x528A, // 0x0170 (368)
0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, // 0x0180 (384)
0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, // 0x0190 (400)
0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, // 0x01A0 (416)
0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, // 0x01B0 (432)
0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, // 0x01C0 (448)
0x528A, 0x528A, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, // 0x01D0 (464)
0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, // 0x01E0 (480)
0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, // 0x01F0 (496)
0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, // 0x0200 (512)
0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, // 0x0210 (528)
0x528A, 0x528A, 0x528A, 0x528A, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, // 0x0220 (544)
0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, // 0x0230 (560)
0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, // 0x0240 (576)
0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, // 0x0250 (592)
0xFFFF, 0xFFFF, 0xFFFF, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, // 0x0260 (608)
0x528A, 0x528A, 0x528A, 0x528A, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, // 0x0270 (624)
0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0x528A, // 0x0280 (640)
0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0xFFFF, 0xFFFF, 0xFFFF, 0x528A, 0x528A, 0x528A, // 0x0290 (656)
0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, // 0x02A0 (672)
0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0x528A, 0x528A, 0xFFFF, 0xFFFF, // 0x02B0 (688)
0xFFFF, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0x528A, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, // 0x02C0 (704)
0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, // 0x02D0 (720)
0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0x528A, 0x528A, 0xFFFF, 0xFFFF, 0xFFFF, 0x528A, 0x528A, // 0x02E0 (736)
0x528A, 0x528A, 0x528A, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0x528A, 0x528A, 0x528A, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, // 0x02F0 (752)
0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, // 0x0300 (768)
0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0x528A, 0x528A, 0x528A, 0xFFFF, 0xFFFF, 0x528A, 0x528A, 0x528A, 0xFFFF, 0xFFFF, // 0x0310 (784)
0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, // 0x0320 (800)
0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, // 0x0330 (816)
0xFFFF, 0xFFFF, 0xFFFF, 0x528A, 0x528A, 0x528A, 0xFFFF, 0xFFFF, 0x528A, 0x528A, 0x528A, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, // 0x0340 (832)
0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, // 0x0350 (848)
0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, // 0x0360 (864)
0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0x528A, 0x528A, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, // 0x0370 (880)
0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, // 0x0380 (896)
0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, // 0x0390 (912)
0xFFFF, 0xFFFF, 0x528A, 0x528A, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, // 0x03A0 (928)
0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, // 0x03B0 (944)
0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0x528A, // 0x03C0 (960)
0x528A, 0x528A, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, // 0x03D0 (976)
0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF};
const uint16_t dino_ducking_rightstep[] PROGMEM = {
0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, // 0x0010 (16)
0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, // 0x0020 (32)
0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0x528A, 0x528A, 0xFFFF, // 0x0030 (48)
0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, // 0x0040 (64)
0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0x528A, 0x528A, 0x528A, 0x528A, // 0x0050 (80)
0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0xFFFF, 0xFFFF, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0xFFFF, // 0x0060 (96)
0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, // 0x0070 (112)
0x528A, 0x528A, 0x528A, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, // 0x0080 (128)
0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0xFFFF, 0xFFFF, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, // 0x0090 (144)
0xFFFF, 0xFFFF, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, // 0x00A0 (160)
0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, // 0x00B0 (176)
0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, // 0x00C0 (192)
0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, // 0x00D0 (208)
0x528A, 0x528A, 0x528A, 0x528A, 0xFFFF, 0xFFFF, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, // 0x00E0 (224)
0x528A, 0xFFFF, 0xFFFF, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, // 0x00F0 (240)
0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, // 0x0100 (256)
0x528A, 0xFFFF, 0xFFFF, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0xFFFF, 0xFFFF, // 0x0110 (272)
0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, // 0x0120 (288)
0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, // 0x0130 (304)
0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0xFFFF, 0xFFFF, 0xFFFF, 0x528A, 0x528A, // 0x0140 (320)
0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, // 0x0150 (336)
0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, // 0x0160 (352)
0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0x528A, 0x528A, 0x528A, 0x528A, // 0x0170 (368)
0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, // 0x0180 (384)
0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, // 0x0190 (400)
0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, // 0x01A0 (416)
0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, // 0x01B0 (432)
0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, // 0x01C0 (448)
0x528A, 0x528A, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, // 0x01D0 (464)
0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, // 0x01E0 (480)
0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, // 0x01F0 (496)
0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, // 0x0200 (512)
0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, // 0x0210 (528)
0x528A, 0x528A, 0x528A, 0x528A, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, // 0x0220 (544)
0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, // 0x0230 (560)
0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, // 0x0240 (576)
0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, // 0x0250 (592)
0xFFFF, 0xFFFF, 0xFFFF, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, // 0x0260 (608)
0x528A, 0x528A, 0x528A, 0x528A, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, // 0x0270 (624)
0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0x528A, // 0x0280 (640)
0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0xFFFF, 0xFFFF, 0xFFFF, 0x528A, 0x528A, 0x528A, // 0x0290 (656)
0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, // 0x02A0 (672)
0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0x528A, 0x528A, 0x528A, 0x528A, // 0x02B0 (688)
0x528A, 0xFFFF, 0xFFFF, 0xFFFF, 0x528A, 0x528A, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0x528A, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, // 0x02C0 (704)
0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, // 0x02D0 (720)
0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0xFFFF, 0xFFFF, // 0x02E0 (736)
0xFFFF, 0x528A, 0x528A, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0x528A, 0x528A, 0x528A, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, // 0x02F0 (752)
0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, // 0x0300 (768)
0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0x528A, 0x528A, 0x528A, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0x528A, 0x528A, // 0x0310 (784)
0x528A, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, // 0x0320 (800)
0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, // 0x0330 (816)
0xFFFF, 0xFFFF, 0xFFFF, 0x528A, 0x528A, 0x528A, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0x528A, 0x528A, 0x528A, 0xFFFF, 0xFFFF, // 0x0340 (832)
0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, // 0x0350 (848)
0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, // 0x0360 (864)
0x528A, 0x528A, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, // 0x0370 (880)
0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, // 0x0380 (896)
0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0x528A, 0x528A, 0xFFFF, // 0x0390 (912)
0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, // 0x03A0 (928)
0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, // 0x03B0 (944)
0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0x528A, 0x528A, 0x528A, 0xFFFF, 0xFFFF, 0xFFFF, // 0x03C0 (960)
0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, // 0x03D0 (976)
0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF};
const uint16_t dino_gameover[] PROGMEM = {
0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, // 0x0010 (16)
0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, // 0x0020 (32)
0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, // 0x0030 (48)
0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, // 0x0040 (64)
0x528A, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, // 0x0050 (80)
0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, // 0x0060 (96)
0x528A, 0x528A, 0x528A, 0x528A, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, // 0x0070 (112)
0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0x528A, 0x528A, 0x528A, 0xFFFF, 0xFFFF, 0xFFFF, 0x528A, 0x528A, 0x528A, // 0x0080 (128)
0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, // 0x0090 (144)
0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0x528A, 0x528A, 0x528A, 0xFFFF, 0x528A, 0xFFFF, 0x528A, // 0x00A0 (160)
0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, // 0x00B0 (176)
0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0x528A, 0x528A, 0x528A, 0xFFFF, 0xFFFF, // 0x00C0 (192)
0xFFFF, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, // 0x00D0 (208)
0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0x528A, 0x528A, 0x528A, // 0x00E0 (224)
0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0xFFFF, 0xFFFF, 0xFFFF, // 0x00F0 (240)
0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0x528A, // 0x0100 (256)
0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0xFFFF, // 0x0110 (272)
0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, // 0x0120 (288)
0xFFFF, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, // 0x0130 (304)
0x528A, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, // 0x0140 (320)
0xFFFF, 0xFFFF, 0xFFFF, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, // 0x0150 (336)
0x528A, 0x528A, 0x528A, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, // 0x0160 (352)
0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0xFFFF, 0xFFFF, 0xFFFF, // 0x0170 (368)
0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, // 0x0180 (384)
0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, // 0x0190 (400)
0x528A, 0x528A, 0x528A, 0x528A, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, // 0x01A0 (416)
0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, // 0x01B0 (432)
0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0x528A, 0x528A, 0xFFFF, 0xFFFF, 0xFFFF, // 0x01C0 (448)
0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, // 0x01D0 (464)
0x528A, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0x528A, 0x528A, 0xFFFF, // 0x01E0 (480)
0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, // 0x01F0 (496)
0x528A, 0x528A, 0x528A, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0x528A, // 0x0200 (512)
0x528A, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, // 0x0210 (528)
0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, // 0x0220 (544)
0xFFFF, 0x528A, 0x528A, 0x528A, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, // 0x0230 (560)
0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, // 0x0240 (576)
0xFFFF, 0xFFFF, 0xFFFF, 0x528A, 0x528A, 0x528A, 0x528A, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0x528A, 0x528A, 0x528A, 0x528A, // 0x0250 (592)
0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0xFFFF, 0xFFFF, 0xFFFF, // 0x0260 (608)
0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0xFFFF, 0xFFFF, 0xFFFF, 0x528A, 0x528A, 0x528A, // 0x0270 (624)
0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0xFFFF, 0xFFFF, 0x528A, 0x528A, 0xFFFF, // 0x0280 (640)
0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, // 0x0290 (656)
0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0xFFFF, 0xFFFF, 0xFFFF, // 0x02A0 (672)
0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, // 0x02B0 (688)
0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0xFFFF, // 0x02C0 (704)
0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, // 0x02D0 (720)
0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, // 0x02E0 (736)
0x528A, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0x528A, // 0x02F0 (752)
0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, // 0x0300 (768)
0x528A, 0x528A, 0x528A, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, // 0x0310 (784)
0xFFFF, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, // 0x0320 (800)
0x528A, 0x528A, 0x528A, 0x528A, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, // 0x0330 (816)
0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, // 0x0340 (832)
0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, // 0x0350 (848)
0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, // 0x0360 (864)
0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, // 0x0370 (880)
0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, // 0x0380 (896)
0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, // 0x0390 (912)
0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0x528A, 0x528A, 0x528A, // 0x03A0 (928)
0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, // 0x03B0 (944)
0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, // 0x03C0 (960)
0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, // 0x03D0 (976)
0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, // 0x03E0 (992)
0xFFFF, 0xFFFF, 0xFFFF, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0xFFFF, 0x528A, 0x528A, 0x528A, 0x528A, 0xFFFF, 0xFFFF, 0xFFFF, // 0x03F0 (1008)
0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, // 0x0400 (1024)
0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0x528A, 0x528A, 0x528A, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0x528A, 0x528A, 0xFFFF, // 0x0410 (1040)
0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, // 0x0420 (1056)
0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0x528A, 0x528A, 0x528A, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0x528A, // 0x0430 (1072)
0x528A, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, // 0x0440 (1088)
0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0x528A, 0x528A, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, // 0x0450 (1104)
0xFFFF, 0x528A, 0x528A, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, // 0x0460 (1120)
0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0x528A, 0x528A, 0x528A, 0xFFFF, 0xFFFF, // 0x0470 (1136)
0xFFFF, 0xFFFF, 0xFFFF, 0x528A, 0x528A, 0x528A, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, // 0x0480 (1152)
0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0x528A, 0x528A, 0x528A, // 0x0490 (1168)
0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0x528A, 0x528A, 0x528A, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, // 0x04A0 (1184)
0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, // 0x04B0 (1200)
0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, // 0x04C0 (1216)
0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF};
@@ -0,0 +1,121 @@
// 34x27
const uint16_t pterodactyl_upflop[] PROGMEM = {
0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0x528A, 0x528A, 0xFFFF, // 0x0010 (16)
0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, // 0x0020 (32)
0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0x528A, // 0x0030 (48)
0x528A, 0x528A, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, // 0x0040 (64)
0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, // 0x0050 (80)
0xFFFF, 0x528A, 0x528A, 0x528A, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, // 0x0060 (96)
0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0x528A, 0x528A, 0x528A, 0x528A, // 0x0070 (112)
0xFFFF, 0xFFFF, 0xFFFF, 0x528A, 0x528A, 0x528A, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, // 0x0080 (128)
0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0x528A, 0x528A, // 0x0090 (144)
0x528A, 0x528A, 0xFFFF, 0xFFFF, 0xFFFF, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, // 0x00A0 (160)
0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0x528A, 0x528A, // 0x00B0 (176)
0x528A, 0x528A, 0x528A, 0x528A, 0xFFFF, 0xFFFF, 0xFFFF, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, // 0x00C0 (192)
0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0x528A, // 0x00D0 (208)
0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0xFFFF, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0xFFFF, // 0x00E0 (224)
0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, // 0x00F0 (240)
0xFFFF, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0xFFFF, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, // 0x0100 (256)
0x528A, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, // 0x0110 (272)
0xFFFF, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0xFFFF, 0x528A, 0x528A, 0x528A, // 0x0120 (288)
0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, // 0x0130 (304)
0xFFFF, 0xFFFF, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, // 0x0140 (320)
0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, // 0x0150 (336)
0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, // 0x0160 (352)
0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, // 0x0170 (368)
0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0x528A, // 0x0180 (384)
0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0xFFFF, 0xFFFF, // 0x0190 (400)
0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, // 0x01A0 (416)
0xFFFF, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, // 0x01B0 (432)
0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, // 0x01C0 (448)
0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, // 0x01D0 (464)
0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, // 0x01E0 (480)
0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, // 0x01F0 (496)
0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, // 0x0200 (512)
0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, // 0x0210 (528)
0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0xFFFF, 0xFFFF, // 0x0220 (544)
0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0x528A, 0x528A, 0x528A, // 0x0230 (560)
0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, // 0x0240 (576)
0x528A, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0x528A, // 0x0250 (592)
0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, // 0x0260 (608)
0x528A, 0x528A, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, // 0x0270 (624)
0xFFFF, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, // 0x0280 (640)
0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, // 0x0290 (656)
0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, // 0x02A0 (672)
0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, // 0x02B0 (688)
0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, // 0x02C0 (704)
0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, // 0x02D0 (720)
0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, // 0x02E0 (736)
0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, // 0x02F0 (752)
0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, // 0x0300 (768)
0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, // 0x0310 (784)
0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, // 0x0320 (800)
0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, // 0x0330 (816)
0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, // 0x0340 (832)
0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, // 0x0350 (848)
0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, // 0x0360 (864)
0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, // 0x0370 (880)
0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, // 0x0380 (896)
0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, // 0x0390 (912)
0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF};
const uint16_t pterodactyl_downflop[] PROGMEM = {
0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, // 0x0010 (16)
0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, // 0x0020 (32)
0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, // 0x0030 (48)
0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, // 0x0040 (64)
0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, // 0x0050 (80)
0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, // 0x0060 (96)
0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0x528A, 0x528A, 0x528A, 0x528A, // 0x0070 (112)
0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, // 0x0080 (128)
0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0x528A, 0x528A, // 0x0090 (144)
0x528A, 0x528A, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, // 0x00A0 (160)
0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0x528A, 0x528A, // 0x00B0 (176)
0x528A, 0x528A, 0x528A, 0x528A, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, // 0x00C0 (192)
0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0x528A, // 0x00D0 (208)
0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, // 0x00E0 (224)
0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, // 0x00F0 (240)
0xFFFF, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, // 0x0100 (256)
0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, // 0x0110 (272)
0xFFFF, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, // 0x0120 (288)
0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, // 0x0130 (304)
0xFFFF, 0xFFFF, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, // 0x0140 (320)
0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, // 0x0150 (336)
0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, // 0x0160 (352)
0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, // 0x0170 (368)
0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0x528A, // 0x0180 (384)
0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0xFFFF, 0xFFFF, // 0x0190 (400)
0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, // 0x01A0 (416)
0xFFFF, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, // 0x01B0 (432)
0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, // 0x01C0 (448)
0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, // 0x01D0 (464)
0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, // 0x01E0 (480)
0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, // 0x01F0 (496)
0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, // 0x0200 (512)
0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, // 0x0210 (528)
0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0xFFFF, 0xFFFF, // 0x0220 (544)
0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0x528A, 0x528A, 0x528A, // 0x0230 (560)
0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, // 0x0240 (576)
0x528A, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0x528A, // 0x0250 (592)
0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, // 0x0260 (608)
0x528A, 0x528A, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, // 0x0270 (624)
0xFFFF, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, // 0x0280 (640)
0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, // 0x0290 (656)
0xFFFF, 0xFFFF, 0xFFFF, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, // 0x02A0 (672)
0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, // 0x02B0 (688)
0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, // 0x02C0 (704)
0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, // 0x02D0 (720)
0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, // 0x02E0 (736)
0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, // 0x02F0 (752)
0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0x528A, 0x528A, 0x528A, 0x528A, 0x528A, 0xFFFF, 0xFFFF, // 0x0300 (768)
0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, // 0x0310 (784)
0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0x528A, 0x528A, 0x528A, 0xFFFF, 0xFFFF, // 0x0320 (800)
0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, // 0x0330 (816)
0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0x528A, 0x528A, 0x528A, // 0x0340 (832)
0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, // 0x0350 (848)
0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0x528A, // 0x0360 (864)
0x528A, 0x528A, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, // 0x0370 (880)
0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, // 0x0380 (896)
0xFFFF, 0x528A, 0x528A, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, // 0x0390 (912)
0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF};
+808
View File
@@ -0,0 +1,808 @@
/*
* ------------------------------------------------------------
* | Made by RocketGod |
* | Find me at https://betaskynet.com |
* | Argh matey! |
* ------------------------------------------------------------
*/
/*
* Chrome Dino Game for Portapack Mayhem
* Based on the original DinoGame by various contributors
*/
#include "ui_dinogame.hpp"
namespace ui::external_app::dinogame {
#define PROGMEM
#include "sprites/dino.h"
#include "sprites/pterodactyl.h"
#undef PROGMEM
// Global variables
Ticker game_timer;
Painter painter;
static Callback game_update_callback = nullptr;
static uint32_t game_update_timeout = 0;
static uint32_t game_update_counter = 0;
static DinoGameView* current_instance = nullptr;
const Color pp_colors[] = {
Color::white(),
Color::blue(),
Color::yellow(),
Color::purple(),
Color::green(),
Color::red(),
Color::magenta(),
Color::orange(),
Color::black(),
};
// Drawing functions
void cls() {
painter.fill_rectangle({0, 0, portapack::display.width(), portapack::display.height()}, Color::black());
}
void 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) {
painter.draw_rectangle({x1, y1, x2 - x1, y2 - y1}, pp_colors[color]);
}
// Timer implementation
void check_game_timer() {
if (game_update_callback) {
if (++game_update_counter >= game_update_timeout) {
game_update_counter = 0;
game_update_callback();
}
}
}
void Ticker::attach(Callback func, double delay_sec) {
game_update_callback = func;
game_update_timeout = delay_sec * 60;
}
void Ticker::detach() {
game_update_callback = nullptr;
}
// String helper
std::string DinoGameView::score_to_string(uint32_t score) {
std::string score_s = std::to_string(score);
if (score_s.length() < 5) {
std::string temp = "";
for (uint8_t i = 0; i < 5 - score_s.length(); ++i) temp += "0";
temp += score_s;
score_s = temp;
}
return score_s;
}
// Game timer callback
void game_timer_check() {
if (current_instance) {
if (current_instance->game_state == GameState::PLAYING) {
current_instance->game_loop();
} else if (current_instance->game_state == GameState::MENU) {
current_instance->show_menu();
}
}
}
DinoGameView::DinoGameView(NavigationView& nav)
: nav_{nav}, bird_info{}, game_timer{} {
add_children({&dummy, &button_difficulty});
current_instance = this;
game_timer.attach(&game_timer_check, 1.0 / 60.0);
button_difficulty.on_select = [this](Button&) {
easy_mode = !easy_mode;
button_difficulty.set_text(easy_mode ? "Mode: EASY" : "Mode: HARD");
};
}
void DinoGameView::on_show() {
}
void DinoGameView::paint(Painter& painter) {
(void)painter;
if (!initialized) {
initialized = true;
std::srand(LPC_RTC->CTIME0);
init_game();
}
}
void DinoGameView::frame_sync() {
check_game_timer();
set_dirty();
}
void DinoGameView::init_game() {
game_state = GameState::MENU;
menu_initialized = false;
blink_state = true;
blink_counter = 0;
new_game();
}
void DinoGameView::new_game() {
steps = 0;
score = 0;
collided = false;
ducking = false;
duck_timer = 0;
displayedGameOver = false;
bird_info.inGame = false;
bird_info.x_offset = 320;
bird_info.y_offset = 0;
bird_info.y_velocity = 0;
jumping = false;
falling = false;
jumpHeight = 0;
last_dino_y = DINO_Y;
last_bird_x = -1;
last_bird_y = -1;
last_ducking = false;
last_runstate = false;
ground_offset = 0;
speed_modifier = 0;
runstate = false;
score_drawn = false;
last_score = 999999;
obstacle_spawn_timer = 100; // Initial delay before first obstacle
// Initialize obstacles
for (int i = 0; i < MAX_OBSTACLES; i++) {
obstacles[i].active = false;
obstacles[i].x = -100;
obstacles[i].last_x = -100;
}
cls();
}
void DinoGameView::show_menu() {
if (!menu_initialized) {
cls();
auto style = *ui::Theme::getInstance()->fg_medium;
auto style_title = *ui::Theme::getInstance()->fg_light;
// Draw title
painter.draw_string({80, 60}, style_title, "DINO GAME");
// Draw instructions
painter.draw_string({45, 130}, style, "SELECT: Jump/Start");
painter.draw_string({65, 150}, style, "DOWN: Duck");
painter.draw_string({50, 170}, style, "Avoid obstacles!");
// Draw high score
draw_high_score();
menu_initialized = true;
}
// Show difficulty button
button_difficulty.hidden(false);
// Animate the menu dino
bool menu_run_frame = (blink_counter / 15) % 2;
// Clear previous dino position
fillrect(103, 90, 103 + DINO_WIDTH, 90 + DINO_HEIGHT, Black);
// Draw animated dino
draw_dino_at(103, 90, false, menu_run_frame);
// Blinking start prompt
auto style_prompt = *ui::Theme::getInstance()->fg_light;
if (++blink_counter >= 30) {
blink_counter = 0;
blink_state = !blink_state;
painter.fill_rectangle({55, 258, 130, 20}, Color::black());
if (blink_state) {
painter.draw_string({55, 260}, style_prompt, "* PRESS SELECT *");
}
}
}
void DinoGameView::show_game_over() {
if (!displayedGameOver) {
displayedGameOver = true;
// Clear the last normal dino position
if (last_ducking) {
fillrect(DINO_X, last_dino_y, DINO_X + DINO_DUCK_WIDTH, last_dino_y + DINO_DUCK_HEIGHT, Black);
} else {
fillrect(DINO_X, last_dino_y, DINO_X + DINO_WIDTH, last_dino_y + DINO_HEIGHT, Black);
}
// Draw the game over dino sprite
draw_dino_sprite(DINO_X, DINO_Y, dino_gameover);
auto style = *ui::Theme::getInstance()->fg_light;
auto style_score = *ui::Theme::getInstance()->fg_medium;
// Game over text
painter.draw_string({85, 70}, style, "GAME OVER");
// Show final score
std::string score_text = "SCORE: " + score_to_string(score);
int score_x = (240 - score_text.length() * 8) / 2;
painter.draw_string({score_x, 90}, style_score, score_text);
painter.draw_string({65, 110}, style, "SELECT TO RETRY");
// Update high score
if (score > highScore) {
highScore = score;
painter.draw_string({55, 130}, style, "NEW HIGH SCORE!");
}
}
}
void DinoGameView::game_loop() {
button_difficulty.hidden(true);
if (collided && game_state == GameState::PLAYING) {
game_state = GameState::GAME_OVER;
show_game_over();
return;
}
// Update ground animation
ground_offset = (ground_offset + GAME_SPEED_BASE + speed_modifier) % 20;
// Clear only the game area (not the whole screen to reduce flicker)
draw_ground();
// Update and draw obstacles
update_obstacles();
// Manage bird
manage_bird();
// Handle jumping
if (jumping) {
if (!falling) jumpHeight += JUMP_SPEED + speed_modifier;
if (jumpHeight > JUMP_MAX_HEIGHT && !falling) falling = true;
if (falling) jumpHeight -= JUMP_SPEED + speed_modifier;
if (jumpHeight < 0) {
falling = false;
jumping = false;
jumpHeight = 0;
}
}
// Handle auto-stand from duck
if (ducking && duck_timer > 0) {
duck_timer--;
if (duck_timer == 0) {
stand();
}
}
// Update run animation
if (get_steps() % (10 - speed_modifier) == 0) runstate = !runstate;
// Draw dino with minimal redraw
int current_dino_y = jumping ? (DINO_Y - jumpHeight) : (ducking ? DINO_DUCK_Y : DINO_Y);
// Clear old dino position more precisely
if (current_dino_y != last_dino_y || runstate != last_runstate || ducking != last_ducking) {
// Clear based on last state
if (last_ducking) {
fillrect(DINO_X, last_dino_y, DINO_X + DINO_DUCK_WIDTH, last_dino_y + DINO_DUCK_HEIGHT, Black);
} else {
fillrect(DINO_X, last_dino_y, DINO_X + DINO_WIDTH, last_dino_y + DINO_HEIGHT, Black);
}
}
// Draw dino at new position
if (jumping) {
draw_dino_sprite(DINO_X, current_dino_y, dino_default);
} else if (ducking) {
if (runstate)
draw_dino_sprite(DINO_X, current_dino_y, dino_ducking_leftstep);
else
draw_dino_sprite(DINO_X, current_dino_y, dino_ducking_rightstep);
} else {
if (runstate)
draw_dino_sprite(DINO_X, current_dino_y, dino_leftstep);
else
draw_dino_sprite(DINO_X, current_dino_y, dino_rightstep);
}
// Update last state
last_dino_y = current_dino_y;
last_ducking = ducking;
last_runstate = runstate;
// Check collisions
check_collision();
// Update score
score = get_steps() / 10;
if (score % 100 == 0 && score > 0 && get_steps() % 1000 == 0) {
if (speed_modifier < 5) speed_modifier++;
}
step();
draw_current_score();
draw_high_score();
}
void DinoGameView::draw_ground() {
int ground_y = GAME_AREA_TOP + GAME_AREA_HEIGHT - GROUND_HEIGHT;
// Clear ground area
fillrect(0, ground_y, 320, ground_y + GROUND_HEIGHT, Black);
// Draw ground line
painter.draw_hline({0, ground_y}, 320, Color::white());
painter.draw_hline({0, ground_y + 1}, 320, Color::dark_grey());
// Draw ground texture
for (int x = -ground_offset; x < 320; x += 20) {
painter.draw_hline({x, ground_y + 3}, 10, Color::dark_grey());
painter.draw_hline({x + 5, ground_y + 5}, 5, Color::dark_grey());
}
}
void DinoGameView::update_obstacles() {
// Move obstacles
for (int i = 0; i < MAX_OBSTACLES; i++) {
if (obstacles[i].active) {
// Clear old position
if (obstacles[i].last_x != obstacles[i].x && obstacles[i].last_x < 320) {
clear_obstacle_area(obstacles[i].last_x, obstacles[i].width + 10, obstacles[i].height + 10);
}
obstacles[i].last_x = obstacles[i].x;
obstacles[i].x -= GAME_SPEED_BASE + speed_modifier;
// Remove off-screen obstacles
if (obstacles[i].x < -50) {
obstacles[i].active = false;
clear_obstacle_area(obstacles[i].last_x, obstacles[i].width + 10, obstacles[i].height + 10);
} else {
// Draw obstacle at new position
draw_obstacle(obstacles[i]);
}
}
}
// Decrement spawn timer
if (obstacle_spawn_timer > 0) {
obstacle_spawn_timer -= GAME_SPEED_BASE + speed_modifier;
}
// Check if we should spawn a new obstacle
if (obstacle_spawn_timer <= 0) {
// Try to spawn an obstacle
bool spawned = false;
for (int i = 0; i < MAX_OBSTACLES; i++) {
if (!obstacles[i].active) {
obstacles[i].active = true;
obstacles[i].x = 320;
obstacles[i].last_x = 320;
obstacles[i].type = rand() % 4;
// Set obstacle dimensions based on type
switch (obstacles[i].type) {
case 0: // Small cactus
obstacles[i].width = 15;
obstacles[i].height = 25;
break;
case 1: // Large cactus
obstacles[i].width = 20;
obstacles[i].height = 35;
break;
case 2: // Double cactus
obstacles[i].width = 35;
obstacles[i].height = 30;
break;
case 3: // Triple cactus
obstacles[i].width = 45;
obstacles[i].height = 28;
break;
}
spawned = true;
break;
}
}
if (spawned) {
// Set timer for next obstacle with random variation
obstacle_spawn_timer = MIN_OBSTACLE_DISTANCE + (rand() % (MAX_OBSTACLE_DISTANCE - MIN_OBSTACLE_DISTANCE));
}
}
}
void DinoGameView::clear_obstacle_area(int x, int width, int height) {
int ground_y = GAME_AREA_TOP + GAME_AREA_HEIGHT - GROUND_HEIGHT;
// Only clear the obstacle area, not the entire vertical space
fillrect(x - 5, ground_y - height - 5, x + width + 5, ground_y, Black);
}
void DinoGameView::draw_obstacle(const SimpleObstacle& obstacle) {
if (!obstacle.active) return;
int ground_y = GAME_AREA_TOP + GAME_AREA_HEIGHT - GROUND_HEIGHT;
int y = ground_y - obstacle.height;
// Draw cactus with green color
switch (obstacle.type) {
case 0: // Small single cactus
fillrect(obstacle.x + 5, y, obstacle.x + 10, ground_y, Green);
fillrect(obstacle.x, y + 8, obstacle.x + 5, y + 15, Green);
fillrect(obstacle.x + 10, y + 12, obstacle.x + 15, y + 19, Green);
// Add darker edges for definition
painter.draw_vline({obstacle.x + 4, y + 8}, 7, Color::dark_green());
painter.draw_vline({obstacle.x + 10, y + 12}, 7, Color::dark_green());
break;
case 1: // Large single cactus
fillrect(obstacle.x + 7, y, obstacle.x + 13, ground_y, Green);
fillrect(obstacle.x, y + 10, obstacle.x + 7, y + 20, Green);
fillrect(obstacle.x + 13, y + 15, obstacle.x + 20, y + 25, Green);
fillrect(obstacle.x + 5, y + 5, obstacle.x + 7, y + 12, Green);
fillrect(obstacle.x + 13, y + 8, obstacle.x + 15, y + 15, Green);
// Add darker edges
painter.draw_vline({obstacle.x + 6, y}, ground_y - y, Color::dark_green());
painter.draw_vline({obstacle.x + 13, y}, ground_y - y, Color::dark_green());
break;
case 2: // Double cactus
fillrect(obstacle.x + 5, y + 5, obstacle.x + 10, ground_y, Green);
fillrect(obstacle.x + 20, y, obstacle.x + 25, ground_y, Green);
fillrect(obstacle.x, y + 12, obstacle.x + 5, y + 18, Green);
fillrect(obstacle.x + 10, y + 15, obstacle.x + 15, y + 22, Green);
fillrect(obstacle.x + 25, y + 10, obstacle.x + 30, y + 17, Green);
// Darker outlines
painter.draw_vline({obstacle.x + 4, y + 5}, ground_y - y - 5, Color::dark_green());
painter.draw_vline({obstacle.x + 19, y}, ground_y - y, Color::dark_green());
break;
case 3: // Triple cactus
fillrect(obstacle.x + 5, y + 8, obstacle.x + 10, ground_y, Green);
fillrect(obstacle.x + 20, y, obstacle.x + 25, ground_y, Green);
fillrect(obstacle.x + 35, y + 5, obstacle.x + 40, ground_y, Green);
fillrect(obstacle.x, y + 15, obstacle.x + 5, y + 20, Green);
fillrect(obstacle.x + 25, y + 8, obstacle.x + 30, y + 15, Green);
fillrect(obstacle.x + 40, y + 12, obstacle.x + 45, y + 18, Green);
// Darker outlines
painter.draw_vline({obstacle.x + 4, y + 8}, ground_y - y - 8, Color::dark_green());
painter.draw_vline({obstacle.x + 19, y}, ground_y - y, Color::dark_green());
painter.draw_vline({obstacle.x + 34, y + 5}, ground_y - y - 5, Color::dark_green());
break;
}
}
void DinoGameView::manage_bird() {
if (!bird_info.inGame) {
// Only spawn bird if no obstacles are too close
bool obstacle_nearby = false;
for (int i = 0; i < MAX_OBSTACLES; i++) {
if (obstacles[i].active && obstacles[i].x > 200) {
obstacle_nearby = true;
break;
}
}
// Randomly spawn bird if no nearby obstacles
if (!obstacle_nearby && rand() % 400 == 0 && get_steps() > 100) {
bird_info.inGame = true;
bird_info.x_offset = 320;
bird_info.y_offset = 0;
bird_info.y_velocity = easy_mode ? 0 : (rand() % 3) - 1; // No vertical movement in easy mode
bird_info.y_position = BirdPosition::DOWN; // Always spawn at standing height in easy mode
}
} else {
// Calculate bird Y position
int base_y = (bird_info.y_position == BirdPosition::UP) ? BIRD_Y_UP : BIRD_Y_DOWN;
int current_y = base_y + bird_info.y_offset;
// Clear previous bird position
if (last_bird_x < 320 && last_bird_x >= -BIRD_WIDTH) {
int clear_start_x = last_bird_x;
int clear_end_x = last_bird_x + BIRD_WIDTH;
if (clear_start_x < 0) clear_start_x = 0;
if (clear_end_x > 320) clear_end_x = 320;
if (clear_end_x > clear_start_x) {
fillrect(clear_start_x, last_bird_y, clear_end_x, last_bird_y + BIRD_HEIGHT, Black);
}
}
// Move bird
bird_info.x_offset -= GAME_SPEED_BASE + speed_modifier + 1;
// Update vertical position only in hard mode
if (!easy_mode) {
bird_info.y_offset += bird_info.y_velocity;
if (rand() % 30 == 0) {
bird_info.y_velocity = (rand() % 3) - 1;
}
// Keep bird within bounds
if (current_y < GAME_AREA_TOP + 10) {
current_y = GAME_AREA_TOP + 10;
bird_info.y_offset = current_y - base_y;
bird_info.y_velocity = 1;
} else if (current_y > GAME_AREA_TOP + GAME_AREA_HEIGHT - GROUND_HEIGHT - BIRD_HEIGHT - 10) {
current_y = GAME_AREA_TOP + GAME_AREA_HEIGHT - GROUND_HEIGHT - BIRD_HEIGHT - 10;
bird_info.y_offset = current_y - base_y;
bird_info.y_velocity = -1;
}
} else {
// In easy mode, keep bird at perfect ducking height
// Bird should hit standing dino but miss ducking dino
current_y = DINO_Y - 10; // Position bird just above ducking height
}
// Animate wings
if (get_steps() % 15 == 0) bird_info.flop = !bird_info.flop;
// Draw bird only if any part is visible
if (bird_info.x_offset > -BIRD_WIDTH && bird_info.x_offset < 320) {
if (bird_info.flop) {
draw_bird_sprite(bird_info.x_offset, current_y, pterodactyl_upflop);
} else {
draw_bird_sprite(bird_info.x_offset, current_y, pterodactyl_downflop);
}
}
// Update last position
last_bird_x = bird_info.x_offset;
last_bird_y = current_y;
// Remove bird only when completely off screen
if (bird_info.x_offset < -BIRD_WIDTH - 5) {
bird_info.inGame = false;
last_bird_x = -1;
last_bird_y = -1;
}
}
}
void DinoGameView::draw_dino_sprite(int x, int y, const uint16_t* sprite) {
int height, width;
// Determine dimensions based on which sprite we're drawing
if (sprite == dino_ducking_leftstep || sprite == dino_ducking_rightstep) {
height = DINO_DUCK_HEIGHT;
width = DINO_DUCK_WIDTH;
} else {
height = DINO_HEIGHT;
width = DINO_WIDTH;
}
for (int dy = 0; dy < height; dy++) {
int run_start = -1;
uint16_t run_color = 0;
for (int dx = 0; dx < width; dx++) {
uint16_t pixel = sprite[dy * width + dx];
if (pixel != TRANSPARENT_COLOR) {
if (run_start == -1 || pixel != run_color) {
// Draw previous run if any
if (run_start != -1) {
painter.fill_rectangle({x + run_start, y + dy, dx - run_start, 1}, Color(run_color));
}
run_start = dx;
run_color = pixel;
}
} else {
// Draw previous run if any
if (run_start != -1) {
painter.fill_rectangle({x + run_start, y + dy, dx - run_start, 1}, Color(run_color));
run_start = -1;
}
}
}
// Draw final run if any
if (run_start != -1) {
painter.fill_rectangle({x + run_start, y + dy, width - run_start, 1}, Color(run_color));
}
}
}
void DinoGameView::draw_bird_sprite(int x, int y, const uint16_t* sprite) {
for (int dy = 0; dy < BIRD_HEIGHT; dy++) {
int run_start = -1;
uint16_t run_color = 0;
for (int dx = 0; dx < BIRD_WIDTH; dx++) {
// Skip pixels that would be off-screen
if (x + dx < 0 || x + dx >= 320) {
// End any current run
if (run_start != -1) {
painter.fill_rectangle({x + run_start, y + dy, dx - run_start, 1}, Color(run_color));
run_start = -1;
}
continue;
}
uint16_t pixel = sprite[dy * BIRD_WIDTH + dx];
if (pixel != TRANSPARENT_COLOR) {
if (pixel == SPRITE_COLOR) {
pixel = Color::red().v;
}
if (run_start == -1 || pixel != run_color) {
if (run_start != -1) {
painter.fill_rectangle({x + run_start, y + dy, dx - run_start, 1}, Color(run_color));
}
run_start = dx;
run_color = pixel;
}
} else {
if (run_start != -1) {
painter.fill_rectangle({x + run_start, y + dy, dx - run_start, 1}, Color(run_color));
run_start = -1;
}
}
}
if (run_start != -1 && x + run_start < 320) {
int width = BIRD_WIDTH - run_start;
if (x + run_start + width > 320) {
width = 320 - (x + run_start);
}
if (width > 0) {
painter.fill_rectangle({x + run_start, y + dy, width, 1}, Color(run_color));
}
}
}
}
void DinoGameView::draw_dino_at(int x, int y, bool is_ducking, bool run_frame) {
if (is_ducking) {
if (run_frame) {
draw_dino_sprite(x, y, dino_ducking_leftstep);
} else {
draw_dino_sprite(x, y, dino_ducking_rightstep);
}
} else {
if (run_frame) {
draw_dino_sprite(x, y, dino_leftstep);
} else {
draw_dino_sprite(x, y, dino_default);
}
}
}
void DinoGameView::check_collision() {
int dino_left = DINO_X;
int dino_right = DINO_X + (ducking ? DINO_DUCK_WIDTH : DINO_WIDTH);
int dino_top = ducking ? DINO_DUCK_Y : (DINO_Y - jumpHeight);
int dino_bottom = dino_top + (ducking ? DINO_DUCK_HEIGHT : DINO_HEIGHT);
// Give a small hitbox reduction for fairness
dino_left += 5;
dino_right -= 5;
dino_top += 5;
dino_bottom -= 5;
// Check cactus collisions
for (int i = 0; i < MAX_OBSTACLES; i++) {
if (obstacles[i].active) {
int ground_y = GAME_AREA_TOP + GAME_AREA_HEIGHT - GROUND_HEIGHT;
int obs_top = ground_y - obstacles[i].height;
int obs_bottom = ground_y;
if (dino_right > obstacles[i].x &&
dino_left < obstacles[i].x + obstacles[i].width &&
dino_bottom > obs_top &&
dino_top < obs_bottom) {
collided = true;
return;
}
}
}
// Check bird collision
if (bird_info.inGame) {
int base_y = (bird_info.y_position == BirdPosition::UP) ? BIRD_Y_UP : BIRD_Y_DOWN;
int bird_y = base_y + bird_info.y_offset;
if (dino_right > bird_info.x_offset + 5 &&
dino_left < bird_info.x_offset + BIRD_WIDTH - 5 &&
dino_bottom > bird_y + 5 &&
dino_top < bird_y + BIRD_HEIGHT - 5) {
collided = true;
}
}
}
void DinoGameView::draw_current_score() {
auto style = *ui::Theme::getInstance()->fg_medium;
if (last_score != score) {
painter.fill_rectangle({10, 28, 60, 14}, Color::black());
painter.draw_string({10, 30}, style, score_to_string(score));
last_score = score;
}
}
void DinoGameView::draw_high_score() {
auto style = *ui::Theme::getInstance()->fg_light;
painter.fill_rectangle({152, 28, 100, 14}, Color::black());
painter.draw_string({152, 30}, style, "HI " + score_to_string(highScore));
}
void DinoGameView::jump() {
if (!jumping) {
jumping = true;
// If we're ducking when we jump, stand up
if (ducking) {
stand();
}
}
}
void DinoGameView::duck() {
if (!jumping) {
ducking = true;
duck_timer = 60; // 1 second at 60 FPS
}
}
void DinoGameView::stand() {
ducking = false;
duck_timer = 0;
}
void DinoGameView::step() {
++steps;
}
bool DinoGameView::on_key(const KeyEvent key) {
if (key == KeyEvent::Select) {
if (game_state == GameState::MENU) {
game_state = GameState::PLAYING;
new_game();
draw_high_score();
} else if (game_state == GameState::PLAYING && !collided) {
jump();
} else if (game_state == GameState::GAME_OVER || collided) {
game_state = GameState::PLAYING; // Restart immediately
new_game();
draw_high_score();
}
} else if (key == KeyEvent::Down) {
if (game_state == GameState::PLAYING && !collided) {
duck();
}
} else if (key == KeyEvent::Up) {
if (game_state == GameState::PLAYING && !collided) {
stand();
}
}
set_dirty();
return true;
}
bool DinoGameView::on_encoder(const EncoderEvent delta) {
(void)delta;
return true;
}
} // namespace ui::external_app::dinogame
+254
View File
@@ -0,0 +1,254 @@
/*
* ------------------------------------------------------------
* | Made by RocketGod |
* | Find me at https://betaskynet.com |
* | Argh matey! |
* ------------------------------------------------------------
*/
/*
* Chrome Dino Game for Portapack Mayhem
* Based on the original DinoGame by various contributors
*/
#ifndef __UI_DINOGAME_H__
#define __UI_DINOGAME_H__
#include "ui.hpp"
#include "ui_navigation.hpp"
#include "event_m0.hpp"
#include "message.hpp"
#include "irq_controls.hpp"
#include "random.hpp"
#include "lpc43xx_cpp.hpp"
#include "ui_widget.hpp"
#include "file.hpp"
#include <string>
#include "app_settings.hpp"
namespace ui::external_app::dinogame {
// Color definitions
enum {
White,
Blue,
Yellow,
Purple,
Green,
Red,
Maroon,
Orange,
Black,
};
// Game constants
#define DINO_WIDTH 34
#define DINO_HEIGHT 36
#define DINO_DUCK_WIDTH 45
#define DINO_DUCK_HEIGHT 22
#define BIRD_WIDTH 34
#define BIRD_HEIGHT 27
#define GROUND_HEIGHT 10
#define GAME_AREA_TOP 78
#define GAME_AREA_HEIGHT 160
#define DINO_X 30
#define DINO_Y (GAME_AREA_TOP + GAME_AREA_HEIGHT - GROUND_HEIGHT - DINO_HEIGHT)
#define DINO_DUCK_Y (GAME_AREA_TOP + GAME_AREA_HEIGHT - GROUND_HEIGHT - DINO_DUCK_HEIGHT)
#define BIRD_Y_UP (GAME_AREA_TOP + 20)
#define BIRD_Y_DOWN (GAME_AREA_TOP + 60)
#define JUMP_MAX_HEIGHT 70
#define JUMP_SPEED 3
#define GAME_SPEED_BASE 3
#define SPRITE_COLOR 0x528A
#define TRANSPARENT_COLOR 0xFFFF
#define MIN_OBSTACLE_DISTANCE 300
#define MAX_OBSTACLE_DISTANCE 600
// Game states
enum class GameState {
MENU,
PLAYING,
GAME_OVER
};
// Walk styles
enum class WalkStyle {
WALKING,
DUCKING
};
// Bird position
enum class BirdPosition {
UP,
DOWN
};
// Structures
struct Pterodactyl {
bool inGame = false;
BirdPosition y_position = BirdPosition::UP;
int16_t x_offset = 320;
int16_t y_offset = 0;
int8_t y_velocity = 0;
bool flop = false;
};
// Simple obstacle structure with proper initialization
struct SimpleObstacle {
bool active;
int16_t x;
int16_t last_x;
uint8_t width;
uint8_t height;
uint8_t type; // 0=small, 1=large, 2=double, 3=triple
SimpleObstacle()
: active(false), x(0), last_x(0), width(0), height(0), type(0) {}
};
// External references to sprite data
extern const uint16_t dino_default[];
extern const uint16_t dino_leftstep[];
extern const uint16_t dino_rightstep[];
extern const uint16_t dino_ducking_leftstep[];
extern const uint16_t dino_ducking_rightstep[];
extern const uint16_t dino_gameover[];
extern const uint16_t pterodactyl_upflop[];
extern const uint16_t pterodactyl_downflop[];
// Global painter
extern Painter painter;
// Function declarations
void cls();
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 check_game_timer();
void game_timer_check();
// Ticker class
using Callback = void (*)(void);
class Ticker {
public:
Ticker() = default;
void attach(Callback func, double delay_sec);
void detach();
};
// Main view class
class DinoGameView : public View {
public:
DinoGameView(NavigationView& nav);
void on_show() override;
std::string title() const override { return "Dino Game"; }
void focus() override { dummy.focus(); }
void paint(Painter& painter) override;
void frame_sync();
bool on_encoder(const EncoderEvent event) override;
bool on_key(KeyEvent key) override;
// Public for timer callback
GameState game_state = GameState::MENU;
void game_loop();
void show_menu();
private:
bool initialized = false;
NavigationView& nav_;
// Game variables
static constexpr uint8_t MAX_OBSTACLES = 1;
SimpleObstacle obstacles[MAX_OBSTACLES];
Pterodactyl bird_info;
int16_t jumpHeight = 0;
uint8_t ground_offset = 0;
uint8_t speed_modifier = 0;
bool runstate = false;
bool jumping = false;
bool falling = false;
bool collided = false;
bool ducking = false;
bool displayedGameOver = false;
bool last_ducking = false;
bool last_runstate = false;
uint32_t steps = 0;
uint32_t score = 0;
uint32_t highScore = 0;
uint32_t last_score = 999999; // Initialize to impossible value
int32_t next_obstacle_distance = 100;
int16_t last_obstacle_x = 320; // Track position of last spawned obstacle
int32_t obstacle_spawn_timer = 0;
// Position tracking for minimal redraw
int16_t last_dino_y = DINO_Y;
int16_t last_bird_x = -1;
int16_t last_bird_y = -1;
uint8_t duck_timer = 0;
// Menu animation
bool menu_initialized = false;
bool blink_state = true;
uint32_t blink_counter = 0;
// Current Score
bool score_drawn = false;
// Game timer
Ticker game_timer;
// Private methods
void init_game();
void new_game();
void update_obstacles();
void spawn_obstacle();
void draw_screen();
void draw_ground();
void draw_obstacle(const SimpleObstacle& obstacle);
void clear_obstacle_area(int x, int width, int height);
void draw_dino_sprite(int x, int y, const uint16_t* sprite);
void draw_bird_sprite(int x, int y, const uint16_t* sprite);
void draw_dino_at(int x, int y, bool is_ducking, bool run_frame);
void manage_bird();
void draw_score();
void draw_high_score();
void draw_current_score();
void show_game_over();
void jump();
void duck();
void stand();
void check_collision();
void step();
uint32_t get_steps() { return steps; }
std::string score_to_string(uint32_t score);
bool easy_mode = false;
Button button_difficulty{
{70, 195, 100, 20},
"Mode: HARD"};
app_settings::SettingsManager settings_{
"dinogame",
app_settings::Mode::NO_RF,
{{"highscore"sv, &highScore},
{"easy_mode"sv, &easy_mode}}};
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::dinogame
#endif /* __UI_DINOGAME_H__ */
+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
};
}
+42 -6
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
@@ -193,6 +194,10 @@ set(EXTCPPSRC
external/breakout/main.cpp
external/breakout/ui_breakout.cpp
#dinogame
external/dinogame/main.cpp
external/dinogame/ui_dinogame.cpp
#doom
external/doom/main.cpp
external/doom/ui_doom.cpp
@@ -211,11 +216,35 @@ 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
#blackjack
external/blackjack/main.cpp
external/blackjack/ui_blackjack.cpp
#battleship
external/battleship/main.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
)
set(EXTAPPLIST
@@ -244,7 +273,7 @@ set(EXTAPPLIST
morse_tx
sstvtx
random_password
#acars_rx
acars_rx
ookbrute
ook_editor
wefax_rx
@@ -265,10 +294,17 @@ set(EXTAPPLIST
snake
stopwatch
breakout
dinogame
doom
debug_pmem
scanner
level
gfxeq
detector_rx
spaceinv
blackjack
battleship
ert
epirb_rx
soundboard
)
+54 -5
View File
@@ -75,6 +75,14 @@ MEMORY
ram_external_app_gfxeq (rwx) : org = 0xADE20000, len = 32k
ram_external_app_noaaapt_rx (rwx) : org = 0xADE30000, len = 32k
ram_external_app_detector_rx (rwx) : org = 0xADE40000, len = 32k
ram_external_app_dinogame (rwx) : org = 0xADE50000, len = 32k
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
}
SECTIONS
@@ -336,7 +344,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));
@@ -359,19 +367,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)
{
@@ -391,6 +399,47 @@ SECTIONS
*(*ui*external_app*detector_rx*);
} > ram_external_app_detector_rx
.external_app_dinogame : ALIGN(4) SUBALIGN(4)
{
KEEP(*(.external_app.app_dinogame.application_information));
*(*ui*external_app*dinogame*);
} > ram_external_app_dinogame
.external_app_spaceinv : ALIGN(4) SUBALIGN(4)
{
KEEP(*(.external_app.app_spaceinv.application_information));
*(*ui*external_app*spaceinv*);
} > ram_external_app_spaceinv
.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)
{
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
}
+1 -1
View File
@@ -1,6 +1,6 @@
/*
* Copyright (C) 2024 HTotoo
* Copyright (C) 2025 RocketGod - Added modes from my Flipper Zero RF Jammer App - https://betaskynet.com
* Copyright (C) 2025 RocketGod
*
* This file is part of PortaPack.
*
+1 -1
View File
@@ -1,6 +1,6 @@
/*
* Copyright (C) 2024 HTotoo
* Copyright (C) 2025 RocketGod - Added modes from my Flipper Zero RF Jammer App - https://betaskynet.com
* Copyright (C) 2025 RocketGod
*
* This file is part of PortaPack.
*
+20 -5
View File
@@ -78,7 +78,7 @@ void HopperView::set_hopper_channel(uint32_t i, uint32_t width, uint64_t center,
hopper_channels[i].enabled = true;
hopper_channels[i].width = (width * 0xFFFFFFULL) / 1536000;
hopper_channels[i].center = center;
hopper_channels[i].duration = 30720 * duration;
hopper_channels[i].duration = duration ? 3072 * duration : 10;
}
void HopperView::start_tx() {
@@ -274,7 +274,7 @@ HopperView::HopperView(
options_type.set_selected_index(3); // Rand CW
options_speed.set_selected_index(3); // 10kHz
options_hop.set_selected_index(1); // 50ms
options_hop.set_selected_index(3); // 50ms
button_transmit.set_style(&style_val);
field_timetx.set_value(30);
@@ -327,10 +327,25 @@ HopperView::HopperView(
};
button_transmit.on_select = [this](Button&) {
if (jamming || cooling)
if (jamming || cooling) {
stop_tx();
else
start_tx();
} else {
// if hop speed is 0, alert the user that this will cause a freeze on UI
if (options_hop.selected_index_value() == 0) {
nav_.display_modal(
"Warning", "Hopping set to 0ms (fastest).\n\nTHIS WILL FREEZE THE HACKRF,\npress RESET button to stop\n\nAre you sure?", YESNO, [this](bool choice) {
if (choice) {
// Wait for UI update before the freeze
chThdSleepMilliseconds(50);
start_tx();
}
},
TRUE);
} else {
// if hop speed is not 0, just start the transmission
start_tx();
}
}
};
menu_freq_list.on_left = [this]() {
+17 -8
View File
@@ -143,6 +143,13 @@ class HopperView : public View {
{"FM tone", 1},
{"CW sweep", 2},
{"Rand CW", 3},
{"Sine", 4},
{"Square", 5},
{"Sawtooth", 6},
{"Triangle", 7},
{"Chirp", 8},
{"Gauss", 9},
{"Brute", 10},
}};
Text text_range_number{
@@ -163,14 +170,16 @@ class HopperView : public View {
OptionsField options_hop{
{7 * 8, 27 * 8},
5,
{{"10ms ", 1},
{"50ms ", 5},
{"100ms", 10},
{"1s ", 100},
{"2s ", 200},
{"5s ", 500},
{"10s ", 1000}}};
6,
{{"0ms !!", 0},
{"1ms ", 1},
{"10ms ", 10},
{"50ms ", 50},
{"100ms", 100},
{"1s ", 1000},
{"2s ", 2000},
{"5s ", 5000},
{"10s ", 10000}}};
NumberField field_timetx{
{7 * 8, 29 * 8},
+1 -1
View File
@@ -274,7 +274,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;
+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__*/
+55
View File
@@ -0,0 +1,55 @@
/*
* ------------------------------------------------------------
* | Made by RocketGod |
* | Find me at https://betaskynet.com |
* | Argh matey! |
* ------------------------------------------------------------
*/
#include "ui.hpp"
#include "ui_spaceinv.hpp"
#include "ui_navigation.hpp"
#include "external_app.hpp"
namespace ui::external_app::spaceinv {
void initialize_app(ui::NavigationView& nav) {
nav.push<SpaceInvadersView>();
}
} // namespace ui::external_app::spaceinv
extern "C" {
__attribute__((section(".external_app.app_spaceinv.application_information"), used)) application_information_t _application_information_spaceinv = {
(uint8_t*)0x00000000,
ui::external_app::spaceinv::initialize_app,
CURRENT_HEADER_VERSION,
VERSION_MD5,
"Space Invaders",
{
// Space Invader alien icon 16x16
0x00, 0x18, // .......##.......
0x00, 0x3C, // ......####......
0x00, 0x7E, // .....######.....
0x00, 0xDB, // ....##.##.##....
0x00, 0xFF, // ....########....
0x00, 0xFF, // ....########....
0x00, 0x24, // ......#..#......
0x00, 0x66, // .....##..##.....
0x00, 0x42, // .....#....#.....
0x00, 0x00, // ................
0x00, 0x24, // ......#..#......
0x00, 0x18, // .......##.......
0x00, 0x24, // ......#..#......
0x00, 0x42, // .....#....#.....
0x00, 0x81, // ....#......#....
0x00, 0x00, // ................
},
ui::Color::magenta().v,
app_location_t::GAMES,
-1,
{0, 0, 0, 0},
0x00000000,
};
} // namespace ui::external_app::spaceinv
+879
View File
@@ -0,0 +1,879 @@
/*
* ------------------------------------------------------------
* | Made by RocketGod |
* | Find me at https://betaskynet.com |
* | Argh matey! |
* ------------------------------------------------------------
*/
#include "ui_spaceinv.hpp"
namespace ui::external_app::spaceinv {
// Game constants
#define PLAYER_WIDTH 26
#define PLAYER_HEIGHT 16
#define PLAYER_Y 280
#define BULLET_WIDTH 3
#define BULLET_HEIGHT 10
#define BULLET_SPEED 8
#define MAX_BULLETS 3
#define INVADER_WIDTH 20
#define INVADER_HEIGHT 16
#define INVADER_ROWS 5 // Classic 5 rows
#define INVADER_COLS 6 // Reduced for more movement space on narrow PP screen
#define INVADER_GAP_X 10
#define INVADER_GAP_Y 8 // Gap between invaders
#define MAX_ENEMY_BULLETS 3
#define ENEMY_BULLET_SPEED 3
// Game state
static int game_state = 0; // 0=menu, 1=playing, 2=game over, 3=wave_complete
static bool initialized = false;
static int player_x = 120;
static uint32_t score = 0;
static int lives = 3;
static uint32_t wave = 1;
static uint32_t speed_bonus = 0;
static uint32_t wave_complete_timer = 0;
// Cannon Bullets
static int bullet_x[MAX_BULLETS] = {0, 0, 0};
static int bullet_y[MAX_BULLETS] = {0, 0, 0};
static bool bullet_active[MAX_BULLETS] = {false, false, false};
// Enemy bullets
static int enemy_bullet_x[MAX_ENEMY_BULLETS] = {0, 0, 0};
static int enemy_bullet_y[MAX_ENEMY_BULLETS] = {0, 0, 0};
static bool enemy_bullet_active[MAX_ENEMY_BULLETS] = {false, false, false};
static uint32_t enemy_fire_counter = 0;
// Invaders
static bool invaders[INVADER_ROWS][INVADER_COLS];
static int invaders_x = 40;
static int invaders_y = 60;
static int invader_direction = 1;
static uint32_t invader_move_counter = 0;
static uint32_t invader_move_delay = 30;
static bool invader_animation_frame = false;
// Menu state
static bool menu_initialized = false;
static bool game_over_initialized = false;
static bool blink_state = true;
static uint32_t blink_counter = 0;
static int16_t prompt_x = 0;
// Timer
static Ticker game_timer;
static Callback game_update_callback = nullptr;
static uint32_t game_update_timeout = 0;
static uint32_t game_update_counter = 0;
// Painter
static Painter painter;
// For high score access
static SpaceInvadersView* current_instance = nullptr;
void check_game_timer() {
if (game_update_callback) {
if (++game_update_counter >= game_update_timeout) {
game_update_counter = 0;
game_update_callback();
}
}
}
void Ticker::attach(Callback func, double delay_sec) {
game_update_callback = func;
game_update_timeout = delay_sec * 60;
}
void Ticker::detach() {
game_update_callback = nullptr;
}
static void init_invaders() {
// Initialize invader array
for (int row = 0; row < INVADER_ROWS; row++) {
for (int col = 0; col < INVADER_COLS; col++) {
invaders[row][col] = true;
}
}
invaders_x = 40;
invaders_y = 60;
invader_direction = 1;
invader_move_counter = 0;
// Clear any active enemy bullets
for (int i = 0; i < MAX_ENEMY_BULLETS; i++) {
enemy_bullet_active[i] = false;
}
// Clear any active player bullets
for (int i = 0; i < MAX_BULLETS; i++) {
bullet_active[i] = false;
}
}
static void save_high_score() {
if (current_instance && score > current_instance->highScore) {
current_instance->highScore = score;
// Settings are automatically saved when the view is destroyed
}
}
static void init_menu() {
painter.fill_rectangle({0, 0, 240, 320}, Color::black());
auto style_green = *ui::Theme::getInstance()->fg_green;
auto style_yellow = *ui::Theme::getInstance()->fg_yellow;
auto style_cyan = *ui::Theme::getInstance()->fg_cyan;
int16_t screen_width = 240;
int16_t title_x = (screen_width - 15 * 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 = 12 * 8;
prompt_x = (screen_width - prompt_width) / 2;
painter.fill_rectangle({0, 30, screen_width, 30}, Color::black());
painter.draw_string({title_x, 42}, style_green, "SPACE INVADERS");
painter.draw_string({divider_x, 70}, style_yellow, "========================");
painter.fill_rectangle({instruction_x - 5, 100, instruction_width + 10, 80}, Color::black());
painter.draw_rectangle({instruction_x - 5, 100, instruction_width + 10, 80}, Color::white());
painter.draw_string({instruction_x, 110}, style_cyan, " ROTARY: MOVE SHIP");
painter.draw_string({instruction_x, 130}, style_cyan, " SELECT: FIRE");
painter.draw_string({instruction_x, 150}, style_cyan, " DEFEND EARTH!");
// Draw point values
auto style_purple = *ui::Theme::getInstance()->fg_magenta;
painter.draw_string({50, 190}, style_purple, "TOP ROW = 30 PTS");
painter.draw_string({50, 205}, style_yellow, "MID ROW = 20 PTS");
painter.draw_string({50, 220}, style_green, "BOT ROW = 10 PTS");
// Draw high score
if (current_instance) {
auto style_white = *ui::Theme::getInstance()->fg_light;
std::string high_score_text = "HIGH SCORE: " + std::to_string(current_instance->highScore);
int16_t high_score_x = (screen_width - high_score_text.length() * 8) / 2;
painter.draw_string({high_score_x, 240}, style_white, high_score_text);
}
menu_initialized = true;
}
static void init_game_over() {
painter.fill_rectangle({0, 0, 240, 320}, Color::black());
auto style_red = *ui::Theme::getInstance()->fg_red;
auto style_yellow = *ui::Theme::getInstance()->fg_yellow;
auto style_cyan = *ui::Theme::getInstance()->fg_cyan;
auto style_white = *ui::Theme::getInstance()->fg_light;
int16_t screen_width = 240;
// Game Over title
int16_t title_x = (screen_width - 9 * 8) / 2;
painter.draw_string({title_x, 42}, style_red, "GAME OVER");
// Divider
int16_t divider_width = 24 * 8;
int16_t divider_x = (screen_width - divider_width) / 2;
painter.draw_string({divider_x, 70}, style_yellow, "========================");
// Score box
int16_t box_width = 22 * 8;
int16_t box_x = (screen_width - box_width) / 2;
painter.fill_rectangle({box_x - 5, 100, box_width + 10, 80}, Color::black());
painter.draw_rectangle({box_x - 5, 100, box_width + 10, 80}, Color::white());
// Display scores
std::string score_text = "SCORE: " + std::to_string(score);
int16_t score_x = (screen_width - score_text.length() * 8) / 2;
painter.draw_string({score_x, 120}, style_cyan, score_text);
std::string wave_text = "WAVE: " + std::to_string(wave);
int16_t wave_x = (screen_width - wave_text.length() * 8) / 2;
painter.draw_string({wave_x, 140}, style_cyan, wave_text);
// High score section
if (current_instance) {
if (score > current_instance->highScore) {
// New high score!
std::string new_high = "NEW HIGH SCORE!";
int16_t new_high_x = (screen_width - new_high.length() * 8) / 2;
painter.draw_string({new_high_x, 200}, style_yellow, new_high);
std::string high_score_text = std::to_string(score);
int16_t high_score_x = (screen_width - high_score_text.length() * 8) / 2;
painter.draw_string({high_score_x, 220}, style_yellow, high_score_text);
} else {
// Show existing high score
std::string high_label = "HIGH SCORE:";
int16_t label_x = (screen_width - high_label.length() * 8) / 2;
painter.draw_string({label_x, 200}, style_white, high_label);
std::string high_score_text = std::to_string(current_instance->highScore);
int16_t high_score_x = (screen_width - high_score_text.length() * 8) / 2;
painter.draw_string({high_score_x, 220}, style_white, high_score_text);
}
}
game_over_initialized = true;
}
static void draw_player() {
// Clear the entire player area
painter.fill_rectangle({0, PLAYER_Y - 4, 240, PLAYER_HEIGHT + 8}, Color::black());
// Draw the classic Space Invaders cannon
Color green = Color::green();
// Top part - the cannon barrel
painter.draw_hline({player_x + 12, PLAYER_Y}, 2, green);
painter.draw_hline({player_x + 12, PLAYER_Y + 1}, 2, green);
// Upper body
painter.draw_hline({player_x + 11, PLAYER_Y + 2}, 4, green);
painter.draw_hline({player_x + 11, PLAYER_Y + 3}, 4, green);
painter.draw_hline({player_x + 10, PLAYER_Y + 4}, 6, green);
painter.draw_hline({player_x + 10, PLAYER_Y + 5}, 6, green);
// Main body
painter.draw_hline({player_x + 2, PLAYER_Y + 6}, 22, green);
painter.draw_hline({player_x + 2, PLAYER_Y + 7}, 22, green);
painter.draw_hline({player_x + 1, PLAYER_Y + 8}, 24, green);
painter.draw_hline({player_x + 1, PLAYER_Y + 9}, 24, green);
painter.draw_hline({player_x, PLAYER_Y + 10}, 26, green);
painter.draw_hline({player_x, PLAYER_Y + 11}, 26, green);
painter.draw_hline({player_x, PLAYER_Y + 12}, 26, green);
painter.draw_hline({player_x, PLAYER_Y + 13}, 26, green);
painter.draw_hline({player_x, PLAYER_Y + 14}, 26, green);
painter.draw_hline({player_x, PLAYER_Y + 15}, 26, green);
}
static void draw_invader(int row, int col) {
int x = invaders_x + col * (INVADER_WIDTH + INVADER_GAP_X);
int y = invaders_y + row * (INVADER_HEIGHT + INVADER_GAP_Y);
// Clear the area first
painter.fill_rectangle({x, y, INVADER_WIDTH, INVADER_HEIGHT}, Color::black());
// Different colors and shapes for different rows
Color color;
if (row == 0) {
color = Color::red(); // Top row - 30 points
} else if (row == 1 || row == 2) {
color = Color::yellow(); // Middle rows - 20 points
} else {
color = Color::green(); // Bottom rows - 10 points
}
// Draw different invader types based on row
if (row == 0) {
// Top row - squid-like invader (30 points)
if (invader_animation_frame) {
// Frame 1 - arms down
painter.draw_hline({x + 8, y + 2}, 4, color);
painter.draw_hline({x + 6, y + 3}, 8, color);
painter.draw_hline({x + 4, y + 4}, 12, color);
painter.draw_hline({x + 2, y + 5}, 16, color);
painter.draw_hline({x + 2, y + 6}, 16, color);
painter.draw_hline({x, y + 7}, 20, color);
painter.draw_hline({x, y + 8}, 20, color);
painter.draw_hline({x + 2, y + 9}, 2, color);
painter.draw_hline({x + 6, y + 9}, 8, color);
painter.draw_hline({x + 16, y + 9}, 2, color);
painter.draw_hline({x + 4, y + 10}, 2, color);
painter.draw_hline({x + 14, y + 10}, 2, color);
} else {
// Frame 2 - arms up
painter.draw_hline({x + 8, y + 2}, 4, color);
painter.draw_hline({x + 6, y + 3}, 8, color);
painter.draw_hline({x + 4, y + 4}, 12, color);
painter.draw_hline({x + 2, y + 5}, 16, color);
painter.draw_hline({x + 2, y + 6}, 16, color);
painter.draw_hline({x, y + 7}, 20, color);
painter.draw_hline({x, y + 8}, 20, color);
painter.draw_hline({x + 4, y + 9}, 2, color);
painter.draw_hline({x + 8, y + 9}, 4, color);
painter.draw_hline({x + 14, y + 9}, 2, color);
painter.draw_hline({x + 2, y + 10}, 2, color);
painter.draw_hline({x + 16, y + 10}, 2, color);
}
} else if (row == 1 || row == 2) {
// Middle rows - crab-like invader (20 points)
if (invader_animation_frame) {
// Frame 1 - arms in
painter.draw_hline({x + 4, y + 2}, 2, color);
painter.draw_hline({x + 14, y + 2}, 2, color);
painter.draw_hline({x + 2, y + 3}, 2, color);
painter.draw_hline({x + 6, y + 3}, 8, color);
painter.draw_hline({x + 16, y + 3}, 2, color);
painter.draw_hline({x + 2, y + 4}, 16, color);
painter.draw_hline({x, y + 5}, 6, color);
painter.draw_hline({x + 8, y + 5}, 4, color);
painter.draw_hline({x + 14, y + 5}, 6, color);
painter.draw_hline({x, y + 6}, 20, color);
painter.draw_hline({x + 2, y + 7}, 16, color);
painter.draw_hline({x + 4, y + 8}, 2, color);
painter.draw_hline({x + 14, y + 8}, 2, color);
painter.draw_hline({x + 2, y + 9}, 4, color);
painter.draw_hline({x + 14, y + 9}, 4, color);
} else {
// Frame 2 - arms out
painter.draw_hline({x + 4, y + 2}, 2, color);
painter.draw_hline({x + 14, y + 2}, 2, color);
painter.draw_hline({x + 2, y + 3}, 2, color);
painter.draw_hline({x + 6, y + 3}, 8, color);
painter.draw_hline({x + 16, y + 3}, 2, color);
painter.draw_hline({x + 2, y + 4}, 16, color);
painter.draw_hline({x, y + 5}, 6, color);
painter.draw_hline({x + 8, y + 5}, 4, color);
painter.draw_hline({x + 14, y + 5}, 6, color);
painter.draw_hline({x, y + 6}, 20, color);
painter.draw_hline({x + 2, y + 7}, 16, color);
painter.draw_hline({x + 4, y + 8}, 2, color);
painter.draw_hline({x + 14, y + 8}, 2, color);
painter.draw_hline({x, y + 9}, 2, color);
painter.draw_hline({x + 6, y + 9}, 2, color);
painter.draw_hline({x + 12, y + 9}, 2, color);
painter.draw_hline({x + 18, y + 9}, 2, color);
}
} else {
// Bottom rows - octopus-like invader (10 points)
if (invader_animation_frame) {
// Frame 1
painter.draw_hline({x + 6, y + 2}, 8, color);
painter.draw_hline({x + 4, y + 3}, 12, color);
painter.draw_hline({x + 2, y + 4}, 16, color);
painter.draw_hline({x, y + 5}, 20, color);
painter.draw_hline({x, y + 6}, 20, color);
painter.draw_hline({x + 4, y + 7}, 4, color);
painter.draw_hline({x + 12, y + 7}, 4, color);
painter.draw_hline({x + 2, y + 8}, 4, color);
painter.draw_hline({x + 8, y + 8}, 4, color);
painter.draw_hline({x + 14, y + 8}, 4, color);
painter.draw_hline({x + 4, y + 9}, 2, color);
painter.draw_hline({x + 14, y + 9}, 2, color);
} else {
// Frame 2
painter.draw_hline({x + 6, y + 2}, 8, color);
painter.draw_hline({x + 4, y + 3}, 12, color);
painter.draw_hline({x + 2, y + 4}, 16, color);
painter.draw_hline({x, y + 5}, 20, color);
painter.draw_hline({x, y + 6}, 20, color);
painter.draw_hline({x + 4, y + 7}, 4, color);
painter.draw_hline({x + 12, y + 7}, 4, color);
painter.draw_hline({x + 2, y + 8}, 4, color);
painter.draw_hline({x + 8, y + 8}, 4, color);
painter.draw_hline({x + 14, y + 8}, 4, color);
painter.draw_hline({x + 2, y + 9}, 2, color);
painter.draw_hline({x + 16, y + 9}, 2, color);
}
}
}
static void draw_all_invaders() {
for (int row = 0; row < INVADER_ROWS; row++) {
for (int col = 0; col < INVADER_COLS; col++) {
if (invaders[row][col]) {
draw_invader(row, col);
}
}
}
}
static void clear_all_invaders() {
for (int row = 0; row < INVADER_ROWS; row++) {
for (int col = 0; col < INVADER_COLS; col++) {
if (invaders[row][col]) {
int x = invaders_x + col * (INVADER_WIDTH + INVADER_GAP_X);
int y = invaders_y + row * (INVADER_HEIGHT + INVADER_GAP_Y);
painter.fill_rectangle({x, y, INVADER_WIDTH, INVADER_HEIGHT}, Color::black());
}
}
}
}
static void clear_all_enemy_bullets() {
// Clear any visible enemy bullets
for (int i = 0; i < MAX_ENEMY_BULLETS; i++) {
if (enemy_bullet_active[i]) {
painter.fill_rectangle({enemy_bullet_x[i], enemy_bullet_y[i], BULLET_WIDTH, BULLET_HEIGHT}, Color::black());
enemy_bullet_active[i] = false;
}
}
}
static void fire_enemy_bullet() {
// Find a random invader to fire from
int active_count = 0;
for (int row = 0; row < INVADER_ROWS; row++) {
for (int col = 0; col < INVADER_COLS; col++) {
if (invaders[row][col]) active_count++;
}
}
if (active_count == 0) return;
int shooter = rand() % active_count;
int count = 0;
for (int row = 0; row < INVADER_ROWS; row++) {
for (int col = 0; col < INVADER_COLS; col++) {
if (invaders[row][col]) {
if (count == shooter) {
// Fire from this invader
for (int i = 0; i < MAX_ENEMY_BULLETS; i++) {
if (!enemy_bullet_active[i]) {
int x = invaders_x + col * (INVADER_WIDTH + INVADER_GAP_X);
int y = invaders_y + row * (INVADER_HEIGHT + INVADER_GAP_Y);
enemy_bullet_x[i] = x + INVADER_WIDTH / 2;
enemy_bullet_y[i] = y + INVADER_HEIGHT;
enemy_bullet_active[i] = true;
return;
}
}
}
count++;
}
}
}
}
static void update_enemy_bullets() {
for (int i = 0; i < MAX_ENEMY_BULLETS; i++) {
if (enemy_bullet_active[i]) {
// Clear old position - but protect the border line
if (enemy_bullet_y[i] != 49) { // Don't clear if we're exactly on the border line
painter.fill_rectangle({enemy_bullet_x[i], enemy_bullet_y[i], BULLET_WIDTH, BULLET_HEIGHT}, Color::black());
}
enemy_bullet_y[i] += ENEMY_BULLET_SPEED;
if (enemy_bullet_y[i] > 320) {
enemy_bullet_active[i] = false;
} else {
// Draw at new position
painter.fill_rectangle({enemy_bullet_x[i], enemy_bullet_y[i], BULLET_WIDTH, BULLET_HEIGHT}, Color::red());
// Check collision with player
if (enemy_bullet_x[i] >= player_x &&
enemy_bullet_x[i] <= player_x + PLAYER_WIDTH &&
enemy_bullet_y[i] >= PLAYER_Y &&
enemy_bullet_y[i] <= PLAYER_Y + PLAYER_HEIGHT) {
// Player hit!
enemy_bullet_active[i] = false;
lives--;
// Update lives display
auto style = *ui::Theme::getInstance()->fg_green;
painter.fill_rectangle({5, 30, 100, 20}, Color::black());
painter.draw_string({5, 30}, style, "Lives: " + std::to_string(lives));
if (lives <= 0) {
game_state = 2; // Game over
save_high_score();
}
}
}
}
}
}
static void update_invaders() {
uint32_t adjusted_delay = (speed_bonus >= invader_move_delay) ? 1 : invader_move_delay - speed_bonus;
if (++invader_move_counter >= adjusted_delay) {
invader_move_counter = 0;
// Clear old positions
clear_all_invaders();
// Check bounds - find the actual leftmost and rightmost invaders
int leftmost = INVADER_COLS;
int rightmost = -1;
for (int col = 0; col < INVADER_COLS; col++) {
for (int row = 0; row < INVADER_ROWS; row++) {
if (invaders[row][col]) {
if (col < leftmost) leftmost = col;
if (col > rightmost) rightmost = col;
}
}
}
bool hit_edge = false;
if (invader_direction > 0) {
// Moving right - check rightmost invader
int right_edge = invaders_x + rightmost * (INVADER_WIDTH + INVADER_GAP_X) + INVADER_WIDTH;
if (right_edge >= 240) {
hit_edge = true;
}
} else {
// Moving left - check leftmost invader
int left_edge = invaders_x + leftmost * (INVADER_WIDTH + INVADER_GAP_X);
if (left_edge <= 0) {
hit_edge = true;
}
}
// Move invaders
if (hit_edge) {
invaders_y += 15; // Move down
invader_direction = -invader_direction;
} else {
invaders_x += invader_direction * 8; // Horizontal movement
}
// Toggle animation frame
invader_animation_frame = !invader_animation_frame;
// Draw new positions
draw_all_invaders();
}
// Enemy firing logic - only in hard mode or always with lower frequency
if (!current_instance || !current_instance->easy_mode) {
if (++enemy_fire_counter >= 120) { // Fire every 2 seconds at 60fps
enemy_fire_counter = 0;
fire_enemy_bullet();
}
}
}
static void fire_bullet() {
for (int i = 0; i < MAX_BULLETS; i++) {
if (!bullet_active[i]) {
bullet_active[i] = true;
bullet_x[i] = player_x + PLAYER_WIDTH / 2 - BULLET_WIDTH / 2;
bullet_y[i] = PLAYER_Y - BULLET_HEIGHT;
break;
}
}
}
static void update_bullets() {
for (int i = 0; i < MAX_BULLETS; i++) {
if (bullet_active[i]) {
painter.fill_rectangle({bullet_x[i], bullet_y[i], BULLET_WIDTH, BULLET_HEIGHT}, Color::black());
bullet_y[i] -= BULLET_SPEED;
if (bullet_y[i] < 50) {
bullet_active[i] = false;
} else {
painter.fill_rectangle({bullet_x[i], bullet_y[i], BULLET_WIDTH, BULLET_HEIGHT}, Color::white());
}
}
}
}
static void check_collisions() {
for (int i = 0; i < MAX_BULLETS; i++) {
if (!bullet_active[i]) continue;
for (int row = 0; row < INVADER_ROWS; row++) {
for (int col = 0; col < INVADER_COLS; col++) {
if (!invaders[row][col]) continue;
int inv_x = invaders_x + col * (INVADER_WIDTH + INVADER_GAP_X);
int inv_y = invaders_y + row * (INVADER_HEIGHT + INVADER_GAP_Y);
// Simple collision check
if (bullet_x[i] < inv_x + INVADER_WIDTH &&
bullet_x[i] + BULLET_WIDTH > inv_x &&
bullet_y[i] < inv_y + INVADER_HEIGHT &&
bullet_y[i] + BULLET_HEIGHT > inv_y) {
// Hit!
bullet_active[i] = false;
painter.fill_rectangle({bullet_x[i], bullet_y[i], BULLET_WIDTH, BULLET_HEIGHT}, Color::black());
invaders[row][col] = false;
painter.fill_rectangle({inv_x, inv_y, INVADER_WIDTH, INVADER_HEIGHT}, Color::black());
// Score based on row: top=30, middle=20, bottom=10
if (row == 0) {
score += 30;
} else if (row == 1 || row == 2) {
score += 20;
} else {
score += 10;
}
return;
}
}
}
}
}
static void check_wave_complete() {
// Check if all invaders are destroyed
bool all_destroyed = true;
for (int row = 0; row < INVADER_ROWS; row++) {
for (int col = 0; col < INVADER_COLS; col++) {
if (invaders[row][col]) {
all_destroyed = false;
break;
}
}
if (!all_destroyed) break;
}
if (all_destroyed) {
// Next wave!
wave++;
speed_bonus = (wave - 1) * 3; // Each wave is slightly faster
if (speed_bonus > 15) speed_bonus = 15; // Cap the speed increase
// Clear any enemy bullets before transitioning
clear_all_enemy_bullets();
// Set state to wave complete and start timer
game_state = 3;
wave_complete_timer = 60; // 1 second at 60fps
// Clear screen and show wave message - but preserve the border line
painter.fill_rectangle({0, 51, 240, 269}, Color::black()); // Start at 51 to preserve line at 49
// Redraw the border line to ensure it's intact - stupid bullet clearing wants to damage it
painter.draw_hline({0, 49}, 240, Color::white());
auto style = *ui::Theme::getInstance()->fg_green;
std::string wave_text = "WAVE " + std::to_string(wave);
int wave_x = (240 - wave_text.length() * 8) / 2;
painter.draw_string({wave_x, 150}, style, wave_text);
return;
}
// Check if invaders reached player
for (int row = 0; row < INVADER_ROWS; row++) {
for (int col = 0; col < INVADER_COLS; col++) {
if (invaders[row][col]) {
int y = invaders_y + row * (INVADER_HEIGHT + INVADER_GAP_Y);
if (y + INVADER_HEIGHT >= PLAYER_Y) { // Actual collision with player
game_state = 2; // Game over
save_high_score();
return;
}
}
}
}
}
void game_timer_check() {
if (game_state == 0) {
// Menu state
if (!menu_initialized) {
init_menu();
}
if (++blink_counter >= 30) {
blink_counter = 0;
blink_state = !blink_state;
auto style = *ui::Theme::getInstance()->fg_red;
if (blink_state) {
painter.draw_string({prompt_x, 260}, style, "PRESS SELECT");
} else {
painter.fill_rectangle({prompt_x, 260, 16 * 8, 20}, Color::black());
}
}
} else if (game_state == 1) {
// Playing state
update_bullets();
update_enemy_bullets();
update_invaders();
check_collisions();
check_wave_complete();
// Update score display
static uint32_t last_score = 999999;
if (score != last_score) {
last_score = score;
auto style = *ui::Theme::getInstance()->fg_green;
painter.fill_rectangle({5, 10, 100, 20}, Color::black());
painter.draw_string({5, 10}, style, "Score: " + std::to_string(score));
// Redraw border line after score update (in case it was damaged) - stupid bullet clearing wants to damage it
painter.draw_hline({0, 49}, 240, Color::white());
}
// Periodically redraw the border line to fix any damage because it's a pain in the ass
static uint32_t border_redraw_counter = 0;
if (++border_redraw_counter >= 60) { // Once per second - might make less if causing flickering
border_redraw_counter = 0;
painter.draw_hline({0, 49}, 240, Color::white());
}
} else if (game_state == 2) {
// Game over state
if (!game_over_initialized) {
init_game_over();
}
if (++blink_counter >= 30) {
blink_counter = 0;
blink_state = !blink_state;
auto style = *ui::Theme::getInstance()->fg_red;
if (blink_state) {
painter.draw_string({prompt_x, 260}, style, "PRESS SELECT");
} else {
painter.fill_rectangle({prompt_x, 260, 16 * 8, 20}, Color::black());
}
}
} else if (game_state == 3) {
// Wave complete state - wait for timer
if (--wave_complete_timer <= 0) {
// Timer expired, start next wave
game_state = 1;
// Clear and redraw game area - preserve border line
painter.fill_rectangle({0, 51, 240, 269}, Color::black()); // Start at 51 to preserve line
// Restore UI
auto style = *ui::Theme::getInstance()->fg_green;
painter.draw_string({5, 10}, style, "Score: " + std::to_string(score));
painter.draw_string({5, 30}, style, "Lives: " + std::to_string(lives));
// Redraw the border line in case it was damaged again
painter.draw_hline({0, 49}, 240, Color::white());
// Initialize new wave of invaders
init_invaders();
draw_all_invaders();
draw_player();
}
}
}
SpaceInvadersView::SpaceInvadersView(NavigationView& nav)
: nav_{nav} {
add_children({&dummy, &button_difficulty});
current_instance = this;
game_timer.attach(&game_timer_check, 1.0 / 60.0);
// Update button text based on loaded setting
button_difficulty.set_text(easy_mode ? "Mode: EASY" : "Mode: HARD");
button_difficulty.on_select = [this](Button&) {
easy_mode = !easy_mode;
button_difficulty.set_text(easy_mode ? "Mode: EASY" : "Mode: HARD");
// Settings will be saved when the view is destroyed
};
}
SpaceInvadersView::~SpaceInvadersView() {
// Settings are automatically saved when destroyed
current_instance = nullptr;
}
void SpaceInvadersView::on_show() {
}
void SpaceInvadersView::paint(Painter& painter) {
(void)painter;
if (!initialized) {
initialized = true;
game_state = 0;
menu_initialized = false;
game_over_initialized = false;
blink_state = true;
blink_counter = 0;
player_x = 107;
score = 0;
lives = 3;
wave = 1;
speed_bonus = 0;
// Initialize arrays
for (int i = 0; i < MAX_BULLETS; i++) {
bullet_active[i] = false;
bullet_x[i] = 0;
bullet_y[i] = 0;
}
for (int i = 0; i < MAX_ENEMY_BULLETS; i++) {
enemy_bullet_active[i] = false;
enemy_bullet_x[i] = 0;
enemy_bullet_y[i] = 0;
}
init_invaders();
}
}
void SpaceInvadersView::frame_sync() {
check_game_timer();
set_dirty();
}
bool SpaceInvadersView::on_encoder(const EncoderEvent delta) {
if (game_state == 1) {
if (delta > 0) {
player_x += 5;
if (player_x > 214) player_x = 214;
draw_player();
} else if (delta < 0) {
player_x -= 5;
if (player_x < 0) player_x = 0;
draw_player();
}
set_dirty();
}
return true;
}
bool SpaceInvadersView::on_key(const KeyEvent key) {
if (key == KeyEvent::Select) {
if (game_state == 0) {
// Start game
game_state = 1;
button_difficulty.hidden(true);
score = 0;
lives = 3;
wave = 1;
speed_bonus = 0;
invader_move_delay = 30;
enemy_fire_counter = 0;
init_invaders();
painter.fill_rectangle({0, 0, 240, 320}, Color::black());
painter.draw_hline({0, 49}, 240, Color::white());
auto style = *ui::Theme::getInstance()->fg_green;
painter.draw_string({5, 10}, style, "Score: 0");
painter.draw_string({5, 30}, style, "Lives: 3");
draw_all_invaders();
draw_player();
set_dirty();
} else if (game_state == 1) {
fire_bullet();
} else if (game_state == 2) {
// Return to menu
game_state = 0;
menu_initialized = false;
game_over_initialized = false;
button_difficulty.hidden(false);
set_dirty();
}
}
return true;
}
} // namespace ui::external_app::spaceinv
+79
View File
@@ -0,0 +1,79 @@
/*
* ------------------------------------------------------------
* | Made by RocketGod |
* | Find me at https://betaskynet.com |
* | Argh matey! |
* ------------------------------------------------------------
*/
#ifndef __UI_SPACEINV_H__
#define __UI_SPACEINV_H__
#include "ui.hpp"
#include "ui_navigation.hpp"
#include "event_m0.hpp"
#include "message.hpp"
#include "irq_controls.hpp"
#include "random.hpp"
#include "lpc43xx_cpp.hpp"
#include "ui_widget.hpp"
#include "app_settings.hpp"
namespace ui::external_app::spaceinv {
using Callback = void (*)(void);
class Ticker {
public:
Ticker() = default;
void attach(Callback func, double delay_sec);
void detach();
};
void check_game_timer();
void game_timer_check();
class SpaceInvadersView : public View {
public:
SpaceInvadersView(NavigationView& nav);
~SpaceInvadersView(); // Destructor will trigger settings save
void on_show() override;
std::string title() const override { return "Space Invaders"; }
void focus() override { dummy.focus(); }
void paint(Painter& painter) override;
void frame_sync();
bool on_encoder(const EncoderEvent event) override;
bool on_key(KeyEvent key) override;
uint32_t highScore = 0;
bool easy_mode = false;
private:
NavigationView& nav_;
Button button_difficulty{
{70, 285, 100, 20},
"Mode: HARD"};
app_settings::SettingsManager settings_{
"spaceinv",
app_settings::Mode::NO_RF,
{{"highscore"sv, &highScore},
{"easy_mode"sv, &easy_mode}}};
Button dummy{
{240, 0, 0, 0},
""};
MessageHandlerRegistration message_handler_frame_sync{
Message::ID::DisplayFrameSync,
[this](const Message* const) {
this->frame_sync();
}};
};
} // namespace ui::external_app::spaceinv
#endif /* __UI_SPACEINV_H__ */
+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
View File
@@ -52,3 +52,4 @@ const std::filesystem::path ook_editor_dir = u"OOKFILES";
const std::filesystem::path hopper_dir = u"HOPPER";
const std::filesystem::path subghz_dir = u"SUBGHZ";
const std::filesystem::path waterfalls_dir = u"WATERFALLS";
const std::filesystem::path macaddress_dir = u"MACADDRESS";
+1
View File
@@ -54,5 +54,6 @@ extern const std::filesystem::path ook_editor_dir;
extern const std::filesystem::path hopper_dir;
extern const std::filesystem::path subghz_dir;
extern const std::filesystem::path waterfalls_dir;
extern const std::filesystem::path macaddress_dir;
#endif /* __FILE_PATH_H__ */
+2 -2
View File
@@ -28,10 +28,10 @@ Optional<File::Error> LogFile::write_entry(const std::string& entry) {
Optional<File::Error> LogFile::write_entry(const rtc::RTC& datetime, const std::string& entry) {
std::string timestamp = to_string_timestamp(datetime);
return write_line(timestamp + " " + entry);
return write_raw(timestamp + " " + entry);
}
Optional<File::Error> LogFile::write_line(const std::string& message) {
Optional<File::Error> LogFile::write_raw(const std::string& message) {
auto error = file.write_line(message);
if (!error) {
file.sync();
+1 -2
View File
@@ -39,11 +39,10 @@ class LogFile {
Optional<File::Error> write_entry(const std::string& entry);
Optional<File::Error> write_entry(const rtc::RTC& datetime, const std::string& entry);
Optional<File::Error> write_raw(const std::string& message);
private:
File file{};
Optional<File::Error> write_line(const std::string& message);
};
#endif /*__LOG_FILE_H__*/
+1 -1
View File
@@ -24,7 +24,7 @@
namespace ui {
RecentEntriesColumns::RecentEntriesColumns(
const std::initializer_list<RecentEntriesColumn> columns)
std::initializer_list<RecentEntriesColumn> columns)
: _columns{columns} {
}
+18 -5
View File
@@ -133,14 +133,27 @@ class RecentEntriesColumns {
public:
using ContainerType = std::vector<RecentEntriesColumn>;
RecentEntriesColumns(
const std::initializer_list<RecentEntriesColumn> columns);
RecentEntriesColumns(std::initializer_list<RecentEntriesColumn> columns);
ContainerType::const_iterator begin() const { return std::begin(_columns); }
ContainerType::const_iterator end() const { return std::end(_columns); }
ContainerType::iterator begin() { return _columns.begin(); }
ContainerType::iterator end() { return _columns.end(); }
ContainerType::const_iterator begin() const { return _columns.begin(); }
ContainerType::const_iterator end() const { return _columns.end(); }
void set(size_t index, const std::string& name, size_t width) {
if (index < _columns.size()) {
_columns[index] = {name, width};
}
}
const RecentEntriesColumn& at(size_t index) const {
return _columns.at(index);
}
size_t size() const { return _columns.size(); }
private:
const ContainerType _columns;
ContainerType _columns;
};
class RecentEntriesHeader : public Widget {
+136 -1
View File
@@ -1,4 +1,3 @@
#include "theme.hpp"
namespace ui {
@@ -25,6 +24,9 @@ void Theme::SetTheme(ThemeId theme) {
case Red:
current = new ThemeRed();
break;
case Dark:
current = new ThemeDark();
break;
case DefaultGrey:
default:
current = new ThemeDefault();
@@ -725,4 +727,137 @@ ThemeRed::ThemeRed() {
bg_table_header = new Color{205, 30, 0};
}
ThemeDark::ThemeDark() {
bg_lightest = new Style{
.font = font::fixed_8x16,
.background = {32, 32, 32},
.foreground = Color::white(),
};
bg_lightest_small = new Style{
.font = font::fixed_5x8,
.background = {32, 32, 32},
.foreground = Color::white(),
};
bg_light = new Style{
.font = font::fixed_8x16,
.background = {24, 24, 24},
.foreground = Color::white(),
};
bg_medium = new Style{
.font = font::fixed_8x16,
.background = {16, 16, 16},
.foreground = Color::white(),
};
bg_dark = new Style{
.font = font::fixed_8x16,
.background = {8, 8, 8},
.foreground = Color::white(),
};
bg_darker = new Style{
.font = font::fixed_8x16,
.background = {4, 4, 4},
.foreground = Color::white(),
};
bg_darkest = new Style{
.font = font::fixed_8x16,
.background = Color::black(),
.foreground = Color::white(),
};
bg_darkest_small = new Style{
.font = font::fixed_5x8,
.background = Color::black(),
.foreground = Color::white(),
};
bg_important_small = new Style{
.font = font::fixed_5x8,
.background = {64, 64, 64},
.foreground = Color::white(),
};
error_dark = new Style{
.font = font::fixed_8x16,
.background = Color::black(),
.foreground = Color::red(),
};
warning_dark = new Style{
.font = font::fixed_8x16,
.background = Color::black(),
.foreground = Color::yellow(),
};
ok_dark = new Style{
.font = font::fixed_8x16,
.background = Color::black(),
.foreground = Color::green(),
};
fg_dark = new Style{
.font = font::fixed_8x16,
.background = Color::black(),
.foreground = {96, 96, 96},
};
fg_medium = new Style{
.font = font::fixed_8x16,
.background = Color::black(),
.foreground = {128, 128, 128},
};
fg_light = new Style{
.font = font::fixed_8x16,
.background = Color::black(),
.foreground = Color::white(),
};
fg_red = new Style{
.font = font::fixed_8x16,
.background = Color::black(),
.foreground = Color::red(),
};
fg_green = new Style{
.font = font::fixed_8x16,
.background = Color::black(),
.foreground = Color::green(),
};
fg_yellow = new Style{
.font = font::fixed_8x16,
.background = Color::black(),
.foreground = Color::yellow(),
};
fg_orange = new Style{
.font = font::fixed_8x16,
.background = Color::black(),
.foreground = Color::orange(),
};
fg_blue = new Style{
.font = font::fixed_8x16,
.background = Color::black(),
.foreground = Color::blue(),
};
fg_cyan = new Style{
.font = font::fixed_8x16,
.background = Color::black(),
.foreground = Color::cyan(),
};
fg_darkcyan = new Style{
.font = font::fixed_8x16,
.background = Color::black(),
.foreground = Color::dark_cyan(),
};
fg_magenta = new Style{
.font = font::fixed_8x16,
.background = Color::black(),
.foreground = Color::magenta(),
};
option_active = new Style{
.font = font::fixed_8x16,
.background = {64, 64, 64},
.foreground = Color::white(),
};
status_active = new Color{0, 255, 0};
bg_table_header = new Color{48, 48, 48};
}
} // namespace ui
+6
View File
@@ -93,6 +93,11 @@ class ThemeRed : public ThemeTemplate {
ThemeRed();
};
class ThemeDark : public ThemeTemplate {
public:
ThemeDark();
};
class Theme {
public:
enum ThemeId {
@@ -101,6 +106,7 @@ class Theme {
Aqua = 2,
Green = 3,
Red = 4,
Dark = 5,
MAX
};
static ThemeTemplate* getInstance();
+3
View File
@@ -1,4 +1,7 @@
Copyright (C) 2025 HTotoo
# External UI elements
+21
View File
@@ -1,3 +1,24 @@
/*
* Copyright (C) 2025 RocketGod
* Copyright (C) 2025 HTotoo
*
* This file is part of PortaPack.
*
* This program is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation; either version 2, or (at your option)
* any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program; see the file COPYING. If not, write to
* the Free Software Foundation, Inc., 51 Franklin Street,
* Boston, MA 02110-1301, USA.
*/
#include "ui_grapheq.hpp"
+22
View File
@@ -1,3 +1,25 @@
/*
* Copyright (C) 2025 RocketGod
* Copyright (C) 2025 HTotoo
*
* This file is part of PortaPack.
*
* This program is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation; either version 2, or (at your option)
* any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program; see the file COPYING. If not, write to
* the Free Software Foundation, Inc., 51 Franklin Street,
* Boston, MA 02110-1301, USA.
*/
#ifndef __UI_GRAPHEQ_H__
#define __UI_GRAPHEQ_H__
+1
View File
@@ -110,6 +110,7 @@ GeoPos::GeoPos(
text_alt_unit.set(altitude_unit_ ? "m" : "ft");
if (speed_unit_ == KMPH) text_speed_unit.set("kmph");
if (speed_unit_ == MPH) text_speed_unit.set("mph");
if (speed_unit_ == KNOTS) text_speed_unit.set("knots");
if (speed_unit_ == HIDDEN) {
text_speed_unit.hidden(true);
label_spd_position.hidden(true);
+2 -1
View File
@@ -81,6 +81,7 @@ class GeoPos : public View {
NONE = 0,
MPH,
KMPH,
KNOTS,
HIDDEN = 255
};
@@ -134,7 +135,7 @@ class GeoPos : public View {
{12 * 8, 0 * 16, 2 * 8, 16},
""};
Text text_speed_unit{
{25 * 8, 0 * 16, 4 * 8, 16},
{25 * 8, 0 * 16, 5 * 8, 16},
""};
NumberField field_lat_degrees{
+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();
+4 -8
View File
@@ -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();
+22 -15
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
@@ -397,13 +390,6 @@ set(MODE_CPPSRC
)
DeclareTargets(PSON sonde)
### FSK TX
set(MODE_CPPSRC
proc_fsk.cpp
)
DeclareTargets(PFSK fsktx)
### FSK RX
set(MODE_CPPSRC
@@ -411,6 +397,12 @@ set(MODE_CPPSRC
)
DeclareTargets(PFSR fskrx)
### FSK TX
set(MODE_CPPSRC
proc_fsk.cpp
)
DeclareTargets(PFSK fsktx)
### Microphone transmit
@@ -579,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
@@ -683,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};
+134 -129
View File
@@ -27,7 +27,16 @@
#include "event_m4.hpp"
uint32_t BTLERxProcessor::crc_init_reorder(uint32_t crc_init) {
inline float BTLERxProcessor::get_phase_diff(const complex16_t& sample0, const complex16_t& sample1) {
// Calculate the phase difference between two samples.
float dI = sample1.real() * sample0.real() + sample1.imag() * sample0.imag();
float dQ = sample1.imag() * sample0.real() - sample1.real() * sample0.imag();
float phase_diff = atan2f(dQ, dI);
return phase_diff;
}
inline uint32_t BTLERxProcessor::crc_init_reorder(uint32_t crc_init) {
int i;
uint32_t crc_init_tmp, crc_init_input, crc_init_input_tmp;
@@ -53,7 +62,7 @@ uint32_t BTLERxProcessor::crc_init_reorder(uint32_t crc_init) {
return (crc_init_tmp);
}
uint_fast32_t BTLERxProcessor::crc_update(uint_fast32_t crc, const void* data, size_t data_len) {
inline uint_fast32_t BTLERxProcessor::crc_update(uint_fast32_t crc, const void* data, size_t data_len) {
const unsigned char* d = (const unsigned char*)data;
unsigned int tbl_idx;
@@ -67,7 +76,7 @@ uint_fast32_t BTLERxProcessor::crc_update(uint_fast32_t crc, const void* data, s
return crc & 0xffffff;
}
uint_fast32_t BTLERxProcessor::crc24_byte(uint8_t* byte_in, int num_byte, uint32_t init_hex) {
inline uint_fast32_t BTLERxProcessor::crc24_byte(uint8_t* byte_in, int num_byte, uint32_t init_hex) {
uint_fast32_t crc = init_hex;
crc = crc_update(crc, byte_in, num_byte);
@@ -75,7 +84,7 @@ uint_fast32_t BTLERxProcessor::crc24_byte(uint8_t* byte_in, int num_byte, uint32
return (crc);
}
bool BTLERxProcessor::crc_check(uint8_t* tmp_byte, int body_len, uint32_t crc_init) {
inline bool BTLERxProcessor::crc_check(uint8_t* tmp_byte, int body_len, uint32_t crc_init) {
int crc24_checksum;
crc24_checksum = crc24_byte(tmp_byte, body_len, crc_init); // 0x555555 --> 0xaaaaaa. maybe because byte order
@@ -87,7 +96,7 @@ bool BTLERxProcessor::crc_check(uint8_t* tmp_byte, int body_len, uint32_t crc_in
return (crc24_checksum != checksumReceived);
}
void BTLERxProcessor::scramble_byte(uint8_t* byte_in, int num_byte, const uint8_t* scramble_table_byte, uint8_t* byte_out) {
inline void BTLERxProcessor::scramble_byte(uint8_t* byte_in, int num_byte, const uint8_t* scramble_table_byte, uint8_t* byte_out) {
int i;
for (i = 0; i < num_byte; i++) {
@@ -95,7 +104,7 @@ void BTLERxProcessor::scramble_byte(uint8_t* byte_in, int num_byte, const uint8_
}
}
int BTLERxProcessor::verify_payload_byte(int num_payload_byte, ADV_PDU_TYPE pdu_type) {
inline int BTLERxProcessor::verify_payload_byte(int num_payload_byte, ADV_PDU_TYPE pdu_type) {
// Should at least have 6 bytes for the MAC Address.
// Also ensuring that there is at least 1 byte of data.
if (num_payload_byte <= 6) {
@@ -122,120 +131,125 @@ int BTLERxProcessor::verify_payload_byte(int num_payload_byte, ADV_PDU_TYPE pdu_
return 0;
}
void BTLERxProcessor::handleBeginState() {
int num_symbol_left = dst_buffer.count / SAMPLE_PER_SYMBOL; // One buffer sample consist of I and Q.
inline void BTLERxProcessor::resetOffsetTracking() {
frequency_offset = 0.0f;
frequency_offset_estimate = 0.0f;
phase_buffer_index = 0;
memset(phase_buffer, 0, sizeof(phase_buffer));
}
static uint8_t demod_buf_access[SAMPLE_PER_SYMBOL][LEN_DEMOD_BUF_ACCESS];
inline void BTLERxProcessor::resetBitPacketIndex() {
memset(rb_buf, 0, sizeof(rb_buf));
packet_index = 0;
bit_index = 0;
}
uint32_t uint32_tmp = DEFAULT_ACCESS_ADDR;
uint8_t accessAddrBits[LEN_DEMOD_BUF_ACCESS];
inline void BTLERxProcessor::resetToDefaultState() {
parseState = Parse_State_Begin;
resetOffsetTracking();
resetBitPacketIndex();
crc_init_internal = crc_init_reorder(crc_initalVale);
}
uint32_t accesssAddress = 0;
inline void BTLERxProcessor::demodulateFSKBits(int num_demod_byte) {
for (; packet_index < num_demod_byte; packet_index++) {
for (; bit_index < 8; bit_index++) {
if (samples_eaten >= (int)dst_buffer.count) {
return;
}
// Filling up addressBits with the access address we are looking to find.
for (int i = 0; i < 32; i++) {
accessAddrBits[i] = 0x01 & uint32_tmp;
uint32_tmp = (uint32_tmp >> 1);
float phaseSum = 0.0f;
for (int k = 0; k < SAMPLE_PER_SYMBOL; ++k) {
float phase = get_phase_diff(
dst_buffer.p[samples_eaten + k],
dst_buffer.p[samples_eaten + k + 1]);
phaseSum += phase;
}
// phaseSum /= (SAMPLE_PER_SYMBOL);
// phaseSum -= frequency_offset;
/*
alternate method. faster, but less precise. with this, you need to check against this: if (samples_eaten >= (int)dst_buffer.count + SAMPLE_PER_SYMBOL) (not so good...)
int I0 = dst_buffer.p[samples_eaten].real();
int Q0 = dst_buffer.p[samples_eaten].imag();
int I1 = dst_buffer.p[samples_eaten + 1 * SAMPLE_PER_SYMBOL].real();
int Q1 = dst_buffer.p[samples_eaten + 1 * SAMPLE_PER_SYMBOL].imag();
bool bitDecision = (I0 * Q1 - I1 * Q0) > 0 ? 1 : 0;
*/
bool bitDecision = (phaseSum > 0.0f);
rb_buf[packet_index] = rb_buf[packet_index] | (bitDecision << bit_index);
samples_eaten += SAMPLE_PER_SYMBOL;
}
bit_index = 0;
}
}
inline void BTLERxProcessor::handleBeginState() {
uint32_t validAccessAddress = DEFAULT_ACCESS_ADDR;
static uint32_t accesssAddress = 0;
const int demod_buf_len = LEN_DEMOD_BUF_ACCESS; // For AA
int demod_buf_offset = 0;
int hit_idx = (-1);
bool unequal_flag = false;
memset(demod_buf_access, 0, SAMPLE_PER_SYMBOL * demod_buf_len);
for (int i = 0; i < num_symbol_left * SAMPLE_PER_SYMBOL; i += SAMPLE_PER_SYMBOL) {
int sp = ((demod_buf_offset - demod_buf_len + 1) & (demod_buf_len - 1));
for (int i = samples_eaten; i < (int)dst_buffer.count; i += SAMPLE_PER_SYMBOL) {
float phaseDiff = 0;
for (int j = 0; j < SAMPLE_PER_SYMBOL; j++) {
// Sample and compare with the adjacent next sample.
int I0 = dst_buffer.p[i + j].real();
int Q0 = dst_buffer.p[i + j].imag();
int I1 = dst_buffer.p[i + j + 1].real();
int Q1 = dst_buffer.p[i + j + 1].imag();
int phase_idx = j;
demod_buf_access[phase_idx][demod_buf_offset] = (I0 * Q1 - I1 * Q0) > 0 ? 1 : 0;
int k = sp;
unequal_flag = false;
accesssAddress = 0;
for (int p = 0; p < demod_buf_len; p++) {
if (demod_buf_access[phase_idx][k] != accessAddrBits[p]) {
unequal_flag = true;
hit_idx = (-1);
break;
}
accesssAddress = (accesssAddress & (~(1 << p))) | (demod_buf_access[phase_idx][k] << p);
k = ((k + 1) & (demod_buf_len - 1));
}
if (unequal_flag == false) {
hit_idx = (i + j - (demod_buf_len - 1) * SAMPLE_PER_SYMBOL);
break;
}
phaseDiff += get_phase_diff(dst_buffer.p[i + j], dst_buffer.p[i + j + 1]);
}
if (unequal_flag == false) {
// disabled, due to not used anywhere
/* phase_buffer[phase_buffer_index] = phaseDiff / (SAMPLE_PER_SYMBOL);
phase_buffer_index = (phase_buffer_index + 1) % ROLLING_WINDOW;
*/
bool bitDecision = (phaseDiff > 0);
accesssAddress = (accesssAddress >> 1 | (bitDecision << 31));
int errors = __builtin_popcount(accesssAddress ^ validAccessAddress) & 0xFFFFFFFF;
if (!errors) {
hit_idx = i + SAMPLE_PER_SYMBOL;
// disabled, due to not used anywhere
/* for (int k = 0; k < ROLLING_WINDOW; k++) {
frequency_offset_estimate += phase_buffer[k];
}
frequency_offset = frequency_offset_estimate / ROLLING_WINDOW;
*/
break;
}
demod_buf_offset = ((demod_buf_offset + 1) & (demod_buf_len - 1));
}
if (hit_idx == -1) {
// Process more samples.
samples_eaten = (int)dst_buffer.count + 1;
return;
}
symbols_eaten += hit_idx;
symbols_eaten += (8 * NUM_ACCESS_ADDR_BYTE * SAMPLE_PER_SYMBOL); // move to the beginning of PDU header
num_symbol_left = num_symbol_left - symbols_eaten;
samples_eaten += hit_idx;
parseState = Parse_State_PDU_Header;
}
void BTLERxProcessor::handlePDUHeaderState() {
int num_demod_byte = 2; // PDU header has 2 octets
symbols_eaten += 8 * num_demod_byte * SAMPLE_PER_SYMBOL;
if (symbols_eaten > (int)dst_buffer.count) {
inline void BTLERxProcessor::handlePDUHeaderState() {
if (samples_eaten > (int)dst_buffer.count) {
return;
}
// Jump back down to the beginning of PDU header.
sample_idx = symbols_eaten - (8 * num_demod_byte * SAMPLE_PER_SYMBOL);
demodulateFSKBits(NUM_PDU_HEADER_BYTE);
packet_index = 0;
for (int i = 0; i < num_demod_byte; i++) {
rb_buf[packet_index] = 0;
for (int j = 0; j < 8; j++) {
int I0 = dst_buffer.p[sample_idx].real();
int Q0 = dst_buffer.p[sample_idx].imag();
int I1 = dst_buffer.p[sample_idx + 1].real();
int Q1 = dst_buffer.p[sample_idx + 1].imag();
bit_decision = (I0 * Q1 - I1 * Q0) > 0 ? 1 : 0;
rb_buf[packet_index] = rb_buf[packet_index] | (bit_decision << j);
sample_idx += SAMPLE_PER_SYMBOL;
}
packet_index++;
if (packet_index < NUM_PDU_HEADER_BYTE || bit_index != 0) {
resetToDefaultState();
return;
}
scramble_byte(rb_buf, num_demod_byte, scramble_table[channel_number], rb_buf);
scramble_byte(rb_buf, 2, scramble_table[channel_number], rb_buf);
pdu_type = (ADV_PDU_TYPE)(rb_buf[0] & 0x0F);
// uint8_t tx_add = ((rb_buf[0] & 0x40) != 0);
@@ -244,38 +258,25 @@ void BTLERxProcessor::handlePDUHeaderState() {
// Not a valid Advertise Payload.
if ((payload_len < 6) || (payload_len > 37)) {
parseState = Parse_State_Begin;
resetToDefaultState();
return;
} else {
parseState = Parse_State_PDU_Payload;
}
}
void BTLERxProcessor::handlePDUPayloadState() {
int i;
int num_demod_byte = (payload_len + 3);
symbols_eaten += 8 * num_demod_byte * SAMPLE_PER_SYMBOL;
inline void BTLERxProcessor::handlePDUPayloadState() {
const int num_demod_byte = (payload_len + 3);
if (symbols_eaten > (int)dst_buffer.count) {
if (samples_eaten > (int)dst_buffer.count) {
return;
}
for (i = 0; i < num_demod_byte; i++) {
rb_buf[packet_index] = 0;
demodulateFSKBits(num_demod_byte + NUM_PDU_HEADER_BYTE);
for (int j = 0; j < 8; j++) {
int I0 = dst_buffer.p[sample_idx].real();
int Q0 = dst_buffer.p[sample_idx].imag();
int I1 = dst_buffer.p[sample_idx + 1].real();
int Q1 = dst_buffer.p[sample_idx + 1].imag();
bit_decision = (I0 * Q1 - I1 * Q0) > 0 ? 1 : 0;
rb_buf[packet_index] = rb_buf[packet_index] | (bit_decision << j);
sample_idx += SAMPLE_PER_SYMBOL;
}
packet_index++;
if (packet_index < (num_demod_byte + NUM_PDU_HEADER_BYTE) || bit_index != 0) {
resetToDefaultState();
return;
}
scramble_byte(rb_buf + 2, num_demod_byte, scramble_table[channel_number] + 2, rb_buf + 2);
@@ -310,6 +311,8 @@ void BTLERxProcessor::handlePDUPayloadState() {
// Skip Header Byte and MAC Address
uint8_t startIndex = 8;
int i;
for (i = 0; i < payload_len - 6; i++) {
blePacketData.data[i] = rb_buf[startIndex++];
}
@@ -322,46 +325,50 @@ void BTLERxProcessor::handlePDUPayloadState() {
}
}
parseState = Parse_State_Begin;
resetToDefaultState();
}
void BTLERxProcessor::execute(const buffer_c8_t& buffer) {
if (!configured) return;
// Pulled this implementation from channel_stats_collector.c to time slice a specific packet's dB.
// a less computationally expensive method
max_dB = -128;
uint32_t max_squared = 0;
int8_t imag = 0;
int8_t real = 0;
void* src_p = buffer.p;
while (src_p < &buffer.p[buffer.count]) {
const uint32_t sample = *__SIMD32(src_p)++;
const uint32_t mag_sq = __SMUAD(sample, sample);
if (mag_sq > max_squared) {
max_squared = mag_sq;
imag = ((complex8_t*)src_p)->imag();
real = ((complex8_t*)src_p)->real();
}
}
const float max_squared_f = max_squared;
max_dB = mag2_to_dbv_norm(max_squared_f * (1.0f / (32768.0f * 32768.0f)));
max_dB = mag2_to_dbm_8bit_normalized(real, imag, 1.0f, 50.0f);
// 4Mhz 2048 samples
// Decimated by 4 to achieve 2048/4 = 512 samples at 1 sample per symbol.
decim_0.execute(buffer, dst_buffer);
feed_channel_stats(dst_buffer);
symbols_eaten = 0;
samples_eaten = 0;
// Handle parsing based on parseState
if (parseState == Parse_State_Begin) {
handleBeginState();
}
while (samples_eaten < (int)dst_buffer.count) {
// Handle parsing based on parseState
if (parseState == Parse_State_Begin) {
handleBeginState();
}
if (parseState == Parse_State_PDU_Header) {
handlePDUHeaderState();
}
if (parseState == Parse_State_PDU_Header) {
handlePDUHeaderState();
}
if (parseState == Parse_State_PDU_Payload) {
handlePDUPayloadState();
if (parseState == Parse_State_PDU_Payload) {
handlePDUPayloadState();
}
}
}
@@ -372,11 +379,9 @@ void BTLERxProcessor::on_message(const Message* const message) {
void BTLERxProcessor::configure(const BTLERxConfigureMessage& message) {
channel_number = message.channel_number;
decim_0.configure(taps_BTLE_1M_PHY_decim_0.taps);
decim_0.configure(taps_BTLE_Dual_PHY.taps);
configured = true;
crc_init_internal = crc_init_reorder(crc_initalVale);
}
int main() {
+17 -4
View File
@@ -47,6 +47,8 @@ class BTLERxProcessor : public BasebandProcessor {
static constexpr int LEN_DEMOD_BUF_ACCESS{32};
static constexpr uint32_t DEFAULT_ACCESS_ADDR{0x8E89BED6};
static constexpr int NUM_ACCESS_ADDR_BYTE{4};
static constexpr int NUM_PDU_HEADER_BYTE{2};
static constexpr int ROLLING_WINDOW{32};
enum Parse_State {
Parse_State_Begin = 0,
@@ -81,8 +83,8 @@ class BTLERxProcessor : public BasebandProcessor {
};
static constexpr size_t baseband_fs = 4000000;
static constexpr size_t audio_fs = baseband_fs / 8 / 8 / 2;
float get_phase_diff(const complex16_t& sample0, const complex16_t& sample1);
uint_fast32_t crc_update(uint_fast32_t crc, const void* data, size_t data_len);
uint_fast32_t crc24_byte(uint8_t* byte_in, int num_byte, uint32_t init_hex);
bool crc_check(uint8_t* tmp_byte, int body_len, uint32_t crc_init);
@@ -95,6 +97,12 @@ class BTLERxProcessor : public BasebandProcessor {
// void demod_byte(int num_byte, uint8_t *out_byte);
int verify_payload_byte(int num_payload_byte, ADV_PDU_TYPE pdu_type);
void resetOffsetTracking();
void resetBitPacketIndex();
void resetToDefaultState();
void demodulateFSKBits(int num_demod_byte);
void handleBeginState();
void handlePDUHeaderState();
void handlePDUPayloadState();
@@ -120,13 +128,18 @@ class BTLERxProcessor : public BasebandProcessor {
BlePacketData blePacketData{};
Parse_State parseState{Parse_State_Begin};
uint16_t packet_index{0};
int sample_idx{0};
int symbols_eaten{0};
int samples_eaten{0};
uint8_t bit_decision{0};
uint8_t payload_len{0};
uint8_t pdu_type{0};
int32_t max_dB{0};
uint16_t packet_index{0};
uint8_t bit_index{0};
float frequency_offset_estimate{0.0f};
float frequency_offset{0.0f};
float phase_buffer[ROLLING_WINDOW] = {0.0f};
int phase_buffer_index = 0;
/* NB: Threads should be the last members in the class definition. */
BasebandThread baseband_thread{baseband_fs, this, baseband::Direction::Receive};
+95
View File
@@ -0,0 +1,95 @@
/*
* 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) {
}
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__*/
+246 -166
View File
@@ -24,6 +24,7 @@
*/
#include "proc_fsk_rx.hpp"
#include "dsp_decimate.hpp"
#include "event_m4.hpp"
@@ -32,135 +33,253 @@
#include <cstdint>
#include <cstddef>
using namespace std;
using namespace dsp::decimate;
#ifndef M_PI
#define M_PI 3.14159265358979323846
#endif
namespace {
/* Count of bits that differ between the two values. */
uint8_t diff_bit_count(uint32_t left, uint32_t right) {
uint32_t diff = left ^ right;
uint8_t count = 0;
for (size_t i = 0; i < sizeof(diff) * 8; ++i) {
if (((diff >> i) & 0x1) == 1)
++count;
float FSKRxProcessor::detect_peak_power(const buffer_c8_t& buffer, int N) {
int32_t power = 0;
// Initial window power
for (int i = 0; i < N; i++) {
int16_t i_sample = buffer.p[i].real();
int16_t q_sample = buffer.p[i].imag();
power += i_sample * i_sample + q_sample * q_sample;
}
return count;
}
} // namespace
power = power / N;
/* AudioNormalizer ***************************************/
// Convert to dB over noise floor
float power_db = 10.0f * log10f((float)power / noise_floor);
void AudioNormalizer::execute_in_place(const buffer_f32_t& audio) {
// Decay min/max every second (@24kHz).
if (counter_ >= 24'000) {
// 90% decay factor seems to work well.
// This keeps large transients from wrecking the filter.
max_ *= 0.9f;
min_ *= 0.9f;
counter_ = 0;
calculate_thresholds();
}
// If too weak, treat as no signal
if (power_db <= 0.0f) return 0;
counter_ += audio.count;
for (size_t i = 0; i < audio.count; ++i) {
auto& val = audio.p[i];
if (val > max_) {
max_ = val;
calculate_thresholds();
}
if (val < min_) {
min_ = val;
calculate_thresholds();
}
if (val >= t_hi_)
val = 1.0f;
else if (val <= t_lo_)
val = -1.0f;
else
val = 0.0;
}
return power_db;
}
void AudioNormalizer::calculate_thresholds() {
auto center = (max_ + min_) / 2.0f;
auto range = (max_ - min_) / 2.0f;
void FSKRxProcessor::agc_correct_iq(const buffer_c8_t& buffer, int N, float measured_power) {
float power_db = 10.0f * log10f(measured_power / noise_floor);
float error_db = target_power_db - power_db;
// 10% off center force either +/-1.0f.
// Higher == larger dead zone.
// Lower == more false positives.
auto threshold = range * 0.1;
t_hi_ = center + threshold;
t_lo_ = center - threshold;
}
/* FSKRxProcessor ******************************************/
void FSKRxProcessor::clear_data_bits() {
data = 0;
bit_count = 0;
}
void FSKRxProcessor::handle_sync(bool inverted) {
clear_data_bits();
has_sync_ = true;
inverted = inverted;
word_count = 0;
}
void FSKRxProcessor::process_bits(const buffer_c8_t& buffer) {
// Process all of the bits in the bits queue.
while (buffer.count > 0) {
// Wait until data_ is full.
if (bit_count < data_bit_count)
continue;
// Wait for the sync frame.
if (!has_sync_) {
if (diff_bit_count(data, sync_codeword) <= 2)
handle_sync(/*inverted=*/false);
else if (diff_bit_count(data, ~sync_codeword) <= 2)
handle_sync(/*inverted=*/true);
continue;
}
}
}
/* FSKRxProcessor ***************************************/
FSKRxProcessor::FSKRxProcessor() {
}
void FSKRxProcessor::execute(const buffer_c8_t& buffer) {
if (!configured) {
if (error_db <= 0) {
return;
}
// Decimate by current decim 0 and decim 1.
float gain_scalar = powf(10.0f, error_db / 20.0f);
for (int i = 0; i < N; i++) {
buffer.p[i] = {(int8_t)(buffer.p[i].real() * gain_scalar), (int8_t)(buffer.p[i].imag() * gain_scalar)};
}
}
float FSKRxProcessor::get_phase_diff(const complex16_t& sample0, const complex16_t& sample1) {
// Calculate the phase difference between two samples.
float dI = sample1.real() * sample0.real() + sample1.imag() * sample0.imag();
float dQ = sample1.imag() * sample0.real() - sample1.real() * sample0.imag();
float phase_diff = atan2f(dQ, dI);
return phase_diff;
}
void FSKRxProcessor::demodulateFSKBits(const buffer_c16_t& decimator_out, int num_demod_byte) {
for (; packet_index < num_demod_byte; packet_index++) {
for (; bit_index < 8; bit_index++) {
if (samples_eaten >= (int)decimator_out.count) {
return;
}
float phaseSum = 0.0f;
for (int k = 0; k < SAMPLE_PER_SYMBOL - 1; ++k) {
float phase = get_phase_diff(
decimator_out.p[samples_eaten + k],
decimator_out.p[samples_eaten + k + 1]);
phaseSum += phase;
}
phaseSum /= (SAMPLE_PER_SYMBOL - 1);
phaseSum -= frequency_offset;
bool bitDecision = (phaseSum > 0.0f);
rb_buf[packet_index] |= (bitDecision << (7 - bit_index));
samples_eaten += SAMPLE_PER_SYMBOL;
}
bit_index = 0;
}
}
void FSKRxProcessor::resetPreambleTracking() {
frequency_offset = 0.0f;
frequency_offset_estimate = 0.0f;
phase_buffer_index = 0;
memset(phase_buffer, 0, sizeof(phase_buffer));
}
void FSKRxProcessor::resetBitPacketIndex() {
packet_index = 0;
bit_index = 0;
}
void FSKRxProcessor::resetToDefaultState() {
parseState = Parse_State_Wait_For_Peak;
peak_timeout = 0;
fskPacketData.power = 0.0f;
resetPreambleTracking();
resetBitPacketIndex();
}
void FSKRxProcessor::handlePreambleState(const buffer_c16_t& decimator_out) {
const uint32_t validPreamble = DEFAULT_PREAMBLE;
static uint32_t preambleValue = 0;
int hit_idx = -1;
for (; samples_eaten < (int)decimator_out.count; samples_eaten += SAMPLE_PER_SYMBOL) {
float phaseSum = 0.0f;
for (int j = 0; j < SAMPLE_PER_SYMBOL - 1; j++) {
phaseSum += get_phase_diff(decimator_out.p[samples_eaten + j], decimator_out.p[samples_eaten + j + 1]);
}
phase_buffer[phase_buffer_index] = phaseSum / (SAMPLE_PER_SYMBOL - 1);
phase_buffer_index = (phase_buffer_index + 1) % ROLLING_WINDOW;
bool bitDecision = (phaseSum > 0.0f);
preambleValue = (preambleValue << 1) | bitDecision;
int errors = __builtin_popcountl(preambleValue ^ validPreamble) & 0xFFFFFFFF;
if (errors == 0) {
hit_idx = samples_eaten + SAMPLE_PER_SYMBOL;
fskPacketData.syncWord = preambleValue;
fskPacketData.max_dB = max_dB;
for (int k = 0; k < ROLLING_WINDOW; k++) {
frequency_offset_estimate += phase_buffer[k];
}
frequency_offset = frequency_offset_estimate / ROLLING_WINDOW;
fskPacketData.frequency_offset_hz = (frequency_offset * demod_input_fs) / (2.0f * M_PI);
preambleValue = 0;
break;
}
}
if (hit_idx == -1) {
samples_eaten = samples_eaten;
return;
}
samples_eaten = hit_idx;
parseState = Parse_State_Sync;
}
void FSKRxProcessor::handleSyncWordState(const buffer_c16_t& decimator_out) {
const int syncword_bytes = 4;
const uint32_t validSyncWord = DEFAULT_SYNC_WORD;
if ((int)decimator_out.count - samples_eaten <= 0) {
return;
}
demodulateFSKBits(decimator_out, syncword_bytes);
if (packet_index < syncword_bytes || bit_index != 0) {
return;
}
uint32_t receivedSyncWord = (rb_buf[0] << 24) | (rb_buf[1] << 16) | (rb_buf[2] << 8) | rb_buf[3];
int errors = __builtin_popcountl(receivedSyncWord ^ validSyncWord) & 0xFFFFFFFF;
if (errors <= 3) {
fskPacketData.syncWord = receivedSyncWord;
parseState = Parse_State_PDU_Payload;
memset(fskPacketData.data, 0, sizeof(fskPacketData.data));
} else {
resetToDefaultState();
}
memset(rb_buf, 0, sizeof(rb_buf));
resetBitPacketIndex();
}
void FSKRxProcessor::handlePDUPayloadState(const buffer_c16_t& decimator_out) {
if ((int)decimator_out.count - samples_eaten <= 0) {
return;
}
demodulateFSKBits(decimator_out, NUM_DATA_BYTE);
if (packet_index < NUM_DATA_BYTE || bit_index != 0) {
return;
}
fskPacketData.dataLen = NUM_DATA_BYTE;
// Copy the decoded bits to the packet data
for (int i = 0; i < NUM_DATA_BYTE; i++) {
fskPacketData.data[i] |= rb_buf[i];
}
FSKRxPacketMessage data_message{&fskPacketData};
shared_memory.application_queue.push(data_message);
memset(rb_buf, 0, sizeof(rb_buf));
resetToDefaultState();
}
void FSKRxProcessor::execute(const buffer_c8_t& buffer) {
if (!configured || parseState == Parse_State_Parsing_Data) return;
const auto decim_0_out = decim_0.execute(buffer, dst_buffer);
const auto decim_1_out = decim_1.execute(decim_0_out, dst_buffer);
feed_channel_stats(decim_1_out);
spectrum_samples += decim_1_out.count;
samples_eaten = 0;
if (spectrum_samples >= spectrum_interval_samples) {
spectrum_samples -= spectrum_interval_samples;
channel_spectrum.feed(decim_1_out, channel_filter_low_f,
channel_filter_high_f, channel_filter_transition);
}
while ((int)decim_1_out.count - samples_eaten > 0) {
if ((parseState == Parse_State_Wait_For_Peak) || (parseState == Parse_State_Preamble)) {
float power = detect_peak_power(buffer, buffer.count);
// process_bits();
if (power) {
parseState = Parse_State_Preamble;
agc_power = power;
fskPacketData.power = power;
} else {
break;
}
}
// Update the status.
samples_processed += buffer.count;
if (agc_power) {
agc_correct_iq(buffer, buffer.count, agc_power);
}
if (samples_processed >= stat_update_threshold) {
// send_packet(data);
samples_processed -= stat_update_threshold;
if (parseState == Parse_State_Preamble) {
peak_timeout++;
// 960,000 fs / 2048 samples = 468.75 Hz, so 55 calls is about 0.053 seconds before timeout.
if (peak_timeout == 4) {
resetToDefaultState();
} else {
handlePreambleState(decim_1_out);
}
}
if (parseState == Parse_State_Sync) {
handleSyncWordState(decim_1_out);
}
if (parseState == Parse_State_PDU_Payload) {
handlePDUPayloadState(decim_1_out);
}
}
}
@@ -171,7 +290,7 @@ void FSKRxProcessor::on_message(const Message* const message) {
break;
case Message::ID::UpdateSpectrum:
case Message::ID::SpectrumStreamingConfig:
channel_spectrum.on_message(message);
// channel_spectrum.on_message(message);
break;
case Message::ID::SampleRateConfig:
@@ -179,7 +298,7 @@ void FSKRxProcessor::on_message(const Message* const message) {
break;
case Message::ID::CaptureConfig:
capture_config(*reinterpret_cast<const CaptureConfigMessage*>(message));
// capture_config(*reinterpret_cast<const CaptureConfigMessage*>(message));
break;
default:
@@ -188,83 +307,65 @@ void FSKRxProcessor::on_message(const Message* const message) {
}
void FSKRxProcessor::configure(const FSKRxConfigureMessage& message) {
// Extract message variables.
deviation = message.deviation;
channel_decimation = message.channel_decimation;
// channel_filter_taps = message.channel_filter;
channel_spectrum.set_decimation_factor(1);
}
void FSKRxProcessor::capture_config(const CaptureConfigMessage& message) {
if (message.config) {
audio_output.set_stream(std::make_unique<StreamInput>(message.config));
} else {
audio_output.set_stream(nullptr);
}
SAMPLE_PER_SYMBOL = message.samplesPerSymbol;
DEFAULT_SYNC_WORD = message.syncWord;
NUM_SYNC_WORD_BYTE = message.syncWordLength;
DEFAULT_PREAMBLE = message.preamble;
NUM_PREAMBLE_BYTE = message.preambleLength;
NUM_DATA_BYTE = message.numDataBytes;
}
void FSKRxProcessor::sample_rate_config(const SampleRateConfigMessage& message) {
const auto sample_rate = message.sample_rate;
// The actual sample rate is the requested rate * the oversample rate.
// See oversample.hpp for more details on oversampling.
baseband_fs = sample_rate * toUType(message.oversample_rate);
baseband_thread.set_sampling_rate(baseband_fs);
// TODO: Do we need to use the taps that the decimators get configured with?
channel_filter_low_f = taps_200k_decim_1.low_frequency_normalized * sample_rate;
channel_filter_high_f = taps_200k_decim_1.high_frequency_normalized * sample_rate;
channel_filter_transition = taps_200k_decim_1.transition_normalized * sample_rate;
// Compute the scalar that corrects the oversample_rate to be x8 when computing
// the spectrum update interval. The original implementation only supported x8.
// TODO: Why is this needed here but not in proc_replay? There must be some other
// assumption about x8 oversampling in some component that makes this necessary.
const auto oversample_correction = toUType(message.oversample_rate) / 8.0;
// The spectrum update interval controls how often the waterfall is fed new samples.
spectrum_interval_samples = sample_rate / (spectrum_rate_hz * oversample_correction);
spectrum_samples = 0;
// For high sample rates, the M4 is busy collecting samples so the
// waterfall runs slower. Reduce the update interval so it runs faster.
// NB: Trade off: looks nicer, but more frequent updates == more CPU.
if (sample_rate >= 1'500'000)
spectrum_interval_samples /= (sample_rate / 750'000);
switch (message.oversample_rate) {
case OversampleRate::x4:
// M4 can't handle 2 decimation passes for sample rates needing x4.
decim_0.set<FIRC8xR16x24FS4Decim4>().configure(taps_200k_decim_0.taps);
decim_0.set<dsp::decimate::FIRC8xR16x24FS4Decim4>().configure(taps_200k_decim_0.taps);
decim_1.set<NoopDecim>();
break;
case OversampleRate::x8:
// M4 can't handle 2 decimation passes for sample rates <= 600k.
if (message.sample_rate < 600'000) {
decim_0.set<FIRC8xR16x24FS4Decim4>().configure(taps_200k_decim_0.taps);
decim_1.set<FIRC16xR16x16Decim2>().configure(taps_200k_decim_1.taps);
decim_0.set<dsp::decimate::FIRC8xR16x24FS4Decim4>().configure(taps_200k_decim_0.taps);
decim_1.set<dsp::decimate::FIRC16xR16x16Decim2>().configure(taps_200k_decim_1.taps);
} else {
// Using 180k taps to provide better filtering with a single pass.
decim_0.set<FIRC8xR16x24FS4Decim8>().configure(taps_180k_wfm_decim_0.taps);
decim_0.set<dsp::decimate::FIRC8xR16x24FS4Decim8>().configure(taps_180k_wfm_decim_0.taps);
decim_1.set<NoopDecim>();
}
break;
case OversampleRate::x16:
decim_0.set<FIRC8xR16x24FS4Decim8>().configure(taps_200k_decim_0.taps);
decim_1.set<FIRC16xR16x16Decim2>().configure(taps_200k_decim_1.taps);
decim_0.set<dsp::decimate::FIRC8xR16x24FS4Decim8>().configure(taps_200k_decim_0.taps);
decim_1.set<dsp::decimate::FIRC16xR16x16Decim2>().configure(taps_200k_decim_1.taps);
break;
case OversampleRate::x32:
decim_0.set<FIRC8xR16x24FS4Decim4>().configure(taps_200k_decim_0.taps);
decim_1.set<FIRC16xR16x32Decim8>().configure(taps_16k0_decim_1.taps);
decim_0.set<dsp::decimate::FIRC8xR16x24FS4Decim4>().configure(taps_200k_decim_0.taps);
decim_1.set<dsp::decimate::FIRC16xR16x32Decim8>().configure(taps_16k0_decim_1.taps);
break;
case OversampleRate::x64:
decim_0.set<FIRC8xR16x24FS4Decim8>().configure(taps_200k_decim_0.taps);
decim_1.set<FIRC16xR16x32Decim8>().configure(taps_16k0_decim_1.taps);
decim_0.set<dsp::decimate::FIRC8xR16x24FS4Decim8>().configure(taps_200k_decim_0.taps);
decim_1.set<dsp::decimate::FIRC16xR16x32Decim8>().configure(taps_16k0_decim_1.taps);
break;
default:
@@ -282,33 +383,12 @@ void FSKRxProcessor::sample_rate_config(const SampleRateConfigMessage& message)
// size_t channel_filter_input_fs = decim_1_output_fs;
// size_t channel_filter_output_fs = channel_filter_input_fs / channel_decimation;
size_t demod_input_fs = decim_1_output_fs;
send_packet((uint32_t)demod_input_fs);
demod_input_fs = decim_1_output_fs;
// Set ready to process data.
configured = true;
}
void FSKRxProcessor::flush() {
// word_extractor.flush();
}
void FSKRxProcessor::reset() {
clear_data_bits();
has_sync_ = false;
inverted = false;
word_count = 0;
samples_processed = 0;
}
void FSKRxProcessor::send_packet(uint32_t data) {
data_message.is_data = true;
data_message.value = data;
shared_memory.application_queue.push(data_message);
}
/* main **************************************************/
int main() {
+71 -86
View File
@@ -30,7 +30,6 @@
#include "dsp_decimate.hpp"
#include "dsp_demodulate.hpp"
#include "dsp_iir_config.hpp"
#include "dsp_fir_taps.hpp"
#include "spectrum_collector.hpp"
@@ -46,21 +45,6 @@
#include <cstdint>
#include <functional>
/* Normalizes audio stream to +/-1.0f */
class AudioNormalizer {
public:
void execute_in_place(const buffer_f32_t& audio);
private:
void calculate_thresholds();
uint32_t counter_ = 0;
float min_ = 99.0f;
float max_ = -99.0f;
float t_hi_ = 1.0;
float t_lo_ = 1.0;
};
/* A decimator that just returns the source buffer. */
class NoopDecim {
public:
@@ -111,44 +95,92 @@ class MultiDecimator {
class FSKRxProcessor : public BasebandProcessor {
public:
FSKRxProcessor();
void execute(const buffer_c8_t& buffer) override;
void on_message(const Message* const message) override;
private:
size_t baseband_fs = 1024000; // aka: sample_rate
static constexpr int ROLLING_WINDOW{32};
static constexpr uint16_t MAX_BUFFER_SIZE{512};
enum Parse_State {
Parse_State_Wait_For_Peak = 0,
Parse_State_Preamble,
Parse_State_Sync,
Parse_State_PDU_Payload,
Parse_State_Parsing_Data
};
size_t baseband_fs = 960000;
uint8_t stat_update_interval = 10;
uint32_t stat_update_threshold = baseband_fs / stat_update_interval;
static constexpr auto spectrum_rate_hz = 50.0f;
float detect_peak_power(const buffer_c8_t& buffer, int N);
void agc_correct_iq(const buffer_c8_t& buffer, int N, float measured_power);
float get_phase_diff(const complex16_t& sample0, const complex16_t& sample1);
void demodulateFSKBits(const buffer_c16_t& decimator_out, int num_demod_byte);
void resetPreambleTracking();
void resetBitPacketIndex();
void resetToDefaultState();
void handlePreambleState(const buffer_c16_t& decimator_out);
void handleSyncWordState(const buffer_c16_t& decimator_out);
void handlePDUPayloadState(const buffer_c16_t& decimator_out);
void configure(const FSKRxConfigureMessage& message);
void capture_config(const CaptureConfigMessage& message);
void sample_rate_config(const SampleRateConfigMessage& message);
void flush();
void reset();
void send_packet(uint32_t data);
void process_bits(const buffer_c8_t& buffer);
void clear_data_bits();
void handle_sync(bool inverted);
/* Returns true if the batch has as sync frame. */
bool has_sync() const { return has_sync_; }
/* Set once app is ready to receive messages. */
bool configured = false;
/* Buffer for decimated IQ data. */
std::array<complex16_t, 512> dst{};
std::array<complex16_t, MAX_BUFFER_SIZE> dst{};
const buffer_c16_t dst_buffer{
dst.data(),
dst.size()};
/* Buffer for demodulated audio. */
std::array<float, 16> audio{};
const buffer_f32_t audio_buffer{audio.data(), audio.size()};
uint8_t rb_buf[MAX_BUFFER_SIZE];
dsp::demodulate::FM demod{};
int rb_head{-1};
int32_t g_threshold{0};
uint8_t channel_number{0};
uint16_t process = 0;
bool configured{false};
FskPacketData fskPacketData{};
Parse_State parseState{Parse_State_Wait_For_Peak};
int sample_idx{0};
int samples_eaten{0};
int32_t max_dB{0};
int8_t real{0};
int8_t imag{0};
uint16_t peak_timeout{0};
float noise_floor{12.0}; // Using LNA 40 and VGA 20. 10.0 was 40/0 ratio.
float target_power_db{5.0};
float agc_power{0.0f};
float frequency_offset_estimate{0.0f};
float frequency_offset{0.0f};
float phase_buffer[ROLLING_WINDOW] = {0.0f};
int phase_buffer_index = 0;
uint16_t packet_index{0};
uint8_t bit_index{0};
size_t demod_input_fs{0};
uint8_t SAMPLE_PER_SYMBOL{1};
uint32_t DEFAULT_PREAMBLE{0xAAAAAAAA};
uint32_t DEFAULT_SYNC_WORD{0xFFFFFFFF};
uint8_t NUM_SYNC_WORD_BYTE{4};
uint8_t NUM_PREAMBLE_BYTE{4};
uint16_t NUM_DATA_BYTE = MAX_BUFFER_SIZE - NUM_SYNC_WORD_BYTE - NUM_PREAMBLE_BYTE;
SpectrumCollector channel_spectrum{};
size_t spectrum_interval_samples = 0;
size_t spectrum_samples = 0;
static constexpr auto spectrum_rate_hz = 50.0f;
/* The actual type will be configured depending on the sample rate. */
MultiDecimator<
dsp::decimate::FIRC8xR16x24FS4Decim4,
dsp::decimate::FIRC8xR16x24FS4Decim8>
@@ -159,9 +191,7 @@ class FSKRxProcessor : public BasebandProcessor {
NoopDecim>
decim_1{};
/* Filter to 24kHz and demodulate. */
dsp::decimate::FIRAndDecimateComplex channel_filter{};
size_t deviation = 3750;
// fir_taps_real<32> channel_filter_taps = 0;
size_t channel_decimation = 2;
int32_t channel_filter_low_f = 0;
@@ -172,51 +202,6 @@ class FSKRxProcessor : public BasebandProcessor {
FMSquelch squelch{};
uint64_t squelch_history = 0;
// /* LPF to reduce noise. POCSAG supports 2400 baud, but that falls
// * nicely into the transition band of this 1800Hz filter.
// * scipy.signal.butter(2, 1800, "lowpass", fs=24000, analog=False) */
// IIRBiquadFilter lpf{{{0.04125354f, 0.082507070f, 0.04125354f},
// {1.00000000f, -1.34896775f, 0.51398189f}}};
/* Attempts to de-noise and normalize signal. */
AudioNormalizer normalizer{};
/* Handles writing audio stream to hardware. */
AudioOutput audio_output{};
/* Holds the data sent to the app. */
AFSKDataMessage data_message{false, 0};
/* Used to keep track of how many samples were processed
* between status update messages. */
uint32_t samples_processed = 0;
/* Number of bits in 'data_' member. */
static constexpr uint8_t data_bit_count = sizeof(uint32_t) * 8;
/* Sync frame codeword. */
static constexpr uint32_t sync_codeword = 0x12345678;
/* When true, sync frame has been received. */
bool has_sync_ = false;
/* When true, bit vales are flipped in the codewords. */
bool inverted = false;
uint32_t data = 0;
uint8_t bit_count = 0;
uint8_t word_count = 0;
/* LPF to reduce noise. POCSAG supports 2400 baud, but that falls
* nicely into the transition band of this 1800Hz filter.
* scipy.signal.butter(2, 1800, "lowpass", fs=24000, analog=False) */
IIRBiquadFilter lpf{{{0.04125354f, 0.082507070f, 0.04125354f},
{1.00000000f, -1.34896775f, 0.51398189f}}};
SpectrumCollector channel_spectrum{};
size_t spectrum_interval_samples = 0;
size_t spectrum_samples = 0;
/* NB: Threads should be the last members in the class definition. */
BasebandThread baseband_thread{baseband_fs, this, baseband::Direction::Receive};
RSSIThread rssi_thread{};
+32
View File
@@ -1649,4 +1649,36 @@ static constexpr fir_taps_real<24> taps_BTLE_2M_PHY_decim_0 = {
}},
};
// Tested to be better at capturing both 4.0 and 5.0 device. Better attenuation at channel end.
static constexpr fir_taps_real<24> taps_BTLE_Dual_PHY = {
.low_frequency_normalized = -750000.0f / 4000000.0f,
.high_frequency_normalized = 750000.0f / 4000000.0f,
.transition_normalized = 250000.0f / 4000000.0f,
.taps = {{3,
-5,
-97,
-144,
317,
1099,
396,
-2887,
-4814,
1912,
18134,
32767,
32767,
18134,
1912,
-4814,
-2887,
396,
1099,
317,
-144,
-97,
-5,
3}},
};
#endif /*__DSP_FIR_TAPS_H__*/
+53 -15
View File
@@ -134,6 +134,8 @@ class Message {
NoaaAptRxConfigure = 77,
NoaaAptRxStatusData = 78,
NoaaAptRxImageData = 79,
FSKPacket = 80,
EPIRBPacket = 81,
MAX
};
@@ -338,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(
@@ -452,6 +465,17 @@ struct BlePacketData {
uint8_t dataLen;
};
struct FskPacketData {
int8_t real;
int8_t imag;
int max_dB;
uint8_t data[512];
uint16_t dataLen;
uint64_t syncWord;
float power;
float frequency_offset_hz;
};
class BLEPacketMessage : public Message {
public:
constexpr BLEPacketMessage(
@@ -463,6 +487,17 @@ class BLEPacketMessage : public Message {
BlePacketData* packet{nullptr};
};
class FSKRxPacketMessage : public Message {
public:
constexpr FSKRxPacketMessage(
FskPacketData* packet)
: Message{ID::FSKPacket},
packet{packet} {
}
FskPacketData* packet{nullptr};
};
class CodedSquelchMessage : public Message {
public:
constexpr CodedSquelchMessage(
@@ -1127,24 +1162,27 @@ class FSKConfigureMessage : public Message {
class FSKRxConfigureMessage : public Message {
public:
constexpr FSKRxConfigureMessage(
const fir_taps_real<24> decim_0_filter,
const fir_taps_real<32> decim_1_filter,
const fir_taps_real<32> channel_filter,
const size_t channel_decimation,
const size_t deviation)
const uint8_t samplesPerSymbol,
const uint32_t syncWord,
const uint8_t syncWordLength,
const uint32_t preamble,
const uint8_t preambleLength,
const uint16_t numDataBytes)
: Message{ID::FSKRxConfigure},
decim_0_filter(decim_0_filter),
decim_1_filter(decim_1_filter),
channel_filter(channel_filter),
channel_decimation{channel_decimation},
deviation{deviation} {
samplesPerSymbol(samplesPerSymbol),
syncWord(syncWord),
syncWordLength(syncWordLength),
preamble(preamble),
preambleLength(preambleLength),
numDataBytes(numDataBytes) {
}
const fir_taps_real<24> decim_0_filter;
const fir_taps_real<32> decim_1_filter;
const fir_taps_real<32> channel_filter;
const size_t channel_decimation;
const size_t deviation;
const uint8_t samplesPerSymbol;
const uint32_t syncWord;
const uint8_t syncWordLength;
const uint32_t preamble;
const uint8_t preambleLength;
const uint16_t numDataBytes;
};
class POCSAGConfigureMessage : public Message {
+11 -16
View File
@@ -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'};
+2
View File
@@ -1,6 +1,8 @@
/*
* Copyright (C) 2014 Jared Boone, ShareBrained Technology, Inc.
* Copyright (C) 2016 Furrtek
* Copyright (C) 2025 RocketGod
* Copyright (C) 2025 HTotoo
*
* This file is part of PortaPack.
*
+2
View File
@@ -1,6 +1,8 @@
/*
* Copyright (C) 2014 Jared Boone, ShareBrained Technology, Inc.
* Copyright (C) 2016 Furrtek
* Copyright (C) 2025 RocketGod
* Copyright (C) 2025 HTotoo
*
* This file is part of PortaPack.
*
+20
View File
@@ -91,6 +91,26 @@ float mag2_to_dbv_norm(const float mag2) {
return (fast_log2(mag2) - mag2_log2_max) * mag2_to_db_factor;
}
// Function to calculate dBm and normalize based on LNA and VGA settings
float mag2_to_dbm_8bit_normalized(int8_t real, int8_t imag, float v_ref, float R) {
// Step 1: Normalize IQ values (convert 8-bit signed to -1.0 to +1.0)
float I = real / 127.0f; // Map the 8-bit real part to the [-1, 1] range
float Q = imag / 127.0f; // Map the 8-bit imaginary part to the [-1, 1] range
// Step 2: Compute the magnitude squared (I^2 + Q^2)
float mag2 = I * I + Q * Q;
// Step 3: Convert the magnitude squared to actual power (in watts)
float voltage_squared = mag2 * v_ref * v_ref;
float power_watts = voltage_squared / R;
// Step 4: Convert the power to dBm (multiply by 1000 to convert watts to milliwatts)
float power_milliwatts = power_watts * 1000.0f;
float dbm_measured = 10.0f * log10f(power_milliwatts);
return dbm_measured;
}
// Integer in and out approximation
// >40 times faster float sqrt(x*x+y*y) on Cortex M0
// derived from https://dspguru.com/dsp/tricks/magnitude-estimator/
+1
View File
@@ -90,6 +90,7 @@ float fast_log2(const float val);
float fast_pow2(const float val);
float mag2_to_dbv_norm(const float mag2);
float mag2_to_dbm_8bit_normalized(int8_t real, int8_t imag, float v_ref, float R);
inline float magnitude_squared(const std::complex<float> c) {
const auto r = c.real();
+1
View File
@@ -98,6 +98,7 @@ for external_image_prefix in sys.argv[4:]:
# COMMAND ${CMAKE_OBJCOPY} -v -O binary ${PROJECT_NAME}.elf ${PROJECT_NAME}_ext_pacman.bin --only-section=.external_app_pacman
himg = "{}/external_app_{}.himg".format(binary_dir, external_image_prefix)
print("Creating external application image for {}".format(external_image_prefix))
subprocess.run([cmake_objcopy, "-v", "-O", "binary", "{}/application.elf".format(binary_dir), himg, "--only-section=.external_app_{}".format(external_image_prefix)])
external_application_image = read_image(himg)
+1 -2
View File
@@ -3,6 +3,5 @@
Licensed under [GNU GPL v3](../../../LICENSE)
USAGE:
- Copy file from: https://raw.githubusercontent.com/kx1t/planefence-airlinecodes/main/airlinecodes.txt
- Run Python 3 script: `./make_airlines_db.py`
- Copy file to /ADSB folder on SDCARD
- Move file "airlines.db" to /ADSB folder on SDCARD
@@ -325,7 +325,7 @@ AMT,ATA Airlines,,United States
AMU,Air Macau,AIR MACAO,Macao
AMV,AMC Airlines,,Egypt
AMW,Air Midwest,,United States
AMX,Aeroméxico,AEROMEXICO,Mexico
AMX,Aeromexico,AEROMEXICO,Mexico
AMY,Air Ambar,AIR AMBAR,Dominican Republic
AMZ,Amiya Airline,AMIYA AIR,Nigeria
ANA,All Nippon Airways,ALL NIPPON,Japan
@@ -408,7 +408,7 @@ ARC,Air Routing International Corp.,,United States
ARD,Aerocondor,,Portugal
ARE,Aires Aerovías de Integración Regional S.A.,AIRES,Colombia
ARF,Aero Flight,,Germany
ARG,Aerolíneas Argentinas,ARGENTINA,Argentina
ARG,Aerolineas Argentinas,ARGENTINA,Argentina
ARH,Aerohelicopteros,AEROHELCA,Venezuela
ARI,Aero Vics,AEROVICS,Mexico
ARJ,Aerojet de Costa Rica S.A.,,Costa Rica
@@ -820,7 +820,7 @@ BNT,Bentiu Air Transport,BENTIU AIR,Sudan
BNV,Benane Aviation Corporation,BENANE,Mauritania
BNW,British North West Airlines,BRITISH NORTH,United Kingdom
BNX,LAI - Línea Aérea IAACA,AIR BARINAS,Venezuela
BNZ,Aerolíneas Bonanza,AERO BONANZA,Mexico
BNZ,Bonza,BONZA,Australia
BOA,Boniair,KUMANOVO,Macedonia
BOB,Backbone A/S,BACKBONE,Denmark
BOC,Aerobona,AEROBONA,Mexico
@@ -1465,6 +1465,7 @@ DET,DETA Air,SAMAL,Kazakhstan
DEV,Red Devils Parachute Display Team,RED DEVILS,United Kingdom
DFA,Aero Coach Aviation,AERO COACH,United States
DFC,Aeropartner,DARK BLUE,Czech Republic
DFL,Babcock AirAmbulance,MEDIFLIGHT,Sweden
DFS,Dwyer Aircraft Services,DWYAIR,United States
DGA,Yellow River Delta General Aviation,YELLOW RIVER,China
DGO,DGO Jet,DGO JET,Mexico
@@ -1592,8 +1593,10 @@ DVA,Discovery Airways,DISCOVERY AIRWAYS,United States
DVB,Don Avia,DONSEBAI,Kazakhstan
DVI,Aero Davinci International,AERO DAVINCI,Mexico
DVN,Adventia,,Spain
DVY,Legends Airways,DEVIL RAY,United States
DWA,Dense Airways,,United States
DWD,Deutsche Luftverkehrsgesellschaft (DLT),,Germany
DWI,Arajet,DOMINICAN,Dominican Republic
DWN,Dawn Air,DAWN AIR,United States
DWR,Delaware Skyways,DELAWARE,United States
DWT,Darwin Airline,DARWIN,Switzerland
@@ -1905,6 +1908,7 @@ EZJ,Ezjet GT,GUYANA JET,Guyana
EZS,easyJet Switzerland,TOPSWISS,Switzerland
EZX,Eagle Express Air Charter,EAGLEXPRESS,Malaysia
EZY,easyJet UK,EASY,United Kingdom
EZZ,ETF Airways,ENTERPRIZE,Croatia
FAB,First Air,FIRST AIR,Canada
FAC,Atlantic Helicopters,FAROECOPTER,Denmark
FAD,Rog-Air,AIR FRONTIER,Canada
@@ -2034,7 +2038,7 @@ FJI,Fiji Airways,PACIFIC,Fiji
FJK,Fly Jet Kz,,Kazakhstan
FJM,Fly Jamaica Airways,GREENHEART,Jamaica
FJO,Flexjet Operations Malta Limited,FLEX MALTA,Malta
FRJ,Fly Jordan,SOLITAIRE AIR,Jordan
FJR,Fly Jordan,SOLITAIRE AIR,Jordan
FJS,Florida Jet Service,FLORIDAJET,United States
FKA,Flying kangaroo Airline,,Australia
FKI,FLM Aviation Mohrdieck,KIEL AIR,Germany
@@ -2397,7 +2401,7 @@ GPL,DLR,GERMAN POLAR,Germany
GPM,Grup Air-Med,GRUPOMED,Spain
GPR,GPM Aeroservicio,GPM AEROSERVICIO,Mexico
GPX,GP Aviation,,Bulgaria
GRA,Guardian Air Asset Management,FLEX,South Africa
GRA,Costa Rica Green Airways,,,Costa Rica
GRB,Phoenix Air Group,GREY BIRD,United States
GRD,National Grid plc,GRID,United Kingdom
GRE,Air Scotland,GREECE AIRWAYS,Greece
@@ -2459,7 +2463,7 @@ GWN,Gwyn Aviation,GWYN,United Kingdom
GWR,Aura Airlines,LEMON,Spain
GWS,General Airways,GENAIR,South Africa
GWY,USA3000 Airlines,GETAWAY,United States
GXA,Grixona,GRIXONA,Moldova
GXA,GlobalX Airlines,GEMINI,United States
GXG,GermanXL,,Germany
GXL,Star XL German Airlines,STARDUST,Germany
GXY,Galaxy Airlines,GALAX,Japan
@@ -2608,6 +2612,7 @@ HMC,Heliamerica De Mexico,HELIAMERICA,Mexico
HMD,Charlie Hammonds Flying Service,HAMMOND,United States
HME,Babcock MCS,HELIMED,United Kingdom
HMF,Norrlandsflyg,LIFEGUARD SWEDEN,Sweden
HMJ,Harmony Jets,HARMONG JETS,Malta
HMM,Hamra Air,HAMRA,United Arab Emirates
HMP,Papair Terminal,PAPAIR TERMINAL,Haiti
HMR,North American Charters,HAMMER,Canada
@@ -2623,6 +2628,7 @@ HNR,Haiti National Airlines (HANA),HANAIR,Haiti
HNT,Helicopteros Internacionales,HELICOP INTER,Mexico
HNX,Hankook Airline,,South Korea
HOA,Hola Airlines,HOLA,Spain
HOB,HL Aircraft LLC,HOBBY JET,United States
HOG,Mahogany Air Charters,HOGAN AIR,Zambia
HOL,Holiday Airlines (US Airline),HOLIDAY,United States
HOM,Aero Homex,AERO HOMEX,Mexico
@@ -3077,7 +3083,7 @@ JSM,Jet Stream,,Moldova
JSN,Suncor Energy Inc,JETSUN,Canada
JSP,Palmer Aviation,PALMER,United Kingdom
JSR,Jusur airways,,Egypt
JST,Eastern Australia Airlines,,Australia
JST,Jetstar Airways,,Australia
JSV,Japan Aircraft Service,,Japan
JSW,Jigsaw Project,,United Kingdom
JSX,JetSuiteX Air,BIGSTRIPE,United States
@@ -3617,6 +3623,7 @@ MAK,MAT Macedonian Airlines,MAKAVIO,Macedonia
MAL,Morningstar Air Express,MORNINGSTAR,Canada
MAM,Aeródromo De La Mancha,AEROMAN,Spain
MAN,Mannion Air Charter,MANNION,United States
MAO,Mayo Clinic,MAYO,United States
MAQ,Mac Aviation,MAC AVIATION,Spain
MAR,March Helicopters,MARCH,United Kingdom
MAS,Malaysia Airlines,MALAYSIAN,Malaysia
@@ -3633,6 +3640,7 @@ MBC,Airjet Exploracao Aerea de Carga,MABECO,Angola
MBE,Martin-Baker,MARTIN,United Kingdom
MBG,Zephyr Aviation,CHALGROVE,United Kingdom
MBI,Mountain Bird,MOUNTAIN BIRD,United States
MBK,Chrono Jet,MATBLACK,Canada
MBL,First City Air,FIRST CITY,United Kingdom
MBN,Zambian Airways,ZAMBIANA,Zambia
MBO,Mobil Oil,MOBIL,Canada
@@ -4441,6 +4449,7 @@ PDI,Paradise Island Airways,PARADISE ISLAND,United States
PDQ,PDQ Air Charter,DISPATCH,United States
PDR,COMAV,SPEEDSTER,Namibia
PDT,Piedmont Airlines,PIEDMONT,United States
PDU,Purdue University,PURDUE,United States
PDV,Elicar,ELICAR,Italy
PDY,Pen-Avia,PENDLEY,United Kingdom
PEA,Pan Europeenne Air Service,,France
@@ -4564,6 +4573,7 @@ PNH,Panh,KUBAN LIK,Russia
PNK,Air Pink,AIRPINK,Serbia
PNL,Aero Personal,AEROPERSONAL,Mexico
PNM,Panorama,PANORAMA,Spain
PNO,Panorama Aviation,PANO,Canada
PNP,Pineapple Air,PINEAPPLE AIR,Bahamas
PNR,PAN Air,SKYJET,Spain
PNS,Survey Udara (Penas),PENAS,Indonesia
@@ -4931,6 +4941,7 @@ RNB,Rosneft-Baltika,ROSBALT,Russia
RND,Rutland Aviation,RUTLAND,United Kingdom
RNE,Air Salone,AIR SALONE,Sierra Leone
RNG,Orange Aircraft Leasing,ORANGE,Netherlands
RNI,Rennia Aviation,RENNIA,United States
RNM,Aeronem Air Cargo,AEROMNEM,Ecuador
RNR,Air Cargo Masters,RUNNER,United States
RNS,Ronso,RONSO,Mexico
@@ -5216,6 +5227,7 @@ SFU,Solent Flight,SAINTS,United Kingdom
SFW,Safi Airways,SAFI,AF
SFX,S.K. Logistics,SWAMP FOX,United States
SFY,Gulf Flite Center,SKY FLITE,United States
SFZ,Pionair,SKYFORCE,Australia
SGA,Air Saigon,AIR SAIGON,Vietnam
SGB,Sky King Inc.,SONGBIRD,United States
SGC,SGC Aviation,SAINT GEORGE,Austria
@@ -5270,7 +5282,7 @@ SIE,Sierra Express,SEREX,United States
SIH,Skynet Airlines,BLUEJET,Ireland
SII,Aero Servicios Ejecutivos Internacionales,ASEISA,Mexico
SIJ,Seco International,,Japan
SIL,Servicios Aeronáuticos Integrales,SERVICIOS INTEGRALES,Mexico
SIL,Silver Airways,SILVER WINGS,United States
SIM,Star Air,,Sierra Leone
SIO,Sirio,SIRIO,Italy
SIP,Air Spirit,AIR SPIRIT,United States
@@ -5802,6 +5814,7 @@ TKC,Tikal Jets Airlines,TIKAL,Guatemala
TKE,Take Air Line,ISLAND BIRD,France
TKJ,Tarkim Aviation,TARKIM AVIATION,Turkey
TKK,FlyADVANCED,,United States
TKM,JM Family Aviation,TACOMA,United States
TKR,Canadian Interagency Forest Fire Centre,TANKER,Canada
TKS,Tomsk-Avia,,Russia
TKX,Tropical International Airways,TROPEXPRESS,Saint Kitts and Nevis
@@ -5827,6 +5840,7 @@ TLW,Teamline Air,Teamline,Austria
TLX,Telesis Transair,TELESIS,United States
TLY,Top Fly,TOPFLY,Spain
TMA,Trans Mediterranean Airlines,TANGO LIMA,Lebanon
TMB,Volato,TOMBO,United States
TMC,Travel Management Company,TRAIL BLAZER,United States
TMD,Transmandu,TRANSMANDU,Venezuela
TME,Aero Taxi del Centro de Mexico,TAXICENTRO,Mexico
@@ -6086,6 +6100,7 @@ UDC,DonbassAero,DONBASS AERO,Ukraine
UDN,Dniproavia,DNIEPRO,Ukraine
UEA,United Eagle Airlines,UNITED EAGLE,China
UED,Air LA,AIR L-A,United States
UEE,United Eagle,EAGLE SLOVENIA,Slovenia
UEJ,Jetcorp,,United States
UES,Ues-Avia Aircompany,AVIASYSTEM,Ukraine
UEU,United European Airlines,UNITED EUROPEAN,Romania
@@ -6200,7 +6215,7 @@ VAL,Voyageur Airways,VOYAGEUR,Canada
VAM,Ameravia,AMERAVIA,Uruguay
VAN,Caravan Air,CAMEL,Mauritania
VAP,Phuket Air,PHUKET AIR,Thailand
VAR,Veca Airlines,VECA,El Salvador
VAR,United Aviate Academy,VARNEY,United States
VAS,ATRAN Cargo Airlines,ATRAN,Russian Federation
VAT,Visionair,VISIONAIR,Ireland
VAU,V Australia Airlines,,Australia
@@ -6278,7 +6293,7 @@ VIS,Vision Air International,,Pakistan
VIT,Aviastar Mandiri,AVIASTAR,Indonesia
VIV,Aeroenlaces Nacionales,AEROENLACES,Mexico
VIZ,Aerovis Airlines,AEROVIZ,Ukraine
VJA,ValuJet Airlines,CRITTER,United States
VJA,Vista America,ICONIC,United States
VJC,Vietjet Air,VIETJET,Vietnam
VJE,AvJet Routing,,United Arab Emirates
VJM,Viajes Ejecutivos Mexicanos,VIAJES MEXICANOS,Mexico
@@ -6320,7 +6335,7 @@ VNG,Aero Servicios Vanguardia,VANGUARDIA,Mexico
VNK,Vipport Joint Stock Company,,Russia
VNL,Vanilla Air,VANILLA,Japan
VNP,Virgin Pacific,,Fiji
VNT,Avient Air Zambia,AVIENT,Zambia
VNT,Ventura,VENTURA,United States
VNX,Fly Advance,VANCE,United States
VNZ,Tbilaviamsheni,TBILAVIA,Georgia
VOA,Viaggio Air,VIAGGIO,Bulgaria
@@ -6331,7 +6346,7 @@ VOI,Volaris,VOLARIS,Mexico
VOL,Blue Chip Jet,BLUE SPEED,Sweden
VOO,Volotea,,Spain
VOR,Flight Calibration Services Ltd.,FLIGHT CAL,United Kingdom
VOS,Rovos Air,ROVOS,South Africa
VOS,Volaris El Salvador,JETSAL,South Africa
VOZ,Virgin Australia,VELOCITY,Australia
VPA,DanubeWings,VIP TAXI,Slovakia
VPB,Veteran Air,VETERAN,Ukraine
@@ -6422,7 +6437,7 @@ WAU,Wizz Air Ukraine,,Ukraine
WAV,Warbelow's Air Ventures,WARBELOW,United States
WAW,Wings Airways,WING SHUTTLE,United States
WAY,Airways,GARONNE,France
WAZ,Wizz Air,WIZZ SKY,United Arab Emirates
WAZ,Wizz Air Abu Dabi,WIZZ SKY,United Arab Emirates
WBA,Finncomm Airlines,WESTBIRD,Finland
WBR,Multi-Aero,WEBER,United States
WCA,West Coast Airways,WEST-LEONE,Sierra Leone
@@ -6499,6 +6514,8 @@ WLX,West Air Luxembourg,WEST LUX,Luxembourg
WMA,Watermakers Air,WATERMAKERS,United States
WML,Chantilly Air,MARLIN,United States
WMN,Trident Aircraft,WATERMAN,United States
WMT,Wizz Air Malta,WIZZAIR MALTA,Malta
WMU,W.M. Uni. College of Aviation,SKYBRONCO,United States
WNA,Winair,WINAIR,United States
WNR,Wondair on Demand Aviation,WONDAIR,Spain
WOA,World Airways,WORLD,United States
@@ -6535,6 +6552,7 @@ WTP,Westpoint Air,WESTPOINT,Canada
WTV,Western Aviators,WESTAVIA,United States
WUE,Adolf Wurth GmbH,FASTY,Germany
WUK,Wizz Air UK,WIZZ GO,United Kingdom
WUP,Wheels Up,UPJET,United States
WVA,Hand D Aviation,WABASH VALLEY,United States
WVL,Wizz Air Bulgaria,WIZZBUL,Bulgaria
WVZ,Winair,,HR
@@ -6548,7 +6566,7 @@ WYC,Wycombe Air Centre,WYCOMBE,United Kingdom
WYG,Wyoming Airlines,WYOMING,United States
WYT,2 Sqn No 1 Elementary Flying Training School,WYTON,United Kingdom
WZP,Zip,ZIPPER,Canada
WZZ,Wizz Air,WIZZAIR,Hungary
WZZ,Wizz Air Hungary,WIZZAIR,Hungary
X9F,FRA Air,,Germany
XAA,Aeronautical Radio Inc,,United States
XAB,Xabre Aerolineas,AERO XABRE,Mexico
@@ -1,5 +1,6 @@
#!/usr/bin/env python3
# Copyright (C) 2021 ArjanOnwezen
# Copyright (C) 2025 Tommaso Ventafridda
#
# 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
@@ -22,26 +23,251 @@
# as a source.
# -------------------------------------------------------------------------------------
import csv
import os
import shutil
import unicodedata
icao_codes=bytearray()
airlines_countries=bytearray()
row_count=0
database=open("airlines.db", "wb")
with open('airlinecodes.txt', 'rt', encoding="utf-8") as csv_file:
sorted_lines=sorted(csv_file.readlines()[1:])
for row in csv.reader(sorted_lines, quotechar='"', delimiter=',', quoting=csv.QUOTE_ALL, skipinitialspace=True):
icao_code=row[0]
# Normalize some unicode characters
airline=unicodedata.normalize('NFKD', row[1][:32]).encode('ascii', 'ignore')
country=unicodedata.normalize('NFKD', row[3][:32]).encode('ascii', 'ignore')
if len(icao_code) == 3 :
airline_padding=bytearray()
country_padding=bytearray()
icao_codes=icao_codes+bytearray(icao_code+'\0', encoding='ascii')
airline_padding=bytearray('\0' * (32 - len(airline)), encoding='ascii')
country_padding=bytearray('\0' * (32 - len(country)), encoding='ascii')
airlines_countries=airlines_countries+bytearray(airline+airline_padding+country+country_padding)
row_count+=1
database.write(icao_codes+airlines_countries)
print("Total of", row_count, "ICAO codes stored in database")
import urllib.request
from dataclasses import dataclass
from typing import Dict, Set, Tuple
@dataclass
class AirlineRecord:
"""Represents an airline record with all relevant fields"""
icao_code: str
airline: str
country: str
def __hash__(self):
return hash(self.icao_code)
def __eq__(self, other):
if not isinstance(other, AirlineRecord):
return False
return self.icao_code == other.icao_code
def data_equals(self, other):
"""Check if all data fields are equal (excluding ICAO code which is the key)"""
if not isinstance(other, AirlineRecord):
return False
return self.airline == other.airline and self.country == other.country
def backup_previous_file() -> bool:
"""Backup the existing airlinecodes.txt file if it exists. Returns True if backup was made."""
if os.path.exists("airlinecodes.txt"):
shutil.copy2("airlinecodes.txt", "airlinecodes_previous.txt")
print("Backed up previous airline codes file")
return True
return False
def download_airline_codes() -> None:
"""Download the airline codes file from GitHub"""
url = "https://raw.githubusercontent.com/kx1t/planefence-airlinecodes/main/airlinecodes.txt"
print(f"Downloading airline codes database from {url}...")
try:
urllib.request.urlretrieve(url, "airlinecodes.txt")
print("Download completed successfully.")
except Exception as e:
print(f"Error downloading airline codes file: {e}")
raise
def parse_airline_file(filename: str) -> Dict[str, AirlineRecord]:
"""Parse airline codes file and return a dictionary of records"""
records = {}
if not os.path.exists(filename):
return records
try:
with open(filename, "rt", encoding="utf-8") as csv_file:
sorted_lines = sorted(csv_file.readlines()[1:])
for row in csv.reader(
sorted_lines,
quotechar='"',
delimiter=",",
quoting=csv.QUOTE_ALL,
skipinitialspace=True,
):
if len(row) >= 4: # Ensure we have enough columns
icao_code = row[0]
# Normalize some unicode characters
airline = (
unicodedata.normalize("NFKD", row[1][:32])
.encode("ascii", "ignore")
.decode("ascii")
)
country = (
unicodedata.normalize("NFKD", row[3][:32])
.encode("ascii", "ignore")
.decode("ascii")
)
if len(icao_code) == 3:
records[icao_code] = AirlineRecord(
icao_code=icao_code, airline=airline, country=country
)
except Exception as e:
print(f"Warning: Could not parse airline file {filename}: {e}")
return {}
return records
def compare_records(
old_records: Dict[str, AirlineRecord], new_records: Dict[str, AirlineRecord]
) -> Tuple[Set[str], Set[str], Set[str]]:
"""Compare old and new records, return sets of new, deleted, and changed ICAO codes"""
old_keys = set(old_records.keys())
new_keys = set(new_records.keys())
new_icao_codes = new_keys - old_keys
deleted_icao_codes = old_keys - new_keys
# Check for changes in existing records
changed_icao_codes = set()
for icao in old_keys & new_keys:
if not old_records[icao].data_equals(new_records[icao]):
changed_icao_codes.add(icao)
return new_icao_codes, deleted_icao_codes, changed_icao_codes
def write_database(records: Dict[str, AirlineRecord]) -> int:
"""Write records to database file using original format"""
icao_codes = bytearray()
airlines_countries = bytearray()
row_count = 0
# Sort by ICAO code for consistency
sorted_records = sorted(records.values(), key=lambda r: r.icao_code)
for record in sorted_records:
# Normalize and encode data (same as original)
airline = unicodedata.normalize("NFKD", record.airline[:32]).encode(
"ascii", "ignore"
)
country = unicodedata.normalize("NFKD", record.country[:32]).encode(
"ascii", "ignore"
)
# Add ICAO code with null terminator (original format)
icao_codes = icao_codes + bytearray(record.icao_code + "\0", encoding="ascii")
# Add padded data fields (original format)
airline_padding = bytearray("\0" * (32 - len(airline)), encoding="ascii")
country_padding = bytearray("\0" * (32 - len(country)), encoding="ascii")
airlines_countries = airlines_countries + bytearray(
airline + airline_padding + country + country_padding
)
row_count += 1
with open("airlines.db", "wb") as database:
database.write(icao_codes + airlines_countries)
return row_count
def create_database() -> None:
"""Create the airline database from the downloaded file"""
# Backup existing file before downloading new one
has_previous = backup_previous_file()
# Download new file
download_airline_codes()
# Parse both files for comparison
old_records = {}
if has_previous:
print("Parsing previous airline codes file...")
old_records = parse_airline_file("airlinecodes_previous.txt")
print(f"Found {len(old_records)} records in previous file")
print("Parsing new airline codes file...")
new_records = parse_airline_file("airlinecodes.txt")
print(f"Found {len(new_records)} records in new file")
# Compare records if we have a previous version
if old_records:
new_icao_codes, deleted_icao_codes, changed_icao_codes = compare_records(
old_records, new_records
)
# Print change statistics
print("\n" + "=" * 50)
print("AIRLINE CODES CHANGE SUMMARY")
print("=" * 50)
print(f"New records: {len(new_icao_codes):>8}")
print(f"Deleted records: {len(deleted_icao_codes):>8}")
print(f"Changed records: {len(changed_icao_codes):>8}")
print(f"Total records: {len(new_records):>8}")
print("=" * 50)
# Show examples of changes (limited to avoid spam)
if new_icao_codes and len(new_icao_codes) <= 15:
print(f"\nNew ICAO codes: {', '.join(sorted(new_icao_codes))}")
elif new_icao_codes:
sample_new = sorted(list(new_icao_codes))[:10]
print(
f"\nSample new ICAO codes: {', '.join(sample_new)} (and {len(new_icao_codes)-10} more)"
)
if deleted_icao_codes and len(deleted_icao_codes) <= 15:
print(f"Deleted ICAO codes: {', '.join(sorted(deleted_icao_codes))}")
elif deleted_icao_codes:
sample_deleted = sorted(list(deleted_icao_codes))[:10]
print(
f"Sample deleted ICAO codes: {', '.join(sample_deleted)} (and {len(deleted_icao_codes)-10} more)"
)
if changed_icao_codes and len(changed_icao_codes) <= 15:
print(f"Changed ICAO codes: {', '.join(sorted(changed_icao_codes))}")
elif changed_icao_codes:
sample_changed = sorted(list(changed_icao_codes))[:10]
print(
f"Sample changed ICAO codes: {', '.join(sample_changed)} (and {len(changed_icao_codes)-10} more)"
)
# Show some specific examples of changes
if changed_icao_codes:
print(f"\nExample changes:")
for icao in sorted(list(changed_icao_codes))[:3]:
old_rec = old_records[icao]
new_rec = new_records[icao]
if old_rec.airline != new_rec.airline:
print(
f" {icao}: Airline '{old_rec.airline}''{new_rec.airline}'"
)
if old_rec.country != new_rec.country:
print(
f" {icao}: Country '{old_rec.country}''{new_rec.country}'"
)
else:
print(
"\nNo previous airline codes file found - this appears to be the first run"
)
# Create database from new records
print("\nCreating airline database...")
row_count = write_database(new_records)
print("Total of", row_count, "ICAO codes stored in database")
def cleanup_temp() -> None:
"""Cleanup temporary files created during the process"""
if os.path.exists("airlinecodes_previous.txt"):
os.remove("airlinecodes_previous.txt")
if __name__ == "__main__":
try:
create_database()
cleanup_temp()
except Exception as e:
print(f"Error creating airline database: {e}")
else:
print("Airline database created successfully.")
+1 -2
View File
@@ -3,6 +3,5 @@
Licensed under [GNU GPL v3](../../../LICENSE)
USAGE:
- Copy file from: https://opensky-network.org/datasets/metadata/aircraftDatabase.csv
- Run Python 3 script: `./make_icao24_db.py`
- Copy file to /ADSB folder on SDCARD
- Move "icao24.db" file to /ADSB folder on SDCARD
File diff suppressed because it is too large Load Diff
+267 -34
View File
@@ -1,6 +1,7 @@
#!/usr/bin/env python3
# Copyright (C) 2021 ArjanOnwezen
# Copyright (C) 2025 Tommaso Ventafridda
#
# 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
@@ -20,43 +21,275 @@
# -------------------------------------------------------------------------------------
# Create icao24.db, used for ADS-B receiver application, using
# https://opensky-network.org/datasets/metadata/aircraftDatabase.csv
# https://opensky-network.org/datasets/metadata/aircraftDatabase.csv
# as a source.
# -------------------------------------------------------------------------------------
import csv
import unicodedata
icao24_codes=bytearray()
data=bytearray()
row_count=0
import os
import shutil
import urllib.request
from dataclasses import dataclass
from typing import Dict, Set, Tuple
database=open("icao24.db", "wb")
with open('aircraftDatabase.csv', 'rt') as csv_file:
sorted_lines=sorted(csv_file.readlines()[1:])
for row in csv.reader(sorted_lines, quotechar='"', delimiter=',', quoting=csv.QUOTE_ALL, skipinitialspace=True):
# only store in case enough info is available
if len(row) == 27 and len(row[0]) == 6 and len(row[1]) > 0:
icao24_code=row[0][:6].upper()
registration=row[1][:8].encode('ascii', 'ignore')
manufacturer=row[3][:32].encode('ascii', 'ignore')
model=row[4][:32].encode('ascii', 'ignore')
# in case icao aircraft type isn't, use ac type like BALL for balloon
if len(row[8]) == 3:
actype=row[8][:3].encode('ascii', 'ignore')
else:
actype=row[5][:4].encode('ascii', 'ignore')
owner=row[13][:32].encode('ascii', 'ignore')
operator=row[9][:32].encode('ascii', 'ignore')
#padding
icao24_codes.extend(bytearray(icao24_code+'\0', encoding='ascii'))
registration_padding=bytearray('\0' * (9 - len(registration)), encoding='ascii')
manufacturer_padding=bytearray('\0' * (33 - len(manufacturer)), encoding='ascii')
model_padding=bytearray('\0' * (33 - len(model)), encoding='ascii')
actype_padding=bytearray('\0' * (5 - len(actype)), encoding='ascii')
owner_padding=bytearray('\0' * (33 - len(owner)), encoding='ascii')
operator_padding=bytearray('\0' * (33 - len(operator)), encoding='ascii')
data.extend(bytearray(registration+registration_padding+manufacturer+manufacturer_padding+model+model_padding+actype+actype_padding+owner+owner_padding+operator+operator_padding))
row_count+=1
database.write(icao24_codes+data)
print("Total of", row_count, "ICAO codes stored in database")
@dataclass
class AircraftRecord:
"""Represents an aircraft record with all relevant fields"""
icao24: str
registration: str
manufacturer: str
model: str
actype: str
owner: str
operator: str
def __hash__(self):
return hash(self.icao24)
def __eq__(self, other):
if not isinstance(other, AircraftRecord):
return False
return self.icao24 == other.icao24
def data_equals(self, other):
"""Check if all data fields are equal (excluding ICAO24 which is the key)"""
if not isinstance(other, AircraftRecord):
return False
return (
self.registration == other.registration
and self.manufacturer == other.manufacturer
and self.model == other.model
and self.actype == other.actype
and self.owner == other.owner
and self.operator == other.operator
)
def backup_previous_csv() -> bool:
"""Backup the existing CSV file if it exists. Returns True if backup was made."""
if os.path.exists("aircraftDatabase.csv"):
shutil.copy2("aircraftDatabase.csv", "aircraftDatabase_previous.csv")
print("Backed up previous CSV file")
return True
return False
def download_aircraft_database() -> None:
"""Download the aircraft database file from OpenSky Network"""
url = "https://opensky-network.org/datasets/metadata/aircraftDatabase.csv"
print(f"Downloading aircraft database from {url}...")
try:
urllib.request.urlretrieve(url, "aircraftDatabase.csv")
print("Download completed successfully.")
except Exception as e:
print(f"Error downloading aircraft database file: {e}")
raise
def parse_csv_file(filename: str) -> Dict[str, AircraftRecord]:
"""Parse CSV file and return a dictionary of records"""
records = {}
if not os.path.exists(filename):
return records
try:
with open(filename, "rt", encoding="utf-8") as csv_file:
sorted_lines = sorted(csv_file.readlines()[1:])
for row in csv.reader(
sorted_lines,
quotechar='"',
delimiter=",",
quoting=csv.QUOTE_ALL,
skipinitialspace=True,
):
# only store in case enough info is available
if len(row) == 27 and len(row[0]) == 6 and len(row[1]) > 0:
icao24_code = row[0][:6].upper()
registration = row[1][:8]
manufacturer = row[3][:32]
model = row[4][:32]
# in case icao aircraft type isn't, use ac type like BALL for balloon
if len(row[8]) == 3:
actype = row[8][:3]
else:
actype = row[5][:4]
owner = row[13][:32]
operator = row[9][:32]
records[icao24_code] = AircraftRecord(
icao24=icao24_code,
registration=registration,
manufacturer=manufacturer,
model=model,
actype=actype,
owner=owner,
operator=operator,
)
except Exception as e:
print(f"Warning: Could not parse CSV file {filename}: {e}")
return {}
return records
def compare_records(
old_records: Dict[str, AircraftRecord], new_records: Dict[str, AircraftRecord]
) -> Tuple[Set[str], Set[str], Set[str]]:
"""Compare old and new records, return sets of new, deleted, and changed ICAO codes"""
old_keys = set(old_records.keys())
new_keys = set(new_records.keys())
new_icao_codes = new_keys - old_keys
deleted_icao_codes = old_keys - new_keys
# Check for changes in existing records
changed_icao_codes = set()
for icao in old_keys & new_keys:
if not old_records[icao].data_equals(new_records[icao]):
changed_icao_codes.add(icao)
return new_icao_codes, deleted_icao_codes, changed_icao_codes
def write_database(records: Dict[str, AircraftRecord]) -> int:
"""Write records to database file using original format"""
# bytearrays to store icao24 codes and data
icao24_codes = bytearray()
data = bytearray()
row_count = 0
# Sort by ICAO code for consistency
sorted_records = sorted(records.values(), key=lambda r: r.icao24)
for record in sorted_records:
# Encode data fields
registration = record.registration.encode("ascii", "ignore")
manufacturer = record.manufacturer.encode("ascii", "ignore")
model = record.model.encode("ascii", "ignore")
actype = record.actype.encode("ascii", "ignore")
owner = record.owner.encode("ascii", "ignore")
operator = record.operator.encode("ascii", "ignore")
# Add ICAO code with null terminator (original format)
icao24_codes.extend(bytearray(record.icao24 + "\0", encoding="ascii"))
# Add padded data fields (original format)
registration_padding = bytearray(
"\0" * (9 - len(registration)), encoding="ascii"
)
manufacturer_padding = bytearray(
"\0" * (33 - len(manufacturer)), encoding="ascii"
)
model_padding = bytearray("\0" * (33 - len(model)), encoding="ascii")
actype_padding = bytearray("\0" * (5 - len(actype)), encoding="ascii")
owner_padding = bytearray("\0" * (33 - len(owner)), encoding="ascii")
operator_padding = bytearray("\0" * (33 - len(operator)), encoding="ascii")
data.extend(
bytearray(
registration
+ registration_padding
+ manufacturer
+ manufacturer_padding
+ model
+ model_padding
+ actype
+ actype_padding
+ owner
+ owner_padding
+ operator
+ operator_padding
)
)
row_count += 1
with open("icao24.db", "wb") as database:
database.write(icao24_codes + data)
return row_count
def create_database() -> None:
"""Create the ICAO24 database from the downloaded file"""
# Backup existing CSV before downloading new one
has_previous = backup_previous_csv()
# Download new CSV
download_aircraft_database()
# Parse both CSV files for comparison
old_records = {}
if has_previous:
print("Parsing previous CSV file...")
old_records = parse_csv_file("aircraftDatabase_previous.csv")
print(f"Found {len(old_records)} records in previous CSV")
print("Parsing new CSV file...")
new_records = parse_csv_file("aircraftDatabase.csv")
print(f"Found {len(new_records)} records in new CSV")
# Compare records if we have a previous version
if old_records:
new_icao_codes, deleted_icao_codes, changed_icao_codes = compare_records(
old_records, new_records
)
# Print change statistics
print("\n" + "=" * 50)
print("CSV CHANGE SUMMARY")
print("=" * 50)
print(f"New records: {len(new_icao_codes):>8}")
print(f"Deleted records: {len(deleted_icao_codes):>8}")
print(f"Changed records: {len(changed_icao_codes):>8}")
print(f"Total records: {len(new_records):>8}")
print("=" * 50)
# Show examples of changes (limited to avoid spam)
if new_icao_codes and len(new_icao_codes) <= 10:
print(f"\nNew ICAO24 codes: {', '.join(sorted(new_icao_codes))}")
elif new_icao_codes:
sample_new = sorted(list(new_icao_codes))[:5]
print(
f"\nSample new ICAO24 codes: {', '.join(sample_new)} (and {len(new_icao_codes)-5} more)"
)
if deleted_icao_codes and len(deleted_icao_codes) <= 10:
print(f"Deleted ICAO24 codes: {', '.join(sorted(deleted_icao_codes))}")
elif deleted_icao_codes:
sample_deleted = sorted(list(deleted_icao_codes))[:5]
print(
f"Sample deleted ICAO24 codes: {', '.join(sample_deleted)} (and {len(deleted_icao_codes)-5} more)"
)
if changed_icao_codes and len(changed_icao_codes) <= 10:
print(f"Changed ICAO24 codes: {', '.join(sorted(changed_icao_codes))}")
elif changed_icao_codes:
sample_changed = sorted(list(changed_icao_codes))[:5]
print(
f"Sample changed ICAO24 codes: {', '.join(sample_changed)} (and {len(changed_icao_codes)-5} more)"
)
else:
print("\nNo previous CSV found - this appears to be the first run")
# Create database from new records
print("\nCreating ICAO24 database...")
row_count = write_database(new_records)
print("Total of", row_count, "ICAO codes stored in database")
def cleanup_previous_csv() -> None:
"""Remove the previous CSV file if it exists"""
if os.path.exists("aircraftDatabase_previous.csv"):
os.remove("aircraftDatabase_previous.csv")
if __name__ == "__main__":
try:
create_database()
cleanup_previous_csv()
except Exception as e:
print(f"Error creating ICAO24 database: {e}")
else:
print("ICAO24 database created successfully.")

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