Stable merge v2.2.0 (#2736)

* Update README.md

Actually valid complete ali h4m link

* Fake brightness reimplementation  (#2444)

* Fake brightness reimplementation
* indentation
* added call to the function which is caching the display settings values
* use cached values instead of pmem

* app manager (#2442)

* fix unset autostart
* clean up - add comments to prevent misleading
* move the app to external and with necessary changes
* replace autostart app

* Add + - buttons in Encoder dial settings (#2447)

* M10 additional parser (#2448)

* Remember previous capture settings (#2450)

* Renamed parameters in rx_capture.ini file (#2452)

* Rename settings in file to match screen
* Renamed variables for hopefully better clarity

* Navigation buttons (#2458)

* regenerate bitmap data
* pagination in submenu
* using little font so we are not eating menu buttons

* docker improvements (#2455)

* Update README.md

New metal case link

* The gerber files of the portapack H4 (#2463)

* Create README.txt

* Update README.txt

* Add files via upload

The gerber files of the portapack h4.

* delete

* Upload the gerber files for H4

* Update README.md (#2456)

Added a link to Lab401.com as a purchase option for EU customers.
(Lab401 was added as the EU exclusive distributor for the H4M - https://opensourcesdrlab.com/pages/distributors)

* Update README.md

* rename bitmaps into bmp

* disabling button on main menu, change labels and add 'icons' (#2466)

* disabling button on main menu, change labels and add 'icons'

* fix reverse order of buttons

---------

Co-authored-by: gullradriel <gullradriel@no-mail.com>

* fix for empty text prompt crash (#2468)

* added discord server information (#2471)

* Add the feature to decide rotate direction of encoder (#2472)

* Update README.md

Lab401 link with our redirection

* Fix encoder setting p.mem issue (#2475)

* Update README.md

Discord badge was broken, switching to shields.io

* Add fast flash script for sdcard switch hardware (#2480)

* fix cmake_minimum_required to 3.16 (#2499)

* fix fallthrough warning (#2497)

* Flipper tx: use file_path, example file (#2496)

* added subghz_dir
* use subghz_dir from file_path
* example file

* Externalize antenna calc and wav view (#2498)

* externalize antenna calc and wav view
* Added a tool to check if all the pictures in graphics are used in internal apps

* APRS: add frequency settings for Brazil (#2494)

* Add frequency settings for Brazil, named 'BR' that tunes to 145.570 MHz.
* Also added: Japan: 144.640 MHz (JAP), Thailand: 144.900 MHz (THA), Philippines: 144.740 MHz (PHI)
* Reordered list by increasing frequency
Co-authored-by: gullradriel <3157857+gullradriel@users.noreply.github.com>

* Added different modulations in signal generator (#2492)

* Added DSB, AM 100% mod index and AM 50% mod index. Changed UI.

* put back app in 'Utilities' (#2500)

Co-authored-by: gullradriel <gullradriel@no-mail.com>

* fix baseband (#2501)

* externalize wipe sdcard (#2502)

* Removing vim swap files (#2503)

* Removing vim swap files
* Added vim swap file to .gitignore

* Add modal to turn off screen when charging is detected (#2514)

* Moved country-specific FREQMAN files to the separate repository set up for it. (#2517)

* fix docker build warnings in dockerfile-nogit (#2518)

* Add new app "hopper" app. (#2482)

* make both jammer and hopper exist
* add example hopper payload
* example files
* swap scanner and recon app location

* Add widget preview tool (#2520)

* PoC

* opt

* opt

* Playlist editor (#2506)

* make both exist
* format
* fix focusing issue
* add example hopper payload
* fix compiler err
* clean up
* correct linker script addr
* lint
* PoC
* unknown: write_line issue
* clean up
* merge
* fix read line
* remove debug code
* fix english
* support new file
* support enter delay
* fix crash
* remove debug code
* some final tune

* Support Bug Key AKA Auto Key for OOK Editor app (#2523)

* _

* some final tune

* textual

* rename following gull's suggestion

* add cursor to font viewer app (#2528)

* Allow disable/enable waveform in Audio app to remove decoding problem on some frequencies

* Added different modulations in signal generator

* Added DSB, AM 100% mod index and AM 50% mod index. 
* Changed UI.
* Added pulsed CW

* Adding Wefax demodulation mode inside Audio App (#2539)

* Adding_new_WFAX_GUI_mode_Audio_App

* Wefax_APT_demodulation_structure

* Solving REC Apt signal.wav from WFAX

* clang format issues

* correcting comments

* Breakout - The Portapack remake game rises from the pirate's lair (#2541)

* Breakout - The Portapack remake game rises from the pirate's lair

* Fixes

* Added a signature

* Trivial textual change about missing SD content (#2542)

* _

* _

* waveform fix 3 and trivial change (#2540)

* Moved games to new game menu (#2544)

* Moved games to new game menu
* There's enough games to have a menu now and I plan to make more. Having them in "Utilities" made no sense.

* Wefax warning fix modulation fix (#2543)

* changed order of modulations, changed case to avoid capture
* added missing AMAudioFMApt mode to dump pmem
* reorder demod, adding missing ones, fix warnings
* removed uneeded 'previous_modulation', renamed WFAX to AMFM to match other places
* removing uneeded 'previous_modulation' uneeded check in change_modulation
* move capture at the end so AMAUdioFMApt is matching the other arrays for position 4
* added AMFM to Recon Level and Scanner

* clang + more details in some comments

---------

Co-authored-by: gullradriel <gullradriel@no-mail.com>

* Snake (#2549)

* Create the Shopping Cart Lock app

Will demonstrate tomorrow. Don't merge until I do 😁

* Fixes for HTotoo's comments 😎

* Improved audio the best I can.

If nobody has any ideas to further improve high frequencies of the audio, the hardware may not be capable. I still need to check with line-out to better speaker to make sure it's not just the speaker, but it shouldn't be.

* Compared against baseband_api.cpp - matched some things better but still playback seems to be missing higher fq sounds

* renamed wav files to a more specific / less generic name

* indentation + using variables instead of litteral names for wav files to use

* indentation

* Made a Snake game - enjoy

* Code formatting. I always forget.

* move to keep sort order

* Update external.ld

Sorry I should have also asked if there was any reason that address ranges 0xADDA0000--0xADDD0000 were skipped in external.ld.  I assumed there wasn't so I changed it to be consecutive using the same 0x10000 step as the other modules.  If there is any reason to skip them then we should add a comment to note it.  Of course these are all just temporary address values used for linking and get overwritten by a kludgy "search & replace" during the build process.

Resolves enhancement request #764

---------

Co-authored-by: gullradriel <gullradriel@no-mail.com>
Co-authored-by: Mark Thompson <129641948+NotherNgineer@users.noreply.github.com>

* Nested Debug menu into Utilities menu to tidy Home screen (#2551)

* rename rand pwd app (#2552)

* _

* remove wip code that forgot to add in old branch

* Moved speaker 1px to the right to match muted icon variant. (#2554)

* Stopwatch external app (#2553)

* updated bitmaps for speaker icon (#2555)

* updated bitmaps for speaker icon
* removed opera cake icon that was added by mistake
* adding missing Game menu source png
* cyan for Game menu
* regenerated icons

* Breakout icon change (#2556)

- Changed Breakout icon
- Changed Tetris icon color to green

* Snake icon change (#2557)

* Remove deprecated QR Code code (#2558)

* Setting for faster Button Repeat delays (#2559)

* Setting for faster Button Repeat delays

* Tweak fast delay times

* Tweak delay times

* Added description line and tweaked delay again

* OokBrute app opt (#2561)

* zooming_spectrum_AMFM_mode (#2565)

* Update README.md

Fixing opesourcesdrlab link

* Update README.md

Other link broken

* WeFax rx ext app (#2566)

* wf3

* Ookbrute (#2354)

* Revert "Ookbrute (#2354)"

This reverts commit abb8143eec.

* fix

* test edition

* re enable ble

* re enable ert

* steal amfm stuff

* something happens

* save bmp on start btn

* kinda works

* exit crash fixed

* redline, remove some hardcoded

* removed cpu killer red line, and some fixes

* simplify #1

* seems ok. time to improve

* added hidden freq offset to receiver model, so wefax can be set to the "correct" freq without users needs to substract 300 hz

* badly implemented sync detection, and disabled it.

* fix for fix

* fixes

* fix offset to real life off

* no line on freq enter

* fixes

* Doom - Mayhem Edition (#2570)

* Doom - Mini Mayhem version

* Update ui_doom.cpp

* Update ui_doom.cpp

* Update ui_doom.cpp

* Update ui_doom.cpp

* Update ui_doom.cpp

* Update ui_doom.cpp

* Update ui_doom.cpp

* Update ui_doom.cpp

* Doom - Mayhem Edition

Made the Doom - Mayhem Edition game. Some little bugs but good enough for nightly.

* Code formatting. Forgot like always.

* give more initial ammo until i put ammo around the maze to collect

* Update Doom main.cpp for better icon (#2575)

* fix external app address list (#2573)

* Add WEFAX freqman file (#2567)

* Add WEFAX freqman file
* fix bad escaped spaces and unicode characters

* Add icon for the doom game (#2574)

* Add icon for the doom game
* Add b/w .png to convert as doom icon

* Added wefax offset to audio app too. (#2572)

* added wefax offset to audio app too.

* moved from head to cpp

* Put ticker class and pp_colors in hpp file in namespace and remove helper files (#2577)

* stopwatch opt (#2578)

* stopwatch opt

* comments

* format

* fxi ms display when user tune display level

* issue template fine tune (#2579)

* Combined cpp files, stuffed helper files in hpp, updates start and game over screens (#2583)

* Combine cpp, move helpers to hpp (#2584)

* naming space (#2585)

* Tetris: Combined cpp files. Helper files into hpp. Dark mode. Encoder on. (#2587)

* Adding_Waterfall_ZOOM_x2_in_AM_modes_Audio_App (#2586)

* adding zoom_factor to app settings
* separated zoom_factor settings for AM and AMFM
* fix order so zoom factor is also applied correctly on modulation change
* fix zoom not applied when changing bandwidth
* temporary disable the Scanner so we are not breaking the nightly. Until we are choosing to finally remove it or find a better solution

---------

Co-authored-by: gullradriel <3157857+gullradriel@users.noreply.github.com>
Co-authored-by: gullradriel <gullradriel@no-mail.com>

* Fixed the I Tetromino rotation using SRS (Super Rotation System) (#2588)

* Externalize dump pmem (#2590)

* initial commit

* clang

* memory icon

* text output and exit button, FOCUS OVERRIDE TO AVOID COMPILATION ERROR

* modem and data_structure_version accessor

---------

Co-authored-by: gullradriel <gullradriel@no-mail.com>

* remove dead code (#2593)

* remove not yet enabled screening app (#2594)

* move default splash into sdcard (#2595)

* move bmps to sdcard
* remove unrelated files
* gitignore
* credit
* format

* Externalize scanner (#2589)

* externalize scanner
* NFM as main baseband as it's the biggest used one
* fix modulation bug introduced with AMFM

* Externalize level (#2596)

* removing ability to focus on RSSI bars and to launch level app, until a solution to launch external apps from internal ones is given
* externalize Level app

* Remove unneeded AMFM support in those apps (#2597)

* Create CODE_OF_CONDUCT.md

* Create pull_request_template.md

* Create SECURITY.md

* Create CONTRIBUTING.md (#2598)

* Force 433.92 and remove metadata check (#2599)

* Force 433.92 and remove metadata check: we already know the frequency for all files so don't need a million metadata files to match.
* Variable fixes and move to header

* Added more Wefax stations (#2600)

* Delete sdcard/FREQMAN/WEFAX.TXT

Replace with 2 files

* Add more WeFax frequencies

* Update comments

* Oops wrong folder

* Oops wrong folder

* Change comments

* Remove RF TX and use PATX baseband for audio --> speaker out only (#2601)

* Force 433.92 and remove metadata check

We already know the frequency for all files so don't need a million metadata files to match.

* Remove RF TX. Improve PATX baseband.

* code formatting of course

* Issue template again (#2602)

* test1

* test2

* add tap tempo to metronomic app (#2605)

* _

* format

* fix new tree in Arch

* solving_Audio_App_AM_GUI_Problem_issue_2604 (#2609)

* make the ptext_prompt func can define which keyboard to enter (#2608)

* _

* format

* use define

* prevent long life var for audio app - AM (#2610)

* static vars so no external linkage is possible

* persistent settings and no more global living variables

---------

Co-authored-by: gullradriel <gullradriel@no-mail.com>

* theme fix again (#2611)

* theme fix again

* _

* user can disable battery change hint (#2612)

* theme fix again

* _

* _

* GFX EQ App (#2607)

* Make the beginnings of rf3d
* Name change...
* Add mood button
* Remove forced amp settings and add persistent user settings
* Fix options bar layout and SettingsManager
* Make the background paint to black again after opening fq modal
* fix audio/mod/settings and cleaned unneeded parts
* Mapped bars to audio spectrum
* Improved frequency response... still needs work i think
* add on_freqchg to be able to answer to serial frequency change command
* Made calculations for 14 bars to fit screen and little adjustments
* Visual improvements
Co-authored-by: gullradriel

* Improved make_bitmap tool (#2615)

* Enhance Graphic Equalizer Visualization with Improved Frequency Bands and Response (#2614)

* Custom waterfall colors (#2617)

* Custom waterfall gradient
* Installing a custom waterfall gradient via fileman
* default file for user friendly swap
Co-authored-by: gullradriel <gullradriel@no-mail.com>

* Delete dead code in ble_rx_app.cpp (#2620)

Duplicate include on line 24 removed.

* Fix default waterfall file (#2621)

* correct default settings from file
* correct colors names

* fix comments (#2622)

* Touch on waterfall to set cursor pos (#2624)

* init

* fix typo that found by Copilot

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>

---------

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>

* Adding 8.33KHz spacing (#2628)

* Adding 8.33KHz spacing
* remove extern options_db_t freqman_steps which is now brought by ui_receiver.hpp
* use freqman db steps instead of static array
Co-authored-by: gullradriel <gullradriel@no-mail.com>

* Added menu group for transceivers (#2623)

* Added menu group for transceivers

* Reorder apps icons

* Support IPS screen & brightness set for IPS screen (#2629)

* _

* format

* format

* format

* Fix bug that created by PR "Added menu group for transceivers" (#2630)

* spectrum cursor opt again (#2634)

* spectrum cursor opt

* fmt

* remove blink

* remove End event

* cleanup

* Update README.md

Links were dead for opensourcesdrlab

* Fix for #2538 (#2635)

* Fix for #2538

Fix for #2538
Added on_bandwidth_changed Callback to ui_transmitter.hpp
Modified the field_bw.on_change lambda in the TransmitterView constructor to trigger the on_bandwidth_changed callback
Connected the Callback in ui_siggen.cpp

I am not a C++ programmer so this change was proposed by Gemini AI.

I have built and tested the App and it works as expected and I don't think the change will have any unexpected side effects.

* Fix clang issues

Fix clang issues

* Update ui_transmitter.cpp

typo

* Revised change

The proposed change mirrors the way a change to the frequency (on_edit_frequency) is triggered in ui_siggen by the tx_view.
The bw parameter is not passed because it is stored in _setting in the tx_view and will be read by update_config.
A change to the bw is not checked against auto_update to keep its behaviour consistent with a change to the gain, amplitude or frequency.

* Make changes to the channel_bandwidth dynamic whist playing

Behaviour of channel bandwidth is now consistent with frequency, amp and gain.

* comment edit

* revert hackrf submodule checkpoint to the repo

* comment

---------

Co-authored-by: zxkmm <zxkmm@hotmail.com>

* Adding_WFM_AM_mode_to_Audio_App (#2644)

* Adding_WFM_AM_mode_to_Audio_App
* more precise values for cos and sin theta, fix sen_theta to sin_theta
* fix sen_theta to sin_theta

* going back to WFM as main baseband in main.cpp as NFM is now making the apps crashing. Looks like last additions to WFM made it bigger. (#2646)

* remove dead code (#2647)

* Update ui_tetris.cpp (#2650)

Start "I" tetromino a bit higher and block rotate if it will cause out of bounds collision.

* Noaa apt decoder (#2648)

* Explicit_naming_wefax_NOAA_and_small_addition (#2651)

* slightly improved ads-b receiver module (#2649)

* slightly improved ads-b receiver module:
* fix Heading, Speed and Vrate decoders
* decode more ModeS messages
* log all ModeS messages (except DF11)
* fix formatting (clang-style); advice on data alignment taken into account
* ADS-B module: convert Indicated AirSpeed to True AirSpeed if altitute is known
* ADS-B rx module: replacing floating point with integer arithmetic

* adding 10Hz and 50Hz to freqman_steps (#2652)

* Improve_RF_sensitivity_NOAA_signal (#2654)

* Upload the PCB file of PortaPack H4 and update the schematic file (#2657)

* Create README.txt

* Update README.txt

* Add files via upload

The gerber files of the portapack h4.

* delete

* Upload the gerber files for H4

* Create README.txt

This is the V1.0 version PCB file of PortaPack H4.

* Upload the pcb file for H4

* Update README.txt

* Update LCD_TF_Schematic.pdf

* Jammer app add modes (#2659)

* Add new jammer modes

Overview

This PR enhances the PortaPack Jammer app by introducing eight new signal types, ported from my Flipper Zero RF Jammer app (https://github.com/RocketGod-git/flipper-zero-rf-jammer). These modes expand the app's capability to disrupt a wide range of RF communication protocols, from analog radios to modern digital systems. The implementation preserves the original app structure, resolves namespace conflicts, and ensures compatibility with the Mayhem firmware.

New Modes

The following modes have been added to the options_type in ui_jammer.hpp, with corresponding signal generation in proc_jammer.cpp:

Noise: Generates broadband white noise to interfere with analog and digital signals (e.g., Wi-Fi, Bluetooth, key fobs). Highly effective for overwhelming receivers across a frequency range.

Sine: Produces a continuous, unmodulated sine wave to jam narrowband receivers, ideal for analog FM/AM radios or telemetry systems.

Square: Emits a harmonic-rich square wave, disrupting digital protocols (e.g., OOK, ASK) and systems sensitive to sharp transitions, such as remote keyless entry.

Sawtooth (Experimental): Generates a sawtooth wave with a unique harmonic profile, useful for testing interference against PWM-based or niche analog systems.

Triangle (Experimental): Creates a triangle wave with minimal harmonics, suitable for exploratory jamming of narrowband systems or receiver linearity testing.

Chirp: Outputs a rapid frequency-sweeping chirp signal, effective against frequency-hopping and spread-spectrum systems (e.g., some Wi-Fi, Bluetooth, or military radios).

Gauss: Generates Gaussian noise to mimic natural interference, targeting digital systems like GPS or data links by degrading signal-to-noise ratios.

Brute (Experimental): Transmits a constant maximum-amplitude signal to saturate simple receiver front-ends, useful for brute-force jamming of basic analog devices.

* Add new jammer modes

Overview

This PR enhances the PortaPack Jammer app by introducing eight new signal types, ported from my Flipper Zero RF Jammer app (https://github.com/RocketGod-git/flipper-zero-rf-jammer). These modes expand the app's capability to disrupt a wide range of RF communication protocols, from analog radios to modern digital systems. The implementation preserves the original app structure, resolves namespace conflicts, and ensures compatibility with the Mayhem firmware.

New Modes

The following modes have been added to the options_type in ui_jammer.hpp, with corresponding signal generation in proc_jammer.cpp:

Noise: Generates broadband white noise to interfere with analog and digital signals (e.g., Wi-Fi, Bluetooth, key fobs). Highly effective for overwhelming receivers across a frequency range.

Sine: Produces a continuous, unmodulated sine wave to jam narrowband receivers, ideal for analog FM/AM radios or telemetry systems.

Square: Emits a harmonic-rich square wave, disrupting digital protocols (e.g., OOK, ASK) and systems sensitive to sharp transitions, such as remote keyless entry.

Sawtooth (Experimental): Generates a sawtooth wave with a unique harmonic profile, useful for testing interference against PWM-based or niche analog systems.

Triangle (Experimental): Creates a triangle wave with minimal harmonics, suitable for exploratory jamming of narrowband systems or receiver linearity testing.

Chirp: Outputs a rapid frequency-sweeping chirp signal, effective against frequency-hopping and spread-spectrum systems (e.g., some Wi-Fi, Bluetooth, or military radios).

Gauss: Generates Gaussian noise to mimic natural interference, targeting digital systems like GPS or data links by degrading signal-to-noise ratios.

Brute (Experimental): Transmits a constant maximum-amplitude signal to saturate simple receiver front-ends, useful for brute-force jamming of basic analog devices.

* refactor the serial log logic of BLE Rx (#2660)

* Prepare for display orientation part 1 (#2661)

* fix png part

* screen max width fixes (#2663)

* max width fixes

* format

* Audio to right (#2664)

* r.align

* Storing_selected_NOAA_filter_in_settings_file (#2665)

* Storing_selected_NOAA_filter_in_settings_file

* format_issues

* wfm_filters_GUI_name_std (#2668)

* getres cmd (#2671)

* ui new coord system examples and macros (#2672)

* Detector RX ext app (#2673)

* Jammer improvements (#2674)

* Add new jammer modes
Overview:
This PR enhances the PortaPack Jammer app by introducing eight new signal types, ported from my Flipper Zero RF Jammer app (https://github.com/RocketGod-git/flipper-zero-rf-jammer). These modes expand the app's capability to disrupt a wide range of RF communication protocols, from analog radios to modern digital systems. The implementation preserves the original app structure, resolves namespace conflicts, and ensures compatibility with the Mayhem firmware.

New Modes

The following modes have been added to the options_type in ui_jammer.hpp, with corresponding signal generation in proc_jammer.cpp:

Noise: Generates broadband white noise to interfere with analog and digital signals (e.g., Wi-Fi, Bluetooth, key fobs). Highly effective for overwhelming receivers across a frequency range.

Sine: Produces a continuous, unmodulated sine wave to jam narrowband receivers, ideal for analog FM/AM radios or telemetry systems.

Square: Emits a harmonic-rich square wave, disrupting digital protocols (e.g., OOK, ASK) and systems sensitive to sharp transitions, such as remote keyless entry.

Sawtooth (Experimental): Generates a sawtooth wave with a unique harmonic profile, useful for testing interference against PWM-based or niche analog systems.

Triangle (Experimental): Creates a triangle wave with minimal harmonics, suitable for exploratory jamming of narrowband systems or receiver linearity testing.

Chirp: Outputs a rapid frequency-sweeping chirp signal, effective against frequency-hopping and spread-spectrum systems (e.g., some Wi-Fi, Bluetooth, or military radios).

Gauss: Generates Gaussian noise to mimic natural interference, targeting digital systems like GPS or data links by degrading signal-to-noise ratios.

Brute (Experimental): Transmits a constant maximum-amplitude signal to saturate simple receiver front-ends, useful for brute-force jamming of basic analog devices.

* Fixed and made brutal.

This PR introduces user-focused improvements to the Jammer App in the HackRF PortaPack Mayhem Firmware, enhancing usability and flexibility. The changes address specific user requirements for a more intuitive default configuration, continuous waveform support, and dynamic setting adjustments during transmission.

* jammer fix (#2676)

* jammer fix

* Adding_BPF_selection_to_the_NOAA_APT_signal (#2675)

* Adding_BPF_selection_to_the_NOAA_APT_signal
* comments, spell mistake .

* trivial apps folder movement (#2677)

* Clean_LCD_beat_in_NOAA_Rx_App (#2678)

* Added ability to enter custom tone values in Morse app (#2679)

* Added ability to enter custom tone values in Morse app

Added the ability to type in a custom tone value in the morse TX app (issue#2582)

*Click on the tone field to open a keyboard for entering in a desired value between 100hz - 9999hz.

*Maintains original step value of 20 when scrolling the rotary wheel.

* Update ui_morse.cpp

Replaced std::to_string with to_string_dec_uint

* Moved tone_input_buffer init to in-class

* removed some std stuff only used here (#2681)

* 80mhz jammer range (#2682)

Looks great 😎🤘🚀

* Radio app improvements (#2680)

* Rename looking glass preset for clarity and consistency (#2686)

* Gfx widget and Radio (#2685)

* widgetize
* gfx and Radio improvement
* format + handle not wfm visual states
* wf or gf

* Externalize widget (#2688)

* Add all jammer modes in hopper app (#2691)

Added all modes that jammer app supports in hopper app.

* Super secret dont look (#2690)

* Add new jammer modes

Overview

This PR enhances the PortaPack Jammer app by introducing eight new signal types, ported from my Flipper Zero RF Jammer app (https://github.com/RocketGod-git/flipper-zero-rf-jammer). These modes expand the app's capability to disrupt a wide range of RF communication protocols, from analog radios to modern digital systems. The implementation preserves the original app structure, resolves namespace conflicts, and ensures compatibility with the Mayhem firmware.

New Modes

The following modes have been added to the options_type in ui_jammer.hpp, with corresponding signal generation in proc_jammer.cpp:

Noise: Generates broadband white noise to interfere with analog and digital signals (e.g., Wi-Fi, Bluetooth, key fobs). Highly effective for overwhelming receivers across a frequency range.

Sine: Produces a continuous, unmodulated sine wave to jam narrowband receivers, ideal for analog FM/AM radios or telemetry systems.

Square: Emits a harmonic-rich square wave, disrupting digital protocols (e.g., OOK, ASK) and systems sensitive to sharp transitions, such as remote keyless entry.

Sawtooth (Experimental): Generates a sawtooth wave with a unique harmonic profile, useful for testing interference against PWM-based or niche analog systems.

Triangle (Experimental): Creates a triangle wave with minimal harmonics, suitable for exploratory jamming of narrowband systems or receiver linearity testing.

Chirp: Outputs a rapid frequency-sweeping chirp signal, effective against frequency-hopping and spread-spectrum systems (e.g., some Wi-Fi, Bluetooth, or military radios).

Gauss: Generates Gaussian noise to mimic natural interference, targeting digital systems like GPS or data links by degrading signal-to-noise ratios.

Brute (Experimental): Transmits a constant maximum-amplitude signal to saturate simple receiver front-ends, useful for brute-force jamming of basic analog devices.

* Super secret

* You gotta get (Get) that (That) dirt off your shoulder

* Add 1ms hop option to hopper app + 0ms (freeze UI) (#2692)

* add dark theme (#2695)

* Made the Dino Game (#2697)

* Add vendor name in bluetooth rx app (#2696)

* add macaddress db, add vendor name in bluetooth rx app

* show "missing macaddress.db" instead of unknown if db not found

* bluetooth rx list with colors based on mac vendor

* bug fix

* Modified Text Editor to handle long presses. (#2698)

* Improved FPV_ANALOG.txt FREQMAN file (#2700)

* Improved FPV_ANALOG.txt FREQMAN file
Removed unused or super rare analog fpv bands: U, O, H, D
Added 1.2GHz -1.3GHz channels sometimes used for long range analog fpv


* Corrected and updated the labels to be more consistent.

Corrected the labels to be more consistent.

I also somehow messed up the correct channels because 1.3GHz FPV is not fully standardized, but these channels seem to be the most common.
It should be all correct now.

Example transmitters using those channels:
https://greenchip.com.ua/0-0-1615-2.html
https://flymod.net/en/item/walksnail_vtx_9ch
https://pl.aliexpress.com/item/1005006505365351.html

* Filemanager: go to parent directory keep track of the right selected … (#2702)

* Filemanager: go to parent directory keep track of the right selected item and page number
* review: avoid unnecessary copies in get_extension

* ADSB database update (tools, db) (#2701)

* enhance make_airlines_db tool
* enhance make_icao24_db tool
* update airlinescode (.txt, .db), aircraftdatabase/icao24 (.csv, .db)

* Made the Space Invaders game. Argh matey! (#2709)

* Made the Space Invaders game. Argh matey!
* Format code, sigh.

* Made the Blackjack game (#2712)

* Made the Blackjack game
* Format Blackjack main.cpp
* Changed spade to diamond for dark mode visibility
* Format code

* Update app icons for Space Invaders and Dino Game (#2713)

* BLE Rx Improvements (#2710)

* Work to allow for unique beacon parsing functions.
* Fix Copyright
* Update firmware/application/apps/ble_rx_app.cpp
* Update firmware/baseband/proc_btlerx.cpp
* PR suggestions.
* Fix String.
* Refactor

* Added 3d printed cases for the H4M (#2715)

* Battleship (#2720)

* Made the Battleship 2P 2PP game - FSK is wip
* Using POCSAG

* Adding simple FSK Rx Processor. Can be used with New Apps. (#2716)

* Work to allow for unique beacon parsing functions.

* Fixing pull.

* Changes.

* Formatting.

* Fix Copyright

* Update firmware/application/apps/ble_rx_app.cpp

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>

* Update firmware/baseband/proc_btlerx.cpp

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>

* PR suggestions.

* Fix String.

* FSK Rx Improvements. Works for my custom protocol.

* Fix buffer size.

* Refactor

* Formatting.

* Formatting.

* Fixing compiling, and BLE Rx UI/Performance.

* More improvements.

* Fixing stuck state.

* More stuck parsing fix.

* Combining PR changes.

* Improvements from previous PR.

* Fix dbM calculation relative to device RSSI.

* Formatting.

---------

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
Co-authored-by: TJ <tj.baginski@cognosos.com>

* Add blue bar to subghzd+weather (#2724)

* AIS map improv (#2725)

* AIS map improv

* format code mismatch with vc

* Add radio settings, new app icon, and other UI improvements (#2732)

* update submodule (#2734)

Co-authored-by: gullradriel <gullradriel@no-mail.com>

* update version (#2735)

Co-authored-by: gullradriel <gullradriel@no-mail.com>

---------

Co-authored-by: Erwin Ried <1091420+eried@users.noreply.github.com>
Co-authored-by: hackrfstuff <leszczyleszczy@icloud.com>
Co-authored-by: sommermorgentraum <24917424+zxkmm@users.noreply.github.com>
Co-authored-by: Totoo <ttotoo@gmail.com>
Co-authored-by: Mark Thompson <129641948+NotherNgineer@users.noreply.github.com>
Co-authored-by: E.T. <tamas@eisenberger.hu>
Co-authored-by: OpenSourceSDRLab <opensourcesdr@outlook.com>
Co-authored-by: quantum-x <simon.yorkston@gmail.com>
Co-authored-by: gullradriel <gullradriel@no-mail.com>
Co-authored-by: Lucas C. Villa Real <lucasvr@users.noreply.github.com>
Co-authored-by: Davide Rovelli <103165301+daviderud@users.noreply.github.com>
Co-authored-by: Gaurav Chaturvedi <oddtazz@users.noreply.github.com>
Co-authored-by: RocketGod <57732082+RocketGod-git@users.noreply.github.com>
Co-authored-by: Lerold <github@lerold.slmail.me>
Co-authored-by: Brumi-2021 <86470699+Brumi-2021@users.noreply.github.com>
Co-authored-by: dark-juju <2839275+dark-juju@users.noreply.github.com>
Co-authored-by: Benjamin Møller <37707273+LupusE@users.noreply.github.com>
Co-authored-by: Oleg Belousov <belousov.oleg@gmail.com>
Co-authored-by: haruk <104354987+exe-noisy@users.noreply.github.com>
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
Co-authored-by: Luca <61653175+iu2frl@users.noreply.github.com>
Co-authored-by: Richard <richard.toy@gmail.com>
Co-authored-by: zxkmm <zxkmm@hotmail.com>
Co-authored-by: horrordash <45861453+horrordash@users.noreply.github.com>
Co-authored-by: Alien <2142224+mythic-alien@users.noreply.github.com>
Co-authored-by: Petro Danylevskyi <petro@danylevskyi.com>
Co-authored-by: Tommaso Ventafridda <33782489+tomventa@users.noreply.github.com>
Co-authored-by: Netro <146584182+iNetro@users.noreply.github.com>
Co-authored-by: plomek <86431917+plomek@users.noreply.github.com>
Co-authored-by: TJ <tj.baginski@cognosos.com>
This commit is contained in:
gullradriel
2025-07-11 16:33:21 +02:00
committed by GitHub
parent 18e89d28a8
commit 1dbfc50dbe
396 changed files with 77723 additions and 9898 deletions

View File

@@ -397,13 +397,6 @@ set(MODE_CPPSRC
)
DeclareTargets(PSON sonde)
### FSK TX
set(MODE_CPPSRC
proc_fsk.cpp
)
DeclareTargets(PFSK fsktx)
### FSK RX
set(MODE_CPPSRC
@@ -411,6 +404,12 @@ set(MODE_CPPSRC
)
DeclareTargets(PFSR fskrx)
### FSK TX
set(MODE_CPPSRC
proc_fsk.cpp
)
DeclareTargets(PFSK fsktx)
### Microphone transmit
@@ -668,6 +667,22 @@ DeclareTargets(POSK ookstream)
### WeFax Rx
set(MODE_CPPSRC
proc_wefaxrx.cpp
)
DeclareTargets(PWFX wefaxrx)
### NoaaApt Rx
set(MODE_CPPSRC
proc_noaaapt_rx.cpp
)
DeclareTargets(PNOA noaaapt_rx)
### HackRF "factory" firmware
add_custom_command(

View File

@@ -52,6 +52,31 @@ void AudioOutput::write_unprocessed(const buffer_s16_t& audio) {
});
}
void AudioOutput::apt_write(const buffer_s16_t& audio) {
std::array<float, 32> audio_f;
for (size_t i = 0; i < audio.count; i++) {
cur = audio.p[i];
cur2 = cur * cur;
mag_am = sqrtf(prev2 + cur2 - (2 * prev * cur * cos_theta)) / sin_theta;
audio_f[i] = mag_am * ki; // normalize.
prev = cur;
prev2 = cur2;
}
write(buffer_f32_t{audio_f.data(), audio.count, audio.sampling_rate});
}
void AudioOutput::apt_write(const buffer_s16_t& audio, std::array<float, 32>& audio_f) {
for (size_t i = 0; i < audio.count; i++) {
cur = audio.p[i];
cur2 = cur * cur;
mag_am = sqrtf(prev2 + cur2 - (2 * prev * cur * cos_theta)) / sin_theta;
audio_f[i] = mag_am * ki; // normalize.
prev = cur;
prev2 = cur2;
}
write(buffer_f32_t{audio_f.data(), audio.count, audio.sampling_rate});
}
void AudioOutput::write(const buffer_s16_t& audio) {
std::array<float, 32> audio_f;
for (size_t i = 0; i < audio.count; i++) {
@@ -72,8 +97,8 @@ void AudioOutput::on_block(const buffer_f32_t& audio) {
if (do_processing) {
const auto audio_present_now = squelch.execute(audio);
hpf.execute_in_place(audio);
deemph.execute_in_place(audio);
hpf.execute_in_place(audio); // IIRBiquadFilter name is "hpf", but we will call with "hpf-coef" for all except AMFM (WFAX) with "lpf-coef" and notch for WFMAM (NOAA)
deemph.execute_in_place(audio); // IIRBiquadFilter name is "deemph", but we will call LPF de-emphasis or other LPF for WFAM (NOAA).
audio_present_history = (audio_present_history << 1) | (audio_present_now ? 1 : 0);
audio_present = (audio_present_history != 0);

View File

@@ -46,6 +46,8 @@ class AudioOutput {
const float squelch_threshold = 0.0f);
void write_unprocessed(const buffer_s16_t& audio);
void apt_write(const buffer_s16_t& audio);
void apt_write(const buffer_s16_t& audio, std::array<float, 32>& audio_f);
void write(const buffer_s16_t& audio);
void write(const buffer_f32_t& audio);
@@ -58,6 +60,10 @@ class AudioOutput {
private:
static constexpr float k = 32768.0f;
static constexpr float ki = 1.0f / k;
static constexpr float cos_theta = 0.30901699437494742410f;
static constexpr float sin_theta = 0.95105651629515357212f;
float cur = 0.0f, cur2 = 0.0f, prev = 0.0f, prev2 = 0.0f, mag_am = 0.0f;
BlockDecimator<int16_t, 32> block_buffer_s16{1};
BlockDecimator<float, 32> block_buffer{1};

View File

@@ -24,6 +24,8 @@
#include "complex.hpp"
#include "fxpt_atan2.hpp"
#include "utility_m4.hpp"
#include "dsp_hilbert.hpp"
#include "dsp_modulate.hpp"
#include <hal.h>
@@ -63,12 +65,7 @@ buffer_f32_t SSB::execute(
return {dst.p, src.count, src.sampling_rate};
}
/*
static inline float angle_approx_4deg0(const complex32_t t) {
const auto x = static_cast<float>(t.imag()) / static_cast<float>(t.real());
return 16384.0f * x;
}
*/
static inline float angle_approx_0deg27(const complex32_t t) {
if (t.real()) {
const auto x = static_cast<float>(t.imag()) / static_cast<float>(t.real());
@@ -82,6 +79,32 @@ static inline float angle_precise(const complex32_t t) {
return atan2f(t.imag(), t.real());
}
buffer_f32_t SSB_FM::execute( // Added to handle AMFM (WFAX, HF weather map )
const buffer_c16_t& src, // input arg , pointer Complex c16 i,q buffer.
const buffer_f32_t& dst) { // input arg , pointer f32 buffer audio demodulated
complex16_t* src_p = src.p; // removed const ; init src_p pointer with the mem address pointed by src.p.
const auto src_end = &src.p[src.count];
auto dst_p = dst.p;
float mag_sq_lpf_norm;
while (src_p < src_end) {
// FM APT audio tone demod: real part (USB-differentiator) and AM tone demodulation + lpf (to remove the subcarrier.)
real_to_complex.execute((src_p++)->real(), mag_sq_lpf_norm);
*(dst_p++) = mag_sq_lpf_norm; // already normalized/32.768f and clipped to +1.0f for the wav file.
real_to_complex.execute((src_p++)->real(), mag_sq_lpf_norm);
*(dst_p++) = mag_sq_lpf_norm;
real_to_complex.execute((src_p++)->real(), mag_sq_lpf_norm);
*(dst_p++) = mag_sq_lpf_norm;
real_to_complex.execute((src_p++)->real(), mag_sq_lpf_norm);
*(dst_p++) = mag_sq_lpf_norm;
}
return {dst.p, src.count, src.sampling_rate};
}
buffer_f32_t FM::execute(
const buffer_c16_t& src,
const buffer_f32_t& dst) {

View File

@@ -23,6 +23,7 @@
#define __DSP_DEMODULATE_H__
#include "dsp_types.hpp"
#include "dsp_hilbert.hpp"
namespace dsp {
namespace demodulate {
@@ -47,6 +48,17 @@ class SSB {
static constexpr float k = 1.0f / 32768.0f;
};
class SSB_FM { // Added to handle AMFM for WFAX
public:
buffer_f32_t execute(
const buffer_c16_t& src,
const buffer_f32_t& dst);
private:
static constexpr float k = 1.0f / 32768.0f;
dsp::Real_to_Complex real_to_complex{}; // It is a member variable of SSB_FM.
};
class FM {
public:
buffer_f32_t execute(

View File

@@ -21,6 +21,7 @@
#include "dsp_hilbert.hpp"
#include "dsp_sos_config.hpp"
#include "utility_m4.hpp"
namespace dsp {
@@ -83,4 +84,88 @@ void HilbertTransform::execute(float in, float& out_i, float& out_q) {
n = (n + 1) % 4;
}
Real_to_Complex::Real_to_Complex() {
// No need to call a separate configuration method like "Real_to_Complex()" externally before using the execute() method
// This is the constructor for the Real_to_Complex class.
// It initializes the member variables and calls the configure function for the sos_input, sos_i, and sos_q filters.
// to ensure the object is ready to use right after instantiation.
n = 0;
sos_input.configure(full_band_lpf_config);
sos_i.configure(full_band_lpf_config);
sos_q.configure(full_band_lpf_config);
sos_mag_sq.configure(quarter_band_lpf_config); // for APT LPF subcarrier filter. (1/4 Nyquist fs/2 = 1/4 * 12Khz/2 = 1.5khz)
}
void Real_to_Complex::execute(float in, float& out_mag_sq_lpf) {
// Full_band LPF means a LP filter with f_cut_off = fs/2; Full band = Full max band = 1/2 * fs_max = 1.0 x f_Nyquist = 1 * fs/2 = fs/2
float a = 0, b = 0;
float out_i = 0, out_q = 0, out_mag_sq = 0;
// int32_t packed;
float in_filtered = sos_input.execute(in) * 1.0f; // Anti-aliasing full band LPF, fc = fs/2= 6k, audio filter front-end.
switch (n) {
case 0:
a = in_filtered;
b = 0;
break;
case 1:
a = 0;
b = -in_filtered;
break;
case 2:
a = -in_filtered;
b = 0;
break;
case 3:
a = 0;
b = in_filtered;
break;
}
float i = sos_i.execute(a) * 1.0f; // better keep <1.0f to minimize recorded APT(t) black level artifacts.-
float q = sos_q.execute(b) * 1.0f;
switch (n) { // shifting down -fs4 (fs = 12khz , fs/4 = 3khz)
case 0:
out_i = i;
out_q = q;
break;
case 1:
out_i = -q;
out_q = i;
break;
case 2:
out_i = -i;
out_q = -q;
break;
case 3:
out_i = q;
out_q = -i;
break;
}
n = (n + 1) % 4;
/* res = __smuad(val1,val2); p1 = val1[15:0] × val2[15:0]
p2 = val1[31:16] × val2[31:16]
res[31:0] = p1 + p2
return res; */
// Not strict Magnitude complex calculation, it is a cross multiplication (lower 16 bit real x lower 16 imag) + 0 (higher 16 bits comp),
// but better visual results comparing real magnitude calculation, (better map diagonal lines reproduction, and less artifacts in APT signal(t)
out_mag_sq = __SMUAD(out_i, out_q); // "cross-magnitude" of the complex (out_i + j out_q)
out_mag_sq_lpf = sos_mag_sq.execute((out_mag_sq)) * 2.0f; // LPF quater band = 1.5khz APT signal
out_mag_sq_lpf /= 32768.0f; // normalize ;
// Compress clipping positive APT signal [-1.5 ..1.5] input , converted to [-1.0 ...1.0] with "S" compressor gain shape.
if (out_mag_sq_lpf > 1.0f) {
out_mag_sq_lpf = 1.0f; // clipped signal at +1.0f, APT signal is positive, no need to clip -1.0
} else {
out_mag_sq_lpf = out_mag_sq_lpf * (1.5f - ((out_mag_sq_lpf * out_mag_sq_lpf) / 2.0f));
}
}
} /* namespace dsp */

View File

@@ -39,6 +39,19 @@ class HilbertTransform {
SOSFilter<5> sos_q = {};
};
class Real_to_Complex {
public:
Real_to_Complex(); // Additional initialization
void execute(float in, float& out_mag_sq_lpf);
private:
uint8_t n = 0;
SOSFilter<5> sos_input = {};
SOSFilter<5> sos_i = {};
SOSFilter<5> sos_q = {};
SOSFilter<5> sos_mag_sq = {};
};
} /* namespace dsp */
#endif /*__DSP_HILBERT_H__*/

View File

@@ -73,13 +73,8 @@ void ADSBRXProcessor::execute(const buffer_c8_t& buffer) {
// Perform additional check on the first byte.
if (bit_count == 8) {
// Abandon all frames that aren't DF17 or DF18 extended squitters.
uint8_t df = (byte >> 3);
if (df != 17 && df != 18) {
decoding = false;
bit = (prev_mag > mag) ? 1 : 0;
frame.clear();
}
// try to receive all frames instead
msg_len = (byte & 0x80) ? 112 : 56; // determine message len by type
}
}
}

View File

@@ -40,7 +40,7 @@ class ADSBRXProcessor : public BasebandProcessor {
private:
static constexpr size_t baseband_fs = 2'000'000;
static constexpr size_t msg_len = 112;
size_t msg_len{112};
ADSBFrame frame{};
bool configured{false};

View File

@@ -27,6 +27,7 @@
#include "event_m4.hpp"
#include <array>
#include "dsp_hilbert.hpp"
void NarrowbandAMAudio::execute(const buffer_c8_t& buffer) {
if (!configured) {
@@ -44,16 +45,30 @@ void NarrowbandAMAudio::execute(const buffer_c8_t& buffer) {
// TODO: Feed channel_stats post-decimation data?
feed_channel_stats(channel_out);
auto audio = demodulate(channel_out);
auto audio = demodulate(channel_out); // now 3 AM demodulation types : demod_am, demod_ssb, demod_ssb_fm (for Wefax)
audio_compressor.execute_in_place(audio);
audio_output.write(audio);
}
buffer_f32_t NarrowbandAMAudio::demodulate(const buffer_c16_t& channel) {
if (modulation_ssb) {
return demod_ssb.execute(channel, audio_buffer);
} else {
return demod_am.execute(channel, audio_buffer);
switch (modulation_ssb) { // enum class Modulation : int32_t {DSB = 0, SSB = 1, SSB_FM = 2}
case (int)(AMConfigureMessage::Modulation::DSB):
return demod_am.execute(channel, audio_buffer);
break;
case (int)(AMConfigureMessage::Modulation::SSB):
return demod_ssb.execute(channel, audio_buffer);
break;
case (int)(AMConfigureMessage::Modulation::SSB_FM): // Added to handle Weather Fax mode.
// chDbgPanic("case SSB_FM demodulation"); // Debug.
return demod_ssb_fm.execute(channel, audio_buffer); // Calling a derivative of demod_ssb (USB) , but with different FIR taps + FM audio tones demod.
break;
// return demod am as a default
default:
return demod_am.execute(channel, audio_buffer);
break;
}
}
@@ -97,9 +112,10 @@ void NarrowbandAMAudio::configure(const AMConfigureMessage& message) {
channel_filter_low_f = message.channel_filter.low_frequency_normalized * channel_filter_input_fs;
channel_filter_high_f = message.channel_filter.high_frequency_normalized * channel_filter_input_fs;
channel_filter_transition = message.channel_filter.transition_normalized * channel_filter_input_fs;
channel_spectrum.set_decimation_factor(1.0f);
modulation_ssb = (message.modulation == AMConfigureMessage::Modulation::SSB);
audio_output.configure(message.audio_hpf_config);
modulation_ssb = (int)message.modulation; // now sending by message , 3 types of AM demod : enum class Modulation : int32_t {DSB = 0, SSB = 1, SSB_FM = 2}
channel_spectrum.set_decimation_factor(message.channel_spectrum_decimation_factor);
audio_output.configure(message.audio_hpf_lpf_config); // hpf in all AM demod modes (AM-6K/9K, USB/LSB,DSB), except Wefax (lpf there).
configured = true;
}

View File

@@ -63,9 +63,11 @@ class NarrowbandAMAudio : public BasebandProcessor {
int32_t channel_filter_transition = 0;
bool configured{false};
bool modulation_ssb = false;
// bool modulation_ssb = false; // Origianlly we only had 2 AM demod types {DSB = 0, SSB = 1} , and we could handle it with bool var , 1 bit.
int8_t modulation_ssb = 0; // Now we have 3 AM demod types we will send now index integer {DSB = 0, SSB = 1, SSB_FM = 2}
dsp::demodulate::AM demod_am{};
dsp::demodulate::SSB demod_ssb{};
dsp::demodulate::SSB_FM demod_ssb_fm{}; // added for Wfax mode.
FeedForwardCompressor audio_compressor{};
AudioOutput audio_output{};

View File

@@ -27,7 +27,16 @@
#include "event_m4.hpp"
uint32_t BTLERxProcessor::crc_init_reorder(uint32_t crc_init) {
inline float BTLERxProcessor::get_phase_diff(const complex16_t& sample0, const complex16_t& sample1) {
// Calculate the phase difference between two samples.
float dI = sample1.real() * sample0.real() + sample1.imag() * sample0.imag();
float dQ = sample1.imag() * sample0.real() - sample1.real() * sample0.imag();
float phase_diff = atan2f(dQ, dI);
return phase_diff;
}
inline uint32_t BTLERxProcessor::crc_init_reorder(uint32_t crc_init) {
int i;
uint32_t crc_init_tmp, crc_init_input, crc_init_input_tmp;
@@ -53,7 +62,7 @@ uint32_t BTLERxProcessor::crc_init_reorder(uint32_t crc_init) {
return (crc_init_tmp);
}
uint_fast32_t BTLERxProcessor::crc_update(uint_fast32_t crc, const void* data, size_t data_len) {
inline uint_fast32_t BTLERxProcessor::crc_update(uint_fast32_t crc, const void* data, size_t data_len) {
const unsigned char* d = (const unsigned char*)data;
unsigned int tbl_idx;
@@ -67,7 +76,7 @@ uint_fast32_t BTLERxProcessor::crc_update(uint_fast32_t crc, const void* data, s
return crc & 0xffffff;
}
uint_fast32_t BTLERxProcessor::crc24_byte(uint8_t* byte_in, int num_byte, uint32_t init_hex) {
inline uint_fast32_t BTLERxProcessor::crc24_byte(uint8_t* byte_in, int num_byte, uint32_t init_hex) {
uint_fast32_t crc = init_hex;
crc = crc_update(crc, byte_in, num_byte);
@@ -75,7 +84,7 @@ uint_fast32_t BTLERxProcessor::crc24_byte(uint8_t* byte_in, int num_byte, uint32
return (crc);
}
bool BTLERxProcessor::crc_check(uint8_t* tmp_byte, int body_len, uint32_t crc_init) {
inline bool BTLERxProcessor::crc_check(uint8_t* tmp_byte, int body_len, uint32_t crc_init) {
int crc24_checksum;
crc24_checksum = crc24_byte(tmp_byte, body_len, crc_init); // 0x555555 --> 0xaaaaaa. maybe because byte order
@@ -87,7 +96,7 @@ bool BTLERxProcessor::crc_check(uint8_t* tmp_byte, int body_len, uint32_t crc_in
return (crc24_checksum != checksumReceived);
}
void BTLERxProcessor::scramble_byte(uint8_t* byte_in, int num_byte, const uint8_t* scramble_table_byte, uint8_t* byte_out) {
inline void BTLERxProcessor::scramble_byte(uint8_t* byte_in, int num_byte, const uint8_t* scramble_table_byte, uint8_t* byte_out) {
int i;
for (i = 0; i < num_byte; i++) {
@@ -95,7 +104,7 @@ void BTLERxProcessor::scramble_byte(uint8_t* byte_in, int num_byte, const uint8_
}
}
int BTLERxProcessor::verify_payload_byte(int num_payload_byte, ADV_PDU_TYPE pdu_type) {
inline int BTLERxProcessor::verify_payload_byte(int num_payload_byte, ADV_PDU_TYPE pdu_type) {
// Should at least have 6 bytes for the MAC Address.
// Also ensuring that there is at least 1 byte of data.
if (num_payload_byte <= 6) {
@@ -122,120 +131,125 @@ int BTLERxProcessor::verify_payload_byte(int num_payload_byte, ADV_PDU_TYPE pdu_
return 0;
}
void BTLERxProcessor::handleBeginState() {
int num_symbol_left = dst_buffer.count / SAMPLE_PER_SYMBOL; // One buffer sample consist of I and Q.
inline void BTLERxProcessor::resetOffsetTracking() {
frequency_offset = 0.0f;
frequency_offset_estimate = 0.0f;
phase_buffer_index = 0;
memset(phase_buffer, 0, sizeof(phase_buffer));
}
static uint8_t demod_buf_access[SAMPLE_PER_SYMBOL][LEN_DEMOD_BUF_ACCESS];
inline void BTLERxProcessor::resetBitPacketIndex() {
memset(rb_buf, 0, sizeof(rb_buf));
packet_index = 0;
bit_index = 0;
}
uint32_t uint32_tmp = DEFAULT_ACCESS_ADDR;
uint8_t accessAddrBits[LEN_DEMOD_BUF_ACCESS];
inline void BTLERxProcessor::resetToDefaultState() {
parseState = Parse_State_Begin;
resetOffsetTracking();
resetBitPacketIndex();
crc_init_internal = crc_init_reorder(crc_initalVale);
}
uint32_t accesssAddress = 0;
inline void BTLERxProcessor::demodulateFSKBits(int num_demod_byte) {
for (; packet_index < num_demod_byte; packet_index++) {
for (; bit_index < 8; bit_index++) {
if (samples_eaten >= (int)dst_buffer.count) {
return;
}
// Filling up addressBits with the access address we are looking to find.
for (int i = 0; i < 32; i++) {
accessAddrBits[i] = 0x01 & uint32_tmp;
uint32_tmp = (uint32_tmp >> 1);
float phaseSum = 0.0f;
for (int k = 0; k < SAMPLE_PER_SYMBOL; ++k) {
float phase = get_phase_diff(
dst_buffer.p[samples_eaten + k],
dst_buffer.p[samples_eaten + k + 1]);
phaseSum += phase;
}
// phaseSum /= (SAMPLE_PER_SYMBOL);
// phaseSum -= frequency_offset;
/*
alternate method. faster, but less precise. with this, you need to check against this: if (samples_eaten >= (int)dst_buffer.count + SAMPLE_PER_SYMBOL) (not so good...)
int I0 = dst_buffer.p[samples_eaten].real();
int Q0 = dst_buffer.p[samples_eaten].imag();
int I1 = dst_buffer.p[samples_eaten + 1 * SAMPLE_PER_SYMBOL].real();
int Q1 = dst_buffer.p[samples_eaten + 1 * SAMPLE_PER_SYMBOL].imag();
bool bitDecision = (I0 * Q1 - I1 * Q0) > 0 ? 1 : 0;
*/
bool bitDecision = (phaseSum > 0.0f);
rb_buf[packet_index] = rb_buf[packet_index] | (bitDecision << bit_index);
samples_eaten += SAMPLE_PER_SYMBOL;
}
bit_index = 0;
}
}
inline void BTLERxProcessor::handleBeginState() {
uint32_t validAccessAddress = DEFAULT_ACCESS_ADDR;
static uint32_t accesssAddress = 0;
const int demod_buf_len = LEN_DEMOD_BUF_ACCESS; // For AA
int demod_buf_offset = 0;
int hit_idx = (-1);
bool unequal_flag = false;
memset(demod_buf_access, 0, SAMPLE_PER_SYMBOL * demod_buf_len);
for (int i = 0; i < num_symbol_left * SAMPLE_PER_SYMBOL; i += SAMPLE_PER_SYMBOL) {
int sp = ((demod_buf_offset - demod_buf_len + 1) & (demod_buf_len - 1));
for (int i = samples_eaten; i < (int)dst_buffer.count; i += SAMPLE_PER_SYMBOL) {
float phaseDiff = 0;
for (int j = 0; j < SAMPLE_PER_SYMBOL; j++) {
// Sample and compare with the adjacent next sample.
int I0 = dst_buffer.p[i + j].real();
int Q0 = dst_buffer.p[i + j].imag();
int I1 = dst_buffer.p[i + j + 1].real();
int Q1 = dst_buffer.p[i + j + 1].imag();
int phase_idx = j;
demod_buf_access[phase_idx][demod_buf_offset] = (I0 * Q1 - I1 * Q0) > 0 ? 1 : 0;
int k = sp;
unequal_flag = false;
accesssAddress = 0;
for (int p = 0; p < demod_buf_len; p++) {
if (demod_buf_access[phase_idx][k] != accessAddrBits[p]) {
unequal_flag = true;
hit_idx = (-1);
break;
}
accesssAddress = (accesssAddress & (~(1 << p))) | (demod_buf_access[phase_idx][k] << p);
k = ((k + 1) & (demod_buf_len - 1));
}
if (unequal_flag == false) {
hit_idx = (i + j - (demod_buf_len - 1) * SAMPLE_PER_SYMBOL);
break;
}
phaseDiff += get_phase_diff(dst_buffer.p[i + j], dst_buffer.p[i + j + 1]);
}
if (unequal_flag == false) {
// disabled, due to not used anywhere
/* phase_buffer[phase_buffer_index] = phaseDiff / (SAMPLE_PER_SYMBOL);
phase_buffer_index = (phase_buffer_index + 1) % ROLLING_WINDOW;
*/
bool bitDecision = (phaseDiff > 0);
accesssAddress = (accesssAddress >> 1 | (bitDecision << 31));
int errors = __builtin_popcount(accesssAddress ^ validAccessAddress) & 0xFFFFFFFF;
if (!errors) {
hit_idx = i + SAMPLE_PER_SYMBOL;
// disabled, due to not used anywhere
/* for (int k = 0; k < ROLLING_WINDOW; k++) {
frequency_offset_estimate += phase_buffer[k];
}
frequency_offset = frequency_offset_estimate / ROLLING_WINDOW;
*/
break;
}
demod_buf_offset = ((demod_buf_offset + 1) & (demod_buf_len - 1));
}
if (hit_idx == -1) {
// Process more samples.
samples_eaten = (int)dst_buffer.count + 1;
return;
}
symbols_eaten += hit_idx;
symbols_eaten += (8 * NUM_ACCESS_ADDR_BYTE * SAMPLE_PER_SYMBOL); // move to the beginning of PDU header
num_symbol_left = num_symbol_left - symbols_eaten;
samples_eaten += hit_idx;
parseState = Parse_State_PDU_Header;
}
void BTLERxProcessor::handlePDUHeaderState() {
int num_demod_byte = 2; // PDU header has 2 octets
symbols_eaten += 8 * num_demod_byte * SAMPLE_PER_SYMBOL;
if (symbols_eaten > (int)dst_buffer.count) {
inline void BTLERxProcessor::handlePDUHeaderState() {
if (samples_eaten > (int)dst_buffer.count) {
return;
}
// Jump back down to the beginning of PDU header.
sample_idx = symbols_eaten - (8 * num_demod_byte * SAMPLE_PER_SYMBOL);
demodulateFSKBits(NUM_PDU_HEADER_BYTE);
packet_index = 0;
for (int i = 0; i < num_demod_byte; i++) {
rb_buf[packet_index] = 0;
for (int j = 0; j < 8; j++) {
int I0 = dst_buffer.p[sample_idx].real();
int Q0 = dst_buffer.p[sample_idx].imag();
int I1 = dst_buffer.p[sample_idx + 1].real();
int Q1 = dst_buffer.p[sample_idx + 1].imag();
bit_decision = (I0 * Q1 - I1 * Q0) > 0 ? 1 : 0;
rb_buf[packet_index] = rb_buf[packet_index] | (bit_decision << j);
sample_idx += SAMPLE_PER_SYMBOL;
}
packet_index++;
if (packet_index < NUM_PDU_HEADER_BYTE || bit_index != 0) {
resetToDefaultState();
return;
}
scramble_byte(rb_buf, num_demod_byte, scramble_table[channel_number], rb_buf);
scramble_byte(rb_buf, 2, scramble_table[channel_number], rb_buf);
pdu_type = (ADV_PDU_TYPE)(rb_buf[0] & 0x0F);
// uint8_t tx_add = ((rb_buf[0] & 0x40) != 0);
@@ -244,38 +258,25 @@ void BTLERxProcessor::handlePDUHeaderState() {
// Not a valid Advertise Payload.
if ((payload_len < 6) || (payload_len > 37)) {
parseState = Parse_State_Begin;
resetToDefaultState();
return;
} else {
parseState = Parse_State_PDU_Payload;
}
}
void BTLERxProcessor::handlePDUPayloadState() {
int i;
int num_demod_byte = (payload_len + 3);
symbols_eaten += 8 * num_demod_byte * SAMPLE_PER_SYMBOL;
inline void BTLERxProcessor::handlePDUPayloadState() {
const int num_demod_byte = (payload_len + 3);
if (symbols_eaten > (int)dst_buffer.count) {
if (samples_eaten > (int)dst_buffer.count) {
return;
}
for (i = 0; i < num_demod_byte; i++) {
rb_buf[packet_index] = 0;
demodulateFSKBits(num_demod_byte + NUM_PDU_HEADER_BYTE);
for (int j = 0; j < 8; j++) {
int I0 = dst_buffer.p[sample_idx].real();
int Q0 = dst_buffer.p[sample_idx].imag();
int I1 = dst_buffer.p[sample_idx + 1].real();
int Q1 = dst_buffer.p[sample_idx + 1].imag();
bit_decision = (I0 * Q1 - I1 * Q0) > 0 ? 1 : 0;
rb_buf[packet_index] = rb_buf[packet_index] | (bit_decision << j);
sample_idx += SAMPLE_PER_SYMBOL;
}
packet_index++;
if (packet_index < (num_demod_byte + NUM_PDU_HEADER_BYTE) || bit_index != 0) {
resetToDefaultState();
return;
}
scramble_byte(rb_buf + 2, num_demod_byte, scramble_table[channel_number] + 2, rb_buf + 2);
@@ -310,6 +311,8 @@ void BTLERxProcessor::handlePDUPayloadState() {
// Skip Header Byte and MAC Address
uint8_t startIndex = 8;
int i;
for (i = 0; i < payload_len - 6; i++) {
blePacketData.data[i] = rb_buf[startIndex++];
}
@@ -322,46 +325,50 @@ void BTLERxProcessor::handlePDUPayloadState() {
}
}
parseState = Parse_State_Begin;
resetToDefaultState();
}
void BTLERxProcessor::execute(const buffer_c8_t& buffer) {
if (!configured) return;
// Pulled this implementation from channel_stats_collector.c to time slice a specific packet's dB.
// a less computationally expensive method
max_dB = -128;
uint32_t max_squared = 0;
int8_t imag = 0;
int8_t real = 0;
void* src_p = buffer.p;
while (src_p < &buffer.p[buffer.count]) {
const uint32_t sample = *__SIMD32(src_p)++;
const uint32_t mag_sq = __SMUAD(sample, sample);
if (mag_sq > max_squared) {
max_squared = mag_sq;
imag = ((complex8_t*)src_p)->imag();
real = ((complex8_t*)src_p)->real();
}
}
const float max_squared_f = max_squared;
max_dB = mag2_to_dbv_norm(max_squared_f * (1.0f / (32768.0f * 32768.0f)));
max_dB = mag2_to_dbm_8bit_normalized(real, imag, 1.0f, 50.0f);
// 4Mhz 2048 samples
// Decimated by 4 to achieve 2048/4 = 512 samples at 1 sample per symbol.
decim_0.execute(buffer, dst_buffer);
feed_channel_stats(dst_buffer);
symbols_eaten = 0;
samples_eaten = 0;
// Handle parsing based on parseState
if (parseState == Parse_State_Begin) {
handleBeginState();
}
while (samples_eaten < (int)dst_buffer.count) {
// Handle parsing based on parseState
if (parseState == Parse_State_Begin) {
handleBeginState();
}
if (parseState == Parse_State_PDU_Header) {
handlePDUHeaderState();
}
if (parseState == Parse_State_PDU_Header) {
handlePDUHeaderState();
}
if (parseState == Parse_State_PDU_Payload) {
handlePDUPayloadState();
if (parseState == Parse_State_PDU_Payload) {
handlePDUPayloadState();
}
}
}
@@ -372,11 +379,9 @@ void BTLERxProcessor::on_message(const Message* const message) {
void BTLERxProcessor::configure(const BTLERxConfigureMessage& message) {
channel_number = message.channel_number;
decim_0.configure(taps_BTLE_1M_PHY_decim_0.taps);
decim_0.configure(taps_BTLE_Dual_PHY.taps);
configured = true;
crc_init_internal = crc_init_reorder(crc_initalVale);
}
int main() {

View File

@@ -47,6 +47,8 @@ class BTLERxProcessor : public BasebandProcessor {
static constexpr int LEN_DEMOD_BUF_ACCESS{32};
static constexpr uint32_t DEFAULT_ACCESS_ADDR{0x8E89BED6};
static constexpr int NUM_ACCESS_ADDR_BYTE{4};
static constexpr int NUM_PDU_HEADER_BYTE{2};
static constexpr int ROLLING_WINDOW{32};
enum Parse_State {
Parse_State_Begin = 0,
@@ -81,8 +83,8 @@ class BTLERxProcessor : public BasebandProcessor {
};
static constexpr size_t baseband_fs = 4000000;
static constexpr size_t audio_fs = baseband_fs / 8 / 8 / 2;
float get_phase_diff(const complex16_t& sample0, const complex16_t& sample1);
uint_fast32_t crc_update(uint_fast32_t crc, const void* data, size_t data_len);
uint_fast32_t crc24_byte(uint8_t* byte_in, int num_byte, uint32_t init_hex);
bool crc_check(uint8_t* tmp_byte, int body_len, uint32_t crc_init);
@@ -95,6 +97,12 @@ class BTLERxProcessor : public BasebandProcessor {
// void demod_byte(int num_byte, uint8_t *out_byte);
int verify_payload_byte(int num_payload_byte, ADV_PDU_TYPE pdu_type);
void resetOffsetTracking();
void resetBitPacketIndex();
void resetToDefaultState();
void demodulateFSKBits(int num_demod_byte);
void handleBeginState();
void handlePDUHeaderState();
void handlePDUPayloadState();
@@ -120,13 +128,18 @@ class BTLERxProcessor : public BasebandProcessor {
BlePacketData blePacketData{};
Parse_State parseState{Parse_State_Begin};
uint16_t packet_index{0};
int sample_idx{0};
int symbols_eaten{0};
int samples_eaten{0};
uint8_t bit_decision{0};
uint8_t payload_len{0};
uint8_t pdu_type{0};
int32_t max_dB{0};
uint16_t packet_index{0};
uint8_t bit_index{0};
float frequency_offset_estimate{0.0f};
float frequency_offset{0.0f};
float phase_buffer[ROLLING_WINDOW] = {0.0f};
int phase_buffer_index = 0;
/* NB: Threads should be the last members in the class definition. */
BasebandThread baseband_thread{baseband_fs, this, baseband::Direction::Receive};

View File

@@ -24,6 +24,7 @@
*/
#include "proc_fsk_rx.hpp"
#include "dsp_decimate.hpp"
#include "event_m4.hpp"
@@ -32,135 +33,253 @@
#include <cstdint>
#include <cstddef>
using namespace std;
using namespace dsp::decimate;
#ifndef M_PI
#define M_PI 3.14159265358979323846
#endif
namespace {
/* Count of bits that differ between the two values. */
uint8_t diff_bit_count(uint32_t left, uint32_t right) {
uint32_t diff = left ^ right;
uint8_t count = 0;
for (size_t i = 0; i < sizeof(diff) * 8; ++i) {
if (((diff >> i) & 0x1) == 1)
++count;
float FSKRxProcessor::detect_peak_power(const buffer_c8_t& buffer, int N) {
int32_t power = 0;
// Initial window power
for (int i = 0; i < N; i++) {
int16_t i_sample = buffer.p[i].real();
int16_t q_sample = buffer.p[i].imag();
power += i_sample * i_sample + q_sample * q_sample;
}
return count;
}
} // namespace
power = power / N;
/* AudioNormalizer ***************************************/
// Convert to dB over noise floor
float power_db = 10.0f * log10f((float)power / noise_floor);
void AudioNormalizer::execute_in_place(const buffer_f32_t& audio) {
// Decay min/max every second (@24kHz).
if (counter_ >= 24'000) {
// 90% decay factor seems to work well.
// This keeps large transients from wrecking the filter.
max_ *= 0.9f;
min_ *= 0.9f;
counter_ = 0;
calculate_thresholds();
}
// If too weak, treat as no signal
if (power_db <= 0.0f) return 0;
counter_ += audio.count;
for (size_t i = 0; i < audio.count; ++i) {
auto& val = audio.p[i];
if (val > max_) {
max_ = val;
calculate_thresholds();
}
if (val < min_) {
min_ = val;
calculate_thresholds();
}
if (val >= t_hi_)
val = 1.0f;
else if (val <= t_lo_)
val = -1.0f;
else
val = 0.0;
}
return power_db;
}
void AudioNormalizer::calculate_thresholds() {
auto center = (max_ + min_) / 2.0f;
auto range = (max_ - min_) / 2.0f;
void FSKRxProcessor::agc_correct_iq(const buffer_c8_t& buffer, int N, float measured_power) {
float power_db = 10.0f * log10f(measured_power / noise_floor);
float error_db = target_power_db - power_db;
// 10% off center force either +/-1.0f.
// Higher == larger dead zone.
// Lower == more false positives.
auto threshold = range * 0.1;
t_hi_ = center + threshold;
t_lo_ = center - threshold;
}
/* FSKRxProcessor ******************************************/
void FSKRxProcessor::clear_data_bits() {
data = 0;
bit_count = 0;
}
void FSKRxProcessor::handle_sync(bool inverted) {
clear_data_bits();
has_sync_ = true;
inverted = inverted;
word_count = 0;
}
void FSKRxProcessor::process_bits(const buffer_c8_t& buffer) {
// Process all of the bits in the bits queue.
while (buffer.count > 0) {
// Wait until data_ is full.
if (bit_count < data_bit_count)
continue;
// Wait for the sync frame.
if (!has_sync_) {
if (diff_bit_count(data, sync_codeword) <= 2)
handle_sync(/*inverted=*/false);
else if (diff_bit_count(data, ~sync_codeword) <= 2)
handle_sync(/*inverted=*/true);
continue;
}
}
}
/* FSKRxProcessor ***************************************/
FSKRxProcessor::FSKRxProcessor() {
}
void FSKRxProcessor::execute(const buffer_c8_t& buffer) {
if (!configured) {
if (error_db <= 0) {
return;
}
// Decimate by current decim 0 and decim 1.
float gain_scalar = powf(10.0f, error_db / 20.0f);
for (int i = 0; i < N; i++) {
buffer.p[i] = {(int8_t)(buffer.p[i].real() * gain_scalar), (int8_t)(buffer.p[i].imag() * gain_scalar)};
}
}
float FSKRxProcessor::get_phase_diff(const complex16_t& sample0, const complex16_t& sample1) {
// Calculate the phase difference between two samples.
float dI = sample1.real() * sample0.real() + sample1.imag() * sample0.imag();
float dQ = sample1.imag() * sample0.real() - sample1.real() * sample0.imag();
float phase_diff = atan2f(dQ, dI);
return phase_diff;
}
void FSKRxProcessor::demodulateFSKBits(const buffer_c16_t& decimator_out, int num_demod_byte) {
for (; packet_index < num_demod_byte; packet_index++) {
for (; bit_index < 8; bit_index++) {
if (samples_eaten >= (int)decimator_out.count) {
return;
}
float phaseSum = 0.0f;
for (int k = 0; k < SAMPLE_PER_SYMBOL - 1; ++k) {
float phase = get_phase_diff(
decimator_out.p[samples_eaten + k],
decimator_out.p[samples_eaten + k + 1]);
phaseSum += phase;
}
phaseSum /= (SAMPLE_PER_SYMBOL - 1);
phaseSum -= frequency_offset;
bool bitDecision = (phaseSum > 0.0f);
rb_buf[packet_index] |= (bitDecision << (7 - bit_index));
samples_eaten += SAMPLE_PER_SYMBOL;
}
bit_index = 0;
}
}
void FSKRxProcessor::resetPreambleTracking() {
frequency_offset = 0.0f;
frequency_offset_estimate = 0.0f;
phase_buffer_index = 0;
memset(phase_buffer, 0, sizeof(phase_buffer));
}
void FSKRxProcessor::resetBitPacketIndex() {
packet_index = 0;
bit_index = 0;
}
void FSKRxProcessor::resetToDefaultState() {
parseState = Parse_State_Wait_For_Peak;
peak_timeout = 0;
fskPacketData.power = 0.0f;
resetPreambleTracking();
resetBitPacketIndex();
}
void FSKRxProcessor::handlePreambleState(const buffer_c16_t& decimator_out) {
const uint32_t validPreamble = DEFAULT_PREAMBLE;
static uint32_t preambleValue = 0;
int hit_idx = -1;
for (; samples_eaten < (int)decimator_out.count; samples_eaten += SAMPLE_PER_SYMBOL) {
float phaseSum = 0.0f;
for (int j = 0; j < SAMPLE_PER_SYMBOL - 1; j++) {
phaseSum += get_phase_diff(decimator_out.p[samples_eaten + j], decimator_out.p[samples_eaten + j + 1]);
}
phase_buffer[phase_buffer_index] = phaseSum / (SAMPLE_PER_SYMBOL - 1);
phase_buffer_index = (phase_buffer_index + 1) % ROLLING_WINDOW;
bool bitDecision = (phaseSum > 0.0f);
preambleValue = (preambleValue << 1) | bitDecision;
int errors = __builtin_popcountl(preambleValue ^ validPreamble) & 0xFFFFFFFF;
if (errors == 0) {
hit_idx = samples_eaten + SAMPLE_PER_SYMBOL;
fskPacketData.syncWord = preambleValue;
fskPacketData.max_dB = max_dB;
for (int k = 0; k < ROLLING_WINDOW; k++) {
frequency_offset_estimate += phase_buffer[k];
}
frequency_offset = frequency_offset_estimate / ROLLING_WINDOW;
fskPacketData.frequency_offset_hz = (frequency_offset * demod_input_fs) / (2.0f * M_PI);
preambleValue = 0;
break;
}
}
if (hit_idx == -1) {
samples_eaten = samples_eaten;
return;
}
samples_eaten = hit_idx;
parseState = Parse_State_Sync;
}
void FSKRxProcessor::handleSyncWordState(const buffer_c16_t& decimator_out) {
const int syncword_bytes = 4;
const uint32_t validSyncWord = DEFAULT_SYNC_WORD;
if ((int)decimator_out.count - samples_eaten <= 0) {
return;
}
demodulateFSKBits(decimator_out, syncword_bytes);
if (packet_index < syncword_bytes || bit_index != 0) {
return;
}
uint32_t receivedSyncWord = (rb_buf[0] << 24) | (rb_buf[1] << 16) | (rb_buf[2] << 8) | rb_buf[3];
int errors = __builtin_popcountl(receivedSyncWord ^ validSyncWord) & 0xFFFFFFFF;
if (errors <= 3) {
fskPacketData.syncWord = receivedSyncWord;
parseState = Parse_State_PDU_Payload;
memset(fskPacketData.data, 0, sizeof(fskPacketData.data));
} else {
resetToDefaultState();
}
memset(rb_buf, 0, sizeof(rb_buf));
resetBitPacketIndex();
}
void FSKRxProcessor::handlePDUPayloadState(const buffer_c16_t& decimator_out) {
if ((int)decimator_out.count - samples_eaten <= 0) {
return;
}
demodulateFSKBits(decimator_out, NUM_DATA_BYTE);
if (packet_index < NUM_DATA_BYTE || bit_index != 0) {
return;
}
fskPacketData.dataLen = NUM_DATA_BYTE;
// Copy the decoded bits to the packet data
for (int i = 0; i < NUM_DATA_BYTE; i++) {
fskPacketData.data[i] |= rb_buf[i];
}
FSKRxPacketMessage data_message{&fskPacketData};
shared_memory.application_queue.push(data_message);
memset(rb_buf, 0, sizeof(rb_buf));
resetToDefaultState();
}
void FSKRxProcessor::execute(const buffer_c8_t& buffer) {
if (!configured || parseState == Parse_State_Parsing_Data) return;
const auto decim_0_out = decim_0.execute(buffer, dst_buffer);
const auto decim_1_out = decim_1.execute(decim_0_out, dst_buffer);
feed_channel_stats(decim_1_out);
spectrum_samples += decim_1_out.count;
samples_eaten = 0;
if (spectrum_samples >= spectrum_interval_samples) {
spectrum_samples -= spectrum_interval_samples;
channel_spectrum.feed(decim_1_out, channel_filter_low_f,
channel_filter_high_f, channel_filter_transition);
}
while ((int)decim_1_out.count - samples_eaten > 0) {
if ((parseState == Parse_State_Wait_For_Peak) || (parseState == Parse_State_Preamble)) {
float power = detect_peak_power(buffer, buffer.count);
// process_bits();
if (power) {
parseState = Parse_State_Preamble;
agc_power = power;
fskPacketData.power = power;
} else {
break;
}
}
// Update the status.
samples_processed += buffer.count;
if (agc_power) {
agc_correct_iq(buffer, buffer.count, agc_power);
}
if (samples_processed >= stat_update_threshold) {
// send_packet(data);
samples_processed -= stat_update_threshold;
if (parseState == Parse_State_Preamble) {
peak_timeout++;
// 960,000 fs / 2048 samples = 468.75 Hz, so 55 calls is about 0.053 seconds before timeout.
if (peak_timeout == 4) {
resetToDefaultState();
} else {
handlePreambleState(decim_1_out);
}
}
if (parseState == Parse_State_Sync) {
handleSyncWordState(decim_1_out);
}
if (parseState == Parse_State_PDU_Payload) {
handlePDUPayloadState(decim_1_out);
}
}
}
@@ -171,7 +290,7 @@ void FSKRxProcessor::on_message(const Message* const message) {
break;
case Message::ID::UpdateSpectrum:
case Message::ID::SpectrumStreamingConfig:
channel_spectrum.on_message(message);
// channel_spectrum.on_message(message);
break;
case Message::ID::SampleRateConfig:
@@ -179,7 +298,7 @@ void FSKRxProcessor::on_message(const Message* const message) {
break;
case Message::ID::CaptureConfig:
capture_config(*reinterpret_cast<const CaptureConfigMessage*>(message));
// capture_config(*reinterpret_cast<const CaptureConfigMessage*>(message));
break;
default:
@@ -188,83 +307,65 @@ void FSKRxProcessor::on_message(const Message* const message) {
}
void FSKRxProcessor::configure(const FSKRxConfigureMessage& message) {
// Extract message variables.
deviation = message.deviation;
channel_decimation = message.channel_decimation;
// channel_filter_taps = message.channel_filter;
channel_spectrum.set_decimation_factor(1);
}
void FSKRxProcessor::capture_config(const CaptureConfigMessage& message) {
if (message.config) {
audio_output.set_stream(std::make_unique<StreamInput>(message.config));
} else {
audio_output.set_stream(nullptr);
}
SAMPLE_PER_SYMBOL = message.samplesPerSymbol;
DEFAULT_SYNC_WORD = message.syncWord;
NUM_SYNC_WORD_BYTE = message.syncWordLength;
DEFAULT_PREAMBLE = message.preamble;
NUM_PREAMBLE_BYTE = message.preambleLength;
NUM_DATA_BYTE = message.numDataBytes;
}
void FSKRxProcessor::sample_rate_config(const SampleRateConfigMessage& message) {
const auto sample_rate = message.sample_rate;
// The actual sample rate is the requested rate * the oversample rate.
// See oversample.hpp for more details on oversampling.
baseband_fs = sample_rate * toUType(message.oversample_rate);
baseband_thread.set_sampling_rate(baseband_fs);
// TODO: Do we need to use the taps that the decimators get configured with?
channel_filter_low_f = taps_200k_decim_1.low_frequency_normalized * sample_rate;
channel_filter_high_f = taps_200k_decim_1.high_frequency_normalized * sample_rate;
channel_filter_transition = taps_200k_decim_1.transition_normalized * sample_rate;
// Compute the scalar that corrects the oversample_rate to be x8 when computing
// the spectrum update interval. The original implementation only supported x8.
// TODO: Why is this needed here but not in proc_replay? There must be some other
// assumption about x8 oversampling in some component that makes this necessary.
const auto oversample_correction = toUType(message.oversample_rate) / 8.0;
// The spectrum update interval controls how often the waterfall is fed new samples.
spectrum_interval_samples = sample_rate / (spectrum_rate_hz * oversample_correction);
spectrum_samples = 0;
// For high sample rates, the M4 is busy collecting samples so the
// waterfall runs slower. Reduce the update interval so it runs faster.
// NB: Trade off: looks nicer, but more frequent updates == more CPU.
if (sample_rate >= 1'500'000)
spectrum_interval_samples /= (sample_rate / 750'000);
switch (message.oversample_rate) {
case OversampleRate::x4:
// M4 can't handle 2 decimation passes for sample rates needing x4.
decim_0.set<FIRC8xR16x24FS4Decim4>().configure(taps_200k_decim_0.taps);
decim_0.set<dsp::decimate::FIRC8xR16x24FS4Decim4>().configure(taps_200k_decim_0.taps);
decim_1.set<NoopDecim>();
break;
case OversampleRate::x8:
// M4 can't handle 2 decimation passes for sample rates <= 600k.
if (message.sample_rate < 600'000) {
decim_0.set<FIRC8xR16x24FS4Decim4>().configure(taps_200k_decim_0.taps);
decim_1.set<FIRC16xR16x16Decim2>().configure(taps_200k_decim_1.taps);
decim_0.set<dsp::decimate::FIRC8xR16x24FS4Decim4>().configure(taps_200k_decim_0.taps);
decim_1.set<dsp::decimate::FIRC16xR16x16Decim2>().configure(taps_200k_decim_1.taps);
} else {
// Using 180k taps to provide better filtering with a single pass.
decim_0.set<FIRC8xR16x24FS4Decim8>().configure(taps_180k_wfm_decim_0.taps);
decim_0.set<dsp::decimate::FIRC8xR16x24FS4Decim8>().configure(taps_180k_wfm_decim_0.taps);
decim_1.set<NoopDecim>();
}
break;
case OversampleRate::x16:
decim_0.set<FIRC8xR16x24FS4Decim8>().configure(taps_200k_decim_0.taps);
decim_1.set<FIRC16xR16x16Decim2>().configure(taps_200k_decim_1.taps);
decim_0.set<dsp::decimate::FIRC8xR16x24FS4Decim8>().configure(taps_200k_decim_0.taps);
decim_1.set<dsp::decimate::FIRC16xR16x16Decim2>().configure(taps_200k_decim_1.taps);
break;
case OversampleRate::x32:
decim_0.set<FIRC8xR16x24FS4Decim4>().configure(taps_200k_decim_0.taps);
decim_1.set<FIRC16xR16x32Decim8>().configure(taps_16k0_decim_1.taps);
decim_0.set<dsp::decimate::FIRC8xR16x24FS4Decim4>().configure(taps_200k_decim_0.taps);
decim_1.set<dsp::decimate::FIRC16xR16x32Decim8>().configure(taps_16k0_decim_1.taps);
break;
case OversampleRate::x64:
decim_0.set<FIRC8xR16x24FS4Decim8>().configure(taps_200k_decim_0.taps);
decim_1.set<FIRC16xR16x32Decim8>().configure(taps_16k0_decim_1.taps);
decim_0.set<dsp::decimate::FIRC8xR16x24FS4Decim8>().configure(taps_200k_decim_0.taps);
decim_1.set<dsp::decimate::FIRC16xR16x32Decim8>().configure(taps_16k0_decim_1.taps);
break;
default:
@@ -282,33 +383,12 @@ void FSKRxProcessor::sample_rate_config(const SampleRateConfigMessage& message)
// size_t channel_filter_input_fs = decim_1_output_fs;
// size_t channel_filter_output_fs = channel_filter_input_fs / channel_decimation;
size_t demod_input_fs = decim_1_output_fs;
send_packet((uint32_t)demod_input_fs);
demod_input_fs = decim_1_output_fs;
// Set ready to process data.
configured = true;
}
void FSKRxProcessor::flush() {
// word_extractor.flush();
}
void FSKRxProcessor::reset() {
clear_data_bits();
has_sync_ = false;
inverted = false;
word_count = 0;
samples_processed = 0;
}
void FSKRxProcessor::send_packet(uint32_t data) {
data_message.is_data = true;
data_message.value = data;
shared_memory.application_queue.push(data_message);
}
/* main **************************************************/
int main() {

View File

@@ -30,7 +30,6 @@
#include "dsp_decimate.hpp"
#include "dsp_demodulate.hpp"
#include "dsp_iir_config.hpp"
#include "dsp_fir_taps.hpp"
#include "spectrum_collector.hpp"
@@ -46,21 +45,6 @@
#include <cstdint>
#include <functional>
/* Normalizes audio stream to +/-1.0f */
class AudioNormalizer {
public:
void execute_in_place(const buffer_f32_t& audio);
private:
void calculate_thresholds();
uint32_t counter_ = 0;
float min_ = 99.0f;
float max_ = -99.0f;
float t_hi_ = 1.0;
float t_lo_ = 1.0;
};
/* A decimator that just returns the source buffer. */
class NoopDecim {
public:
@@ -111,44 +95,92 @@ class MultiDecimator {
class FSKRxProcessor : public BasebandProcessor {
public:
FSKRxProcessor();
void execute(const buffer_c8_t& buffer) override;
void on_message(const Message* const message) override;
private:
size_t baseband_fs = 1024000; // aka: sample_rate
static constexpr int ROLLING_WINDOW{32};
static constexpr uint16_t MAX_BUFFER_SIZE{512};
enum Parse_State {
Parse_State_Wait_For_Peak = 0,
Parse_State_Preamble,
Parse_State_Sync,
Parse_State_PDU_Payload,
Parse_State_Parsing_Data
};
size_t baseband_fs = 960000;
uint8_t stat_update_interval = 10;
uint32_t stat_update_threshold = baseband_fs / stat_update_interval;
static constexpr auto spectrum_rate_hz = 50.0f;
float detect_peak_power(const buffer_c8_t& buffer, int N);
void agc_correct_iq(const buffer_c8_t& buffer, int N, float measured_power);
float get_phase_diff(const complex16_t& sample0, const complex16_t& sample1);
void demodulateFSKBits(const buffer_c16_t& decimator_out, int num_demod_byte);
void resetPreambleTracking();
void resetBitPacketIndex();
void resetToDefaultState();
void handlePreambleState(const buffer_c16_t& decimator_out);
void handleSyncWordState(const buffer_c16_t& decimator_out);
void handlePDUPayloadState(const buffer_c16_t& decimator_out);
void configure(const FSKRxConfigureMessage& message);
void capture_config(const CaptureConfigMessage& message);
void sample_rate_config(const SampleRateConfigMessage& message);
void flush();
void reset();
void send_packet(uint32_t data);
void process_bits(const buffer_c8_t& buffer);
void clear_data_bits();
void handle_sync(bool inverted);
/* Returns true if the batch has as sync frame. */
bool has_sync() const { return has_sync_; }
/* Set once app is ready to receive messages. */
bool configured = false;
/* Buffer for decimated IQ data. */
std::array<complex16_t, 512> dst{};
std::array<complex16_t, MAX_BUFFER_SIZE> dst{};
const buffer_c16_t dst_buffer{
dst.data(),
dst.size()};
/* Buffer for demodulated audio. */
std::array<float, 16> audio{};
const buffer_f32_t audio_buffer{audio.data(), audio.size()};
uint8_t rb_buf[MAX_BUFFER_SIZE];
dsp::demodulate::FM demod{};
int rb_head{-1};
int32_t g_threshold{0};
uint8_t channel_number{0};
uint16_t process = 0;
bool configured{false};
FskPacketData fskPacketData{};
Parse_State parseState{Parse_State_Wait_For_Peak};
int sample_idx{0};
int samples_eaten{0};
int32_t max_dB{0};
int8_t real{0};
int8_t imag{0};
uint16_t peak_timeout{0};
float noise_floor{12.0}; // Using LNA 40 and VGA 20. 10.0 was 40/0 ratio.
float target_power_db{5.0};
float agc_power{0.0f};
float frequency_offset_estimate{0.0f};
float frequency_offset{0.0f};
float phase_buffer[ROLLING_WINDOW] = {0.0f};
int phase_buffer_index = 0;
uint16_t packet_index{0};
uint8_t bit_index{0};
size_t demod_input_fs{0};
uint8_t SAMPLE_PER_SYMBOL{1};
uint32_t DEFAULT_PREAMBLE{0xAAAAAAAA};
uint32_t DEFAULT_SYNC_WORD{0xFFFFFFFF};
uint8_t NUM_SYNC_WORD_BYTE{4};
uint8_t NUM_PREAMBLE_BYTE{4};
uint16_t NUM_DATA_BYTE = MAX_BUFFER_SIZE - NUM_SYNC_WORD_BYTE - NUM_PREAMBLE_BYTE;
SpectrumCollector channel_spectrum{};
size_t spectrum_interval_samples = 0;
size_t spectrum_samples = 0;
static constexpr auto spectrum_rate_hz = 50.0f;
/* The actual type will be configured depending on the sample rate. */
MultiDecimator<
dsp::decimate::FIRC8xR16x24FS4Decim4,
dsp::decimate::FIRC8xR16x24FS4Decim8>
@@ -159,9 +191,7 @@ class FSKRxProcessor : public BasebandProcessor {
NoopDecim>
decim_1{};
/* Filter to 24kHz and demodulate. */
dsp::decimate::FIRAndDecimateComplex channel_filter{};
size_t deviation = 3750;
// fir_taps_real<32> channel_filter_taps = 0;
size_t channel_decimation = 2;
int32_t channel_filter_low_f = 0;
@@ -172,51 +202,6 @@ class FSKRxProcessor : public BasebandProcessor {
FMSquelch squelch{};
uint64_t squelch_history = 0;
// /* LPF to reduce noise. POCSAG supports 2400 baud, but that falls
// * nicely into the transition band of this 1800Hz filter.
// * scipy.signal.butter(2, 1800, "lowpass", fs=24000, analog=False) */
// IIRBiquadFilter lpf{{{0.04125354f, 0.082507070f, 0.04125354f},
// {1.00000000f, -1.34896775f, 0.51398189f}}};
/* Attempts to de-noise and normalize signal. */
AudioNormalizer normalizer{};
/* Handles writing audio stream to hardware. */
AudioOutput audio_output{};
/* Holds the data sent to the app. */
AFSKDataMessage data_message{false, 0};
/* Used to keep track of how many samples were processed
* between status update messages. */
uint32_t samples_processed = 0;
/* Number of bits in 'data_' member. */
static constexpr uint8_t data_bit_count = sizeof(uint32_t) * 8;
/* Sync frame codeword. */
static constexpr uint32_t sync_codeword = 0x12345678;
/* When true, sync frame has been received. */
bool has_sync_ = false;
/* When true, bit vales are flipped in the codewords. */
bool inverted = false;
uint32_t data = 0;
uint8_t bit_count = 0;
uint8_t word_count = 0;
/* LPF to reduce noise. POCSAG supports 2400 baud, but that falls
* nicely into the transition band of this 1800Hz filter.
* scipy.signal.butter(2, 1800, "lowpass", fs=24000, analog=False) */
IIRBiquadFilter lpf{{{0.04125354f, 0.082507070f, 0.04125354f},
{1.00000000f, -1.34896775f, 0.51398189f}}};
SpectrumCollector channel_spectrum{};
size_t spectrum_interval_samples = 0;
size_t spectrum_samples = 0;
/* NB: Threads should be the last members in the class definition. */
BasebandThread baseband_thread{baseband_fs, this, baseband::Direction::Receive};
RSSIThread rssi_thread{};

View File

@@ -1,6 +1,7 @@
/*
* Copyright (C) 2014 Jared Boone, ShareBrained Technology, Inc.
* Copyright (C) 2016 Furrtek
* Copyright (C) 2025 RocketGod - Added modes from my Flipper Zero RF Jammer App - https://betaskynet.com
*
* This file is part of PortaPack.
*
@@ -26,22 +27,26 @@
#include "event_m4.hpp"
#include <cstdint>
#include <random>
#include <cmath>
#ifndef M_PI
#define M_PI 3.14159265358979323846f
#endif
void JammerProcessor::execute(const buffer_c8_t& buffer) {
if (!configured) return;
for (size_t i = 0; i < buffer.count; i++) {
if (!jammer_duration) {
// Find next enabled range
do {
current_range++;
if (current_range == JAMMER_MAX_CH) current_range = 0;
} while (!jammer_channels[current_range].enabled);
jammer_duration = jammer_channels[current_range].duration;
jammer_bw = jammer_channels[current_range].width / 2; // TODO: Exact value
jammer_bw = jammer_channels[current_range].width / 2;
// Ask for retune
message.freq = jammer_channels[current_range].center;
message.range = current_range;
shared_memory.application_queue.push(message);
@@ -49,26 +54,51 @@ void JammerProcessor::execute(const buffer_c8_t& buffer) {
jammer_duration--;
}
// Phase noise
if (!period_counter) {
period_counter = noise_period;
if (noise_type == JammerType::TYPE_FSK) {
if (noise_type == jammer::JammerType::TYPE_FSK) {
sample = (sample + lfsr) >> 1;
} else if (noise_type == JammerType::TYPE_TONE) {
tone_delta = 150000 + (lfsr >> 9); // Approx 100Hz to 6kHz
} else if (noise_type == JammerType::TYPE_SWEEP) {
sample++; // This is like saw wave FM
} else if (noise_type == jammer::JammerType::TYPE_TONE) {
tone_delta = 150000 + (lfsr >> 9);
} else if (noise_type == jammer::JammerType::TYPE_SWEEP) {
sample++;
} else if (noise_type == jammer::JammerType::TYPE_RANDOM) {
sample = lfsr & 0xFF;
} else if (noise_type == jammer::JammerType::TYPE_SINE) {
wave_phase += 0x01000000;
sample = sine_table_i8[(wave_phase >> 24) & 0xFF];
} else if (noise_type == jammer::JammerType::TYPE_SQUARE) {
wave_index = (wave_index + 1) % 2;
sample = wave_index ? 127 : -128;
} else if (noise_type == jammer::JammerType::TYPE_SAWTOOTH) {
wave_index = (wave_index + 1) % 256;
sample = (wave_index * 127) / 255 - 128;
} else if (noise_type == jammer::JammerType::TYPE_TRIANGLE) {
wave_index = (wave_index + 1) % 256;
sample = (wave_index < 128 ? wave_index : (255 - wave_index)) * 127 / 127 - 128;
} else if (noise_type == jammer::JammerType::TYPE_CHIRP) {
chirp_freq += 0.01f;
if (chirp_freq > 1.0f) chirp_freq = 0.0f;
wave_phase += static_cast<uint32_t>(0x01000000 * (1.0f + chirp_freq));
sample = sine_table_i8[(wave_phase >> 24) & 0xFF];
} else if (noise_type == jammer::JammerType::TYPE_GAUSSIAN) {
float u1 = static_cast<float>(lfsr & 0xFFFF) / 0x10000;
float u2 = static_cast<float>((lfsr >> 16) & 0xFFFF) / 0x10000;
float gaussian = std::sqrt(-2.0f * std::log(u1)) * std::cos(2 * M_PI * u2);
sample = static_cast<int8_t>(gaussian * 32);
} else if (noise_type == jammer::JammerType::TYPE_BRUTEFORCE) {
sample = 127;
}
feedback = ((lfsr >> 31) ^ (lfsr >> 29) ^ (lfsr >> 15) ^ (lfsr >> 11)) & 1;
lfsr = (lfsr << 1) | feedback;
if (!lfsr) lfsr = 0x1337; // Shouldn't do this :(
if (!lfsr) lfsr = 0x1337;
} else {
period_counter--;
}
if (noise_type == JammerType::TYPE_TONE) {
if (noise_type == jammer::JammerType::TYPE_TONE) {
aphase += tone_delta;
sample = sine_table_i8[(aphase & 0xFF000000) >> 24];
}
@@ -78,12 +108,12 @@ void JammerProcessor::execute(const buffer_c8_t& buffer) {
phase += delta;
sphase = phase + (64 << 24);
re = (sine_table_i8[(sphase & 0xFF000000) >> 24]);
im = (sine_table_i8[(phase & 0xFF000000) >> 24]);
re = sine_table_i8[(sphase & 0xFF000000) >> 24];
im = sine_table_i8[(phase & 0xFF000000) >> 24];
buffer.p[i] = {re, im};
}
};
}
void JammerProcessor::on_message(const Message* const msg) {
if (msg->id == Message::ID::JammerConfigure) {
@@ -93,12 +123,18 @@ void JammerProcessor::on_message(const Message* const msg) {
jammer_channels = (JammerChannel*)shared_memory.bb_data.data;
noise_type = message.type;
noise_period = 3072000 / message.speed;
if (noise_type == JammerType::TYPE_SWEEP)
if (noise_type == jammer::JammerType::TYPE_SWEEP || noise_type == jammer::JammerType::TYPE_SINE ||
noise_type == jammer::JammerType::TYPE_SQUARE || noise_type == jammer::JammerType::TYPE_SAWTOOTH ||
noise_type == jammer::JammerType::TYPE_TRIANGLE || noise_type == jammer::JammerType::TYPE_CHIRP ||
noise_type == jammer::JammerType::TYPE_GAUSSIAN || noise_type == jammer::JammerType::TYPE_BRUTEFORCE)
noise_period >>= 8;
period_counter = 0;
jammer_duration = 0;
current_range = 0;
lfsr = 0xDEAD0012;
wave_phase = 0;
wave_index = 0;
chirp_freq = 0.0f;
configured = true;
} else {

View File

@@ -1,6 +1,7 @@
/*
* Copyright (C) 2014 Jared Boone, ShareBrained Technology, Inc.
* Copyright (C) 2016 Furrtek
* Copyright (C) 2025 RocketGod - Added modes from my Flipper Zero RF Jammer App - https://betaskynet.com
*
* This file is part of PortaPack.
*
@@ -27,6 +28,8 @@
#include "baseband_thread.hpp"
#include "portapack_shared_memory.hpp"
#include "jammer.hpp"
#include <random>
#include <cmath>
using namespace jammer;
@@ -50,9 +53,11 @@ class JammerProcessor : public BasebandProcessor {
uint32_t aphase{0}, phase{0}, delta{0}, sphase{0};
int8_t sample{0};
int8_t re{0}, im{0};
uint32_t wave_phase{0};
uint32_t wave_index{0};
float chirp_freq{0.0f};
RetuneMessage message{};
/* NB: Threads should be the last members in the class definition. */
BasebandThread baseband_thread{3072000, this, baseband::Direction::Transmit};
};

View File

@@ -0,0 +1,180 @@
/*
* Copyright (C) 2025 Brumi, 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_noaaapt_rx.hpp"
#include "sine_table_int8.hpp"
#include "portapack_shared_memory.hpp"
#include "audio_dma.hpp"
#include "math.h"
#include "event_m4.hpp"
#include "fxpt_atan2.hpp"
#include <cstdint>
#include <cstddef>
#define NOAAAPT_PX_SIZE 2080.0
// updates the per pixel timers
void NoaaAptRx::update_params() {
// TODO HTOTOO
// 2080 px / line with chan a,b + sync + telemetry
pxRem = (double)12000.0 / NOAAAPT_PX_SIZE / 2.0;
samples_per_pixel = pxRem;
pxRem -= samples_per_pixel;
pxRoll = 0;
status_message.state = 0;
shared_memory.application_queue.push(status_message);
}
void NoaaAptRx::execute(const buffer_c8_t& buffer) {
if (!configured) {
return;
}
const auto decim_0_out = decim_0.execute(buffer, dst_buffer);
const auto channel = decim_1.execute(decim_0_out, dst_buffer);
// TODO: Feed channel_stats post-decimation data?
feed_channel_stats(channel);
/* spectrum_samples += channel.count;
if (spectrum_samples >= spectrum_interval_samples) {
spectrum_samples -= spectrum_interval_samples;
channel_spectrum.feed(channel, channel_filter_low_f, channel_filter_high_f, channel_filter_transition);
}
*/
/* 96kHz complex<int16_t>[64] for wfmam NOAA
* -> FM demodulation
* -> 96kHz int16_t[64] */
auto audio_oversampled = demod.execute(channel, work_audio_buffer); // fs 384khz wfm , 96khz wfmam for NOAA
/* 96kHz int16_t[64] for wfam
* -> 4th order CIC decimation by 2, gain of 1
* -> 48kHz int16_t[32] */
auto audio_4fs = audio_dec_1.execute(audio_oversampled, work_audio_buffer);
/* 48kHz int16_t[32] for wfman
* -> 4th order CIC decimation by 2, gain of 1
* -> 24kHz int16_t[16] */
auto audio_2fs = audio_dec_2.execute(audio_4fs, work_audio_buffer);
/* 24kHz int16_t[16] for wfmam
* -> FIR filter, <4.5kHz (0.1875fs) pass, >5.2kHz (0.2166fs) stop, gain of 1
* -> 12kHz int16_t[8] */
auto audio = audio_filter.execute(audio_2fs, work_audio_buffer);
/* -> 12kHz int16_t[8] for wfmam , */
std::array<float, 32> audio_f;
audio_output.apt_write(audio, audio_f); // we are in added wfmam (noaa), decim_1.decimation_factor == 8
for (size_t c = 0; c < audio.count; c++) {
if (status_message.state == 0 && false) { // disabled this due to NIY
// first look for the sync!
} else {
cnt++;
if (cnt >= (samples_per_pixel + (uint32_t)pxRoll)) { // got a pixel
cnt = 0;
if (pxRoll >= 1) pxRoll -= 1.0;
pxRoll += pxRem;
if (image_message.cnt < 400) {
if (audio_f[c] >= 1) {
image_message.image[image_message.cnt++] = 255;
} else if (audio_f[c] <= 0) {
image_message.image[image_message.cnt++] = 0;
} else {
image_message.image[image_message.cnt++] = (audio_f[c]) * 255;
}
}
if (image_message.cnt >= 399) {
shared_memory.application_queue.push(image_message);
image_message.cnt = 0;
if (status_message.state != 2) {
status_message.state = 2;
shared_memory.application_queue.push(status_message);
}
}
}
}
}
}
void NoaaAptRx::on_message(const Message* const message) {
switch (message->id) {
case Message::ID::UpdateSpectrum:
case Message::ID::SpectrumStreamingConfig:
// channel_spectrum.on_message(message);
break;
case Message::ID::NoaaAptRxConfigure:
configure(*reinterpret_cast<const NoaaAptRxConfigureMessage*>(message));
break;
case Message::ID::CaptureConfig:
capture_config(*reinterpret_cast<const CaptureConfigMessage*>(message));
break;
default:
break;
}
}
void NoaaAptRx::configure(const NoaaAptRxConfigureMessage& message) {
(void)message;
constexpr size_t decim_0_input_fs = baseband_fs;
constexpr size_t decim_0_output_fs = decim_0_input_fs / decim_0.decimation_factor;
constexpr size_t decim_1_input_fs = decim_0_output_fs;
decim_0.configure(taps_16k0_decim_0.taps);
// decim_1.configure(message.decim_1_filter.taps); // Original .
// TODO dynamic decim1 , with decimation 2 / 8 and 16 x taps , / 32 taps .
// Temptatively , I splitted, in two WidebandFMAudio::configure_wfm / WidebandFMAudio::configure_wfmam and dynamically /2, /8 . (here /8)
// decim_1.set<dsp::decimate::FIRC16xR16x16Decim2>().configure(message.decim_1_filter.taps); // for wfm
// decim_1.set<dsp::decimate::FIRC16xR16x32Decim8>().configure(message.decim_1_filter.taps); // for wfmam
decim_1.configure(taps_38k_wfmam_decim_1.taps); // for wfmam
size_t decim_1_output_fs = decim_1_input_fs / decim_1.decimation_factor; // wfmam, decim_1.decimation_factor() = /8 ,if applied after the line, decim_1.set<dsp::decimate::FIRC16xR16x16Decim2>().configure(message.decim_1_filter.taps);
size_t demod_input_fs = decim_1_output_fs;
// spectrum_interval_samples = decim_1_output_fs / spectrum_rate_hz;
// spectrum_samples = 0;
channel_filter_low_f = taps_38k_wfmam_decim_1.low_frequency_normalized * decim_1_input_fs;
channel_filter_high_f = taps_38k_wfmam_decim_1.high_frequency_normalized * decim_1_input_fs;
channel_filter_transition = taps_38k_wfmam_decim_1.transition_normalized * decim_1_input_fs;
demod.configure(demod_input_fs, 17000);
audio_filter.configure(taps_64_bpf_2k4_bw_2k.taps);
audio_output.configure(apt_audio_12k_notch_2k4_config, apt_audio_12k_lpf_2000hz_config);
// channel_spectrum.set_decimation_factor(1);
update_params();
configured = true;
}
void NoaaAptRx::capture_config(const CaptureConfigMessage& message) {
if (message.config) {
audio_output.set_stream(std::make_unique<StreamInput>(message.config));
} else {
audio_output.set_stream(nullptr);
}
}
int main() {
audio::dma::init_audio_out();
EventDispatcher event_dispatcher{std::make_unique<NoaaAptRx>()};
event_dispatcher.run();
return 0;
}

View File

@@ -0,0 +1,124 @@
/*
* Copyright (C) 2025 Brumi, 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_NOAAAPTRX_H__
#define __PROC_NOAAAPTRX_H__
#include "baseband_processor.hpp"
#include "baseband_thread.hpp"
#include "rssi_thread.hpp"
#include "dsp_decimate.hpp"
#include "dsp_demodulate.hpp"
#include "dsp_iir.hpp"
#include "audio_compressor.hpp"
#include "audio_output.hpp"
#include "spectrum_collector.hpp"
#include <cstdint>
class NoaaAptRx : public BasebandProcessor {
public:
void execute(const buffer_c8_t& buffer) override;
void on_message(const Message* const message) override;
private:
void update_params();
// todo rethink
uint32_t samples_per_pixel = 0;
// to exactly match the pixel / samples.
double pxRem = 0; // if has remainder, it'll store it
double pxRoll = 0; // summs remainders, so won't misalign
uint32_t cnt = 0; // signal counter
uint16_t sync_cnt = 0;
uint16_t syncnot_cnt = 0;
static constexpr size_t baseband_fs = 3072000;
static constexpr auto spectrum_rate_hz = 50.0f;
std::array<complex16_t, 512> dst{};
const buffer_c16_t dst_buffer{
dst.data(),
dst.size()};
// work_audio_buffer and dst_buffer use the same data pointer
const buffer_s16_t work_audio_buffer{
(int16_t*)dst.data(),
sizeof(dst) / sizeof(int16_t)};
std::array<complex16_t, 64> complex_audio{};
const buffer_c16_t complex_audio_buffer{
complex_audio.data(),
complex_audio.size()};
dsp::decimate::FIRC8xR16x24FS4Decim4 decim_0{};
// dsp::decimate::FIRC16xR16x16Decim2 decim_1{}; //original condition , before adding wfmam
// decim_1 will handle different types of FIR filters depending on selection.
dsp::decimate::FIRC16xR16x32Decim8 decim_1{};
// dsp::decimate::FIRC16xR16x32Decim8 decim_1{}; // For FMAM
int32_t channel_filter_low_f = 0;
int32_t channel_filter_high_f = 0;
int32_t channel_filter_transition = 0;
dsp::demodulate::FM demod{};
dsp::decimate::DecimateBy2CIC4Real audio_dec_1{};
dsp::decimate::DecimateBy2CIC4Real audio_dec_2{};
dsp::decimate::FIR64AndDecimateBy2Real audio_filter{};
AudioOutput audio_output{};
// For fs=96kHz FFT streaming
BlockDecimator<complex16_t, 256> audio_spectrum_decimator{1};
std::array<std::complex<float>, 256> audio_spectrum{};
uint32_t audio_spectrum_timer{0};
enum AudioSpectrumState {
IDLE = 0,
FEED,
FFT
};
AudioSpectrumState audio_spectrum_state{IDLE};
AudioSpectrum spectrum{};
uint32_t fft_step{0};
// SpectrumCollector channel_spectrum{};
// size_t spectrum_interval_samples = 0;
// size_t spectrum_samples = 0;
bool configured{false};
void configure(const NoaaAptRxConfigureMessage& message);
void capture_config(const CaptureConfigMessage& message);
NoaaAptRxStatusDataMessage status_message{0};
NoaaAptRxImageDataMessage image_message{};
/* NB: Threads should be the last members in the class definition. */
BasebandThread baseband_thread{baseband_fs, this, baseband::Direction::Receive};
RSSIThread rssi_thread{};
};
#endif /*__PROC_NOAAAPTRX_H__*/

View File

@@ -38,28 +38,72 @@ void SigGenProcessor::execute(const buffer_c8_t& buffer) {
} else
sample_count--;
if (tone_shape == 0) {
if (modulation == 0) {
// CW
re = 127; // max. signed 8 bits value . (-128 ...+127), max. amplitude , static phasor at 0º
im = 0;
} else {
if (tone_shape == 1) {
} else if (modulation == 2) {
// Digital BPSK consecutive 0,1,0,...continuous cycle, 1 bit/symbol, at rate of 2 symbols / Freq Tone Periode... without any Pulse shape at the moment .
re = (((tone_phase & 0xFF000000) >> 24) & 0x80) ? 127 : -128; // Sending 2 bits by Periode T of the GUI tone, alternative static phasor to 0, -180º , 0º
im = 0;
tone_phase += tone_delta; // In BPSK-QSPK we are using to calculate each 1/4 of the periode.
} else if (modulation == 3) {
// Digital QPSK consecutive 00, 01, 10, 11,00, ...continuous cycle ,2 bits/symbol, at rate of 4 symbols / Freq Tone Periode. not random., without any Pulse shape at the moment.
switch (((tone_phase & 0xFF000000) >> 24)) {
case 0 ... 63: // equivalent to 1/4 of total 360º degrees.
/* "00" */
re = (sine_table_i8[32]); // we are sending symbol-phasor 45º during 1/4 of the total periode
im = (sine_table_i8[32 + 64]); // 32 index = rounded (45º/360º * 255 total sin table steps) = 31,875
break;
case 64 ... 127:
/* "01" */
re = (sine_table_i8[96]); // symbol-phasor 135º
im = (sine_table_i8[96 + 64]); // 96 index = 32 + 256/4
break;
case 128 ... 191:
/* "10" */
re = (sine_table_i8[159]); // symbol-phasor 225º
im = (sine_table_i8[159 + 64]); // 159 rounded index = 96 + 256/4 = 159.3
break;
case 192 ... 255:
/* "11" */
re = (sine_table_i8[223]); // symbol-phasor 315º ; 223 rounded index = (315/360) * 255 =223.125
im = (sine_table_i8[((223 + 64) & 0xFF)]); // Max index 255, circular periodic conversion.
break;
default:
break;
}
tone_phase += tone_delta; // In BPSK-QSPK we are using to calculate each 1/4 of the periode.
} else if (modulation == 7) {
// Pulsed CW, 25% duty cycle.
if (tone_phase < 1073741824) // 1073741824 = 2^32*(25/100)
re = 127;
else
re = 0;
im = 0;
tone_phase += tone_delta; // In Pulsed CW we are using to calculate each periode.
} else { // Other modulations: FM, DSB, AM
if (tone_shape == 0) {
// Sine
sample = (sine_table_i8[(tone_phase & 0xFF000000) >> 24]);
} else if (tone_shape == 2) {
} else if (tone_shape == 1) {
// Triangle
int8_t a = (tone_phase & 0xFF000000) >> 24;
sample = (a & 0x80) ? ((a << 1) ^ 0xFF) - 0x80 : (a << 1) + 0x80;
} else if (tone_shape == 3) {
} else if (tone_shape == 2) {
// Saw up
sample = ((tone_phase & 0xFF000000) >> 24);
} else if (tone_shape == 4) {
} else if (tone_shape == 3) {
// Saw down
sample = ((tone_phase & 0xFF000000) >> 24) ^ 0xFF;
} else if (tone_shape == 5) {
} else if (tone_shape == 4) {
// Square
sample = (((tone_phase & 0xFF000000) >> 24) & 0x80) ? 127 : -128;
} else if (tone_shape == 6) {
} else if (tone_shape == 5) {
// Noise generator, pseudo random noise generator, 16 bits linear-feedback shift register (LFSR) algorithm, variant Fibonacci.
// https://en.wikipedia.org/wiki/Linear-feedback_shift_register
// 16 bits LFSR .taps: 16, 15, 13, 4 ;feedback polynomial: x^16 + x^15 + x^13 + x^4 + 1
@@ -79,49 +123,13 @@ void SigGenProcessor::execute(const buffer_c8_t& buffer) {
if (counter == 15) {
counter = 0;
}
} else if (tone_shape == 7) {
// Digital BPSK consecutive 0,1,0,...continuous cycle, 1 bit/symbol, at rate of 2 symbols / Freq Tone Periode... without any Pulse shape at the moment .
re = (((tone_phase & 0xFF000000) >> 24) & 0x80) ? 127 : -128; // Sending 2 bits by Periode T of the GUI tone, alternative static phasor to 0, -180º , 0º
im = 0;
} else if (tone_shape == 8) {
// Digital QPSK consecutive 00, 01, 10, 11,00, ...continuous cycle ,2 bits/symbol, at rate of 4 symbols / Freq Tone Periode. not random., without any Pulse shape at the moment .
switch (((tone_phase & 0xFF000000) >> 24)) {
case 0 ... 63: // equivalent to 1/4 of total 360º degrees.
/* "00" */
re = (sine_table_i8[32]); // we are sending symbol-phasor 45º during 1/4 of the total periode
im = (sine_table_i8[32 + 64]); // 32 index = rounded (45º/360º * 255 total sin table steps) = 31,875
break;
case 64 ... 127:
/* "01" */
re = (sine_table_i8[96]); // symbol-phasor 135º
im = (sine_table_i8[96 + 64]); // 96 index = 32 + 256/4
break;
break;
case 128 ... 191:
/* "10" */
re = (sine_table_i8[159]); // symbol-phasor 225º
im = (sine_table_i8[159 + 64]); // 159 rounded index = 96 + 256/4 = 159.3
break;
case 192 ... 255:
/* "11" */
re = (sine_table_i8[223]); // symbol-phasor 315º ; 223 rounded index = (315/360) * 255 =223.125
im = (sine_table_i8[((223 + 64) & 0xFF)]); // Max index 255, circular periodic conversion.
break;
default:
break;
}
}
if (tone_shape != 6) { //(all except Pseudo Random White Noise). We are in (1):periodic signals or (2):BPSK/QPSK , in both cases ,we need Tone updated acum sum phases to modulate in FM / or control phasor phase (BPSK & QPSK.)
tone_phase += tone_delta; // In periodic signals(Sine/triangle/square) we are using to FM mod. in BPSK-QSPK we are using to calculate each 1/4 of the periode.
if (tone_shape != 5) { // All periodic except Pseudo Random White Noise.
tone_phase += tone_delta; // In periodic signals we are using phase to generate the tone to be modulated.
}
if (tone_shape < 7) { // All Option shape signals except BPSK(7) & QPSK(8) we are modulating in FM. (Those two has phase shift modulation XPSK , not FM )
if (modulation == 1) {
// Do FM modulation
delta = sample * fm_delta;
@@ -130,6 +138,21 @@ void SigGenProcessor::execute(const buffer_c8_t& buffer) {
re = (sine_table_i8[(sphase & 0xFF000000) >> 24]); // sin LUT is not dealing with decimals , output range [-128 ,...127]
im = (sine_table_i8[(phase & 0xFF000000) >> 24]);
} else if (modulation == 4) {
// Do Double Side Band modulation
re = sample;
im = 0;
} else if (modulation == 5) {
// Do AM modulation (100% mod index)
re = 64 + (sample >> 1); // 64 = 127 - (127 >> 1): carrier level without modulating signal
im = 0;
} else if (modulation == 6) {
// Do AM modulation (50% mod index)
re = 96 + (sample >> 2); // 96 = 127 - (127 >> 2): carrier level without modulating signal
im = 0;
}
}
@@ -154,7 +177,8 @@ void SigGenProcessor::on_message(const Message* const msg) {
auto_off = false;
fm_delta = message.bw * (0xFFFFFFULL / 1536000);
tone_shape = message.shape;
tone_shape = message.shape & 0xF;
modulation = (message.shape & 0xF0) >> 4;
// lfsr = seed_value ; // Finally not used , init lfsr 8 bits.
lfsr_16 = seed_value_16; // init lfsr 16 bits.

View File

@@ -36,7 +36,7 @@ class SigGenProcessor : public BasebandProcessor {
bool configured{false};
uint32_t tone_delta{0}, fm_delta{}, tone_phase{0};
uint8_t tone_shape{};
uint8_t tone_shape{}, modulation{};
uint32_t sample_count{0};
bool auto_off{};
int32_t phase{0}, sphase{0}, delta{0}; // they may have sign in the pseudo random sample generation.

View File

@@ -0,0 +1,186 @@
/*
* Copyright (C) 2025 Brumi, 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_wefaxrx.hpp"
#include "sine_table_int8.hpp"
#include "portapack_shared_memory.hpp"
#include "audio_dma.hpp"
#include "math.h"
#include "event_m4.hpp"
#include "fxpt_atan2.hpp"
#include <cstdint>
#include <cstddef>
#define STARTSIGNAL_TH 0.33
#define STARTSIGNAL_NEEDCNT 110
#define STARTSIGNAL_MAXBAD 20
#define WEFAX_PX_SIZE 840.0
// updates the per pixel timers
void WeFaxRx::update_params() {
switch (ioc_mode) {
case 1:
freq_start_tone = 675;
break;
default:
case 0:
freq_start_tone = 300;
break;
}
// 840 px / line with line start
pxRem = (double)channel_filter_input_fs / ((lpm / 60.0) * WEFAX_PX_SIZE);
samples_per_pixel = pxRem;
pxRem -= samples_per_pixel;
pxRoll = 0;
status_message.state = 0;
shared_memory.application_queue.push(status_message);
}
void WeFaxRx::execute(const buffer_c8_t& buffer) {
// This is called at 3072000 / 2048 = 1500Hz
if (!configured) return;
const auto decim_0_out = decim_0.execute(buffer, dst_buffer);
const auto decim_1_out = decim_1.execute(decim_0_out, dst_buffer);
channel_spectrum.feed(decim_1_out, channel_filter_low_f, channel_filter_high_f, channel_filter_transition);
const auto decim_2_out = decim_2.execute(decim_1_out, dst_buffer);
const auto channel_out = channel_filter.execute(decim_2_out, dst_buffer);
feed_channel_stats(channel_out);
auto audio = demod_ssb_fm.execute(channel_out, audio_buffer);
audio_compressor.execute_in_place(audio);
audio_output.write(audio);
for (size_t c = 0; c < audio.count; c++) {
if (status_message.state == 0 && false) { // disabled this due to so sensitive to noise
// first look for the sync!
if (audio.p[c] <= STARTSIGNAL_TH && audio.p[c] >= 0.0001) {
sync_cnt++;
if (sync_cnt >= STARTSIGNAL_NEEDCNT) {
status_message.state = 1;
shared_memory.application_queue.push(status_message);
sync_cnt = 0;
syncnot_cnt = 0;
}
} else {
syncnot_cnt++;
if (syncnot_cnt >= STARTSIGNAL_MAXBAD) {
sync_cnt = 0;
syncnot_cnt = 0;
}
}
} else {
cnt++;
if (cnt >= (samples_per_pixel + (uint32_t)pxRoll)) { // got a pixel
cnt = 0;
if (pxRoll >= 1) pxRoll -= 1.0;
pxRoll += pxRem;
if (image_message.cnt < 400) {
if (audio.p[c] >= 0.68) {
image_message.image[image_message.cnt++] = 255;
} else if (audio.p[c] >= 0.45) {
image_message.image[image_message.cnt++] = (uint8_t)(((audio.p[c] - 0.45f) * 1108));
} else {
image_message.image[image_message.cnt++] = 0;
}
}
if (image_message.cnt >= 399) {
shared_memory.application_queue.push(image_message);
image_message.cnt = 0;
if (status_message.state != 2) {
status_message.state = 2;
shared_memory.application_queue.push(status_message);
}
}
}
}
}
}
void WeFaxRx::on_message(const Message* const message) {
switch (message->id) {
case Message::ID::UpdateSpectrum:
case Message::ID::SpectrumStreamingConfig:
channel_spectrum.on_message(message);
break;
case Message::ID::WeFaxRxConfigure:
configure(*reinterpret_cast<const WeFaxRxConfigureMessage*>(message));
break;
case Message::ID::CaptureConfig:
capture_config(*reinterpret_cast<const CaptureConfigMessage*>(message));
break;
default:
break;
}
}
void WeFaxRx::configure(const WeFaxRxConfigureMessage& message) {
constexpr size_t decim_0_input_fs = baseband_fs;
constexpr size_t decim_0_output_fs = decim_0_input_fs / decim_0.decimation_factor;
constexpr size_t decim_1_input_fs = decim_0_output_fs;
constexpr size_t decim_1_output_fs = decim_1_input_fs / decim_1.decimation_factor;
constexpr size_t decim_2_input_fs = decim_1_output_fs;
constexpr size_t decim_2_output_fs = decim_2_input_fs / decim_2_decimation_factor;
channel_filter_input_fs = decim_2_output_fs;
// const size_t channel_filter_output_fs = channel_filter_input_fs / channel_filter_decimation_factor;
decim_0.configure(taps_6k0_decim_0.taps);
decim_1.configure(taps_6k0_decim_1.taps);
decim_2.configure(taps_6k0_decim_2.taps, decim_2_decimation_factor);
channel_filter.configure(taps_2k6_usb_wefax_channel.taps, channel_filter_decimation_factor);
channel_filter_low_f = taps_2k6_usb_wefax_channel.low_frequency_normalized * channel_filter_input_fs;
channel_filter_high_f = taps_2k6_usb_wefax_channel.high_frequency_normalized * channel_filter_input_fs;
channel_filter_transition = taps_2k6_usb_wefax_channel.transition_normalized * channel_filter_input_fs;
channel_spectrum.set_decimation_factor(1.0f);
audio_output.configure(apt_audio_12k_lpf_1500hz_config); // hpf in all AM demod modes (AM-6K/9K, USB/LSB,DSB), except Wefax (lpf there).
lpm = message.lpm;
ioc_mode = message.ioc;
update_params();
configured = true;
}
void WeFaxRx::capture_config(const CaptureConfigMessage& message) {
if (message.config) {
audio_output.set_stream(std::make_unique<StreamInput>(message.config));
} else {
audio_output.set_stream(nullptr);
}
}
int main() {
audio::dma::init_audio_out();
EventDispatcher event_dispatcher{std::make_unique<WeFaxRx>()};
event_dispatcher.run();
return 0;
}

View File

@@ -0,0 +1,104 @@
/*
* Copyright (C) 2025 Brumi, 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_WEFAXRX_H__
#define __PROC_WEFAXRX_H__
#include "baseband_processor.hpp"
#include "baseband_thread.hpp"
#include "rssi_thread.hpp"
#include "dsp_decimate.hpp"
#include "dsp_demodulate.hpp"
#include "dsp_iir.hpp"
#include "audio_compressor.hpp"
#include "audio_output.hpp"
#include "spectrum_collector.hpp"
#include <cstdint>
class WeFaxRx : public BasebandProcessor {
public:
void execute(const buffer_c8_t& buffer) override;
void on_message(const Message* const message) override;
private:
void update_params();
// todo rethink
uint8_t lpm = 120; // 60, 90, 100, 120, 180, 240 lpm
uint8_t ioc_mode = 0; // 0 - ioc576, 1 - ioc 288, 2 - colour fax
uint32_t samples_per_pixel = 0;
// not yet used:
uint32_t time_start_tone = 3000; // 3s - 5s
uint32_t freq_start_tone = 300; // 300hz for ioc576 675hz for ioc288, 200hz for colour fax
uint32_t freq_stop_tone = 450; // 450hz for the 3-5s stop tone
// to exactly match the pixel / samples.
double pxRem = 0; // if has remainder, it'll store it
double pxRoll = 0; // summs remainders, so won't misalign
uint32_t cnt = 0; // signal counter
uint16_t sync_cnt = 0;
uint16_t syncnot_cnt = 0;
static constexpr size_t baseband_fs = 3072000;
static constexpr size_t decim_2_decimation_factor = 4;
static constexpr size_t channel_filter_decimation_factor = 1;
std::array<complex16_t, 512> dst{};
const buffer_c16_t dst_buffer{
dst.data(),
dst.size()};
std::array<float, 32> audio{};
const buffer_f32_t audio_buffer{
audio.data(),
audio.size()};
size_t channel_filter_input_fs = 0;
dsp::decimate::FIRC8xR16x24FS4Decim8 decim_0{};
dsp::decimate::FIRC16xR16x32Decim8 decim_1{};
dsp::decimate::FIRAndDecimateComplex decim_2{};
dsp::decimate::FIRAndDecimateComplex channel_filter{};
int32_t channel_filter_low_f = 0;
int32_t channel_filter_high_f = 0;
int32_t channel_filter_transition = 0;
bool configured{false};
dsp::demodulate::SSB_FM demod_ssb_fm{}; // added for Wfax mode.
FeedForwardCompressor audio_compressor{};
AudioOutput audio_output{};
SpectrumCollector channel_spectrum{};
void configure(const WeFaxRxConfigureMessage& message);
void capture_config(const CaptureConfigMessage& message);
WeFaxRxStatusDataMessage status_message{0};
WeFaxRxImageDataMessage image_message{};
/* NB: Threads should be the last members in the class definition. */
BasebandThread baseband_thread{baseband_fs, this, baseband::Direction::Receive};
RSSIThread rssi_thread{};
};
#endif /*__PROC_WEFAXRX_H__*/

View File

@@ -47,23 +47,37 @@ void WidebandFMAudio::execute(const buffer_c8_t& buffer) {
channel_spectrum.feed(channel, channel_filter_low_f, channel_filter_high_f, channel_filter_transition);
}
/* 384kHz complex<int16_t>[256]
/* 384kHz complex<int16_t>[256] for wfm
* -> FM demodulation
* -> 384kHz int16_t[256] */
/* TODO: To improve adjacent channel rejection, implement complex channel filter:
* pass < +/- 100kHz, stop > +/- 200kHz
*/
auto audio_oversampled = demod.execute(channel, work_audio_buffer);
/* 96kHz complex<int16_t>[64] for wfmam NOAA
* -> FM demodulation
* -> 96kHz int16_t[64] */
/* 384kHz int16_t[256]
auto audio_oversampled = demod.execute(channel, work_audio_buffer); // fs 384khz wfm , 96khz wfmam for NOAA
/* 384kHz int16_t[256] for wfm
* -> 4th order CIC decimation by 2, gain of 1
* -> 192kHz int16_t[128] */
/* 96kHz int16_t[64] for wfam
* -> 4th order CIC decimation by 2, gain of 1
* -> 48kHz int16_t[32] */
auto audio_4fs = audio_dec_1.execute(audio_oversampled, work_audio_buffer);
/* 192kHz int16_t[128]
/* 192kHz int16_t[128] for wfm
* -> 4th order CIC decimation by 2, gain of 1
* -> 96kHz int16_t[64] */
/* 48kHz int16_t[32] for wfman
* -> 4th order CIC decimation by 2, gain of 1
* -> 24kHz int16_t[16] */
auto audio_2fs = audio_dec_2.execute(audio_4fs, work_audio_buffer);
// Input: 96kHz int16_t[64]
@@ -118,13 +132,24 @@ void WidebandFMAudio::execute(const buffer_c8_t& buffer) {
break;
}
/* 96kHz int16_t[64]
/* 96kHz int16_t[64] for wfm
* -> FIR filter, <15kHz (0.156fs) pass, >19kHz (0.198fs) stop, gain of 1
* -> 48kHz int16_t[32] */
/* 24kHz int16_t[16] for wfmam
* -> FIR filter, <4.5kHz (0.1875fs) pass, >5.2kHz (0.2166fs) stop, gain of 1
* -> 12kHz int16_t[8] */
auto audio = audio_filter.execute(audio_2fs, work_audio_buffer);
/* -> 48kHz int16_t[32] */
audio_output.write(audio);
/* -> 48kHz int16_t[32] for wfm , */
/* -> 12kHz int16_t[8] for wfmam , */
if (decim_1.decimation_factor() == 2) {
audio_output.write(audio); // we are in original wfm , decim_1.decimation_factor == 2
} else {
audio_output.apt_write(audio); // we are in added wfmam (noaa), decim_1.decimation_factor == 8
}
}
void WidebandFMAudio::post_message(const buffer_c16_t& data) {
@@ -142,7 +167,11 @@ void WidebandFMAudio::on_message(const Message* const message) {
break;
case Message::ID::WFMConfigure:
configure(*reinterpret_cast<const WFMConfigureMessage*>(message));
configure_wfm(*reinterpret_cast<const WFMConfigureMessage*>(message));
break;
case Message::ID::WFMAMConfigure:
configure_wfmam(*reinterpret_cast<const WFMAMConfigureMessage*>(message));
break;
case Message::ID::CaptureConfig:
@@ -154,20 +183,56 @@ void WidebandFMAudio::on_message(const Message* const message) {
}
}
void WidebandFMAudio::configure(const WFMConfigureMessage& message) {
void WidebandFMAudio::configure_wfm(const WFMConfigureMessage& message) {
constexpr size_t decim_0_input_fs = baseband_fs;
constexpr size_t decim_0_output_fs = decim_0_input_fs / decim_0.decimation_factor;
constexpr size_t decim_1_input_fs = decim_0_output_fs;
constexpr size_t decim_1_output_fs = decim_1_input_fs / decim_1.decimation_factor;
constexpr size_t demod_input_fs = decim_1_output_fs;
decim_0.configure(message.decim_0_filter.taps);
// decim_1.configure(message.decim_1_filter.taps); // Original .
// TODO dynamic decim1 , with decimation 2 / 8 and 16 x taps , / 32 taps .
// Temptatively , I splitted, in two WidebandFMAudio::configure_wfm / WidebandFMAudio::configure_wfmam and dynamically /2, /8 (here /2)
// decim_1.set<dsp::decimate::FIRC16xR16x16Decim2>().configure(message.decim_1_filter.taps); // for wfm
// decim_1.set<dsp::decimate::FIRC16xR16x32Decim8>().configure(taps_84k_wfmam_decim_1.taps); // for wfmam
decim_1.set<dsp::decimate::FIRC16xR16x16Decim2>().configure(message.decim_1_filter.taps); // for wfm
size_t decim_1_output_fs = decim_1_input_fs / decim_1.decimation_factor(); // wfm , decim_1.decimation_factor() = /2 , if applied after the line : decim_1.set<dsp::decimate::FIRC16xR16x16Decim2>().configure(message.decim_1_filter.taps);
size_t demod_input_fs = decim_1_output_fs;
spectrum_interval_samples = decim_1_output_fs / spectrum_rate_hz;
spectrum_samples = 0;
channel_filter_low_f = message.decim_1_filter.low_frequency_normalized * decim_1_input_fs;
channel_filter_high_f = message.decim_1_filter.high_frequency_normalized * decim_1_input_fs;
channel_filter_transition = message.decim_1_filter.transition_normalized * decim_1_input_fs;
demod.configure(demod_input_fs, message.deviation);
audio_filter.configure(message.audio_filter.taps);
audio_output.configure(message.audio_hpf_config, message.audio_deemph_config);
channel_spectrum.set_decimation_factor(1);
configured = true;
}
void WidebandFMAudio::configure_wfmam(const WFMAMConfigureMessage& message) {
constexpr size_t decim_0_input_fs = baseband_fs;
constexpr size_t decim_0_output_fs = decim_0_input_fs / decim_0.decimation_factor;
constexpr size_t decim_1_input_fs = decim_0_output_fs;
decim_0.configure(message.decim_0_filter.taps);
decim_1.configure(message.decim_1_filter.taps);
// decim_1.configure(message.decim_1_filter.taps); // Original .
// TODO dynamic decim1 , with decimation 2 / 8 and 16 x taps , / 32 taps .
// Temptatively , I splitted, in two WidebandFMAudio::configure_wfm / WidebandFMAudio::configure_wfmam and dynamically /2, /8 . (here /8)
// decim_1.set<dsp::decimate::FIRC16xR16x16Decim2>().configure(message.decim_1_filter.taps); // for wfm
// decim_1.set<dsp::decimate::FIRC16xR16x32Decim8>().configure(message.decim_1_filter.taps); // for wfmam
decim_1.set<dsp::decimate::FIRC16xR16x32Decim8>().configure(message.decim_1_filter.taps); // for wfmam
size_t decim_1_output_fs = decim_1_input_fs / decim_1.decimation_factor(); // wfmam, decim_1.decimation_factor() = /8 ,if applied after the line, decim_1.set<dsp::decimate::FIRC16xR16x16Decim2>().configure(message.decim_1_filter.taps);
size_t demod_input_fs = decim_1_output_fs;
spectrum_interval_samples = decim_1_output_fs / spectrum_rate_hz;
spectrum_samples = 0;
channel_filter_low_f = message.decim_1_filter.low_frequency_normalized * decim_1_input_fs;
channel_filter_high_f = message.decim_1_filter.high_frequency_normalized * decim_1_input_fs;
channel_filter_transition = message.decim_1_filter.transition_normalized * decim_1_input_fs;

View File

@@ -35,6 +35,46 @@
#include "audio_output.hpp"
#include "spectrum_collector.hpp"
#include <array>
#include <memory>
#include <tuple>
#include <variant>
template <typename... Args>
class MultiDecimator {
public:
/* Dispatches to the underlying type's execute. */
template <typename Source, typename Destination>
Destination execute(
const Source& src,
const Destination& dst) {
return std::visit(
[&src, &dst](auto&& arg) -> Destination {
return arg.execute(src, dst);
},
decimator_);
}
size_t decimation_factor() const {
return std::visit(
[](auto&& arg) -> size_t {
return arg.decimation_factor;
},
decimator_);
}
/* Sets this decimator to a new instance of the specified decimator type.
* NB: The instance is returned by-ref so 'configure' can easily be called. */
template <typename Decimator>
Decimator& set() {
decimator_ = Decimator{};
return std::get<Decimator>(decimator_);
}
private:
std::variant<Args...> decimator_{};
};
class WidebandFMAudio : public BasebandProcessor {
public:
void execute(const buffer_c8_t& buffer) override;
@@ -59,7 +99,16 @@ class WidebandFMAudio : public BasebandProcessor {
complex_audio.size()};
dsp::decimate::FIRC8xR16x24FS4Decim4 decim_0{};
dsp::decimate::FIRC16xR16x16Decim2 decim_1{};
// dsp::decimate::FIRC16xR16x16Decim2 decim_1{}; //original condition , before adding wfmam
// decim_1 will handle different types of FIR filters depending on selection.
MultiDecimator<
dsp::decimate::FIRC16xR16x16Decim2,
dsp::decimate::FIRC16xR16x32Decim8>
decim_1{};
// dsp::decimate::FIRC16xR16x32Decim8 decim_1{}; // For FMAM
int32_t channel_filter_low_f = 0;
int32_t channel_filter_high_f = 0;
int32_t channel_filter_transition = 0;
@@ -94,7 +143,8 @@ class WidebandFMAudio : public BasebandProcessor {
BasebandThread baseband_thread{baseband_fs, this, baseband::Direction::Receive};
RSSIThread rssi_thread{};
void configure(const WFMConfigureMessage& message);
void configure_wfm(const WFMConfigureMessage& message);
void configure_wfmam(const WFMAMConfigureMessage& message);
void capture_config(const CaptureConfigMessage& message);
void post_message(const buffer_c16_t& data);
};