diff --git a/.gitignore b/.gitignore index 559d5257f..dae8329db 100644 --- a/.gitignore +++ b/.gitignore @@ -62,6 +62,7 @@ cmake-build-debug/ .vscode .idea *.swp +.claude # VSCodium extensions .history diff --git a/firmware/application/external/epirb_rx/main.cpp b/firmware/application/external/epirb_rx/main.cpp new file mode 100644 index 000000000..1da7b6f94 --- /dev/null +++ b/firmware/application/external/epirb_rx/main.cpp @@ -0,0 +1,83 @@ +/* + * Copyright (C) 2024 EPIRB Decoder Implementation + * + * This file is part of PortaPack. + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2, or (at your option) + * any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; see the file COPYING. If not, write to + * the Free Software Foundation, Inc., 51 Franklin Street, + * Boston, MA 02110-1301, USA. + */ + +#include "ui.hpp" +#include "ui_epirb_rx.hpp" +#include "ui_navigation.hpp" +#include "external_app.hpp" + +namespace ui::external_app::epirb_rx { +void initialize_app(ui::NavigationView& nav) { + nav.push(); +} +} // namespace ui::external_app::epirb_rx + +extern "C" { + +__attribute__((section(".external_app.app_epirb_rx.application_information"), used)) application_information_t _application_information_epirb_rx = { + /*.memory_location = */ (uint8_t*)0x00000000, + /*.externalAppEntry = */ ui::external_app::epirb_rx::initialize_app, + /*.header_version = */ CURRENT_HEADER_VERSION, + /*.app_version = */ VERSION_MD5, + + /*.app_name = */ "EPIRB RX", + /*.bitmap_data = */ { + 0x00, + 0x00, + 0x00, + 0x00, + 0x7C, + 0x3E, + 0xFE, + 0x7F, + 0xFF, + 0xFF, + 0xF7, + 0xEF, + 0xE3, + 0xC7, + 0xE3, + 0xC7, + 0xE3, + 0xC7, + 0xE3, + 0xC7, + 0xF7, + 0xEF, + 0xFF, + 0xFF, + 0xFE, + 0x7F, + 0x7C, + 0x3E, + 0x00, + 0x00, + 0x00, + 0x00, + }, + /*.icon_color = */ ui::Color::red().v, + /*.menu_location = */ app_location_t::RX, + /*.desired_menu_position = */ -1, + + /*.m4_app_tag = portapack::spi_flash::image_tag_epirb_rx */ {'P', 'E', 'P', 'I'}, + /*.m4_app_offset = */ 0x00000000, // will be filled at compile time +}; +} \ No newline at end of file diff --git a/firmware/application/external/epirb_rx/ui_epirb_rx.cpp b/firmware/application/external/epirb_rx/ui_epirb_rx.cpp new file mode 100644 index 000000000..41d45e0d3 --- /dev/null +++ b/firmware/application/external/epirb_rx/ui_epirb_rx.cpp @@ -0,0 +1,515 @@ +/* + * Copyright (C) 2024 EPIRB Decoder Implementation + * + * This file is part of PortaPack. + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2, or (at your option) + * any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; see the file COPYING. If not, write to + * the Free Software Foundation, Inc., 51 Franklin Street, + * Boston, MA 02110-1301, USA. + */ + +#include "baseband_api.hpp" +#include "portapack_persistent_memory.hpp" +#include "file_path.hpp" + +#include "ui_epirb_rx.hpp" + +using namespace portapack; + +#include "rtc_time.hpp" +#include "string_format.hpp" + +#include "message.hpp" + +namespace ui::external_app::epirb_rx { + +EPIRBBeacon EPIRBDecoder::decode_packet(const baseband::Packet& packet) { + EPIRBBeacon beacon; + + if (packet.size() < 112) { + return beacon; // Invalid packet + } + + // Convert packet bits to byte array for easier processing + std::array data{}; + for (size_t i = 0; i < std::min(packet.size() / 8, data.size()); i++) { + uint8_t byte_val = 0; + for (int bit = 0; bit < 8 && (i * 8 + bit) < packet.size(); bit++) { + if (packet[i * 8 + bit]) { + byte_val |= (1 << (7 - bit)); + } + } + data[i] = byte_val; + } + + // Extract beacon ID (bits 26-85, 15 hex digits) + beacon.beacon_id = 0; + for (int i = 3; i < 11; i++) { + beacon.beacon_id = (beacon.beacon_id << 8) | data[i]; + } + + // Extract beacon type (bits 86-88) + uint8_t type_bits = (data[10] >> 5) & 0x07; + beacon.beacon_type = decode_beacon_type(type_bits); + + // Extract emergency type (bits 91-94 for some beacon types) + uint8_t emergency_bits = (data[11] >> 4) & 0x0F; + beacon.emergency_type = decode_emergency_type(emergency_bits); + + // Extract location if encoded (depends on beacon type and protocol) + beacon.location = decode_location(data); + + // Extract country code (bits 1-10) + beacon.country_code = decode_country_code(data); + + // Set timestamp + rtc::RTC datetime; + rtcGetTime(&RTCD1, &datetime); + beacon.timestamp = datetime; + + return beacon; +} + +EPIRBLocation EPIRBDecoder::decode_location(const std::array& data) { + // EPIRB location encoding varies by protocol version + // This is a simplified decoder for the most common format + + // Check for location data presence (bit patterns vary) + if ((data[12] & 0x80) == 0) { + return EPIRBLocation(); // No location data + } + + // Extract latitude (simplified - actual encoding is more complex) + int32_t lat_raw = ((data[12] & 0x7F) << 10) | (data[13] << 2) | ((data[14] >> 6) & 0x03); + if (lat_raw & 0x10000) lat_raw |= 0xFFFE0000; // Sign extend + float latitude = lat_raw * (180.0f / 131072.0f); + + // Extract longitude (simplified - actual encoding is more complex) + int32_t lon_raw = ((data[14] & 0x3F) << 12) | (data[15] << 4) | ((data[0] >> 4) & 0x0F); + if (lon_raw & 0x20000) lon_raw |= 0xFFFC0000; // Sign extend + float longitude = lon_raw * (360.0f / 262144.0f); + + // Validate coordinates + if (latitude < -90.0f || latitude > 90.0f || longitude < -180.0f || longitude > 180.0f) { + return EPIRBLocation(); // Invalid coordinates + } + + return EPIRBLocation(latitude, longitude); +} + +BeaconType EPIRBDecoder::decode_beacon_type(uint8_t type_bits) { + switch (type_bits) { + case 0: + return BeaconType::OrbitingLocationBeacon; + case 1: + return BeaconType::PersonalLocatorBeacon; + case 2: + return BeaconType::EmergencyLocatorTransmitter; + case 3: + return BeaconType::SerialELT; + case 4: + return BeaconType::NationalELT; + default: + return BeaconType::Other; + } +} + +EmergencyType EPIRBDecoder::decode_emergency_type(uint8_t emergency_bits) { + switch (emergency_bits) { + case 0: + return EmergencyType::Fire; + case 1: + return EmergencyType::Flooding; + case 2: + return EmergencyType::Collision; + case 3: + return EmergencyType::Grounding; + case 4: + return EmergencyType::Sinking; + case 5: + return EmergencyType::Disabled; + case 6: + return EmergencyType::Abandoning; + case 7: + return EmergencyType::Piracy; + case 8: + return EmergencyType::Man_Overboard; + default: + return EmergencyType::Other; + } +} + +uint32_t EPIRBDecoder::decode_country_code(const std::array& data) { + // Country code is in bits 1-10 (ITU country code) + return ((data[0] & 0x03) << 8) | data[1]; +} + +std::string EPIRBDecoder::decode_vessel_name(const std::array& /* data */) { + // Vessel name extraction depends on beacon type and protocol + // This is a placeholder - actual implementation would be more complex + return ""; +} + +void EPIRBLogger::on_packet(const EPIRBBeacon& beacon) { + std::string entry = "EPIRB," + + to_string_dec_uint(beacon.beacon_id, 15, '0') + "," + + to_string_dec_uint(static_cast(beacon.beacon_type)) + "," + + to_string_dec_uint(static_cast(beacon.emergency_type)) + ","; + + if (beacon.location.valid) { + entry += to_string_decimal(beacon.location.latitude, 6) + "," + + to_string_decimal(beacon.location.longitude, 6); + } else { + entry += ","; + } + + entry += "," + to_string_dec_uint(beacon.country_code) + "\n"; + + log_file.write_entry(beacon.timestamp, entry); +} + +std::string format_beacon_type(BeaconType type) { + switch (type) { + case BeaconType::OrbitingLocationBeacon: + return "OLB"; + case BeaconType::PersonalLocatorBeacon: + return "PLB"; + case BeaconType::EmergencyLocatorTransmitter: + return "ELT"; + case BeaconType::SerialELT: + return "S-ELT"; + case BeaconType::NationalELT: + return "N-ELT"; + default: + return "Other"; + } +} + +std::string format_emergency_type(EmergencyType type) { + switch (type) { + case EmergencyType::Fire: + return "Fire"; + case EmergencyType::Flooding: + return "Flooding"; + case EmergencyType::Collision: + return "Collision"; + case EmergencyType::Grounding: + return "Grounding"; + case EmergencyType::Sinking: + return "Sinking"; + case EmergencyType::Disabled: + return "Disabled"; + case EmergencyType::Abandoning: + return "Abandoning"; + case EmergencyType::Piracy: + return "Piracy"; + case EmergencyType::Man_Overboard: + return "MOB"; + default: + return "Other"; + } +} + +EPIRBBeaconDetailView::EPIRBBeaconDetailView(ui::NavigationView& nav) { + add_children({&button_done, + &button_see_map}); + + button_done.on_select = [this](Button&) { + if (on_close) on_close(); + }; + + button_see_map.on_select = [this, &nav](Button&) { + if (beacon_.location.valid) { + nav.push( + to_string_hex(beacon_.beacon_id, 8), // tag as string + 0, // altitude + GeoPos::alt_unit::METERS, + GeoPos::spd_unit::NONE, + beacon_.location.latitude, + beacon_.location.longitude, + 0, // angle + [this]() { + if (on_close) on_close(); + }); + } + }; +} + +void EPIRBBeaconDetailView::set_beacon(const EPIRBBeacon& beacon) { + beacon_ = beacon; + set_dirty(); +} + +void EPIRBBeaconDetailView::focus() { + button_see_map.focus(); +} + +void EPIRBBeaconDetailView::paint(ui::Painter& painter) { + View::paint(painter); + + const auto rect = screen_rect(); + const auto s = style(); + + auto draw_cursor = rect.location(); + draw_cursor += {8, 8}; + + draw_cursor = draw_field(painter, {draw_cursor, {200, 16}}, s, + "Beacon ID", to_string_hex(beacon_.beacon_id, 15)) + .location(); + + draw_cursor = draw_field(painter, {draw_cursor, {200, 16}}, s, + "Type", format_beacon_type(beacon_.beacon_type)) + .location(); + + draw_cursor = draw_field(painter, {draw_cursor, {200, 16}}, s, + "Emergency", format_emergency_type(beacon_.emergency_type)) + .location(); + + if (beacon_.location.valid) { + draw_cursor = draw_field(painter, {draw_cursor, {200, 16}}, s, + "Latitude", to_string_decimal(beacon_.location.latitude, 6) + "°") + .location(); + + draw_cursor = draw_field(painter, {draw_cursor, {200, 16}}, s, + "Longitude", to_string_decimal(beacon_.location.longitude, 6) + "°") + .location(); + } else { + draw_cursor = draw_field(painter, {draw_cursor, {200, 16}}, s, + "Location", "Unknown") + .location(); + } + + draw_cursor = draw_field(painter, {draw_cursor, {200, 16}}, s, + "Country", to_string_dec_uint(beacon_.country_code)) + .location(); + + draw_cursor = draw_field(painter, {draw_cursor, {200, 16}}, s, + "Time", to_string_datetime(beacon_.timestamp, HMS)) + .location(); +} + +ui::Rect EPIRBBeaconDetailView::draw_field( + ui::Painter& painter, + const ui::Rect& draw_rect, + const ui::Style& style, + const std::string& label, + const std::string& value) { + const auto label_width = 8 * 8; + + painter.draw_string({draw_rect.location()}, style, label + ":"); + painter.draw_string({draw_rect.location() + ui::Point{label_width, 0}}, style, value); + + return {draw_rect.location() + ui::Point{0, draw_rect.height()}, draw_rect.size()}; +} + +EPIRBAppView::EPIRBAppView(ui::NavigationView& nav) + : nav_(nav) { + baseband::run_prepared_image(portapack::memory::map::m4_code.base()); + + add_children({&label_frequency, + &field_rf_amp, + &field_lna, + &field_vga, + &rssi, + &field_volume, + &channel, + &label_status, + &label_beacons_count, + &label_latest, + &text_latest_info, + &console, + &button_map, + &button_clear, + &button_log}); + + button_map.on_select = [this](Button&) { + this->on_show_map(); + }; + + button_clear.on_select = [this](Button&) { + this->on_clear_beacons(); + }; + + button_log.on_select = [this](Button&) { + this->on_toggle_log(); + }; + + signal_token_tick_second = rtc_time::signal_tick_second += [this]() { + this->on_tick_second(); + }; + + // Configure receiver for 406.028 MHz EPIRB frequency + receiver_model.set_target_frequency(406028000); + receiver_model.set_rf_amp(true); + receiver_model.set_lna(32); + receiver_model.set_vga(32); + receiver_model.set_sampling_rate(2457600); + receiver_model.enable(); + + logger = std::make_unique(); + if (logger) { + logger->append(logs_dir / "epirb_rx.txt"); + } +} + +EPIRBAppView::~EPIRBAppView() { + rtc_time::signal_tick_second -= signal_token_tick_second; + + receiver_model.disable(); + baseband::shutdown(); +} + +void EPIRBAppView::set_parent_rect(const ui::Rect new_parent_rect) { + View::set_parent_rect(new_parent_rect); + + const auto console_rect = ui::Rect{ + new_parent_rect.left(), + new_parent_rect.top() + header_height, + new_parent_rect.width(), + new_parent_rect.height() - header_height - 32}; + console.set_parent_rect(console_rect); +} + +void EPIRBAppView::paint(ui::Painter& /* painter */) { + // Custom painting if needed +} + +void EPIRBAppView::focus() { + field_rf_amp.focus(); +} + +void EPIRBAppView::on_packet(const baseband::Packet& packet) { + // Decode the EPIRB packet + auto beacon = EPIRBDecoder::decode_packet(packet); + + if (beacon.beacon_id != 0) { // Valid beacon decoded + on_beacon_decoded(beacon); + } +} + +void EPIRBAppView::on_beacon_decoded(const EPIRBBeacon& beacon) { + beacons_received++; + recent_beacons.push_back(beacon); + + // Keep only last 50 beacons + if (recent_beacons.size() > 50) { + recent_beacons.erase(recent_beacons.begin()); + } + + // Update display + update_display(); + + // Log the beacon + if (logger) { + logger->on_packet(beacon); + } + + // Display in console with full details + std::string beacon_info = format_beacon_summary(beacon); + if (beacon.emergency_type != EmergencyType::Other) { + beacon_info += " [" + format_emergency_type(beacon.emergency_type) + "]"; + } + console.write(beacon_info + "\n"); +} + +void EPIRBAppView::on_show_map() { + if (!recent_beacons.empty()) { + // Find latest beacon with valid location + for (auto it = recent_beacons.rbegin(); it != recent_beacons.rend(); ++it) { + if (it->location.valid) { + // Create a GeoMapView with all beacon locations + auto map_view = nav_.push( + "EPIRB", // tag + 0, // altitude + ui::GeoPos::alt_unit::METERS, + ui::GeoPos::spd_unit::NONE, + it->location.latitude, + it->location.longitude, + 0 // angle + ); + + // Add all beacons with valid locations as markers + for (const auto& beacon : recent_beacons) { + if (beacon.location.valid) { + ui::GeoMarker marker; + marker.lat = beacon.location.latitude; + marker.lon = beacon.location.longitude; + marker.angle = 0; + marker.tag = to_string_hex(beacon.beacon_id, 8) + " " + + format_beacon_type(beacon.beacon_type); + map_view->store_marker(marker); + } + } + return; + } + } + } + + // No valid location found + nav_.display_modal("No Location", "No beacons with valid\nlocation data found."); +} + +void EPIRBAppView::on_clear_beacons() { + recent_beacons.clear(); + beacons_received = 0; + console.clear(true); + update_display(); +} + +void EPIRBAppView::on_toggle_log() { + // Toggle logging functionality + if (logger) { + logger.reset(); + button_log.set_text("Log"); + } else { + logger = std::make_unique(); + logger->append("epirb_rx.txt"); + button_log.set_text("Stop"); + } +} + +void EPIRBAppView::on_tick_second() { + // Update status display every second + rtc::RTC datetime; + rtcGetTime(&RTCD1, &datetime); + + label_status.set("Listening... " + to_string_datetime(datetime, HM)); +} + +void EPIRBAppView::update_display() { + label_beacons_count.set("Beacons: " + to_string_dec_uint(beacons_received)); + + if (!recent_beacons.empty()) { + const auto& latest = recent_beacons.back(); + text_latest_info.set(format_beacon_summary(latest)); + } +} + +std::string EPIRBAppView::format_beacon_summary(const EPIRBBeacon& beacon) { + std::string summary = to_string_hex(beacon.beacon_id, 8) + " " + + format_beacon_type(beacon.beacon_type); + + if (beacon.location.valid) { + summary += " " + format_location(beacon.location); + } + + return summary; +} + +std::string EPIRBAppView::format_location(const EPIRBLocation& location) { + return to_string_decimal(location.latitude, 4) + "°," + + to_string_decimal(location.longitude, 4) + "°"; +} + +} // namespace ui::external_app::epirb_rx \ No newline at end of file diff --git a/firmware/application/external/epirb_rx/ui_epirb_rx.hpp b/firmware/application/external/epirb_rx/ui_epirb_rx.hpp new file mode 100644 index 000000000..5bb8f2a10 --- /dev/null +++ b/firmware/application/external/epirb_rx/ui_epirb_rx.hpp @@ -0,0 +1,263 @@ +/* + * Copyright (C) 2024 EPIRB Decoder Implementation + * + * This file is part of PortaPack. + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2, or (at your option) + * any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; see the file COPYING. If not, write to + * the Free Software Foundation, Inc., 51 Franklin Street, + * Boston, MA 02110-1301, USA. + */ + +#ifndef __UI_EPIRB_RX_H__ +#define __UI_EPIRB_RX_H__ + +#include "app_settings.hpp" +#include "radio_state.hpp" +#include "ui_widget.hpp" +#include "ui_navigation.hpp" +#include "ui_receiver.hpp" +#include "ui_geomap.hpp" + +#include "event_m0.hpp" +#include "signal.hpp" +#include "message.hpp" +#include "log_file.hpp" + +#include "baseband_packet.hpp" + +/* #include +#include +#include +#include */ + +namespace ui::external_app::epirb_rx { + +// EPIRB 406 MHz beacon types +enum class BeaconType : uint8_t { + OrbitingLocationBeacon = 0, + PersonalLocatorBeacon = 1, + EmergencyLocatorTransmitter = 2, + SerialELT = 3, + NationalELT = 4, + Other = 15 +}; + +// EPIRB distress and emergency types +enum class EmergencyType : uint8_t { + Fire = 0, + Flooding = 1, + Collision = 2, + Grounding = 3, + Sinking = 4, + Disabled = 5, + Abandoning = 6, + Piracy = 7, + Man_Overboard = 8, + Other = 15 +}; + +struct EPIRBLocation { + float latitude; // degrees, -90 to +90 + float longitude; // degrees, -180 to +180 + bool valid; + + EPIRBLocation() + : latitude(0.0f), longitude(0.0f), valid(false) {} + EPIRBLocation(float lat, float lon) + : latitude(lat), longitude(lon), valid(true) {} +}; + +struct EPIRBBeacon { + uint32_t beacon_id; + BeaconType beacon_type; + EmergencyType emergency_type; + EPIRBLocation location; + uint32_t country_code; + std::string vessel_name; + rtc::RTC timestamp; + uint32_t sequence_number; + + EPIRBBeacon() + : beacon_id(0), beacon_type(BeaconType::Other), emergency_type(EmergencyType::Other), location(), country_code(0), vessel_name(), timestamp(), sequence_number(0) {} +}; + +class EPIRBDecoder { + public: + static EPIRBBeacon decode_packet(const baseband::Packet& packet); + + private: + static EPIRBLocation decode_location(const std::array& data); + static BeaconType decode_beacon_type(uint8_t type_bits); + static EmergencyType decode_emergency_type(uint8_t emergency_bits); + static uint32_t decode_country_code(const std::array& data); + static std::string decode_vessel_name(const std::array& data); +}; + +class EPIRBLogger { + public: + Optional append(const std::filesystem::path& filename) { + return log_file.append(filename); + } + + void on_packet(const EPIRBBeacon& beacon); + + private: + LogFile log_file{}; +}; + +// Forward declarations of formatting functions +std::string format_beacon_type(BeaconType type); +std::string format_emergency_type(EmergencyType type); + +class EPIRBBeaconDetailView : public ui::View { + public: + std::function on_close{}; + + EPIRBBeaconDetailView(ui::NavigationView& nav); + EPIRBBeaconDetailView(const EPIRBBeaconDetailView&) = delete; + EPIRBBeaconDetailView& operator=(const EPIRBBeaconDetailView&) = delete; + + void set_beacon(const EPIRBBeacon& beacon); + const EPIRBBeacon& beacon() const { return beacon_; } + + void focus() override; + void paint(ui::Painter&) override; + + ui::GeoMapView* get_geomap_view() { return geomap_view; } + + private: + EPIRBBeacon beacon_{}; + + ui::Button button_done{ + {125, 224, 96, 24}, + "Done"}; + ui::Button button_see_map{ + {19, 224, 96, 24}, + "See on map"}; + + ui::GeoMapView* geomap_view{nullptr}; + + ui::Rect draw_field( + ui::Painter& painter, + const ui::Rect& draw_rect, + const ui::Style& style, + const std::string& label, + const std::string& value); +}; + +class EPIRBAppView : public ui::View { + public: + EPIRBAppView(ui::NavigationView& nav); + ~EPIRBAppView(); + + void set_parent_rect(const ui::Rect new_parent_rect) override; + void paint(ui::Painter&) override; + void focus() override; + + std::string title() const override { return "EPIRB RX"; } + + private: + app_settings::SettingsManager settings_{ + "rx_epirb", app_settings::Mode::RX}; + + ui::NavigationView& nav_; + + std::vector recent_beacons{}; + std::unique_ptr logger{}; + + EPIRBBeaconDetailView beacon_detail_view{nav_}; + + static constexpr auto header_height = 3 * 16; + + ui::Text label_frequency{ + {0 * 8, 0 * 16, 10 * 8, 1 * 16}, + "406.028 MHz"}; + + ui::RFAmpField field_rf_amp{ + {13 * 8, 0 * 16}}; + + ui::LNAGainField field_lna{ + {15 * 8, 0 * 16}}; + + ui::VGAGainField field_vga{ + {18 * 8, 0 * 16}}; + + ui::RSSI rssi{ + {21 * 8, 0, 6 * 8, 4}}; + + ui::AudioVolumeField field_volume{ + {screen_width - 2 * 8, 0 * 16}}; + + ui::Channel channel{ + {21 * 8, 5, 6 * 8, 4}}; + + // Status display + ui::Text label_status{ + {0 * 8, 1 * 16, 15 * 8, 1 * 16}, + "Listening..."}; + + ui::Text label_beacons_count{ + {16 * 8, 1 * 16, 14 * 8, 1 * 16}, + "Beacons: 0"}; + + // Latest beacon info display + ui::Text label_latest{ + {0 * 8, 2 * 16, 8 * 8, 1 * 16}, + "Latest:"}; + + ui::Text text_latest_info{ + {8 * 8, 2 * 16, 22 * 8, 1 * 16}, + ""}; + + // Beacon list + ui::Console console{ + {0, 3 * 16, 240, 168}}; + + ui::Button button_map{ + {0, 224, 60, 24}, + "Map"}; + + ui::Button button_clear{ + {64, 224, 60, 24}, + "Clear"}; + + ui::Button button_log{ + {128, 224, 60, 24}, + "Log"}; + + SignalToken signal_token_tick_second{}; + uint32_t beacons_received = 0; + + MessageHandlerRegistration message_handler_packet{ + Message::ID::EPIRBPacket, + [this](Message* const p) { + const auto message = static_cast(p); + this->on_packet(message->packet); + }}; + + void on_packet(const baseband::Packet& packet); + void on_beacon_decoded(const EPIRBBeacon& beacon); + void on_show_map(); + void on_clear_beacons(); + void on_toggle_log(); + void on_tick_second(); + + void update_display(); + std::string format_beacon_summary(const EPIRBBeacon& beacon); + std::string format_location(const EPIRBLocation& location); +}; + +} // namespace ui::external_app::epirb_rx + +#endif // __UI_EPIRB_RX_H__ \ No newline at end of file diff --git a/firmware/application/external/external.cmake b/firmware/application/external/external.cmake index 9f321ce68..e821138ae 100644 --- a/firmware/application/external/external.cmake +++ b/firmware/application/external/external.cmake @@ -110,12 +110,13 @@ set(EXTCPPSRC #wefax_rx external/wefax_rx/main.cpp external/wefax_rx/ui_wefax_rx.cpp - + + #noaaapt_rx external/noaaapt_rx/main.cpp external/noaaapt_rx/ui_noaaapt_rx.cpp - - + + #shoppingcart_lock external/shoppingcart_lock/main.cpp @@ -215,15 +216,15 @@ set(EXTCPPSRC #gfxEQ external/gfxeq/main.cpp - external/gfxeq/ui_gfxeq.cpp + external/gfxeq/ui_gfxeq.cpp #detector_rx external/detector_rx/main.cpp - external/detector_rx/ui_detector_rx.cpp + external/detector_rx/ui_detector_rx.cpp #space_invaders external/spaceinv/main.cpp - external/spaceinv/ui_spaceinv.cpp + external/spaceinv/ui_spaceinv.cpp #blackjack external/blackjack/main.cpp @@ -231,11 +232,15 @@ set(EXTCPPSRC #battleship external/battleship/main.cpp - external/battleship/ui_battleship.cpp + external/battleship/ui_battleship.cpp #ert external/ert/main.cpp - external/ert/ert_app.cpp + external/ert/ert_app.cpp + + #epirb_rx + external/epirb_rx/main.cpp + external/epirb_rx/ui_epirb_rx.cpp ) set(EXTAPPLIST @@ -264,7 +269,7 @@ set(EXTAPPLIST morse_tx sstvtx random_password - #acars_rx + acars_rx ookbrute ook_editor wefax_rx @@ -296,4 +301,5 @@ set(EXTAPPLIST blackjack battleship ert + epirb_rx ) diff --git a/firmware/application/external/external.ld b/firmware/application/external/external.ld index a04cea6b3..5aa64124e 100644 --- a/firmware/application/external/external.ld +++ b/firmware/application/external/external.ld @@ -80,6 +80,7 @@ MEMORY ram_external_app_blackjack (rwx) : org = 0xADE70000, len = 32k ram_external_app_battleship (rwx) : org = 0xADE80000, len = 32k ram_external_app_ert (rwx) : org = 0xADE90000, len = 32k + ram_external_app_epirb_rx (rwx) : org = 0xADEA0000, len = 32k } SECTIONS @@ -341,7 +342,7 @@ SECTIONS KEEP(*(.external_app.app_stopwatch.application_information)); *(*ui*external_app*stopwatch*); } > ram_external_app_stopwatch - + .external_app_wefax_rx : ALIGN(4) SUBALIGN(4) { KEEP(*(.external_app.app_wefax_rx.application_information)); @@ -364,19 +365,19 @@ SECTIONS { KEEP(*(.external_app.app_doom.application_information)); *(*ui*external_app*doom*); - } > ram_external_app_doom - + } > ram_external_app_doom + .external_app_debug_pmem : ALIGN(4) SUBALIGN(4) { KEEP(*(.external_app.app_debug_pmem.application_information)); *(*ui*external_app*debug_pmem*); - } > ram_external_app_debug_pmem + } > ram_external_app_debug_pmem .external_app_scanner : ALIGN(4) SUBALIGN(4) { KEEP(*(.external_app.app_scanner.application_information)); *(*ui*external_app*scanner*); - } > ram_external_app_scanner + } > ram_external_app_scanner .external_app_level : ALIGN(4) SUBALIGN(4) { @@ -425,5 +426,12 @@ SECTIONS KEEP(*(.external_app.app_ert.application_information)); *(*ui*external_app*ert*); } > ram_external_app_ert + + .external_app_epirb_rx : ALIGN(4) SUBALIGN(4) + { + KEEP(*(.external_app.app_epirb_rx.application_information)); + *(*ui*external_app*epirb_rx*); + } > ram_external_app_epirb_rx + } diff --git a/firmware/application/ui_navigation.cpp b/firmware/application/ui_navigation.cpp index 3b856975a..f40db57ea 100644 --- a/firmware/application/ui_navigation.cpp +++ b/firmware/application/ui_navigation.cpp @@ -762,7 +762,7 @@ void add_apps(NavigationView& nav, BtnGridView& grid, app_location_t loc) { for (auto& app : NavigationView::appList) { if (app.menuLocation == loc) { grid.add_item({app.displayName, app.iconColor, app.icon, - [&nav, &app]() { + [&nav, &app]() { i2cdev::I2CDevManager::set_autoscan_interval(0); //if i navigate away from any menu, turn off autoscan nav.push_view(std::unique_ptr(app.viewFactory->produce(nav))); }}, true); @@ -789,8 +789,8 @@ void add_external_items(NavigationView& nav, app_location_t location, BtnGridVie error_tile_pos); } else { std::sort(externalItems.begin(), externalItems.end(), [](const auto &a, const auto &b) - { - return a.desired_position < b.desired_position; + { + return a.desired_position < b.desired_position; }); for (auto const& gridItem : externalItems) { @@ -799,7 +799,7 @@ void add_external_items(NavigationView& nav, app_location_t location, BtnGridVie } else { grid.insert_item(gridItem, gridItem.desired_position, true); } - + } grid.update_items(); diff --git a/firmware/baseband/CMakeLists.txt b/firmware/baseband/CMakeLists.txt index d995932b6..750a8f8f7 100644 --- a/firmware/baseband/CMakeLists.txt +++ b/firmware/baseband/CMakeLists.txt @@ -279,7 +279,7 @@ macro(DeclareTargets chunk_tag name) include_directories(. ${INCDIR} ${MODE_INCDIR}) link_directories(${LLIBDIR}) target_link_libraries(${PROJECT_NAME}.elf ${LIBS}) - + target_link_libraries(${PROJECT_NAME}.elf -Wl,-Map=${PROJECT_NAME}.map) target_link_libraries(${PROJECT_NAME}.elf -Wl,--print-memory-usage) @@ -578,6 +578,14 @@ set(MODE_CPPSRC ) DeclareTargets(PAFR afskrx) +### EPIRB + +set(MODE_CPPSRC + proc_epirb.cpp +) +DeclareTargets(PEPI epirb_rx) + + ### NRF RX set(MODE_CPPSRC diff --git a/firmware/baseband/proc_epirb.cpp b/firmware/baseband/proc_epirb.cpp new file mode 100644 index 000000000..4166130cf --- /dev/null +++ b/firmware/baseband/proc_epirb.cpp @@ -0,0 +1,95 @@ +/* + * Copyright (C) 2024 EPIRB Receiver Implementation + * + * This file is part of PortaPack. + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2, or (at your option) + * any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; see the file COPYING. If not, write to + * the Free Software Foundation, Inc., 51 Franklin Street, + * Boston, MA 02110-1301, USA. + */ + +#include "proc_epirb.hpp" + +#include "portapack_shared_memory.hpp" + +#include "dsp_fir_taps.hpp" + +#include "event_m4.hpp" +#include + +EPIRBProcessor::EPIRBProcessor() { + // Configure the decimation filters for narrowband EPIRB signal + // Target: Reduce 2.457600 MHz to ~38.4 kHz for 400 bps processing + decim_0.configure(taps_11k0_decim_0.taps); + decim_1.configure(taps_11k0_decim_1.taps); + baseband_thread.start(); +} + +void EPIRBProcessor::execute(const buffer_c8_t& buffer) { + /* 2.4576MHz, 2048 samples */ + + // First decimation stage: 2.4576 MHz -> 307.2 kHz + const auto decim_0_out = decim_0.execute(buffer, dst_buffer); + + // Second decimation stage: 307.2 kHz -> 38.4 kHz + const auto decim_1_out = decim_1.execute(decim_0_out, dst_buffer); + const auto decimator_out = decim_1_out; + + /* 38.4kHz, 32 samples (approximately) */ + feed_channel_stats(decimator_out); + + // Process each decimated sample through the matched filter + for (size_t i = 0; i < decimator_out.count; i++) { + // Apply matched filter for BPSK demodulation + if (mf.execute_once(decimator_out.p[i])) { + // Feed symbol to clock recovery when matched filter triggers + clock_recovery(mf.get_output()); + } + } +} + +void EPIRBProcessor::consume_symbol(const float raw_symbol) { + // BPSK demodulation: positive = 1, negative = 0 + const uint_fast8_t sliced_symbol = (raw_symbol >= 0.0f) ? 1 : 0; + + // Decode bi-phase L encoding manually + // In bi-phase L: 0 = no transition, 1 = transition + // This is a simple edge detector + const auto decoded_symbol = sliced_symbol ^ last_symbol; + last_symbol = sliced_symbol; + + // Build packet from decoded symbols + packet_builder.execute(decoded_symbol); +} + +void EPIRBProcessor::payload_handler(const baseband::Packet& packet) { + // EPIRB packet received - validate and process + if (packet.size() >= 112) { // Minimum EPIRB data payload size (112 bits) + packets_received++; + last_packet_timestamp = Timestamp::now(); + + // Create and send EPIRB packet message to application layer + const EPIRBPacketMessage message{packet}; + shared_memory.application_queue.push(message); + } +} + +void EPIRBProcessor::on_message(const Message* const message) { +} + +int main() { + EventDispatcher event_dispatcher{std::make_unique()}; + event_dispatcher.run(); + return 0; +} \ No newline at end of file diff --git a/firmware/baseband/proc_epirb.hpp b/firmware/baseband/proc_epirb.hpp new file mode 100644 index 000000000..e9d288149 --- /dev/null +++ b/firmware/baseband/proc_epirb.hpp @@ -0,0 +1,135 @@ +/* + * Copyright (C) 2024 EPIRB Receiver Implementation + * + * This file is part of PortaPack. + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2, or (at your option) + * any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; see the file COPYING. If not, write to + * the Free Software Foundation, Inc., 51 Franklin Street, + * Boston, MA 02110-1301, USA. + */ + +#ifndef __PROC_EPIRB_H__ +#define __PROC_EPIRB_H__ + +#include +#include +#include +#include + +#include "baseband_processor.hpp" +#include "baseband_thread.hpp" +#include "rssi_thread.hpp" +#include "channel_decimator.hpp" +#include "matched_filter.hpp" +#include "clock_recovery.hpp" +#include "symbol_coding.hpp" +#include "packet_builder.hpp" +#include "baseband_packet.hpp" +#include "message.hpp" +#include "buffer.hpp" + +// Forward declarations for types only used as pointers/references +class Message; +namespace baseband { +class Packet; +} + +// EPIRB 406 MHz Emergency Position Indicating Radio Beacon +// Signal characteristics: +// - Frequency: 406.025 - 406.028 MHz (typically 406.028 MHz) +// - Modulation: BPSK (Binary Phase Shift Keying) +// - Data rate: 400 bps +// - Encoding: Bi-phase L (Manchester) +// - Transmission: Every 50 seconds ± 2.5 seconds +// - Power: 5W ± 2dB +// - Message length: 144 bits (including sync pattern) + +// Matched filter for BPSK demodulation at 400 bps +// Using raised cosine filter taps optimized for 400 bps BPSK +static constexpr std::array, 64> bpsk_taps = {{// Raised cosine filter coefficients for BPSK 400 bps + -5, -8, -12, -15, -17, -17, -15, -11, + -5, 2, 11, 20, 29, 37, 43, 47, + 48, 46, 42, 35, 26, 16, 4, -8, + -21, -33, -44, -53, -59, -62, -62, -58, + -51, -41, -28, -13, 3, 19, 36, 51, + 64, 74, 80, 82, 80, 74, 64, 51, + 36, 19, 3, -13, -28, -41, -51, -58, + -62, -62, -59, -53, -44, -33, -21, -8}}; + +class EPIRBProcessor : public BasebandProcessor { + public: + EPIRBProcessor(); + + void execute(const buffer_c8_t& buffer) override; + + void on_message(const Message* const message) override; + + private: + // EPIRB operates at 406 MHz with narrow bandwidth + static constexpr size_t baseband_fs = 2457600; + static constexpr uint32_t epirb_center_freq = 406028000; // 406.028 MHz + static constexpr uint32_t symbol_rate = 400; // 400 bps + static constexpr size_t decimation_factor = 64; // Decimate to ~38.4kHz + + std::array dst{}; + const buffer_c16_t dst_buffer{ + dst.data(), + dst.size()}; + + // Decimation chain for 406 MHz EPIRB signal processing + dsp::decimate::FIRC8xR16x24FS4Decim8 decim_0{}; + dsp::decimate::FIRC16xR16x32Decim8 decim_1{}; + + dsp::matched_filter::MatchedFilter mf{bpsk_taps, 2}; + + // Clock recovery for 400 bps symbol rate + // Sampling rate after decimation: ~38.4kHz + // Symbols per sample: 38400 / 400 = 96 samples per symbol + clock_recovery::ClockRecovery clock_recovery{ + 38400, // sampling_rate + 400, // symbol_rate (400 bps) + {0.0555f}, // error_filter coefficient + [this](const float symbol) { this->consume_symbol(symbol); }}; + + // Simple bi-phase L decoder state + uint_fast8_t last_symbol = 0; + + // EPIRB packet structure: + // - Sync pattern: 000101010101... (15 bits) + // - Frame sync: 0111110 (7 bits) + // - Data: 112 bits + // - BCH error correction: 10 bits + // Total: 144 bits + PacketBuilder packet_builder{ + {0b010101010101010, 15, 1}, // Preamble pattern + {0b0111110, 7}, // Frame sync pattern + {0b0111110, 7}, // End pattern (same as sync for simplicity) + [this](const baseband::Packet& packet) { + this->payload_handler(packet); + }}; + + void consume_symbol(const float symbol); + void payload_handler(const baseband::Packet& packet); + + // Statistics + uint32_t packets_received = 0; + Timestamp last_packet_timestamp{}; + + /* NB: Threads should be the last members in the class definition. */ + BasebandThread baseband_thread{ + baseband_fs, this, baseband::Direction::Receive, /*auto_start*/ false}; + RSSIThread rssi_thread{}; +}; + +#endif /*__PROC_EPIRB_H__*/ \ No newline at end of file diff --git a/firmware/common/message.hpp b/firmware/common/message.hpp index 74427ae54..ae6f33bb7 100644 --- a/firmware/common/message.hpp +++ b/firmware/common/message.hpp @@ -135,6 +135,7 @@ class Message { NoaaAptRxStatusData = 78, NoaaAptRxImageData = 79, FSKPacket = 80, + EPIRBPacket = 81, MAX }; @@ -339,6 +340,17 @@ class AISPacketMessage : public Message { baseband::Packet packet; }; +class EPIRBPacketMessage : public Message { + public: + constexpr EPIRBPacketMessage( + const baseband::Packet& packet) + : Message{ID::EPIRBPacket}, + packet{packet} { + } + + baseband::Packet packet; +}; + class TPMSPacketMessage : public Message { public: constexpr TPMSPacketMessage( diff --git a/firmware/common/spi_image.hpp b/firmware/common/spi_image.hpp index cb6dc8c6f..a4b94215c 100644 --- a/firmware/common/spi_image.hpp +++ b/firmware/common/spi_image.hpp @@ -87,6 +87,7 @@ constexpr image_tag_t image_tag_am_audio{'P', 'A', 'M', 'A'}; constexpr image_tag_t image_tag_am_tv{'P', 'A', 'M', 'T'}; constexpr image_tag_t image_tag_capture{'P', 'C', 'A', 'P'}; constexpr image_tag_t image_tag_ert{'P', 'E', 'R', 'T'}; +constexpr image_tag_t image_tag_epirb_rx{'P', 'E', 'P', 'I'}; constexpr image_tag_t image_tag_nfm_audio{'P', 'N', 'F', 'M'}; constexpr image_tag_t image_tag_pocsag{'P', 'P', 'O', 'C'}; constexpr image_tag_t image_tag_pocsag2{'P', 'P', 'O', '2'}; diff --git a/firmware/tools/export_external_apps.py b/firmware/tools/export_external_apps.py index 0c61b4449..feb223e37 100755 --- a/firmware/tools/export_external_apps.py +++ b/firmware/tools/export_external_apps.py @@ -98,6 +98,7 @@ for external_image_prefix in sys.argv[4:]: # COMMAND ${CMAKE_OBJCOPY} -v -O binary ${PROJECT_NAME}.elf ${PROJECT_NAME}_ext_pacman.bin --only-section=.external_app_pacman himg = "{}/external_app_{}.himg".format(binary_dir, external_image_prefix) + print("Creating external application image for {}".format(external_image_prefix)) subprocess.run([cmake_objcopy, "-v", "-O", "binary", "{}/application.elf".format(binary_dir), himg, "--only-section=.external_app_{}".format(external_image_prefix)]) external_application_image = read_image(himg)