mirror of
https://github.com/portapack-mayhem/mayhem-firmware.git
synced 2025-08-22 07:07:49 +00: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:
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
|
||||
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
|
||||
)
|
||||
|
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_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
|
||||
|
||||
}
|
||||
|
||||
|
Reference in New Issue
Block a user