Add MDC-1200 TX external app (#3083)

Implements MDC-1200 single and double-packet TX as a PPMA external app.

Features:
- Supports PTT ID, Emergency, Radio Check, Stun/Unstun, Call Alert,
  Selective Call (1 & 2), Status Request/Response, Remote Monitor, Message
- Auto-fills Arg byte on Op selection
- AFSK FSK modulation: 1800 Hz mark / 1200 Hz space, 1200 baud
- NRZI encoding with Golay FEC per MDC-1200 spec
- Tap-to-edit frequency via keypad
- Default TX gain of 30 dB
- Uses AFSK baseband (same as POCSAG/SAME TX)
- All buffers kept off the stack to avoid hard faults in ext app environment

* mdc_tx: address PR review feedback

- Move MDC helpers into ui::external_app::mdc_tx namespace (drop anonymous namespace)
- Use AFSK_TX_SAMPLERATE macro instead of hardcoded 1536000
- Add #include "modems.hpp" explicitly for AFSK_TX_SAMPLERATE
- Replace n*8/n*16 pixel positions with UI_POS_X/UI_POS_Y macros
- Fix field comments: 'hex bytes' -> 'decimal bytes (0-255)'
- Fix TransmitterView frequency_step: 1750000 -> 12500 (12.5kHz NFM step)
- Fix MDC preamble: replace leading 0x00 bytes with 0xAA
  after NRZI encoding stayed 0x00, terminating TX on the first word.
  0xAA bytes NRZI-encode to 0xFF (continuous 1800Hz mark tone), which
  is the standard MDC-1200 preamble and does not trigger the sentinel.
This commit is contained in:
Sarah Rose
2026-03-16 14:28:07 -04:00
committed by GitHub
parent 52dd2ea2bb
commit 5c2d1d3dfb
5 changed files with 610 additions and 104 deletions
+83
View File
@@ -0,0 +1,83 @@
/*
* Copyright (C) 2025 Sarah Rose
*
* 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_mdc_tx.hpp"
#include "ui_navigation.hpp"
#include "external_app.hpp"
namespace ui::external_app::mdc_tx {
void initialize_app(ui::NavigationView& nav) {
nav.push<MdcTxView>();
}
} // namespace ui::external_app::mdc_tx
extern "C" {
__attribute__((section(".external_app.app_mdc_tx.application_information"), used))
application_information_t _application_information_mdc_tx = {
/*.memory_location = */ (uint8_t*)0x00000000,
/*.externalAppEntry = */ ui::external_app::mdc_tx::initialize_app,
/*.header_version = */ CURRENT_HEADER_VERSION,
/*.app_version = */ VERSION_MD5,
/*.app_name = */ "MDC-1200 TX",
/*.bitmap_data = */ {
0x00,
0x00,
0xFE,
0x7F,
0xFF,
0xFF,
0xBB,
0xD0,
0xFF,
0xFF,
0xFF,
0xFF,
0x0B,
0xE1,
0xFF,
0xFF,
0xFF,
0xFF,
0xEB,
0xD0,
0xFF,
0xFF,
0xFE,
0x7F,
0x70,
0x00,
0x30,
0x00,
0x10,
0x00,
0x00,
0x00,
},
/*.icon_color = */ ui::Color::orange().v,
/*.menu_location = */ app_location_t::TX,
/*.desired_menu_position = */ -1,
/*.m4_app_tag = */ {'P', 'A', 'F', 'T'},
/*.m4_app_offset = */ 0x00000000,
};
}
+289
View File
@@ -0,0 +1,289 @@
/*
* Copyright (C) 2025 Sarah Rose
*
* 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_mdc_tx.hpp"
#include "baseband_api.hpp"
#include "portapack.hpp"
#include "portapack_shared_memory.hpp"
#include "transmitter_model.hpp"
#include "ui/ui_receiver.hpp"
using namespace portapack;
namespace ui::external_app::mdc_tx {
static constexpr uint32_t MDC_SAMPLES_PER_BIT = AFSK_TX_SAMPLERATE / 1200U;
static constexpr uint32_t MDC_MARK_FREQ = 1800U;
static constexpr uint32_t MDC_SPACE_FREQ = 1200U;
static constexpr uint32_t MDC_FM_DEVIATION = 2500U;
static constexpr uint8_t MDC_PREAMBLE[7] = {0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA};
static constexpr uint8_t MDC_SYNC[5] = {0x07, 0x09, 0x2A, 0x44, 0x6F};
static constexpr int MDC_NUM_PREAMBLES = 3;
static constexpr uint8_t MDC_DP_OP = 0x35;
static constexpr uint8_t MDC_DP_ARG = 0x89;
static constexpr uint8_t MDC_DP_CALL_ARG = 0x0D;
static uint8_t mdc_flip8(uint8_t in) {
uint8_t out = 0;
for (int i = 0; i < 8; i++) {
out = static_cast<uint8_t>((out << 1) | (in & 1));
in >>= 1;
}
return out;
}
static uint16_t mdc_flip16(uint16_t in) {
uint16_t out = 0;
for (int i = 0; i < 16; i++) {
out = static_cast<uint16_t>((out << 1) | (in & 1));
in >>= 1;
}
return out;
}
static uint16_t mdc_crc(const uint8_t* data, int length) {
uint16_t crc = 0;
for (int i = 0; i < length; i++) {
uint16_t c = mdc_flip8(data[i]);
for (int j = 0x80; j != 0; j >>= 1) {
uint16_t bit = crc & 0x8000;
crc = static_cast<uint16_t>(crc << 1);
if (c & static_cast<uint16_t>(j)) bit ^= 0x8000;
if (bit) crc ^= 0x1021;
}
}
crc = mdc_flip16(crc);
crc ^= 0xFFFF;
return crc;
}
// CRC + convolutional encoding + bit interleaving for a 14-byte codeword.
// Uses lbits_ (class member) so no large allocation hits the stack.
void MdcTxView::pack_codeword(uint8_t cw[14]) {
uint16_t crc = mdc_crc(cw, 4);
cw[4] = static_cast<uint8_t>(crc & 0xFF);
cw[5] = static_cast<uint8_t>((crc >> 8) & 0xFF);
cw[6] = 0x00;
// Convolutional encoding — csr[7] is only 7 bytes, fine on stack
uint8_t csr[7] = {};
for (int i = 0; i < 7; i++) {
cw[i + 7] = 0;
for (int j = 0; j <= 7; j++) {
for (int k = 6; k > 0; k--) csr[k] = csr[k - 1];
csr[0] = (cw[i] >> j) & 1;
int b = csr[0] + csr[2] + csr[5] + csr[6];
cw[i + 7] |= static_cast<uint8_t>((b & 1) << j);
}
}
// Bit interleaving — uses lbits_ class member (112 bytes off the stack)
for (int i = 0; i < 112; i++) lbits_[i] = 0;
int l = 0, m = 0;
for (int i = 0; i < 14; i++) {
for (int j = 0; j <= 7; j++) {
lbits_[l] = (cw[i] >> j) & 1;
l += 16;
if (l > 111) {
m++;
l = m;
}
}
}
l = 0;
for (int i = 0; i < 14; i++) {
cw[i] = 0;
for (int j = 7; j >= 0; j--) {
if (lbits_[l]) cw[i] |= static_cast<uint8_t>(1 << j);
l++;
}
}
}
// Writes preamble + sync + one packed codeword into tx_stream_, returns length.
int MdcTxView::build_single(uint8_t op, uint8_t arg, uint8_t id_hi, uint8_t id_lo) {
int idx = 0;
for (int i = 0; i < MDC_NUM_PREAMBLES; i++)
for (int j = 0; j < 7; j++) tx_stream_[idx++] = MDC_PREAMBLE[j];
for (int j = 0; j < 5; j++) tx_stream_[idx++] = MDC_SYNC[j];
uint8_t cw[14] = {};
cw[0] = op;
cw[1] = arg;
cw[2] = id_hi;
cw[3] = id_lo;
pack_codeword(cw);
for (int j = 0; j < 14; j++) tx_stream_[idx++] = cw[j];
return idx;
}
// Writes preamble + sync + two packed codewords into tx_stream_, returns length.
int MdcTxView::build_double(uint8_t op1, uint8_t arg1, uint8_t id1_hi, uint8_t id1_lo, uint8_t op2, uint8_t arg2, uint8_t id2_hi, uint8_t id2_lo) {
int idx = 0;
for (int i = 0; i < MDC_NUM_PREAMBLES; i++)
for (int j = 0; j < 7; j++) tx_stream_[idx++] = MDC_PREAMBLE[j];
for (int j = 0; j < 5; j++) tx_stream_[idx++] = MDC_SYNC[j];
uint8_t cw1[14] = {};
cw1[0] = op1;
cw1[1] = arg1;
cw1[2] = id1_hi;
cw1[3] = id1_lo;
pack_codeword(cw1);
for (int j = 0; j < 14; j++) tx_stream_[idx++] = cw1[j];
uint8_t cw2[14] = {};
cw2[0] = op2;
cw2[1] = arg2;
cw2[2] = id2_hi;
cw2[3] = id2_lo;
pack_codeword(cw2);
for (int j = 0; j < 14; j++) tx_stream_[idx++] = cw2[j];
return idx;
}
MdcTxView::MdcTxView(NavigationView& nav)
: nav_{nav} {
baseband::run_image(portapack::spi_flash::image_tag_afsk);
add_children({
&labels_,
&field_op,
&field_id_hi,
&field_id_lo,
&field_arg,
&field_src_hi,
&field_src_lo,
&text_status,
&tx_view,
});
tx_view.on_start = [this]() {
if (!tx_active_) {
start_tx();
tx_view.set_transmitting(true);
}
};
tx_view.on_stop = [this]() { stop_tx(); };
transmitter_model.set_tx_gain(30);
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);
};
};
field_op.on_change = [this](size_t, int32_t op) {
switch (op) {
case 0xFE:
field_arg.set_value(0x0C);
break; // Unstun
case 0x2B:
field_arg.set_value(0x00);
break; // Stun
case 0x63:
field_arg.set_value(0x85);
break; // Radio Check
case 0x11:
field_arg.set_value(0x8A);
break; // Remote Monitor
default:
field_arg.set_value(0x00);
break;
}
};
}
MdcTxView::~MdcTxView() {
transmitter_model.disable();
baseband::shutdown();
}
void MdcTxView::focus() {
field_op.focus();
}
void MdcTxView::start_tx() {
int32_t raw_op = field_op.selected_index_value();
uint8_t op = (raw_op == 0xFE) ? 0x2B : static_cast<uint8_t>(raw_op);
uint8_t arg = static_cast<uint8_t>(field_arg.value());
uint8_t id_hi = static_cast<uint8_t>(field_id_hi.value());
uint8_t id_lo = static_cast<uint8_t>(field_id_lo.value());
uint8_t src_hi = static_cast<uint8_t>(field_src_hi.value());
uint8_t src_lo = static_cast<uint8_t>(field_src_lo.value());
int stream_len;
bool is_double = (op == 0x80 || op == 0x81 || op == 0x82);
if (is_double) {
stream_len = build_double(MDC_DP_OP, MDC_DP_ARG, id_hi, id_lo,
op, MDC_DP_CALL_ARG, src_hi, src_lo);
} else {
stream_len = build_single(op, arg, id_hi, id_lo);
}
// NRZI encode tx_stream_ into shared memory words
auto* words = reinterpret_cast<uint16_t*>(shared_memory.bb_data.data);
uint8_t prev_bit = 0;
for (int i = 0; i < stream_len; i++) {
uint8_t raw = tx_stream_[i];
uint8_t nrzi = 0;
for (int bit = 7; bit >= 0; bit--) {
uint8_t rb = (raw >> bit) & 1;
uint8_t nb = rb ^ prev_bit;
prev_bit = rb;
nrzi = static_cast<uint8_t>((nrzi << 1) | nb);
}
words[i] = nrzi;
}
words[stream_len] = 0;
tx_active_ = true;
text_status.set("TX...");
transmitter_model.enable();
baseband::set_afsk_data(
MDC_SAMPLES_PER_BIT,
MDC_MARK_FREQ,
MDC_SPACE_FREQ,
1,
MDC_FM_DEVIATION,
8);
}
void MdcTxView::stop_tx() {
baseband::kill_afsk();
transmitter_model.disable();
tx_active_ = false;
text_status.set("Ready");
tx_view.set_transmitting(false);
}
void MdcTxView::on_tx_progress(uint32_t, bool done) {
if (done) {
stop_tx();
text_status.set("Done!");
}
}
} // namespace ui::external_app::mdc_tx
+124
View File
@@ -0,0 +1,124 @@
/*
* Copyright (C) 2025 Sarah Rose
*
* 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_MDC_TX_H__
#define __UI_MDC_TX_H__
#include "ui.hpp"
#include "ui_widget.hpp"
#include "ui_navigation.hpp"
#include "ui_transmitter.hpp"
#include "app_settings.hpp"
#include "radio_state.hpp"
#include "message.hpp"
#include "modems.hpp"
namespace ui::external_app::mdc_tx {
class MdcTxView : public View {
public:
MdcTxView(NavigationView& nav);
~MdcTxView();
MdcTxView(const MdcTxView&) = delete;
MdcTxView& operator=(const MdcTxView&) = delete;
void focus() override;
std::string title() const override { return "MDC-1200 TX"; }
private:
NavigationView& nav_;
// TxRadioState MUST be declared before SettingsManager
TxRadioState radio_state_{
462562500,
1750000,
AFSK_TX_SAMPLERATE};
app_settings::SettingsManager settings_{
"tx_mdc_tx",
app_settings::Mode::TX};
Labels labels_{{
{{UI_POS_X(0), UI_POS_Y(0)}, "Op:", ui::Theme::getInstance()->fg_light->foreground},
{{UI_POS_X(0), UI_POS_Y(1)}, "ID:", ui::Theme::getInstance()->fg_light->foreground},
{{UI_POS_X(13), UI_POS_Y(1)}, "Arg:", ui::Theme::getInstance()->fg_light->foreground},
{{UI_POS_X(0), UI_POS_Y(2)}, "Src:", ui::Theme::getInstance()->fg_light->foreground},
}};
// Only one OptionsField allowed per external app
// Stun=Op 0x2B Arg 0x00, Unstun=Op 0x2B Arg 0x0C (auto-set via on_change)
OptionsField field_op{
{UI_POS_X(4), UI_POS_Y(0)},
14,
{{"PTT ID", 0x01},
{"Emergency", 0x00},
{"Radio Check", 0x63},
{"Stun", 0x2B},
{"Unstun", 0xFE}, // sentinel: op=0x2B, arg=0x0C
{"Call Alert", 0x81},
{"Sel Call 1", 0x80},
{"Sel Call 2", 0x82},
{"Status Req", 0x22},
{"Status Resp", 0x06},
{"Rem Monitor", 0x11},
{"Message", 0x07}}};
// Unit ID: two decimal bytes (0-255) combined into a 16-bit ID
NumberField field_id_hi{{UI_POS_X(4), UI_POS_Y(1)}, 3, {0, 255}, 1, '0'};
NumberField field_id_lo{{UI_POS_X(7), UI_POS_Y(1)}, 3, {0, 255}, 1, '0'};
// Argument byte (decimal 0-255, auto-set by op selection)
NumberField field_arg{{UI_POS_X(17), UI_POS_Y(1)}, 3, {0, 255}, 1, '0'};
// Source ID: two decimal bytes (0-255) combined into a 16-bit ID
NumberField field_src_hi{{UI_POS_X(4), UI_POS_Y(2)}, 3, {0, 255}, 1, '0'};
NumberField field_src_lo{{UI_POS_X(7), UI_POS_Y(2)}, 3, {0, 255}, 1, '0'};
Text text_status{{0, UI_POS_Y(3), UI_POS_MAXWIDTH, UI_POS_DEFAULT_HEIGHT}, "Ready"};
TransmitterView tx_view{
(int16_t)UI_POS_Y_BOTTOM(4),
12500,
0};
bool tx_active_{false};
// Class-owned scratch buffers — keep large allocations off the stack
uint8_t tx_stream_[128]{};
uint8_t lbits_[112]{};
void pack_codeword(uint8_t cw[14]);
int build_single(uint8_t op, uint8_t arg, uint8_t id_hi, uint8_t id_lo);
int build_double(uint8_t op1, uint8_t arg1, uint8_t id1_hi, uint8_t id1_lo, uint8_t op2, uint8_t arg2, uint8_t id2_hi, uint8_t id2_lo);
void start_tx();
void stop_tx();
void on_tx_progress(uint32_t progress, bool done);
MessageHandlerRegistration message_handler_tx_progress{
Message::ID::TXProgress,
[this](const Message* const p) {
const auto msg = *reinterpret_cast<const TXProgressMessage*>(p);
this->on_tx_progress(msg.progress, msg.done);
}};
};
} // namespace ui::external_app::mdc_tx
#endif