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