diff --git a/firmware/application/app_settings.cpp b/firmware/application/app_settings.cpp index acf93b0ff..bbae046f9 100644 --- a/firmware/application/app_settings.cpp +++ b/firmware/application/app_settings.cpp @@ -68,6 +68,9 @@ void BoundSetting::parse(std::string_view value) { as() = (parsed != 0); break; } + case SettingType::Float: + as() = atof(value.data()); + break; }; } @@ -101,6 +104,11 @@ void BoundSetting::write(File& file) const { case SettingType::Bool: file.write(as() ? "1" : "0", 1); break; + + case SettingType::Float: { + auto float_str = to_string_decimal(as(), 6); + file.write(float_str.c_str(), float_str.length()); + } } file.write("\r\n", 2); diff --git a/firmware/application/app_settings.hpp b/firmware/application/app_settings.hpp index d0c30d5a3..4db5caf35 100644 --- a/firmware/application/app_settings.hpp +++ b/firmware/application/app_settings.hpp @@ -52,6 +52,7 @@ class BoundSetting { U8, String, Bool, + Float }; public: @@ -73,6 +74,9 @@ class BoundSetting { BoundSetting(std::string_view name, bool* target) : name_{name}, target_{target}, type_{SettingType::Bool} {} + BoundSetting(std::string_view name, float* target) + : name_{name}, target_{target}, type_{SettingType::Float} {} + std::string_view name() const { return name_; } void parse(std::string_view value); void write(File& file) const; diff --git a/firmware/application/apps/ui_aprs_tx.cpp b/firmware/application/apps/ui_aprs_tx.cpp index 021104437..92713a0b7 100644 --- a/firmware/application/apps/ui_aprs_tx.cpp +++ b/firmware/application/apps/ui_aprs_tx.cpp @@ -29,6 +29,7 @@ #include "baseband_api.hpp" #include "portapack_shared_memory.hpp" #include "portapack_persistent_memory.hpp" +#include "ui_geomap.hpp" #include #include @@ -49,10 +50,24 @@ APRSTXView::~APRSTXView() { void APRSTXView::start_tx() { // TODO: Clean up this API to take string_views to avoid allocations. + std::string new_payload = payload; + + std::string gps = text_gps_coord.get(); + std::string token = "?GPS?"; + size_t pos = 0; + while ((pos = new_payload.find(token, pos)) != std::string::npos) { + new_payload.replace(pos, token.size(), gps); + pos += gps.size(); + } + + text_payload.set(new_payload); + + std::string path = text_path.get(); + make_aprs_frame( sym_source.to_string().c_str(), num_ssid_source.value(), sym_dest.to_string().c_str(), num_ssid_dest.value(), - payload); + new_payload, path); // uint8_t * bb_data_ptr = shared_memory.bb_data.data; // text_payload.set(to_string_hex_array(bb_data_ptr + 56, 15)); @@ -77,6 +92,61 @@ void APRSTXView::on_tx_progress(const uint32_t progress, const bool done) { } } +void APRSTXView::process_coordinates(float lat_, float lon_) { + std::string s; + + char ns = lat_ >= 0 ? 'N' : 'S'; + float lat = (lat_ >= 0) ? lat_ : -lat_; + uint32_t lat_d = (uint32_t)lat; + uint32_t lat_m = (uint32_t)((lat - lat_d) * 6000.0f + 0.5f); + + if (lat_m >= 6000) { + lat_m = 0; + lat_d++; + } + + s += to_string_dec_uint(lat_d, 2, '0'); + s += to_string_dec_uint(lat_m / 100, 2, '0'); + s += "."; + s += to_string_dec_uint(lat_m % 100, 2, '0'); + s += ns; + + s += "/"; + + char ew = lon_ >= 0 ? 'E' : 'W'; + float lon = (lon_ >= 0) ? lon_ : -lon_; + uint32_t lon_d = (uint32_t)lon; + uint32_t lon_m = (uint32_t)((lon - lon_d) * 6000.0f + 0.5f); + + if (lon_m >= 6000) { + lon_m = 0; + lon_d++; + } + + s += to_string_dec_uint(lon_d, 3, '0'); + s += to_string_dec_uint(lon_m / 100, 2, '0'); + s += "."; + s += to_string_dec_uint(lon_m % 100, 2, '0'); + s += ew; + text_gps_coord.set(s); +} + +void APRSTXView::on_gps(const GPSPosDataMessage* message) { + if (manual_gps_mode) { + return; // no more update once set manually + } + if (message->lat < -90.0 || message->lat > 90.0 || + message->lon < -180.0 || message->lon > 180.0) { + return; + } + if (message->lat == 0.0f && message->lon == 0.0f) { + return; + } + last_lat = message->lat; + last_lon = message->lon; + process_coordinates(message->lat, message->lon); +} + APRSTXView::APRSTXView(NavigationView& nav) { baseband::run_image(portapack::spi_flash::image_tag_afsk); @@ -87,8 +157,33 @@ APRSTXView::APRSTXView(NavigationView& nav) { &num_ssid_dest, &text_payload, &button_set, + &text_gps_coord, + &button_mangps, + &gps_is_manual, + &text_path, + &button_setpath, &tx_view}); + sym_source.set_value(symsrc); + num_ssid_source.set_value(ssidsrc); + sym_dest.set_value(symdst); + num_ssid_dest.set_value(ssiddst); + text_payload.set(payload); + text_path.set(path_cache); + + sym_source.on_change = [this](SymField&) { + symsrc = sym_source.to_string(); + }; + num_ssid_source.on_change = [this](int32_t v) { + ssidsrc = v; + }; + sym_dest.on_change = [this](SymField&) { + symdst = sym_dest.to_string(); + }; + num_ssid_dest.on_change = [this](int32_t v) { + ssiddst = v; + }; + button_set.on_select = [this, &nav](Button&) { text_prompt( nav, @@ -99,6 +194,17 @@ APRSTXView::APRSTXView(NavigationView& nav) { text_payload.set(s); }); }; + button_setpath.on_select = [this, &nav](Button&) { + text_prompt( + nav, + path_cache, + 60, + ENTER_KEYBOARD_MODE_ALPHA, + [this](std::string& s) { + text_path.set(s); + path_cache = s; + }); + }; tx_view.on_edit_frequency = [this, &nav]() { auto new_view = nav.push(transmitter_model.target_frequency()); @@ -116,6 +222,23 @@ APRSTXView::APRSTXView(NavigationView& nav) { tx_view.set_transmitting(false); transmitter_model.disable(); }; + + button_mangps.on_select = [this, &nav](Button&) { + nav.push( + 0, + GeoPos::alt_unit::METERS, + GeoPos::spd_unit::HIDDEN, + last_lat, + last_lon, + [this](int32_t, float lat, float lon, int32_t) { + last_lat = lat; + last_lon = lon; + manual_gps_mode = true; + gps_is_manual.set("MANUAL"); + process_coordinates(lat, lon); + }); + }; + // process_coordinates(last_lat, last_lon); //don't load last, so won't confuse users } } /* namespace ui */ diff --git a/firmware/application/apps/ui_aprs_tx.hpp b/firmware/application/apps/ui_aprs_tx.hpp index 18f499441..769a0bcf8 100644 --- a/firmware/application/apps/ui_aprs_tx.hpp +++ b/firmware/application/apps/ui_aprs_tx.hpp @@ -50,59 +50,109 @@ class APRSTXView : public View { 1750000 /* bandwidth */, AFSK_TX_SAMPLERATE /* sampling rate */ }; - app_settings::SettingsManager settings_{ - "tx_aprs", app_settings::Mode::TX}; + std::string symsrc = ""; + std::string symdst = ""; std::string payload{""}; + std::string path_cache = ""; + int32_t ssidsrc = 0; + int32_t ssiddst = 0; + + bool manual_gps_mode = false; + float last_lat = 0.0f; + float last_lon = 0.0f; + + app_settings::SettingsManager settings_{ + "tx_aprs", + app_settings::Mode::TX, + {{"symsrc"sv, &symsrc}, + {"symdst"sv, &symdst}, + {"ssidsrc"sv, &ssidsrc}, + {"ssiddst"sv, &ssiddst}, + {"payload"sv, &payload}, + {"last_lat"sv, &last_lat}, + {"last_lon"sv, &last_lon}, + {"path"sv, &path_cache}}}; void start_tx(); void generate_frame(); void generate_frame_pos(); void on_tx_progress(const uint32_t progress, const bool done); + void on_gps(const GPSPosDataMessage* message); + void process_coordinates(float lat, float lon); Labels labels{ - {{UI_POS_X(0), 1 * 16}, "Source: SSID:", Theme::getInstance()->fg_light->foreground}, // 6 alphanum + SSID - {{UI_POS_X(0), 2 * 16}, " Dest.: SSID:", Theme::getInstance()->fg_light->foreground}, - {{UI_POS_X(0), 4 * 16}, "Info field:", Theme::getInstance()->fg_light->foreground}, + {{UI_POS_X(0), UI_POS_Y(0)}, "Source: SSID:", Theme::getInstance()->fg_light->foreground}, // 6 alphanum + SSID + {{UI_POS_X(0), UI_POS_Y(1)}, " Dest.: SSID:", Theme::getInstance()->fg_light->foreground}, + {{UI_POS_X(0), UI_POS_Y(2)}, "Path: (ex.: WIDE1-1,WIDE2-1)", Theme::getInstance()->fg_light->foreground}, + {{UI_POS_X(0), UI_POS_Y(6)}, "Info field:", Theme::getInstance()->fg_light->foreground}, + {{UI_POS_X(0), UI_POS_Y(10)}, "GPS:", Theme::getInstance()->fg_light->foreground}, + {{UI_POS_X_CENTER(22), UI_POS_Y(14)}, "Use ?GPS? in the info", Theme::getInstance()->fg_light->foreground}, + {{UI_POS_X_CENTER(17), UI_POS_Y(15)}, "as a placeholder.", Theme::getInstance()->fg_light->foreground}, + }; + + Text text_path{ + {UI_POS_X(0), UI_POS_Y(3), UI_POS_MAXWIDTH, UI_POS_HEIGHT(1)}, + "WIDE1-1", + }; + Text gps_is_manual{ + {UI_POS_X(6), UI_POS_Y(10), UI_POS_WIDTH(10), UI_POS_HEIGHT(1)}, + "", }; SymField sym_source{ - {7 * 8, 1 * 16}, + {UI_POS_X(7), UI_POS_Y(0)}, 6, SymField::Type::Alpha}; NumberField num_ssid_source{ - {19 * 8, 1 * 16}, + {UI_POS_X(19), UI_POS_Y(0)}, 2, {0, 15}, 1, ' '}; SymField sym_dest{ - {7 * 8, 2 * 16}, + {UI_POS_X(7), UI_POS_Y(1)}, 6, SymField::Type::Alpha}; NumberField num_ssid_dest{ - {19 * 8, 2 * 16}, + {UI_POS_X(19), UI_POS_Y(1)}, 2, {0, 15}, 1, ' '}; Text text_payload{ - {UI_POS_X(0), 5 * 16, screen_width, 16}, - "-"}; + {UI_POS_X(0), UI_POS_Y(7), UI_POS_MAXWIDTH, UI_POS_HEIGHT(1)}, + ""}; Button button_set{ - {UI_POS_X(0), 6 * 16, 80, 32}, + {UI_POS_X(0), UI_POS_Y(8), UI_POS_WIDTH(10), UI_POS_HEIGHT(2)}, "Set"}; + Text text_gps_coord{ + {UI_POS_X(0), UI_POS_Y(11), UI_POS_WIDTH(20), UI_POS_HEIGHT(1)}, + "-"}; + + Button button_mangps{ + {UI_POS_X(0), UI_POS_Y(12), UI_POS_WIDTH(14), UI_POS_HEIGHT(2)}, + "Manual GPS"}; + Button button_setpath{ + {UI_POS_X(0), UI_POS_Y(4), UI_POS_WIDTH(14), UI_POS_HEIGHT(2)}, + "Set Path"}; TransmitterView tx_view{ (int16_t)UI_POS_Y_BOTTOM(4), 5000, 0 // disable setting bandwith, since APRS used fixed 10k bandwidth }; + MessageHandlerRegistration message_handler_gps{ + Message::ID::GPSPosData, + [this](Message* const p) { + const auto message = static_cast(p); + this->on_gps(message); + }}; MessageHandlerRegistration message_handler_tx_progress{ Message::ID::TXProgress, [this](const Message* const p) { diff --git a/firmware/application/protocols/aprs.cpp b/firmware/application/protocols/aprs.cpp index 4317e44cb..27bdd0e01 100644 --- a/firmware/application/protocols/aprs.cpp +++ b/firmware/application/protocols/aprs.cpp @@ -31,20 +31,67 @@ using namespace ax25; namespace aprs { -void make_aprs_frame(const char* src_address, const uint32_t src_ssid, const char* dest_address, const uint32_t dest_ssid, const std::string& payload) { +void make_aprs_frame(const char* src_address, const uint32_t src_ssid, const char* dest_address, const uint32_t dest_ssid, const std::string& payload, const std::string& path) { AX25Frame frame; char address[14] = {0}; + memset(address, ' ', 14); - memcpy(&address[0], dest_address, 6); - memcpy(&address[7], src_address, 6); + size_t dest_len = strlen(dest_address); + if (dest_len > 6) dest_len = 6; + memcpy(&address[0], dest_address, dest_len); + size_t src_len = strlen(src_address); + if (src_len > 6) src_len = 6; + memcpy(&address[7], src_address, src_len); // euquiq: According to ax.25 doc section 2.2.13.x.x and 2.4.1.2 - // SSID need bits 5.6 set, so later when shifted it will end up being 011xxxx0 (xxxx = SSID number) - // Notice that if need to signal usage of AX.25 V2.0, (dest_ssid | 112); (MSb will need to be set at the end) - address[6] = (dest_ssid | 48); - address[13] = (src_ssid | 48); + // SSID need bits 5.6 set, so later when shifted it will end up being 011xxxx0 (xxxx = SSID number) + // Notice that if need to signal usage of AX.25 V2.0, (dest_ssid | 112); (MSb will need to be set at the end) + address[6] = (dest_ssid | 0x30); + address[13] = (src_ssid | 0x30); - frame.make_ui_frame(address, 0x03, protocol_id_t::NO_LAYER3, payload); + // fix path ssid bits. the result will be the same as above + std::string fixed_path = ""; + + const char* p = path.c_str(); + + while (*p) { + while (*p == ' ') p++; + if (!*p) break; + + char call[6] = {' ', ' ', ' ', ' ', ' ', ' '}; + int idx = 0; + + while (*p && *p != '-' && *p != ',') { + if (*p != ' ') { + if (idx < 6) { + char c = *p; + if (c >= 'a' && c <= 'z') c -= 32; + call[idx++] = c; + } + } + p++; + } + + int ssid = 0; + if (*p == '-') { + p++; + while (*p == ' ') p++; + while (*p >= '0' && *p <= '9') { + ssid = ssid * 10 + (*p - '0'); + p++; + } + } + + if (ssid >= 0 && ssid <= 15) { + for (int i = 0; i < 6; i++) fixed_path += call[i]; + fixed_path += (char)(ssid | 0x30); + } + + while (*p && *p != ',') p++; + if (*p == ',') p++; + } + + frame.make_ui_frame(address, 0x03, protocol_id_t::NO_LAYER3, payload, fixed_path); } } /* namespace aprs */ diff --git a/firmware/application/protocols/aprs.hpp b/firmware/application/protocols/aprs.hpp index 25d7a973a..dcca44a08 100644 --- a/firmware/application/protocols/aprs.hpp +++ b/firmware/application/protocols/aprs.hpp @@ -33,7 +33,8 @@ void make_aprs_frame( const uint32_t src_ssid, const char* dest_address, const uint32_t dest_ssid, - const std::string& payload); + const std::string& payload, + const std::string& path = ""); } /* namespace aprs */ diff --git a/firmware/application/protocols/ax25.cpp b/firmware/application/protocols/ax25.cpp index a9bbfb07c..1b0fa9fd5 100644 --- a/firmware/application/protocols/ax25.cpp +++ b/firmware/application/protocols/ax25.cpp @@ -26,13 +26,16 @@ namespace ax25 { -void AX25Frame::make_extended_field(char* const data, size_t length) { +void AX25Frame::make_extended_field(char* const data, size_t length, bool is_last) { size_t i = 0; for (i = 0; i < length - 1; i++) add_data(data[i] << 1); - add_data((data[i] << 1) | 1); + if (is_last) + add_data((data[i] << 1) | 1); + else + add_data((data[i] << 1)); } void AX25Frame::NRZI_add_bit(const uint32_t bit) { @@ -92,7 +95,7 @@ void AX25Frame::add_checksum() { add_byte(checksum >> 8, false, false); } -void AX25Frame::make_ui_frame(char* const address, const uint8_t control, const uint8_t protocol, const std::string& info) { +void AX25Frame::make_ui_frame(char* const address, const uint8_t control, const uint8_t protocol, const std::string& info, const std::string& path) { size_t i; bb_data_ptr = (uint16_t*)shared_memory.bb_data.data; @@ -103,22 +106,27 @@ void AX25Frame::make_ui_frame(char* const address, const uint8_t control, const ones_counter = 0; crc_ccitt.reset(); - add_flag(); - add_flag(); - add_flag(); - add_flag(); + add_flag(); // 0x73 + add_flag(); // 0x73 + add_flag(); // 0x73 + add_flag(); // 0x73 - make_extended_field(address, 14); - add_data(control); - add_data(protocol); + // path must not contain the - character! just the callsign and SSID next to each other + bool has_path = (path.size() > 0 && path.size() <= 63 && path.size() % 7 == 0); + make_extended_field(address, 14, !has_path); + if (has_path) { + make_extended_field(const_cast(path.data()), path.size(), true); + } + add_data(control); // 0x03 + add_data(protocol); // 0xf0 for (i = 0; i < info.size(); i++) add_data(info[i]); - add_checksum(); + add_checksum(); // fcs - add_flag(); - add_flag(); + add_flag(); // 0x73 + add_flag(); // 0x73 flush(); } diff --git a/firmware/application/protocols/ax25.hpp b/firmware/application/protocols/ax25.hpp index 4b02e5934..daeb57bd3 100644 --- a/firmware/application/protocols/ax25.hpp +++ b/firmware/application/protocols/ax25.hpp @@ -43,11 +43,11 @@ enum protocol_id_t { class AX25Frame { public: - void make_ui_frame(char* const address, const uint8_t control, const uint8_t protocol, const std::string& info); + void make_ui_frame(char* const address, const uint8_t control, const uint8_t protocol, const std::string& info, const std::string& path = ""); private: void NRZI_add_bit(const uint32_t bit); - void make_extended_field(char* const data, size_t length); + void make_extended_field(char* const data, size_t length, bool is_last = false); void add_byte(uint8_t byte, bool is_flag, bool is_data); void add_data(uint8_t byte); void add_checksum(); diff --git a/firmware/common/aprs_packet.hpp b/firmware/common/aprs_packet.hpp index 52894b19c..a0d986471 100644 --- a/firmware/common/aprs_packet.hpp +++ b/firmware/common/aprs_packet.hpp @@ -103,8 +103,10 @@ class APRSPacket { std::string get_digipeaters_formatted() { uint8_t position = DIGIPEATER_START; bool has_more = parse_address(SOURCE_START, REPEATER); - - std::string repeaters = ""; + if (!has_more) { + return ""; + } + std::string repeaters = ","; while (has_more) { has_more = parse_address(position, REPEATER); repeaters += std::string(address_buffer); @@ -112,7 +114,7 @@ class APRSPacket { position += ADDRESS_SIZE; if (has_more) { - repeaters += ">"; + repeaters += ","; } } @@ -146,8 +148,8 @@ class APRSPacket { } std::string get_stream_text() { - std::string stream = get_source_formatted() + ">" + get_destination_formatted() + ";" + get_digipeaters_formatted() + ";" + get_information_text_formatted(); - + // get_digipeaters_formatted will add the "," to the beginning if there are digipeaters + std::string stream = get_source_formatted() + ">" + get_destination_formatted() + get_digipeaters_formatted() + ":" + get_information_text_formatted(); return stream; } diff --git a/firmware/common/ui_widget.cpp b/firmware/common/ui_widget.cpp index e174c2dc9..08279c6b3 100644 --- a/firmware/common/ui_widget.cpp +++ b/firmware/common/ui_widget.cpp @@ -384,6 +384,10 @@ void Text::set(std::string_view value) { set_dirty(); } +std::string Text::get() { + return text; +} + void Text::getAccessibilityText(std::string& result) { result = text; } diff --git a/firmware/common/ui_widget.hpp b/firmware/common/ui_widget.hpp index 317b1bb69..f2ebe7199 100644 --- a/firmware/common/ui_widget.hpp +++ b/firmware/common/ui_widget.hpp @@ -217,6 +217,7 @@ class Text : public Widget { Text(Rect parent_rect); void set(std::string_view value); + std::string get(); void paint(Painter& painter) override; void getAccessibilityText(std::string& result) override;