mirror of
https://github.com/portapack-mayhem/mayhem-firmware.git
synced 2026-09-11 17:19:29 +00:00
Rtty tx and rx (#2977)
This commit is contained in:
@@ -397,6 +397,21 @@ void set_bitstream_config(uint32_t deviation, uint8_t mode) {
|
|||||||
send_message(&message);
|
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;
|
static bool baseband_image_running = false;
|
||||||
|
|
||||||
void run_image(const spi_flash::image_tag_t image_tag) {
|
void run_image(const spi_flash::image_tag_t image_tag) {
|
||||||
|
|||||||
@@ -108,7 +108,9 @@ void set_morsetx_key(bool key_down);
|
|||||||
void set_wefax_config(uint8_t lpm, uint8_t ioc);
|
void set_wefax_config(uint8_t lpm, uint8_t ioc);
|
||||||
void set_noaaapt_config();
|
void set_noaaapt_config();
|
||||||
void set_flex_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_roger_beep();
|
||||||
void request_rssi_beep();
|
void request_rssi_beep();
|
||||||
|
|||||||
+12
@@ -292,6 +292,16 @@ set(EXTCPPSRC
|
|||||||
#morseradiotx
|
#morseradiotx
|
||||||
external/morseradiotx/main.cpp
|
external/morseradiotx/main.cpp
|
||||||
external/morseradiotx/ui_morse_radiotx.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
|
set(EXTAPPLIST
|
||||||
@@ -365,6 +375,8 @@ set(EXTAPPLIST
|
|||||||
siggen
|
siggen
|
||||||
morse_radio
|
morse_radio
|
||||||
morseradiotx
|
morseradiotx
|
||||||
|
rtty_rx
|
||||||
|
rtty_tx
|
||||||
)
|
)
|
||||||
|
|
||||||
# sdusb has type conflicts with PRALINE (HackRF Pro) - add only for non-PRALINE builds
|
# sdusb has type conflicts with PRALINE (HackRF Pro) - add only for non-PRALINE builds
|
||||||
|
|||||||
+16
@@ -94,6 +94,8 @@ MEMORY
|
|||||||
ram_external_app_morse_radio (rwx) : org = 0xADF50000, len = 32k
|
ram_external_app_morse_radio (rwx) : org = 0xADF50000, len = 32k
|
||||||
ram_external_app_waterfall_designer (rwx) : org = 0xADF60000, len = 32k
|
ram_external_app_waterfall_designer (rwx) : org = 0xADF60000, len = 32k
|
||||||
ram_external_app_morseradiotx (rwx) : org = 0xADF70000, 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
|
SECTIONS
|
||||||
@@ -526,5 +528,19 @@ SECTIONS
|
|||||||
*(*ui*external_app*morseradiotx*);
|
*(*ui*external_app*morseradiotx*);
|
||||||
} > ram_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
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+104
@@ -0,0 +1,104 @@
|
|||||||
|
#include "baudot.hpp"
|
||||||
|
#include <cctype>
|
||||||
|
|
||||||
|
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
|
||||||
@@ -0,0 +1,28 @@
|
|||||||
|
#ifndef BAUDOT_HPP
|
||||||
|
#define BAUDOT_HPP
|
||||||
|
|
||||||
|
#include <cstdint>
|
||||||
|
#include <string>
|
||||||
|
|
||||||
|
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
|
||||||
+83
@@ -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<RttyRxView>();
|
||||||
|
}
|
||||||
|
} // 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
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -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
|
||||||
@@ -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<const RTTYDataMessage*>(p);
|
||||||
|
this->on_data(message);
|
||||||
|
}};
|
||||||
|
};
|
||||||
|
|
||||||
|
} // namespace ui::external_app::rtty_rx
|
||||||
|
|
||||||
|
#endif /*__UI_RTTY_RX_H__*/
|
||||||
+104
@@ -0,0 +1,104 @@
|
|||||||
|
#include "baudot.hpp"
|
||||||
|
#include <cctype>
|
||||||
|
|
||||||
|
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
|
||||||
@@ -0,0 +1,28 @@
|
|||||||
|
#ifndef BAUDOT_HPP
|
||||||
|
#define BAUDOT_HPP
|
||||||
|
|
||||||
|
#include <cstdint>
|
||||||
|
#include <string>
|
||||||
|
|
||||||
|
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
|
||||||
+83
@@ -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<RttyTxView>();
|
||||||
|
}
|
||||||
|
} // 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
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -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<FrequencyKeypadView>(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
|
||||||
@@ -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<const TXProgressMessage*>(p);
|
||||||
|
this->on_tx_progress(message.done);
|
||||||
|
}};
|
||||||
|
};
|
||||||
|
|
||||||
|
} // namespace ui::external_app::rtty_tx
|
||||||
|
|
||||||
|
#endif /*__UI_RTTY_TX_H__*/
|
||||||
@@ -674,6 +674,22 @@ set(MODE_CPPSRC
|
|||||||
)
|
)
|
||||||
DeclareTargets(PMRS morse)
|
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
|
### SD over USB
|
||||||
|
|
||||||
set(MODE_INCDIR
|
set(MODE_INCDIR
|
||||||
|
|||||||
@@ -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<const RTTYDataMessage&>(*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<RTTYRxProcessor>()};
|
||||||
|
event_dispatcher.run();
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
@@ -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<complex16_t, 512> dst_buffer_data{};
|
||||||
|
const buffer_c16_t dst_buffer{dst_buffer_data.data(), dst_buffer_data.size()};
|
||||||
|
|
||||||
|
std::array<int16_t, 32> 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__*/
|
||||||
@@ -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 <algorithm>
|
||||||
|
|
||||||
|
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<const RTTYDataMessage*>(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<RTTYTXProcessor>()};
|
||||||
|
event_dispatcher.run();
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
@@ -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 <array>
|
||||||
|
|
||||||
|
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<uint8_t, 1024> 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__ */
|
||||||
@@ -153,6 +153,7 @@ class Message {
|
|||||||
MorseTXConfigure = 95,
|
MorseTXConfigure = 95,
|
||||||
MorseTXkey = 96,
|
MorseTXkey = 96,
|
||||||
StreamTXConfiguration = 97,
|
StreamTXConfiguration = 97,
|
||||||
|
RTTYData = 98,
|
||||||
MAX
|
MAX
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -1770,4 +1771,26 @@ class StreamTXConfigurationMessage : public Message {
|
|||||||
uint8_t mode = 0; // am = 0, 2fsk = 1
|
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__*/
|
#endif /*__MESSAGE_H__*/
|
||||||
|
|||||||
@@ -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_sstv_rx{'P', 'S', 'R', 'X'};
|
||||||
constexpr image_tag_t image_tag_morse{'P', 'M', 'R', 'S'};
|
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_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'};
|
constexpr image_tag_t image_tag_noop{'P', 'N', 'O', 'P'};
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user