diff --git a/firmware/application/external/external.cmake b/firmware/application/external/external.cmake index d8acd93e8..14d46ea4c 100644 --- a/firmware/application/external/external.cmake +++ b/firmware/application/external/external.cmake @@ -107,6 +107,10 @@ set(EXTCPPSRC external/same_tx/main.cpp external/same_tx/ui_same_tx.cpp + #mdc_tx + external/mdc_tx/main.cpp + external/mdc_tx/ui_mdc_tx.cpp + #sstvrx external/sstvrx/main.cpp external/sstvrx/ui_sstvrx.cpp @@ -360,6 +364,7 @@ set(EXTAPPLIST #morse_tx sstvtx same_tx + mdc_tx sstvrx random_password acars_rx diff --git a/firmware/application/external/external.ld b/firmware/application/external/external.ld index 41235499f..0ea98acf2 100644 --- a/firmware/application/external/external.ld +++ b/firmware/application/external/external.ld @@ -23,87 +23,88 @@ MEMORY * Also need to consider processor memory map - reading 0xADxxxxxx generates a fault which may be better than unexpected behavior. * External app address ranges below must match those in python file "external_app_info.py". */ - ram_external_app_afsk_rx (rwx) : org = 0xADB10000, len = 32k - ram_external_app_calculator (rwx) : org = 0xADB20000, len = 32k - ram_external_app_font_viewer(rwx) : org = 0xADB30000, len = 32k - ram_external_app_blespam (rwx) : org = 0xADB40000, len = 32k - ram_external_app_analogtv (rwx) : org = 0xADB50000, len = 32k - ram_external_app_nrf_rx (rwx) : org = 0xADB60000, len = 32k - ram_external_app_coasterp (rwx) : org = 0xADB70000, len = 32k - ram_external_app_lge (rwx) : org = 0xADB80000, len = 32k - ram_external_app_lcr (rwx) : org = 0xADB90000, len = 32k - ram_external_app_jammer (rwx) : org = 0xADBA0000, len = 32k - ram_external_app_gpssim (rwx) : org = 0xADBB0000, len = 32k - ram_external_app_spainter (rwx) : org = 0xADBC0000, len = 32k - ram_external_app_keyfob (rwx) : org = 0xADBD0000, len = 32k - ram_external_app_tetris (rwx) : org = 0xADBE0000, len = 32k - ram_external_app_extsensors (rwx) : org = 0xADBF0000, len = 32k - ram_external_app_foxhunt_rx (rwx) : org = 0xADC00000, len = 32k - ram_external_app_audio_test (rwx) : org = 0xADC10000, len = 32k - ram_external_app_wardrivemap(rwx) : org = 0xADC20000, len = 32k - ram_external_app_tpmsrx (rwx) : org = 0xADC30000, len = 32k - ram_external_app_protoview (rwx) : org = 0xADC40000, len = 32k - ram_external_app_adsbtx (rwx) : org = 0xADC50000, len = 32k - ram_external_app_morse_tx (rwx) : org = 0xADC60000, len = 32k - ram_external_app_sstvtx (rwx) : org = 0xADC70000, len = 32k - ram_external_app_random_password(rwx): org = 0xADC80000, len = 32k - ram_external_app_acars_rx (rwx) : org = 0xADC90000, len = 32k - ram_external_app_shoppingcart_lock(rwx): org = 0xADCA0000, len = 32k - ram_external_app_cvs_spam (rwx) : org = 0xADCB0000, len = 32k - ram_external_app_ookbrute (rwx) : org = 0xADCC0000, len = 32k - ram_external_app_flippertx (rwx) : org = 0xADCD0000, len = 32k - ram_external_app_ook_editor (rwx) : org = 0xADCE0000, len = 32k - ram_external_app_remote (rwx) : org = 0xADCF0000, len = 32k - ram_external_app_mcu_temperature(rwx): org = 0xADD00000, len = 32k - ram_external_app_fmradio (rwx) : org = 0xADD10000, len = 32k - ram_external_app_tuner (rwx) : org = 0xADD20000, len = 32k - ram_external_app_metronome (rwx) : org = 0xADD30000, len = 32k - ram_external_app_app_manager(rwx) : org = 0xADD40000, len = 32k - ram_external_app_hopper (rwx) : org = 0xADD50000, len = 32k - ram_external_app_antenna_length(rwx): org = 0xADD60000, len = 32k - ram_external_app_view_wav (rwx) : org = 0xADD70000, len = 32k - ram_external_app_sd_wipe (rwx) : org = 0xADD80000, len = 32k - ram_external_app_playlist_editor(rwx): org = 0xADD90000, len = 32k - ram_external_app_snake (rwx) : org = 0xADDA0000, len = 32k - ram_external_app_stopwatch (rwx) : org = 0xADDB0000, len = 32k - ram_external_app_wefax_rx (rwx) : org = 0xADDC0000, len = 32k - ram_external_app_breakout (rwx) : org = 0xADDD0000, len = 32k - ram_external_app_doom (rwx) : org = 0xADDE0000, len = 32k - ram_external_app_debug_pmem (rwx) : org = 0xADDF0000, len = 32k - ram_external_app_scanner (rwx) : org = 0xADE00000, len = 32k - ram_external_app_level (rwx) : org = 0xADE10000, len = 32k - ram_external_app_gfxeq (rwx) : org = 0xADE20000, len = 32k - ram_external_app_noaaapt_rx (rwx) : org = 0xADE30000, len = 32k - ram_external_app_detector_rx (rwx) : org = 0xADE40000, len = 32k - ram_external_app_dinogame (rwx) : org = 0xADE50000, len = 32k - ram_external_app_spaceinv (rwx) : org = 0xADE60000, len = 32k - ram_external_app_blackjack (rwx) : org = 0xADE70000, len = 32k - ram_external_app_battleship (rwx) : org = 0xADE80000, len = 32k - ram_external_app_ert (rwx) : org = 0xADE90000, len = 32k - ram_external_app_epirb_rx (rwx) : org = 0xADEA0000, len = 32k - ram_external_app_soundboard (rwx) : org = 0xADEB0000, len = 32k - ram_external_app_game2048 (rwx) : org = 0xADEC0000, len = 32k - ram_external_app_bht_tx (rwx) : org = 0xADED0000, len = 32k - ram_external_app_morse_practice (rwx) : org = 0xADEE0000, len = 32k - ram_external_app_adult_toys_controller (rwx) : org = 0xADEF0000, len = 32k - ram_external_app_flex_rx (rwx) : org = 0xADF00000, len = 32k - ram_external_app_sstvrx (rwx) : org = 0xADF10000, len = 32k - ram_external_app_subcarrx (rwx) : org = 0xADF20000, len = 32k - ram_external_app_siggen (rwx) : org = 0xADF30000, len = 32k - ram_external_app_sdusb (rwx) : org = 0xADF40000, 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_morseradiotx (rwx) : org = 0xADF70000, len = 32k - ram_external_app_keeloqtx (rwx) : org = 0xADF80000, len = 32k - ram_external_app_rtty_rx (rwx) : org = 0xADF90000, len = 32k - ram_external_app_rtty_tx (rwx) : org = 0xADFA0000, len = 32k - 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 - ram_external_app_kiss_tnc (rwx) : org = 0xADFF0000, len = 32k - ram_external_app_epirb_tx (rwx) : org = 0xAE000000, len = 32k - ram_external_app_fpv_detect (rwx) : org = 0xAE010000, len = 32k + ram_external_app_afsk_rx (rwx) : org = 0xADB10000, len = 32k + ram_external_app_calculator (rwx) : org = 0xADB20000, len = 32k + ram_external_app_font_viewer (rwx) : org = 0xADB30000, len = 32k + ram_external_app_blespam (rwx) : org = 0xADB40000, len = 32k + ram_external_app_analogtv (rwx) : org = 0xADB50000, len = 32k + ram_external_app_nrf_rx (rwx) : org = 0xADB60000, len = 32k + ram_external_app_coasterp (rwx) : org = 0xADB70000, len = 32k + ram_external_app_lge (rwx) : org = 0xADB80000, len = 32k + ram_external_app_lcr (rwx) : org = 0xADB90000, len = 32k + ram_external_app_jammer (rwx) : org = 0xADBA0000, len = 32k + ram_external_app_gpssim (rwx) : org = 0xADBB0000, len = 32k + ram_external_app_spainter (rwx) : org = 0xADBC0000, len = 32k + ram_external_app_keyfob (rwx) : org = 0xADBD0000, len = 32k + ram_external_app_tetris (rwx) : org = 0xADBE0000, len = 32k + ram_external_app_extsensors (rwx) : org = 0xADBF0000, len = 32k + ram_external_app_foxhunt_rx (rwx) : org = 0xADC00000, len = 32k + ram_external_app_audio_test (rwx) : org = 0xADC10000, len = 32k + ram_external_app_wardrivemap (rwx) : org = 0xADC20000, len = 32k + ram_external_app_tpmsrx (rwx) : org = 0xADC30000, len = 32k + ram_external_app_protoview (rwx) : org = 0xADC40000, len = 32k + ram_external_app_adsbtx (rwx) : org = 0xADC50000, len = 32k + ram_external_app_morse_tx (rwx) : org = 0xADC60000, len = 32k + ram_external_app_sstvtx (rwx) : org = 0xADC70000, len = 32k + ram_external_app_random_password (rwx) : org = 0xADC80000, len = 32k + ram_external_app_acars_rx (rwx) : org = 0xADC90000, len = 32k + ram_external_app_shoppingcart_lock (rwx) : org = 0xADCA0000, len = 32k + ram_external_app_cvs_spam (rwx) : org = 0xADCB0000, len = 32k + ram_external_app_ookbrute (rwx) : org = 0xADCC0000, len = 32k + ram_external_app_flippertx (rwx) : org = 0xADCD0000, len = 32k + ram_external_app_ook_editor (rwx) : org = 0xADCE0000, len = 32k + ram_external_app_remote (rwx) : org = 0xADCF0000, len = 32k + ram_external_app_mcu_temperature (rwx) : org = 0xADD00000, len = 32k + ram_external_app_fmradio (rwx) : org = 0xADD10000, len = 32k + ram_external_app_tuner (rwx) : org = 0xADD20000, len = 32k + ram_external_app_metronome (rwx) : org = 0xADD30000, len = 32k + ram_external_app_app_manager (rwx) : org = 0xADD40000, len = 32k + ram_external_app_hopper (rwx) : org = 0xADD50000, len = 32k + ram_external_app_antenna_length (rwx) : org = 0xADD60000, len = 32k + ram_external_app_view_wav (rwx) : org = 0xADD70000, len = 32k + ram_external_app_sd_wipe (rwx) : org = 0xADD80000, len = 32k + ram_external_app_playlist_editor (rwx) : org = 0xADD90000, len = 32k + ram_external_app_snake (rwx) : org = 0xADDA0000, len = 32k + ram_external_app_stopwatch (rwx) : org = 0xADDB0000, len = 32k + ram_external_app_wefax_rx (rwx) : org = 0xADDC0000, len = 32k + ram_external_app_breakout (rwx) : org = 0xADDD0000, len = 32k + ram_external_app_doom (rwx) : org = 0xADDE0000, len = 32k + ram_external_app_debug_pmem (rwx) : org = 0xADDF0000, len = 32k + ram_external_app_scanner (rwx) : org = 0xADE00000, len = 32k + ram_external_app_level (rwx) : org = 0xADE10000, len = 32k + ram_external_app_gfxeq (rwx) : org = 0xADE20000, len = 32k + ram_external_app_noaaapt_rx (rwx) : org = 0xADE30000, len = 32k + ram_external_app_detector_rx (rwx) : org = 0xADE40000, len = 32k + ram_external_app_dinogame (rwx) : org = 0xADE50000, len = 32k + ram_external_app_spaceinv (rwx) : org = 0xADE60000, len = 32k + ram_external_app_blackjack (rwx) : org = 0xADE70000, len = 32k + ram_external_app_battleship (rwx) : org = 0xADE80000, len = 32k + ram_external_app_ert (rwx) : org = 0xADE90000, len = 32k + ram_external_app_epirb_rx (rwx) : org = 0xADEA0000, len = 32k + ram_external_app_soundboard (rwx) : org = 0xADEB0000, len = 32k + ram_external_app_game2048 (rwx) : org = 0xADEC0000, len = 32k + ram_external_app_bht_tx (rwx) : org = 0xADED0000, len = 32k + ram_external_app_morse_practice (rwx) : org = 0xADEE0000, len = 32k + ram_external_app_adult_toys_controller (rwx) : org = 0xADEF0000, len = 32k + ram_external_app_flex_rx (rwx) : org = 0xADF00000, len = 32k + ram_external_app_sstvrx (rwx) : org = 0xADF10000, len = 32k + ram_external_app_subcarrx (rwx) : org = 0xADF20000, len = 32k + ram_external_app_siggen (rwx) : org = 0xADF30000, len = 32k + ram_external_app_sdusb (rwx) : org = 0xADF40000, 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_morseradiotx (rwx) : org = 0xADF70000, len = 32k + ram_external_app_keeloqtx (rwx) : org = 0xADF80000, len = 32k + ram_external_app_rtty_rx (rwx) : org = 0xADF90000, len = 32k + ram_external_app_rtty_tx (rwx) : org = 0xADFA0000, len = 32k + 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 + ram_external_app_kiss_tnc (rwx) : org = 0xADFF0000, len = 32k + ram_external_app_mdc_tx (rwx) : org = 0xAE000000, len = 32k + ram_external_app_epirb_tx (rwx) : org = 0xAE010000, len = 32k + ram_external_app_fpv_detect (rwx) : org = 0xAE020000, len = 32k } SECTIONS @@ -144,7 +145,7 @@ SECTIONS *(*ui*external_app*nrf_rx*); } > ram_external_app_nrf_rx - .external_app_coasterp : ALIGN(4) SUBALIGN(4) + .external_app_coasterp : ALIGN(4) SUBALIGN(4) { KEEP(*(.external_app.app_coasterp.application_information)); *(*ui*external_app*coasterp*); @@ -192,61 +193,61 @@ SECTIONS *(*ui*external_app*tetris*); } > ram_external_app_tetris - .external_app_extsensors : ALIGN(4) SUBALIGN(4) + .external_app_extsensors : ALIGN(4) SUBALIGN(4) { KEEP(*(.external_app.app_extsensors.application_information)); *(*ui*external_app*extsensors*); } > ram_external_app_extsensors - .external_app_foxhunt_rx : ALIGN(4) SUBALIGN(4) + .external_app_foxhunt_rx : ALIGN(4) SUBALIGN(4) { KEEP(*(.external_app.app_foxhunt_rx.application_information)); *(*ui*external_app*foxhunt_rx*); } > ram_external_app_foxhunt_rx - .external_app_audio_test : ALIGN(4) SUBALIGN(4) + .external_app_audio_test : ALIGN(4) SUBALIGN(4) { KEEP(*(.external_app.app_audio_test.application_information)); *(*ui*external_app*audio_test*); } > ram_external_app_audio_test - .external_app_wardrivemap : ALIGN(4) SUBALIGN(4) + .external_app_wardrivemap : ALIGN(4) SUBALIGN(4) { KEEP(*(.external_app.app_wardrivemap.application_information)); *(*ui*external_app*wardrivemap*); } > ram_external_app_wardrivemap - .external_app_tpmsrx : ALIGN(4) SUBALIGN(4) + .external_app_tpmsrx : ALIGN(4) SUBALIGN(4) { KEEP(*(.external_app.app_tpmsrx.application_information)); *(*ui*external_app*tpmsrx*); } > ram_external_app_tpmsrx - .external_app_tpmstx : ALIGN(4) SUBALIGN(4) + .external_app_tpmstx : ALIGN(4) SUBALIGN(4) { KEEP(*(.external_app.app_tpmstx.application_information)); *(*ui*external_app*tpmstx*); } > ram_external_app_tpmstx - .external_app_protoview : ALIGN(4) SUBALIGN(4) + .external_app_protoview : ALIGN(4) SUBALIGN(4) { KEEP(*(.external_app.app_protoview.application_information)); *(*ui*external_app*protoview*); } > ram_external_app_protoview - .external_app_adsbtx : ALIGN(4) SUBALIGN(4) + .external_app_adsbtx : ALIGN(4) SUBALIGN(4) { KEEP(*(.external_app.app_adsbtx.application_information)); *(*ui*external_app*adsbtx*); } > ram_external_app_adsbtx - .external_app_morse_tx : ALIGN(4) SUBALIGN(4) + .external_app_morse_tx : ALIGN(4) SUBALIGN(4) { KEEP(*(.external_app.app_morse_tx.application_information)); *(*ui*external_app*morse_tx*); } > ram_external_app_morse_tx - .external_app_sstvtx : ALIGN(4) SUBALIGN(4) + .external_app_sstvtx : ALIGN(4) SUBALIGN(4) { KEEP(*(.external_app.app_sstvtx.application_information)); *(*ui*external_app*sstvtx*); @@ -264,19 +265,19 @@ SECTIONS *(*ui*external_app*acars_rx*); } > ram_external_app_acars_rx - .external_app_shoppingcart_lock : ALIGN(4) SUBALIGN(4) + .external_app_shoppingcart_lock : ALIGN(4) SUBALIGN(4) { KEEP(*(.external_app.app_shoppingcart_lock.application_information)); *(*ui*external_app*shoppingcart_lock*); } > ram_external_app_shoppingcart_lock - .external_app_cvs_spam : ALIGN(4) SUBALIGN(4) + .external_app_cvs_spam : ALIGN(4) SUBALIGN(4) { KEEP(*(.external_app.app_cvs_spam.application_information)); *(*ui*external_app*cvs_spam*); } > ram_external_app_cvs_spam - .external_app_ookbrute : ALIGN(4) SUBALIGN(4) + .external_app_ookbrute : ALIGN(4) SUBALIGN(4) { KEEP(*(.external_app.app_ookbrute.application_information)); *(*ui*external_app*ookbrute*); @@ -288,7 +289,7 @@ SECTIONS *(*ui*external_app*ook_editor*); } > ram_external_app_ook_editor - .external_app_flippertx : ALIGN(4) SUBALIGN(4) + .external_app_flippertx : ALIGN(4) SUBALIGN(4) { KEEP(*(.external_app.app_flippertx.application_information)); *(*ui*external_app*flippertx*); @@ -372,13 +373,13 @@ SECTIONS *(*ui*external_app*stopwatch*); } > ram_external_app_stopwatch - .external_app_wefax_rx : ALIGN(4) SUBALIGN(4) + .external_app_wefax_rx : ALIGN(4) SUBALIGN(4) { KEEP(*(.external_app.app_wefax_rx.application_information)); *(*ui*external_app*wefax_rx*); } > ram_external_app_wefax_rx - .external_app_noaaapt_rx : ALIGN(4) SUBALIGN(4) + .external_app_noaaapt_rx : ALIGN(4) SUBALIGN(4) { KEEP(*(.external_app.app_noaaapt_rx.application_information)); *(*ui*external_app*noaaapt_rx*); @@ -480,14 +481,12 @@ SECTIONS *(*ui*external_app*game2048*); } > ram_external_app_game2048 - .external_app_bht_tx : ALIGN(4) SUBALIGN(4) { KEEP(*(.external_app.app_bht_tx.application_information)); *(*ui*external_app*bht_tx*); } > ram_external_app_bht_tx - .external_app_morse_practice : ALIGN(4) SUBALIGN(4) { KEEP(*(.external_app.app_morse_practice.application_information)); @@ -504,7 +503,7 @@ SECTIONS { KEEP(*(.external_app.app_flex_rx.application_information)); *(*ui*external_app*flex_rx*); - } > ram_external_app_flex_rx + } > ram_external_app_flex_rx .external_app_sstvrx : ALIGN(4) SUBALIGN(4) { @@ -524,12 +523,12 @@ SECTIONS *(*ui*external_app*siggen*); } > ram_external_app_siggen - .external_app_sdusb : ALIGN(4) SUBALIGN(4) + .external_app_sdusb : ALIGN(4) SUBALIGN(4) { KEEP(*(.external_app.app_sdusb.application_information)); *(*ui*external_app*sdusb*); } > ram_external_app_sdusb - + .external_app_morse_radio : ALIGN(4) SUBALIGN(4) { KEEP(*(.external_app.app_morse_radio.application_information)); @@ -547,6 +546,7 @@ SECTIONS KEEP(*(.external_app.app_keeloqtx.application_information)); *(*ui*external_app*keeloqtx*); } > ram_external_app_keeloqtx + .external_app_rtty_rx : ALIGN(4) SUBALIGN(4) { KEEP(*(.external_app.app_rtty_rx.application_information)); @@ -583,6 +583,12 @@ SECTIONS *(*ui*external_app*kiss_tnc*); } > ram_external_app_kiss_tnc + .external_app_mdc_tx : ALIGN(4) SUBALIGN(4) + { + KEEP(*(.external_app.app_mdc_tx.application_information)); + *(*ui*external_app*mdc_tx*); + } > ram_external_app_mdc_tx + .external_app_epirb_tx : ALIGN(4) SUBALIGN(4) { KEEP(*(.external_app.app_epirb_tx.application_information)); @@ -595,4 +601,3 @@ SECTIONS *(*ui*external_app*fpv_detect*); } > ram_external_app_fpv_detect } - diff --git a/firmware/application/external/mdc_tx/main.cpp b/firmware/application/external/mdc_tx/main.cpp new file mode 100644 index 000000000..93956a7f8 --- /dev/null +++ b/firmware/application/external/mdc_tx/main.cpp @@ -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(); +} +} // 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, +}; +} diff --git a/firmware/application/external/mdc_tx/ui_mdc_tx.cpp b/firmware/application/external/mdc_tx/ui_mdc_tx.cpp new file mode 100644 index 000000000..44693f3ce --- /dev/null +++ b/firmware/application/external/mdc_tx/ui_mdc_tx.cpp @@ -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((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((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(crc << 1); + if (c & static_cast(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(crc & 0xFF); + cw[5] = static_cast((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((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(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(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(raw_op); + uint8_t arg = static_cast(field_arg.value()); + uint8_t id_hi = static_cast(field_id_hi.value()); + uint8_t id_lo = static_cast(field_id_lo.value()); + uint8_t src_hi = static_cast(field_src_hi.value()); + uint8_t src_lo = static_cast(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(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((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 diff --git a/firmware/application/external/mdc_tx/ui_mdc_tx.hpp b/firmware/application/external/mdc_tx/ui_mdc_tx.hpp new file mode 100644 index 000000000..3fe80762e --- /dev/null +++ b/firmware/application/external/mdc_tx/ui_mdc_tx.hpp @@ -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(p); + this->on_tx_progress(msg.progress, msg.done); + }}; +}; + +} // namespace ui::external_app::mdc_tx + +#endif