From 4a836aef86efcbdd09e5b0d5402b4a2476dd9a8d Mon Sep 17 00:00:00 2001 From: Belousov Oleg Date: Mon, 27 Jul 2026 21:23:32 +0300 Subject: [PATCH] Move AIS receiver to external app --- firmware/application/CMakeLists.txt | 1 - .../{apps => external/ais_rx}/ais_app.cpp | 66 +++++++++++-------- .../{apps => external/ais_rx}/ais_app.hpp | 6 +- firmware/application/external/ais_rx/main.cpp | 57 ++++++++++++++++ firmware/application/external/external.cmake | 5 ++ firmware/application/external/external.ld | 7 ++ firmware/application/ui_navigation.cpp | 2 - firmware/tools/external_app_info.py | 2 +- 8 files changed, 110 insertions(+), 36 deletions(-) rename firmware/application/{apps => external/ais_rx}/ais_app.cpp (84%) rename firmware/application/{apps => external/ais_rx}/ais_app.hpp (98%) create mode 100644 firmware/application/external/ais_rx/main.cpp diff --git a/firmware/application/CMakeLists.txt b/firmware/application/CMakeLists.txt index 64edcd517..eae7fa53d 100644 --- a/firmware/application/CMakeLists.txt +++ b/firmware/application/CMakeLists.txt @@ -279,7 +279,6 @@ set(CPPSRC ui/ui_tone_key.cpp ui/ui_transmitter.cpp ui/ui_bmpview.cpp - apps/ais_app.cpp apps/analog_audio_app.cpp apps/ble_rx_app.cpp apps/ble_tx_app.cpp diff --git a/firmware/application/apps/ais_app.cpp b/firmware/application/external/ais_rx/ais_app.cpp similarity index 84% rename from firmware/application/apps/ais_app.cpp rename to firmware/application/external/ais_rx/ais_app.cpp index 8eaa23bb0..0317d1b6f 100644 --- a/firmware/application/apps/ais_app.cpp +++ b/firmware/application/external/ais_rx/ais_app.cpp @@ -35,7 +35,8 @@ using namespace portapack; namespace pmem = portapack::persistent_memory; -namespace ais { +namespace ui::external_app::ais_rx { + namespace format { static std::string latlon_abs_normalized(const int32_t normalized, const char suffixes[2]) { @@ -47,7 +48,7 @@ static std::string latlon_abs_normalized(const int32_t normalized, const char su return to_string_dec_uint(degrees) + "." + to_string_dec_uint(fraction, 6, '0') + suffix; } -static std::string latlon(const Latitude latitude, const Longitude longitude) { +static std::string latlon(const ais::Latitude latitude, const ais::Longitude longitude) { if (latitude.is_valid() && longitude.is_valid()) { return latlon_abs_normalized(latitude.normalized(), "SN") + " " + latlon_abs_normalized(longitude.normalized(), "WE"); } else if (latitude.is_not_available() && longitude.is_not_available()) { @@ -128,7 +129,7 @@ static std::string navigational_status(const unsigned int value) { } } -static std::string rate_of_turn(const RateOfTurn value) { +static std::string rate_of_turn(const ais::RateOfTurn value) { switch (value) { case -128: return "not available"; @@ -149,7 +150,7 @@ static std::string rate_of_turn(const RateOfTurn value) { } } -static std::string speed_over_ground(const SpeedOverGround value) { +static std::string speed_over_ground(const ais::SpeedOverGround value) { if (value == 1023) { return "not available"; } else if (value == 1022) { @@ -159,7 +160,7 @@ static std::string speed_over_ground(const SpeedOverGround value) { } } -static std::string course_over_ground(const CourseOverGround value) { +static std::string course_over_ground(const ais::CourseOverGround value) { if (value > 3600) { return "invalid"; } else if (value == 3600) { @@ -169,7 +170,7 @@ static std::string course_over_ground(const CourseOverGround value) { } } -static std::string true_heading(const TrueHeading value) { +static std::string true_heading(const ais::TrueHeading value) { if (value == 511) { return "not available"; } else if (value > 359) { @@ -180,7 +181,6 @@ static std::string true_heading(const TrueHeading value) { } } /* namespace format */ -} /* namespace ais */ void AISLogger::on_packet(const ais::Packet& packet) { // TODO: Unstuff here, not in baseband! @@ -265,25 +265,33 @@ void AISRecentEntry::update(const ais::Packet& packet) { } } +} // namespace ui::external_app::ais_rx + namespace ui { template <> -void RecentEntriesTable::draw( +void RecentEntriesTable::draw( const Entry& entry, const Rect& target_rect, Painter& painter, const Style& style, RecentEntriesColumns&) { - std::string line = ais::format::mmsi(entry.mmsi) + " "; + using namespace external_app::ais_rx; + + std::string line = format::mmsi(entry.mmsi) + " "; if (!entry.name.empty()) { - line += ais::format::text(entry.name); + line += format::text(entry.name); } else { - line += ais::format::text(entry.call_sign); + line += format::text(entry.call_sign); } line.resize(target_rect.width() / 8, ' '); painter.draw_string(target_rect.location(), style, line); } +} // namespace ui + +namespace ui::external_app::ais_rx { + AISRecentEntryDetailView::AISRecentEntryDetailView(NavigationView& nav) { add_children({ &button_done, @@ -298,12 +306,12 @@ AISRecentEntryDetailView::AISRecentEntryDetailView(NavigationView& nav) { button_see_map.on_select = [this, &nav](Button&) { geomap_view = nav.push( - ais::format::text(entry_.name), + format::text(entry_.name), 0, GeoPos::alt_unit::METERS, GeoPos::spd_unit::KNOTS, - ais::format::latlon_float(entry_.last_position.latitude.normalized()), - ais::format::latlon_float(entry_.last_position.longitude.normalized()), + format::latlon_float(entry_.last_position.latitude.normalized()), + format::latlon_float(entry_.last_position.longitude.normalized()), entry_.last_position.true_heading, [this]() { send_updates = false; @@ -324,14 +332,14 @@ AISRecentEntryDetailView& AISRecentEntryDetailView::operator=(const AISRecentEnt void AISRecentEntryDetailView::update_position() { if (send_updates) - geomap_view->update_position(ais::format::latlon_float(entry_.last_position.latitude.normalized()), ais::format::latlon_float(entry_.last_position.longitude.normalized()), (float)entry_.last_position.true_heading, 0, entry_.last_position.speed_over_ground > 1022 ? 0 : entry_.last_position.speed_over_ground / 10); + geomap_view->update_position(format::latlon_float(entry_.last_position.latitude.normalized()), format::latlon_float(entry_.last_position.longitude.normalized()), (float)entry_.last_position.true_heading, 0, entry_.last_position.speed_over_ground > 1022 ? 0 : entry_.last_position.speed_over_ground / 10); } bool AISRecentEntryDetailView::add_map_marker(const AISRecentEntry& entry) { if (geomap_view && send_updates) { GeoMarker marker{}; - marker.lon = ais::format::latlon_float(entry.last_position.longitude.normalized()); - marker.lat = ais::format::latlon_float(entry.last_position.latitude.normalized()); + marker.lon = format::latlon_float(entry.last_position.longitude.normalized()); + marker.lat = format::latlon_float(entry.last_position.latitude.normalized()); marker.angle = entry.last_position.true_heading; marker.tag = entry.call_sign.empty() ? to_string_dec_uint(entry.mmsi) : entry.call_sign; auto markerStored = geomap_view->store_marker(marker); @@ -377,18 +385,18 @@ void AISRecentEntryDetailView::paint(Painter& painter) { auto field_rect = Rect{rect.left(), rect.top() + 16, rect.width(), 16}; - field_rect = draw_field(painter, field_rect, s, "MMSI", ais::format::mmsi(entry_.mmsi)); - field_rect = draw_field(painter, field_rect, s, "Ctry", ais::format::mid(entry_.mmsi)); - field_rect = draw_field(painter, field_rect, s, "Name", ais::format::text(entry_.name)); - field_rect = draw_field(painter, field_rect, s, "Call", ais::format::text(entry_.call_sign)); - field_rect = draw_field(painter, field_rect, s, "Dest", ais::format::text(entry_.destination)); + field_rect = draw_field(painter, field_rect, s, "MMSI", format::mmsi(entry_.mmsi)); + field_rect = draw_field(painter, field_rect, s, "Ctry", format::mid(entry_.mmsi)); + field_rect = draw_field(painter, field_rect, s, "Name", format::text(entry_.name)); + field_rect = draw_field(painter, field_rect, s, "Call", format::text(entry_.call_sign)); + field_rect = draw_field(painter, field_rect, s, "Dest", format::text(entry_.destination)); field_rect = draw_field(painter, field_rect, s, "Last", to_string_datetime(entry_.last_position.timestamp)); - field_rect = draw_field(painter, field_rect, s, "Pos ", ais::format::latlon(entry_.last_position.latitude, entry_.last_position.longitude)); - field_rect = draw_field(painter, field_rect, s, "Stat", ais::format::navigational_status(entry_.navigational_status)); - field_rect = draw_field(painter, field_rect, s, "RoT ", ais::format::rate_of_turn(entry_.last_position.rate_of_turn)); - field_rect = draw_field(painter, field_rect, s, "SoG ", ais::format::speed_over_ground(entry_.last_position.speed_over_ground)); - field_rect = draw_field(painter, field_rect, s, "CoG ", ais::format::course_over_ground(entry_.last_position.course_over_ground)); - field_rect = draw_field(painter, field_rect, s, "Head", ais::format::true_heading(entry_.last_position.true_heading)); + field_rect = draw_field(painter, field_rect, s, "Pos ", format::latlon(entry_.last_position.latitude, entry_.last_position.longitude)); + field_rect = draw_field(painter, field_rect, s, "Stat", format::navigational_status(entry_.navigational_status)); + field_rect = draw_field(painter, field_rect, s, "RoT ", format::rate_of_turn(entry_.last_position.rate_of_turn)); + field_rect = draw_field(painter, field_rect, s, "SoG ", format::speed_over_ground(entry_.last_position.speed_over_ground)); + field_rect = draw_field(painter, field_rect, s, "CoG ", format::course_over_ground(entry_.last_position.course_over_ground)); + field_rect = draw_field(painter, field_rect, s, "Head", format::true_heading(entry_.last_position.true_heading)); field_rect = draw_field(painter, field_rect, s, "Rx #", to_string_dec_uint(entry_.received_count)); } @@ -504,4 +512,4 @@ void AISAppView::on_show_detail(const AISRecentEntry& entry) { recent_entry_detail_view.update_map_markers(recent); } -} /* namespace ui */ +} // namespace ui::external_app::ais_rx diff --git a/firmware/application/apps/ais_app.hpp b/firmware/application/external/ais_rx/ais_app.hpp similarity index 98% rename from firmware/application/apps/ais_app.hpp rename to firmware/application/external/ais_rx/ais_app.hpp index 22566de6e..ea9bbd6fb 100644 --- a/firmware/application/apps/ais_app.hpp +++ b/firmware/application/external/ais_rx/ais_app.hpp @@ -50,6 +50,8 @@ using namespace lpc43xx; #include "recent_entries.hpp" +namespace ui::external_app::ais_rx { + struct AISPosition { rtc::RTC timestamp{}; ais::Latitude latitude{}; @@ -109,8 +111,6 @@ class AISLogger { LogFile log_file{}; }; -namespace ui { - using AISRecentEntriesView = RecentEntriesView; class AISRecentEntryDetailView : public View { @@ -239,6 +239,6 @@ class AISAppView : public View { void on_tick_second(); }; -} /* namespace ui */ +} // namespace ui::external_app::ais_rx #endif /*__AIS_APP_H__*/ diff --git a/firmware/application/external/ais_rx/main.cpp b/firmware/application/external/ais_rx/main.cpp new file mode 100644 index 000000000..bf6da5c4b --- /dev/null +++ b/firmware/application/external/ais_rx/main.cpp @@ -0,0 +1,57 @@ +/* + * Copyright (C) 2026 PortaPack Mayhem + * + * 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 "ais_app.hpp" +#include "ui_navigation.hpp" +#include "external_app.hpp" + +namespace ui::external_app::ais_rx { + +void initialize_app(ui::NavigationView& nav) { + nav.push(); +} + +} // namespace ui::external_app::ais_rx + +extern "C" { + +__attribute__((section(".external_app.app_ais_rx.application_information"), used)) application_information_t _application_information_ais_rx = { + /*.memory_location = */ (uint8_t*)0x00000000, + /*.externalAppEntry = */ ui::external_app::ais_rx::initialize_app, + /*.header_version = */ CURRENT_HEADER_VERSION, + /*.app_version = */ VERSION_MD5, + + /*.app_name = */ "AIS Boats", + /*.bitmap_data = */ { + 0x00, 0x01, 0x80, 0x01, 0xC0, 0x01, 0xC0, 0x0D, + 0xE0, 0x3D, 0xF0, 0x3D, 0xF8, 0x7D, 0xFC, 0x7D, + 0xFC, 0x7D, 0xFE, 0x7D, 0xFF, 0x7D, 0x00, 0x00, + 0xF8, 0x7F, 0xF8, 0x3F, 0xF0, 0x0F, 0x00, 0x00, + }, + /*.icon_color = */ ui::Color::green().v, + /*.menu_location = */ app_location_t::RX, + /*.desired_menu_position = */ -1, + + /*.m4_app_tag = portapack::spi_flash::image_tag_ais */ {'P', 'A', 'I', 'S'}, + /*.m4_app_offset = */ 0x00000000, +}; + +} // extern "C" diff --git a/firmware/application/external/external.cmake b/firmware/application/external/external.cmake index fcb010143..ac1d01e43 100644 --- a/firmware/application/external/external.cmake +++ b/firmware/application/external/external.cmake @@ -395,6 +395,10 @@ set(EXTCPPSRC external/adsbrx/main.cpp external/adsbrx/ui_adsb_rx.cpp + #ais rx + external/ais_rx/main.cpp + external/ais_rx/ais_app.cpp + ) set(EXTAPPLIST @@ -490,6 +494,7 @@ set(EXTAPPLIST signal_hunter tetra_rx adsbrx + ais_rx ) # sdusb has type conflicts with PRALINE (HackRF Pro) - add only for non-PRALINE builds diff --git a/firmware/application/external/external.ld b/firmware/application/external/external.ld index adcba5d35..911d3d793 100644 --- a/firmware/application/external/external.ld +++ b/firmware/application/external/external.ld @@ -116,6 +116,7 @@ MEMORY ram_external_app_signal_hunter (rwx) : org = 0xAE0B0000, len = 32k ram_external_app_tetra_rx (rwx) : org = 0xAE0C0000, len = 32k ram_external_app_adsbrx (rwx) : org = 0xAE0D0000, len = 32k + ram_external_app_ais_rx (rwx) : org = 0xAE0E0000, len = 32k } SECTIONS @@ -678,6 +679,12 @@ SECTIONS *(*ui*external_app*adsbrx*); } > ram_external_app_adsbrx + .external_app_ais_rx : ALIGN(4) SUBALIGN(4) + { + KEEP(*(.external_app.app_ais_rx.application_information)); + *(*ui*external_app*ais_rx*); + } > ram_external_app_ais_rx + } diff --git a/firmware/application/ui_navigation.cpp b/firmware/application/ui_navigation.cpp index e20056c53..7a3d901e2 100644 --- a/firmware/application/ui_navigation.cpp +++ b/firmware/application/ui_navigation.cpp @@ -59,7 +59,6 @@ #include "ui_battinfo.hpp" #include "ui_external_items_menu_loader.hpp" -#include "ais_app.hpp" #include "analog_audio_app.hpp" #include "ble_rx_app.hpp" #include "ble_tx_app.hpp" @@ -98,7 +97,6 @@ const NavigationView::AppList NavigationView::appList = { {nullptr, "Games", HOME, Color::cyan(), &bitmap_icon_games, new ViewFactory()}, {nullptr, "Settings", HOME, Color::cyan(), &bitmap_icon_setup, new ViewFactory()}, /* RX ********************************************************************/ - {"ais", "AIS Boats", RX, Color::green(), &bitmap_icon_ais, new ViewFactory()}, {"aprsrx", "APRS", RX, Color::green(), &bitmap_icon_aprs, new ViewFactory()}, {"audio", "Audio", RX, Color::green(), &bitmap_icon_speaker, new ViewFactory()}, {"blerx", "BLE Rx", RX, Color::green(), &bitmap_icon_btle, new ViewFactory()}, diff --git a/firmware/tools/external_app_info.py b/firmware/tools/external_app_info.py index 433aa3109..fb82f3716 100644 --- a/firmware/tools/external_app_info.py +++ b/firmware/tools/external_app_info.py @@ -24,4 +24,4 @@ # external app address ranges below must match those in linker file "external.ld" maximum_application_size = 32*1024 external_apps_address_start = 0xADB00000 -external_apps_address_end = 0xAE0E0000 +external_apps_address_end = 0xAE0F0000