Move AIS receiver to external app

This commit is contained in:
Belousov Oleg
2026-07-27 21:23:32 +03:00
parent 838a8de5d0
commit 4a836aef86
8 changed files with 110 additions and 36 deletions
-1
View File
@@ -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
@@ -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<AISRecentEntries>::draw(
void RecentEntriesTable<external_app::ais_rx::AISRecentEntries>::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<GeoMapView>(
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
@@ -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<AISRecentEntries>;
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__*/
+57
View File
@@ -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<AISAppView>();
}
} // 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"
+5
View File
@@ -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
+7
View File
@@ -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
}
-2
View File
@@ -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<GamesMenuView>()},
{nullptr, "Settings", HOME, Color::cyan(), &bitmap_icon_setup, new ViewFactory<SettingsMenuView>()},
/* RX ********************************************************************/
{"ais", "AIS Boats", RX, Color::green(), &bitmap_icon_ais, new ViewFactory<AISAppView>()},
{"aprsrx", "APRS", RX, Color::green(), &bitmap_icon_aprs, new ViewFactory<APRSRXView>()},
{"audio", "Audio", RX, Color::green(), &bitmap_icon_speaker, new ViewFactory<AnalogAudioView>()},
{"blerx", "BLE Rx", RX, Color::green(), &bitmap_icon_btle, new ViewFactory<BLERxView>()},
+1 -1
View File
@@ -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