/* * Copyright (C) 2024 EPIRB Decoder Implementation * Copyright (C) 2026 Frederic BORRY - ADRASEC 31 * * This file is part of PortaPack. * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 2, or (at your option) * any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program; see the file COPYING. If not, write to * the Free Software Foundation, Inc., 51 Franklin Street, * Boston, MA 02110-1301, USA. */ #include "baseband_api.hpp" #include "portapack_persistent_memory.hpp" #include "file_path.hpp" #include "ui_epirb_rx.hpp" using namespace portapack; #include "rtc_time.hpp" #include "string_format.hpp" #include "ui.hpp" #include "message.hpp" #include "resources.hpp" namespace ui::external_app::epirb_rx { // URL templates #define MAPS_URL_TEMPLATE "https://www.google.com/maps/search/?api=1&query=%s%%2C%s" #define BEACON_URL_TEMPLATE "https://decoder2.herokuapp.com/decoded/" #ifndef DISABLE_COUNTRY_CACHE int CountryManager::cache_count = 0; Country CountryManager::cache[16]; #endif // ResourceManager class ResourceManager* ResourceManager::current = nullptr; void ResourceManager::destroy() { if (current != nullptr) delete current; current = nullptr; } ResourceManager* ResourceManager::getInstance() { if (current == nullptr) current = new ResourceManager(); return ResourceManager::current; } // TextArea class TextArea::TextArea( Rect parent_rect) : Widget{parent_rect} { } void TextArea::paint(Painter& painter) { const auto rect = screen_rect(); const Style& s = has_focus() ? style().invert() : style(); painter.fill_rectangle(rect, s.background); const int line_height = s.font.line_height(); int line_idx = 0; // Efficiently draw lines separated by \t without heap allocations std::string_view sv{content}; size_t start = 0; size_t end; while ((end = sv.find('\t', start)) != std::string_view::npos) { painter.draw_string(rect.location() + Point(0, line_idx * line_height), s, sv.substr(start, end - start)); start = end + 1; line_idx++; } if (start < sv.length()) { painter.draw_string(rect.location() + Point(0, line_idx * line_height), s, sv.substr(start)); } } void TextArea::set_content(std::string_view value) { content = std::string{value}; set_dirty(); } #ifdef RESET_TIMER bool TextArea::on_key(const KeyEvent key) { if (key == KeyEvent::Select && on_select) { on_select(*this); set_dirty(); return true; } return false; } #endif // EPIRBAppView class void EPIRBAppView::decode_packet(const baseband::Packet& packet, Beacon& beacon) { // Convert packet bits to byte array for easier processing uint8_t data[BEACON_DATA_SIZE]{}; size_t max_bytes = std::min(packet.size() / 8, (size_t)BEACON_DATA_SIZE); size_t packet_bit_idx = 0; for (size_t i = 0; i < max_bytes; i++) { uint8_t byte_val = 0; for (int bit = 0; bit < 8; bit++, packet_bit_idx++) { if (packet[packet_bit_idx]) { byte_val |= (1 << (7 - bit)); } } data[i] = byte_val; } beacon.setFrame(data); // Set timestamp rtc::RTC datetime; rtcGetTime(&RTCD1, &datetime); beacon.date = datetime; } #ifdef LOGGER void EPIRBLogger::on_packet(Beacon& beacon) { std::string entry; // Pre-allocate enough memory to avoid heap fragmentation and resizing // 128 bytes should be plenty for this specific log line entry.reserve(128); entry += beacon.getType(); entry += ","; entry += beacon.hexId; entry += ","; entry += beacon.getProtocolName(); /* If you ever uncomment the rest of the logging, * keep using the same pattern! Like this: * * if (!beacon.location.isUnknown()) { * entry += beacon.location.toString(Location::LocationFormat::DECIMAL); * } else { * entry += ","; * } * entry += ","; * entry += beacon.country.toString(); * entry += ","; * entry += beacon.getSatus(); * entry += "\n"; */ log_file.write_entry(beacon.date, entry); } #endif // EPIRBDetailView class EPIRBDetailView::EPIRBDetailView( Rect parent_rect, EPIRBAppView& parent) : View(parent_rect), parent_app(parent) { add_children({&text_beacon}); set_focusable(true); } #ifdef DETAIL_TAB_BEACON_SEL bool EPIRBDetailView::on_encoder(EncoderEvent delta) { // Change position in the list according to encoder int32_t size = (int32_t)parent_app.beacon_db.size(); if (size == 0) return true; // Get current index int32_t current_index = (int32_t)parent_app.beacon_db.get_current_beacon_index(); // Compute new index with wrap around int32_t new_index = (current_index + delta + size) % size; // Set new index parent_app.beacon_db.set_current_beacon(new_index % size); parent_app.on_beacon_change(); return true; } #endif // Append "CYAN