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