diff --git a/firmware/application/CMakeLists.txt b/firmware/application/CMakeLists.txt index 3ee7d074..064452be 100644 --- a/firmware/application/CMakeLists.txt +++ b/firmware/application/CMakeLists.txt @@ -140,6 +140,7 @@ set(CPPSRC ${COMMON}/png_writer.cpp ${COMMON}/pocsag.cpp ${COMMON}/pocsag_packet.cpp + ${COMMON}/aprs_packet.cpp ${COMMON}/portapack_io.cpp ${COMMON}/portapack_persistent_memory.cpp ${COMMON}/portapack_shared_memory.cpp @@ -218,6 +219,7 @@ set(CPPSRC apps/ui_adsb_rx.cpp apps/ui_adsb_tx.cpp apps/ui_afsk_rx.cpp + apps/ui_aprs_rx.cpp apps/ui_btle_rx.cpp apps/ui_nrf_rx.cpp apps/ui_aprs_tx.cpp diff --git a/firmware/application/apps/ui_about_simple.cpp b/firmware/application/apps/ui_about_simple.cpp index dceb869d..ff0852db 100644 --- a/firmware/application/apps/ui_about_simple.cpp +++ b/firmware/application/apps/ui_about_simple.cpp @@ -31,6 +31,7 @@ namespace ui console.writeln("N0vaPixel,klockee,GullCode"); console.writeln("jamesshao8,ITAxReal,rascafr"); console.writeln("mcules,dqs105,strijar"); + console.writeln("East2West"); console.writeln(""); break; diff --git a/firmware/application/apps/ui_aprs_rx.cpp b/firmware/application/apps/ui_aprs_rx.cpp new file mode 100644 index 00000000..db898a69 --- /dev/null +++ b/firmware/application/apps/ui_aprs_rx.cpp @@ -0,0 +1,409 @@ +/* + * Copyright (C) 2014 Jared Boone, ShareBrained Technology, Inc. + * Copyright (C) 2017 Furrtek + * + * 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_aprs_rx.hpp" + +#include "audio.hpp" +#include "rtc_time.hpp" +#include "baseband_api.hpp" +#include "string_format.hpp" +#include "portapack_persistent_memory.hpp" + +using namespace portapack; + +void APRSLogger::log_raw_data(const std::string& data) { + rtc::RTC datetime; + rtcGetTime(&RTCD1, &datetime); + + log_file.write_entry(datetime, data); +} + +namespace ui { + +template<> +void RecentEntriesTable::draw( + const Entry& entry, + const Rect& target_rect, + Painter& painter, + const Style& style +) { + char aged_color; + Color target_color; + auto entry_age = entry.age; + + target_color = Color::green(); + + aged_color = 0x10; + std::string entry_string = "\x1B"; + entry_string += aged_color; + + entry_string += entry.source_formatted; + entry_string.append(10 - entry.source_formatted.size(),' '); + entry_string += " "; + entry_string += (entry.hits <= 999 ? to_string_dec_uint(entry.hits, 4) : "999+"); + entry_string += " "; + entry_string += entry.time_string; + + painter.draw_string( + target_rect.location(), + style, + entry_string + ); + + if (entry.has_position){ + painter.draw_bitmap(target_rect.location() + Point(12 * 8, 0), bitmap_target, target_color, style.background); + } +} + + +void APRSRxView::focus() { + options_region.focus(); +} + +void APRSRxView::update_freq(rf::Frequency f) { + receiver_model.set_tuning_frequency(f); +} + +APRSRxView::APRSRxView(NavigationView& nav, Rect parent_rect) : View(parent_rect) { + baseband::run_image(portapack::spi_flash::image_tag_aprs_rx); + + add_children({ + &rssi, + &channel, + &field_rf_amp, + &field_lna, + &field_vga, + &options_region, + &field_frequency, + &record_view, + &console + }); + + // DEBUG + record_view.on_error = [&nav](std::string message) { + nav.display_modal("Error", message); + }; + + record_view.set_sampling_rate(24000); + + options_region.on_change = [this](size_t, int32_t i) { + if (i == 0){ + field_frequency.set_value(144390000); + } + if(i == 1){ + field_frequency.set_value(144800000); + } + if(i == 2){ + field_frequency.set_value(145175000); + } + }; + + field_frequency.set_value(receiver_model.tuning_frequency()); + field_frequency.set_step(100); + field_frequency.on_change = [this](rf::Frequency f) { + update_freq(f); + }; + field_frequency.on_edit = [this, &nav]() { + auto new_view = nav.push(receiver_model.tuning_frequency()); + new_view->on_changed = [this](rf::Frequency f) { + update_freq(f); + field_frequency.set_value(f); + }; + }; + + options_region.set_selected_index(0, true); + + logger = std::make_unique(); + if (logger) + logger->append("APRS_RX_LOG.TXT"); + + baseband::set_aprs(1200); + + audio::set_rate(audio::Rate::Hz_24000); + audio::output::start(); + + receiver_model.set_sampling_rate(3072000); + receiver_model.set_baseband_bandwidth(1750000); + receiver_model.set_modulation(ReceiverModel::Mode::NarrowbandFMAudio); + receiver_model.enable(); +} + +void APRSRxView::on_packet(const APRSPacketMessage* message){ + std::string str_console = "\x1B"; + + aprs::APRSPacket packet = message->packet; + + std::string stream_text = packet.get_stream_text(); + str_console += (char)((console_color++ & 3) + 9); + str_console += stream_text; + + if(logger){ + logger->log_raw_data(stream_text); + } + + //if(reset_console){ //having more than one console causes issues when switching tabs where one is disabled, and the other enabled breaking the scoll setup. + // console.on_hide(); + // console.on_show(); + // reset_console = false; + //} + + console.writeln(str_console); +} + +void APRSRxView::on_data(uint32_t value, bool is_data) { + std::string str_console = "\x1B"; + std::string str_byte = ""; + + if (is_data) { + // Colorize differently after message splits + str_console += (char)((console_color & 3) + 9); + + if (value == '\n') { + // Message split + console.writeln(""); + console_color++; + + if (logger) { + logger->log_raw_data(str_log); + str_log = ""; + } + } + else { + if ((value >= 32) && (value < 127)) { + str_console += (char)value; // Printable + str_byte += (char)value; + } else { + str_console += "[" + to_string_hex(value, 2) + "]"; // Not printable + str_byte += "[" + to_string_hex(value, 2) + "]"; + } + + console.write(str_console); + + if (logger) str_log += str_byte; + } + } else { + + } +} + +void APRSRxView::on_show(){ + //some bug where the display scroll area is set to the entire screen when switching from the list tab with details showing back to the stream view. + //reset_console = true; +} + +APRSRxView::~APRSRxView() { + audio::output::stop(); + receiver_model.disable(); + baseband::shutdown(); +} + +void APRSTableView::on_show_list() { + details_view.hidden(true); + recent_entries_view.hidden(false); + send_updates = false; + focus(); +} + +void APRSTableView::on_show_detail(const APRSRecentEntry& entry) { + recent_entries_view.hidden(true); + details_view.hidden(false); + details_view.set_entry(entry); + details_view.update(); + details_view.focus(); + detailed_entry_key = entry.key(); + send_updates = true; +} + +APRSTableView::APRSTableView(NavigationView& nav, Rect parent_rec) : View(parent_rec), nav_ {nav} { + add_children({ + &recent_entries_view, + &details_view + }); + + hidden(true); + + details_view.hidden(true); + + recent_entries_view.set_parent_rect({0, 0, 240, 280}); + details_view.set_parent_rect({0, 0, 240, 280}); + + recent_entries_view.on_select = [this](const APRSRecentEntry& entry) { + this->on_show_detail(entry); + }; + + details_view.on_close = [this]() { + this->on_show_list(); + }; + + +/* for(size_t i = 0; i <32 ; i++){ + std::string id = "test" + i; + auto& entry = ::on_packet(recent, i); + entry.set_source_formatted(id); + } + + recent_entries_view.set_dirty(); */ + + /* + + std::string str1 = "test1"; + std::string str12 = "test2"; + std::string str13 = "test2"; + + auto& entry = ::on_packet(recent, 0x1); + entry.set_source_formatted(str1); + + auto& entry2 = ::on_packet(recent, 0x2); + entry2.set_source_formatted(str12); + + auto& entry3 = ::on_packet(recent, 0x2); + entry2.set_source_formatted(str13); + + recent_entries_view.set_dirty(); + + */ +} + +void APRSTableView::on_pkt(const APRSPacketMessage* message){ + aprs::APRSPacket packet = message->packet; + rtc::RTC datetime; + std::string str_timestamp; + std::string source_formatted = packet.get_source_formatted(); + std::string info_string = packet.get_stream_text(); + + rtcGetTime(&RTCD1, &datetime); + auto& entry = ::on_packet(recent, packet.get_source()); + entry.reset_age(); + entry.inc_hit(); + str_timestamp = to_string_datetime(datetime, HMS); + entry.set_time_string(str_timestamp); + entry.set_info_string(info_string); + + entry.set_source_formatted(source_formatted); + + if(entry.has_position && !packet.has_position()){ + //maintain position info + } + else { + entry.set_has_position(packet.has_position()); + entry.set_pos(packet.get_position()); + } + + if( entry.key() == details_view.entry().key() ) { + details_view.set_entry(entry); + details_view.update(); + } + + recent_entries_view.set_dirty(); +} + +void APRSTableView::on_show(){ + on_show_list(); +} + +void APRSTableView::on_hide(){ + details_view.hidden(true); +} + +void APRSTableView::focus(){ + recent_entries_view.focus(); +} + +APRSTableView::~APRSTableView(){ + +} + +void APRSDetailsView::focus() { + button_done.focus(); +} + +void APRSDetailsView::set_entry(const APRSRecentEntry& entry){ + entry_copy = entry; +} + +void APRSDetailsView::update() { + if(!hidden()){ + uint32_t age = entry_copy.age; + + console.clear(true); + console.write(entry_copy.info_string); + + button_see_map.hidden(!entry_copy.has_position); + } + + if (send_updates) + geomap_view->update_position(entry_copy.pos.latitude, entry_copy.pos.longitude, 0); +} + +APRSDetailsView::~APRSDetailsView() { +} + +APRSDetailsView::APRSDetailsView( + NavigationView& nav +) +{ + add_children({ + &console, + &button_done, + &button_see_map + }); + + button_done.on_select = [this, &nav](Button&) { + console.clear(true); + this->on_close(); + }; + + button_see_map.on_select = [this, &nav](Button&) { + geomap_view = nav.push( + entry_copy.source_formatted, + 0, //entry_copy.pos.altitude, + GeoPos::alt_unit::FEET, + entry_copy.pos.latitude, + entry_copy.pos.longitude, + 0, /*entry_copy.velo.heading,*/ + [this]() { + send_updates = false; + hidden(false); + update(); + }); + send_updates = true; + hidden(true); + + }; +}; + +APRSRXView::APRSRXView(NavigationView& nav) : nav_ {nav} { + add_children({ + &tab_view, + &view_stream, + &view_table + }); +} + +void APRSRXView::focus(){ + tab_view.focus(); +} + +APRSRXView::~APRSRXView() { + +} +} /* namespace ui */ diff --git a/firmware/application/apps/ui_aprs_rx.hpp b/firmware/application/apps/ui_aprs_rx.hpp new file mode 100644 index 00000000..e392e62f --- /dev/null +++ b/firmware/application/apps/ui_aprs_rx.hpp @@ -0,0 +1,276 @@ +/* + * Copyright (C) 2014 Jared Boone, ShareBrained Technology, Inc. + * Copyright (C) 2017 Furrtek + * + * 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_APRS_RX_H__ +#define __UI_APRS_RX_H__ + +#include "ui.hpp" +#include "ui_navigation.hpp" +#include "ui_receiver.hpp" +#include "ui_record_view.hpp" // DEBUG +#include "ui_geomap.hpp" + +#include "recent_entries.hpp" +#include "ui_tabview.hpp" + +#include "log_file.hpp" +#include "utility.hpp" + +class APRSLogger { +public: + Optional append(const std::string& filename) { + return log_file.append(filename); + } + + void log_raw_data(const std::string& data); + +private: + LogFile log_file { }; +}; + +namespace ui { + +struct APRSRecentEntry { + using Key = uint64_t; + + static constexpr Key invalid_key = 0xffffffffffffffff; + + uint16_t hits { 0 }; + uint32_t age { 0 }; + + uint64_t source; + std::string source_formatted { " " }; + std::string time_string { "" }; + std::string info_string { "" }; + + aprs::aprs_pos pos; + bool has_position = false; + APRSRecentEntry(uint64_t src) + { + source = src; + } + + Key key() const { + return source; + } + + void set_source_formatted(std::string& new_source) { + source_formatted = new_source; + } + + void inc_hit() { + hits++; + } + + void set_info_string(std::string& new_info_string) { + info_string = new_info_string; + } + + void set_time_string(std::string& new_time_string) { + time_string = new_time_string; + } + + void set_pos(aprs::aprs_pos pos_in){ + pos = pos_in; + } + + void set_has_position(bool has_pos){ + has_position = has_pos; + } + + void reset_age() { + age = 0; + } + + void inc_age() { + age++; + } +}; + +class APRSDetailsView : public View { +public: + APRSDetailsView(NavigationView&); + ~APRSDetailsView(); + + APRSDetailsView(const APRSDetailsView&) = delete; + APRSDetailsView(APRSDetailsView&&) = delete; + APRSDetailsView& operator=(const APRSDetailsView&) = delete; + APRSDetailsView& operator=(APRSDetailsView&&) = delete; + + void focus() override; + + void update(); + void set_entry(const APRSRecentEntry& entry); + + const APRSRecentEntry& entry() const { return entry_copy; }; + + std::string title() const override { return "Details"; }; + std::function on_close { }; + +private: + APRSRecentEntry entry_copy { 0 }; + GeoMapView* geomap_view { nullptr }; + bool send_updates { false }; + + Console console { + { 0, 0 * 16, 240, 224 } + }; + + Button button_done { + { 160, 14 * 16, 8 * 8, 3 * 16 }, + "Close" + }; + + Button button_see_map { + { 80, 14 * 16, 8 * 8, 3 * 16 }, + "Map" + }; +}; + +using APRSRecentEntries = RecentEntries; + +class APRSTableView: public View { +public: + APRSTableView(NavigationView& nav, Rect parent_rec); + ~APRSTableView(); + + void on_show() override; + void on_hide() override; + void focus() override; + void on_pkt(const APRSPacketMessage* message); + + std::string title() const override { return "Stations"; }; +private: + NavigationView& nav_; + const RecentEntriesColumns columns { { + { "Source", 9 }, + { "Loc", 6 }, + { "Hits", 4 }, + { "Time", 8 } + } }; + APRSRecentEntries recent { }; + RecentEntriesView> recent_entries_view { columns, recent }; + APRSDetailsView details_view { nav_ }; + uint32_t detailed_entry_key { 0 }; + bool send_updates { false }; + + void on_show_list(); + void on_show_detail(const APRSRecentEntry& entry); +}; + +class APRSRxView : public View { +public: + APRSRxView(NavigationView& nav, Rect parent_rect); + ~APRSRxView(); + + void on_show() override; + void focus() override; + + std::string title() const override { return "APRS RX"; }; + void on_packet(const APRSPacketMessage* message); + +private: + void on_data(uint32_t value, bool is_data); + bool reset_console = false; + + uint8_t console_color { 0 }; + std::string str_log { "" }; + + RFAmpField field_rf_amp { + { 13 * 8, 0 * 16 } + }; + LNAGainField field_lna { + { 15 * 8, 0 * 16 } + }; + VGAGainField field_vga { + { 18 * 8, 0 * 16 } + }; + RSSI rssi { + { 21 * 8, 0, 6 * 8, 4 }, + }; + Channel channel { + { 21 * 8, 5, 6 * 8, 4 }, + }; + + OptionsField options_region { + { 0 * 8, 0 * 8 }, + 3, + { + { "NA ", 0 }, + { "EUR", 1 }, + { "AUS", 2 } + } + }; + + FrequencyField field_frequency { + { 3 * 8, 0 * 16 }, + }; + + // DEBUG + RecordView record_view { + { 0 * 8, 1 * 16, 30 * 8, 1 * 16 }, + u"AFS_????", RecordView::FileType::WAV, 4096, 4 + }; + + Console console { + { 0, 2 * 16, 240, 240 } + }; + + void update_freq(rf::Frequency f); + + std::unique_ptr logger { }; +}; + +class APRSRXView : public View { +public: + APRSRXView(NavigationView& nav); + ~APRSRXView(); + + void focus() override; + + std::string title() const override { return "APRS RX"; }; + +private: + NavigationView& nav_; + Rect view_rect = { 0, 3 * 8, 240, 280 }; + + APRSRxView view_stream { nav_, view_rect }; + APRSTableView view_table { nav_, view_rect }; + + TabView tab_view { + { "Stream", Color::cyan(), &view_stream }, + { "List", Color::yellow(), &view_table } + }; + + MessageHandlerRegistration message_handler_packet { + Message::ID::APRSPacket, + [this](Message* const p) { + const auto message = static_cast(p); + this->view_stream.on_packet(message); + this->view_table.on_pkt(message); + } + }; +}; + +} /* namespace ui */ + +#endif/*__UI_APRS_RX_H__*/ diff --git a/firmware/application/baseband_api.cpp b/firmware/application/baseband_api.cpp index 2bff3c53..e570458a 100644 --- a/firmware/application/baseband_api.cpp +++ b/firmware/application/baseband_api.cpp @@ -130,6 +130,13 @@ void set_afsk(const uint32_t baudrate, const uint32_t word_length, const uint32_ send_message(&message); } +void set_aprs(const uint32_t baudrate) { + const APRSRxConfigureMessage message { + baudrate + }; + send_message(&message); +} + void set_btle(const uint32_t baudrate, const uint32_t word_length, const uint32_t trigger_value, const bool trigger_word) { const BTLERxConfigureMessage message { baudrate, diff --git a/firmware/application/baseband_api.hpp b/firmware/application/baseband_api.hpp index 43d0eebe..4d19e800 100644 --- a/firmware/application/baseband_api.hpp +++ b/firmware/application/baseband_api.hpp @@ -68,6 +68,7 @@ void set_afsk_data(const uint32_t afsk_samples_per_bit, const uint32_t afsk_phas const uint8_t afsk_repeat, const uint32_t afsk_bw, const uint8_t symbol_count); void kill_afsk(); void set_afsk(const uint32_t baudrate, const uint32_t word_length, const uint32_t trigger_value, const bool trigger_word); +void set_aprs(const uint32_t baudrate); void set_btle(const uint32_t baudrate, const uint32_t word_length, const uint32_t trigger_value, const bool trigger_word); diff --git a/firmware/application/recent_entries.hpp b/firmware/application/recent_entries.hpp index 90fd5203..661d7660 100644 --- a/firmware/application/recent_entries.hpp +++ b/firmware/application/recent_entries.hpp @@ -179,6 +179,24 @@ public: } } } + else if( event == ui::KeyEvent::Up ) { + if(selected_key == recent.front().key()){ + return false; + } + else { + advance(-1); + return true; + } + } + else if( event == ui::KeyEvent::Down ) { + if( selected_key == recent.back().key()) { + return false; + } + else { + advance(1); + return true; + } + } return false; } @@ -264,7 +282,7 @@ public: // TODO: What happens here shouldn't matter if I do proper damage detection! } - void on_focus() override { + void focus() override { _table.focus(); } diff --git a/firmware/application/ui/ui_geomap.cpp b/firmware/application/ui/ui_geomap.cpp index 91fcb545..0f4809dc 100644 --- a/firmware/application/ui/ui_geomap.cpp +++ b/firmware/application/ui/ui_geomap.cpp @@ -264,8 +264,13 @@ void GeoMapView::focus() { void GeoMapView::update_position(float lat, float lon, uint16_t angle) { lat_ = lat; lon_ = lon; + + // Stupid hack to avoid an event loop + geopos.set_report_change(false); geopos.set_lat(lat_); geopos.set_lon(lon_); + geopos.set_report_change(true); + geomap.set_angle(angle); geomap.move(lon_, lat_); geomap.set_dirty(); diff --git a/firmware/application/ui_navigation.cpp b/firmware/application/ui_navigation.cpp index 9c583aed..c44e7b6e 100644 --- a/firmware/application/ui_navigation.cpp +++ b/firmware/application/ui_navigation.cpp @@ -34,6 +34,7 @@ #include "ui_adsb_rx.hpp" #include "ui_adsb_tx.hpp" #include "ui_afsk_rx.hpp" +#include "ui_aprs_rx.hpp" #include "ui_btle_rx.hpp" #include "ui_nrf_rx.hpp" #include "ui_aprs_tx.hpp" @@ -468,7 +469,8 @@ ReceiversMenuView::ReceiversMenuView(NavigationView& nav) { { "POCSAG", ui::Color::green(), &bitmap_icon_pocsag, [&nav](){ nav.push(); } }, { "Radiosnde", ui::Color::green(), &bitmap_icon_sonde, [&nav](){ nav.push(); } }, { "TPMS Cars", ui::Color::green(), &bitmap_icon_tpms, [&nav](){ nav.push(); } }, - /*{ "APRS", ui::Color::dark_grey(), &bitmap_icon_aprs, [&nav](){ nav.push(); } }, + { "APRS", ui::Color::green(), &bitmap_icon_aprs, [&nav](){ nav.push(); } } + /* { "DMR", ui::Color::dark_grey(), &bitmap_icon_dmr, [&nav](){ nav.push(); } }, { "SIGFOX", ui::Color::dark_grey(), &bitmap_icon_fox, [&nav](){ nav.push(); } }, // SIGFRXView { "LoRa", ui::Color::dark_grey(), &bitmap_icon_lora, [&nav](){ nav.push(); } }, diff --git a/firmware/baseband/CMakeLists.txt b/firmware/baseband/CMakeLists.txt index c4aafa56..0344ac68 100644 --- a/firmware/baseband/CMakeLists.txt +++ b/firmware/baseband/CMakeLists.txt @@ -312,6 +312,14 @@ set(MODE_CPPSRC proc_afskrx.cpp ) DeclareTargets(PAFR afskrx) + +### APRS RX + +set(MODE_CPPSRC + proc_aprsrx.cpp +) +DeclareTargets(PAPR aprsrx) + ### NRF RX set(MODE_CPPSRC diff --git a/firmware/baseband/proc_aprsrx.cpp b/firmware/baseband/proc_aprsrx.cpp new file mode 100644 index 00000000..56249ce1 --- /dev/null +++ b/firmware/baseband/proc_aprsrx.cpp @@ -0,0 +1,258 @@ +/* + * Copyright (C) 2015 Jared Boone, ShareBrained Technology, Inc. + * Copyright (C) 2016 Furrtek + * + * 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_aprsrx.hpp" +#include "portapack_shared_memory.hpp" + +#include "event_m4.hpp" + +#include "stdio.h" + +void APRSRxProcessor::execute(const buffer_c8_t& buffer) { + // This is called at 3072000 / 2048 = 1500Hz + + if (!configured) return; + + // FM demodulation + const auto decim_0_out = decim_0.execute(buffer, dst_buffer); // 2048 / 8 = 256 (512 I/Q samples) + const auto decim_1_out = decim_1.execute(decim_0_out, dst_buffer); // 256 / 8 = 32 (64 I/Q samples) + const auto channel_out = channel_filter.execute(decim_1_out, dst_buffer); // 32 / 2 = 16 (32 I/Q samples) + + feed_channel_stats(channel_out); + + auto audio = demod.execute(channel_out, audio_buffer); + + audio_output.write(audio); + + // Audio signal processing + for (size_t c = 0; c < audio.count; c++) { + + const int32_t sample_int = audio.p[c] * 32768.0f; + int32_t current_sample = __SSAT(sample_int, 16); + + current_sample /= 128; + + // Delay line put + delay_line[delay_line_index & 0x3F] = current_sample; + + // Delay line get, and LPF + sample_mixed = (delay_line[(delay_line_index - (samples_per_bit/2)) & 0x3F] * current_sample) / 4; + sample_filtered = prev_mixed + sample_mixed + (prev_filtered / 2); + + delay_line_index++; + + prev_filtered = sample_filtered; + prev_mixed = sample_mixed; + + // Slice + sample_bits <<= 1; + + uint8_t bit = (sample_filtered < -20) ? 1 : 0; + sample_bits |= bit; + +/* + int16_t scaled = bit == 1 ? 32767 : -32767; + + if( stream ) { + const size_t bytes_to_write = sizeof(scaled) * 1; + const auto result = stream->write(&scaled, bytes_to_write); + } +*/ + + // Check for "clean" transition: either 0011 or 1100 + if ((((sample_bits >> 2) ^ sample_bits) & 3) == 3) { + // Adjust phase + if (phase < 0x8000) + phase += 0x800; // Is this a proper value ? + else + phase -= 0x800; + } + + phase += phase_inc; + + if (phase >= 0x10000) { + phase &= 0xFFFF; + + if (true) { + uint8_t bit; + if(__builtin_popcount(sample_bits & 0xFF) >= 0x05){ + bit = 0x1; + } + else { + bit = 0x0; + } + + if(parse_bit(bit)){ + parse_packet(); + } + } + } + } +} + +void APRSRxProcessor::parse_packet(){ + //validate crc + if(packet_buffer_size >= aprs::APRS_MIN_LENGTH){ + uint16_t crc = 0xFFFF; + + for(size_t i = 0; i < packet_buffer_size; i++){ + uint8_t byte = packet_buffer[i]; + crc = ((crc >> 8) ^ crc_ccitt_tab[(crc ^ byte) & 0xFF]) & 0xFFFF; + } + + if(crc == 0xF0B8){ + parse_ax25(); + } + } +} + +void APRSRxProcessor::parse_ax25(){ + aprs_packet.clear(); + aprs_packet.set_valid_checksum(true); + + for(size_t i = 0; i < packet_buffer_size; i++){ + aprs_packet.set(i, packet_buffer[i]); + } + + APRSPacketMessage packet_message { aprs_packet }; + shared_memory.application_queue.push(packet_message); +} + +bool APRSRxProcessor::parse_bit(const uint8_t current_bit){ + uint8_t decoded_bit = ~(current_bit ^ last_bit) & 0x1; + last_bit = current_bit; + + int16_t log = decoded_bit == 0 ? -32768 : 32767; + + //if( stream ) { + // const size_t bytes_to_write = sizeof(log) * 1; +// const auto result = stream->write(&log, bytes_to_write); +// } + + if(decoded_bit & 0x1){ + if(ones_count < 8){ + ones_count++; + } + } + else { + if(ones_count > 6){ //not valid + state = WAIT_FLAG; + current_byte = 0; + ones_count = 0; + byte_index = 0; + packet_buffer_size = 0; + return false; + } + else if(ones_count == 6){ //flag + bool done = false; + if(state == IN_FRAME){ + done = true; + } + else { + packet_buffer_size = 0; + } + state = WAIT_FRAME; + current_byte = 0; + ones_count = 0; + byte_index = 0; + + return done; + } + else if(ones_count == 5){ //bit stuff + ones_count = 0; + return false; + } + else { + ones_count = 0; + } + } + + //store + current_byte = current_byte >> 1; + current_byte |= (decoded_bit == 0x1 ? 0x80 : 0x0); + byte_index++; + + if(byte_index >= 8){ + byte_index = 0; + if(state == WAIT_FRAME){ + state = IN_FRAME; + } + + if(state == IN_FRAME){ + if(packet_buffer_size + 1 >= 256){ + state = WAIT_FLAG; + current_byte = 0; + ones_count = 0; + byte_index = 0; + packet_buffer_size = 0; + return false; + } + packet_buffer[packet_buffer_size++] = current_byte; + } + } + + return false; +} + +void APRSRxProcessor::on_message(const Message* const message) { + if (message->id == Message::ID::APRSRxConfigure) + configure(*reinterpret_cast(message)); + if(message->id == Message::ID::CaptureConfig) + capture_config(*reinterpret_cast(message)); +} + +void APRSRxProcessor::capture_config(const CaptureConfigMessage& message) { + if( message.config ) { + //stream = std::make_unique(message.config); + audio_output.set_stream(std::make_unique(message.config)); + } else { + //stream.reset(); + audio_output.set_stream(nullptr); + } +} + +void APRSRxProcessor::configure(const APRSRxConfigureMessage& message) { + decim_0.configure(taps_11k0_decim_0.taps, 33554432); + decim_1.configure(taps_11k0_decim_1.taps, 131072); + channel_filter.configure(taps_11k0_channel.taps, 2); + demod.configure(audio_fs, 5000); + + audio_output.configure(audio_24k_hpf_300hz_config, audio_24k_deemph_300_6_config, 0); + + samples_per_bit = audio_fs / message.baudrate; + + phase_inc = (0x10000 * message.baudrate) / audio_fs; + phase = 0; + + // Delay line + delay_line_index = 0; + + state = WAIT_FLAG; + + configured = true; +} + +int main() { + EventDispatcher event_dispatcher { std::make_unique() }; + event_dispatcher.run(); + return 0; +} diff --git a/firmware/baseband/proc_aprsrx.hpp b/firmware/baseband/proc_aprsrx.hpp new file mode 100644 index 00000000..2d3c4568 --- /dev/null +++ b/firmware/baseband/proc_aprsrx.hpp @@ -0,0 +1,148 @@ +/* + * Copyright (C) 2015 Jared Boone, ShareBrained Technology, Inc. + * Copyright (C) 2016 Furrtek + * + * 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_APRSRX_H__ +#define __PROC_APRSRX_H__ + +#include "baseband_processor.hpp" +#include "baseband_thread.hpp" +#include "rssi_thread.hpp" + +#include "dsp_decimate.hpp" +#include "dsp_demodulate.hpp" +#include "stream_input.hpp" + +#include "audio_output.hpp" + +#include "fifo.hpp" +#include "message.hpp" + +#include "aprs_packet.hpp" + +static uint16_t crc_ccitt_tab[256] = { + 0x0000, 0x1189, 0x2312, 0x329b, 0x4624, 0x57ad, 0x6536, 0x74bf, + 0x8c48, 0x9dc1, 0xaf5a, 0xbed3, 0xca6c, 0xdbe5, 0xe97e, 0xf8f7, + 0x1081, 0x0108, 0x3393, 0x221a, 0x56a5, 0x472c, 0x75b7, 0x643e, + 0x9cc9, 0x8d40, 0xbfdb, 0xae52, 0xdaed, 0xcb64, 0xf9ff, 0xe876, + 0x2102, 0x308b, 0x0210, 0x1399, 0x6726, 0x76af, 0x4434, 0x55bd, + 0xad4a, 0xbcc3, 0x8e58, 0x9fd1, 0xeb6e, 0xfae7, 0xc87c, 0xd9f5, + 0x3183, 0x200a, 0x1291, 0x0318, 0x77a7, 0x662e, 0x54b5, 0x453c, + 0xbdcb, 0xac42, 0x9ed9, 0x8f50, 0xfbef, 0xea66, 0xd8fd, 0xc974, + 0x4204, 0x538d, 0x6116, 0x709f, 0x0420, 0x15a9, 0x2732, 0x36bb, + 0xce4c, 0xdfc5, 0xed5e, 0xfcd7, 0x8868, 0x99e1, 0xab7a, 0xbaf3, + 0x5285, 0x430c, 0x7197, 0x601e, 0x14a1, 0x0528, 0x37b3, 0x263a, + 0xdecd, 0xcf44, 0xfddf, 0xec56, 0x98e9, 0x8960, 0xbbfb, 0xaa72, + 0x6306, 0x728f, 0x4014, 0x519d, 0x2522, 0x34ab, 0x0630, 0x17b9, + 0xef4e, 0xfec7, 0xcc5c, 0xddd5, 0xa96a, 0xb8e3, 0x8a78, 0x9bf1, + 0x7387, 0x620e, 0x5095, 0x411c, 0x35a3, 0x242a, 0x16b1, 0x0738, + 0xffcf, 0xee46, 0xdcdd, 0xcd54, 0xb9eb, 0xa862, 0x9af9, 0x8b70, + 0x8408, 0x9581, 0xa71a, 0xb693, 0xc22c, 0xd3a5, 0xe13e, 0xf0b7, + 0x0840, 0x19c9, 0x2b52, 0x3adb, 0x4e64, 0x5fed, 0x6d76, 0x7cff, + 0x9489, 0x8500, 0xb79b, 0xa612, 0xd2ad, 0xc324, 0xf1bf, 0xe036, + 0x18c1, 0x0948, 0x3bd3, 0x2a5a, 0x5ee5, 0x4f6c, 0x7df7, 0x6c7e, + 0xa50a, 0xb483, 0x8618, 0x9791, 0xe32e, 0xf2a7, 0xc03c, 0xd1b5, + 0x2942, 0x38cb, 0x0a50, 0x1bd9, 0x6f66, 0x7eef, 0x4c74, 0x5dfd, + 0xb58b, 0xa402, 0x9699, 0x8710, 0xf3af, 0xe226, 0xd0bd, 0xc134, + 0x39c3, 0x284a, 0x1ad1, 0x0b58, 0x7fe7, 0x6e6e, 0x5cf5, 0x4d7c, + 0xc60c, 0xd785, 0xe51e, 0xf497, 0x8028, 0x91a1, 0xa33a, 0xb2b3, + 0x4a44, 0x5bcd, 0x6956, 0x78df, 0x0c60, 0x1de9, 0x2f72, 0x3efb, + 0xd68d, 0xc704, 0xf59f, 0xe416, 0x90a9, 0x8120, 0xb3bb, 0xa232, + 0x5ac5, 0x4b4c, 0x79d7, 0x685e, 0x1ce1, 0x0d68, 0x3ff3, 0x2e7a, + 0xe70e, 0xf687, 0xc41c, 0xd595, 0xa12a, 0xb0a3, 0x8238, 0x93b1, + 0x6b46, 0x7acf, 0x4854, 0x59dd, 0x2d62, 0x3ceb, 0x0e70, 0x1ff9, + 0xf78f, 0xe606, 0xd49d, 0xc514, 0xb1ab, 0xa022, 0x92b9, 0x8330, + 0x7bc7, 0x6a4e, 0x58d5, 0x495c, 0x3de3, 0x2c6a, 0x1ef1, 0x0f78 +}; + +class APRSRxProcessor : public BasebandProcessor { +public: + void execute(const buffer_c8_t& buffer) override; + + void on_message(const Message* const message) override; + +private: + static constexpr size_t baseband_fs = 3072000; + static constexpr size_t audio_fs = baseband_fs / 8 / 8 / 2; + + size_t samples_per_bit { }; + + enum State { + WAIT_FLAG, + WAIT_FRAME, + IN_FRAME + }; + + BasebandThread baseband_thread { baseband_fs, this, NORMALPRIO + 20, baseband::Direction::Receive }; + RSSIThread rssi_thread { NORMALPRIO + 10 }; + + std::array dst { }; + const buffer_c16_t dst_buffer { + dst.data(), + dst.size() + }; + std::array audio { }; + const buffer_f32_t audio_buffer { + audio.data(), + audio.size() + }; + + // Array size ok down to 375 bauds (24000 / 375) + std::array delay_line { 0 }; + + dsp::decimate::FIRC8xR16x24FS4Decim8 decim_0 { }; + dsp::decimate::FIRC16xR16x32Decim8 decim_1 { }; + dsp::decimate::FIRAndDecimateComplex channel_filter { }; + + std::unique_ptr stream { }; + + dsp::demodulate::FM demod { }; + + AudioOutput audio_output { }; + + State state { }; + size_t delay_line_index { }; + uint32_t bit_counter { 0 }; + uint32_t word_bits { 0 }; + uint32_t sample_bits { 0 }; + uint32_t phase { }, phase_inc { }; + int32_t sample_mixed { }, prev_mixed { }, sample_filtered { }, prev_filtered { }; + uint8_t last_bit; + uint8_t ones_count = 0; + uint8_t current_byte = 0; + uint8_t byte_index = 0; + uint8_t packet_buffer[256]; + size_t packet_buffer_size = 0; + + bool configured { false }; + bool wait_start { }; + bool bit_value { }; + + aprs::APRSPacket aprs_packet; + + void configure(const APRSRxConfigureMessage& message); + void capture_config(const CaptureConfigMessage& message); + void parse_packet(); + bool parse_bit(const uint8_t bit); + void parse_ax25(); +}; + +#endif/*__PROC_TPMS_H__*/ diff --git a/firmware/common/aprs_packet.cpp b/firmware/common/aprs_packet.cpp new file mode 100644 index 00000000..e7347ca0 --- /dev/null +++ b/firmware/common/aprs_packet.cpp @@ -0,0 +1,27 @@ +/* + * Copyright (C) 2015 Jared Boone, ShareBrained Technology, Inc. + * Copyright (C) 2016 Furrtek + * + * 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 "aprs_packet.hpp" + +namespace aprs { + +} /* namespace zwave */ diff --git a/firmware/common/aprs_packet.hpp b/firmware/common/aprs_packet.hpp new file mode 100644 index 00000000..e3d76ece --- /dev/null +++ b/firmware/common/aprs_packet.hpp @@ -0,0 +1,495 @@ +/* + * Copyright (C) 2015 Jared Boone, ShareBrained Technology, Inc. + * Copyright (C) 2016 Furrtek + * + * 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 __APRS_PACKET_H__ +#define __APRS_PACKET_H__ + +#include +#include +#include + +#include "baseband.hpp" + +namespace aprs { + +const int APRS_MIN_LENGTH = 18; //14 bytes address, control byte and pid. 2 CRC. + +struct aprs_pos{ + float latitude; + float longitude; + uint8_t symbol_code; + uint8_t sym_table_id; +}; + +enum ADDRESS_TYPE { + SOURCE, + DESTINATION, + REPEATER +}; + +class APRSPacket { +public: + void set_timestamp(const Timestamp& value) { + timestamp_ = value; + } + + Timestamp timestamp() const { + return timestamp_; + } + + void set(const size_t index, const uint8_t data) { + payload[index] = data; + + if(index + 1 > payload_size){ + payload_size = index + 1; + } + } + + uint32_t operator[](const size_t index) const { + return payload[index]; + } + + + uint8_t size() const { + return payload_size; + } + + void set_valid_checksum(const bool valid) { + valid_checksum = valid; + } + + bool is_valid_checksum() const { + return valid_checksum; + } + + uint64_t get_source(){ + uint64_t source = 0x0; + + for(uint8_t i = SOURCE_START; i < SOURCE_START + ADDRESS_SIZE; i++){ + source |= ( ((uint64_t)payload[i]) << ((i - SOURCE_START) * 8)); + } + + return source; + } + + std::string get_source_formatted(){ + parse_address(SOURCE_START, SOURCE); + return std::string(address_buffer); + } + + std::string get_destination_formatted(){ + parse_address(DESTINATION_START, DESTINATION); + return std::string(address_buffer); + } + + std::string get_digipeaters_formatted(){ + uint8_t position = DIGIPEATER_START; + bool has_more = parse_address(SOURCE_START, REPEATER); + + std::string repeaters = ""; + while(has_more){ + has_more = parse_address(position, REPEATER); + repeaters += std::string(address_buffer); + + position += ADDRESS_SIZE; + + if(has_more){ + repeaters += ">"; + } + } + + return repeaters; + } + + uint8_t get_number_of_digipeaters(){ + uint8_t position = DIGIPEATER_START; + bool has_more = parse_address(SOURCE_START, REPEATER); + uint8_t repeaters = 0; + while(has_more){ + has_more = parse_address(position, REPEATER); + position += ADDRESS_SIZE; + repeaters++; + } + + return repeaters; + } + + uint8_t get_information_start_index(){ + return DIGIPEATER_START + (get_number_of_digipeaters() * ADDRESS_SIZE) + 2; + } + + std::string get_information_text_formatted(){ + std::string information_text = ""; + for(uint8_t i = get_information_start_index(); i < payload_size - 2; i++){ + information_text += payload[i]; + } + + return information_text; + } + + std::string get_stream_text(){ + std::string stream = get_source_formatted() + ">" + get_destination_formatted() + ";" + get_digipeaters_formatted() + ";" + get_information_text_formatted(); + + return stream; + } + + char get_data_type_identifier(){ + char ident = '\0'; + for(uint8_t i = get_information_start_index(); i < payload_size - 2; i++){ + ident = payload[i]; + break; + } + return ident; + } + + bool has_position(){ + char ident = get_data_type_identifier(); + + return + ident == '!' || + ident == '=' || + ident == '/' || + ident == '@' || + ident == ';' || + ident == '`' || + ident == '\''|| + ident == 0x1d|| + ident == 0x1c; + } + + aprs_pos get_position(){ + aprs::aprs_pos pos; + + char ident = get_data_type_identifier(); + std::string info_text = get_information_text_formatted(); + + std::string lat_str, lng_str; + char first; + //bool supports_compression = true; + bool is_mic_e_format = false; + std::string::size_type start; + + switch(ident){ + case '/': + case '@': + start = 8; + break; + case '=': + case '!': + start = 1; + break; + case ';': + start = 18; + //supports_compression = false; + break; + case '`': + case '\'': + case 0x1c: + case 0x1d: + is_mic_e_format = true; + break; + default: + return pos; + } + if(is_mic_e_format){ + parse_mic_e_format(pos); + } + else { + if(start < info_text.size()){ + first = info_text.at(start); + + if(std::isdigit(first)){ + if(start + 18 < info_text.size()){ + lat_str = info_text.substr(start, 8); + pos.sym_table_id = info_text.at(start + 8); + lng_str = info_text.substr(start + 9, 9); + pos.symbol_code = info_text.at(start + 18); + + pos.latitude = parse_lat_str(lat_str); + pos.longitude = parse_lng_str(lng_str); + } + + } + else { + if(start + 9 < info_text.size()){ + pos.sym_table_id = info_text.at(start); + lat_str = info_text.substr(start + 1, 4); + lng_str = info_text.substr(start + 5, 4); + pos.symbol_code = info_text.at(start + 9); + + pos.latitude = parse_lat_str_cmp(lat_str); + pos.longitude = parse_lng_str_cmp(lng_str); + } + } + } + } + + return pos; + } + + void clear() { + payload_size = 0; + } + +private: + const uint8_t DIGIPEATER_START = 14; + const uint8_t SOURCE_START = 7; + const uint8_t DESTINATION_START = 0; + const uint8_t ADDRESS_SIZE = 7; + + bool valid_checksum = false; + uint8_t payload[256]; + char address_buffer[15]; + uint8_t payload_size; + Timestamp timestamp_ { }; + + float parse_lat_str_cmp(const std::string& lat_str){ + return 90.0 - ( (lat_str.at(0) - 33) * (91*91*91) + (lat_str.at(1) - 33) * (91*91) + (lat_str.at(2) - 33) * 91 + (lat_str.at(3)) ) / 380926.0; + } + + float parse_lng_str_cmp(const std::string& lng_str){ + return -180.0 + ( (lng_str.at(0) - 33) * (91*91*91) + (lng_str.at(1) - 33) * (91*91) + (lng_str.at(2) - 33) * 91 + (lng_str.at(3)) ) / 190463.0; + } + + uint8_t parse_digits(const std::string& str){ + if(str.at(0) == ' '){ + return 0; + } + uint8_t end = str.find_last_not_of(' ') + 1; + std::string sub = str.substr(0, end); + + if(!is_digits(sub)){ + return 0; + } + else { + return std::stoi(sub); + } + } + + bool is_digits(const std::string& str){ + return str.find_last_not_of("0123456789") == std::string::npos; + } + + float parse_lat_str(const std::string& lat_str){ + float lat = 0.0; + + std::string str_lat_deg = lat_str.substr(0, 2); + std::string str_lat_min = lat_str.substr(2, 2); + std::string str_lat_hund = lat_str.substr(5, 2); + std::string dir = lat_str.substr(7, 1); + + uint8_t lat_deg = parse_digits(str_lat_deg); + uint8_t lat_min = parse_digits(str_lat_min); + uint8_t lat_hund = parse_digits(str_lat_hund); + + lat += lat_deg; + lat += (lat_min + (lat_hund/ 100.0))/60.0; + + if(dir.c_str()[0] == 'S'){ + lat = -lat; + } + + return lat; + + } + + float parse_lng_str(std::string& lng_str){ + float lng = 0.0; + + std::string str_lng_deg = lng_str.substr(0, 3); + std::string str_lng_min = lng_str.substr(3, 2); + std::string str_lng_hund = lng_str.substr(6, 2); + std::string dir = lng_str.substr(8, 1); + + uint8_t lng_deg = parse_digits(str_lng_deg); + uint8_t lng_min = parse_digits(str_lng_min); + uint8_t lng_hund = parse_digits(str_lng_hund); + + lng += lng_deg; + lng += (lng_min + (lng_hund/ 100.0))/60.0; + + if(dir.c_str()[0] == 'W'){ + lng = -lng; + } + + return lng; + + } + + void parse_mic_e_format(aprs::aprs_pos& pos){ + std::string lat_str = ""; + std::string lng_str = ""; + + bool is_north; + bool is_west; + uint8_t lng_offset; + for(uint8_t i = DESTINATION_START; i < DESTINATION_START + ADDRESS_SIZE - 1; i++){ + uint8_t ascii = payload[i] >> 1; + + lat_str += get_mic_e_lat_digit(ascii); + + if(i - DESTINATION_START == 3){ + lat_str += "."; + } + if(i - DESTINATION_START == 3){ + is_north = is_mic_e_lat_N(ascii); + } + if(i - DESTINATION_START == 4){ + lng_offset = get_mic_e_lng_offset(ascii); + } + if(i - DESTINATION_START == 5){ + is_west = is_mic_e_lng_W(ascii); + } + } + if(is_north){ + lat_str += "N"; + } + else { + lat_str += "S"; + } + + pos.latitude = parse_lat_str(lat_str); + + float lng = 0.0; + uint8_t information_start = get_information_start_index() + 1; + for(uint8_t i = information_start; i < information_start + 3 && i < payload_size - 2; i++){ + uint8_t ascii = payload[i]; + + if(i - information_start == 0){ //deg + ascii -=28; + ascii += lng_offset; + + if(ascii >= 180 && ascii <= 189){ + ascii -= 80; + } + else if (ascii >= 190 && ascii <= 199){ + ascii -= 190; + } + + lng += ascii; + } + else if(i - information_start == 1){ //min + ascii -= 28; + if(ascii >= 60){ + ascii -= 60; + } + lng += ascii/60.0; + } + else if(i - information_start == 2){ //hundredth minutes + ascii -= 28; + + lng += (ascii/100.0)/60.0; + } + } + + if(is_west){ + lng = -lng; + } + + pos.longitude = lng; + + } + + uint8_t get_mic_e_lat_digit(uint8_t ascii){ + if(ascii >= '0' && ascii <= '9'){ + return ascii; + } + if(ascii >= 'A' && ascii <= 'J'){ + return ascii - 17; + } + if(ascii >= 'P' && ascii <='Y'){ + return ascii - 32; + } + if(ascii == 'K' || ascii == 'L' || ascii == 'Z'){ + return ' '; + } + + return '\0'; + } + + bool is_mic_e_lat_N(uint8_t ascii){ + if(ascii >= 'P' && ascii <='Z'){ + return true; + } + return false; //not technical definition, but the other case is invalid + } + + bool is_mic_e_lng_W(uint8_t ascii){ + if(ascii >= 'P' && ascii <='Z'){ + return true; + } + return false; //not technical definition, but the other case is invalid + } + + uint8_t get_mic_e_lng_offset(uint8_t ascii){ + if(ascii >= 'P' && ascii <='Z'){ + return 100; + } + return 0; //not technical definition, but the other case is invalid + } + + bool parse_address(uint8_t start, ADDRESS_TYPE address_type){ + uint8_t byte = 0; + uint8_t has_more = false; + uint8_t ssid = 0; + uint8_t buffer_index = 0; + + for(uint8_t i = start; i < start + ADDRESS_SIZE && i < payload_size - 2; i++){ + byte = payload[i]; + + if(i - start == 6){ + has_more = (byte & 0x1) == 0; + ssid = (byte >> 1) & 0x0F; + + if(ssid != 0 || address_type == REPEATER){ + address_buffer[buffer_index++] = '-'; + + if(ssid < 10){ + address_buffer[buffer_index++] = '0' + ssid; + address_buffer[buffer_index++] = '\0'; + } + else { + address_buffer[buffer_index++] = '1'; + address_buffer[buffer_index++] = '0' + ssid - 10; + address_buffer[buffer_index++] = '\0'; + } + } + else { + address_buffer[buffer_index++] = '\0'; + } + } + else { + byte >>= 1; + + if(byte != ' '){ + address_buffer[buffer_index++] = byte; + } + } + } + + return has_more; + } +}; + +} /* namespace aprs */ + +#endif/*__APRS_PACKET_H__*/ diff --git a/firmware/common/message.hpp b/firmware/common/message.hpp index 32a8fc7b..239c1530 100644 --- a/firmware/common/message.hpp +++ b/firmware/common/message.hpp @@ -36,6 +36,7 @@ #include "adsb_frame.hpp" #include "ert_packet.hpp" #include "pocsag_packet.hpp" +#include "aprs_packet.hpp" #include "sonde_packet.hpp" #include "tpms_packet.hpp" #include "jammer.hpp" @@ -111,6 +112,8 @@ public: AudioLevelReport = 51, CodedSquelch = 52, AudioSpectrum = 53, + APRSPacket = 54, + APRSRxConfigure = 55, MAX }; @@ -725,6 +728,18 @@ public: const bool trigger_word; }; +class APRSRxConfigureMessage : public Message { +public: + constexpr APRSRxConfigureMessage( + const uint32_t baudrate + ) : Message { ID::APRSRxConfigure }, + baudrate(baudrate) + { + } + + const uint32_t baudrate; +}; + class BTLERxConfigureMessage : public Message { public: constexpr BTLERxConfigureMessage( @@ -1002,6 +1017,19 @@ public: const bool phase; }; +class APRSPacketMessage : public Message { +public: + constexpr APRSPacketMessage( + const aprs::APRSPacket& packet + ) : Message { ID::APRSPacket }, + packet { packet } + { + } + + aprs::APRSPacket packet; +}; + + class ADSBConfigureMessage : public Message { public: constexpr ADSBConfigureMessage( diff --git a/firmware/common/spi_image.hpp b/firmware/common/spi_image.hpp index 5ff88054..758429c8 100644 --- a/firmware/common/spi_image.hpp +++ b/firmware/common/spi_image.hpp @@ -77,6 +77,7 @@ private: constexpr image_tag_t image_tag_acars { 'P', 'A', 'C', 'A' }; constexpr image_tag_t image_tag_adsb_rx { 'P', 'A', 'D', 'R' }; constexpr image_tag_t image_tag_afsk_rx { 'P', 'A', 'F', 'R' }; +constexpr image_tag_t image_tag_aprs_rx { 'P', 'A', 'P', 'R' }; constexpr image_tag_t image_tag_btle_rx { 'P', 'B', 'T', 'R' }; constexpr image_tag_t image_tag_nrf_rx { 'P', 'N', 'R', 'R' }; constexpr image_tag_t image_tag_ais { 'P', 'A', 'I', 'S' }; diff --git a/firmware/common/ui_widget.cpp b/firmware/common/ui_widget.cpp index 12bf1927..85ad91f1 100644 --- a/firmware/common/ui_widget.cpp +++ b/firmware/common/ui_widget.cpp @@ -597,11 +597,14 @@ Console::Console( void Console::clear(bool clear_buffer = false) { if(clear_buffer) buffer.clear(); - - display.fill_rectangle( - screen_rect(), - Color::black() - ); + + if(!hidden() && visible()){ + display.fill_rectangle( + screen_rect(), + Color::black() + ); + } + pos = { 0, 0 }; } @@ -648,8 +651,7 @@ void Console::write(std::string message) { } void Console::writeln(std::string message) { - write(message); - write("\n"); + write(message + "\n"); //crlf(); } @@ -658,18 +660,31 @@ void Console::paint(Painter&) { } void Console::on_show() { - const auto screen_r = screen_rect(); - display.scroll_set_area(screen_r.top(), screen_r.bottom()); - display.scroll_set_position(0); + enable_scrolling(true); clear(); //visible = true; } +bool Console::scrolling_enabled = false; + +void Console::enable_scrolling(bool enable){ + if(enable){ + const auto screen_r = screen_rect(); + display.scroll_set_area(screen_r.top(), screen_r.bottom()); + display.scroll_set_position(0); + scrolling_enabled = true; + } + else { + display.scroll_disable(); + scrolling_enabled = false; + } +} + void Console::on_hide() { /* TODO: Clear region to eliminate brief flash of content at un-shifted * position? */ - display.scroll_disable(); + enable_scrolling(false); //visible = false; } @@ -682,6 +697,9 @@ void Console::crlf() { pos = { 0, pos.y() + line_height }; const int32_t y_excess = pos.y() + line_height - sr.height(); if( y_excess > 0 ) { + if(!scrolling_enabled){ + enable_scrolling(true); + } display.scroll(-y_excess); pos = { pos.x(), pos.y() - y_excess }; @@ -1293,9 +1311,9 @@ size_t OptionsField::selected_index_value() const { return options[selected_index_].second; } -void OptionsField::set_selected_index(const size_t new_index) { +void OptionsField::set_selected_index(const size_t new_index, bool trigger_change) { if( new_index < options.size() ) { - if( new_index != selected_index() ) { + if( new_index != selected_index() || trigger_change) { selected_index_ = new_index; if( on_change ) { on_change(selected_index(), options[selected_index()].second); diff --git a/firmware/common/ui_widget.hpp b/firmware/common/ui_widget.hpp index f66da710..71cd8f91 100644 --- a/firmware/common/ui_widget.hpp +++ b/firmware/common/ui_widget.hpp @@ -325,6 +325,7 @@ public: void paint(Painter&) override; + void enable_scrolling(bool enable); void on_show() override; void on_hide() override; @@ -332,6 +333,7 @@ private: //bool visible = false; Point pos { 0, 0 }; std::string buffer { }; + static bool scrolling_enabled; void crlf(); }; @@ -545,7 +547,7 @@ public: size_t selected_index() const; size_t selected_index_value() const; - void set_selected_index(const size_t new_index); + void set_selected_index(const size_t new_index, bool trigger_change = true); void set_by_value(value_t v);