mirror of
https://github.com/portapack-mayhem/mayhem-firmware.git
synced 2026-08-22 07:29:03 +00:00
Compare commits
25 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 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
|
||||
|
||||
@@ -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 */
|
||||
|
||||
@@ -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,10 +233,14 @@ 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();
|
||||
@@ -370,6 +429,10 @@ 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();
|
||||
}
|
||||
|
||||
@@ -483,7 +546,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)
|
||||
@@ -505,6 +567,7 @@ BLERxView::BLERxView(NavigationView& nav)
|
||||
|
||||
button_clear_list.on_select = [this](Button&) {
|
||||
recent.clear();
|
||||
recent_entries_view.set_dirty();
|
||||
};
|
||||
|
||||
button_switch.on_select = [&nav](Button&) {
|
||||
@@ -546,7 +609,10 @@ 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);
|
||||
uniqueParsing = filter_index == 2 ? true : false;
|
||||
recent_entries_view.set_dirty();
|
||||
};
|
||||
|
||||
options_channel.set_selected_index(channel_index, true);
|
||||
@@ -719,10 +785,61 @@ bool BLERxView::saveFile(const std::filesystem::path& path) {
|
||||
}
|
||||
|
||||
void BLERxView::on_data(BlePacketData* packet) {
|
||||
if (!logging) {
|
||||
str_log = "";
|
||||
uint64_t macAddressEncoded = copy_mac_address_to_uint64(packet->macAddress);
|
||||
|
||||
// Start of Packet stuffing.
|
||||
// Masking off the top 2 bytes to avoid invalid keys.
|
||||
|
||||
uint64_t key = macAddressEncoded & 0xFFFFFFFFFFFF;
|
||||
bool packetExists = false;
|
||||
|
||||
// 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,49 +853,14 @@ 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) {
|
||||
@@ -841,13 +923,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) {
|
||||
@@ -923,16 +1002,18 @@ 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;
|
||||
|
||||
bool success = false;
|
||||
|
||||
int i;
|
||||
|
||||
for (i = 0; i < packet->dataLen; i++) {
|
||||
data_string += to_string_hex(packet->data[i], 2);
|
||||
}
|
||||
|
||||
entry.dbValue = packet->max_dB;
|
||||
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;
|
||||
|
||||
@@ -948,9 +1029,14 @@ void BLERxView::updateEntry(const BlePacketData* packet, BleRecentEntry& entry,
|
||||
entry.packetData.macAddress[4] = packet->macAddress[4];
|
||||
entry.packetData.macAddress[5] = packet->macAddress[5];
|
||||
|
||||
entry.numHits++;
|
||||
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++) {
|
||||
@@ -960,35 +1046,55 @@ void BLERxView::updateEntry(const BlePacketData* packet, BleRecentEntry& entry,
|
||||
entry.include_name = check_name.value();
|
||||
|
||||
// Only parse name for advertisment packets and empty name entries
|
||||
if ((pdu_type == ADV_IND || pdu_type == ADV_NONCONN_IND || pdu_type == SCAN_RSP || pdu_type == ADV_SCAN_IND) && entry.nameString.empty()) {
|
||||
ADV_PDU_PAYLOAD_TYPE_0_2_4_6* advertiseData = (ADV_PDU_PAYLOAD_TYPE_0_2_4_6*)entry.packetData.data;
|
||||
|
||||
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 (pdu_type == ADV_IND || pdu_type == ADV_NONCONN_IND || pdu_type == SCAN_RSP || pdu_type == ADV_SCAN_IND) {
|
||||
if (uniqueParsing) {
|
||||
// Add your unique beacon parsing function here.
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
return success;
|
||||
}
|
||||
|
||||
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"
|
||||
@@ -72,10 +74,17 @@ typedef enum {
|
||||
RESERVED8 = 15
|
||||
} 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 = 0xFFFFFFFFFFFF;
|
||||
|
||||
uint64_t macAddress;
|
||||
int dbValue;
|
||||
@@ -83,10 +92,12 @@ struct BleRecentEntry {
|
||||
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()
|
||||
@@ -101,10 +112,12 @@ struct BleRecentEntry {
|
||||
timestamp{},
|
||||
dataString{},
|
||||
nameString{},
|
||||
informationString{},
|
||||
include_name{},
|
||||
numHits{},
|
||||
pduType{},
|
||||
channelNumber{},
|
||||
vendor_status{MAC_VENDOR_UNKNOWN},
|
||||
entryFound{} {
|
||||
}
|
||||
|
||||
@@ -152,6 +165,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 +218,18 @@ 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);
|
||||
|
||||
NavigationView& nav_;
|
||||
|
||||
RxRadioState radio_state_{
|
||||
2402000000 /* frequency */,
|
||||
4000000 /* bandwidth */,
|
||||
@@ -216,6 +239,7 @@ class BLERxView : public View {
|
||||
uint8_t channel_index{0};
|
||||
uint8_t sort_index{0};
|
||||
uint8_t filter_index{0};
|
||||
bool uniqueParsing = false;
|
||||
std::string filter{};
|
||||
bool logging{false};
|
||||
bool serial_logging{false};
|
||||
@@ -243,7 +267,7 @@ class BLERxView : public View {
|
||||
bool auto_channel = false;
|
||||
|
||||
int16_t timer_count{0};
|
||||
int16_t timer_period{6}; // 100ms
|
||||
int16_t timer_period{2}; // 25ms
|
||||
|
||||
std::string filterBuffer{};
|
||||
std::string listFileBuffer{};
|
||||
@@ -307,9 +331,10 @@ class BLERxView : public View {
|
||||
|
||||
OptionsField options_filter{
|
||||
{18 * 8 + 2, 2 * 8},
|
||||
4,
|
||||
7,
|
||||
{{"Data", 0},
|
||||
{"MAC", 1}}};
|
||||
{"MAC", 1},
|
||||
{"Unique", 2}}};
|
||||
|
||||
Checkbox check_log{
|
||||
{10 * 8, 4 * 8 + 2},
|
||||
@@ -361,10 +386,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};
|
||||
|
||||
@@ -319,7 +319,8 @@ void BLETxView::on_tx_progress(const bool done) {
|
||||
|
||||
BLETxView::BLETxView(NavigationView& nav)
|
||||
: nav_{nav} {
|
||||
add_children({&button_open,
|
||||
add_children({&dataEditView,
|
||||
&button_open,
|
||||
&text_filename,
|
||||
&progressbar,
|
||||
&check_rand_mac,
|
||||
@@ -340,7 +341,6 @@ BLETxView::BLETxView(NavigationView& nav)
|
||||
&label_mac_address,
|
||||
&text_mac_address,
|
||||
&label_data_packet,
|
||||
&dataEditView,
|
||||
&button_clear_marked,
|
||||
&button_save_packet,
|
||||
&button_switch});
|
||||
@@ -430,6 +430,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();
|
||||
@@ -531,7 +548,6 @@ 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();
|
||||
@@ -546,6 +562,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) {
|
||||
|
||||
@@ -287,9 +287,6 @@ 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}};
|
||||
|
||||
|
||||
@@ -34,6 +34,7 @@ constexpr std::string_view mayhem_information_list[] = {
|
||||
"RedFox-Fr,nemanjan00,",
|
||||
"MichalLeonBorsuk,",
|
||||
"MatiasFernandez,Giorgiofox",
|
||||
"TommasoVentafridda",
|
||||
" ",
|
||||
"#Havoc:",
|
||||
"jboone,furrtek,eried,argilo,",
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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__ */
|
||||
+20
@@ -193,6 +193,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
|
||||
@@ -216,6 +220,18 @@ set(EXTCPPSRC
|
||||
#detector_rx
|
||||
external/detector_rx/main.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
|
||||
)
|
||||
|
||||
set(EXTAPPLIST
|
||||
@@ -265,10 +281,14 @@ set(EXTAPPLIST
|
||||
snake
|
||||
stopwatch
|
||||
breakout
|
||||
dinogame
|
||||
doom
|
||||
debug_pmem
|
||||
scanner
|
||||
level
|
||||
gfxeq
|
||||
detector_rx
|
||||
spaceinv
|
||||
blackjack
|
||||
battleship
|
||||
)
|
||||
|
||||
+27
@@ -75,6 +75,10 @@ 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
|
||||
}
|
||||
|
||||
SECTIONS
|
||||
@@ -391,6 +395,29 @@ 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
|
||||
|
||||
}
|
||||
|
||||
|
||||
+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},
|
||||
|
||||
+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__ */
|
||||
@@ -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{
|
||||
|
||||
@@ -397,13 +397,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 +404,12 @@ set(MODE_CPPSRC
|
||||
)
|
||||
DeclareTargets(PFSR fskrx)
|
||||
|
||||
### FSK TX
|
||||
|
||||
set(MODE_CPPSRC
|
||||
proc_fsk.cpp
|
||||
)
|
||||
DeclareTargets(PFSK fsktx)
|
||||
|
||||
### Microphone transmit
|
||||
|
||||
|
||||
+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};
|
||||
|
||||
+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__*/
|
||||
|
||||
+41
-15
@@ -134,6 +134,7 @@ class Message {
|
||||
NoaaAptRxConfigure = 77,
|
||||
NoaaAptRxStatusData = 78,
|
||||
NoaaAptRxImageData = 79,
|
||||
FSKPacket = 80,
|
||||
MAX
|
||||
};
|
||||
|
||||
@@ -452,6 +453,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 +475,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 +1150,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 {
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -3,6 +3,5 @@
|
||||
Licensed under [GNU GPL v3](../../../LICENSE)
|
||||
|
||||
USAGE:
|
||||
- Copy file from: https://raw.githubusercontent.com/kx1t/planefence-airlinecodes/main/airlinecodes.txt
|
||||
- Run Python 3 script: `./make_airlines_db.py`
|
||||
- Copy file to /ADSB folder on SDCARD
|
||||
- Move file "airlines.db" to /ADSB folder on SDCARD
|
||||
|
||||
@@ -325,7 +325,7 @@ AMT,ATA Airlines,,United States
|
||||
AMU,Air Macau,AIR MACAO,Macao
|
||||
AMV,AMC Airlines,,Egypt
|
||||
AMW,Air Midwest,,United States
|
||||
AMX,Aeroméxico,AEROMEXICO,Mexico
|
||||
AMX,Aeromexico,AEROMEXICO,Mexico
|
||||
AMY,Air Ambar,AIR AMBAR,Dominican Republic
|
||||
AMZ,Amiya Airline,AMIYA AIR,Nigeria
|
||||
ANA,All Nippon Airways,ALL NIPPON,Japan
|
||||
@@ -408,7 +408,7 @@ ARC,Air Routing International Corp.,,United States
|
||||
ARD,Aerocondor,,Portugal
|
||||
ARE,Aires Aerovías de Integración Regional S.A.,AIRES,Colombia
|
||||
ARF,Aero Flight,,Germany
|
||||
ARG,Aerolíneas Argentinas,ARGENTINA,Argentina
|
||||
ARG,Aerolineas Argentinas,ARGENTINA,Argentina
|
||||
ARH,Aerohelicopteros,AEROHELCA,Venezuela
|
||||
ARI,Aero Vics,AEROVICS,Mexico
|
||||
ARJ,Aerojet de Costa Rica S.A.,,Costa Rica
|
||||
@@ -820,7 +820,7 @@ BNT,Bentiu Air Transport,BENTIU AIR,Sudan
|
||||
BNV,Benane Aviation Corporation,BENANE,Mauritania
|
||||
BNW,British North West Airlines,BRITISH NORTH,United Kingdom
|
||||
BNX,LAI - Línea Aérea IAACA,AIR BARINAS,Venezuela
|
||||
BNZ,Aerolíneas Bonanza,AERO BONANZA,Mexico
|
||||
BNZ,Bonza,BONZA,Australia
|
||||
BOA,Boniair,KUMANOVO,Macedonia
|
||||
BOB,Backbone A/S,BACKBONE,Denmark
|
||||
BOC,Aerobona,AEROBONA,Mexico
|
||||
@@ -1465,6 +1465,7 @@ DET,DETA Air,SAMAL,Kazakhstan
|
||||
DEV,Red Devils Parachute Display Team,RED DEVILS,United Kingdom
|
||||
DFA,Aero Coach Aviation,AERO COACH,United States
|
||||
DFC,Aeropartner,DARK BLUE,Czech Republic
|
||||
DFL,Babcock AirAmbulance,MEDIFLIGHT,Sweden
|
||||
DFS,Dwyer Aircraft Services,DWYAIR,United States
|
||||
DGA,Yellow River Delta General Aviation,YELLOW RIVER,China
|
||||
DGO,DGO Jet,DGO JET,Mexico
|
||||
@@ -1592,8 +1593,10 @@ DVA,Discovery Airways,DISCOVERY AIRWAYS,United States
|
||||
DVB,Don Avia,DONSEBAI,Kazakhstan
|
||||
DVI,Aero Davinci International,AERO DAVINCI,Mexico
|
||||
DVN,Adventia,,Spain
|
||||
DVY,Legends Airways,DEVIL RAY,United States
|
||||
DWA,Dense Airways,,United States
|
||||
DWD,Deutsche Luftverkehrsgesellschaft (DLT),,Germany
|
||||
DWI,Arajet,DOMINICAN,Dominican Republic
|
||||
DWN,Dawn Air,DAWN AIR,United States
|
||||
DWR,Delaware Skyways,DELAWARE,United States
|
||||
DWT,Darwin Airline,DARWIN,Switzerland
|
||||
@@ -1905,6 +1908,7 @@ EZJ,Ezjet GT,GUYANA JET,Guyana
|
||||
EZS,easyJet Switzerland,TOPSWISS,Switzerland
|
||||
EZX,Eagle Express Air Charter,EAGLEXPRESS,Malaysia
|
||||
EZY,easyJet UK,EASY,United Kingdom
|
||||
EZZ,ETF Airways,ENTERPRIZE,Croatia
|
||||
FAB,First Air,FIRST AIR,Canada
|
||||
FAC,Atlantic Helicopters,FAROECOPTER,Denmark
|
||||
FAD,Rog-Air,AIR FRONTIER,Canada
|
||||
@@ -2034,7 +2038,7 @@ FJI,Fiji Airways,PACIFIC,Fiji
|
||||
FJK,Fly Jet Kz,,Kazakhstan
|
||||
FJM,Fly Jamaica Airways,GREENHEART,Jamaica
|
||||
FJO,Flexjet Operations Malta Limited,FLEX MALTA,Malta
|
||||
FRJ,Fly Jordan,SOLITAIRE AIR,Jordan
|
||||
FJR,Fly Jordan,SOLITAIRE AIR,Jordan
|
||||
FJS,Florida Jet Service,FLORIDAJET,United States
|
||||
FKA,Flying kangaroo Airline,,Australia
|
||||
FKI,FLM Aviation Mohrdieck,KIEL AIR,Germany
|
||||
@@ -2397,7 +2401,7 @@ GPL,DLR,GERMAN POLAR,Germany
|
||||
GPM,Grup Air-Med,GRUPOMED,Spain
|
||||
GPR,GPM Aeroservicio,GPM AEROSERVICIO,Mexico
|
||||
GPX,GP Aviation,,Bulgaria
|
||||
GRA,Guardian Air Asset Management,FLEX,South Africa
|
||||
GRA,Costa Rica Green Airways,,,Costa Rica
|
||||
GRB,Phoenix Air Group,GREY BIRD,United States
|
||||
GRD,National Grid plc,GRID,United Kingdom
|
||||
GRE,Air Scotland,GREECE AIRWAYS,Greece
|
||||
@@ -2459,7 +2463,7 @@ GWN,Gwyn Aviation,GWYN,United Kingdom
|
||||
GWR,Aura Airlines,LEMON,Spain
|
||||
GWS,General Airways,GENAIR,South Africa
|
||||
GWY,USA3000 Airlines,GETAWAY,United States
|
||||
GXA,Grixona,GRIXONA,Moldova
|
||||
GXA,GlobalX Airlines,GEMINI,United States
|
||||
GXG,GermanXL,,Germany
|
||||
GXL,Star XL German Airlines,STARDUST,Germany
|
||||
GXY,Galaxy Airlines,GALAX,Japan
|
||||
@@ -2608,6 +2612,7 @@ HMC,Heliamerica De Mexico,HELIAMERICA,Mexico
|
||||
HMD,Charlie Hammonds Flying Service,HAMMOND,United States
|
||||
HME,Babcock MCS,HELIMED,United Kingdom
|
||||
HMF,Norrlandsflyg,LIFEGUARD SWEDEN,Sweden
|
||||
HMJ,Harmony Jets,HARMONG JETS,Malta
|
||||
HMM,Hamra Air,HAMRA,United Arab Emirates
|
||||
HMP,Papair Terminal,PAPAIR TERMINAL,Haiti
|
||||
HMR,North American Charters,HAMMER,Canada
|
||||
@@ -2623,6 +2628,7 @@ HNR,Haiti National Airlines (HANA),HANAIR,Haiti
|
||||
HNT,Helicopteros Internacionales,HELICOP INTER,Mexico
|
||||
HNX,Hankook Airline,,South Korea
|
||||
HOA,Hola Airlines,HOLA,Spain
|
||||
HOB,HL Aircraft LLC,HOBBY JET,United States
|
||||
HOG,Mahogany Air Charters,HOGAN AIR,Zambia
|
||||
HOL,Holiday Airlines (US Airline),HOLIDAY,United States
|
||||
HOM,Aero Homex,AERO HOMEX,Mexico
|
||||
@@ -3077,7 +3083,7 @@ JSM,Jet Stream,,Moldova
|
||||
JSN,Suncor Energy Inc,JETSUN,Canada
|
||||
JSP,Palmer Aviation,PALMER,United Kingdom
|
||||
JSR,Jusur airways,,Egypt
|
||||
JST,Eastern Australia Airlines,,Australia
|
||||
JST,Jetstar Airways,,Australia
|
||||
JSV,Japan Aircraft Service,,Japan
|
||||
JSW,Jigsaw Project,,United Kingdom
|
||||
JSX,JetSuiteX Air,BIGSTRIPE,United States
|
||||
@@ -3617,6 +3623,7 @@ MAK,MAT Macedonian Airlines,MAKAVIO,Macedonia
|
||||
MAL,Morningstar Air Express,MORNINGSTAR,Canada
|
||||
MAM,Aeródromo De La Mancha,AEROMAN,Spain
|
||||
MAN,Mannion Air Charter,MANNION,United States
|
||||
MAO,Mayo Clinic,MAYO,United States
|
||||
MAQ,Mac Aviation,MAC AVIATION,Spain
|
||||
MAR,March Helicopters,MARCH,United Kingdom
|
||||
MAS,Malaysia Airlines,MALAYSIAN,Malaysia
|
||||
@@ -3633,6 +3640,7 @@ MBC,Airjet Exploracao Aerea de Carga,MABECO,Angola
|
||||
MBE,Martin-Baker,MARTIN,United Kingdom
|
||||
MBG,Zephyr Aviation,CHALGROVE,United Kingdom
|
||||
MBI,Mountain Bird,MOUNTAIN BIRD,United States
|
||||
MBK,Chrono Jet,MATBLACK,Canada
|
||||
MBL,First City Air,FIRST CITY,United Kingdom
|
||||
MBN,Zambian Airways,ZAMBIANA,Zambia
|
||||
MBO,Mobil Oil,MOBIL,Canada
|
||||
@@ -4441,6 +4449,7 @@ PDI,Paradise Island Airways,PARADISE ISLAND,United States
|
||||
PDQ,PDQ Air Charter,DISPATCH,United States
|
||||
PDR,COMAV,SPEEDSTER,Namibia
|
||||
PDT,Piedmont Airlines,PIEDMONT,United States
|
||||
PDU,Purdue University,PURDUE,United States
|
||||
PDV,Elicar,ELICAR,Italy
|
||||
PDY,Pen-Avia,PENDLEY,United Kingdom
|
||||
PEA,Pan Europeenne Air Service,,France
|
||||
@@ -4564,6 +4573,7 @@ PNH,Panh,KUBAN LIK,Russia
|
||||
PNK,Air Pink,AIRPINK,Serbia
|
||||
PNL,Aero Personal,AEROPERSONAL,Mexico
|
||||
PNM,Panorama,PANORAMA,Spain
|
||||
PNO,Panorama Aviation,PANO,Canada
|
||||
PNP,Pineapple Air,PINEAPPLE AIR,Bahamas
|
||||
PNR,PAN Air,SKYJET,Spain
|
||||
PNS,Survey Udara (Penas),PENAS,Indonesia
|
||||
@@ -4931,6 +4941,7 @@ RNB,Rosneft-Baltika,ROSBALT,Russia
|
||||
RND,Rutland Aviation,RUTLAND,United Kingdom
|
||||
RNE,Air Salone,AIR SALONE,Sierra Leone
|
||||
RNG,Orange Aircraft Leasing,ORANGE,Netherlands
|
||||
RNI,Rennia Aviation,RENNIA,United States
|
||||
RNM,Aeronem Air Cargo,AEROMNEM,Ecuador
|
||||
RNR,Air Cargo Masters,RUNNER,United States
|
||||
RNS,Ronso,RONSO,Mexico
|
||||
@@ -5216,6 +5227,7 @@ SFU,Solent Flight,SAINTS,United Kingdom
|
||||
SFW,Safi Airways,SAFI,AF
|
||||
SFX,S.K. Logistics,SWAMP FOX,United States
|
||||
SFY,Gulf Flite Center,SKY FLITE,United States
|
||||
SFZ,Pionair,SKYFORCE,Australia
|
||||
SGA,Air Saigon,AIR SAIGON,Vietnam
|
||||
SGB,Sky King Inc.,SONGBIRD,United States
|
||||
SGC,SGC Aviation,SAINT GEORGE,Austria
|
||||
@@ -5270,7 +5282,7 @@ SIE,Sierra Express,SEREX,United States
|
||||
SIH,Skynet Airlines,BLUEJET,Ireland
|
||||
SII,Aero Servicios Ejecutivos Internacionales,ASEISA,Mexico
|
||||
SIJ,Seco International,,Japan
|
||||
SIL,Servicios Aeronáuticos Integrales,SERVICIOS INTEGRALES,Mexico
|
||||
SIL,Silver Airways,SILVER WINGS,United States
|
||||
SIM,Star Air,,Sierra Leone
|
||||
SIO,Sirio,SIRIO,Italy
|
||||
SIP,Air Spirit,AIR SPIRIT,United States
|
||||
@@ -5802,6 +5814,7 @@ TKC,Tikal Jets Airlines,TIKAL,Guatemala
|
||||
TKE,Take Air Line,ISLAND BIRD,France
|
||||
TKJ,Tarkim Aviation,TARKIM AVIATION,Turkey
|
||||
TKK,FlyADVANCED,,United States
|
||||
TKM,JM Family Aviation,TACOMA,United States
|
||||
TKR,Canadian Interagency Forest Fire Centre,TANKER,Canada
|
||||
TKS,Tomsk-Avia,,Russia
|
||||
TKX,Tropical International Airways,TROPEXPRESS,Saint Kitts and Nevis
|
||||
@@ -5827,6 +5840,7 @@ TLW,Teamline Air,Teamline,Austria
|
||||
TLX,Telesis Transair,TELESIS,United States
|
||||
TLY,Top Fly,TOPFLY,Spain
|
||||
TMA,Trans Mediterranean Airlines,TANGO LIMA,Lebanon
|
||||
TMB,Volato,TOMBO,United States
|
||||
TMC,Travel Management Company,TRAIL BLAZER,United States
|
||||
TMD,Transmandu,TRANSMANDU,Venezuela
|
||||
TME,Aero Taxi del Centro de Mexico,TAXICENTRO,Mexico
|
||||
@@ -6086,6 +6100,7 @@ UDC,DonbassAero,DONBASS AERO,Ukraine
|
||||
UDN,Dniproavia,DNIEPRO,Ukraine
|
||||
UEA,United Eagle Airlines,UNITED EAGLE,China
|
||||
UED,Air LA,AIR L-A,United States
|
||||
UEE,United Eagle,EAGLE SLOVENIA,Slovenia
|
||||
UEJ,Jetcorp,,United States
|
||||
UES,Ues-Avia Aircompany,AVIASYSTEM,Ukraine
|
||||
UEU,United European Airlines,UNITED EUROPEAN,Romania
|
||||
@@ -6200,7 +6215,7 @@ VAL,Voyageur Airways,VOYAGEUR,Canada
|
||||
VAM,Ameravia,AMERAVIA,Uruguay
|
||||
VAN,Caravan Air,CAMEL,Mauritania
|
||||
VAP,Phuket Air,PHUKET AIR,Thailand
|
||||
VAR,Veca Airlines,VECA,El Salvador
|
||||
VAR,United Aviate Academy,VARNEY,United States
|
||||
VAS,ATRAN Cargo Airlines,ATRAN,Russian Federation
|
||||
VAT,Visionair,VISIONAIR,Ireland
|
||||
VAU,V Australia Airlines,,Australia
|
||||
@@ -6278,7 +6293,7 @@ VIS,Vision Air International,,Pakistan
|
||||
VIT,Aviastar Mandiri,AVIASTAR,Indonesia
|
||||
VIV,Aeroenlaces Nacionales,AEROENLACES,Mexico
|
||||
VIZ,Aerovis Airlines,AEROVIZ,Ukraine
|
||||
VJA,ValuJet Airlines,CRITTER,United States
|
||||
VJA,Vista America,ICONIC,United States
|
||||
VJC,Vietjet Air,VIETJET,Vietnam
|
||||
VJE,AvJet Routing,,United Arab Emirates
|
||||
VJM,Viajes Ejecutivos Mexicanos,VIAJES MEXICANOS,Mexico
|
||||
@@ -6320,7 +6335,7 @@ VNG,Aero Servicios Vanguardia,VANGUARDIA,Mexico
|
||||
VNK,Vipport Joint Stock Company,,Russia
|
||||
VNL,Vanilla Air,VANILLA,Japan
|
||||
VNP,Virgin Pacific,,Fiji
|
||||
VNT,Avient Air Zambia,AVIENT,Zambia
|
||||
VNT,Ventura,VENTURA,United States
|
||||
VNX,Fly Advance,VANCE,United States
|
||||
VNZ,Tbilaviamsheni,TBILAVIA,Georgia
|
||||
VOA,Viaggio Air,VIAGGIO,Bulgaria
|
||||
@@ -6331,7 +6346,7 @@ VOI,Volaris,VOLARIS,Mexico
|
||||
VOL,Blue Chip Jet,BLUE SPEED,Sweden
|
||||
VOO,Volotea,,Spain
|
||||
VOR,Flight Calibration Services Ltd.,FLIGHT CAL,United Kingdom
|
||||
VOS,Rovos Air,ROVOS,South Africa
|
||||
VOS,Volaris El Salvador,JETSAL,South Africa
|
||||
VOZ,Virgin Australia,VELOCITY,Australia
|
||||
VPA,DanubeWings,VIP TAXI,Slovakia
|
||||
VPB,Veteran Air,VETERAN,Ukraine
|
||||
@@ -6422,7 +6437,7 @@ WAU,Wizz Air Ukraine,,Ukraine
|
||||
WAV,Warbelow's Air Ventures,WARBELOW,United States
|
||||
WAW,Wings Airways,WING SHUTTLE,United States
|
||||
WAY,Airways,GARONNE,France
|
||||
WAZ,Wizz Air,WIZZ SKY,United Arab Emirates
|
||||
WAZ,Wizz Air Abu Dabi,WIZZ SKY,United Arab Emirates
|
||||
WBA,Finncomm Airlines,WESTBIRD,Finland
|
||||
WBR,Multi-Aero,WEBER,United States
|
||||
WCA,West Coast Airways,WEST-LEONE,Sierra Leone
|
||||
@@ -6499,6 +6514,8 @@ WLX,West Air Luxembourg,WEST LUX,Luxembourg
|
||||
WMA,Watermakers Air,WATERMAKERS,United States
|
||||
WML,Chantilly Air,MARLIN,United States
|
||||
WMN,Trident Aircraft,WATERMAN,United States
|
||||
WMT,Wizz Air Malta,WIZZAIR MALTA,Malta
|
||||
WMU,W.M. Uni. College of Aviation,SKYBRONCO,United States
|
||||
WNA,Winair,WINAIR,United States
|
||||
WNR,Wondair on Demand Aviation,WONDAIR,Spain
|
||||
WOA,World Airways,WORLD,United States
|
||||
@@ -6535,6 +6552,7 @@ WTP,Westpoint Air,WESTPOINT,Canada
|
||||
WTV,Western Aviators,WESTAVIA,United States
|
||||
WUE,Adolf Wurth GmbH,FASTY,Germany
|
||||
WUK,Wizz Air UK,WIZZ GO,United Kingdom
|
||||
WUP,Wheels Up,UPJET,United States
|
||||
WVA,Hand D Aviation,WABASH VALLEY,United States
|
||||
WVL,Wizz Air Bulgaria,WIZZBUL,Bulgaria
|
||||
WVZ,Winair,,HR
|
||||
@@ -6548,7 +6566,7 @@ WYC,Wycombe Air Centre,WYCOMBE,United Kingdom
|
||||
WYG,Wyoming Airlines,WYOMING,United States
|
||||
WYT,2 Sqn No 1 Elementary Flying Training School,WYTON,United Kingdom
|
||||
WZP,Zip,ZIPPER,Canada
|
||||
WZZ,Wizz Air,WIZZAIR,Hungary
|
||||
WZZ,Wizz Air Hungary,WIZZAIR,Hungary
|
||||
X9F,FRA Air,,Germany
|
||||
XAA,Aeronautical Radio Inc,,United States
|
||||
XAB,Xabre Aerolineas,AERO XABRE,Mexico
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
#!/usr/bin/env python3
|
||||
# Copyright (C) 2021 ArjanOnwezen
|
||||
# Copyright (C) 2025 Tommaso Ventafridda
|
||||
#
|
||||
# This program is free software; you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
@@ -22,26 +23,251 @@
|
||||
# as a source.
|
||||
# -------------------------------------------------------------------------------------
|
||||
import csv
|
||||
import os
|
||||
import shutil
|
||||
import unicodedata
|
||||
icao_codes=bytearray()
|
||||
airlines_countries=bytearray()
|
||||
row_count=0
|
||||
database=open("airlines.db", "wb")
|
||||
with open('airlinecodes.txt', 'rt', encoding="utf-8") as csv_file:
|
||||
sorted_lines=sorted(csv_file.readlines()[1:])
|
||||
for row in csv.reader(sorted_lines, quotechar='"', delimiter=',', quoting=csv.QUOTE_ALL, skipinitialspace=True):
|
||||
icao_code=row[0]
|
||||
# Normalize some unicode characters
|
||||
airline=unicodedata.normalize('NFKD', row[1][:32]).encode('ascii', 'ignore')
|
||||
country=unicodedata.normalize('NFKD', row[3][:32]).encode('ascii', 'ignore')
|
||||
if len(icao_code) == 3 :
|
||||
airline_padding=bytearray()
|
||||
country_padding=bytearray()
|
||||
icao_codes=icao_codes+bytearray(icao_code+'\0', encoding='ascii')
|
||||
airline_padding=bytearray('\0' * (32 - len(airline)), encoding='ascii')
|
||||
country_padding=bytearray('\0' * (32 - len(country)), encoding='ascii')
|
||||
airlines_countries=airlines_countries+bytearray(airline+airline_padding+country+country_padding)
|
||||
row_count+=1
|
||||
database.write(icao_codes+airlines_countries)
|
||||
print("Total of", row_count, "ICAO codes stored in database")
|
||||
import urllib.request
|
||||
from dataclasses import dataclass
|
||||
from typing import Dict, Set, Tuple
|
||||
|
||||
|
||||
@dataclass
|
||||
class AirlineRecord:
|
||||
"""Represents an airline record with all relevant fields"""
|
||||
|
||||
icao_code: str
|
||||
airline: str
|
||||
country: str
|
||||
|
||||
def __hash__(self):
|
||||
return hash(self.icao_code)
|
||||
|
||||
def __eq__(self, other):
|
||||
if not isinstance(other, AirlineRecord):
|
||||
return False
|
||||
return self.icao_code == other.icao_code
|
||||
|
||||
def data_equals(self, other):
|
||||
"""Check if all data fields are equal (excluding ICAO code which is the key)"""
|
||||
if not isinstance(other, AirlineRecord):
|
||||
return False
|
||||
return self.airline == other.airline and self.country == other.country
|
||||
|
||||
|
||||
def backup_previous_file() -> bool:
|
||||
"""Backup the existing airlinecodes.txt file if it exists. Returns True if backup was made."""
|
||||
if os.path.exists("airlinecodes.txt"):
|
||||
shutil.copy2("airlinecodes.txt", "airlinecodes_previous.txt")
|
||||
print("Backed up previous airline codes file")
|
||||
return True
|
||||
return False
|
||||
|
||||
|
||||
def download_airline_codes() -> None:
|
||||
"""Download the airline codes file from GitHub"""
|
||||
url = "https://raw.githubusercontent.com/kx1t/planefence-airlinecodes/main/airlinecodes.txt"
|
||||
print(f"Downloading airline codes database from {url}...")
|
||||
try:
|
||||
urllib.request.urlretrieve(url, "airlinecodes.txt")
|
||||
print("Download completed successfully.")
|
||||
except Exception as e:
|
||||
print(f"Error downloading airline codes file: {e}")
|
||||
raise
|
||||
|
||||
|
||||
def parse_airline_file(filename: str) -> Dict[str, AirlineRecord]:
|
||||
"""Parse airline codes file and return a dictionary of records"""
|
||||
records = {}
|
||||
|
||||
if not os.path.exists(filename):
|
||||
return records
|
||||
|
||||
try:
|
||||
with open(filename, "rt", encoding="utf-8") as csv_file:
|
||||
sorted_lines = sorted(csv_file.readlines()[1:])
|
||||
for row in csv.reader(
|
||||
sorted_lines,
|
||||
quotechar='"',
|
||||
delimiter=",",
|
||||
quoting=csv.QUOTE_ALL,
|
||||
skipinitialspace=True,
|
||||
):
|
||||
if len(row) >= 4: # Ensure we have enough columns
|
||||
icao_code = row[0]
|
||||
# Normalize some unicode characters
|
||||
airline = (
|
||||
unicodedata.normalize("NFKD", row[1][:32])
|
||||
.encode("ascii", "ignore")
|
||||
.decode("ascii")
|
||||
)
|
||||
country = (
|
||||
unicodedata.normalize("NFKD", row[3][:32])
|
||||
.encode("ascii", "ignore")
|
||||
.decode("ascii")
|
||||
)
|
||||
|
||||
if len(icao_code) == 3:
|
||||
records[icao_code] = AirlineRecord(
|
||||
icao_code=icao_code, airline=airline, country=country
|
||||
)
|
||||
except Exception as e:
|
||||
print(f"Warning: Could not parse airline file {filename}: {e}")
|
||||
return {}
|
||||
|
||||
return records
|
||||
|
||||
|
||||
def compare_records(
|
||||
old_records: Dict[str, AirlineRecord], new_records: Dict[str, AirlineRecord]
|
||||
) -> Tuple[Set[str], Set[str], Set[str]]:
|
||||
"""Compare old and new records, return sets of new, deleted, and changed ICAO codes"""
|
||||
old_keys = set(old_records.keys())
|
||||
new_keys = set(new_records.keys())
|
||||
|
||||
new_icao_codes = new_keys - old_keys
|
||||
deleted_icao_codes = old_keys - new_keys
|
||||
|
||||
# Check for changes in existing records
|
||||
changed_icao_codes = set()
|
||||
for icao in old_keys & new_keys:
|
||||
if not old_records[icao].data_equals(new_records[icao]):
|
||||
changed_icao_codes.add(icao)
|
||||
|
||||
return new_icao_codes, deleted_icao_codes, changed_icao_codes
|
||||
|
||||
|
||||
def write_database(records: Dict[str, AirlineRecord]) -> int:
|
||||
"""Write records to database file using original format"""
|
||||
icao_codes = bytearray()
|
||||
airlines_countries = bytearray()
|
||||
row_count = 0
|
||||
|
||||
# Sort by ICAO code for consistency
|
||||
sorted_records = sorted(records.values(), key=lambda r: r.icao_code)
|
||||
|
||||
for record in sorted_records:
|
||||
# Normalize and encode data (same as original)
|
||||
airline = unicodedata.normalize("NFKD", record.airline[:32]).encode(
|
||||
"ascii", "ignore"
|
||||
)
|
||||
country = unicodedata.normalize("NFKD", record.country[:32]).encode(
|
||||
"ascii", "ignore"
|
||||
)
|
||||
|
||||
# Add ICAO code with null terminator (original format)
|
||||
icao_codes = icao_codes + bytearray(record.icao_code + "\0", encoding="ascii")
|
||||
|
||||
# Add padded data fields (original format)
|
||||
airline_padding = bytearray("\0" * (32 - len(airline)), encoding="ascii")
|
||||
country_padding = bytearray("\0" * (32 - len(country)), encoding="ascii")
|
||||
|
||||
airlines_countries = airlines_countries + bytearray(
|
||||
airline + airline_padding + country + country_padding
|
||||
)
|
||||
row_count += 1
|
||||
|
||||
with open("airlines.db", "wb") as database:
|
||||
database.write(icao_codes + airlines_countries)
|
||||
|
||||
return row_count
|
||||
|
||||
|
||||
def create_database() -> None:
|
||||
"""Create the airline database from the downloaded file"""
|
||||
# Backup existing file before downloading new one
|
||||
has_previous = backup_previous_file()
|
||||
|
||||
# Download new file
|
||||
download_airline_codes()
|
||||
|
||||
# Parse both files for comparison
|
||||
old_records = {}
|
||||
if has_previous:
|
||||
print("Parsing previous airline codes file...")
|
||||
old_records = parse_airline_file("airlinecodes_previous.txt")
|
||||
print(f"Found {len(old_records)} records in previous file")
|
||||
|
||||
print("Parsing new airline codes file...")
|
||||
new_records = parse_airline_file("airlinecodes.txt")
|
||||
print(f"Found {len(new_records)} records in new file")
|
||||
|
||||
# Compare records if we have a previous version
|
||||
if old_records:
|
||||
new_icao_codes, deleted_icao_codes, changed_icao_codes = compare_records(
|
||||
old_records, new_records
|
||||
)
|
||||
|
||||
# Print change statistics
|
||||
print("\n" + "=" * 50)
|
||||
print("AIRLINE CODES CHANGE SUMMARY")
|
||||
print("=" * 50)
|
||||
print(f"New records: {len(new_icao_codes):>8}")
|
||||
print(f"Deleted records: {len(deleted_icao_codes):>8}")
|
||||
print(f"Changed records: {len(changed_icao_codes):>8}")
|
||||
print(f"Total records: {len(new_records):>8}")
|
||||
print("=" * 50)
|
||||
|
||||
# Show examples of changes (limited to avoid spam)
|
||||
if new_icao_codes and len(new_icao_codes) <= 15:
|
||||
print(f"\nNew ICAO codes: {', '.join(sorted(new_icao_codes))}")
|
||||
elif new_icao_codes:
|
||||
sample_new = sorted(list(new_icao_codes))[:10]
|
||||
print(
|
||||
f"\nSample new ICAO codes: {', '.join(sample_new)} (and {len(new_icao_codes)-10} more)"
|
||||
)
|
||||
|
||||
if deleted_icao_codes and len(deleted_icao_codes) <= 15:
|
||||
print(f"Deleted ICAO codes: {', '.join(sorted(deleted_icao_codes))}")
|
||||
elif deleted_icao_codes:
|
||||
sample_deleted = sorted(list(deleted_icao_codes))[:10]
|
||||
print(
|
||||
f"Sample deleted ICAO codes: {', '.join(sample_deleted)} (and {len(deleted_icao_codes)-10} more)"
|
||||
)
|
||||
|
||||
if changed_icao_codes and len(changed_icao_codes) <= 15:
|
||||
print(f"Changed ICAO codes: {', '.join(sorted(changed_icao_codes))}")
|
||||
elif changed_icao_codes:
|
||||
sample_changed = sorted(list(changed_icao_codes))[:10]
|
||||
print(
|
||||
f"Sample changed ICAO codes: {', '.join(sample_changed)} (and {len(changed_icao_codes)-10} more)"
|
||||
)
|
||||
|
||||
# Show some specific examples of changes
|
||||
if changed_icao_codes:
|
||||
print(f"\nExample changes:")
|
||||
for icao in sorted(list(changed_icao_codes))[:3]:
|
||||
old_rec = old_records[icao]
|
||||
new_rec = new_records[icao]
|
||||
if old_rec.airline != new_rec.airline:
|
||||
print(
|
||||
f" {icao}: Airline '{old_rec.airline}' → '{new_rec.airline}'"
|
||||
)
|
||||
if old_rec.country != new_rec.country:
|
||||
print(
|
||||
f" {icao}: Country '{old_rec.country}' → '{new_rec.country}'"
|
||||
)
|
||||
else:
|
||||
print(
|
||||
"\nNo previous airline codes file found - this appears to be the first run"
|
||||
)
|
||||
|
||||
# Create database from new records
|
||||
print("\nCreating airline database...")
|
||||
row_count = write_database(new_records)
|
||||
print("Total of", row_count, "ICAO codes stored in database")
|
||||
|
||||
|
||||
def cleanup_temp() -> None:
|
||||
"""Cleanup temporary files created during the process"""
|
||||
if os.path.exists("airlinecodes_previous.txt"):
|
||||
os.remove("airlinecodes_previous.txt")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
try:
|
||||
create_database()
|
||||
cleanup_temp()
|
||||
except Exception as e:
|
||||
print(f"Error creating airline database: {e}")
|
||||
else:
|
||||
print("Airline database created successfully.")
|
||||
|
||||
@@ -3,6 +3,5 @@
|
||||
Licensed under [GNU GPL v3](../../../LICENSE)
|
||||
|
||||
USAGE:
|
||||
- Copy file from: https://opensky-network.org/datasets/metadata/aircraftDatabase.csv
|
||||
- Run Python 3 script: `./make_icao24_db.py`
|
||||
- Copy file to /ADSB folder on SDCARD
|
||||
- Move "icao24.db" file to /ADSB folder on SDCARD
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,6 +1,7 @@
|
||||
#!/usr/bin/env python3
|
||||
|
||||
# Copyright (C) 2021 ArjanOnwezen
|
||||
# Copyright (C) 2025 Tommaso Ventafridda
|
||||
#
|
||||
# This program is free software; you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
@@ -20,43 +21,275 @@
|
||||
|
||||
# -------------------------------------------------------------------------------------
|
||||
# Create icao24.db, used for ADS-B receiver application, using
|
||||
# https://opensky-network.org/datasets/metadata/aircraftDatabase.csv
|
||||
# https://opensky-network.org/datasets/metadata/aircraftDatabase.csv
|
||||
# as a source.
|
||||
# -------------------------------------------------------------------------------------
|
||||
import csv
|
||||
import unicodedata
|
||||
icao24_codes=bytearray()
|
||||
data=bytearray()
|
||||
row_count=0
|
||||
import os
|
||||
import shutil
|
||||
import urllib.request
|
||||
from dataclasses import dataclass
|
||||
from typing import Dict, Set, Tuple
|
||||
|
||||
database=open("icao24.db", "wb")
|
||||
|
||||
with open('aircraftDatabase.csv', 'rt') as csv_file:
|
||||
sorted_lines=sorted(csv_file.readlines()[1:])
|
||||
for row in csv.reader(sorted_lines, quotechar='"', delimiter=',', quoting=csv.QUOTE_ALL, skipinitialspace=True):
|
||||
# only store in case enough info is available
|
||||
if len(row) == 27 and len(row[0]) == 6 and len(row[1]) > 0:
|
||||
icao24_code=row[0][:6].upper()
|
||||
registration=row[1][:8].encode('ascii', 'ignore')
|
||||
manufacturer=row[3][:32].encode('ascii', 'ignore')
|
||||
model=row[4][:32].encode('ascii', 'ignore')
|
||||
# in case icao aircraft type isn't, use ac type like BALL for balloon
|
||||
if len(row[8]) == 3:
|
||||
actype=row[8][:3].encode('ascii', 'ignore')
|
||||
else:
|
||||
actype=row[5][:4].encode('ascii', 'ignore')
|
||||
owner=row[13][:32].encode('ascii', 'ignore')
|
||||
operator=row[9][:32].encode('ascii', 'ignore')
|
||||
#padding
|
||||
icao24_codes.extend(bytearray(icao24_code+'\0', encoding='ascii'))
|
||||
registration_padding=bytearray('\0' * (9 - len(registration)), encoding='ascii')
|
||||
manufacturer_padding=bytearray('\0' * (33 - len(manufacturer)), encoding='ascii')
|
||||
model_padding=bytearray('\0' * (33 - len(model)), encoding='ascii')
|
||||
actype_padding=bytearray('\0' * (5 - len(actype)), encoding='ascii')
|
||||
owner_padding=bytearray('\0' * (33 - len(owner)), encoding='ascii')
|
||||
operator_padding=bytearray('\0' * (33 - len(operator)), encoding='ascii')
|
||||
data.extend(bytearray(registration+registration_padding+manufacturer+manufacturer_padding+model+model_padding+actype+actype_padding+owner+owner_padding+operator+operator_padding))
|
||||
row_count+=1
|
||||
database.write(icao24_codes+data)
|
||||
print("Total of", row_count, "ICAO codes stored in database")
|
||||
@dataclass
|
||||
class AircraftRecord:
|
||||
"""Represents an aircraft record with all relevant fields"""
|
||||
|
||||
icao24: str
|
||||
registration: str
|
||||
manufacturer: str
|
||||
model: str
|
||||
actype: str
|
||||
owner: str
|
||||
operator: str
|
||||
|
||||
def __hash__(self):
|
||||
return hash(self.icao24)
|
||||
|
||||
def __eq__(self, other):
|
||||
if not isinstance(other, AircraftRecord):
|
||||
return False
|
||||
return self.icao24 == other.icao24
|
||||
|
||||
def data_equals(self, other):
|
||||
"""Check if all data fields are equal (excluding ICAO24 which is the key)"""
|
||||
if not isinstance(other, AircraftRecord):
|
||||
return False
|
||||
return (
|
||||
self.registration == other.registration
|
||||
and self.manufacturer == other.manufacturer
|
||||
and self.model == other.model
|
||||
and self.actype == other.actype
|
||||
and self.owner == other.owner
|
||||
and self.operator == other.operator
|
||||
)
|
||||
|
||||
|
||||
def backup_previous_csv() -> bool:
|
||||
"""Backup the existing CSV file if it exists. Returns True if backup was made."""
|
||||
if os.path.exists("aircraftDatabase.csv"):
|
||||
shutil.copy2("aircraftDatabase.csv", "aircraftDatabase_previous.csv")
|
||||
print("Backed up previous CSV file")
|
||||
return True
|
||||
return False
|
||||
|
||||
|
||||
def download_aircraft_database() -> None:
|
||||
"""Download the aircraft database file from OpenSky Network"""
|
||||
url = "https://opensky-network.org/datasets/metadata/aircraftDatabase.csv"
|
||||
print(f"Downloading aircraft database from {url}...")
|
||||
try:
|
||||
urllib.request.urlretrieve(url, "aircraftDatabase.csv")
|
||||
print("Download completed successfully.")
|
||||
except Exception as e:
|
||||
print(f"Error downloading aircraft database file: {e}")
|
||||
raise
|
||||
|
||||
|
||||
def parse_csv_file(filename: str) -> Dict[str, AircraftRecord]:
|
||||
"""Parse CSV file and return a dictionary of records"""
|
||||
records = {}
|
||||
|
||||
if not os.path.exists(filename):
|
||||
return records
|
||||
|
||||
try:
|
||||
with open(filename, "rt", encoding="utf-8") as csv_file:
|
||||
sorted_lines = sorted(csv_file.readlines()[1:])
|
||||
for row in csv.reader(
|
||||
sorted_lines,
|
||||
quotechar='"',
|
||||
delimiter=",",
|
||||
quoting=csv.QUOTE_ALL,
|
||||
skipinitialspace=True,
|
||||
):
|
||||
# only store in case enough info is available
|
||||
if len(row) == 27 and len(row[0]) == 6 and len(row[1]) > 0:
|
||||
icao24_code = row[0][:6].upper()
|
||||
registration = row[1][:8]
|
||||
manufacturer = row[3][:32]
|
||||
model = row[4][:32]
|
||||
# in case icao aircraft type isn't, use ac type like BALL for balloon
|
||||
if len(row[8]) == 3:
|
||||
actype = row[8][:3]
|
||||
else:
|
||||
actype = row[5][:4]
|
||||
owner = row[13][:32]
|
||||
operator = row[9][:32]
|
||||
|
||||
records[icao24_code] = AircraftRecord(
|
||||
icao24=icao24_code,
|
||||
registration=registration,
|
||||
manufacturer=manufacturer,
|
||||
model=model,
|
||||
actype=actype,
|
||||
owner=owner,
|
||||
operator=operator,
|
||||
)
|
||||
except Exception as e:
|
||||
print(f"Warning: Could not parse CSV file {filename}: {e}")
|
||||
return {}
|
||||
|
||||
return records
|
||||
|
||||
|
||||
def compare_records(
|
||||
old_records: Dict[str, AircraftRecord], new_records: Dict[str, AircraftRecord]
|
||||
) -> Tuple[Set[str], Set[str], Set[str]]:
|
||||
"""Compare old and new records, return sets of new, deleted, and changed ICAO codes"""
|
||||
old_keys = set(old_records.keys())
|
||||
new_keys = set(new_records.keys())
|
||||
|
||||
new_icao_codes = new_keys - old_keys
|
||||
deleted_icao_codes = old_keys - new_keys
|
||||
|
||||
# Check for changes in existing records
|
||||
changed_icao_codes = set()
|
||||
for icao in old_keys & new_keys:
|
||||
if not old_records[icao].data_equals(new_records[icao]):
|
||||
changed_icao_codes.add(icao)
|
||||
|
||||
return new_icao_codes, deleted_icao_codes, changed_icao_codes
|
||||
|
||||
|
||||
def write_database(records: Dict[str, AircraftRecord]) -> int:
|
||||
"""Write records to database file using original format"""
|
||||
# bytearrays to store icao24 codes and data
|
||||
icao24_codes = bytearray()
|
||||
data = bytearray()
|
||||
row_count = 0
|
||||
|
||||
# Sort by ICAO code for consistency
|
||||
sorted_records = sorted(records.values(), key=lambda r: r.icao24)
|
||||
|
||||
for record in sorted_records:
|
||||
# Encode data fields
|
||||
registration = record.registration.encode("ascii", "ignore")
|
||||
manufacturer = record.manufacturer.encode("ascii", "ignore")
|
||||
model = record.model.encode("ascii", "ignore")
|
||||
actype = record.actype.encode("ascii", "ignore")
|
||||
owner = record.owner.encode("ascii", "ignore")
|
||||
operator = record.operator.encode("ascii", "ignore")
|
||||
|
||||
# Add ICAO code with null terminator (original format)
|
||||
icao24_codes.extend(bytearray(record.icao24 + "\0", encoding="ascii"))
|
||||
|
||||
# Add padded data fields (original format)
|
||||
registration_padding = bytearray(
|
||||
"\0" * (9 - len(registration)), encoding="ascii"
|
||||
)
|
||||
manufacturer_padding = bytearray(
|
||||
"\0" * (33 - len(manufacturer)), encoding="ascii"
|
||||
)
|
||||
model_padding = bytearray("\0" * (33 - len(model)), encoding="ascii")
|
||||
actype_padding = bytearray("\0" * (5 - len(actype)), encoding="ascii")
|
||||
owner_padding = bytearray("\0" * (33 - len(owner)), encoding="ascii")
|
||||
operator_padding = bytearray("\0" * (33 - len(operator)), encoding="ascii")
|
||||
|
||||
data.extend(
|
||||
bytearray(
|
||||
registration
|
||||
+ registration_padding
|
||||
+ manufacturer
|
||||
+ manufacturer_padding
|
||||
+ model
|
||||
+ model_padding
|
||||
+ actype
|
||||
+ actype_padding
|
||||
+ owner
|
||||
+ owner_padding
|
||||
+ operator
|
||||
+ operator_padding
|
||||
)
|
||||
)
|
||||
row_count += 1
|
||||
|
||||
with open("icao24.db", "wb") as database:
|
||||
database.write(icao24_codes + data)
|
||||
|
||||
return row_count
|
||||
|
||||
|
||||
def create_database() -> None:
|
||||
"""Create the ICAO24 database from the downloaded file"""
|
||||
# Backup existing CSV before downloading new one
|
||||
has_previous = backup_previous_csv()
|
||||
|
||||
# Download new CSV
|
||||
download_aircraft_database()
|
||||
|
||||
# Parse both CSV files for comparison
|
||||
old_records = {}
|
||||
if has_previous:
|
||||
print("Parsing previous CSV file...")
|
||||
old_records = parse_csv_file("aircraftDatabase_previous.csv")
|
||||
print(f"Found {len(old_records)} records in previous CSV")
|
||||
|
||||
print("Parsing new CSV file...")
|
||||
new_records = parse_csv_file("aircraftDatabase.csv")
|
||||
print(f"Found {len(new_records)} records in new CSV")
|
||||
|
||||
# Compare records if we have a previous version
|
||||
if old_records:
|
||||
new_icao_codes, deleted_icao_codes, changed_icao_codes = compare_records(
|
||||
old_records, new_records
|
||||
)
|
||||
|
||||
# Print change statistics
|
||||
print("\n" + "=" * 50)
|
||||
print("CSV CHANGE SUMMARY")
|
||||
print("=" * 50)
|
||||
print(f"New records: {len(new_icao_codes):>8}")
|
||||
print(f"Deleted records: {len(deleted_icao_codes):>8}")
|
||||
print(f"Changed records: {len(changed_icao_codes):>8}")
|
||||
print(f"Total records: {len(new_records):>8}")
|
||||
print("=" * 50)
|
||||
|
||||
# Show examples of changes (limited to avoid spam)
|
||||
if new_icao_codes and len(new_icao_codes) <= 10:
|
||||
print(f"\nNew ICAO24 codes: {', '.join(sorted(new_icao_codes))}")
|
||||
elif new_icao_codes:
|
||||
sample_new = sorted(list(new_icao_codes))[:5]
|
||||
print(
|
||||
f"\nSample new ICAO24 codes: {', '.join(sample_new)} (and {len(new_icao_codes)-5} more)"
|
||||
)
|
||||
|
||||
if deleted_icao_codes and len(deleted_icao_codes) <= 10:
|
||||
print(f"Deleted ICAO24 codes: {', '.join(sorted(deleted_icao_codes))}")
|
||||
elif deleted_icao_codes:
|
||||
sample_deleted = sorted(list(deleted_icao_codes))[:5]
|
||||
print(
|
||||
f"Sample deleted ICAO24 codes: {', '.join(sample_deleted)} (and {len(deleted_icao_codes)-5} more)"
|
||||
)
|
||||
|
||||
if changed_icao_codes and len(changed_icao_codes) <= 10:
|
||||
print(f"Changed ICAO24 codes: {', '.join(sorted(changed_icao_codes))}")
|
||||
elif changed_icao_codes:
|
||||
sample_changed = sorted(list(changed_icao_codes))[:5]
|
||||
print(
|
||||
f"Sample changed ICAO24 codes: {', '.join(sample_changed)} (and {len(changed_icao_codes)-5} more)"
|
||||
)
|
||||
else:
|
||||
print("\nNo previous CSV found - this appears to be the first run")
|
||||
|
||||
# Create database from new records
|
||||
print("\nCreating ICAO24 database...")
|
||||
row_count = write_database(new_records)
|
||||
print("Total of", row_count, "ICAO codes stored in database")
|
||||
|
||||
|
||||
def cleanup_previous_csv() -> None:
|
||||
"""Remove the previous CSV file if it exists"""
|
||||
if os.path.exists("aircraftDatabase_previous.csv"):
|
||||
os.remove("aircraftDatabase_previous.csv")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
try:
|
||||
create_database()
|
||||
cleanup_previous_csv()
|
||||
except Exception as e:
|
||||
print(f"Error creating ICAO24 database: {e}")
|
||||
else:
|
||||
print("ICAO24 database created successfully.")
|
||||
|
||||
@@ -0,0 +1,130 @@
|
||||
#!/usr/bin/env python3
|
||||
# Copyright (C) 2025 Tommaso Ventafridda
|
||||
#
|
||||
# This program is free software; you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
# 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.
|
||||
#
|
||||
# -------------------------------------------------------------------------------------
|
||||
# Create macaddress.db, used for BLE receiver application, using
|
||||
# https://standards-oui.ieee.org/oui/oui.txt as a source.
|
||||
# -------------------------------------------------------------------------------------
|
||||
|
||||
import urllib.request
|
||||
import unicodedata
|
||||
import re
|
||||
from typing import List, Tuple
|
||||
|
||||
|
||||
def download_oui_file() -> str:
|
||||
"""Download the OUI file from IEEE"""
|
||||
url = "https://standards-oui.ieee.org/oui/oui.txt"
|
||||
print(f"Downloading OUI database from {url}...")
|
||||
try:
|
||||
with urllib.request.urlopen(url) as response:
|
||||
content = response.read().decode("utf-8")
|
||||
print("Download completed successfully.")
|
||||
return content
|
||||
except Exception as e:
|
||||
print(f"Error downloading OUI file: {e}")
|
||||
raise
|
||||
|
||||
|
||||
def parse_oui_data(content: str) -> List[Tuple[str, str]]:
|
||||
"""Parse the OUI file content and extract MAC prefixes and vendor names"""
|
||||
entries = []
|
||||
lines = content.split("\n")
|
||||
|
||||
for _, line in enumerate(lines):
|
||||
# Look for lines with (hex) pattern
|
||||
if "(hex)" in line:
|
||||
# Extract MAC prefix and vendor name
|
||||
match = re.match(
|
||||
r"^([0-9A-F]{2}-[0-9A-F]{2}-[0-9A-F]{2})\s+\(hex\)\s+(.+)$",
|
||||
line.strip(),
|
||||
)
|
||||
if match:
|
||||
# Remove dashes: "28-6F-B9" -> "286FB9"
|
||||
mac_prefix = match.group(1).replace(
|
||||
"-", ""
|
||||
)
|
||||
vendor_name = match.group(2).strip()
|
||||
|
||||
# Normalize vendor name and limit to 63 characters
|
||||
vendor_name = (
|
||||
unicodedata.normalize("NFKD", vendor_name[:63])
|
||||
.encode("ascii", "ignore")
|
||||
.decode("ascii")
|
||||
)
|
||||
|
||||
if mac_prefix and vendor_name:
|
||||
entries.append((mac_prefix, vendor_name))
|
||||
|
||||
print(f"Parsed {len(entries)} MAC address entries.")
|
||||
return entries
|
||||
|
||||
|
||||
def create_database(
|
||||
entries: List[Tuple[str, str]], output_filename: str = "macaddress.db"
|
||||
):
|
||||
"""Create the binary database file."""
|
||||
# Sort entries by MAC prefix for binary search
|
||||
entries.sort(key=lambda x: x[0])
|
||||
|
||||
mac_prefixes = bytearray()
|
||||
vendor_data = bytearray()
|
||||
row_count = 0
|
||||
|
||||
for mac_prefix, vendor_name in entries:
|
||||
# MAC prefix: 6 hex chars + null terminator = 7 bytes
|
||||
mac_prefixes += bytearray(mac_prefix + "\0", encoding="ascii")
|
||||
|
||||
# Vendor name: pad to 64 bytes
|
||||
vendor_bytes = vendor_name.encode("ascii", "ignore")
|
||||
vendor_padding = bytearray("\0" * (64 - len(vendor_bytes)), encoding="ascii")
|
||||
vendor_data += vendor_bytes + vendor_padding
|
||||
|
||||
row_count += 1
|
||||
|
||||
# Write database: index section followed by data section
|
||||
with open(output_filename, "wb") as database:
|
||||
database.write(mac_prefixes + vendor_data)
|
||||
|
||||
print(f"Created database '{output_filename}' with {row_count} MAC address entries.")
|
||||
print(f"Index section: {len(mac_prefixes)} bytes")
|
||||
print(f"Data section: {len(vendor_data)} bytes")
|
||||
print(f"Total database size: {len(mac_prefixes) + len(vendor_data)} bytes")
|
||||
|
||||
|
||||
def main():
|
||||
"""Main function to create the MAC address database."""
|
||||
try:
|
||||
oui_content = download_oui_file()
|
||||
entries = parse_oui_data(oui_content)
|
||||
|
||||
if not entries:
|
||||
print("No valid entries found in OUI file!")
|
||||
return
|
||||
|
||||
create_database(entries)
|
||||
|
||||
print("MAC address database creation completed successfully!")
|
||||
|
||||
except Exception as e:
|
||||
print(f"Error creating MAC address database: {e}")
|
||||
return 1
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
exit(main())
|
||||
+1
-1
Submodule hackrf updated: 65bf67f9b5...651d0a7f23
@@ -0,0 +1,6 @@
|
||||
Upgraded standard case<br>https://www.printables.com/model/1337429-upgraded-hackrf-portapack-h4h4m-case
|
||||
|
||||
|
||||
Upgraded 18650 version case<br>https://www.printables.com/model/1337451-18650-power-hackrf-portapack-h4m-case
|
||||
|
||||
Official case, made for injection molding. Can be 3D printed, but has very tight tolerances.<br>https://www.printables.com/model/1019145-official-hackrf-portapack-h4h4m-case/comments
|
||||
Binary file not shown.
Binary file not shown.
@@ -1,5 +1,4 @@
|
||||
# Drone Analog FPV Channels
|
||||
# https://oscarliang.com/fpv-channels/
|
||||
# 5.8GHz Analog FPV Channels
|
||||
f=5865000000,d=FPV Ch A-1
|
||||
f=5845000000,d=FPV Ch A-2
|
||||
f=5825000000,d=FPV Ch A-3
|
||||
@@ -40,30 +39,6 @@ f=5806000000,d=FPV Ch R-5
|
||||
f=5843000000,d=FPV Ch R-6
|
||||
f=5880000000,d=FPV Ch R-7
|
||||
f=5917000000,d=FPV Ch R-8
|
||||
f=5362000000,d=FPV Ch D-1
|
||||
f=5399000000,d=FPV Ch D-2
|
||||
f=5436000000,d=FPV Ch D-3
|
||||
f=5473000000,d=FPV Ch D-4
|
||||
f=5510000000,d=FPV Ch D-5
|
||||
f=5547000000,d=FPV Ch D-6
|
||||
f=5584000000,d=FPV Ch D-7
|
||||
f=5621000000,d=FPV Ch D-8
|
||||
f=5325000000,d=FPV Ch U-1
|
||||
f=5348000000,d=FPV Ch U-2
|
||||
f=5366000000,d=FPV Ch U-3
|
||||
f=5384000000,d=FPV Ch U-4
|
||||
f=5402000000,d=FPV Ch U-5
|
||||
f=5420000000,d=FPV Ch U-6
|
||||
f=5438000000,d=FPV Ch U-7
|
||||
f=5456000000,d=FPV Ch U-8
|
||||
f=5474000000,d=FPV Ch O-1
|
||||
f=5492000000,d=FPV Ch O-2
|
||||
f=5510000000,d=FPV Ch O-3
|
||||
f=5528000000,d=FPV Ch O-4
|
||||
f=5546000000,d=FPV Ch O-5
|
||||
f=5564000000,d=FPV Ch O-6
|
||||
f=5582000000,d=FPV Ch O-7
|
||||
f=5600000000,d=FPV Ch O-8
|
||||
f=5333000000,d=FPV Ch L-1
|
||||
f=5373000000,d=FPV Ch L-2
|
||||
f=5413000000,d=FPV Ch L-3
|
||||
@@ -72,11 +47,14 @@ f=5493000000,d=FPV Ch L-5
|
||||
f=5533000000,d=FPV Ch L-6
|
||||
f=5573000000,d=FPV Ch L-7
|
||||
f=5613000000,d=FPV Ch L-8
|
||||
f=5653000000,d=FPV Ch H-1
|
||||
f=5693000000,d=FPV Ch H-2
|
||||
f=5733000000,d=FPV Ch H-3
|
||||
f=5773000000,d=FPV Ch H-4
|
||||
f=5813000000,d=FPV Ch H-5
|
||||
f=5853000000,d=FPV Ch H-6
|
||||
f=5893000000,d=FPV Ch H-7
|
||||
f=5933000000,d=FPV Ch H-8
|
||||
|
||||
# 1.2-1.3GHz Analog Channels
|
||||
f=1080000000,d=FPV Ch 1
|
||||
f=1120000000,d=FPV Ch 2
|
||||
f=1160000000,d=FPV Ch 3
|
||||
f=1200000000,d=FPV Ch 4
|
||||
f=1240000000,d=FPV Ch 5
|
||||
f=1258000000,d=FPV Ch 6
|
||||
f=1280000000,d=FPV Ch 7
|
||||
f=1320000000,d=FPV Ch 8
|
||||
f=1360000000,d=FPV Ch 9
|
||||
|
||||
@@ -14,7 +14,7 @@
|
||||
87,108,FM BROADCAST USA
|
||||
76,108,FM BROADCAST BRAZIL
|
||||
76,95,FM BROADCAST JAPAN
|
||||
65,74,FM BROADCAST RUSSIA
|
||||
65,74,FM OIRT EAST EU
|
||||
# CITIZENS BAND RADIO
|
||||
26,28,CB RADIO
|
||||
# COMMON PUBLIC SERVICE BANDS
|
||||
|
||||
Binary file not shown.
Reference in New Issue
Block a user