diff --git a/firmware/application/baseband_api.cpp b/firmware/application/baseband_api.cpp index abff8bc08..2d9335715 100644 --- a/firmware/application/baseband_api.cpp +++ b/firmware/application/baseband_api.cpp @@ -397,6 +397,21 @@ void set_bitstream_config(uint32_t deviation, uint8_t mode) { send_message(&message); } +void set_rtty_config(uint16_t baud, uint16_t shift, uint8_t* payload, uint16_t payload_length) { + RTTYDataMessage message{baud, shift}; + if (payload && payload_length > 0) { + message.data_len = payload_length > message.max_len ? message.max_len : payload_length; + for (size_t i = 0; i < message.data_len; ++i) { + message.data[i] = payload[i]; + } + } + send_message(&message); +} + +void set_rtty_config(RTTYDataMessage& message) { + send_message(&message); +} + static bool baseband_image_running = false; void run_image(const spi_flash::image_tag_t image_tag) { diff --git a/firmware/application/baseband_api.hpp b/firmware/application/baseband_api.hpp index c0e0b8760..ab8a40899 100644 --- a/firmware/application/baseband_api.hpp +++ b/firmware/application/baseband_api.hpp @@ -108,7 +108,9 @@ void set_morsetx_key(bool key_down); void set_wefax_config(uint8_t lpm, uint8_t ioc); void set_noaaapt_config(); void set_flex_config(); -void set_bitstream_config(uint32_t deviation, uint8_t mode); // mode 0 for am, 1 for 2fsk +void set_bitstream_config(uint32_t deviation, uint8_t mode); // mode 0 for am, 1 for 2fsk +void set_rtty_config(uint16_t baud, uint16_t shift, uint8_t* payload = nullptr, uint16_t payload_length = 0); // baud*100 +void set_rtty_config(RTTYDataMessage& message); void request_roger_beep(); void request_rssi_beep(); diff --git a/firmware/application/external/external.cmake b/firmware/application/external/external.cmake index dadbe2416..1114727da 100644 --- a/firmware/application/external/external.cmake +++ b/firmware/application/external/external.cmake @@ -292,6 +292,16 @@ set(EXTCPPSRC #morseradiotx external/morseradiotx/main.cpp external/morseradiotx/ui_morse_radiotx.cpp + + #rtty_rx + external/rtty_rx/main.cpp + external/rtty_rx/ui_rtty_rx.cpp + external/rtty_rx/baudot.cpp + + #rtty_tx + external/rtty_tx/main.cpp + external/rtty_tx/ui_rtty_tx.cpp + external/rtty_tx/baudot.cpp ) set(EXTAPPLIST @@ -365,6 +375,8 @@ set(EXTAPPLIST siggen morse_radio morseradiotx + rtty_rx + rtty_tx ) # 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 25c4e8447..b66b06d7d 100644 --- a/firmware/application/external/external.ld +++ b/firmware/application/external/external.ld @@ -94,6 +94,8 @@ MEMORY ram_external_app_morse_radio (rwx) : org = 0xADF50000, len = 32k ram_external_app_waterfall_designer (rwx) : org = 0xADF60000, len = 32k ram_external_app_morseradiotx (rwx) : org = 0xADF70000, len = 32k + ram_external_app_rtty_rx (rwx) : org = 0xADF80000, len = 32k + ram_external_app_rtty_tx (rwx) : org = 0xADF90000, len = 32k } SECTIONS @@ -526,5 +528,19 @@ SECTIONS *(*ui*external_app*morseradiotx*); } > ram_external_app_morseradiotx + .external_app_rtty_rx : ALIGN(4) SUBALIGN(4) + { + KEEP(*(.external_app.app_rtty_rx.application_information)); + *(*ui*external_app*rtty_rx*); + } > ram_external_app_rtty_rx + + .external_app_rtty_tx : ALIGN(4) SUBALIGN(4) + { + KEEP(*(.external_app.app_rtty_tx.application_information)); + *(*ui*external_app*rtty_tx*); + } > ram_external_app_rtty_tx + + + } diff --git a/firmware/application/external/rtty_rx/baudot.cpp b/firmware/application/external/rtty_rx/baudot.cpp new file mode 100644 index 000000000..bdf3e811a --- /dev/null +++ b/firmware/application/external/rtty_rx/baudot.cpp @@ -0,0 +1,104 @@ +#include "baudot.hpp" +#include + +namespace ui::external_app::rtty_rx { + +constexpr uint8_t CODE_FIGS = 0x1B; +constexpr uint8_t CODE_LTRS = 0x1F; +constexpr uint8_t CODE_SPACE = 0x04; + +char BaudotCoder::get_char_mapping(bool is_figures, uint8_t index) const { + if (index >= 32) return 0; // Safety check + + if (is_figures) { + // ITA2 FIGURES Table + const char figures[32] = { + 0, '3', '\n', '-', ' ', '\'', '8', '7', + '\r', '$', '4', '\a', ',', '!', ':', '(', + '5', '+', ')', '2', '#', '6', '0', '1', + '9', '?', '&', 0, '.', '/', '=', 0}; + return figures[index]; + } else { + // ITA2 LETTERS Table + const char letters[32] = { + 0, 'E', '\n', 'A', ' ', 'S', 'I', 'U', + '\r', 'D', 'R', 'J', 'N', 'F', 'C', 'K', + 'T', 'Z', 'L', 'W', 'H', 'Y', 'P', 'Q', + 'O', 'B', 'G', 0, 'M', 'X', 'V', 0}; + return letters[index]; + } +} + +// --- DECODE --- +char BaudotCoder::decode(uint8_t baudotCode) { + uint8_t code = baudotCode & 0x1F; + if (code == CODE_FIGS) { + shiftState = FIGURES; + return 0; + } + if (code == CODE_LTRS) { + shiftState = LETTERS; + return 0; + } + if (usos_enabled && shiftState == FIGURES && code == CODE_SPACE) { + shiftState = LETTERS; + return ' '; + } + return get_char_mapping((shiftState == FIGURES), code); +} + +void BaudotCoder::encode(const std::string& src, uint8_t* dest, uint16_t* dest_length, uint16_t dest_max_size) { + uint16_t idx = 0; + + for (char c : src) { + if (idx >= dest_max_size) break; + + char upper_c = std::toupper(c); + uint8_t code = 0; + bool found = false; + bool target_is_figures = false; + if (upper_c == ' ') { + code = CODE_SPACE; + found = true; + } else { + for (int i = 1; i < 32; i++) { + if (get_char_mapping(false, i) == upper_c) { + code = i; + found = true; + target_is_figures = false; + break; + } + } + if (!found) { + for (int i = 1; i < 32; i++) { + if (get_char_mapping(true, i) == upper_c) { + code = i; + found = true; + target_is_figures = true; + break; + } + } + } + } + + if (found) { + if (target_is_figures && shiftState != FIGURES) { + if (idx + 1 >= dest_max_size) break; + dest[idx++] = CODE_FIGS; + shiftState = FIGURES; + } else if (!target_is_figures && shiftState != LETTERS && code != CODE_SPACE) { + if (idx + 1 >= dest_max_size) break; + dest[idx++] = CODE_LTRS; + shiftState = LETTERS; + } + + dest[idx++] = code; + } + } + + if (dest_length) { + *dest_length = idx; + } +} + +} // namespace ui::external_app::rtty_rx \ No newline at end of file diff --git a/firmware/application/external/rtty_rx/baudot.hpp b/firmware/application/external/rtty_rx/baudot.hpp new file mode 100644 index 000000000..9e9b5f068 --- /dev/null +++ b/firmware/application/external/rtty_rx/baudot.hpp @@ -0,0 +1,28 @@ +#ifndef BAUDOT_HPP +#define BAUDOT_HPP + +#include +#include + +namespace ui::external_app::rtty_rx { + +class BaudotCoder { + public: + enum ShiftState { LETTERS, + FIGURES }; + + BaudotCoder() + : shiftState(LETTERS) {} + char decode(uint8_t baudotCode); + void encode(const std::string& src, uint8_t* dest, uint16_t* dest_length, uint16_t dest_max_size); + void set_usos(bool enable) { usos_enabled = enable; } // shift == set to letters too + + private: + ShiftState shiftState; + bool usos_enabled = true; + char get_char_mapping(bool is_figures, uint8_t index) const; +}; + +} // namespace ui::external_app::rtty_rx + +#endif // BAUDOT_HPP \ No newline at end of file diff --git a/firmware/application/external/rtty_rx/main.cpp b/firmware/application/external/rtty_rx/main.cpp new file mode 100644 index 000000000..fe3d9255e --- /dev/null +++ b/firmware/application/external/rtty_rx/main.cpp @@ -0,0 +1,83 @@ +/* + * Copyright (C) 2026 HTotoo + * + * This file is part of PortaPack. + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2, or (at your option) + * any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; see the file COPYING. If not, write to + * the Free Software Foundation, Inc., 51 Franklin Street, + * Boston, MA 02110-1301, USA. + */ + +#include "ui.hpp" +#include "ui_rtty_rx.hpp" +#include "ui_navigation.hpp" +#include "external_app.hpp" + +namespace ui::external_app::rtty_rx { +void initialize_app(ui::NavigationView& nav) { + nav.push(); +} +} // namespace ui::external_app::rtty_rx + +extern "C" { + +__attribute__((section(".external_app.app_rtty_rx.application_information"), used)) application_information_t _application_information_rtty_rx = { + /*.memory_location = */ (uint8_t*)0x00000000, + /*.externalAppEntry = */ ui::external_app::rtty_rx::initialize_app, + /*.header_version = */ CURRENT_HEADER_VERSION, + /*.app_version = */ VERSION_MD5, + + /*.app_name = */ "RTTY", + /*.bitmap_data = */ { + 0x00, + 0x00, + 0xFE, + 0x1F, + 0xFE, + 0x3F, + 0x0E, + 0x78, + 0x0E, + 0x70, + 0x0E, + 0x70, + 0x0E, + 0x70, + 0x0E, + 0x78, + 0xFE, + 0x3F, + 0xFE, + 0x1F, + 0x8E, + 0x07, + 0x0E, + 0x0F, + 0x0E, + 0x1E, + 0x0E, + 0x3C, + 0x0E, + 0x78, + 0x00, + 0x00, + }, + /*.icon_color = */ ui::Color::orange().v, + /*.menu_location = */ app_location_t::RX, + /*.desired_menu_position = */ -1, + + /*.m4_app_tag = portapack::spi_flash::image_tag_rttyrx */ {'P', 'R', 'T', 'R'}, + /*.m4_app_offset = */ 0x00000000, // will be filled at compile time +}; +} \ No newline at end of file diff --git a/firmware/application/external/rtty_rx/ui_rtty_rx.cpp b/firmware/application/external/rtty_rx/ui_rtty_rx.cpp new file mode 100644 index 000000000..d36050dc1 --- /dev/null +++ b/firmware/application/external/rtty_rx/ui_rtty_rx.cpp @@ -0,0 +1,96 @@ +/* + * Copyright (C) 2026 HTotoo + * + * 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_rtty_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; +using namespace ui; + +// todo add timeout, so add newline and time when new data arrives after a time. + +namespace ui::external_app::rtty_rx { + +void RttyRxView::focus() { + field_frequency.focus(); +} + +RttyRxView::RttyRxView(NavigationView& nav) + : nav_{nav} { + baseband::run_prepared_image(portapack::memory::map::m4_code.base()); + add_children({ + &rssi, + &field_rf_amp, + &field_lna, + &field_vga, + &field_volume, + &field_frequency, + &labels, + &options_baud, + &console, + }); + field_frequency.set_step(100); + audio::set_rate(audio::Rate::Hz_24000); + audio::output::start(); + + receiver_model.set_hidden_offset(0); + receiver_model.set_sampling_rate(3072000); // set the needed baseband SR. + receiver_model.set_baseband_bandwidth(1750000); // set the front-end RF BW filter. + receiver_model.enable(); + console.enable_scrolling(false); + + options_baud.on_change = [this](size_t, int32_t value) { + baseband::set_rtty_config(value, 170); + baud_conf = value; + }; + options_baud.set_by_value(baud_conf); + if (baud_conf == 0) { // to trigger it when not changed + baseband::set_rtty_config(0, 170); + } +} + +void RttyRxView::got_message(std::string msg) { + console.write(msg); +} + +RttyRxView::~RttyRxView() { + receiver_model.set_hidden_offset(0); + receiver_model.disable(); + baseband::shutdown(); + audio::output::stop(); +} + +void RttyRxView::on_data(const RTTYDataMessage* message) { + std::string msg = ""; + for (size_t i = 0; i < message->data_len; i++) { + const auto c = baudot_decoder.decode(message->data[i]); + if (c != 0) + msg += c; + } + got_message(msg); +} + +} // namespace ui::external_app::rtty_rx \ No newline at end of file diff --git a/firmware/application/external/rtty_rx/ui_rtty_rx.hpp b/firmware/application/external/rtty_rx/ui_rtty_rx.hpp new file mode 100644 index 000000000..47101dcbe --- /dev/null +++ b/firmware/application/external/rtty_rx/ui_rtty_rx.hpp @@ -0,0 +1,109 @@ +/* + * Copyright (C) 2026 HTotoo + * + * 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_RTTY_RX_H__ +#define __UI_RTTY_RX_H__ + +#include "ui.hpp" +#include "ui_language.hpp" +#include "ui_navigation.hpp" +#include "ui_receiver.hpp" +#include "ui_freq_field.hpp" +#include "app_settings.hpp" +#include "radio_state.hpp" +#include "log_file.hpp" +#include "utility.hpp" +#include "ui_fileman.hpp" +#include "file_path.hpp" +#include "baudot.hpp" + +using namespace ui; + +namespace ui::external_app::rtty_rx { + +class RttyRxView : public View { + public: + RttyRxView(NavigationView& nav); + ~RttyRxView(); + + void focus() override; + + std::string title() const override { return "RTTY"; }; + + private: + NavigationView& nav_; + RxRadioState radio_state_{}; + uint32_t baud_conf = 0; + app_settings::SettingsManager settings_{ + "rx_rtty", + app_settings::Mode::RX, + { + {"baud_conf"sv, &baud_conf}, + }}; + + RxFrequencyField field_frequency{ + {UI_POS_X(0), UI_POS_Y(0)}, + nav_}; + RFAmpField field_rf_amp{ + {UI_POS_X(13), UI_POS_Y(0)}}; + LNAGainField field_lna{ + {UI_POS_X(15), UI_POS_Y(0)}}; + VGAGainField field_vga{ + {UI_POS_X(18), UI_POS_Y(0)}}; + RSSI rssi{ + {UI_POS_X(21), UI_POS_Y(0), UI_POS_WIDTH_REMAINING(24), 4}}; + AudioVolumeField field_volume{{UI_POS_X_RIGHT(2), UI_POS_Y(0)}}; + + Labels labels{ + {{UI_POS_X(0), UI_POS_Y(1)}, "Baud:", Theme::getInstance()->fg_light->foreground}}; + + OptionsField options_baud{ + {UI_POS_X(7), UI_POS_Y(1)}, + 5, + {{"Auto", 0}, + {"45", 4500}, + {"45.45", 4545}, + {"50", 5000}, + {"75", 7500}, + {"100", 10000}, + {"110", 11000}, + {"150", 15000}, + {"200", 20000}}}; + + Console console{ + {UI_POS_X(0), UI_POS_Y(2), UI_POS_MAXWIDTH, UI_POS_HEIGHT_REMAINING(3)}}; + + BaudotCoder baudot_decoder{}; + + void on_data(const RTTYDataMessage* message); + void got_message(std::string msg); + + MessageHandlerRegistration message_handler_data{ + Message::ID::RTTYData, + [this](Message* const p) { + const auto message = static_cast(p); + this->on_data(message); + }}; +}; + +} // namespace ui::external_app::rtty_rx + +#endif /*__UI_RTTY_RX_H__*/ \ No newline at end of file diff --git a/firmware/application/external/rtty_tx/baudot.cpp b/firmware/application/external/rtty_tx/baudot.cpp new file mode 100644 index 000000000..be47ed2a3 --- /dev/null +++ b/firmware/application/external/rtty_tx/baudot.cpp @@ -0,0 +1,104 @@ +#include "baudot.hpp" +#include + +namespace ui::external_app::rtty_tx { + +constexpr uint8_t CODE_FIGS = 0x1B; +constexpr uint8_t CODE_LTRS = 0x1F; +constexpr uint8_t CODE_SPACE = 0x04; + +char BaudotCoder::get_char_mapping(bool is_figures, uint8_t index) const { + if (index >= 32) return 0; // Safety check + + if (is_figures) { + // ITA2 FIGURES Table + const char figures[32] = { + 0, '3', '\n', '-', ' ', '\'', '8', '7', + '\r', '$', '4', '\a', ',', '!', ':', '(', + '5', '+', ')', '2', '#', '6', '0', '1', + '9', '?', '&', 0, '.', '/', '=', 0}; + return figures[index]; + } else { + // ITA2 LETTERS Table + const char letters[32] = { + 0, 'E', '\n', 'A', ' ', 'S', 'I', 'U', + '\r', 'D', 'R', 'J', 'N', 'F', 'C', 'K', + 'T', 'Z', 'L', 'W', 'H', 'Y', 'P', 'Q', + 'O', 'B', 'G', 0, 'M', 'X', 'V', 0}; + return letters[index]; + } +} + +// --- DECODE --- +char BaudotCoder::decode(uint8_t baudotCode) { + uint8_t code = baudotCode & 0x1F; + if (code == CODE_FIGS) { + shiftState = FIGURES; + return 0; + } + if (code == CODE_LTRS) { + shiftState = LETTERS; + return 0; + } + if (usos_enabled && shiftState == FIGURES && code == CODE_SPACE) { + shiftState = LETTERS; + return ' '; + } + return get_char_mapping((shiftState == FIGURES), code); +} + +void BaudotCoder::encode(const std::string& src, uint8_t* dest, uint16_t* dest_length, uint16_t dest_max_size) { + uint16_t idx = 0; + + for (char c : src) { + if (idx >= dest_max_size) break; + + char upper_c = std::toupper(c); + uint8_t code = 0; + bool found = false; + bool target_is_figures = false; + if (upper_c == ' ') { + code = CODE_SPACE; + found = true; + } else { + for (int i = 1; i < 32; i++) { + if (get_char_mapping(false, i) == upper_c) { + code = i; + found = true; + target_is_figures = false; + break; + } + } + if (!found) { + for (int i = 1; i < 32; i++) { + if (get_char_mapping(true, i) == upper_c) { + code = i; + found = true; + target_is_figures = true; + break; + } + } + } + } + + if (found) { + if (target_is_figures && shiftState != FIGURES) { + if (idx + 1 >= dest_max_size) break; + dest[idx++] = CODE_FIGS; + shiftState = FIGURES; + } else if (!target_is_figures && shiftState != LETTERS && code != CODE_SPACE) { + if (idx + 1 >= dest_max_size) break; + dest[idx++] = CODE_LTRS; + shiftState = LETTERS; + } + + dest[idx++] = code; + } + } + + if (dest_length) { + *dest_length = idx; + } +} + +} // namespace ui::external_app::rtty_tx \ No newline at end of file diff --git a/firmware/application/external/rtty_tx/baudot.hpp b/firmware/application/external/rtty_tx/baudot.hpp new file mode 100644 index 000000000..7ccea500f --- /dev/null +++ b/firmware/application/external/rtty_tx/baudot.hpp @@ -0,0 +1,28 @@ +#ifndef BAUDOT_HPP +#define BAUDOT_HPP + +#include +#include + +namespace ui::external_app::rtty_tx { + +class BaudotCoder { + public: + enum ShiftState { LETTERS, + FIGURES }; + + BaudotCoder() + : shiftState(LETTERS) {} + char decode(uint8_t baudotCode); + void encode(const std::string& src, uint8_t* dest, uint16_t* dest_length, uint16_t dest_max_size); + void set_usos(bool enable) { usos_enabled = enable; } // shift == set to letters too + + private: + ShiftState shiftState; + bool usos_enabled = true; + char get_char_mapping(bool is_figures, uint8_t index) const; +}; + +} // namespace ui::external_app::rtty_tx + +#endif // BAUDOT_HPP \ No newline at end of file diff --git a/firmware/application/external/rtty_tx/main.cpp b/firmware/application/external/rtty_tx/main.cpp new file mode 100644 index 000000000..a939024ff --- /dev/null +++ b/firmware/application/external/rtty_tx/main.cpp @@ -0,0 +1,83 @@ +/* + * Copyright (C) 2026 HTotoo + * + * This file is part of PortaPack. + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2, or (at your option) + * any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; see the file COPYING. If not, write to + * the Free Software Foundation, Inc., 51 Franklin Street, + * Boston, MA 02110-1301, USA. + */ + +#include "ui.hpp" +#include "ui_rtty_tx.hpp" +#include "ui_navigation.hpp" +#include "external_app.hpp" + +namespace ui::external_app::rtty_tx { +void initialize_app(ui::NavigationView& nav) { + nav.push(); +} +} // namespace ui::external_app::rtty_tx + +extern "C" { + +__attribute__((section(".external_app.app_rtty_tx.application_information"), used)) application_information_t _application_information_rtty_tx = { + /*.memory_location = */ (uint8_t*)0x00000000, + /*.externalAppEntry = */ ui::external_app::rtty_tx::initialize_app, + /*.header_version = */ CURRENT_HEADER_VERSION, + /*.app_version = */ VERSION_MD5, + + /*.app_name = */ "RTTY", + /*.bitmap_data = */ { + 0x00, + 0x00, + 0xFE, + 0x1F, + 0xFE, + 0x3F, + 0x0E, + 0x78, + 0x0E, + 0x70, + 0x0E, + 0x70, + 0x0E, + 0x70, + 0x0E, + 0x78, + 0xFE, + 0x3F, + 0xFE, + 0x1F, + 0x8E, + 0x07, + 0x0E, + 0x0F, + 0x0E, + 0x1E, + 0x0E, + 0x3C, + 0x0E, + 0x78, + 0x00, + 0x00, + }, + /*.icon_color = */ ui::Color::orange().v, + /*.menu_location = */ app_location_t::TX, + /*.desired_menu_position = */ -1, + + /*.m4_app_tag = portapack::spi_flash::image_tag_rttytx */ {'P', 'R', 'T', 'T'}, + /*.m4_app_offset = */ 0x00000000, // will be filled at compile time +}; +} \ No newline at end of file diff --git a/firmware/application/external/rtty_tx/ui_rtty_tx.cpp b/firmware/application/external/rtty_tx/ui_rtty_tx.cpp new file mode 100644 index 000000000..8bbd552e7 --- /dev/null +++ b/firmware/application/external/rtty_tx/ui_rtty_tx.cpp @@ -0,0 +1,154 @@ +/* + * Copyright (C) 2026 HTotoo + * + * 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_rtty_tx.hpp" + +#include "audio.hpp" +#include "rtc_time.hpp" +#include "baseband_api.hpp" +#include "string_format.hpp" +#include "portapack_persistent_memory.hpp" + +using namespace portapack; +using namespace ui; + +namespace ui::external_app::rtty_tx { + +void RttyTxView::focus() { + options_baud.focus(); +} + +RttyTxView::RttyTxView(NavigationView& nav) + : nav_{nav} { + add_children({&tx_view, + &labels, + &options_baud, + &options_shift, + &btn_message, + &text_message, + &check_inverted, + &options_tone, + &text_tones}); + + options_baud.on_change = [this](size_t, int32_t value) { + baud_conf = value; + }; + options_baud.set_by_value(baud_conf); + + options_shift.on_change = [this](size_t, int32_t value) { + shift = value; + refresh_tones(); + }; + options_shift.set_by_value(shift); + + btn_message.on_select = [this](Button&) { + text_prompt(nav_, message, 64, ENTER_KEYBOARD_MODE_ALPHA, [this](std::string& new_message) { + message = new_message; + text_message.set(message); // maybe replace to console + }); + }; + text_message.set(message); + options_tone.set_by_value(mark_tone); + options_tone.on_change = [this](size_t, int32_t value) { + mark_tone = value; + refresh_tones(); + }; + check_inverted.on_select = [this](Checkbox&, bool) { + refresh_tones(); + }; + + tx_view.on_edit_frequency = [this, &nav]() { + auto new_view = nav.push(transmitter_model.target_frequency()); + new_view->on_changed = [this](rf::Frequency f) { + transmitter_model.set_target_frequency(f); + }; + }; + + tx_view.on_start = [this]() { + if (message.empty()) { + nav_.display_modal("Error", "Message is empty. Please set a message to transmit."); + return; + } + baseband::run_prepared_image(portapack::memory::map::m4_code.base()); + transmitter_model.set_baseband_bandwidth(2048000); + baseband::set_sample_rate(2048000, OversampleRate::None); + transmitter_model.set_sampling_rate(2048000); + transmitter_model.set_baseband_bandwidth(2048000); + transmitter_model.enable(); + rtty_message.baud = baud_conf; + rtty_message.shift = shift; + rtty_message.inverted = false; // check_inverted.value(); //i already invert it with the mark and space calculation + rtty_message.mark_tone = mark; + rtty_message.space_tone = space; + rtty_message.stopbits = stop_bits; + baudot_encoder.set_usos(false); // to be compatible with all + baudot_encoder.encode(message, rtty_message.data, &rtty_message.data_len, rtty_message.max_len); + baseband::set_rtty_config(rtty_message); + tx_view.set_transmitting(true); + }; + + tx_view.on_stop = [this]() { + stop(); + }; + + refresh_tones(); +} + +void RttyTxView::refresh_tones() { + std::string tones_str = ""; + + int16_t shift = options_shift.selected_index_value(); + if (mark_tone == -32000) { + mark = -shift / 2; + space = shift / 2; + } else { + if (mark_tone < 0) { + space = -(shift + abs(mark_tone)); + } else { + space = shift + mark_tone; + } + mark = mark_tone; + } + + if (check_inverted.value()) { + int16_t tmp = mark; + mark = space; + space = tmp; + } + tones_str = "Mark:" + to_string_dec_int(mark) + " Hz, Space:" + to_string_dec_int(space) + " Hz"; + text_tones.set(tones_str); +} + +void RttyTxView::on_tx_progress(bool done) { + if (done) stop(); +} + +void RttyTxView::stop() { + tx_view.set_transmitting(false); + transmitter_model.disable(); + baseband::shutdown(); +} + +RttyTxView::~RttyTxView() { + stop(); +} + +} // namespace ui::external_app::rtty_tx \ No newline at end of file diff --git a/firmware/application/external/rtty_tx/ui_rtty_tx.hpp b/firmware/application/external/rtty_tx/ui_rtty_tx.hpp new file mode 100644 index 000000000..3bf4ba435 --- /dev/null +++ b/firmware/application/external/rtty_tx/ui_rtty_tx.hpp @@ -0,0 +1,145 @@ +/* + * Copyright (C) 2026 HTotoo + * + * 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_RTTY_TX_H__ +#define __UI_RTTY_TX_H__ + +#include "ui.hpp" +#include "ui_language.hpp" +#include "ui_navigation.hpp" +#include "ui_transmitter.hpp" +#include "ui_freq_field.hpp" +#include "app_settings.hpp" +#include "radio_state.hpp" +#include "log_file.hpp" +#include "utility.hpp" +#include "ui_fileman.hpp" +#include "file_path.hpp" +#include "baudot.hpp" + +using namespace ui; + +namespace ui::external_app::rtty_tx { + +class RttyTxView : public View { + public: + RttyTxView(NavigationView& nav); + ~RttyTxView(); + void focus() override; + std::string title() const override { return "RTTY"; }; + + private: + void on_tx_progress(bool done); + void stop(); + void refresh_tones(); + + NavigationView& nav_; + RxRadioState radio_state_{}; + uint32_t baud_conf = 4545; // default to 45.45 baud, which is common for RTTY. + uint32_t shift = 170; // 170 hz + std::string message = "PORTAPACK"; + int32_t mark_tone = 0; // for option + int16_t mark = 0, space = 170; + uint8_t stop_bits = 3; // 2=1.0, 3=1.5, 4=2.0 + app_settings::SettingsManager settings_{ + "tx_rtty", + app_settings::Mode::TX, + { + {"baud_conf"sv, &baud_conf}, + {"shift"sv, &shift}, + {"message"sv, &message}, + {"mark_tone"sv, &mark_tone}, + }}; + + Labels labels{ + {{UI_POS_X(0), UI_POS_Y(0)}, "Baud:", Theme::getInstance()->fg_light->foreground}, + {{UI_POS_X(14), UI_POS_Y(0)}, "Shift:", Theme::getInstance()->fg_light->foreground}, + {{UI_POS_X(15), UI_POS_Y(1)}, "Mark t:", Theme::getInstance()->fg_light->foreground}, + {{UI_POS_X(0), UI_POS_Y(3)}, "Message:", Theme::getInstance()->fg_light->foreground}}; + + OptionsField options_baud{ + {UI_POS_X(7), UI_POS_Y(0)}, + 5, + {{"45", 4500}, + {"45.45", 4545}, + {"50", 5000}, + {"75", 7500}, + {"100", 10000}, + {"110", 11000}, + {"150", 15000}, + {"200", 20000}}}; + + OptionsField options_shift{ + {UI_POS_X(21), UI_POS_Y(0)}, + 5, + {{"170", 170}, + {"85", 85}, + {"200", 200}, + {"450", 450}, + {"850", 850}}}; + + Checkbox check_inverted{ + {UI_POS_X(0), UI_POS_Y(1)}, + 10, + "Inverted", + true}; + + OptionsField options_tone{ + {UI_POS_X(25), UI_POS_Y(1)}, + 5, + {{"CNT", -32000}, + {"0", 0}, + {"2125", 2125}, + {"-2125", -2125}, + {"1275", 1275}, + {"-1275", -1275}}}; + + Text text_tones{ + {UI_POS_X(0), UI_POS_Y(2), UI_POS_MAXWIDTH, UI_POS_HEIGHT(1)}, + "-"}; + + Button btn_message{ + {UI_POS_X(0), UI_POS_Y(3), UI_POS_WIDTH(15), UI_POS_HEIGHT(2)}, + "Set message"}; + + Text text_message{ + {UI_POS_X(0), UI_POS_Y(5), UI_POS_MAXWIDTH, UI_POS_HEIGHT(1)}, + ""}; + + TransmitterView tx_view{ + (int16_t)UI_POS_Y_BOTTOM(4), + 1000, + 0}; + + RTTYDataMessage rtty_message{}; + BaudotCoder baudot_encoder{}; + + MessageHandlerRegistration message_handler_tx_progress{ + Message::ID::TXProgress, + [this](const Message* const p) { + const auto message = *reinterpret_cast(p); + this->on_tx_progress(message.done); + }}; +}; + +} // namespace ui::external_app::rtty_tx + +#endif /*__UI_RTTY_TX_H__*/ \ No newline at end of file diff --git a/firmware/baseband/CMakeLists.txt b/firmware/baseband/CMakeLists.txt index 57db4d043..ed20f0f6b 100644 --- a/firmware/baseband/CMakeLists.txt +++ b/firmware/baseband/CMakeLists.txt @@ -674,6 +674,22 @@ set(MODE_CPPSRC ) DeclareTargets(PMRS morse) + +### RTTY RX + +set(MODE_CPPSRC + proc_rtty_rx.cpp +) +DeclareTargets(PRTR rtty_rx) + + +### RTTY TX + +set(MODE_CPPSRC + proc_rtty_tx.cpp +) +DeclareTargets(PRTT rtty_tx) + ### SD over USB set(MODE_INCDIR diff --git a/firmware/baseband/proc_rtty_rx.cpp b/firmware/baseband/proc_rtty_rx.cpp new file mode 100644 index 000000000..baa7ee0c4 --- /dev/null +++ b/firmware/baseband/proc_rtty_rx.cpp @@ -0,0 +1,334 @@ +/* + * Copyright (C) 2026 HTotoo + * + * 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_rtty_rx.hpp" +#include "portapack_shared_memory.hpp" +#include "audio_dma.hpp" +#include "event_m4.hpp" + +// RTTY Timing Limits (at 24kHz) +static constexpr uint32_t MIN_VALID_PULSE = 200; +static constexpr uint32_t MAX_VALID_PULSE = 800; + +void RTTYRxProcessor::configure() { + configured = false; + + baseband_thread.set_sampling_rate(baseband_fs); + + // 1. 3.072M -> 384k + decim_0.configure(taps_4k25_decim_0.taps); + + // 2. 384k -> 48k + decim_1.configure(taps_4k25_decim_1.taps); + + // 3. 48k -> 24k + channel_filter.configure(taps_11k0_channel.taps, 2); + + // FM Demodulator + demod.configure(24000, 9000); + + // Audio Output + audio_output.configure(iir_config_passthrough, iir_config_passthrough, 1.0f); + + // Reset State + val_max = -200000; + val_min = 200000; + + uart_state = WAIT_START; + + inverted_polarity = false; + // Default to standard 45.45 baud + estimated_bit_width = 528; + samples_per_bit = 528; + pulse_measure_counter = 0; + + configured = true; +} + +// Variables for Fast-Lock Auto Baud +uint32_t candidate_width = 0; +uint8_t candidate_hits = 0; +uint32_t squelch_closed_timer = 0; +bool is_squelched = true; + +void RTTYRxProcessor::execute(const buffer_c8_t& buffer) { + if (!configured) return; + + const auto decim_0_out = decim_0.execute(buffer, dst_buffer); + buffer_c16_t decim_1_target{dst_buffer_data.data() + 256, 256}; + const auto decim_1_out = decim_1.execute(decim_0_out, decim_1_target); + const auto channel_out = channel_filter.execute(decim_1_out, dst_buffer); + + auto audio = demod.execute(channel_out, audio_buffer); + + feed_channel_stats(channel_out); + + for (size_t i = 0; i < audio.count; i++) { + int16_t sample = audio.p[i]; + + // DECODER INPUT + int32_t fm_val = (int32_t)sample * 32; + + // 1. ENVELOPE TRACKING (Floating) + if (fm_val > val_max) val_max = fm_val; + if (fm_val < val_min) val_min = fm_val; + + // 2. DECAY + // Shrink the envelope spread (Max - Min) slowly. + if (++decay_timer == 0) { + int32_t spread = val_max - val_min; + if (spread > 200) { + int32_t decay = (spread >> 7) + 1; + if (val_max > val_min + decay) val_max -= decay; + if (val_min < val_max - decay) val_min += decay; + } else { + // val_max += 100; + // val_min -= 100; + // temp off. but kept it for memory + } + } + + // 3. OFFSET CALCULATION + int32_t midpoint = (val_max + val_min) / 2; + + // 4. SIGNAL CENTERING + int32_t centered_val = fm_val - midpoint; + + // LPF for Slicer + fm_val_smoothed += (centered_val - fm_val_smoothed) >> LPF_ALPHA_SHIFT; + + // 5. SQUELCH + int32_t spread = val_max - val_min; + + if (is_squelched) { + if (spread > 600) is_squelched = false; + } else { + if (spread < 300) is_squelched = true; + } + + process_demodulated_sample(fm_val_smoothed); + + // 6. AUDIO PATH + if (is_squelched) { + audio.p[i] = 0; + } else { + int32_t audio_boost = sample * 48; + if (audio_boost > 32767) + audio_boost = 32767; + else if (audio_boost < -32768) + audio_boost = -32768; + audio.p[i] = (int16_t)audio_boost; + } + } + + audio_output.write(audio); + + // UI Update + if (tx_message.data_len > 0) { + if (baud_rate == 0 && samples_per_bit > 0) { + uint32_t b = (final_fs * 100) / samples_per_bit; + if (b > 4300 && b < 4700) + b = 4500; + else if (b > 4800 && b < 5200) + b = 5000; + else if (b > 7200 && b < 7800) + b = 7500; + tx_message.baud = (uint16_t)b; + } else { + tx_message.baud = baud_rate; + } + tx_message.shift = shift_hz; + + if (shared_memory.application_queue.push(tx_message)) { + tx_message.data_len = 0; + } + } +} + +void RTTYRxProcessor::process_demodulated_sample(int32_t sample) { + // 1. Squelch Check + if (is_squelched) { + squelch_closed_timer++; + if (squelch_closed_timer > 12000) { + uart_state = WAIT_START; + current_slicer_bit = 1; + pulse_measure_counter = 0; + inverted_polarity = false; + if (baud_rate == 0) { + estimated_bit_width = 528; + samples_per_bit = 528; + } + } + return; + } + + squelch_closed_timer = 0; + + // 2. Schmitt Trigger + int32_t hysteresis = (val_max - val_min) / 8; + + uint8_t raw_bit = current_slicer_bit; + if (inverted_polarity) raw_bit = !raw_bit; + + if (sample > hysteresis) + raw_bit = 1; + else if (sample < -hysteresis) + raw_bit = 0; + + // Polarity Check + if (raw_bit == 0) { + if (++polarity_timer > 7200) { + inverted_polarity = !inverted_polarity; + polarity_timer = 0; + val_max = -200000; + val_min = 200000; + uart_state = WAIT_START; + } + } else { + polarity_timer = 0; + } + + current_slicer_bit = inverted_polarity ? !raw_bit : raw_bit; + + // 3. Auto Baud + if (baud_rate == 0) { + pulse_measure_counter++; + if (current_slicer_bit != last_bit_state) { + update_baud_estimation(pulse_measure_counter); + pulse_measure_counter = 0; + last_bit_state = current_slicer_bit; + } + } + + // 4. UART State Machine + switch (uart_state) { + case WAIT_START: + if (current_slicer_bit == 0) { + phase_counter = samples_per_bit / 2; + uart_state = CHECK_START; + } + break; + + case CHECK_START: + if (--phase_counter == 0) { + if (current_slicer_bit == 0) { + phase_counter = samples_per_bit; + bit_counter = 0; + shift_reg = 0; + uart_state = READ_BITS; + } else { + uart_state = WAIT_START; + } + } + break; + + case READ_BITS: + if (--phase_counter == 0) { + if (current_slicer_bit) shift_reg |= (1 << bit_counter); + + phase_counter = samples_per_bit; + bit_counter++; + + if (bit_counter >= 5) { + uart_state = WAIT_STOP; + } + } + break; + + case WAIT_STOP: + if (--phase_counter == 0) { + // Accept data even if stop bit is noisy (0) + // This improves reception during fades + // if (current_slicer_bit == 1) { + append_data(shift_reg & 0x1F); + //} + uart_state = WAIT_START; + } + break; + } +} + +void RTTYRxProcessor::update_baud_estimation(uint32_t pulse_width) { + if (pulse_width < MIN_VALID_PULSE || pulse_width > MAX_VALID_PULSE) return; + + int32_t diff = (int32_t)pulse_width - (int32_t)estimated_bit_width; + if (diff < 0) diff = -diff; + + if (diff < (int32_t)(estimated_bit_width / 6)) { + estimated_bit_width = (estimated_bit_width * 7 + pulse_width) / 8; + samples_per_bit = estimated_bit_width; + candidate_hits = 0; + } else { + int32_t cand_diff = (int32_t)pulse_width - (int32_t)candidate_width; + if (cand_diff < 0) cand_diff = -cand_diff; + + if (cand_diff < (int32_t)(candidate_width / 8)) { + candidate_hits++; + if (candidate_hits >= 3) { + estimated_bit_width = (candidate_width + pulse_width) / 2; + samples_per_bit = estimated_bit_width; + candidate_hits = 0; + uart_state = WAIT_START; + } + } else { + candidate_width = pulse_width; + candidate_hits = 1; + } + } +} + +void RTTYRxProcessor::append_data(uint8_t raw_baudot_code) { + if (tx_message.data_len < tx_message.max_len) { + tx_message.data[tx_message.data_len] = raw_baudot_code; + tx_message.data_len++; + } +} + +void RTTYRxProcessor::on_message(const Message* const message) { + if (message->id == Message::ID::RTTYData) { + const auto& rtty_msg = static_cast(*message); + + if (rtty_msg.baud != baud_rate) { + baud_rate = rtty_msg.baud; + if (baud_rate > 0) { + const float real_baud = (float)baud_rate / 100.0f; + samples_per_bit = (uint32_t)((float)final_fs / real_baud); + estimated_bit_width = samples_per_bit; + } else { + estimated_bit_width = 528; + samples_per_bit = 528; + inverted_polarity = false; + } + uart_state = WAIT_START; + } + shift_hz = rtty_msg.shift; + if (!configured) { + configure(); + } + } +} + +int main() { + audio::dma::init_audio_out(); + EventDispatcher event_dispatcher{std::make_unique()}; + event_dispatcher.run(); + return 0; +} \ No newline at end of file diff --git a/firmware/baseband/proc_rtty_rx.hpp b/firmware/baseband/proc_rtty_rx.hpp new file mode 100644 index 000000000..f7a66ed53 --- /dev/null +++ b/firmware/baseband/proc_rtty_rx.hpp @@ -0,0 +1,119 @@ +/* + * Copyright (C) 2026 HTotoo + * + * 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_RTTY_RX_H__ +#define __PROC_RTTY_RX_H__ + +#include "baseband_processor.hpp" +#include "baseband_thread.hpp" +#include "rssi_thread.hpp" +#include "message.hpp" +#include "dsp_decimate.hpp" +#include "dsp_demodulate.hpp" +#include "audio_output.hpp" +#include "dsp_fir_taps.hpp" + +class RTTYRxProcessor : 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; + + // RTTY Config + uint16_t baud_rate = 0; // 0 = Auto-detect + uint16_t shift_hz = 170; + bool configured = false; + + // DSP Rates + // Stage 0: 3.072M / 8 = 384k + // Stage 1: 384k / 8 = 48k + // Stage 2: 48k / 2 = 24k (Audio/Demod) + static constexpr uint32_t decim_0_out_fs = baseband_fs / 8; + static constexpr uint32_t decim_1_out_fs = decim_0_out_fs / 8; + static constexpr uint32_t final_fs = decim_1_out_fs / 2; // 24000 Hz + + // Tuning Constants + static constexpr int32_t LPF_ALPHA_SHIFT = 2; + static constexpr int32_t MIN_SHIFT_SPREAD = 400; + + // Decimation Chain + dsp::decimate::FIRC8xR16x24FS4Decim8 decim_0{}; + dsp::decimate::FIRC16xR16x32Decim8 decim_1{}; + dsp::decimate::FIRAndDecimateComplex channel_filter{}; + + // Demodulator & Audio + dsp::demodulate::FM demod{}; + AudioOutput audio_output{}; + + // Buffers + std::array dst_buffer_data{}; + const buffer_c16_t dst_buffer{dst_buffer_data.data(), dst_buffer_data.size()}; + + std::array audio_data{}; + const buffer_s16_t audio_buffer{audio_data.data(), audio_data.size()}; + + // Output Message + RTTYDataMessage tx_message{}; + + // Demodulator State + int32_t fm_val_smoothed = 0; + + // Tracker State + int32_t val_max = 0; + int32_t val_min = 0; + uint8_t decay_timer = 0; + + // Auto-Baud State + uint32_t pulse_measure_counter = 0; + uint8_t last_bit_state = 0; + uint32_t estimated_bit_width = 528; // ~45 baud @ 24k + + // UART State + enum UartState { + WAIT_START, + CHECK_START, + READ_BITS, + WAIT_STOP + }; + + UartState uart_state = WAIT_START; + uint32_t samples_per_bit = 528; + uint32_t phase_counter = 0; + uint8_t bit_counter = 0; + uint8_t shift_reg = 0; + uint8_t current_slicer_bit = 1; + + // Polarity + bool inverted_polarity = true; + uint32_t polarity_timer = 0; + + void configure(); + void process_demodulated_sample(int32_t sample); + void update_baud_estimation(uint32_t pulse_width); + void append_data(uint8_t raw_baudot_code); + + BasebandThread baseband_thread{baseband_fs, this, baseband::Direction::Receive}; + RSSIThread rssi_thread{}; +}; + +#endif /*__PROC_RTTY_RX_H__*/ \ No newline at end of file diff --git a/firmware/baseband/proc_rtty_tx.cpp b/firmware/baseband/proc_rtty_tx.cpp new file mode 100644 index 000000000..0b650714d --- /dev/null +++ b/firmware/baseband/proc_rtty_tx.cpp @@ -0,0 +1,245 @@ +/* + * Copyright (C) 2026 HTotoo + * + * 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. + */ + +#include "proc_rtty_tx.hpp" +#include "sine_table_int8.hpp" +#include "event_m4.hpp" +#include + +static constexpr uint32_t LEAD_IN_SAMPLES = 204800; + +static inline uint32_t hz_to_delta(int32_t hz, uint32_t fs) { + int64_t delta = ((int64_t)hz * (int64_t)UINT32_MAX) / (int64_t)fs; + return (uint32_t)delta; +} + +void RTTYTXProcessor::execute(const buffer_c8_t& buffer) { + if (!configured) { + for (size_t i = 0; i < buffer.count; i++) { + buffer.p[i] = {0, 0}; + } + return; + } + + for (size_t i = 0; i < buffer.count; i++) { + bool advance = false; + + if (state == State::LeadIn) { + lead_counter++; + if (lead_counter >= LEAD_IN_SAMPLES) { + advance = true; + } + } else if (state == State::LeadOut) { + lead_counter++; + if (lead_counter >= 460000) { + txprogress_message.done = true; + shared_memory.application_queue.push(txprogress_message); + configured = false; + state = State::Idle; + } + } else if (state != State::Idle) { + uint32_t previous_phase = baud_phase; + baud_phase += baud_phase_increment; + + if (baud_phase < previous_phase) { + advance = true; + } + } + + if (advance) { + advance_state(); + } + + // Tone selection + uint32_t target_delta; + bool is_mark = true; + + switch (state) { + case State::StartBit: + is_mark = false; + break; + case State::DataBits: + is_mark = (current_char >> bit_pos) & 1; + break; + default: + is_mark = true; + break; + } + + target_delta = is_mark ? delta_mark : delta_space; + + // Slew limiter + int32_t diff = (int32_t)target_delta - (int32_t)current_delta; + int32_t abs_diff = diff < 0 ? -diff : diff; + + if (abs_diff <= (int32_t)slew_rate) { + current_delta = target_delta; + } else { + current_delta += (diff > 0) ? slew_rate : -slew_rate; + } + + phase += current_delta; + int8_t re = sine_table_i8[((phase + 0x40000000) & 0xFF000000) >> 24]; + int8_t im = sine_table_i8[(phase & 0xFF000000) >> 24]; + + buffer.p[i] = {re, im}; + } +} + +void RTTYTXProcessor::advance_state() { + switch (state) { + case State::Idle: + state = State::LeadIn; + lead_counter = 0; + break; + + case State::LeadIn: + if (buffer_pop(current_char)) { + state = State::StartBit; + baud_phase = 0; + baud_phase_increment = base_baud_phase_increment; + } else { + state = State::LeadOut; + lead_counter = 0; + } + break; + + case State::StartBit: + state = State::DataBits; + bit_pos = 0; + break; + + case State::DataBits: + bit_pos++; + if (bit_pos >= 5) { + state = State::StopBit; + } + break; + + case State::StopBit: + if (buffer_pop(current_char)) { + state = State::StartBit; + } else { + state = State::LeadOut; + lead_counter = 0; + } + break; + + case State::LeadOut: + break; + } +} + +void RTTYTXProcessor::configure( + uint16_t baud, + uint16_t shift, + int16_t mark_tone_, + int16_t space_tone_, + uint8_t stop_bits_, + bool inverted_) { + if (baud == 0) return; + + // baud phase increment + uint32_t new_base_baud_inc = (uint32_t)((uint64_t)baud * UINT32_MAX / (baseband_fs * 100ULL)); + + // Stop bits + configured_stop_bits = stop_bits_; + if (configured_stop_bits < 2) configured_stop_bits = 2; + + int32_t freq_mark = mark_tone_; + int32_t freq_space = space_tone_; + + if (inverted_) { + std::swap(freq_mark, freq_space); + } + + uint32_t new_delta_mark = hz_to_delta(freq_mark, baseband_fs); + uint32_t new_delta_space = hz_to_delta(freq_space, baseband_fs); + + // Slew rate + uint32_t samples_per_bit = (uint32_t)((uint64_t)UINT32_MAX / new_base_baud_inc); + uint32_t transition_samples = samples_per_bit / 10; + if (transition_samples == 0) transition_samples = 1; + + uint32_t shift_delta = hz_to_delta(shift, baseband_fs); + uint32_t new_slew_rate = shift_delta / transition_samples; + if (new_slew_rate == 0) new_slew_rate = 1; + + base_baud_phase_increment = new_base_baud_inc; + delta_mark = new_delta_mark; + delta_space = new_delta_space; + slew_rate = new_slew_rate; + + if (!configured) { + current_delta = delta_mark; + lead_counter = 0; + phase = 0; + baud_phase = 0; + baud_phase_increment = base_baud_phase_increment; + configured = true; + } +} + +void RTTYTXProcessor::on_message(const Message* const msg) { + if (msg->id == Message::ID::RTTYData) { + const auto& rtty_msg = *reinterpret_cast(msg); + configure(rtty_msg.baud, + rtty_msg.shift, + rtty_msg.mark_tone, + rtty_msg.space_tone, + rtty_msg.stopbits, + rtty_msg.inverted); + + for (int i = 0; i < 15; i++) { + buffer_push(0x1F); // LTRS + } + + buffer_push(0x08); // CR + buffer_push(0x02); // LF + + for (uint16_t i = 0; i < rtty_msg.data_len && i < rtty_msg.max_len; i++) { + buffer_push(rtty_msg.data[i]); + } + buffer_push(0x08); // CR + buffer_push(0x02); // LF + + if (state == State::Idle) { + state = State::LeadIn; + lead_counter = 0; + } + } +} + +// Ring Buffer Logic +bool RTTYTXProcessor::buffer_push(uint8_t byte) { + size_t next_head = (head + 1) % data_buffer.size(); + if (next_head == tail) return false; + data_buffer[head] = byte; + head = next_head; + return true; +} + +bool RTTYTXProcessor::buffer_pop(uint8_t& byte) { + if (head == tail) return false; + byte = data_buffer[tail]; + tail = (tail + 1) % data_buffer.size(); + return true; +} + +bool RTTYTXProcessor::buffer_empty() const { + return head == tail; +} + +int main() { + EventDispatcher event_dispatcher{std::make_unique()}; + event_dispatcher.run(); + return 0; +} \ No newline at end of file diff --git a/firmware/baseband/proc_rtty_tx.hpp b/firmware/baseband/proc_rtty_tx.hpp new file mode 100644 index 000000000..2db628993 --- /dev/null +++ b/firmware/baseband/proc_rtty_tx.hpp @@ -0,0 +1,90 @@ +/* + * Copyright (C) 2026 HTotoo + * + * 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_RTTY_TX_H__ +#define __PROC_RTTY_TX_H__ + +#include "baseband_processor.hpp" +#include "baseband_thread.hpp" +#include "portapack_shared_memory.hpp" +#include + +class RTTYTXProcessor : public BasebandProcessor { + public: + void execute(const buffer_c8_t& buffer) override; + void on_message(const Message* const msg) override; + + private: + static constexpr uint32_t baseband_fs = 2048000; + + bool configured{false}; + + // RTTY Configuration + uint32_t samples_per_bit{0}; + + // Stop bits configuration: 2=1.0, 3=1.5, 4=2.0 + uint8_t configured_stop_bits{2}; + + // FSK State + uint32_t delta_mark{0}; // Phase step for Mark + uint32_t delta_space{0}; // Phase step for Space + uint32_t current_delta{0}; // Smoothed delta + uint32_t slew_rate{0}; // Max change per sample + uint32_t phase{0}; // Phase accumulator + + // Precision Timing + uint32_t baud_phase{0}; + uint32_t baud_phase_increment{0}; + uint32_t base_baud_phase_increment{0}; // Store the standard 1.0 bit rate + + // State Machine + enum class State { + Idle, + LeadIn, + StartBit, + DataBits, + StopBit, + LeadOut + }; + State state{State::Idle}; + + uint32_t lead_counter{0}; + uint8_t current_char{0}; + uint8_t bit_pos{0}; + + // Ring Buffer + std::array data_buffer{}; + volatile size_t head{0}; + volatile size_t tail{0}; + + void configure(uint16_t baud, uint16_t shift, int16_t mark_tone_, int16_t space_tone_, uint8_t stop_bits_, bool inverted_); + void advance_state(); + + bool buffer_push(uint8_t byte); + bool buffer_pop(uint8_t& byte); + bool buffer_empty() const; + + TXProgressMessage txprogress_message{}; + + BasebandThread baseband_thread{baseband_fs, this, baseband::Direction::Transmit}; +}; + +#endif /* __PROC_RTTY_TX_H__ */ \ No newline at end of file diff --git a/firmware/common/message.hpp b/firmware/common/message.hpp index 376a3e40b..9b91af233 100644 --- a/firmware/common/message.hpp +++ b/firmware/common/message.hpp @@ -153,6 +153,7 @@ class Message { MorseTXConfigure = 95, MorseTXkey = 96, StreamTXConfiguration = 97, + RTTYData = 98, MAX }; @@ -1770,4 +1771,26 @@ class StreamTXConfigurationMessage : public Message { uint8_t mode = 0; // am = 0, 2fsk = 1 }; +class RTTYDataMessage : public Message { + public: + constexpr RTTYDataMessage(uint16_t baud = 4545, uint16_t shift = 170, int16_t mark_tone = -85, int16_t space_tone = 85, uint8_t stopbits = 3, bool inverted = false) + : Message{ID::RTTYData}, + baud(baud), + shift(shift), + mark_tone(mark_tone), + space_tone(space_tone), + inverted(inverted), + stopbits(stopbits) { + } + uint8_t data[490]{0}; // 5bit data, stored on 8 bits. + uint16_t data_len = 0; // count of data sent + uint16_t baud = 4545; // /100 baud 45.45 = 4545 + uint16_t shift = 170; // hz + int16_t mark_tone = 0; // for tx., hz + int16_t space_tone = 170; // hz + bool inverted = false; // for tx, if true, mark and space tones are swapped. + uint8_t stopbits = 3; // doubled value is stored here, so stop 2 = 1 stop bit, 3 = 1.5, 4 = 2. + static constexpr uint16_t max_len = 490; +}; + #endif /*__MESSAGE_H__*/ diff --git a/firmware/common/spi_image.hpp b/firmware/common/spi_image.hpp index 4412c66a4..2cff109ef 100644 --- a/firmware/common/spi_image.hpp +++ b/firmware/common/spi_image.hpp @@ -126,6 +126,8 @@ constexpr image_tag_t image_tag_noaaapt_rx{'P', 'N', 'O', 'A'}; constexpr image_tag_t image_tag_sstv_rx{'P', 'S', 'R', 'X'}; constexpr image_tag_t image_tag_morse{'P', 'M', 'R', 'S'}; constexpr image_tag_t image_tag_morsetx{'P', 'M', 'R', 'T'}; +constexpr image_tag_t image_tag_rttyrx{'P', 'R', 'T', 'R'}; +constexpr image_tag_t image_tag_rttytx{'P', 'R', 'T', 'T'}; constexpr image_tag_t image_tag_noop{'P', 'N', 'O', 'P'};