From bae3854d272d91bf4fe13c1953649a7e597a9590 Mon Sep 17 00:00:00 2001 From: Sarah Rose <156587765+SarahRoseLives@users.noreply.github.com> Date: Thu, 5 Mar 2026 01:49:17 -0500 Subject: [PATCH] Add SAME TX external app (#3069) * Add SAME TX external app Adds a Specific Area Message Encoding (SAME/EAS) transmitter as an external app (.ppma) for HackRF PortaPack. SAME is the digital protocol used by NOAA Weather Radio and the Emergency Alert System in the US and Canada for broadcasting emergency alerts (tornado warnings, flash floods, EAN, etc.) on 162.400-162.550 MHz and AM/FM broadcast stations. Features: - Originator code selector (WXR, EAS, CIV, PEP) - Event code selector: 26 standard SAME event codes (RWT, TOR, SVR, FFW, EAN, etc.) with live name display - FIPS location code entry (state 00-99 + county 000-999) - Purge time entry (hours + minutes) - Live message preview showing the encoded header - AFSK encoding: mark=2083Hz, space=1563Hz, 520.833 baud - FM deviation: 5 kHz, repeated 3x per SAME spec - Default frequency: 162.550 MHz (WX channel 7) Protocol details: - 16-byte 0xAB preamble (bit-reversed for LSB-first AFSK) - Header: ZCZC-ORG-EVT-SSPCCC+HHMM-JJJHHMM-STATION- - Uses existing PAFT (proc_afsk) baseband image * Apply clang-format via format-code.sh * Add GPL license headers, guard re-entry, call set_transmitting(true) - Add standard PortaPack GPL-2.0 license header to ui_same_tx.cpp and .hpp - Guard start_tx() with tx_active_ to prevent re-entrant calls - Call tx_view.set_transmitting(true) after starting TX so the TransmitterView button switches to stop mode --- firmware/application/external/external.cmake | 5 + firmware/application/external/external.ld | 7 + .../application/external/same_tx/main.cpp | 79 ++++++ .../external/same_tx/ui_same_tx.cpp | 239 ++++++++++++++++++ .../external/same_tx/ui_same_tx.hpp | 84 ++++++ 5 files changed, 414 insertions(+) create mode 100644 firmware/application/external/same_tx/main.cpp create mode 100644 firmware/application/external/same_tx/ui_same_tx.cpp create mode 100644 firmware/application/external/same_tx/ui_same_tx.hpp diff --git a/firmware/application/external/external.cmake b/firmware/application/external/external.cmake index eb0b942f3..ea394d35c 100644 --- a/firmware/application/external/external.cmake +++ b/firmware/application/external/external.cmake @@ -103,6 +103,10 @@ set(EXTCPPSRC external/sstvtx/main.cpp external/sstvtx/ui_sstvtx.cpp + #same_tx + external/same_tx/main.cpp + external/same_tx/ui_same_tx.cpp + #sstvrx external/sstvrx/main.cpp external/sstvrx/ui_sstvrx.cpp @@ -343,6 +347,7 @@ set(EXTAPPLIST adsbtx #morse_tx sstvtx + same_tx sstvrx random_password acars_rx diff --git a/firmware/application/external/external.ld b/firmware/application/external/external.ld index 5794c6282..5b40d1908 100644 --- a/firmware/application/external/external.ld +++ b/firmware/application/external/external.ld @@ -100,6 +100,7 @@ MEMORY ram_external_app_tpmstx (rwx) : org = 0xADFB0000, len = 32k ram_external_app_pocsag_tx (rwx) : org = 0xADFC0000, len = 32k ram_external_app_time_sink (rwx) : org = 0xADFD0000, len = 32k + ram_external_app_same_tx (rwx) : org = 0xADFE0000, len = 32k } @@ -567,5 +568,11 @@ SECTIONS KEEP(*(.external_app.app_time_sink.application_information)); *(*ui*external_app*time_sink*); } > ram_external_app_time_sink + + .external_app_same_tx : ALIGN(4) SUBALIGN(4) + { + KEEP(*(.external_app.app_same_tx.application_information)); + *(*ui*external_app*same_tx*); + } > ram_external_app_same_tx } diff --git a/firmware/application/external/same_tx/main.cpp b/firmware/application/external/same_tx/main.cpp new file mode 100644 index 000000000..c27355fbb --- /dev/null +++ b/firmware/application/external/same_tx/main.cpp @@ -0,0 +1,79 @@ +/* + * SAME TX - Specific Area Message Encoding Transmitter + * + * 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. + */ + +#include "ui.hpp" +#include "ui_same_tx.hpp" +#include "ui_navigation.hpp" +#include "external_app.hpp" + +namespace ui::external_app::same_tx { +void initialize_app(ui::NavigationView& nav) { + nav.push(); +} +} // namespace ui::external_app::same_tx + +extern "C" { + +__attribute__((section(".external_app.app_same_tx.application_information"), used)) application_information_t _application_information_same_tx = { + /*.memory_location = */ (uint8_t*)0x00000000, + /*.externalAppEntry = */ ui::external_app::same_tx::initialize_app, + /*.header_version = */ CURRENT_HEADER_VERSION, + /*.app_version = */ VERSION_MD5, + + /*.app_name = */ "SAME TX", + /*.bitmap_data = */ { + // 16x16 icon - simple "!" alert symbol + 0x00, + 0x00, + 0x18, + 0x00, + 0x3C, + 0x00, + 0x3C, + 0x00, + 0x3C, + 0x00, + 0x3C, + 0x00, + 0x18, + 0x00, + 0x18, + 0x00, + 0x00, + 0x00, + 0x18, + 0x00, + 0x3C, + 0x00, + 0x3C, + 0x00, + 0x18, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 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_afsk */ {'P', 'A', 'F', 'T'}, + /*.m4_app_offset = */ 0x00000000, +}; +} diff --git a/firmware/application/external/same_tx/ui_same_tx.cpp b/firmware/application/external/same_tx/ui_same_tx.cpp new file mode 100644 index 000000000..849ee6c85 --- /dev/null +++ b/firmware/application/external/same_tx/ui_same_tx.cpp @@ -0,0 +1,239 @@ +/* + * Copyright (C) 2024 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_same_tx.hpp" +#include "theme.hpp" +#include "baseband_api.hpp" +#include "portapack.hpp" +#include "portapack_shared_memory.hpp" +#include "transmitter_model.hpp" + +using namespace portapack; + +namespace ui::external_app::same_tx { + +static constexpr uint32_t SAME_SAMPLES_PER_BIT = 2949; +static constexpr uint32_t SAME_MARK_FREQ = 2083; +static constexpr uint32_t SAME_SPACE_FREQ = 1563; +static constexpr uint32_t SAME_FM_DEVIATION = 5000; +static constexpr uint8_t SAME_REPEAT = 3; + +static constexpr char SAME_ORG_CODES[][4] = {"WXR", "EAS", "CIV", "PEP"}; +static constexpr char SAME_EVT_CODES[][4] = { + "RWT", + "RMT", + "NPT", + "NST", + "NMT", + "EAN", + "EAT", + "NIC", + "ADR", + "AVA", + "AVW", + "BZW", + "CFW", + "CFS", + "DSW", + "EQW", + "EVI", + "FFW", + "FFS", + "FFH", + "FRW", + "HLS", + "HUW", + "HUH", + "SVR", + "TOR", +}; + +static uint8_t bitrev8(uint8_t b) { + b = (b & 0xF0) >> 4 | (b & 0x0F) << 4; + b = (b & 0xCC) >> 2 | (b & 0x33) << 2; + b = (b & 0xAA) >> 1 | (b & 0x55) << 1; + return b; +} + +static void fmt2d(char* buf, int v) { + buf[0] = '0' + v / 10; + buf[1] = '0' + v % 10; +} +static void fmt3d(char* buf, int v) { + buf[0] = '0' + v / 100; + buf[1] = '0' + (v / 10) % 10; + buf[2] = '0' + v % 10; +} + +void SameTxView::update_msg_preview() { + const char* org = SAME_ORG_CODES[field_org.selected_index()]; + const char* evt = SAME_EVT_CODES[field_event_idx.value()]; + int ss = field_fips_state.value(); + int ccc = field_fips_county.value(); + int dh = field_dur_h.value(); + int dm = field_dur_m.value(); + char buf[48]; + char* p = buf; + // "WXR-RWT-SS0CCC+HHmm" (preview, no ZCZC header) + for (const char* s = org; *s; s++) *p++ = *s; + *p++ = '-'; + for (const char* s = evt; *s; s++) *p++ = *s; + *p++ = '-'; + fmt2d(p, ss); + p += 2; + *p++ = '0'; + fmt3d(p, ccc); + p += 3; + *p++ = '+'; + fmt2d(p, dh); + p += 2; + fmt2d(p, dm); + p += 2; + *p = '\0'; + text_msg.set(buf); +} + +void SameTxView::start_tx() { + const char* org = SAME_ORG_CODES[field_org.selected_index()]; + const char* evt = SAME_EVT_CODES[field_event_idx.value()]; + int ss = field_fips_state.value(); + int ccc = field_fips_county.value(); + int dh = field_dur_h.value(); + int dm = field_dur_m.value(); + + // Build full SAME message: "ZCZC-ORG-EVT-SS0CCC+HHMM-0010000-STATION-" + char msg[64]; + char* p = msg; + const char* hdr = "ZCZC-"; + for (const char* s = hdr; *s; s++) *p++ = *s; + for (const char* s = org; *s; s++) *p++ = *s; + *p++ = '-'; + for (const char* s = evt; *s; s++) *p++ = *s; + *p++ = '-'; + fmt2d(p, ss); + p += 2; + *p++ = '0'; + fmt3d(p, ccc); + p += 3; + *p++ = '+'; + fmt2d(p, dh); + p += 2; + fmt2d(p, dm); + p += 2; + *p++ = '-'; + // Issue time: fixed 0010000 (placeholder), station: SAME-TX + const char* tail = "0010000-SAMETX--"; + for (const char* s = tail; *s; s++) *p++ = *s; + *p = '\0'; + + auto* words = reinterpret_cast(shared_memory.bb_data.data); + size_t idx = 0; + for (uint8_t i = 0; i < 16; i++) + words[idx++] = bitrev8(0xAB); + for (const char* c = msg; *c; c++) + words[idx++] = bitrev8(static_cast(*c)); + words[idx] = 0; + + tx_active_ = true; + text_status.set("Transmitting..."); + transmitter_model.enable(); + baseband::set_afsk_data( + SAME_SAMPLES_PER_BIT, + SAME_MARK_FREQ, + SAME_SPACE_FREQ, + SAME_REPEAT, + SAME_FM_DEVIATION, + 8); +} + +void SameTxView::stop_tx() { + baseband::kill_afsk(); + transmitter_model.disable(); + tx_active_ = false; + text_status.set("Ready"); + tx_view.set_transmitting(false); +} + +void SameTxView::on_tx_progress(const uint32_t progress, const bool done) { + if (done) { + stop_tx(); + text_status.set("Done!"); + } else { + (void)progress; + text_status.set("TX..."); + } +} + +void SameTxView::focus() { + field_org.focus(); +} + +SameTxView::SameTxView(NavigationView& nav) + : nav_{nav} { + baseband::run_image(portapack::spi_flash::image_tag_afsk); + + add_children({ + &labels, + &field_org, + &field_event_idx, + &text_evt, + &field_fips_state, + &field_fips_county, + &field_dur_h, + &field_dur_m, + &text_msg, + &text_status, + &tx_view, + }); + + // Defaults: Ohio, county 007 (Ashtabula), 30 min + field_fips_state.set_value(39); + field_fips_county.set_value(7); + field_dur_h.set_value(0); + field_dur_m.set_value(30); + + auto refresh = [this](int32_t) { + text_evt.set(SAME_EVT_CODES[field_event_idx.value()]); + update_msg_preview(); + }; + field_event_idx.on_change = refresh; + field_org.on_change = [this](size_t, int32_t) { update_msg_preview(); }; + field_fips_state.on_change = [this](int32_t) { update_msg_preview(); }; + field_fips_county.on_change = [this](int32_t) { update_msg_preview(); }; + field_dur_h.on_change = [this](int32_t) { update_msg_preview(); }; + field_dur_m.on_change = [this](int32_t) { update_msg_preview(); }; + + update_msg_preview(); + + tx_view.on_start = [this]() { + if (!tx_active_) { + start_tx(); + tx_view.set_transmitting(true); + } + }; + tx_view.on_stop = [this]() { stop_tx(); }; +} + +SameTxView::~SameTxView() { + transmitter_model.disable(); + baseband::shutdown(); +} + +} // namespace ui::external_app::same_tx diff --git a/firmware/application/external/same_tx/ui_same_tx.hpp b/firmware/application/external/same_tx/ui_same_tx.hpp new file mode 100644 index 000000000..2331935d7 --- /dev/null +++ b/firmware/application/external/same_tx/ui_same_tx.hpp @@ -0,0 +1,84 @@ +#ifndef __UI_SAME_TX_H__ +#define __UI_SAME_TX_H__ + +/* + * Copyright (C) 2024 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_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::same_tx { + +class SameTxView : public View { + public: + SameTxView(NavigationView& nav); + ~SameTxView(); + SameTxView(const SameTxView&) = delete; + SameTxView& operator=(const SameTxView&) = delete; + void focus() override; + std::string title() const override { return "SAME TX"; } + + private: + NavigationView& nav_; + bool tx_active_{false}; + void start_tx(); + void stop_tx(); + void on_tx_progress(const uint32_t progress, const bool done); + void update_msg_preview(); + + TxRadioState radio_state_{162550000, 1750000, AFSK_TX_SAMPLERATE}; + app_settings::SettingsManager settings_{"tx_same", app_settings::Mode::TX}; + + Labels labels{{ + {{0 * 8, 0 * 16}, "Org:", ui::Theme::getInstance()->fg_light->foreground}, + {{8 * 8, 0 * 16}, "Evt:", ui::Theme::getInstance()->fg_light->foreground}, + {{0 * 8, 1 * 16}, "FIPS:", ui::Theme::getInstance()->fg_light->foreground}, + {{16 * 8, 1 * 16}, "Dur:", ui::Theme::getInstance()->fg_light->foreground}, + }}; + OptionsField field_org{ + {4 * 8, 0 * 16}, + 3, + {{"WXR", 0}, {"EAS", 1}, {"CIV", 2}, {"PEP", 3}}}; + NumberField field_event_idx{{12 * 8, 0 * 16}, 2, {0, 25}, 1, '0', true}; + Text text_evt{{15 * 8, 0 * 16, 3 * 8, 16}, "RWT"}; + NumberField field_fips_state{{5 * 8, 1 * 16}, 2, {0, 99}, 1, '0'}; + NumberField field_fips_county{{7 * 8 + 4, 1 * 16}, 3, {0, 999}, 1, '0'}; + NumberField field_dur_h{{20 * 8, 1 * 16}, 2, {0, 99}, 1, '0'}; + NumberField field_dur_m{{22 * 8, 1 * 16}, 2, {0, 59}, 1, '0'}; + Text text_msg{{0, 2 * 16, 240, 16}, ""}; + Text text_status{{0, 3 * 16, 240, 16}, "Ready"}; + TransmitterView tx_view{(int16_t)UI_POS_Y_BOTTOM(4), 1000, 0}; + MessageHandlerRegistration message_handler_tx_progress{ + Message::ID::TXProgress, + [this](const Message* const p) { + const auto msg = *reinterpret_cast(p); + this->on_tx_progress(msg.progress, msg.done); + }}; +}; + +} // namespace ui::external_app::same_tx +#endif