mirror of
https://github.com/portapack-mayhem/mayhem-firmware.git
synced 2026-09-13 18:19:33 +00:00
Compare commits
33 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| a6f886ad0a | |||
| 2d2d9223f2 | |||
| 6e33007cb6 | |||
| 3f7b59f27e | |||
| 375d1ad54e | |||
| 6b05878532 | |||
| 3983749f11 | |||
| 28e2f770e5 | |||
| 4c5275247e | |||
| a0185d28eb | |||
| 2500df310f | |||
| 54f9ff116b | |||
| 47f231ad63 | |||
| f90d3fabce | |||
| 4e276cdc71 | |||
| 0eb03373b1 | |||
| d5ea0f0369 | |||
| 22cc311447 | |||
| 4fbba205ad | |||
| 7b91103118 | |||
| c0aa4a1738 | |||
| e26f77ee77 | |||
| f53262783a | |||
| b456c18008 | |||
| fa4b74fd6f | |||
| 18bc2cf11c | |||
| 20f28c8331 | |||
| ea38a0fe48 | |||
| fb2e576b34 | |||
| 1d79e30d4b | |||
| be372e12bc | |||
| 00853f526a | |||
| 37ca7a601c |
@@ -1 +1 @@
|
||||
v2.0.2
|
||||
v2.1.0
|
||||
|
||||
@@ -1 +1 @@
|
||||
v2.1.0
|
||||
v2.2.0
|
||||
|
||||
@@ -62,6 +62,7 @@ cmake-build-debug/
|
||||
.vscode
|
||||
.idea
|
||||
*.swp
|
||||
.claude
|
||||
|
||||
# VSCodium extensions
|
||||
.history
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 */
|
||||
|
||||
@@ -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 */
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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 */
|
||||
|
||||
@@ -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};
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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,",
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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};
|
||||
|
||||
|
||||
@@ -84,6 +84,7 @@ void SubGhzDView::focus() {
|
||||
SubGhzDView::SubGhzDView(NavigationView& nav)
|
||||
: nav_{nav} {
|
||||
add_children({&rssi,
|
||||
&channel,
|
||||
&field_rf_amp,
|
||||
&field_lna,
|
||||
&field_vga,
|
||||
|
||||
@@ -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_};
|
||||
|
||||
@@ -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}};
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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__*/
|
||||
@@ -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
|
||||
@@ -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,
|
||||
};
|
||||
}
|
||||
@@ -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
|
||||
@@ -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
@@ -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,
|
||||
};
|
||||
}
|
||||
@@ -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};
|
||||
@@ -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
|
||||
@@ -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
@@ -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
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,515 @@
|
||||
/*
|
||||
* 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 "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;
|
||||
}
|
||||
|
||||
// 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 "";
|
||||
}
|
||||
|
||||
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) + "\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";
|
||||
}
|
||||
}
|
||||
|
||||
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();
|
||||
}
|
||||
|
||||
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,
|
||||
&field_rf_amp,
|
||||
&field_lna,
|
||||
&field_vga,
|
||||
&rssi,
|
||||
&field_volume,
|
||||
&channel,
|
||||
&label_status,
|
||||
&label_beacons_count,
|
||||
&label_latest,
|
||||
&text_latest_info,
|
||||
&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();
|
||||
};
|
||||
|
||||
signal_token_tick_second = rtc_time::signal_tick_second += [this]() {
|
||||
this->on_tick_second();
|
||||
};
|
||||
|
||||
// Configure receiver for 406.028 MHz EPIRB frequency
|
||||
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() {
|
||||
field_rf_amp.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++;
|
||||
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
|
||||
std::string beacon_info = format_beacon_summary(beacon);
|
||||
if (beacon.emergency_type != EmergencyType::Other) {
|
||||
beacon_info += " [" + format_emergency_type(beacon.emergency_type) + "]";
|
||||
}
|
||||
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;
|
||||
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));
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
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
|
||||
@@ -0,0 +1,263 @@
|
||||
/*
|
||||
* 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) {}
|
||||
};
|
||||
|
||||
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;
|
||||
|
||||
EPIRBBeacon()
|
||||
: beacon_id(0), beacon_type(BeaconType::Other), emergency_type(EmergencyType::Other), location(), country_code(0), vessel_name(), timestamp(), sequence_number(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);
|
||||
};
|
||||
|
||||
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);
|
||||
|
||||
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 = 3 * 16;
|
||||
|
||||
ui::Text label_frequency{
|
||||
{0 * 8, 0 * 16, 10 * 8, 1 * 16},
|
||||
"406.028 MHz"};
|
||||
|
||||
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"};
|
||||
|
||||
// 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, 3 * 16, 240, 168}};
|
||||
|
||||
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;
|
||||
|
||||
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__
|
||||
+19
-17
@@ -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
|
||||
+3
-4
@@ -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
@@ -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
@@ -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
@@ -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
@@ -38,7 +38,7 @@ __attribute__((section(".external_app.app_fmradio.application_information"), use
|
||||
/*.header_version = */ CURRENT_HEADER_VERSION,
|
||||
/*.app_version = */ VERSION_MD5,
|
||||
|
||||
/*.app_name = */ "FM Radio",
|
||||
/*.app_name = */ "Radio",
|
||||
/*.bitmap_data = */ {
|
||||
0x00,
|
||||
0x00,
|
||||
|
||||
+48
-7
@@ -1,6 +1,6 @@
|
||||
/*
|
||||
* Copyright (C) 2024 HT Otto
|
||||
* Copyright (C) 2025 RocketGod - Added modes from my Flipper Zero RF Jammer App - https://betaskynet.com
|
||||
* Copyright (C) 2024 HTotoo
|
||||
* Copyright (C) 2025 RocketGod
|
||||
*
|
||||
* This file is part of PortaPack.
|
||||
*
|
||||
@@ -35,10 +35,34 @@ using namespace ui;
|
||||
|
||||
namespace ui::external_app::fmradio {
|
||||
|
||||
#include "external/ui_grapheq.cpi"
|
||||
|
||||
void FmRadioView::focus() {
|
||||
field_frequency.focus();
|
||||
}
|
||||
|
||||
void FmRadioView::show_hide_gfx(bool show) {
|
||||
gr.hidden(!show);
|
||||
gr.set_paused(!show);
|
||||
waveform.set_paused(show);
|
||||
btn_fav_0.hidden(show);
|
||||
btn_fav_1.hidden(show);
|
||||
btn_fav_2.hidden(show);
|
||||
btn_fav_3.hidden(show);
|
||||
btn_fav_4.hidden(show);
|
||||
btn_fav_5.hidden(show);
|
||||
btn_fav_6.hidden(show);
|
||||
btn_fav_7.hidden(show);
|
||||
btn_fav_8.hidden(show);
|
||||
btn_fav_9.hidden(show);
|
||||
txt_save_help.hidden(show);
|
||||
btn_fav_save.hidden(show);
|
||||
field_bw.hidden(show);
|
||||
field_modulation.hidden(show);
|
||||
text_mode_label.hidden(show);
|
||||
set_dirty();
|
||||
}
|
||||
|
||||
void FmRadioView::change_mode(int32_t mod) {
|
||||
field_bw.on_change = [this](size_t n, OptionsField::value_t) { (void)n; };
|
||||
|
||||
@@ -48,8 +72,8 @@ void FmRadioView::change_mode(int32_t mod) {
|
||||
|
||||
audio_spectrum_update = false; // Reset spectrum update flag
|
||||
std::fill(audio_spectrum, audio_spectrum + 128, 0); // Clear spectrum buffer
|
||||
|
||||
ReceiverModel::Mode receiver_mode = static_cast<ReceiverModel::Mode>(mod);
|
||||
waveform.set_dirty();
|
||||
receiver_mode = static_cast<ReceiverModel::Mode>(mod);
|
||||
bool is_ssb = (mod == static_cast<int32_t>(ReceiverModel::Mode::AMAudio) &&
|
||||
(field_modulation.selected_index() == 3 || field_modulation.selected_index() == 4));
|
||||
|
||||
@@ -70,6 +94,7 @@ void FmRadioView::change_mode(int32_t mod) {
|
||||
radio_bw = index;
|
||||
receiver_model.set_am_configuration(n);
|
||||
};
|
||||
show_hide_gfx(false);
|
||||
break;
|
||||
case static_cast<int32_t>(ReceiverModel::Mode::NarrowbandFMAudio):
|
||||
audio_sampling_rate = audio::Rate::Hz_24000;
|
||||
@@ -82,6 +107,7 @@ void FmRadioView::change_mode(int32_t mod) {
|
||||
radio_bw = index;
|
||||
receiver_model.set_nbfm_configuration(n);
|
||||
};
|
||||
show_hide_gfx(false);
|
||||
break;
|
||||
case static_cast<int32_t>(ReceiverModel::Mode::WidebandFMAudio):
|
||||
audio_sampling_rate = audio::Rate::Hz_48000;
|
||||
@@ -106,7 +132,6 @@ void FmRadioView::change_mode(int32_t mod) {
|
||||
audio::set_rate(audio_sampling_rate);
|
||||
audio::output::start();
|
||||
receiver_model.set_headphone_volume(receiver_model.headphone_volume()); // WM8731 hack
|
||||
|
||||
receiver_model.enable();
|
||||
}
|
||||
|
||||
@@ -136,8 +161,10 @@ FmRadioView::FmRadioView(NavigationView& nav)
|
||||
&btn_fav_9,
|
||||
&audio,
|
||||
&waveform,
|
||||
&rssi});
|
||||
&rssi,
|
||||
&gr});
|
||||
|
||||
txt_save_help.set_focusable(false);
|
||||
txt_save_help.visible(false);
|
||||
for (uint8_t i = 0; i < 12; ++i) {
|
||||
if (freq_fav_list[i].frequency == 0) {
|
||||
@@ -179,7 +206,20 @@ FmRadioView::FmRadioView(NavigationView& nav)
|
||||
}
|
||||
};
|
||||
|
||||
waveform.on_select = [this](Waveform&) {
|
||||
if (receiver_mode != ReceiverModel::Mode::WidebandFMAudio) { // only there is spectrum message
|
||||
return;
|
||||
}
|
||||
show_hide_gfx(!btn_fav_0.hidden());
|
||||
};
|
||||
gr.set_theme(themes[current_theme].base_color, themes[current_theme].peak_color);
|
||||
gr.on_select = [this](GraphEq&) {
|
||||
current_theme = (current_theme + 1) % themes.size();
|
||||
gr.set_theme(themes[current_theme].base_color, themes[current_theme].peak_color);
|
||||
gr.set_paused(false);
|
||||
};
|
||||
update_fav_btn_texts();
|
||||
show_hide_gfx(false);
|
||||
}
|
||||
|
||||
void FmRadioView::on_btn_clicked(uint8_t i) {
|
||||
@@ -226,8 +266,9 @@ FmRadioView::~FmRadioView() {
|
||||
}
|
||||
|
||||
void FmRadioView::on_audio_spectrum() {
|
||||
if (gr.visible() && audio_spectrum_data) gr.update_audio_spectrum(*audio_spectrum_data);
|
||||
if (audio_spectrum_data && audio_spectrum_data->db.size() <= 128) {
|
||||
for (size_t i = 0; i < audio_spectrum_data->db.size(); i++) {
|
||||
for (size_t i = 0; i < audio_spectrum_data->db.size(); ++i) {
|
||||
audio_spectrum[i] = ((int16_t)audio_spectrum_data->db[i] - 127) * 256;
|
||||
}
|
||||
waveform.set_dirty();
|
||||
|
||||
+40
-5
@@ -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.
|
||||
*
|
||||
@@ -47,6 +47,8 @@ using namespace ui;
|
||||
|
||||
namespace ui::external_app::fmradio {
|
||||
|
||||
#include "external/ui_grapheq.hpp"
|
||||
|
||||
#define FMR_BTNGRID_TOP 60
|
||||
|
||||
class FmRadioView : public View {
|
||||
@@ -58,13 +60,14 @@ class FmRadioView : public View {
|
||||
|
||||
void focus() override;
|
||||
|
||||
std::string title() const override { return "FM radio"; };
|
||||
std::string title() const override { return "Radio"; };
|
||||
|
||||
private:
|
||||
NavigationView& nav_;
|
||||
RxRadioState radio_state_{};
|
||||
int16_t audio_spectrum[128]{0};
|
||||
bool audio_spectrum_update = false;
|
||||
ReceiverModel::Mode receiver_mode = ReceiverModel::Mode::WidebandFMAudio;
|
||||
AudioSpectrum* audio_spectrum_data{nullptr};
|
||||
struct Favorite {
|
||||
rf::Frequency frequency = 0;
|
||||
@@ -74,7 +77,7 @@ class FmRadioView : public View {
|
||||
Favorite freq_fav_list[12];
|
||||
audio::Rate audio_sampling_rate = audio::Rate::Hz_48000;
|
||||
uint8_t radio_bw = 0;
|
||||
|
||||
uint32_t current_theme{0};
|
||||
app_settings::SettingsManager settings_{
|
||||
"rx_fmradio",
|
||||
app_settings::Mode::RX,
|
||||
@@ -114,7 +117,8 @@ class FmRadioView : public View {
|
||||
{"favlist9_bw"sv, &freq_fav_list[9].bandwidth},
|
||||
{"favlist10_bw"sv, &freq_fav_list[10].bandwidth},
|
||||
{"favlist11_bw"sv, &freq_fav_list[11].bandwidth},
|
||||
{"radio_bw"sv, &radio_bw}}};
|
||||
{"radio_bw"sv, &radio_bw},
|
||||
{"theme"sv, ¤t_theme}}};
|
||||
|
||||
RFAmpField field_rf_amp{
|
||||
{13 * 8, 0 * 16}};
|
||||
@@ -157,7 +161,7 @@ class FmRadioView : public View {
|
||||
{21 * 8, 10, 6 * 8, 4}};
|
||||
|
||||
Waveform waveform{
|
||||
{0, 20, screen_width, 2 * 16},
|
||||
{0, 20, UI_POS_MAXWIDTH, 2 * 16},
|
||||
audio_spectrum,
|
||||
128,
|
||||
0,
|
||||
@@ -165,6 +169,8 @@ class FmRadioView : public View {
|
||||
Theme::getInstance()->bg_darkest->foreground,
|
||||
true};
|
||||
|
||||
GraphEq gr{{2, FMR_BTNGRID_TOP, UI_POS_MAXWIDTH - 4, UI_POS_MAXHEIGHT - FMR_BTNGRID_TOP}, true};
|
||||
|
||||
Button btn_fav_0{{2, FMR_BTNGRID_TOP + 0 * 34, 10 * 8, 28}, "---"};
|
||||
Button btn_fav_1{{2 + 15 * 8, FMR_BTNGRID_TOP + 0 * 34, 10 * 8, 28}, "---"};
|
||||
Button btn_fav_2{{2, FMR_BTNGRID_TOP + 1 * 34, 10 * 8, 28}, "---"};
|
||||
@@ -185,6 +191,35 @@ class FmRadioView : public View {
|
||||
void on_audio_spectrum();
|
||||
void change_mode(int32_t mod);
|
||||
|
||||
void show_hide_gfx(bool show);
|
||||
|
||||
struct ColorTheme {
|
||||
Color base_color;
|
||||
Color peak_color;
|
||||
};
|
||||
|
||||
const std::array<ColorTheme, 20> themes{
|
||||
ColorTheme{Color(255, 0, 255), Color(255, 255, 255)},
|
||||
ColorTheme{Color(0, 255, 0), Color(255, 0, 0)},
|
||||
ColorTheme{Color(0, 0, 255), Color(255, 255, 0)},
|
||||
ColorTheme{Color(255, 128, 0), Color(255, 0, 128)},
|
||||
ColorTheme{Color(128, 0, 255), Color(0, 255, 255)},
|
||||
ColorTheme{Color(255, 255, 0), Color(0, 255, 128)},
|
||||
ColorTheme{Color(255, 0, 0), Color(0, 128, 255)},
|
||||
ColorTheme{Color(0, 255, 128), Color(255, 128, 255)},
|
||||
ColorTheme{Color(128, 128, 128), Color(255, 255, 255)},
|
||||
ColorTheme{Color(255, 64, 0), Color(0, 255, 64)},
|
||||
ColorTheme{Color(0, 128, 128), Color(255, 192, 0)},
|
||||
ColorTheme{Color(0, 255, 0), Color(0, 128, 0)},
|
||||
ColorTheme{Color(32, 64, 32), Color(0, 255, 0)},
|
||||
ColorTheme{Color(64, 0, 128), Color(255, 0, 255)},
|
||||
ColorTheme{Color(0, 64, 0), Color(0, 255, 128)},
|
||||
ColorTheme{Color(255, 255, 255), Color(0, 0, 255)},
|
||||
ColorTheme{Color(128, 0, 0), Color(255, 128, 0)},
|
||||
ColorTheme{Color(0, 128, 255), Color(255, 255, 128)},
|
||||
ColorTheme{Color(64, 64, 64), Color(255, 0, 0)},
|
||||
ColorTheme{Color(255, 192, 0), Color(0, 64, 128)}};
|
||||
|
||||
MessageHandlerRegistration message_handler_audio_spectrum{
|
||||
Message::ID::AudioSpectrum,
|
||||
[this](const Message* const p) {
|
||||
|
||||
+6
-104
@@ -20,10 +20,12 @@ using namespace portapack;
|
||||
|
||||
namespace ui::external_app::gfxeq {
|
||||
|
||||
#include "external/ui_grapheq.cpi"
|
||||
|
||||
gfxEQView::gfxEQView(NavigationView& nav)
|
||||
: nav_{nav}, bar_heights(NUM_BARS, 0), prev_bar_heights(NUM_BARS, 0) {
|
||||
: nav_{nav} {
|
||||
add_children({&button_frequency, &field_rf_amp, &field_lna, &field_vga,
|
||||
&button_mood, &field_volume});
|
||||
&button_mood, &field_volume, &gr});
|
||||
|
||||
audio::output::stop();
|
||||
receiver_model.disable();
|
||||
@@ -69,6 +71,7 @@ gfxEQView::gfxEQView(NavigationView& nav)
|
||||
};
|
||||
|
||||
button_mood.on_select = [this](Button&) { this->cycle_theme(); };
|
||||
gr.set_theme(themes[current_theme].base_color, themes[current_theme].peak_color);
|
||||
}
|
||||
|
||||
// needed to answer usb serial frequency set
|
||||
@@ -87,110 +90,9 @@ void gfxEQView::focus() {
|
||||
button_frequency.focus();
|
||||
}
|
||||
|
||||
void gfxEQView::on_show() {
|
||||
needs_background_redraw = true;
|
||||
}
|
||||
|
||||
void gfxEQView::on_hide() {
|
||||
needs_background_redraw = true;
|
||||
}
|
||||
|
||||
void gfxEQView::update_audio_spectrum(const AudioSpectrum& spectrum) {
|
||||
const float bin_frequency_size = 48000.0f / 128;
|
||||
|
||||
for (int bar = 0; bar < NUM_BARS; bar++) {
|
||||
float start_freq = FREQUENCY_BANDS[bar];
|
||||
float end_freq = FREQUENCY_BANDS[bar + 1];
|
||||
|
||||
int start_bin = std::max(1, (int)(start_freq / bin_frequency_size));
|
||||
int end_bin = std::min(127, (int)(end_freq / bin_frequency_size));
|
||||
|
||||
if (start_bin >= end_bin) {
|
||||
end_bin = start_bin + 1;
|
||||
}
|
||||
|
||||
float total_energy = 0;
|
||||
int bin_count = 0;
|
||||
|
||||
for (int bin = start_bin; bin <= end_bin; bin++) {
|
||||
total_energy += spectrum.db[bin];
|
||||
bin_count++;
|
||||
}
|
||||
|
||||
float avg_db = bin_count > 0 ? (total_energy / bin_count) : 0;
|
||||
|
||||
// Manually boost highs for better visual balance
|
||||
float treble_boost = 1.0f;
|
||||
if (bar == 10)
|
||||
treble_boost = 1.7f;
|
||||
else if (bar >= 9)
|
||||
treble_boost = 1.3f;
|
||||
else if (bar >= 7)
|
||||
treble_boost = 1.3f;
|
||||
|
||||
// Mid emphasis for a V-shape effect
|
||||
float mid_boost = 1.0f;
|
||||
if (bar == 4 || bar == 5 || bar == 6) mid_boost = 1.2f;
|
||||
|
||||
float amplified_db = avg_db * treble_boost * mid_boost;
|
||||
|
||||
if (amplified_db > 255) amplified_db = 255;
|
||||
|
||||
float band_scale = 1.0f;
|
||||
int target_height = (amplified_db * RENDER_HEIGHT * band_scale) / 255;
|
||||
|
||||
if (target_height > RENDER_HEIGHT) {
|
||||
target_height = RENDER_HEIGHT;
|
||||
}
|
||||
|
||||
// Adjusted to look nice to my eyes
|
||||
float rise_speed = 0.8f;
|
||||
float fall_speed = 1.0f;
|
||||
|
||||
if (target_height > bar_heights[bar]) {
|
||||
bar_heights[bar] = bar_heights[bar] * (1.0f - rise_speed) + target_height * rise_speed;
|
||||
} else {
|
||||
bar_heights[bar] = bar_heights[bar] * (1.0f - fall_speed) + target_height * fall_speed;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void gfxEQView::render_equalizer(Painter& painter) {
|
||||
const int num_segments = RENDER_HEIGHT / SEGMENT_HEIGHT;
|
||||
const ColorTheme& theme = themes[current_theme];
|
||||
|
||||
for (int bar = 0; bar < NUM_BARS; bar++) {
|
||||
int x = HORIZONTAL_OFFSET + bar * (BAR_WIDTH + BAR_SPACING);
|
||||
int active_segments = (bar_heights[bar] * num_segments) / RENDER_HEIGHT;
|
||||
|
||||
if (prev_bar_heights[bar] > active_segments) {
|
||||
int clear_height = (prev_bar_heights[bar] - active_segments) * SEGMENT_HEIGHT;
|
||||
int clear_y = screen_height - prev_bar_heights[bar] * SEGMENT_HEIGHT;
|
||||
painter.fill_rectangle({x, clear_y, BAR_WIDTH, clear_height}, Color(0, 0, 0));
|
||||
}
|
||||
|
||||
for (int seg = 0; seg < active_segments; seg++) {
|
||||
int y = screen_height - (seg + 1) * SEGMENT_HEIGHT;
|
||||
if (y < header_height) break;
|
||||
|
||||
Color segment_color = (seg >= active_segments - 2 && seg < active_segments) ? theme.peak_color : theme.base_color;
|
||||
painter.fill_rectangle({x, y, BAR_WIDTH, SEGMENT_HEIGHT - 1}, segment_color);
|
||||
}
|
||||
|
||||
prev_bar_heights[bar] = active_segments;
|
||||
}
|
||||
}
|
||||
|
||||
void gfxEQView::paint(Painter& painter) {
|
||||
if (needs_background_redraw) {
|
||||
painter.fill_rectangle({0, header_height, screen_width, RENDER_HEIGHT}, Color(0, 0, 0));
|
||||
needs_background_redraw = false;
|
||||
}
|
||||
render_equalizer(painter);
|
||||
}
|
||||
|
||||
void gfxEQView::cycle_theme() {
|
||||
current_theme = (current_theme + 1) % themes.size();
|
||||
gr.set_theme(themes[current_theme].base_color, themes[current_theme].peak_color);
|
||||
}
|
||||
|
||||
} // namespace ui::external_app::gfxeq
|
||||
+5
-33
@@ -22,6 +22,8 @@
|
||||
|
||||
namespace ui::external_app::gfxeq {
|
||||
|
||||
#include "external/ui_grapheq.hpp"
|
||||
|
||||
class gfxEQView : public View {
|
||||
public:
|
||||
gfxEQView(NavigationView& nav);
|
||||
@@ -32,45 +34,17 @@ class gfxEQView : public View {
|
||||
|
||||
void focus() override;
|
||||
std::string title() const override { return "gfxEQ"; }
|
||||
void on_show() override;
|
||||
void on_hide() override;
|
||||
|
||||
void paint(Painter& painter) override;
|
||||
void on_freqchg(int64_t freq);
|
||||
|
||||
private:
|
||||
static constexpr ui::Dim header_height = 2 * 16;
|
||||
static constexpr int RENDER_HEIGHT = 288;
|
||||
static constexpr int NUM_BARS = 11;
|
||||
static constexpr int BAR_SPACING = 2;
|
||||
int BAR_WIDTH = (screen_width - (BAR_SPACING * (NUM_BARS - 1))) / NUM_BARS;
|
||||
static constexpr int HORIZONTAL_OFFSET = 2;
|
||||
static constexpr int SEGMENT_HEIGHT = 10;
|
||||
|
||||
static constexpr std::array<int, NUM_BARS + 1> FREQUENCY_BANDS = {
|
||||
375, // Bass warmth and low rumble (e.g., deep basslines, kick drum body)
|
||||
750, // Upper bass punch (e.g., bass guitar punch, kick drum attack)
|
||||
1500, // Lower midrange fullness (e.g., warmth in vocals, guitar body)
|
||||
2250, // Midrange clarity (e.g., vocal presence, snare crack)
|
||||
3375, // Upper midrange bite (e.g., instrument definition, vocal articulation)
|
||||
4875, // Presence and edge (e.g., guitar bite, vocal sibilance start)
|
||||
6750, // Lower brilliance (e.g., cymbal shimmer, vocal clarity)
|
||||
9375, // Brilliance and air (e.g., hi-hat crispness, breathy vocals)
|
||||
13125, // High treble sparkle (e.g., subtle overtones, synth shimmer)
|
||||
16875, // Upper treble airiness (e.g., faint harmonics, room ambiance)
|
||||
20625, // Top-end sheen (e.g., ultra-high harmonics, noise floor)
|
||||
24375 // Extreme treble limit (e.g., inaudible overtones, signal cutoff, static)
|
||||
};
|
||||
|
||||
struct ColorTheme {
|
||||
Color base_color;
|
||||
Color peak_color;
|
||||
};
|
||||
|
||||
NavigationView& nav_;
|
||||
bool needs_background_redraw{false};
|
||||
std::vector<int> bar_heights;
|
||||
std::vector<int> prev_bar_heights;
|
||||
|
||||
uint32_t current_theme{0};
|
||||
const std::array<ColorTheme, 20> themes{
|
||||
ColorTheme{Color(255, 0, 255), Color(255, 255, 255)},
|
||||
@@ -100,6 +74,7 @@ class gfxEQView : public View {
|
||||
VGAGainField field_vga{{18 * 8, 0 * 16}};
|
||||
Button button_mood{{21 * 8, 0, 6 * 8, 16}, "MOOD"};
|
||||
AudioVolumeField field_volume{{screen_width - 2 * 8, 0 * 16}};
|
||||
GraphEq gr{{2, UI_POS_DEFAULT_HEIGHT, UI_POS_MAXWIDTH - 4, UI_POS_HEIGHT_REMAINING(2)}, false};
|
||||
|
||||
rf::Frequency frequency_value{93100000};
|
||||
|
||||
@@ -111,16 +86,13 @@ class gfxEQView : public View {
|
||||
{{"theme", ¤t_theme},
|
||||
{"frequency", &frequency_value}}};
|
||||
|
||||
void update_audio_spectrum(const AudioSpectrum& spectrum);
|
||||
void render_equalizer(Painter& painter);
|
||||
void cycle_theme();
|
||||
|
||||
MessageHandlerRegistration message_handler_audio_spectrum{
|
||||
Message::ID::AudioSpectrum,
|
||||
[this](const Message* const p) {
|
||||
const auto message = *reinterpret_cast<const AudioSpectrumMessage*>(p);
|
||||
this->update_audio_spectrum(*message.data);
|
||||
this->set_dirty();
|
||||
this->gr.update_audio_spectrum(*message.data);
|
||||
}};
|
||||
|
||||
MessageHandlerRegistration message_handler_freqchg{
|
||||
|
||||
+20
-5
@@ -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
@@ -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
@@ -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;
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
};
|
||||
}
|
||||
+3
-3
@@ -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
|
||||
+2
-2
@@ -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
@@ -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
|
||||
@@ -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
|
||||
@@ -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
@@ -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,
|
||||
|
||||
@@ -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";
|
||||
|
||||
@@ -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__ */
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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__*/
|
||||
|
||||
@@ -24,7 +24,7 @@
|
||||
namespace ui {
|
||||
|
||||
RecentEntriesColumns::RecentEntriesColumns(
|
||||
const std::initializer_list<RecentEntriesColumn> columns)
|
||||
std::initializer_list<RecentEntriesColumn> columns)
|
||||
: _columns{columns} {
|
||||
}
|
||||
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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
|
||||
@@ -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();
|
||||
|
||||
+43
@@ -0,0 +1,43 @@
|
||||
|
||||
Copyright (C) 2025 HTotoo
|
||||
|
||||
|
||||
# External UI elements
|
||||
|
||||
|
||||
## Read carefully
|
||||
|
||||
|
||||
These widgets are only used in external apps!
|
||||
The concept is, we move these widgets out of FW space, and compile them with external apps. To all separately. This is multiple include, but since we compile the widget under different namespaces (and those namespaces will be under the external_app namespace) these all will be removed from the base firmware.
|
||||
|
||||
This way we can free up some spaces, and still reuse the same widgets in different apps easily.
|
||||
|
||||
## How to create external widget
|
||||
|
||||
You create a **hpp** file indet the ui/external folder, and include everything you need for your widget as you normally wanted to do.
|
||||
**Don't forget the ifdef guards!**
|
||||
**Never use any namespace for your class there!** The namespace of the actual ext app will be used where you include this.
|
||||
|
||||
Then create a **cpi** file, where you include your hpp file, and create the implementation of your widget class. Still, don't use namespace.
|
||||
|
||||
You won't need to include these files in any cmake file! (see usage, why)
|
||||
|
||||
## How to use these widgets
|
||||
|
||||
This is important, so follow the rules carefully. If it works, doesn't mean, you did it good!
|
||||
In your external app's hpp file, you need to include the widget's hpp file.
|
||||
**But be careful**, you must include it under the ext app's namespace, before your first class. For examlpe:
|
||||
|
||||
namespace ui::external_app::gfxeq {
|
||||
#include "external/ui_grapheq.hpp"
|
||||
class gfxEQView : public View {
|
||||
Then you must include the cpi file in the external app's cpp file. Again, it must be under the ext app's namespace!
|
||||
|
||||
using namespace portapack;
|
||||
namespace ui::external_app::gfxeq {
|
||||
#include "external/ui_grapheq.cpi"
|
||||
gfxEQView::gfxEQView(NavigationView& nav)
|
||||
|
||||
This must be done under each ext app that uses the same widget.
|
||||
This way, the widget will be compiled multiple times under the external_app namespace, but those will be removed from the base. Each app will be able to use it, easy to handle, easy to create.
|
||||
+221
@@ -0,0 +1,221 @@
|
||||
/*
|
||||
* 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"
|
||||
|
||||
/* GraphEq *************************************************************/
|
||||
|
||||
GraphEq::GraphEq(
|
||||
Rect parent_rect,
|
||||
bool clickable)
|
||||
: Widget{parent_rect},
|
||||
clickable_{clickable},
|
||||
bar_heights(NUM_BARS, 0),
|
||||
prev_bar_heights(NUM_BARS, 0) {
|
||||
if (clickable) {
|
||||
set_focusable(true);
|
||||
// previous_data.resize(length_, 0);
|
||||
}
|
||||
}
|
||||
|
||||
void GraphEq::set_parent_rect(const Rect new_parent_rect) {
|
||||
Widget::set_parent_rect(new_parent_rect);
|
||||
calculate_params();
|
||||
}
|
||||
|
||||
void GraphEq::calculate_params() {
|
||||
y_top = screen_rect().top();
|
||||
RENDER_HEIGHT = parent_rect().height();
|
||||
BAR_WIDTH = (parent_rect().width() - (BAR_SPACING * (NUM_BARS - 1))) / NUM_BARS;
|
||||
HORIZONTAL_OFFSET = screen_rect().left();
|
||||
}
|
||||
|
||||
bool GraphEq::is_paused() const {
|
||||
return paused_;
|
||||
}
|
||||
|
||||
void GraphEq::set_paused(bool paused) {
|
||||
paused_ = paused;
|
||||
needs_background_redraw = true;
|
||||
set_dirty();
|
||||
}
|
||||
|
||||
bool GraphEq::is_clickable() const {
|
||||
return clickable_;
|
||||
}
|
||||
|
||||
void GraphEq::getAccessibilityText(std::string& result) {
|
||||
result = paused_ ? "paused GraphEq" : "GraphEq";
|
||||
}
|
||||
|
||||
void GraphEq::getWidgetName(std::string& result) {
|
||||
result = "GraphEq";
|
||||
}
|
||||
|
||||
bool GraphEq::on_key(const KeyEvent key) {
|
||||
if (!clickable_) return false;
|
||||
|
||||
if (key == KeyEvent::Select) {
|
||||
set_paused(!paused_);
|
||||
if (on_select) {
|
||||
on_select(*this);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
bool GraphEq::on_keyboard(const KeyboardEvent key) {
|
||||
if (!clickable_) return false;
|
||||
|
||||
if (key == 32 || key == 10) {
|
||||
set_paused(!paused_);
|
||||
if (on_select) {
|
||||
on_select(*this);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
bool GraphEq::on_touch(const TouchEvent event) {
|
||||
if (!clickable_) return false;
|
||||
|
||||
switch (event.type) {
|
||||
case TouchEvent::Type::Start:
|
||||
focus();
|
||||
return true;
|
||||
|
||||
case TouchEvent::Type::End:
|
||||
set_paused(!paused_);
|
||||
if (on_select) {
|
||||
on_select(*this);
|
||||
}
|
||||
return true;
|
||||
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
void GraphEq::set_theme(Color base_color_, Color peak_color_) {
|
||||
base_color = base_color_;
|
||||
peak_color = peak_color_;
|
||||
set_dirty();
|
||||
}
|
||||
|
||||
void GraphEq::update_audio_spectrum(const AudioSpectrum& spectrum) {
|
||||
const float bin_frequency_size = 48000.0f / 128;
|
||||
|
||||
for (int bar = 0; bar < NUM_BARS; bar++) {
|
||||
float start_freq = FREQUENCY_BANDS[bar];
|
||||
float end_freq = FREQUENCY_BANDS[bar + 1];
|
||||
|
||||
int start_bin = std::max(1, (int)(start_freq / bin_frequency_size));
|
||||
int end_bin = std::min(127, (int)(end_freq / bin_frequency_size));
|
||||
|
||||
if (start_bin >= end_bin) {
|
||||
end_bin = start_bin + 1;
|
||||
}
|
||||
|
||||
float total_energy = 0;
|
||||
int bin_count = 0;
|
||||
|
||||
for (int bin = start_bin; bin <= end_bin; bin++) {
|
||||
total_energy += spectrum.db[bin];
|
||||
bin_count++;
|
||||
}
|
||||
|
||||
float avg_db = bin_count > 0 ? (total_energy / bin_count) : 0;
|
||||
|
||||
// Manually boost highs for better visual balance
|
||||
float treble_boost = 1.0f;
|
||||
if (bar == 10)
|
||||
treble_boost = 1.7f;
|
||||
else if (bar >= 9)
|
||||
treble_boost = 1.3f;
|
||||
else if (bar >= 7)
|
||||
treble_boost = 1.3f;
|
||||
|
||||
// Mid emphasis for a V-shape effect
|
||||
float mid_boost = 1.0f;
|
||||
if (bar == 4 || bar == 5 || bar == 6) mid_boost = 1.2f;
|
||||
|
||||
float amplified_db = avg_db * treble_boost * mid_boost;
|
||||
|
||||
if (amplified_db > 255) amplified_db = 255;
|
||||
|
||||
float band_scale = 1.0f;
|
||||
int target_height = (amplified_db * RENDER_HEIGHT * band_scale) / 255;
|
||||
|
||||
if (target_height > RENDER_HEIGHT) {
|
||||
target_height = RENDER_HEIGHT;
|
||||
}
|
||||
|
||||
// Adjusted to look nice to my eyes
|
||||
float rise_speed = 0.8f;
|
||||
float fall_speed = 1.0f;
|
||||
|
||||
if (target_height > bar_heights[bar]) {
|
||||
bar_heights[bar] = bar_heights[bar] * (1.0f - rise_speed) + target_height * rise_speed;
|
||||
} else {
|
||||
bar_heights[bar] = bar_heights[bar] * (1.0f - fall_speed) + target_height * fall_speed;
|
||||
}
|
||||
}
|
||||
set_dirty();
|
||||
}
|
||||
|
||||
void GraphEq::paint(Painter& painter) {
|
||||
if (!visible()) return;
|
||||
if (!is_calculated) { // calc positions first
|
||||
calculate_params();
|
||||
is_calculated = true;
|
||||
}
|
||||
if (needs_background_redraw) {
|
||||
painter.fill_rectangle(screen_rect(), Theme::getInstance()->bg_darkest->background);
|
||||
needs_background_redraw = false;
|
||||
}
|
||||
if (paused_) {
|
||||
return;
|
||||
}
|
||||
const int num_segments = RENDER_HEIGHT / SEGMENT_HEIGHT;
|
||||
uint16_t bottom = screen_rect().bottom();
|
||||
for (int bar = 0; bar < NUM_BARS; bar++) {
|
||||
int x = HORIZONTAL_OFFSET + bar * (BAR_WIDTH + BAR_SPACING);
|
||||
int active_segments = (bar_heights[bar] * num_segments) / RENDER_HEIGHT;
|
||||
|
||||
if (prev_bar_heights[bar] > active_segments) {
|
||||
int clear_height = (prev_bar_heights[bar] - active_segments) * SEGMENT_HEIGHT;
|
||||
int clear_y = bottom - prev_bar_heights[bar] * SEGMENT_HEIGHT;
|
||||
painter.fill_rectangle({x, clear_y, BAR_WIDTH, clear_height}, Theme::getInstance()->bg_darkest->background);
|
||||
}
|
||||
|
||||
for (int seg = 0; seg < active_segments; seg++) {
|
||||
int y = bottom - (seg + 1) * SEGMENT_HEIGHT;
|
||||
if (y < y_top) break;
|
||||
|
||||
Color segment_color = (seg >= active_segments - 2 && seg < active_segments) ? peak_color : base_color;
|
||||
painter.fill_rectangle({x, y, BAR_WIDTH, SEGMENT_HEIGHT - 1}, segment_color);
|
||||
}
|
||||
prev_bar_heights[bar] = active_segments;
|
||||
}
|
||||
}
|
||||
+88
@@ -0,0 +1,88 @@
|
||||
/*
|
||||
* 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__
|
||||
|
||||
#include "ui_widget.hpp"
|
||||
|
||||
class GraphEq : public Widget {
|
||||
public:
|
||||
std::function<void(GraphEq&)> on_select{};
|
||||
|
||||
GraphEq(Rect parent_rect, bool clickable = false);
|
||||
GraphEq(const GraphEq&) = delete;
|
||||
GraphEq(GraphEq&&) = delete;
|
||||
GraphEq& operator=(const GraphEq&) = delete;
|
||||
GraphEq& operator=(GraphEq&&) = delete;
|
||||
|
||||
bool is_paused() const;
|
||||
void set_paused(bool paused);
|
||||
bool is_clickable() const;
|
||||
|
||||
void paint(Painter& painter) override;
|
||||
bool on_key(const KeyEvent key) override;
|
||||
bool on_touch(const TouchEvent event) override;
|
||||
bool on_keyboard(const KeyboardEvent event) override;
|
||||
void set_parent_rect(const Rect new_parent_rect) override;
|
||||
|
||||
void getAccessibilityText(std::string& result) override;
|
||||
void getWidgetName(std::string& result) override;
|
||||
void update_audio_spectrum(const AudioSpectrum& spectrum);
|
||||
void set_theme(Color base_color, Color peak_color);
|
||||
|
||||
private:
|
||||
bool is_calculated{false};
|
||||
bool paused_{false};
|
||||
bool clickable_{false};
|
||||
bool needs_background_redraw{true}; // Redraw background only when needed.
|
||||
Color base_color = Color(255, 0, 255);
|
||||
Color peak_color = Color(255, 255, 255);
|
||||
std::vector<ui::Dim> bar_heights;
|
||||
std::vector<ui::Dim> prev_bar_heights;
|
||||
|
||||
ui::Dim y_top = 2 * 16;
|
||||
ui::Dim RENDER_HEIGHT = 288;
|
||||
ui::Dim BAR_WIDTH = 20;
|
||||
ui::Dim HORIZONTAL_OFFSET = 2;
|
||||
static const int NUM_BARS = 11;
|
||||
static const int BAR_SPACING = 2;
|
||||
static const int SEGMENT_HEIGHT = 10;
|
||||
static constexpr std::array<int16_t, NUM_BARS + 1> FREQUENCY_BANDS = {
|
||||
375, // Bass warmth and low rumble (e.g., deep basslines, kick drum body)
|
||||
750, // Upper bass punch (e.g., bass guitar punch, kick drum attack)
|
||||
1500, // Lower midrange fullness (e.g., warmth in vocals, guitar body)
|
||||
2250, // Midrange clarity (e.g., vocal presence, snare crack)
|
||||
3375, // Upper midrange bite (e.g., instrument definition, vocal articulation)
|
||||
4875, // Presence and edge (e.g., guitar bite, vocal sibilance start)
|
||||
6750, // Lower brilliance (e.g., cymbal shimmer, vocal clarity)
|
||||
9375, // Brilliance and air (e.g., hi-hat crispness, breathy vocals)
|
||||
13125, // High treble sparkle (e.g., subtle overtones, synth shimmer)
|
||||
16875, // Upper treble airiness (e.g., faint harmonics, room ambiance)
|
||||
20625, // Top-end sheen (e.g., ultra-high harmonics, noise floor)
|
||||
24375 // Extreme treble limit (e.g., inaudible overtones, signal cutoff, static)
|
||||
};
|
||||
|
||||
void calculate_params(); // re calculate some parameters based on parent_rect()
|
||||
};
|
||||
|
||||
#endif /*__UI_GRAPHEQ_H__*/
|
||||
@@ -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);
|
||||
|
||||
@@ -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{
|
||||
|
||||
@@ -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() {
|
||||
|
||||
@@ -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();
|
||||
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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() {
|
||||
|
||||
@@ -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
@@ -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() {
|
||||
|
||||
@@ -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};
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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
@@ -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() {
|
||||
|
||||
@@ -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{};
|
||||
|
||||
@@ -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
@@ -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 {
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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'};
|
||||
|
||||
@@ -57,6 +57,8 @@ namespace ui {
|
||||
#define UI_POS_HEIGHT_REMAINING(linenum) ((int)(screen_height - ((linenum)*UI_POS_DEFAULT_HEIGHT)))
|
||||
// remaining px from the charnum-th character to the right of the screen
|
||||
#define UI_POS_WIDTH_REMAINING(charnum) ((int)(screen_width - ((charnum)*UI_POS_DEFAULT_WIDTH)))
|
||||
// px width of the screen
|
||||
#define UI_POS_MAXHEIGHT (screen_height)
|
||||
|
||||
// Escape sequences for colored text; second character is index into term_colors[]
|
||||
#define STR_COLOR_BLACK "\x1B\x00"
|
||||
|
||||
@@ -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.
|
||||
*
|
||||
@@ -2667,7 +2669,6 @@ bool Waveform::is_clickable() const {
|
||||
}
|
||||
|
||||
void Waveform::getAccessibilityText(std::string& result) {
|
||||
// no idea what this is in use in any places, but others have it
|
||||
result = paused_ ? "paused waveform" : "waveform";
|
||||
}
|
||||
|
||||
@@ -2689,7 +2690,6 @@ bool Waveform::on_key(const KeyEvent key) {
|
||||
}
|
||||
|
||||
bool Waveform::on_keyboard(const KeyboardEvent key) {
|
||||
// no idea what this is for, but others have it
|
||||
if (!clickable_) return false;
|
||||
|
||||
if (key == 32 || key == 10) {
|
||||
@@ -2822,6 +2822,204 @@ void Waveform::paint(Painter& painter) {
|
||||
}
|
||||
}
|
||||
|
||||
/* GraphEq *************************************************************/
|
||||
|
||||
GraphEq::GraphEq(
|
||||
Rect parent_rect,
|
||||
bool clickable)
|
||||
: Widget{parent_rect},
|
||||
clickable_{clickable},
|
||||
bar_heights(NUM_BARS, 0),
|
||||
prev_bar_heights(NUM_BARS, 0) {
|
||||
if (clickable) {
|
||||
set_focusable(true);
|
||||
// previous_data.resize(length_, 0);
|
||||
}
|
||||
}
|
||||
|
||||
void GraphEq::set_parent_rect(const Rect new_parent_rect) {
|
||||
Widget::set_parent_rect(new_parent_rect);
|
||||
calculate_params();
|
||||
}
|
||||
|
||||
void GraphEq::calculate_params() {
|
||||
y_top = screen_rect().top();
|
||||
RENDER_HEIGHT = parent_rect().height();
|
||||
BAR_WIDTH = (parent_rect().width() - (BAR_SPACING * (NUM_BARS - 1))) / NUM_BARS;
|
||||
HORIZONTAL_OFFSET = screen_rect().left();
|
||||
}
|
||||
|
||||
bool GraphEq::is_paused() const {
|
||||
return paused_;
|
||||
}
|
||||
|
||||
void GraphEq::set_paused(bool paused) {
|
||||
paused_ = paused;
|
||||
needs_background_redraw = true;
|
||||
set_dirty();
|
||||
}
|
||||
|
||||
bool GraphEq::is_clickable() const {
|
||||
return clickable_;
|
||||
}
|
||||
|
||||
void GraphEq::getAccessibilityText(std::string& result) {
|
||||
result = paused_ ? "paused GraphEq" : "GraphEq";
|
||||
}
|
||||
|
||||
void GraphEq::getWidgetName(std::string& result) {
|
||||
result = "GraphEq";
|
||||
}
|
||||
|
||||
bool GraphEq::on_key(const KeyEvent key) {
|
||||
if (!clickable_) return false;
|
||||
|
||||
if (key == KeyEvent::Select) {
|
||||
set_paused(!paused_);
|
||||
if (on_select) {
|
||||
on_select(*this);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
bool GraphEq::on_keyboard(const KeyboardEvent key) {
|
||||
if (!clickable_) return false;
|
||||
|
||||
if (key == 32 || key == 10) {
|
||||
set_paused(!paused_);
|
||||
if (on_select) {
|
||||
on_select(*this);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
bool GraphEq::on_touch(const TouchEvent event) {
|
||||
if (!clickable_) return false;
|
||||
|
||||
switch (event.type) {
|
||||
case TouchEvent::Type::Start:
|
||||
focus();
|
||||
return true;
|
||||
|
||||
case TouchEvent::Type::End:
|
||||
set_paused(!paused_);
|
||||
if (on_select) {
|
||||
on_select(*this);
|
||||
}
|
||||
return true;
|
||||
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
void GraphEq::set_theme(Color base_color_, Color peak_color_) {
|
||||
base_color = base_color_;
|
||||
peak_color = peak_color_;
|
||||
set_dirty();
|
||||
}
|
||||
|
||||
void GraphEq::update_audio_spectrum(const AudioSpectrum& spectrum) {
|
||||
const float bin_frequency_size = 48000.0f / 128;
|
||||
|
||||
for (int bar = 0; bar < NUM_BARS; bar++) {
|
||||
float start_freq = FREQUENCY_BANDS[bar];
|
||||
float end_freq = FREQUENCY_BANDS[bar + 1];
|
||||
|
||||
int start_bin = std::max(1, (int)(start_freq / bin_frequency_size));
|
||||
int end_bin = std::min(127, (int)(end_freq / bin_frequency_size));
|
||||
|
||||
if (start_bin >= end_bin) {
|
||||
end_bin = start_bin + 1;
|
||||
}
|
||||
|
||||
float total_energy = 0;
|
||||
int bin_count = 0;
|
||||
|
||||
for (int bin = start_bin; bin <= end_bin; bin++) {
|
||||
total_energy += spectrum.db[bin];
|
||||
bin_count++;
|
||||
}
|
||||
|
||||
float avg_db = bin_count > 0 ? (total_energy / bin_count) : 0;
|
||||
|
||||
// Manually boost highs for better visual balance
|
||||
float treble_boost = 1.0f;
|
||||
if (bar == 10)
|
||||
treble_boost = 1.7f;
|
||||
else if (bar >= 9)
|
||||
treble_boost = 1.3f;
|
||||
else if (bar >= 7)
|
||||
treble_boost = 1.3f;
|
||||
|
||||
// Mid emphasis for a V-shape effect
|
||||
float mid_boost = 1.0f;
|
||||
if (bar == 4 || bar == 5 || bar == 6) mid_boost = 1.2f;
|
||||
|
||||
float amplified_db = avg_db * treble_boost * mid_boost;
|
||||
|
||||
if (amplified_db > 255) amplified_db = 255;
|
||||
|
||||
float band_scale = 1.0f;
|
||||
int target_height = (amplified_db * RENDER_HEIGHT * band_scale) / 255;
|
||||
|
||||
if (target_height > RENDER_HEIGHT) {
|
||||
target_height = RENDER_HEIGHT;
|
||||
}
|
||||
|
||||
// Adjusted to look nice to my eyes
|
||||
float rise_speed = 0.8f;
|
||||
float fall_speed = 1.0f;
|
||||
|
||||
if (target_height > bar_heights[bar]) {
|
||||
bar_heights[bar] = bar_heights[bar] * (1.0f - rise_speed) + target_height * rise_speed;
|
||||
} else {
|
||||
bar_heights[bar] = bar_heights[bar] * (1.0f - fall_speed) + target_height * fall_speed;
|
||||
}
|
||||
}
|
||||
set_dirty();
|
||||
}
|
||||
|
||||
void GraphEq::paint(Painter& painter) {
|
||||
if (!visible()) return;
|
||||
if (!is_calculated) { // calc positions first
|
||||
calculate_params();
|
||||
is_calculated = true;
|
||||
}
|
||||
if (needs_background_redraw) {
|
||||
painter.fill_rectangle(screen_rect(), Theme::getInstance()->bg_darkest->background);
|
||||
needs_background_redraw = false;
|
||||
}
|
||||
if (paused_) {
|
||||
return;
|
||||
}
|
||||
const int num_segments = RENDER_HEIGHT / SEGMENT_HEIGHT;
|
||||
uint16_t bottom = screen_rect().bottom();
|
||||
for (int bar = 0; bar < NUM_BARS; bar++) {
|
||||
int x = HORIZONTAL_OFFSET + bar * (BAR_WIDTH + BAR_SPACING);
|
||||
int active_segments = (bar_heights[bar] * num_segments) / RENDER_HEIGHT;
|
||||
|
||||
if (prev_bar_heights[bar] > active_segments) {
|
||||
int clear_height = (prev_bar_heights[bar] - active_segments) * SEGMENT_HEIGHT;
|
||||
int clear_y = bottom - prev_bar_heights[bar] * SEGMENT_HEIGHT;
|
||||
painter.fill_rectangle({x, clear_y, BAR_WIDTH, clear_height}, Theme::getInstance()->bg_darkest->background);
|
||||
}
|
||||
|
||||
for (int seg = 0; seg < active_segments; seg++) {
|
||||
int y = bottom - (seg + 1) * SEGMENT_HEIGHT;
|
||||
if (y < y_top) break;
|
||||
|
||||
Color segment_color = (seg >= active_segments - 2 && seg < active_segments) ? peak_color : base_color;
|
||||
painter.fill_rectangle({x, y, BAR_WIDTH, SEGMENT_HEIGHT - 1}, segment_color);
|
||||
}
|
||||
prev_bar_heights[bar] = active_segments;
|
||||
}
|
||||
}
|
||||
|
||||
/* VuMeter **************************************************************/
|
||||
|
||||
VuMeter::VuMeter(
|
||||
|
||||
@@ -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.
|
||||
*
|
||||
@@ -1015,6 +1017,66 @@ class Waveform : public Widget {
|
||||
bool if_ever_painted_pause{false}; // for prevent the "hidden" label keeps painting and being expensive
|
||||
};
|
||||
|
||||
class GraphEq : public Widget {
|
||||
public:
|
||||
std::function<void(GraphEq&)> on_select{};
|
||||
|
||||
GraphEq(Rect parent_rect, bool clickable = false);
|
||||
GraphEq(const GraphEq&) = delete;
|
||||
GraphEq(GraphEq&&) = delete;
|
||||
GraphEq& operator=(const GraphEq&) = delete;
|
||||
GraphEq& operator=(GraphEq&&) = delete;
|
||||
|
||||
bool is_paused() const;
|
||||
void set_paused(bool paused);
|
||||
bool is_clickable() const;
|
||||
|
||||
void paint(Painter& painter) override;
|
||||
bool on_key(const KeyEvent key) override;
|
||||
bool on_touch(const TouchEvent event) override;
|
||||
bool on_keyboard(const KeyboardEvent event) override;
|
||||
void set_parent_rect(const Rect new_parent_rect) override;
|
||||
|
||||
void getAccessibilityText(std::string& result) override;
|
||||
void getWidgetName(std::string& result) override;
|
||||
void update_audio_spectrum(const AudioSpectrum& spectrum);
|
||||
void set_theme(Color base_color, Color peak_color);
|
||||
|
||||
private:
|
||||
bool is_calculated{false};
|
||||
bool paused_{false};
|
||||
bool clickable_{false};
|
||||
bool needs_background_redraw{true}; // Redraw background only when needed.
|
||||
Color base_color = Color(255, 0, 255);
|
||||
Color peak_color = Color(255, 255, 255);
|
||||
std::vector<ui::Dim> bar_heights;
|
||||
std::vector<ui::Dim> prev_bar_heights;
|
||||
|
||||
ui::Dim y_top = 2 * 16;
|
||||
ui::Dim RENDER_HEIGHT = 288;
|
||||
ui::Dim BAR_WIDTH = 20;
|
||||
ui::Dim HORIZONTAL_OFFSET = 2;
|
||||
static const int NUM_BARS = 11;
|
||||
static const int BAR_SPACING = 2;
|
||||
static const int SEGMENT_HEIGHT = 10;
|
||||
static constexpr std::array<int16_t, NUM_BARS + 1> FREQUENCY_BANDS = {
|
||||
375, // Bass warmth and low rumble (e.g., deep basslines, kick drum body)
|
||||
750, // Upper bass punch (e.g., bass guitar punch, kick drum attack)
|
||||
1500, // Lower midrange fullness (e.g., warmth in vocals, guitar body)
|
||||
2250, // Midrange clarity (e.g., vocal presence, snare crack)
|
||||
3375, // Upper midrange bite (e.g., instrument definition, vocal articulation)
|
||||
4875, // Presence and edge (e.g., guitar bite, vocal sibilance start)
|
||||
6750, // Lower brilliance (e.g., cymbal shimmer, vocal clarity)
|
||||
9375, // Brilliance and air (e.g., hi-hat crispness, breathy vocals)
|
||||
13125, // High treble sparkle (e.g., subtle overtones, synth shimmer)
|
||||
16875, // Upper treble airiness (e.g., faint harmonics, room ambiance)
|
||||
20625, // Top-end sheen (e.g., ultra-high harmonics, noise floor)
|
||||
24375 // Extreme treble limit (e.g., inaudible overtones, signal cutoff, static)
|
||||
};
|
||||
|
||||
void calculate_params(); // re calculate some parameters based on parent_rect()
|
||||
};
|
||||
|
||||
class VuMeter : public Widget {
|
||||
public:
|
||||
VuMeter(Rect parent_rect, uint32_t LEDs, bool show_max);
|
||||
|
||||
@@ -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/
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user