├── .github ├── ISSUE_TEMPLATE │ ├── bug_report.yml │ ├── config.yml │ └── feature_request.yml ├── dependabot.yml └── workflows │ ├── build_appimage_bundle.yml │ ├── build_docker_image.yml │ ├── build_nsis_bundle.yml │ ├── build_server.yml │ ├── codeql-analysis.yml │ ├── gui_tests.yml │ ├── modem_tests.yml │ ├── pip_package.yml │ └── prettier.yaml ├── .gitignore ├── .prettierignore ├── .stignore ├── Dockerfile ├── LICENSE ├── README.docker.md ├── README.md ├── add-osx-cert.sh ├── documentation ├── FreeDATA-Frametypes.ods ├── FreeDATA-connect-to-remote-daemon.png ├── FreeDATA-daemon_network_documentation.md ├── FreeDATA-no-daemon-connection.png ├── FreeDATA-protocols.md ├── FreeDATA-settings.png ├── FreeDATA-tnc-running.png ├── FreeDATA_GUI_Preview.png ├── FreeDATA_TNC_Preview.png ├── FreeDATA_chat_screen.png ├── FreeDATA_main_screen.png ├── FreeDATA_preview.gif ├── chat_preview_fast.gif ├── codec2-FreeDATA-start-connected.png ├── codec2-FreeDATA-start-disconnected.png ├── codec2-FreeDATA_05.09.2021_11_29_05.mp4 ├── cube.xcf ├── data_preview.gif ├── freedv_jate_tnc_preview.png └── icon.ico ├── entrypoint.sh ├── freedata-nsis-config.nsi ├── freedata_gui ├── README.md ├── babel.config.js ├── eslint.config.js ├── jsconfig.json ├── package.json ├── public │ ├── android-chrome-192x192.png │ ├── android-chrome-512x512.png │ ├── apple-touch-icon.png │ ├── favicon-16x16.png │ ├── favicon-32x32.png │ ├── favicon.ico │ ├── icon_cube_border.png │ ├── index.html │ └── manifest.json ├── src │ ├── App.vue │ ├── assets │ │ ├── countries-10m.json │ │ ├── countries-110m.json │ │ ├── countries-50m.json │ │ ├── logo.png │ │ └── waterfall │ │ │ ├── LICENSE │ │ │ ├── README.rst │ │ │ ├── colormap.js │ │ │ ├── index.html │ │ │ ├── make_colormap.py │ │ │ ├── spectrum.js │ │ │ └── waterfall.css │ ├── components │ │ ├── chat_conversations.vue │ │ ├── chat_messages.vue │ │ ├── chat_messages_action_menu.vue │ │ ├── chat_messages_image_preview.vue │ │ ├── chat_messages_received.vue │ │ ├── chat_messages_sent.vue │ │ ├── chat_new_message.vue │ │ ├── chat_screen.vue │ │ ├── dynamic_components.vue │ │ ├── grid │ │ │ ├── grid_CQ.vue │ │ │ ├── grid_active_audio.vue │ │ │ ├── grid_active_broadcasts.vue │ │ │ ├── grid_active_broadcasts_vert.vue │ │ │ ├── grid_active_heard_stations.vue │ │ │ ├── grid_active_heard_stations_mini.vue │ │ │ ├── grid_active_rig_control.vue │ │ │ ├── grid_active_stats.vue │ │ │ ├── grid_activities.vue │ │ │ ├── grid_beacon.vue │ │ │ ├── grid_button.vue │ │ │ ├── grid_dbfs.vue │ │ │ ├── grid_frequency.vue │ │ │ ├── grid_mycall small.vue │ │ │ ├── grid_mycall.vue │ │ │ ├── grid_ping.vue │ │ │ ├── grid_ptt.vue │ │ │ ├── grid_s-meter.vue │ │ │ ├── grid_scatter.vue │ │ │ ├── grid_stations_map.vue │ │ │ ├── grid_stats_chart.vue │ │ │ ├── grid_stop.vue │ │ │ ├── grid_swr_meter.vue │ │ │ └── grid_tune.vue │ │ ├── main_footer_navbar.vue │ │ ├── main_left_navbar.vue │ │ ├── main_loading_screen.vue │ │ ├── main_modals.vue │ │ ├── main_screen.vue │ │ ├── main_startup_check.vue │ │ ├── settings_chat.vue │ │ ├── settings_exp.vue │ │ ├── settings_flrig.vue │ │ ├── settings_gui.vue │ │ ├── settings_hamlib.vue │ │ ├── settings_modem.vue │ │ ├── settings_rigcontrol.vue │ │ ├── settings_screen.vue │ │ ├── settings_serial_ptt.vue │ │ ├── settings_station.vue │ │ ├── settings_url.vue │ │ └── settings_web.vue │ ├── js │ │ ├── api.js │ │ ├── eventHandler.js │ │ ├── event_sock.js │ │ ├── freedata.js │ │ ├── i18n.js │ │ ├── messagesHandler.js │ │ ├── mobile_devices.js │ │ ├── popupHandler.js │ │ ├── radioHandler.js │ │ ├── stationHandler.js │ │ └── waterfallHandler.js │ ├── locales │ │ ├── cz_Čeština.json │ │ ├── de_Deutsch.json │ │ ├── en_English.json │ │ ├── it_Italiano.json │ │ └── no_Norsk.json │ ├── main.js │ ├── store │ │ ├── audioStore.js │ │ ├── chatStore.js │ │ ├── index.js │ │ ├── serialStore.js │ │ ├── settingsStore.js │ │ ├── stateStore.js │ │ └── stationStore.js │ └── styles.css └── vue.config.js ├── freedata_server ├── .gitignore ├── __init__.py ├── adif_udp_logger.py ├── api │ ├── __init__.py │ ├── command_helpers.py │ ├── common.py │ ├── config.py │ ├── devices.py │ ├── freedata.py │ ├── general.py │ ├── modem.py │ ├── radio.py │ └── websocket.py ├── api_validations.py ├── arq_data_type_handler.py ├── arq_session.py ├── arq_session_irs.py ├── arq_session_iss.py ├── audio.py ├── audio_buffer.py ├── codec2.py ├── codec2_filter_coeff.py ├── command.py ├── command_arq_raw.py ├── command_beacon.py ├── command_cq.py ├── command_fec.py ├── command_message_send.py ├── command_p2p_connection.py ├── command_ping.py ├── command_qrv.py ├── command_test.py ├── command_transmit_sine.py ├── config.ini.example ├── config.py ├── constants.py ├── context.py ├── cw.py ├── data_frame_factory.py ├── demodulator.py ├── event_manager.py ├── exceptions.py ├── explorer.py ├── flrig.py ├── frame_dispatcher.py ├── frame_handler.py ├── frame_handler_arq_session.py ├── frame_handler_beacon.py ├── frame_handler_cq.py ├── frame_handler_p2p_connection.py ├── frame_handler_ping.py ├── helpers.py ├── lib │ └── codec2 │ │ ├── libcodec2.1.2.dylib │ │ ├── libcodec2.dll │ │ └── libcodec2.so ├── list_ports_winreg.py ├── log_handler.py ├── maidenhead.py ├── message_p2p.py ├── message_system_db_attachments.py ├── message_system_db_beacon.py ├── message_system_db_manager.py ├── message_system_db_messages.py ├── message_system_db_model.py ├── message_system_db_station.py ├── modem.py ├── modem_frametypes.py ├── modulator.py ├── p2p_connection.py ├── radio_manager.py ├── rigctld.py ├── rigdummy.py ├── schedule_manager.py ├── serial_ports.py ├── serial_ptt.py ├── server.py ├── service_manager.py ├── socket_interface.py ├── socket_interface_commands.py ├── socket_interface_data.py ├── state_manager.py ├── stats.py ├── wavelog_api_logger.py └── websocket_manager.py ├── requirements.txt ├── setup.py ├── tests ├── test_arq_session.py ├── test_config.py ├── test_data_frame_factory.py ├── test_data_type_handler.py ├── test_message_database.py ├── test_message_p2p.py ├── test_message_protocol.py ├── test_p2p_connection.py ├── test_protocols.py └── test_server.py └── tools ├── Linux ├── FreeDATA.desktop ├── README-Desktop-icon.txt ├── README.txt ├── install-freedata-linux.sh └── run-freedata-linux.sh ├── Windows ├── GUI-Install-Requirements.bat ├── GUI-Launch.bat ├── GUI-Update-Requirements.bat ├── Modem-Install-Requirements.bat ├── Modem-Launch.bat ├── Modem-Update-Requirements.bat ├── Modem-list-audio-devs.bat └── copy-files.bat ├── custom_mode_tests ├── create_custom_ofdm_mod.py ├── over_the_air_mode_test.py ├── plot_speed_levels.py └── run_mode_tests.py ├── macOS ├── README.md ├── install-freedata-macos.sh └── run-freedata-macos.sh ├── run-server.sh ├── run-tests.sh └── socket_interface ├── socket_client.py └── socket_data_client.py /.github/ISSUE_TEMPLATE/config.yml: -------------------------------------------------------------------------------- 1 | contact_links: 2 | - name: Discord Chat 3 | url: https://discord.gg/QewJE4hrFH 4 | about: Have questions? Try asking on our Discord - this issue tracker is for reporting bugs or feature requests only 5 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.yml: -------------------------------------------------------------------------------- 1 | name: Feature Request 2 | description: Suggest an idea for FreeDATA 3 | title: "[Feature Request]: " 4 | labels: "enhancement :sparkles:" 5 | body: 6 | 7 | - type: textarea 8 | attributes: 9 | label: Problem Description 10 | description: Please add a clear and concise description of the problem you are seeking to solve with this feature request. 11 | validations: 12 | required: true 13 | - type: textarea 14 | attributes: 15 | label: Proposed Solution 16 | description: Describe the solution you'd like in a clear and concise manner. 17 | validations: 18 | required: true 19 | - type: textarea 20 | attributes: 21 | label: Alternatives Considered 22 | description: A clear and concise description of any alternative solutions or features you've considered. 23 | validations: 24 | required: true 25 | - type: textarea 26 | attributes: 27 | label: Additional Information 28 | description: Add any other context about the problem here. 29 | validations: 30 | required: false 31 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | # https://docs.github.com/github/administering-a-repository/configuration-options-for-dependency-updates 2 | version: 2 3 | updates: 4 | 5 | # Maintain dependencies for GitHub Actions 6 | - package-ecosystem: "github-actions" 7 | directory: "/" 8 | schedule: 9 | interval: "monthly" 10 | target-branch: "develop" 11 | 12 | # Maintain dependencies for npm 13 | - package-ecosystem: "npm" 14 | directory: "/freedata_gui" 15 | schedule: 16 | interval: "monthly" 17 | target-branch: "develop" 18 | 19 | # Maintain dependencies for pip 20 | - package-ecosystem: "pip" 21 | directory: "/" 22 | schedule: 23 | interval: "monthly" 24 | target-branch: "develop" -------------------------------------------------------------------------------- /.github/workflows/build_docker_image.yml: -------------------------------------------------------------------------------- 1 | name: Build and Release Docker Image 2 | on: [push] 3 | 4 | 5 | jobs: 6 | build: 7 | runs-on: ubuntu-latest 8 | name: Build image and upload artifact 9 | steps: 10 | - name: Checkout Code 11 | uses: actions/checkout@v4 12 | 13 | - name: Set up Docker Buildx 14 | uses: docker/setup-buildx-action@v3 15 | 16 | # Build a single-architecture image (using linux/amd64) so we can load it locally. 17 | - name: Build Docker image for artifact 18 | run: | 19 | docker buildx build --platform linux/amd64 --load -t ghcr.io/dj2ls/freedata:latest . 20 | 21 | # Save the built image as a tarball. 22 | - name: Save Docker image to tar file 23 | run: | 24 | docker save ghcr.io/dj2ls/freedata:latest -o freedata.tar 25 | 26 | # Upload the image tarball as an artifact. 27 | - name: Upload Docker image artifact 28 | uses: actions/upload-artifact@v4 29 | with: 30 | name: freedata-image 31 | path: freedata.tar 32 | 33 | release: 34 | if: startsWith(github.ref, 'refs/tags/') 35 | needs: build 36 | runs-on: ubuntu-latest 37 | name: Build and Publish Release 38 | permissions: 39 | packages: write 40 | contents: read 41 | steps: 42 | - name: Checkout Repo 43 | uses: actions/checkout@v4 44 | 45 | - name: Set up QEMU 46 | uses: docker/setup-qemu-action@v3 47 | 48 | - name: Set up Docker Buildx 49 | uses: docker/setup-buildx-action@v3 50 | 51 | - name: Log in to GitHub Container Registry 52 | uses: docker/login-action@v3 53 | with: 54 | registry: ghcr.io 55 | username: ${{ github.actor }} 56 | password: ${{ secrets.GITHUB_TOKEN }} 57 | 58 | - name: Build and push multi-arch Docker images 59 | uses: docker/build-push-action@v6 60 | with: 61 | context: . 62 | platforms: linux/amd64,linux/arm64 63 | push: true 64 | tags: | 65 | ghcr.io/dj2ls/freedata:latest 66 | ghcr.io/dj2ls/freedata:${{ github.ref_name }} 67 | labels: | 68 | org.opencontainers.image.title=FreeDATA 69 | org.opencontainers.image.description=Docker image for FreeDATA 70 | org.opencontainers.image.url=https://github.com/dj2ls/freedata/pkgs/container/freedata/ 71 | -------------------------------------------------------------------------------- /.github/workflows/build_nsis_bundle.yml: -------------------------------------------------------------------------------- 1 | name: Build and Release NSIS Installer 2 | on: [push] 3 | 4 | jobs: 5 | build-and-release: 6 | runs-on: windows-latest 7 | 8 | steps: 9 | - name: Check out repository 10 | uses: actions/checkout@v4 11 | 12 | - name: Set up Python 3.11 13 | uses: actions/setup-python@v5 14 | with: 15 | python-version: "3.11" 16 | 17 | - name: Install Node.js, NPM and Yarn 18 | uses: actions/setup-node@v4 19 | with: 20 | node-version: 22 21 | 22 | - name: Vue Builder 23 | working-directory: freedata_gui 24 | run: | 25 | npm i 26 | npm run build 27 | 28 | - name: LIST ALL FILES 29 | run: ls -R 30 | 31 | - name: Install Python dependencies 32 | run: | 33 | python -m pip install --upgrade pip 34 | pip install -r requirements.txt 35 | 36 | - uses: robinraju/release-downloader@v1.12 37 | with: 38 | repository: "Hamlib/Hamlib" 39 | fileName: "hamlib-w64-*.zip" 40 | # latest: true 41 | extract: true 42 | tag: '4.5.5' 43 | out-file-path: "freedata_server/lib/hamlib" 44 | 45 | - name: Move Hamlib Files 46 | working-directory: freedata_server 47 | run: | 48 | # Find the downloaded folder (handle version numbers dynamically) 49 | $HAMLIB_DIR = Get-ChildItem -Directory -Path lib/hamlib -Filter "hamlib-w64*" | Select-Object -First 1 50 | # Move all contents from the found directory to the target directory 51 | Move-Item "$($HAMLIB_DIR.FullName)\*" "lib/hamlib" -Force 52 | # Remove the now empty versioned directory 53 | Remove-Item "$($HAMLIB_DIR.FullName)" -Recurse -Force 54 | shell: pwsh 55 | 56 | 57 | - name: Build binaries 58 | working-directory: freedata_server 59 | run: | 60 | python3 -m nuitka ` 61 | --remove-output ` 62 | --assume-yes-for-downloads ` 63 | --follow-imports ` 64 | --include-data-dir=lib=lib ` 65 | --include-data-dir=../freedata_gui/dist=gui ` 66 | --include-data-files=lib/codec2/*=lib/codec2/ ` 67 | --include-data-files=lib/hamlib/bin/*.exe=lib/hamlib/bin/ ` 68 | --include-data-files=lib/hamlib/bin/*.dll=lib/hamlib/bin/ ` 69 | --include-data-files=config.ini.example=config.ini ` 70 | --standalone server.py ` 71 | --output-filename=freedata-server 72 | shell: pwsh 73 | 74 | 75 | - name: LIST ALL FILES 76 | run: ls -R 77 | 78 | - name: Create installer 79 | uses: joncloud/makensis-action@v4.1 80 | with: 81 | script-file: "freedata-nsis-config.nsi" 82 | arguments: '/V3' 83 | 84 | - name: LIST ALL FILES 85 | working-directory: freedata_server/server.dist 86 | run: ls -R 87 | 88 | - name: Upload artifact 89 | uses: actions/upload-artifact@v4 90 | with: 91 | name: 'FreeDATA-Installer' 92 | path: ./FreeDATA-Installer.exe 93 | 94 | - name: Upload Installer to Release 95 | uses: softprops/action-gh-release@v2 96 | if: startsWith(github.ref, 'refs/tags/v') 97 | with: 98 | draft: false 99 | files: ./FreeDATA-Installer.exe 100 | tag_name: ${{ github.ref_name }} 101 | -------------------------------------------------------------------------------- /.github/workflows/codeql-analysis.yml: -------------------------------------------------------------------------------- 1 | # For most projects, this workflow file will not need changing; you simply need 2 | # to commit it to your repository. 3 | # 4 | # You may wish to alter this file to override the set of languages analyzed, 5 | # or to provide custom queries or build logic. 6 | # 7 | # ******** NOTE ******** 8 | # We have attempted to detect the languages in your repository. Please check 9 | # the `language` matrix defined below to confirm you have the correct set of 10 | # supported CodeQL languages. 11 | # 12 | name: "CodeQL" 13 | 14 | on: 15 | push: 16 | branches: [ main ] 17 | pull_request: 18 | # The branches below must be a subset of the branches above 19 | branches: [ main ] 20 | schedule: 21 | - cron: '43 0 * * 2' 22 | 23 | jobs: 24 | analyze: 25 | name: Analyze 26 | runs-on: ubuntu-latest 27 | permissions: 28 | actions: read 29 | contents: read 30 | security-events: write 31 | 32 | strategy: 33 | fail-fast: false 34 | matrix: 35 | language: [ 'javascript', 'python' ] 36 | # CodeQL supports [ 'cpp', 'csharp', 'go', 'java', 'javascript', 'python' ] 37 | # Learn more: 38 | # https://docs.github.com/en/free-pro-team@latest/github/finding-security-vulnerabilities-and-errors-in-your-code/configuring-code-scanning#changing-the-languages-that-are-analyzed 39 | 40 | steps: 41 | - name: Checkout repository 42 | uses: actions/checkout@v4 43 | with: 44 | ref: ${{ github.head_ref }} 45 | 46 | # Initializes the CodeQL tools for scanning. 47 | - name: Initialize CodeQL 48 | uses: github/codeql-action/init@v3 49 | with: 50 | languages: ${{ matrix.language }} 51 | # If you wish to specify custom queries, you can do so here or in a config file. 52 | # By default, queries listed here will override any specified in a config file. 53 | # Prefix the list here with "+" to use these queries and those in the config file. 54 | # queries: ./path/to/local/query, your-org/your-repo/queries@main 55 | 56 | # Autobuild attempts to build any compiled languages (C/C++, C#, or Java). 57 | # If this step fails, then you should remove it and run the build manually (see below) 58 | - name: Autobuild 59 | uses: github/codeql-action/autobuild@v3 60 | 61 | # ℹ️ Command-line programs to run using the OS shell. 62 | # 📚 https://git.io/JvXDl 63 | 64 | # ✏️ If the Autobuild fails above, remove it and uncomment the following three lines 65 | # and modify them (or add more) to build your code if your project 66 | # uses a compiled language 67 | 68 | #- run: | 69 | # make bootstrap 70 | # make release 71 | 72 | - name: Perform CodeQL Analysis 73 | uses: github/codeql-action/analyze@v3 74 | -------------------------------------------------------------------------------- /.github/workflows/gui_tests.yml: -------------------------------------------------------------------------------- 1 | name: GUI tests 2 | 3 | on: [push] 4 | 5 | jobs: 6 | build: 7 | # The CMake configure and build commands are platform-agnostic and should work equally 8 | # well on Windows or Mac. You can convert this to a matrix build if you need 9 | # cross-platform coverage. 10 | # See: https://docs.github.com/en/free-pro-team@latest/actions/learn-github-actions/managing-complex-workflows#using-a-build-matrix 11 | runs-on: ubuntu-latest 12 | strategy: 13 | # By default, GitHub will maximize the number of jobs run in parallel 14 | # depending on the available runners on GitHub-hosted virtual machines. 15 | # max-parallel: 8 16 | fail-fast: false 17 | matrix: 18 | include: 19 | #- node-version: "16" # EOL 20 | #- node-version: "18" # EOL 21 | - node-version: "20" 22 | - node-version: "22" 23 | 24 | steps: 25 | - uses: actions/checkout@v4 26 | - name: Install Node.js, NPM and Yarn 27 | uses: actions/setup-node@v4 28 | with: 29 | node-version: ${{ matrix.node-version }} 30 | 31 | - name: Install dependencies 32 | working-directory: freedata_gui 33 | run: | 34 | npm i 35 | 36 | - name: GUI Linting 37 | working-directory: freedata_gui 38 | run: | 39 | npm run lint 40 | 41 | - name: GUI Build 42 | working-directory: freedata_gui 43 | run: | 44 | npm run build -------------------------------------------------------------------------------- /.github/workflows/modem_tests.yml: -------------------------------------------------------------------------------- 1 | name: Modem tests 2 | 3 | on: [push] 4 | 5 | jobs: 6 | build: 7 | # The CMake configure and build commands are platform-agnostic and should work equally 8 | # well on Windows or Mac. You can convert this to a matrix build if you need 9 | # cross-platform coverage. 10 | # See: https://docs.github.com/en/free-pro-team@latest/actions/learn-github-actions/managing-complex-workflows#using-a-build-matrix 11 | runs-on: ubuntu-latest 12 | strategy: 13 | # By default, GitHub will maximize the number of jobs run in parallel 14 | # depending on the available runners on GitHub-hosted virtual machines. 15 | # max-parallel: 8 16 | fail-fast: false 17 | matrix: 18 | include: 19 | #- python-version: "3.7" EOL 20 | - python-version: "3.8" 21 | - python-version: "3.9" 22 | - python-version: "3.10" 23 | - python-version: "3.11" 24 | - python-version: "3.12" 25 | - python-version: "3.13" 26 | 27 | steps: 28 | - uses: actions/checkout@v4 29 | 30 | - name: Set up Python ${{ matrix.python-version }} 31 | uses: actions/setup-python@v5 32 | with: 33 | python-version: ${{ matrix.python-version }} 34 | 35 | 36 | - name: Install system packages 37 | shell: bash 38 | run: | 39 | sudo apt-get update || true 40 | sudo apt-get install octave octave-common octave-signal sox portaudio19-dev 41 | 42 | - name: Install python packages 43 | shell: bash 44 | run: | 45 | pip3 install -r requirements.txt 46 | 47 | - name: run config tests 48 | shell: bash 49 | run: | 50 | python -m unittest discover tests -------------------------------------------------------------------------------- /.github/workflows/pip_package.yml: -------------------------------------------------------------------------------- 1 | name: Deploy Python Package 2 | on: [push] 3 | 4 | jobs: 5 | deploy: 6 | runs-on: ubuntu-latest 7 | steps: 8 | - uses: actions/checkout@v4 9 | 10 | - name: Set up Python 3.13 11 | uses: actions/setup-python@v5 12 | with: 13 | python-version: "3.13" 14 | 15 | - name: Install Node.js, NPM and Yarn 16 | uses: actions/setup-node@v4 17 | with: 18 | node-version: 22 19 | 20 | - name: Install Linux dependencies 21 | run: | 22 | sudo apt install -y portaudio19-dev libhamlib-dev libhamlib-utils build-essential cmake patchelf 23 | 24 | - name: Install Python dependencies 25 | run: | 26 | python -m pip install --upgrade pip 27 | pip install -r requirements.txt 28 | pip install wheel 29 | 30 | - name: Build GUI 31 | working-directory: freedata_gui 32 | run: | 33 | npm i 34 | npm run build 35 | 36 | - name: Build package 37 | run: | 38 | python setup.py sdist bdist_wheel 39 | 40 | - name: Publish to PyPI 41 | uses: pypa/gh-action-pypi-publish@v1.12.4 42 | if: startsWith(github.ref, 'refs/tags/v') 43 | with: 44 | user: __token__ 45 | password: ${{ secrets.PYPI_API_TOKEN }} 46 | -------------------------------------------------------------------------------- /.github/workflows/prettier.yaml: -------------------------------------------------------------------------------- 1 | name: Prettier 2 | 3 | # This action works with pull requests and pushes 4 | on: 5 | push: 6 | branches: 7 | - main 8 | 9 | jobs: 10 | prettier: 11 | runs-on: ubuntu-latest 12 | 13 | steps: 14 | - name: Checkout 15 | uses: actions/checkout@v4 16 | with: 17 | # Make sure the actual branch is checked out when running on pull requests 18 | ref: ${{ github.head_ref }} 19 | 20 | - name: Prettify code 21 | uses: creyD/prettier_action@v4.3 22 | with: 23 | # This part is also where you can pass other options, for example: 24 | prettier_options: --write **/*.{js,md,css,html} 25 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # possible installation of codec2 within freedata_server 2 | freedata_server/codec2 3 | 4 | # temporary test artifacts 5 | **/build 6 | **/Testing 7 | 8 | package-lock.json 9 | .DS_Store 10 | 11 | # Other various files and virtual environments 12 | .coverage 13 | .coveragerc 14 | .env 15 | .envrc 16 | .idea 17 | .pytest_cache 18 | .venv 19 | .vscode 20 | *.iml 21 | *.pyc 22 | *.raw 23 | coverage.sh 24 | coverage.xml 25 | 26 | #Ignore GUI config 27 | /freedata_gui/config/config.json 28 | 29 | #GUI_WEB 30 | /freedata_gui/dist 31 | /freedata_gui/node_modules/ 32 | /freedata_gui/node_modules!/venv/ 33 | 34 | # venv packages 35 | venv_3.11/ 36 | venv/ 37 | test_venv/ 38 | freedata_server/lib/codec2/codec2/ 39 | -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | # Ignore artifacts: 2 | build 3 | coverage 4 | assets 5 | freedata_gui/src/waterfall -------------------------------------------------------------------------------- /.stignore: -------------------------------------------------------------------------------- 1 | gui/node_modules 2 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | ################################################################################ 2 | # Build frontend 3 | ################################################################################ 4 | FROM node:20-alpine AS frontend 5 | 6 | WORKDIR /src 7 | 8 | COPY freedata_gui ./ 9 | 10 | RUN npm install && npm run build 11 | 12 | ################################################################################ 13 | # Build server 14 | ################################################################################ 15 | FROM python:3.11-slim-bookworm AS server 16 | 17 | ARG HAMLIB_VERSION=4.5.5 18 | ENV HAMLIB_VERSION=${HAMLIB_VERSION} 19 | 20 | RUN apt-get update && \ 21 | apt-get install --upgrade -y fonts-noto-color-emoji git build-essential cmake portaudio19-dev python3-pyaudio python3-colorama wget && \ 22 | mkdir -p /app/FreeDATA 23 | 24 | WORKDIR /src 25 | 26 | ADD https://github.com/Hamlib/Hamlib/releases/download/${HAMLIB_VERSION}/hamlib-${HAMLIB_VERSION}.tar.gz ./hamlib.tar.gz 27 | 28 | RUN tar -xplf hamlib.tar.gz 29 | 30 | WORKDIR /src/hamlib-${HAMLIB_VERSION} 31 | 32 | RUN ./configure --prefix=/app/FreeDATA-hamlib && \ 33 | make && \ 34 | make install 35 | 36 | WORKDIR /app/FreeDATA 37 | 38 | ADD https://github.com/DJ2LS/FreeDATA.git#v0.16.10-alpha ./ 39 | 40 | RUN python3 -m venv /app/FreeDATA/venv 41 | ENV PATH="/app/FreeDATA/venv/bin:$PATH" 42 | 43 | RUN pip install --no-cache-dir --upgrade pip wheel && \ 44 | pip install --no-cache-dir -r requirements.txt 45 | 46 | WORKDIR /app/FreeDATA/freedata_server/lib 47 | 48 | ADD https://github.com/drowe67/codec2.git ./codec2 49 | 50 | WORKDIR /app/FreeDATA/freedata_server/lib/codec2 51 | 52 | RUN mkdir build_linux 53 | 54 | WORKDIR /app/FreeDATA/freedata_server/lib/codec2/build_linux 55 | 56 | RUN cmake .. && make codec2 -j4 57 | 58 | ################################################################################ 59 | # Final image 60 | ################################################################################ 61 | FROM python:3.11-slim-bookworm 62 | 63 | ENV PATH="/app/FreeDATA-hamlib/bin:/app/FreeDATA/venv/bin:$PATH" 64 | 65 | ENV FREEDATA_CONFIG=/data/config.ini 66 | ENV FREEDATA_DATABASE=/data/freedata-messages.db 67 | ENV HOME=/home/freedata 68 | 69 | WORKDIR /app 70 | 71 | COPY --from=server /app ./ 72 | COPY --from=frontend /src/dist/ ./FreeDATA/freedata_gui/dist/ 73 | COPY entrypoint.sh /entrypoint.sh 74 | 75 | RUN mkdir -p /data && \ 76 | cp FreeDATA/freedata_server/config.ini.example /data/config.ini && \ 77 | apt-get update && \ 78 | apt-get install --upgrade -y \ 79 | portaudio19-dev \ 80 | alsa-utils \ 81 | libasound2 \ 82 | libasound2-plugins \ 83 | pulseaudio \ 84 | pulseaudio-utils && \ 85 | rm -rf /var/lib/apt/lists/* 86 | 87 | RUN useradd --create-home --home-dir $HOME freedata \ 88 | && usermod -aG audio,pulse,pulse-access freedata \ 89 | && chown -R freedata:freedata $HOME 90 | 91 | USER freedata 92 | 93 | ENTRYPOINT [ "/entrypoint.sh" ] 94 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # FreeDATA 2 | 3 | > FreeDATA is a versatile, **open-source platform designed specifically for HF communications**, leveraging **codec2** data modes for robust global digital communication. It features a network-based server-client architecture, a REST API, multi-platform compatibility, and a messaging system. 4 | 5 | > Please keep in mind, this project is still **under development** with many issues which need to be solved. 6 | 7 | [![CodeFactor](https://www.codefactor.io/repository/github/dj2ls/freedata/badge)](https://www.codefactor.io/repository/github/dj2ls/freedata) 8 | [![Modem tests](https://github.com/DJ2LS/FreeDATA/actions/workflows/modem_tests.yml/badge.svg)](https://github.com/DJ2LS/FreeDATA/actions/workflows/modem_tests.yml) 9 | 10 | ![FreeDATA_main_screen.png](documentation%2FFreeDATA_main_screen.png) 11 | 12 | ![FreeDATA_chat_screen.png](documentation%2FFreeDATA_chat_screen.png) 13 | 14 | ## Installation 15 | 16 | Please check the [wiki](https://wiki.freedata.app) for installation instructions 17 | Please check the ['Releases'](https://github.com/DJ2LS/FreeDATA/releases) section for downloading precompiled builds 18 | 19 | ## Credits 20 | 21 | - David Rowe and the FreeDV team for developing the modem and libraries - 22 | FreeDV Codec 2 : https://github.com/drowe67/codec2 23 | -------------------------------------------------------------------------------- /add-osx-cert.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env sh 2 | 3 | KEY_CHAIN=build.keychain 4 | CERTIFICATE_P12=certificate.p12 5 | 6 | # Recreate the certificate from the secure environment variable 7 | echo $CERTIFICATE_OSX_APPLICATION | base64 --decode > $CERTIFICATE_P12 8 | 9 | #create a keychain 10 | security create-keychain -p actions $KEY_CHAIN 11 | 12 | # Make the keychain the default so identities are found 13 | security default-keychain -s $KEY_CHAIN 14 | 15 | # Unlock the keychain 16 | security unlock-keychain -p actions $KEY_CHAIN 17 | 18 | security import $CERTIFICATE_P12 -k $KEY_CHAIN -P $CERTIFICATE_PASSWORD -T /usr/bin/codesign; 19 | 20 | security set-key-partition-list -S apple-tool:,apple: -s -k actions $KEY_CHAIN 21 | 22 | # remove certs 23 | rm -fr *.p12 -------------------------------------------------------------------------------- /documentation/FreeDATA-Frametypes.ods: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DJ2LS/FreeDATA/9fb1c63b4924c5cee3141dbd6928b2f6b66a5e8c/documentation/FreeDATA-Frametypes.ods -------------------------------------------------------------------------------- /documentation/FreeDATA-connect-to-remote-daemon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DJ2LS/FreeDATA/9fb1c63b4924c5cee3141dbd6928b2f6b66a5e8c/documentation/FreeDATA-connect-to-remote-daemon.png -------------------------------------------------------------------------------- /documentation/FreeDATA-no-daemon-connection.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DJ2LS/FreeDATA/9fb1c63b4924c5cee3141dbd6928b2f6b66a5e8c/documentation/FreeDATA-no-daemon-connection.png -------------------------------------------------------------------------------- /documentation/FreeDATA-settings.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DJ2LS/FreeDATA/9fb1c63b4924c5cee3141dbd6928b2f6b66a5e8c/documentation/FreeDATA-settings.png -------------------------------------------------------------------------------- /documentation/FreeDATA-tnc-running.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DJ2LS/FreeDATA/9fb1c63b4924c5cee3141dbd6928b2f6b66a5e8c/documentation/FreeDATA-tnc-running.png -------------------------------------------------------------------------------- /documentation/FreeDATA_GUI_Preview.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DJ2LS/FreeDATA/9fb1c63b4924c5cee3141dbd6928b2f6b66a5e8c/documentation/FreeDATA_GUI_Preview.png -------------------------------------------------------------------------------- /documentation/FreeDATA_TNC_Preview.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DJ2LS/FreeDATA/9fb1c63b4924c5cee3141dbd6928b2f6b66a5e8c/documentation/FreeDATA_TNC_Preview.png -------------------------------------------------------------------------------- /documentation/FreeDATA_chat_screen.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DJ2LS/FreeDATA/9fb1c63b4924c5cee3141dbd6928b2f6b66a5e8c/documentation/FreeDATA_chat_screen.png -------------------------------------------------------------------------------- /documentation/FreeDATA_main_screen.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DJ2LS/FreeDATA/9fb1c63b4924c5cee3141dbd6928b2f6b66a5e8c/documentation/FreeDATA_main_screen.png -------------------------------------------------------------------------------- /documentation/FreeDATA_preview.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DJ2LS/FreeDATA/9fb1c63b4924c5cee3141dbd6928b2f6b66a5e8c/documentation/FreeDATA_preview.gif -------------------------------------------------------------------------------- /documentation/chat_preview_fast.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DJ2LS/FreeDATA/9fb1c63b4924c5cee3141dbd6928b2f6b66a5e8c/documentation/chat_preview_fast.gif -------------------------------------------------------------------------------- /documentation/codec2-FreeDATA-start-connected.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DJ2LS/FreeDATA/9fb1c63b4924c5cee3141dbd6928b2f6b66a5e8c/documentation/codec2-FreeDATA-start-connected.png -------------------------------------------------------------------------------- /documentation/codec2-FreeDATA-start-disconnected.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DJ2LS/FreeDATA/9fb1c63b4924c5cee3141dbd6928b2f6b66a5e8c/documentation/codec2-FreeDATA-start-disconnected.png -------------------------------------------------------------------------------- /documentation/codec2-FreeDATA_05.09.2021_11_29_05.mp4: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DJ2LS/FreeDATA/9fb1c63b4924c5cee3141dbd6928b2f6b66a5e8c/documentation/codec2-FreeDATA_05.09.2021_11_29_05.mp4 -------------------------------------------------------------------------------- /documentation/cube.xcf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DJ2LS/FreeDATA/9fb1c63b4924c5cee3141dbd6928b2f6b66a5e8c/documentation/cube.xcf -------------------------------------------------------------------------------- /documentation/data_preview.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DJ2LS/FreeDATA/9fb1c63b4924c5cee3141dbd6928b2f6b66a5e8c/documentation/data_preview.gif -------------------------------------------------------------------------------- /documentation/freedv_jate_tnc_preview.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DJ2LS/FreeDATA/9fb1c63b4924c5cee3141dbd6928b2f6b66a5e8c/documentation/freedv_jate_tnc_preview.png -------------------------------------------------------------------------------- /documentation/icon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DJ2LS/FreeDATA/9fb1c63b4924c5cee3141dbd6928b2f6b66a5e8c/documentation/icon.ico -------------------------------------------------------------------------------- /entrypoint.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | echo "Starting pulseaudio" 4 | pulseaudio --exit-idle-time=-1 --daemon & 5 | 6 | if [ -z "${RIGCTLD_ARGS+x}" ]; then 7 | echo "No RIGCTLD_ARGS set, not starting rigctld" 8 | else 9 | echo "Starting rigctld with args ${RIGCTLD_ARGS}" 10 | rigctld ${RIGCTLD_ARGS} & 11 | fi 12 | 13 | echo "Starting FreeDATA server" 14 | python3 /app/FreeDATA/freedata_server/server.py 15 | -------------------------------------------------------------------------------- /freedata-nsis-config.nsi: -------------------------------------------------------------------------------- 1 | !include "MUI2.nsh" 2 | 3 | ; Request administrative rights 4 | RequestExecutionLevel admin 5 | 6 | ; The name and file name of the installer 7 | Name "FreeDATA Installer" 8 | OutFile "FreeDATA-Installer.exe" 9 | 10 | ; Default installation directory for the server 11 | InstallDir "$LOCALAPPDATA\FreeDATA" 12 | 13 | ; Registry key to store the installation directory 14 | InstallDirRegKey HKCU "Software\FreeDATA" "Install_Dir" 15 | 16 | ; Modern UI settings 17 | !define MUI_ABORTWARNING 18 | 19 | ; Installer interface settings 20 | !define MUI_ICON "documentation\icon.ico" 21 | !define MUI_UNICON "documentation\icon.ico" ; Icon for the uninstaller 22 | 23 | ; Define the welcome page text 24 | !define MUI_WELCOMEPAGE_TEXT "Welcome to the FreeDATA Setup Wizard. This wizard will guide you through the installation process." 25 | !define MUI_FINISHPAGE_TEXT "Folder: $INSTDIR" 26 | !define MUI_DIRECTORYPAGE_TEXT_TOP "Please select the installation folder. It's recommended to use the suggested one to avoid permission problems." 27 | 28 | ; Pages 29 | !insertmacro MUI_PAGE_WELCOME 30 | !insertmacro MUI_PAGE_LICENSE "LICENSE" 31 | !insertmacro MUI_PAGE_COMPONENTS 32 | !insertmacro MUI_PAGE_DIRECTORY 33 | !insertmacro MUI_PAGE_INSTFILES 34 | !insertmacro MUI_PAGE_FINISH 35 | 36 | ; Uninstaller 37 | !insertmacro MUI_UNPAGE_WELCOME 38 | !insertmacro MUI_UNPAGE_CONFIRM 39 | !insertmacro MUI_UNPAGE_INSTFILES 40 | !insertmacro MUI_UNPAGE_FINISH 41 | 42 | ; Language (you can choose and configure the language(s) you want) 43 | !insertmacro MUI_LANGUAGE "English" 44 | 45 | 46 | ; Installer Sections 47 | Section "FreeData Server" SEC01 48 | ; Set output path to the installation directory 49 | SetOutPath $INSTDIR\freedata-server 50 | 51 | ; Check if "config.ini" exists and back it up 52 | IfFileExists $INSTDIR\freedata-server\config.ini backupConfig 53 | 54 | doneBackup: 55 | ; Add your application files here 56 | File /r "freedata_server\server.dist\*" 57 | 58 | ; Restore the original "config.ini" if it was backed up 59 | IfFileExists $INSTDIR\freedata-server\config.ini.bak restoreConfig 60 | 61 | ; Create a shortcut in the user's desktop 62 | CreateShortCut "$DESKTOP\FreeDATA Server.lnk" "$INSTDIR\freedata-server\freedata-server.exe" 63 | 64 | ; Create Uninstaller 65 | WriteUninstaller "$INSTDIR\Uninstall.exe" 66 | 67 | ; Create a Start Menu directory 68 | CreateDirectory "$SMPROGRAMS\FreeDATA" 69 | 70 | ; Create shortcut in the Start Menu directory 71 | CreateShortCut "$SMPROGRAMS\FreeDATA\FreeDATA Server.lnk" "$INSTDIR\freedata-server\freedata-server.exe" 72 | 73 | ; Create an Uninstall shortcut 74 | CreateShortCut "$SMPROGRAMS\FreeDATA\Uninstall FreeDATA.lnk" "$INSTDIR\Uninstall.exe" 75 | 76 | 77 | ; Backup "config.ini" before overwriting files 78 | backupConfig: 79 | Rename $INSTDIR\freedata-server\config.ini $INSTDIR\freedata-server\config.ini.bak 80 | Goto doneBackup 81 | 82 | ; Restore the original "config.ini" 83 | restoreConfig: 84 | Delete $INSTDIR\freedata-server\config.ini 85 | Rename $INSTDIR\freedata-server\config.ini.bak $INSTDIR\freedata-server\config.ini 86 | 87 | SectionEnd 88 | 89 | ; Uninstaller Section 90 | Section "Uninstall" 91 | ; Delete files and directories for the server 92 | Delete $INSTDIR\freedata-server\*.* 93 | RMDir /r $INSTDIR\freedata-server 94 | 95 | ; Remove the desktop shortcuts 96 | Delete "$DESKTOP\FreeDATA Server.lnk" 97 | 98 | ; Remove Start Menu shortcuts 99 | Delete "$SMPROGRAMS\FreeDATA\*.*" 100 | RMDir "$SMPROGRAMS\FreeDATA" 101 | 102 | ; Attempt to delete the uninstaller itself 103 | Delete $EXEPATH 104 | 105 | ; Now remove the installation directory if it's empty 106 | RMDir /r $INSTDIR 107 | SectionEnd 108 | -------------------------------------------------------------------------------- /freedata_gui/README.md: -------------------------------------------------------------------------------- 1 | # freedata_gui 2 | 3 | ## Project setup 4 | 5 | ``` 6 | npm install 7 | ``` 8 | 9 | ### Compiles and hot-reloads for development 10 | 11 | ``` 12 | npm run serve 13 | ``` 14 | 15 | ### Compiles and minifies for production 16 | 17 | ``` 18 | npm run build 19 | ``` 20 | 21 | ### Lints and fixes files 22 | 23 | ``` 24 | npm run lint 25 | ``` 26 | 27 | ### Customize configuration 28 | 29 | See [Configuration Reference](https://cli.vuejs.org/config/). 30 | -------------------------------------------------------------------------------- /freedata_gui/babel.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | presets: ["@vue/cli-plugin-babel/preset"], 3 | }; 4 | -------------------------------------------------------------------------------- /freedata_gui/eslint.config.js: -------------------------------------------------------------------------------- 1 | import pluginVue from "eslint-plugin-vue"; 2 | import globals from "globals"; 3 | 4 | export default [ 5 | ...pluginVue.configs["flat/base"], 6 | //...pluginVue.configs['flat/recommended'], // causes some errors not able to fix, yet. So disabled for now 7 | { 8 | ignores: [ 9 | "**/*.config.js", 10 | "!**/eslint.config.js", 11 | "**/src/locales/**", 12 | "**/node_modules/**", 13 | "**/dist/**", 14 | ], 15 | rules: { 16 | "vue/no-unused-vars": "error", 17 | "vue/multi-word-component-names": "warn", 18 | }, 19 | languageOptions: { 20 | //sourceType: 'module', 21 | globals: { 22 | ...globals.browser, 23 | }, 24 | }, 25 | }, 26 | ]; 27 | -------------------------------------------------------------------------------- /freedata_gui/jsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es5", 4 | "module": "esnext", 5 | "baseUrl": "./", 6 | "moduleResolution": "node", 7 | "paths": { 8 | "@/*": [ 9 | "src/*" 10 | ] 11 | }, 12 | "lib": [ 13 | "esnext", 14 | "dom", 15 | "dom.iterable", 16 | "scripthost" 17 | ] 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /freedata_gui/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "FreeDATA", 3 | "version": "0.17.3-beta", 4 | "description": "FreeDATA Client application for connecting to FreeDATA server", 5 | "private": true, 6 | "scripts": { 7 | "serve": "vue-cli-service serve", 8 | "build": "npm i && vue-cli-service build", 9 | "lint": "npx eslint ." 10 | }, 11 | "repository": { 12 | "type": "git", 13 | "url": "https://github.com/DJ2LS/FreeDATA.git" 14 | }, 15 | "keywords": [ 16 | "Modem", 17 | "GUI", 18 | "FreeDATA", 19 | "codec2" 20 | ], 21 | "author": "DJ2LS", 22 | "license": "GPL-3.0", 23 | "bugs": { 24 | "url": "https://github.com/DJ2LS/FreeDATA/issues" 25 | }, 26 | "homepage": "https://freedata.app", 27 | "dependencies": { 28 | "@popperjs/core": "^2.11.8", 29 | "bootstrap": "^5.3.5", 30 | "bootstrap-icons": "^1.11.3", 31 | "bootstrap-vue-next": "^0.29.0", 32 | "chart.js": "^4.4.3", 33 | "chartjs-plugin-annotation": "^3.0.1", 34 | "core-js": "^3.8.3", 35 | "d3": "^7.9.0", 36 | "dompurify": "^3.1.6", 37 | "gettext-parser": "^8.0.0", 38 | "gridstack": "^12.1.1", 39 | "i18next": "^25.0.2", 40 | "i18next-vue": "^5.2.0", 41 | "js-image-compressor": "^2.0.0", 42 | "marked": "^15.0.3", 43 | "pinia": "^3.0.1", 44 | "qth-locator": "^2.1.0", 45 | "topojson-client": "^3.1.0", 46 | "uuid": "^11.0.2", 47 | "vue": "^3.2.13", 48 | "vue-chartjs": "^5.3.1", 49 | "vuemoji-picker": "^0.3.1" 50 | }, 51 | "devDependencies": { 52 | "@babel/core": "^7.25.2", 53 | "@babel/eslint-parser": "^7.25.1", 54 | "@eslint/js": "^9.10.0", 55 | "@vue/cli-service": "~5.0.8", 56 | "eslint": "^9.0.0", 57 | "eslint-plugin-vue": "^9.0.0", 58 | "globals": "^16.0.0" 59 | }, 60 | "browserslist": [ 61 | "> 1%", 62 | "last 2 versions", 63 | "not dead", 64 | "not ie 11" 65 | ] 66 | } 67 | -------------------------------------------------------------------------------- /freedata_gui/public/android-chrome-192x192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DJ2LS/FreeDATA/9fb1c63b4924c5cee3141dbd6928b2f6b66a5e8c/freedata_gui/public/android-chrome-192x192.png -------------------------------------------------------------------------------- /freedata_gui/public/android-chrome-512x512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DJ2LS/FreeDATA/9fb1c63b4924c5cee3141dbd6928b2f6b66a5e8c/freedata_gui/public/android-chrome-512x512.png -------------------------------------------------------------------------------- /freedata_gui/public/apple-touch-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DJ2LS/FreeDATA/9fb1c63b4924c5cee3141dbd6928b2f6b66a5e8c/freedata_gui/public/apple-touch-icon.png -------------------------------------------------------------------------------- /freedata_gui/public/favicon-16x16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DJ2LS/FreeDATA/9fb1c63b4924c5cee3141dbd6928b2f6b66a5e8c/freedata_gui/public/favicon-16x16.png -------------------------------------------------------------------------------- /freedata_gui/public/favicon-32x32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DJ2LS/FreeDATA/9fb1c63b4924c5cee3141dbd6928b2f6b66a5e8c/freedata_gui/public/favicon-32x32.png -------------------------------------------------------------------------------- /freedata_gui/public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DJ2LS/FreeDATA/9fb1c63b4924c5cee3141dbd6928b2f6b66a5e8c/freedata_gui/public/favicon.ico -------------------------------------------------------------------------------- /freedata_gui/public/icon_cube_border.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DJ2LS/FreeDATA/9fb1c63b4924c5cee3141dbd6928b2f6b66a5e8c/freedata_gui/public/icon_cube_border.png -------------------------------------------------------------------------------- /freedata_gui/public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | <%= htmlWebpackPlugin.options.title %> 9 | 10 | 11 | 17 | 23 | 29 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 48 | 49 | 50 | 62 | 63 | 64 | 65 | 72 |
73 | 74 | 75 | 76 | -------------------------------------------------------------------------------- /freedata_gui/public/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "FreeDATA", 3 | "short_name": "FreeDATA", 4 | "description": "FreeDATA is a communication platform for transferring data over radio.", 5 | "categories": ["utilities", "communication", "productivity"], 6 | "start_url": ".", 7 | "display": "standalone", 8 | "background_color": "#ffffff", 9 | "theme_color": "#ffffff", 10 | "icons": [ 11 | { 12 | "src": "android-chrome-192x192.png", 13 | "sizes": "192x192", 14 | "type": "image/png" 15 | }, 16 | { 17 | "src": "android-chrome-512x512.png", 18 | "sizes": "512x512", 19 | "type": "image/png" 20 | } 21 | ] 22 | } 23 | -------------------------------------------------------------------------------- /freedata_gui/src/App.vue: -------------------------------------------------------------------------------- 1 | 13 | 14 | 51 | -------------------------------------------------------------------------------- /freedata_gui/src/assets/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DJ2LS/FreeDATA/9fb1c63b4924c5cee3141dbd6928b2f6b66a5e8c/freedata_gui/src/assets/logo.png -------------------------------------------------------------------------------- /freedata_gui/src/assets/waterfall/LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 Jeppe Ledet-Pedersen 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /freedata_gui/src/assets/waterfall/README.rst: -------------------------------------------------------------------------------- 1 | ******************************** 2 | HTML Canvas/WebSockets Waterfall 3 | ******************************** 4 | 5 | This is a small experiment to create a waterfall plot with HTML Canvas and WebSockets to stream live FFT data from an SDR: 6 | 7 | .. image:: img/waterfall.png 8 | 9 | ``spectrum.js`` contains the main JavaScript source code for the plot, while ``colormap.js`` contains colormaps generated using ``make_colormap.py``. 10 | 11 | ``index.html``, ``style.css``, ``script.js`` contain an example page that receives FFT data on a WebSocket and plots it on the waterfall plot. 12 | 13 | ``server.py`` contains a example `Bottle `_ and `gevent-websocket `_ server that broadcasts FFT data to connected clients. The FFT data is generated using `GNU radio `_ using a USRP but it should be fairly easy to change it to a different SDR. 14 | -------------------------------------------------------------------------------- /freedata_gui/src/assets/waterfall/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Spectrum Plot 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | -------------------------------------------------------------------------------- /freedata_gui/src/assets/waterfall/make_colormap.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | import matplotlib.pyplot as plt 4 | 5 | colormaps = ('viridis', 'inferno', 'magma', 'jet', 'binary', 'plasma', 'turbo','rainbow', 'ocean') 6 | for c in colormaps: 7 | cmap_name = c 8 | cmap = plt.get_cmap(cmap_name) 9 | 10 | colors = [[int(round(255 * x)) for x in cmap(i)[:3]] for i in range(256)] 11 | print(f'var {c} = {colors}') 12 | 13 | print(f'var colormaps = [{", ".join(colormaps)}];') 14 | -------------------------------------------------------------------------------- /freedata_gui/src/assets/waterfall/waterfall.css: -------------------------------------------------------------------------------- 1 | html, 2 | body { 3 | width: 100%; 4 | height: 100%; 5 | margin: 0px; 6 | } 7 | 8 | #waterfall { 9 | display: block; 10 | width: 100%; 11 | height: 100%; 12 | } 13 | -------------------------------------------------------------------------------- /freedata_gui/src/components/chat_messages.vue: -------------------------------------------------------------------------------- 1 | 44 | 45 | 83 | -------------------------------------------------------------------------------- /freedata_gui/src/components/chat_messages_action_menu.vue: -------------------------------------------------------------------------------- 1 | 14 | 15 | 33 | 34 | 54 | -------------------------------------------------------------------------------- /freedata_gui/src/components/chat_messages_image_preview.vue: -------------------------------------------------------------------------------- 1 | 10 | 11 | 28 | 29 | -------------------------------------------------------------------------------- /freedata_gui/src/components/grid/grid_CQ.vue: -------------------------------------------------------------------------------- 1 | 26 | 27 | 42 | -------------------------------------------------------------------------------- /freedata_gui/src/components/grid/grid_active_heard_stations_mini.vue: -------------------------------------------------------------------------------- 1 | 34 | 35 | 81 | -------------------------------------------------------------------------------- /freedata_gui/src/components/grid/grid_activities.vue: -------------------------------------------------------------------------------- 1 | 24 | 25 | 67 | -------------------------------------------------------------------------------- /freedata_gui/src/components/grid/grid_beacon.vue: -------------------------------------------------------------------------------- 1 | 18 | 19 | 40 | -------------------------------------------------------------------------------- /freedata_gui/src/components/grid/grid_button.vue: -------------------------------------------------------------------------------- 1 | 11 | 12 | 20 | -------------------------------------------------------------------------------- /freedata_gui/src/components/grid/grid_dbfs.vue: -------------------------------------------------------------------------------- 1 | 9 | 10 | 69 | -------------------------------------------------------------------------------- /freedata_gui/src/components/grid/grid_frequency.vue: -------------------------------------------------------------------------------- 1 | 9 | 10 | 19 | -------------------------------------------------------------------------------- /freedata_gui/src/components/grid/grid_mycall small.vue: -------------------------------------------------------------------------------- 1 | 8 | 9 | 33 | -------------------------------------------------------------------------------- /freedata_gui/src/components/grid/grid_mycall.vue: -------------------------------------------------------------------------------- 1 | 8 | 9 | 20 | -------------------------------------------------------------------------------- /freedata_gui/src/components/grid/grid_ping.vue: -------------------------------------------------------------------------------- 1 | 23 | 24 | 56 | -------------------------------------------------------------------------------- /freedata_gui/src/components/grid/grid_ptt.vue: -------------------------------------------------------------------------------- 1 | 9 | 10 | 18 | -------------------------------------------------------------------------------- /freedata_gui/src/components/grid/grid_s-meter.vue: -------------------------------------------------------------------------------- 1 | 9 | 10 | 69 | -------------------------------------------------------------------------------- /freedata_gui/src/components/grid/grid_scatter.vue: -------------------------------------------------------------------------------- 1 | 91 | 92 | 114 | -------------------------------------------------------------------------------- /freedata_gui/src/components/grid/grid_stats_chart.vue: -------------------------------------------------------------------------------- 1 | 95 | 96 | 114 | -------------------------------------------------------------------------------- /freedata_gui/src/components/grid/grid_stop.vue: -------------------------------------------------------------------------------- 1 | 13 | 14 | 27 | -------------------------------------------------------------------------------- /freedata_gui/src/components/grid/grid_swr_meter.vue: -------------------------------------------------------------------------------- 1 | 9 | 10 | 61 | -------------------------------------------------------------------------------- /freedata_gui/src/components/grid/grid_tune.vue: -------------------------------------------------------------------------------- 1 | 8 | 9 | 23 | -------------------------------------------------------------------------------- /freedata_gui/src/components/main_loading_screen.vue: -------------------------------------------------------------------------------- 1 | 2 | 8 | 9 | 14 | 15 | 46 | -------------------------------------------------------------------------------- /freedata_gui/src/components/main_screen.vue: -------------------------------------------------------------------------------- 1 | 64 | 65 | 90 | -------------------------------------------------------------------------------- /freedata_gui/src/components/settings_exp.vue: -------------------------------------------------------------------------------- 1 | 21 | 22 | 100 | 101 | -------------------------------------------------------------------------------- /freedata_gui/src/components/settings_flrig.vue: -------------------------------------------------------------------------------- 1 | 54 | 55 | 56 | -------------------------------------------------------------------------------- /freedata_gui/src/components/settings_url.vue: -------------------------------------------------------------------------------- 1 | 10 | 11 | 97 | -------------------------------------------------------------------------------- /freedata_gui/src/components/settings_web.vue: -------------------------------------------------------------------------------- 1 | 9 | 10 | 86 | 87 | 88 | -------------------------------------------------------------------------------- /freedata_gui/src/js/event_sock.js: -------------------------------------------------------------------------------- 1 | import { 2 | eventDispatcher, 3 | stateDispatcher, 4 | connectionFailed, 5 | loadAllData, 6 | } from "../js/eventHandler.js"; 7 | import { addDataToWaterfall } from "../js/waterfallHandler.js"; 8 | 9 | // ----------------- init pinia stores ------------- 10 | import { setActivePinia } from "pinia"; 11 | import pinia from "../store/index"; 12 | setActivePinia(pinia); 13 | 14 | import { useStateStore } from "../store/stateStore.js"; 15 | const state = useStateStore(pinia); 16 | 17 | function connect(endpoint, dispatcher) { 18 | const { protocol, hostname, port } = window.location; 19 | const wsProtocol = protocol === "https:" ? "wss:" : "ws:"; 20 | const adjustedPort = port === "8080" ? "5000" : port; 21 | const socket = new WebSocket( 22 | `${wsProtocol}//${hostname}:${adjustedPort}/${endpoint}`, 23 | ); 24 | 25 | // handle opening 26 | socket.addEventListener("open", function () { 27 | console.log(`Connected to the WebSocket server: ${endpoint}`); 28 | // when connected again, initially load all data from server 29 | loadAllData(); 30 | state.modem_connection = "connected"; 31 | }); 32 | 33 | // handle data 34 | socket.addEventListener("message", function (event) { 35 | dispatcher(event.data); 36 | }); 37 | 38 | // handle errors 39 | socket.addEventListener("error", function (event) { 40 | connectionFailed(endpoint, event); 41 | }); 42 | 43 | // handle closing and reconnect 44 | socket.addEventListener("close", function (event) { 45 | console.log(`WebSocket connection closed: ${event.code}`); 46 | 47 | // Reconnect handler 48 | setTimeout(() => { 49 | connect(endpoint, dispatcher); 50 | }, 1000); 51 | }); 52 | } 53 | 54 | // Initial connection attempts to endpoints 55 | export function initConnections() { 56 | connect("states", stateDispatcher); 57 | connect("events", eventDispatcher); 58 | connect("fft", addDataToWaterfall); 59 | } 60 | -------------------------------------------------------------------------------- /freedata_gui/src/js/i18n.js: -------------------------------------------------------------------------------- 1 | import i18next from "i18next"; 2 | 3 | // Function to load translation JSON files from the locales folder. 4 | // It expects file names like "en_english.json" or "de_deutsch.json" 5 | function loadLocaleMessages() { 6 | // Automatically load all JSON files in ../locales 7 | const locales = require.context( 8 | "../locales", 9 | true, 10 | /[A-Za-z0-9-_,\s]+\.json$/i, 11 | ); 12 | const resources = {}; 13 | const availableLanguages = []; 14 | 15 | locales.keys().forEach((key) => { 16 | // Use regex to extract the ISO code and language name from the file name. 17 | // For example, "./en_english.json" extracts iso: "en", name: "english" 18 | const matched = key.match(/\.\/([^_]+)_([^.]+)\.json$/i); 19 | if (matched && matched.length > 2) { 20 | const iso = matched[1]; 21 | const name = matched[2]; 22 | // Load the translation JSON file 23 | const translations = locales(key); 24 | // Wrap translations into the default namespace ("translation") 25 | resources[iso] = { translation: translations }; 26 | availableLanguages.push({ iso, name }); 27 | } 28 | }); 29 | 30 | return { resources, availableLanguages }; 31 | } 32 | 33 | const { resources, availableLanguages } = loadLocaleMessages(); 34 | 35 | i18next.init( 36 | { 37 | lng: "en", 38 | fallbackLng: "en", 39 | resources, 40 | }, 41 | (err) => { 42 | if (err) { 43 | console.error("i18next initialization error:", err); 44 | } else { 45 | console.log("i18next is ready."); 46 | } 47 | }, 48 | ); 49 | 50 | export default i18next; 51 | export { availableLanguages }; 52 | -------------------------------------------------------------------------------- /freedata_gui/src/js/mobile_devices.js: -------------------------------------------------------------------------------- 1 | import { ref, computed, onMounted, onUnmounted } from "vue"; 2 | 3 | export function useIsMobile(breakpoint = 720) { 4 | const windowWidth = ref(window.innerWidth); 5 | 6 | const updateWidth = () => { 7 | windowWidth.value = window.innerWidth; 8 | }; 9 | 10 | onMounted(() => window.addEventListener("resize", updateWidth)); 11 | onUnmounted(() => window.removeEventListener("resize", updateWidth)); 12 | 13 | const isMobile = computed(() => windowWidth.value < breakpoint); 14 | return { isMobile, windowWidth }; 15 | } 16 | -------------------------------------------------------------------------------- /freedata_gui/src/js/popupHandler.js: -------------------------------------------------------------------------------- 1 | import { v4 as uuidv4 } from "uuid"; 2 | import * as bootstrap from "bootstrap"; 3 | 4 | export function displayToast(type, icon, content, duration) { 5 | const mainToastContainer = document.getElementById("mainToastContainer"); 6 | 7 | const randomID = uuidv4(); 8 | const toastCode = ` 9 | 24 | `; 25 | 26 | // Insert toast to toast container 27 | mainToastContainer.insertAdjacentHTML("beforeend", toastCode); 28 | 29 | // Register toast 30 | const toastHTMLElement = document.getElementById(randomID); 31 | const toast = bootstrap.Toast.getOrCreateInstance(toastHTMLElement); // Returns a Bootstrap toast instance 32 | toast._config.delay = duration; 33 | 34 | // Show toast 35 | toast.show(); 36 | 37 | // Register event listener to remove toast when hidden 38 | toastHTMLElement.addEventListener( 39 | "hidden.bs.toast", 40 | function handleToastHidden() { 41 | toastHTMLElement.removeEventListener( 42 | "hidden.bs.toast", 43 | handleToastHidden, 44 | ); 45 | toastHTMLElement.remove(); 46 | }, 47 | ); 48 | } 49 | -------------------------------------------------------------------------------- /freedata_gui/src/js/radioHandler.js: -------------------------------------------------------------------------------- 1 | // pinia store setup 2 | import { setActivePinia } from "pinia"; 3 | import pinia from "../store/index"; 4 | setActivePinia(pinia); 5 | 6 | import { useStateStore } from "../store/stateStore"; 7 | const stateStore = useStateStore(pinia); 8 | 9 | import { getRadioStatus } from "./api"; 10 | 11 | export async function processRadioStatus() { 12 | try { 13 | let result = await getRadioStatus(); 14 | 15 | if (!result || typeof result !== "object") { 16 | throw new Error("Invalid radio status"); 17 | } 18 | 19 | stateStore.mode = result.radio_mode; 20 | stateStore.frequency = result.radio_frequency; 21 | stateStore.rf_level = Math.round(result.radio_rf_level / 5) * 5; // round to 5er steps 22 | stateStore.tuner = result.radio_tuner; 23 | } catch (error) { 24 | console.error("Error fetching radio status:", error); 25 | // Handle the error appropriately 26 | // For example, you can set default values or update the UI to indicate an error 27 | stateStore.mode = "unknown"; 28 | stateStore.frequency = 0; 29 | stateStore.rf_level = 0; 30 | stateStore.tuner = "unknown"; 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /freedata_gui/src/js/stationHandler.js: -------------------------------------------------------------------------------- 1 | import { useStationStore } from "../store/stationStore.js"; 2 | const station = useStationStore(); 3 | 4 | import { getStationInfo, setStationInfo } from "../js/api"; 5 | 6 | export async function getStationInfoByCallsign(callsign) { 7 | try { 8 | const result = await getStationInfo(callsign); 9 | 10 | // Check if info is null and assign default values if it is 11 | if ( 12 | result == null || 13 | typeof result === "undefined" || 14 | result.info == null 15 | ) { 16 | station.stationInfo.callsign = "N/A"; 17 | station.stationInfo.location.gridsquare = "N/A"; 18 | station.stationInfo.info = { 19 | name: "", 20 | city: "", 21 | age: "", 22 | radio: "", 23 | antenna: "", 24 | email: "", 25 | website: "", 26 | socialMedia: { 27 | facebook: "", 28 | "twitter-x": "", 29 | mastodon: "", 30 | instagram: "", 31 | linkedin: "", 32 | youtube: "", 33 | tiktok: "", 34 | }, 35 | comments: "", 36 | }; 37 | } else { 38 | station.stationInfo.callsign = result.callsign || "N/A"; 39 | station.stationInfo.location.gridsquare = 40 | result.location?.gridsquare || "N/A"; 41 | 42 | station.stationInfo.info = { 43 | name: result.info.name || "", 44 | city: result.info.city || "", 45 | age: result.info.age || "", 46 | radio: result.info.radio || "", 47 | antenna: result.info.antenna || "", 48 | email: result.info.email || "", 49 | website: result.info.website || "", 50 | socialMedia: { 51 | facebook: result.info.socialMedia.facebook || "", 52 | "twitter-x": result.info.socialMedia["twitter-x"] || "", 53 | mastodon: result.info.socialMedia.mastodon || "", 54 | instagram: result.info.socialMedia.instagram || "", 55 | linkedin: result.info.socialMedia.linkedin || "", 56 | youtube: result.info.socialMedia.youtube || "", 57 | tiktok: result.info.socialMedia.tiktok || "", 58 | }, 59 | comments: result.info.comments || "", 60 | }; 61 | } 62 | } catch (error) { 63 | console.error("Error fetching station info:", error); 64 | } 65 | } 66 | 67 | export async function setStationInfoByCallsign(callsign) { 68 | console.log(station.stationInfo); 69 | setStationInfo(callsign, station.stationInfo); 70 | } 71 | -------------------------------------------------------------------------------- /freedata_gui/src/js/waterfallHandler.js: -------------------------------------------------------------------------------- 1 | import { Spectrum } from "../assets/waterfall/spectrum.js"; 2 | 3 | import { setActivePinia } from "pinia"; 4 | import pinia from "../store/index"; 5 | setActivePinia(pinia); 6 | 7 | import { settingsStore as settings } from "../store/settingsStore.js"; 8 | 9 | var spectrum = new Object(); 10 | var spectrums = []; 11 | export function initWaterfall(id) { 12 | spectrum = new Spectrum(id, { 13 | spectrumPercent: 0, 14 | wf_rows: 1024, //Assuming 1 row = 1 pixe1, 192 is the height of the spectrum container 15 | wf_size: 1024, 16 | }); 17 | spectrum.setColorMap(settings.local.wf_theme); 18 | spectrums.push(spectrum); 19 | return spectrum; 20 | } 21 | 22 | export function addDataToWaterfall(data) { 23 | data = JSON.parse(data); 24 | if (data.constructor !== Array) return; 25 | spectrums.forEach((element) => { 26 | //console.log(element); 27 | element.addData(data); 28 | }); 29 | //window.dispatchEvent(new CustomEvent("wf-data-avail", {bubbles:true, detail: data })); 30 | } 31 | /** 32 | * Setwaterfall colormap array by index 33 | * @param {number} index colormap index to use 34 | */ 35 | export function setColormap() { 36 | let index = settings.local.wf_theme; 37 | if (isNaN(index)) index = 0; 38 | console.log("Setting waterfall colormap to " + index); 39 | spectrums.forEach((element) => { 40 | //console.log(element); 41 | element.setColorMap(index); 42 | }); 43 | } 44 | -------------------------------------------------------------------------------- /freedata_gui/src/main.js: -------------------------------------------------------------------------------- 1 | import { createApp } from "vue"; 2 | import { createPinia } from "pinia"; 3 | import App from "./App.vue"; 4 | import i18next from "./js/i18n"; 5 | import I18NextVue from "i18next-vue"; 6 | 7 | import { Chart, Filler } from "chart.js"; 8 | import { getRemote, settingsStore as settings } from "./store/settingsStore"; 9 | import { initConnections } from "./js/event_sock.js"; 10 | import { getModemState } from "./js/api"; 11 | import { applyColorMode } from "./js/freedata.js"; 12 | 13 | // Register the Filler plugin globally 14 | Chart.register(Filler); 15 | 16 | // Create the Vue app 17 | const app = createApp(App); 18 | 19 | app.use(I18NextVue, { i18next }); 20 | 21 | // Create and use the Pinia store 22 | const pinia = createPinia(); 23 | app.use(pinia); 24 | 25 | // Mount the app 26 | app.mount("#app"); 27 | 28 | // Initialize settings and connections 29 | getRemote().then(() => { 30 | initConnections(); 31 | getModemState(); 32 | 33 | // Update the i18next language based on the stored settings 34 | i18next.changeLanguage(settings.local.language); 35 | 36 | //Apply Color Mode to gui 37 | applyColorMode(settings.local.colormode); 38 | }); 39 | -------------------------------------------------------------------------------- /freedata_gui/src/store/audioStore.js: -------------------------------------------------------------------------------- 1 | import { defineStore } from "pinia"; 2 | import { getAudioDevices } from "../js/api"; 3 | import { ref } from "vue"; 4 | 5 | // Define skel fallback data 6 | const skel = [ 7 | { 8 | api: "ERR", 9 | id: "0000", 10 | name: "No devices received from modem", 11 | native_index: 0, 12 | }, 13 | ]; 14 | 15 | export const useAudioStore = defineStore("audioStore", () => { 16 | const audioInputs = ref([]); 17 | const audioOutputs = ref([]); 18 | 19 | const loadAudioDevices = async () => { 20 | try { 21 | const devices = await getAudioDevices(); 22 | // Check if devices are valid and have entries, otherwise use skel 23 | audioInputs.value = devices && devices.in.length > 0 ? devices.in : skel; 24 | audioOutputs.value = 25 | devices && devices.out.length > 0 ? devices.out : skel; 26 | } catch (error) { 27 | console.error("Failed to load audio devices:", error); 28 | // Use skel as fallback in case of error 29 | audioInputs.value = skel; 30 | audioOutputs.value = skel; 31 | } 32 | }; 33 | 34 | return { 35 | audioInputs, 36 | audioOutputs, 37 | loadAudioDevices, 38 | }; 39 | }); 40 | -------------------------------------------------------------------------------- /freedata_gui/src/store/chatStore.js: -------------------------------------------------------------------------------- 1 | import { defineStore } from "pinia"; 2 | import { ref } from "vue"; 3 | 4 | export const useChatStore = defineStore("chatStore", () => { 5 | var callsign_list = ref(); 6 | var sorted_chat_list = ref(); 7 | var newChatCallsign = ref(); 8 | var newChatMessage = ref(); 9 | var totalUnreadMessages = ref(0); 10 | 11 | // Indicator if we are loading data 12 | var loading = ref(false); 13 | 14 | /* ------------------------------------------------ */ 15 | // Scroll to bottom functions 16 | const scrollTrigger = ref(0); 17 | 18 | function triggerScrollToBottom() { 19 | scrollTrigger.value++; 20 | } 21 | 22 | var selectedCallsign = ref(); 23 | var messageInfoById = ref(); // holds a unique message if requested by id 24 | // we need a default value in our ref because of our message info modal 25 | 26 | var inputText = ref(""); 27 | 28 | var sorted_beacon_list = ref({}); 29 | var unsorted_beacon_list = ref({}); 30 | 31 | var chartSpeedPER0 = ref(); 32 | var chartSpeedPER25 = ref(); 33 | var chartSpeedPER75 = ref(); 34 | 35 | // var beaconDataArray = ref([-3, 10, 8, 5, 3, 0, -5, 10, 8, 5, 3, 0, -5, 10, 8, 5, 3, 0, -5, 10, 8, 5, 3, 0, -5]) 36 | // var beaconLabelArray = ref(['18:10', '19:00', '23:00', '01:13', '04:25', '08:15', '09:12', '18:10', '19:00', '23:00', '01:13', '04:25', '08:15', '09:12', '18:10', '19:00', '23:00', '01:13', '04:25', '08:15', '09:12', '01:13', '04:25', '08:15', '09:12']) 37 | var beaconDataArray = ref([]); 38 | var beaconLabelArray = ref([]); 39 | 40 | var arq_speed_list_bpm = ref([]); 41 | var arq_speed_list_timestamp = ref([]); 42 | var arq_speed_list_snr = ref([]); 43 | 44 | return { 45 | selectedCallsign, 46 | newChatCallsign, 47 | newChatMessage, 48 | totalUnreadMessages, 49 | inputText, 50 | messageInfoById, 51 | callsign_list, 52 | sorted_chat_list, 53 | chartSpeedPER0, 54 | chartSpeedPER25, 55 | chartSpeedPER75, 56 | beaconDataArray, 57 | beaconLabelArray, 58 | unsorted_beacon_list, 59 | sorted_beacon_list, 60 | arq_speed_list_bpm, 61 | arq_speed_list_snr, 62 | arq_speed_list_timestamp, 63 | scrollTrigger, 64 | triggerScrollToBottom, 65 | loading, 66 | }; 67 | }); 68 | -------------------------------------------------------------------------------- /freedata_gui/src/store/index.js: -------------------------------------------------------------------------------- 1 | import { createPinia } from "pinia"; 2 | 3 | const pinia = createPinia(); 4 | 5 | export default pinia; 6 | -------------------------------------------------------------------------------- /freedata_gui/src/store/serialStore.js: -------------------------------------------------------------------------------- 1 | import { defineStore } from "pinia"; 2 | import { getSerialDevices } from "../js/api"; // Make sure this points to the correct file 3 | import { ref } from "vue"; 4 | 5 | // Define "skel" fallback data for serial devices 6 | const skelSerial = [ 7 | { 8 | description: "No devices received from modem", 9 | port: "ignore", // Using "ignore" as a placeholder value 10 | }, 11 | ]; 12 | 13 | export const useSerialStore = defineStore("serialStore", () => { 14 | const serialDevices = ref([]); 15 | 16 | const loadSerialDevices = async () => { 17 | try { 18 | const devices = await getSerialDevices(); 19 | // Check if devices are valid and have entries, otherwise use skelSerial 20 | serialDevices.value = 21 | devices && devices.length > 0 ? devices : skelSerial; 22 | } catch (error) { 23 | console.error("Failed to load serial devices:", error); 24 | // Use skelSerial as fallback in case of error 25 | serialDevices.value = skelSerial; 26 | } 27 | 28 | // Ensure the "-- ignore --" option is always available 29 | if (!serialDevices.value.some((device) => device.port === "ignore")) { 30 | serialDevices.value.push({ description: "-- ignore --", port: "ignore" }); 31 | } 32 | }; 33 | 34 | return { 35 | serialDevices, 36 | loadSerialDevices, 37 | }; 38 | }); 39 | -------------------------------------------------------------------------------- /freedata_gui/src/store/stationStore.js: -------------------------------------------------------------------------------- 1 | import { defineStore } from "pinia"; 2 | import { ref } from "vue"; 3 | export const useStationStore = defineStore("stationStore", () => { 4 | const stationInfo = ref({ 5 | callsign: "N/A", // Default value for callsign 6 | location: { 7 | gridsquare: "N/A", // Default value for gridsquare 8 | }, 9 | info: { 10 | name: "", 11 | city: "", 12 | age: "", 13 | radio: "", 14 | antenna: "", 15 | email: "", 16 | website: "", 17 | socialMedia: { 18 | facebook: "", 19 | "twitter-x": "", // Use twitter-x to correspond to the Twitter X icon 20 | mastodon: "", 21 | instagram: "", 22 | linkedin: "", 23 | youtube: "", 24 | tiktok: "", 25 | }, 26 | comments: "", 27 | }, 28 | }); 29 | return { 30 | stationInfo, 31 | }; 32 | }); 33 | -------------------------------------------------------------------------------- /freedata_gui/src/styles.css: -------------------------------------------------------------------------------- 1 | /* disable scrolling in main window */ 2 | body { 3 | padding-right: 0px !important; 4 | overflow-y: hidden !important; 5 | overflow-x: hidden !important; 6 | padding-top: calc(env(safe-area-inset-top)); 7 | padding-bottom: env(safe-area-inset-bottom); 8 | padding-left: env(safe-area-inset-left); 9 | margin: 0; 10 | } 11 | 12 | .header { 13 | padding-top: calc(env(safe-area-inset-top)); 14 | } 15 | 16 | .footer { 17 | padding-bottom: env(safe-area-inset-bottom); 18 | } 19 | 20 | /*Progress bars with centered text*/ 21 | .progress { 22 | position: relative; 23 | transform: translateZ(0); 24 | } 25 | 26 | .progress span { 27 | position: absolute; 28 | display: block; 29 | width: 100%; 30 | color: black; 31 | } 32 | 33 | /* smooth scrolling */ 34 | html { 35 | scroll-behavior: smooth; 36 | } 37 | 38 | /* hide scrollbar in callsign list */ 39 | #callsignlist::-webkit-scrollbar { 40 | display: none; 41 | } 42 | 43 | #chatModuleMessage { 44 | resize: none; 45 | } 46 | 47 | #expand_textarea_label { 48 | border: 0; 49 | padding: 1px; 50 | } 51 | 52 | /* fixed border table header */ 53 | .tableFixHead { 54 | overflow: auto; 55 | height: 90vh; 56 | } 57 | .tableFixHead thead th { 58 | position: sticky; 59 | top: 0; 60 | z-index: 1; 61 | } 62 | table { 63 | border-collapse: collapse; 64 | width: 100%; 65 | } 66 | th, 67 | td { 68 | padding: 8px 16px; 69 | } 70 | 71 | th { 72 | background: #eee; 73 | } 74 | 75 | /* ------ emoji picker customization --------- */ 76 | .picker { 77 | border-radius: 10px; 78 | } 79 | 80 | emoji-picker { 81 | width: 100%; 82 | } 83 | 84 | /* force gpu usage 85 | https://stackoverflow.com/questions/13176746/css-keyframe-animation-cpu-usage-is-high-should-it-be-this-way/13293044#13293044 86 | */ 87 | 88 | .force-gpu { 89 | transform: translateZ(0); 90 | -webkit-transform: translateZ(0); 91 | -ms-transform: translateZ(0); 92 | will-change: transform; 93 | } 94 | 95 | /* force disable transition effects 96 | https://stackoverflow.com/a/9622873 97 | */ 98 | .disable-effects { 99 | -webkit-transition: none; 100 | -moz-transition: none; 101 | -ms-transition: none; 102 | -o-transition: none; 103 | transition: none; 104 | } 105 | 106 | /* image overlay */ 107 | 108 | .image-overlay:hover { 109 | opacity: 0.75 !important; 110 | transition: 0.5s; 111 | } 112 | 113 | /* theme area */ 114 | 115 | /* default light theme mods */ 116 | [data-bs-theme="light"] { 117 | .card-header { 118 | background-color: var(--bs-card-cap-bg); 119 | /*--bs-card-cap-bg: rgba(var(--bs-body-color-rgb), 0.3);*/ 120 | } 121 | } 122 | /* default dark theme mods */ 123 | [data-bs-theme="dark"] { 124 | /* default dark theme mods */ 125 | } 126 | -------------------------------------------------------------------------------- /freedata_gui/vue.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | transpileDependencies: [], 3 | publicPath: "/gui/", 4 | }; 5 | -------------------------------------------------------------------------------- /freedata_server/.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # C extensions 7 | *.so 8 | 9 | # Distribution / packaging 10 | .Python 11 | build/ 12 | develop-eggs/ 13 | dist/ 14 | downloads/ 15 | eggs/ 16 | .eggs/ 17 | lib/ 18 | lib64/ 19 | parts/ 20 | sdist/ 21 | var/ 22 | wheels/ 23 | pip-wheel-metadata/ 24 | share/python-wheels/ 25 | *.egg-info/ 26 | .installed.cfg 27 | *.egg 28 | MANIFEST 29 | 30 | # PyInstaller 31 | # Usually these files are written by a python script from a template 32 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 33 | *.manifest 34 | *.spec 35 | 36 | # Installer logs 37 | pip-log.txt 38 | pip-delete-this-directory.txt 39 | 40 | # Unit test / coverage reports 41 | htmlcov/ 42 | .tox/ 43 | .nox/ 44 | .coverage 45 | .coverage.* 46 | .cache 47 | nosetests.xml 48 | coverage.xml 49 | *.cover 50 | *.py,cover 51 | .hypothesis/ 52 | .pytest_cache/ 53 | 54 | # Translations 55 | *.mo 56 | *.pot 57 | 58 | # Django stuff: 59 | *.log 60 | local_settings.py 61 | db.sqlite3 62 | db.sqlite3-journal 63 | 64 | # Flask stuff: 65 | instance/ 66 | .webassets-cache 67 | 68 | # Scrapy stuff: 69 | .scrapy 70 | 71 | # Sphinx documentation 72 | docs/_build/ 73 | 74 | # PyBuilder 75 | target/ 76 | 77 | # Jupyter Notebook 78 | .ipynb_checkpoints 79 | 80 | # IPython 81 | profile_default/ 82 | ipython_config.py 83 | 84 | # pyenv 85 | .python-version 86 | 87 | # pipenv 88 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. 89 | # However, in case of collaboration, if having platform-specific dependencies or dependencies 90 | # having no cross-platform support, pipenv may install dependencies that don't work, or not 91 | # install all needed dependencies. 92 | #Pipfile.lock 93 | 94 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow 95 | __pypackages__/ 96 | 97 | # Celery stuff 98 | celerybeat-schedule 99 | celerybeat.pid 100 | 101 | # SageMath parsed files 102 | *.sage.py 103 | 104 | # Environments 105 | .env 106 | .venv 107 | env/ 108 | venv/ 109 | ENV/ 110 | env.bak/ 111 | venv.bak/ 112 | 113 | # Spyder project settings 114 | .spyderproject 115 | .spyproject 116 | 117 | # Rope project settings 118 | .ropeproject 119 | 120 | # mkdocs documentation 121 | /site 122 | 123 | # mypy 124 | .mypy_cache/ 125 | .dmypy.json 126 | dmypy.json 127 | 128 | # Pyre type checker 129 | .pyre/ 130 | 131 | # FreeDATA config 132 | config.ini 133 | 134 | #FreeData DB 135 | freedata-messages.db -------------------------------------------------------------------------------- /freedata_server/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DJ2LS/FreeDATA/9fb1c63b4924c5cee3141dbd6928b2f6b66a5e8c/freedata_server/__init__.py -------------------------------------------------------------------------------- /freedata_server/adif_udp_logger.py: -------------------------------------------------------------------------------- 1 | import socket 2 | import structlog 3 | import threading 4 | 5 | def send_adif_qso_data(ctx, adif_data): 6 | """ 7 | Sends ADIF QSO data to the specified server via UDP in a non-blocking manner. 8 | 9 | Parameters: 10 | ctx.config_manager (dict): ctx.config_manageruration settings. 11 | ctx.event_manager: An event manager to log success/failure. 12 | adif_data (str): ADIF-formatted QSO data. 13 | """ 14 | log = structlog.get_logger() 15 | 16 | # Check if ADIF UDP logging is enabled 17 | adif = ctx.config_manager.config['QSO_LOGGING'].get('enable_adif_udp', 'False') 18 | if not adif: 19 | return # Exit if ADIF UDP logging is disabled 20 | 21 | adif_log_host = ctx.config_manager.config['QSO_LOGGING'].get('adif_udp_host', '127.0.0.1') 22 | adif_log_port = int(ctx.config_manager.config['QSO_LOGGING'].get('adif_udp_port', '2237')) 23 | 24 | def send_thread(): 25 | sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) 26 | # Set a timeout of 3 seconds to avoid blocking indefinitely 27 | sock.settimeout(3) 28 | 29 | callsign_start = adif_data.find(f">") + 1 30 | callsign_end = adif_data.find(f" bool: 11 | """ 12 | Enqueue a transmit command using the application context's managers. 13 | 14 | Args: 15 | ctx: AppContext containing config, state, event managers, etc. 16 | cmd_class: The command class to instantiate and run. 17 | params: A dict of parameters for the command (optional). 18 | 19 | Returns: 20 | bool: True if the command ran successfully, False otherwise. 21 | """ 22 | params = params or {} 23 | try: 24 | # Instantiate the command with required components 25 | command = cmd_class(ctx, params) 26 | logger.info("Enqueueing transmit command", command=command.get_name()) 27 | # Run in a thread to avoid blocking the event loop 28 | result = await asyncio.to_thread(command.run) 29 | return bool(result) 30 | except Exception as e: 31 | logger.error("Command execution failed", error=str(e)) 32 | return False 33 | 34 | -------------------------------------------------------------------------------- /freedata_server/api/common.py: -------------------------------------------------------------------------------- 1 | from fastapi import HTTPException 2 | from fastapi.responses import JSONResponse 3 | 4 | # Returns a standard API response 5 | def api_response(data, status=200): 6 | return JSONResponse(content=data, status_code=status) 7 | 8 | 9 | def api_abort(message, code): 10 | print(message) 11 | raise HTTPException(status_code=code, detail={"error": message}) 12 | 13 | 14 | def api_ok(message="ok"): 15 | return api_response({'message': message}) 16 | 17 | 18 | # Validates a parameter 19 | def validate(req, param, validator, is_required=True): 20 | if param not in req: 21 | if is_required: 22 | api_abort(f"Required parameter '{param}' is missing.", 400) 23 | else: 24 | return True 25 | if not validator(req[param]): 26 | api_abort(f"Value of '{param}' is invalid.", 400) 27 | -------------------------------------------------------------------------------- /freedata_server/api/general.py: -------------------------------------------------------------------------------- 1 | from fastapi import APIRouter, Depends 2 | import platform 3 | from context import AppContext, get_ctx 4 | 5 | router = APIRouter() 6 | 7 | @router.get( 8 | "/", 9 | summary="API Root", 10 | tags=["General"], 11 | responses={ 12 | 200: {"description": "API information."}, 13 | 404: {"description": "Resource not found."}, 14 | 503: {"description": "Service unavailable."}, 15 | }, 16 | ) 17 | async def index(ctx: AppContext = Depends(get_ctx)): 18 | """ 19 | Retrieve API metadata. 20 | 21 | Returns: 22 | dict: A JSON object containing API metadata. 23 | """ 24 | return { 25 | 'name': 'FreeDATA API', 26 | 'description': 'A sample API that provides free data services', 27 | 'api_version': ctx.constants.API_VERSION, 28 | 'modem_version': ctx.constants.MODEM_VERSION, 29 | 'license': ctx.constants.LICENSE, 30 | 'documentation': ctx.constants.DOCUMENTATION_URL, 31 | } 32 | 33 | @router.get( 34 | "/version", 35 | summary="Get Modem Version", 36 | tags=["General"], 37 | responses={ 38 | 200: {"description": "Successful Response."}, 39 | }, 40 | ) 41 | async def get_modem_version(ctx: AppContext = Depends(get_ctx)): 42 | """ 43 | Retrieve the modem version, API version, OS information, and Python information. 44 | 45 | Returns: 46 | dict: A JSON object containing version information. 47 | """ 48 | os_info = { 49 | 'system': platform.system(), 50 | 'node': platform.node(), 51 | 'release': platform.release(), 52 | 'version': platform.version(), 53 | 'machine': platform.machine(), 54 | 'processor': platform.processor(), 55 | } 56 | 57 | python_info = { 58 | 'build': platform.python_build(), 59 | 'compiler': platform.python_compiler(), 60 | 'implementation': platform.python_implementation(), 61 | 'version': platform.python_version(), 62 | } 63 | 64 | return { 65 | 'api_version': ctx.constants.API_VERSION, 66 | 'modem_version': ctx.constants.MODEM_VERSION, 67 | 'os_info': os_info, 68 | 'python_info': python_info, 69 | } 70 | -------------------------------------------------------------------------------- /freedata_server/api/websocket.py: -------------------------------------------------------------------------------- 1 | from fastapi import APIRouter, WebSocket, Depends 2 | from context import AppContext, get_ctx 3 | 4 | router = APIRouter() 5 | 6 | @router.websocket("/events") 7 | async def websocket_events( 8 | websocket: WebSocket, 9 | ctx: AppContext = Depends(get_ctx) 10 | ): 11 | """ 12 | WebSocket endpoint for event streams. 13 | """ 14 | await websocket.accept() 15 | await ctx.websocket_manager.handle_connection( 16 | websocket, 17 | ctx.websocket_manager.events_client_list, 18 | ctx.modem_events 19 | ) 20 | 21 | @router.websocket("/fft") 22 | async def websocket_fft( 23 | websocket: WebSocket, 24 | ctx: AppContext = Depends(get_ctx) 25 | ): 26 | """ 27 | WebSocket endpoint for FFT data streams. 28 | """ 29 | await websocket.accept() 30 | await ctx.websocket_manager.handle_connection( 31 | websocket, 32 | ctx.websocket_manager.fft_client_list, 33 | ctx.modem_fft 34 | ) 35 | 36 | @router.websocket("/states") 37 | async def websocket_states( 38 | websocket: WebSocket, 39 | ctx: AppContext = Depends(get_ctx) 40 | ): 41 | """ 42 | WebSocket endpoint for state updates. 43 | """ 44 | await websocket.accept() 45 | await ctx.websocket_manager.handle_connection( 46 | websocket, 47 | ctx.websocket_manager.states_client_list, 48 | ctx.state_queue 49 | ) 50 | -------------------------------------------------------------------------------- /freedata_server/api_validations.py: -------------------------------------------------------------------------------- 1 | """ This module provides a set of validation functions used within the FreeData system. It includes: 2 | 3 | validate_remote_config: Ensures that a remote configuration is present. 4 | validate_freedata_callsign: Checks if a callsign conforms to a defined pattern. Note: The current regular expression allows 1 to 7 alphanumeric characters followed by a hyphen and 1 to 3 digits, but it may require adjustment to fully support all SSID values from 0 to 255. 5 | validate_message_attachment: Validates that a message attachment (represented as a dictionary) includes the required fields ('name', 'type', 'data') and that the 'name' and 'data' fields are not empty. 6 | 7 | """ 8 | 9 | 10 | import re 11 | 12 | def validate_remote_config(config): 13 | """Validates the presence of a remote configuration. 14 | 15 | This function checks if a remote configuration is present. 16 | 17 | Args: 18 | config: The configuration to validate. 19 | 20 | Returns: 21 | True if the configuration is present, None otherwise. 22 | """ 23 | if not config: 24 | return 25 | return True 26 | 27 | def validate_freedata_callsign(callsign): 28 | """Validates a FreeData callsign. 29 | 30 | This function checks if a given callsign conforms to the defined pattern. 31 | Currently, the regular expression allows 1 to 7 alphanumeric characters 32 | followed by a hyphen and 1 to 3 digits. Note: This may require adjustment 33 | to fully support all SSID values from 0 to 255. 34 | 35 | Args: 36 | callsign: The callsign to validate. 37 | 38 | Returns: 39 | True if the callsign is valid, False otherwise. 40 | """ 41 | #regexp = "^[a-zA-Z]+\d+\w+-\d{1,2}$" 42 | regexp = "^[A-Za-z0-9]{1,7}-[0-9]{1,3}$" # still broken - we need to allow all ssids form 0 - 255 43 | return re.compile(regexp).match(callsign) is not None 44 | 45 | def validate_message_attachment(attachment): 46 | """Validates a message attachment. 47 | 48 | This function checks if the attachment includes the required fields ('name', 'type', 'data') 49 | and that the 'name' and 'data' fields are not empty. It raises a ValueError if 50 | any of these conditions are not met. Note: The 'type' field is not checked for 51 | emptiness as some files may not have a MIME type. 52 | 53 | Args: 54 | attachment: The message attachment to validate (represented as a dictionary). 55 | 56 | Raises: 57 | ValueError: If the attachment is missing a required field or if the 'name' or 'data' field is empty. 58 | """ 59 | for field in ['name', 'type', 'data']: 60 | if field not in attachment: 61 | raise ValueError(f"Attachment missing '{field}'") 62 | 63 | # check for content length, except type 64 | # there are some files out there, don't having a mime type 65 | if len(attachment[field]) < 1 and field not in ["type"]: 66 | raise ValueError(f"Attachment has empty '{field}'") 67 | -------------------------------------------------------------------------------- /freedata_server/audio_buffer.py: -------------------------------------------------------------------------------- 1 | import numpy as np 2 | import threading 3 | import structlog 4 | log = structlog.get_logger("buffer") 5 | 6 | class CircularBuffer: 7 | """A circular buffer for storing audio samples. 8 | 9 | The buffer is implemented as a NumPy array of a fixed size. The push() 10 | method adds samples to the buffer, and the pop() method removes samples 11 | from the buffer. Both methods block if the buffer is full or empty, 12 | respectively. 13 | """ 14 | def __init__(self, size): 15 | self.size = size 16 | self.buffer = np.zeros(size, dtype=np.int16) 17 | self.head = 0 # Read pointer. 18 | self.tail = 0 # Write pointer. 19 | self.nbuffer = 0 # Number of samples stored. 20 | self.lock = threading.Lock() 21 | self.cond = threading.Condition(self.lock) 22 | log.debug("[BUF] Creating ring buffer", size=size) 23 | 24 | def push(self, samples): 25 | """Push samples onto the buffer. 26 | 27 | Args: 28 | samples: The samples to push onto the buffer. 29 | 30 | Blocks until there is enough space in the buffer. 31 | """ 32 | with self.cond: 33 | samples_len = len(samples) 34 | # Block until there is room. 35 | while self.nbuffer + samples_len > self.size: 36 | self.cond.wait() 37 | end_space = self.size - self.tail 38 | if samples_len <= end_space: 39 | self.buffer[self.tail:self.tail + samples_len] = samples 40 | else: 41 | self.buffer[self.tail:] = samples[:end_space] 42 | self.buffer[:samples_len - end_space] = samples[end_space:] 43 | self.tail = (self.tail + samples_len) % self.size 44 | self.nbuffer += samples_len 45 | self.cond.notify_all() 46 | 47 | def pop(self, n): 48 | with self.cond: 49 | while self.nbuffer < n: 50 | self.cond.wait() 51 | end_space = self.size - self.head 52 | if n <= end_space: 53 | result = self.buffer[self.head:self.head + n].copy() 54 | else: 55 | result = np.concatenate(( 56 | self.buffer[self.head:].copy(), 57 | self.buffer[:n - end_space].copy() 58 | )) 59 | # Update head and count. 60 | self.head = (self.head + n) % self.size 61 | self.nbuffer -= n 62 | if self.nbuffer > 0: 63 | # Reassemble the valid data contiguously at the start. 64 | if self.head + self.nbuffer <= self.size: 65 | self.buffer[:self.nbuffer] = self.buffer[self.head:self.head + self.nbuffer] 66 | else: 67 | part1 = self.size - self.head 68 | self.buffer[:part1] = self.buffer[self.head:] 69 | self.buffer[part1:self.nbuffer] = self.buffer[:self.nbuffer - part1] 70 | self.head = 0 71 | self.tail = self.nbuffer 72 | self.cond.notify_all() 73 | return result 74 | -------------------------------------------------------------------------------- /freedata_server/codec2_filter_coeff.py: -------------------------------------------------------------------------------- 1 | import numpy as np 2 | #from scipy.signal import freqz 3 | import ctypes 4 | 5 | testFilter = (ctypes.c_float * 3)(1.000000,1.000000,1.000000) 6 | 7 | def generate_filter_coefficients(Fs_Hz, bandwidth_Hz, taps): 8 | """Generates filter coefficients for a sinc filter. 9 | 10 | This function calculates the coefficients for a sinc filter based on the 11 | provided sampling frequency, bandwidth, and number of taps. The 12 | coefficients are returned as a ctypes array of floats, with real and 13 | imaginary parts interleaved. 14 | 15 | Args: 16 | Fs_Hz (float): The sampling frequency in Hz. 17 | bandwidth_Hz (float): The bandwidth of the filter in Hz. 18 | taps (int): The number of taps for the filter. 19 | 20 | Returns: 21 | ctypes.c_float array: The filter coefficients as a ctypes array. 22 | """ 23 | B = bandwidth_Hz / Fs_Hz 24 | Ntap = taps 25 | h = np.zeros(Ntap, dtype=np.csingle) 26 | 27 | # Generating filter coefficients 28 | for i in range(Ntap): 29 | n = i - (Ntap - 1) / 2 30 | h[i] = B * np.sinc(n * B) 31 | 32 | # Convert to ctypes array (interleaved real and imaginary) 33 | CArrayType = ctypes.c_float * (len(h) * 2) 34 | return CArrayType(*(np.hstack([np.real(h), np.imag(h)]).tolist())) 35 | 36 | """ 37 | def plot_filter(): 38 | 39 | Fs = 8000 # Sampling frequency 40 | bandwidth = 2438 # Bandwidth in Hz 41 | centre_freq = 1500 # Centre frequency in Hz 42 | 43 | # Generate filter coefficients 44 | h = generate_filter_coefficients(Fs, bandwidth, centre_freq) 45 | print(h) 46 | 47 | # Frequency response 48 | w, H = freqz(h, worN=8000, fs=Fs) 49 | 50 | # Plotting 51 | plt.figure(figsize=(12, 6)) 52 | plt.plot(w, 20 * np.log10(np.abs(H)), 'b') 53 | plt.title('Frequency Response') 54 | plt.ylabel('Magnitude [dB]') 55 | plt.grid(True) 56 | plt.show() 57 | 58 | """ -------------------------------------------------------------------------------- /freedata_server/command_arq_raw.py: -------------------------------------------------------------------------------- 1 | import queue 2 | from command import TxCommand 3 | import api_validations 4 | import base64 5 | from queue import Queue 6 | from arq_session_iss import ARQSessionISS 7 | from arq_data_type_handler import ARQ_SESSION_TYPES 8 | import numpy as np 9 | import threading 10 | 11 | class ARQRawCommand(TxCommand): 12 | """Command for transmitting raw data via ARQ. 13 | 14 | This command handles the transmission of raw data using the ARQ protocol. 15 | It prepares the data, creates an ARQ session, and starts the 16 | transmission. 17 | """ 18 | 19 | def set_params_from_api(self, apiParams): 20 | """Sets parameters for the command from the API request. 21 | 22 | This method extracts parameters such as dxcall, data type, and raw 23 | data from the provided API parameters dictionary. It validates the 24 | dxcall and sets default values if necessary. 25 | 26 | Args: 27 | apiParams (dict): A dictionary containing the API parameters. 28 | """ 29 | self.dxcall = apiParams['dxcall'] 30 | if not api_validations.validate_freedata_callsign(self.dxcall): 31 | self.dxcall = f"{self.dxcall}-0" 32 | 33 | try: 34 | self.type = ARQ_SESSION_TYPES[apiParams['type']] 35 | except KeyError: 36 | self.type = ARQ_SESSION_TYPES.raw 37 | 38 | self.data = base64.b64decode(apiParams['data']) 39 | 40 | def run(self): 41 | """Executes the ARQ raw data transmission command. 42 | 43 | This method prepares the data for transmission, creates an ARQ session, 44 | and starts the transmission process. It includes a random delay to 45 | mitigate packet collisions and handles potential errors during session 46 | startup. 47 | 48 | Args: 49 | event_queue (Queue): The event queue for emitting events. 50 | modem: The modem object for transmission. 51 | 52 | Returns: 53 | ARQSessionISS or bool: The ARQSessionISS object if the session 54 | starts successfully, False otherwise. 55 | """ 56 | try: 57 | self.emit_event() 58 | self.logger.info(self.log_message()) 59 | 60 | # wait some random time and wait if we have an ongoing codec2 transmission 61 | # on our channel. This should prevent some packet collision 62 | random_delay = np.random.randint(0, 6) 63 | threading.Event().wait(random_delay) 64 | self.ctx.state_manager.channel_busy_condition_codec2.wait(5) 65 | 66 | prepared_data, type_byte = self.arq_data_type_handler.prepare(self.data, self.type) 67 | 68 | iss = ARQSessionISS(self.ctx, self.dxcall, prepared_data, type_byte) 69 | if iss.id: 70 | self.ctx.state_manager.register_arq_iss_session(iss) 71 | iss.start() 72 | return iss 73 | except Exception as e: 74 | self.log(f"Error starting ARQ session: {e}", isWarning=True) 75 | 76 | return False -------------------------------------------------------------------------------- /freedata_server/command_beacon.py: -------------------------------------------------------------------------------- 1 | from command import TxCommand 2 | 3 | class BeaconCommand(TxCommand): 4 | """Command for transmitting beacon frames. 5 | 6 | This command builds and transmits beacon frames, indicating the station's 7 | status (away from key or not). 8 | """ 9 | 10 | def build_frame(self): 11 | """Builds a beacon frame. 12 | 13 | This method retrieves the station's "away from key" status from the 14 | state manager and uses it to build a beacon frame. 15 | 16 | Returns: 17 | bytearray: The built beacon frame. 18 | """ 19 | beacon_state = self.ctx.state_manager.is_away_from_key 20 | return self.frame_factory.build_beacon(beacon_state) 21 | 22 | 23 | #def transmit(self, freedata_server): 24 | # super().transmit(freedata_server) 25 | # if self.config['MODEM']['enable_morse_identifier']: 26 | # mycall = f"{self.config['STATION']['mycall']}-{self.config['STATION']['myssid']}" 27 | # freedata_server.transmit_morse("morse", 1, 0, mycall) 28 | -------------------------------------------------------------------------------- /freedata_server/command_cq.py: -------------------------------------------------------------------------------- 1 | from command import TxCommand 2 | from codec2 import FREEDV_MODE 3 | class CQCommand(TxCommand): 4 | """Command for transmitting CQ frames. 5 | 6 | This command builds and transmits CQ (Calling Any Station) frames using 7 | the FreeDV protocol. 8 | """ 9 | 10 | def build_frame(self): 11 | """Builds a CQ frame. 12 | 13 | This method uses the frame factory to build a CQ (Calling Any Station) 14 | frame. 15 | 16 | Returns: 17 | bytearray: The built CQ frame. 18 | """ 19 | return self.frame_factory.build_cq() 20 | -------------------------------------------------------------------------------- /freedata_server/command_fec.py: -------------------------------------------------------------------------------- 1 | from command import TxCommand 2 | import base64 3 | 4 | class FecCommand(TxCommand): 5 | """Command for transmitting data using Forward Error Correction (FEC). 6 | 7 | This command prepares and transmits data packets using FEC, optionally 8 | sending a wakeup frame beforehand. It supports base64-encoded payloads 9 | and handles various FEC modes. 10 | """ 11 | 12 | def set_params_from_api(self, apiParams): 13 | """Sets parameters from the API request. 14 | 15 | This method extracts the FEC mode, wakeup flag, and base64-encoded 16 | payload from the API parameters. It decodes the payload and raises 17 | a TypeError if the payload is not a valid base64 string. 18 | 19 | Args: 20 | apiParams (dict): A dictionary containing the API parameters. 21 | 22 | Raises: 23 | TypeError: If the payload is not a valid base64 string. 24 | 25 | Returns: 26 | dict: The API parameters after processing. 27 | """ 28 | self.mode = apiParams['mode'] 29 | self.wakeup = apiParams['wakeup'] 30 | payload_b64 = apiParams['payload'] 31 | 32 | if len(payload_b64) % 4: 33 | raise TypeError("Invalid base64 payload") 34 | self.payload = base64.b64decode(payload_b64) 35 | 36 | return super().set_params_from_api(apiParams) 37 | 38 | def build_wakeup_frame(self): 39 | """Builds a wakeup frame for FEC. 40 | 41 | This method uses the frame factory to build a wakeup frame for the 42 | specified FEC mode. 43 | 44 | Returns: 45 | bytearray: The built wakeup frame. 46 | """ 47 | return self.frame_factory.build_fec_wakeup(self.mode) 48 | 49 | def build_frame(self): 50 | """Builds the FEC frame. 51 | 52 | This method uses the frame factory to build the FEC frame with the 53 | specified mode and payload. 54 | 55 | Returns: 56 | bytearray: The built FEC frame. 57 | """ 58 | return self.frame_factory.build_fec(self. mode, self.payload) 59 | 60 | def transmit(self, tx_frame_queue): 61 | """Transmits the FEC frame, optionally sending a wakeup frame first. 62 | 63 | This method transmits the built FEC frame via the provided queue. 64 | If the wakeup flag is set, it sends a wakeup frame before the 65 | actual data frame. 66 | 67 | Args: 68 | tx_frame_queue: The transmission queue. 69 | """ 70 | if self.wakeup: 71 | tx_queue_item = self.make_modem_queue_item(self.get_c2_mode(), 1, 0, self.build_wakeup_frame()) 72 | tx_frame_queue.put(tx_queue_item) 73 | 74 | tx_queue_item = self.make_modem_queue_item(self.get_c2_mode(), 1, 0, self.build_frame()) 75 | tx_frame_queue.put(tx_queue_item) 76 | -------------------------------------------------------------------------------- /freedata_server/command_p2p_connection.py: -------------------------------------------------------------------------------- 1 | import queue 2 | from command import TxCommand 3 | import api_validations 4 | import base64 5 | from queue import Queue 6 | from p2p_connection import P2PConnection 7 | 8 | class P2PConnectionCommand(TxCommand): 9 | """Command to initiate a P2P connection. 10 | 11 | This command sets up a P2P connection between two stations, handling 12 | session creation, registration, and connection establishment. 13 | """ 14 | 15 | def set_params_from_api(self, apiParams): 16 | """Sets parameters from the API request. 17 | 18 | This method extracts the origin and destination callsigns from the 19 | API parameters and validates them, adding a default SSID if necessary. 20 | 21 | Args: 22 | apiParams (dict): A dictionary containing the API parameters. 23 | """ 24 | self.origin = apiParams['origin'] 25 | if not api_validations.validate_freedata_callsign(self.origin): 26 | self.origin = f"{self.origin}-0" 27 | 28 | self.destination = apiParams['destination'] 29 | if not api_validations.validate_freedata_callsign(self.destination): 30 | self.destination = f"{self.destination}-0" 31 | 32 | 33 | def connect(self): 34 | """Placeholder for the connect method. 35 | 36 | This method is currently not implemented and serves as a placeholder 37 | for future functionality related to P2P connection establishment. 38 | 39 | Args: 40 | event_queue (Queue): The event queue. 41 | modem: The modem object. 42 | """ 43 | pass 44 | 45 | def run(self): 46 | """Executes the P2P connection command. 47 | 48 | This method creates a P2PConnection session, registers it with the 49 | state manager, initiates the connection, and handles potential errors 50 | during session startup. 51 | 52 | Args: 53 | event_queue (Queue): The event queue. 54 | modem: The modem object. 55 | 56 | Returns: 57 | P2PConnection or bool: The P2PConnection object if successful, False otherwise. 58 | """ 59 | try: 60 | session = P2PConnection(self.ctx, self.origin, self.destination) 61 | if session.session_id: 62 | self.ctx.state_manager.register_p2p_connection_session(session) 63 | session.connect() 64 | return session 65 | return False 66 | 67 | except Exception as e: 68 | self.log(f"Error starting P2P Connection session: {e}", isWarning=True) 69 | 70 | return False -------------------------------------------------------------------------------- /freedata_server/command_ping.py: -------------------------------------------------------------------------------- 1 | from command import TxCommand 2 | import api_validations 3 | from message_system_db_manager import DatabaseManager 4 | 5 | 6 | class PingCommand(TxCommand): 7 | """Command for transmitting ping frames. 8 | 9 | This command sends a ping frame to a specified station, identified by 10 | its callsign. It also updates the callsign database. 11 | """ 12 | 13 | 14 | def set_params_from_api(self, apiParams): 15 | """Sets parameters from the API request. 16 | 17 | This method extracts the destination callsign (dxcall) from the API 18 | parameters, validates it, adds a default SSID if needed, and updates 19 | the callsign database. 20 | 21 | Args: 22 | apiParams (dict): A dictionary containing the API parameters. 23 | 24 | Returns: 25 | dict: The API parameters after processing. 26 | """ 27 | self.dxcall = apiParams['dxcall'] 28 | if not api_validations.validate_freedata_callsign(self.dxcall): 29 | self.dxcall = f"{self.dxcall}-0" 30 | # update callsign database... 31 | DatabaseManager(self.ctx).get_or_create_station(self.dxcall) 32 | 33 | return super().set_params_from_api(apiParams) 34 | 35 | def build_frame(self): 36 | """Builds a ping frame. 37 | 38 | This method uses the frame factory to build a ping frame addressed 39 | to the specified destination callsign. 40 | 41 | Returns: 42 | bytearray: The built ping frame. 43 | """ 44 | return self.frame_factory.build_ping(self.dxcall) 45 | -------------------------------------------------------------------------------- /freedata_server/command_qrv.py: -------------------------------------------------------------------------------- 1 | from command import TxCommand 2 | 3 | class QRVCommand(TxCommand): 4 | """Command for transmitting QRV frames. 5 | 6 | This command builds and transmits QRV (Ready to Receive) frames. 7 | """ 8 | 9 | def build_frame(self): 10 | """Builds a QRV frame. 11 | 12 | This method uses the frame factory to build a QRV (Ready to Receive) 13 | frame. 14 | 15 | Returns: 16 | bytearray: The built QRV frame. 17 | """ 18 | return self.frame_factory.build_qrv() 19 | -------------------------------------------------------------------------------- /freedata_server/command_test.py: -------------------------------------------------------------------------------- 1 | from command import TxCommand 2 | import codec2 3 | from codec2 import FREEDV_MODE 4 | 5 | 6 | class TestCommand(TxCommand): 7 | """Command for transmitting test frames. 8 | 9 | This command builds and transmits test frames using a specific FreeDV 10 | mode (data_ofdm_500). 11 | """ 12 | 13 | def build_frame(self): 14 | """Builds a test frame. 15 | 16 | This method uses the frame factory to build a test frame using the 17 | specified FreeDV mode. 18 | 19 | Returns: 20 | bytearray: The built test frame. 21 | """ 22 | return self.frame_factory.build_test(self.get_tx_mode().name) 23 | 24 | def get_tx_mode(self): 25 | """Returns the transmission mode for test frames. 26 | 27 | This method returns the specific FreeDV mode (data_ofdm_500) used for 28 | transmitting test frames. 29 | 30 | Returns: 31 | codec2.FREEDV_MODE: The FreeDV mode for test frames. 32 | """ 33 | if self.ctx.config_manager.config['EXP'].get('enable_vhf'): 34 | mode = FREEDV_MODE.data_vhf_1 35 | else: 36 | mode = FREEDV_MODE.data_ofdm_500 37 | 38 | return mode 39 | -------------------------------------------------------------------------------- /freedata_server/command_transmit_sine.py: -------------------------------------------------------------------------------- 1 | from command import TxCommand 2 | 3 | class TransmitSine(TxCommand): 4 | """Command for transmitting a sine wave. 5 | 6 | This command instructs the modem to transmit a continuous sine wave, 7 | which can be used for testing and calibration. 8 | """ 9 | def transmit(self): 10 | """Transmits a sine wave. 11 | 12 | This method instructs the modem to transmit a sine wave. It is used 13 | for testing and calibration purposes. 14 | 15 | Args: 16 | modem: The modem object. 17 | """ 18 | self.ctx.rf_modem.transmit_sine() 19 | # Code for debugging morse stuff... 20 | #modem.transmit_morse(0,0,[b'']) -------------------------------------------------------------------------------- /freedata_server/config.ini.example: -------------------------------------------------------------------------------- 1 | [NETWORK] 2 | modemaddress = 127.0.0.1 3 | modemport = 5000 4 | 5 | [STATION] 6 | mycall = AA1AAA 7 | mygrid = JN48ea 8 | myssid = 1 9 | ssid_list = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9] 10 | enable_explorer = False 11 | enable_stats = False 12 | respond_to_cq = True 13 | enable_callsign_blacklist = False 14 | callsign_blacklist = [] 15 | 16 | [AUDIO] 17 | input_device = 5a1c 18 | output_device = bd6c 19 | rx_audio_level = 0 20 | tx_audio_level = 0 21 | rx_auto_audio_level = True 22 | tx_auto_audio_level = False 23 | 24 | [RIGCTLD] 25 | ip = 127.0.0.1 26 | port = 4532 27 | path = 28 | command = 29 | arguments = 30 | enable_vfo = False 31 | 32 | [FLRIG] 33 | ip = 127.0.0.1 34 | port = 12345 35 | 36 | [RADIO] 37 | control = disabled 38 | model_id = 1001 39 | serial_port = /dev/cu.Bluetooth-Incoming-Port 40 | serial_speed = 38400 41 | data_bits = 8 42 | stop_bits = 1 43 | serial_handshake = ignore 44 | ptt_port = ignore 45 | ptt_type = USB 46 | serial_dcd = NONE 47 | serial_dtr = OFF 48 | serial_rts = OFF 49 | 50 | [MODEM] 51 | enable_morse_identifier = False 52 | tx_delay = 50 53 | maximum_bandwidth = 2438 54 | 55 | [SOCKET_INTERFACE] 56 | enable = False 57 | host = 127.0.0.1 58 | cmd_port = 9000 59 | data_port = 9001 60 | 61 | [MESSAGES] 62 | enable_auto_repeat = False 63 | 64 | [QSO_LOGGING] 65 | enable_adif_udp = False 66 | adif_udp_host = 127.0.0.1 67 | adif_udp_port = 2237 68 | enable_adif_wavelog = False 69 | adif_wavelog_host = http://raspberrypi 70 | adif_wavelog_api_key = API-KEY 71 | 72 | [GUI] 73 | auto_run_browser = True 74 | 75 | [EXP] 76 | enable_ring_buffer = False 77 | enable_vhf = False 78 | 79 | -------------------------------------------------------------------------------- /freedata_server/constants.py: -------------------------------------------------------------------------------- 1 | # Module for saving some constants 2 | CONFIG_ENV_VAR = 'FREEDATA_CONFIG' 3 | DEFAULT_CONFIG_FILE = 'config.ini' 4 | MODEM_VERSION = "0.17.3-beta" 5 | API_VERSION = 3 6 | ARQ_PROTOCOL_VERSION = 1 7 | LICENSE = 'GPL3.0' 8 | DOCUMENTATION_URL = 'https://wiki.freedata.app' 9 | STATS_API_URL = 'https://api.freedata.app/stats.php' 10 | EXPLORER_API_URL = 'https://api.freedata.app/explorer.php' 11 | MESSAGE_SYSTEM_DATABASE_VERSION = 0 -------------------------------------------------------------------------------- /freedata_server/context.py: -------------------------------------------------------------------------------- 1 | from queue import Queue 2 | from event_manager import EventManager 3 | from state_manager import StateManager 4 | from schedule_manager import ScheduleManager 5 | from config import CONFIG 6 | from service_manager import SM as ServiceManager 7 | from message_system_db_manager import DatabaseManager 8 | from message_system_db_attachments import DatabaseManagerAttachments 9 | from websocket_manager import wsm as WebsocketManager 10 | import audio 11 | import constants 12 | from fastapi import Request, WebSocket 13 | class AppContext: 14 | def __init__(self, config_file: str): 15 | self.config_manager = CONFIG(self, config_file) 16 | self.constants = constants 17 | self.p2p_data_queue = Queue() 18 | self.state_queue = Queue() 19 | self.modem_events = Queue() 20 | self.modem_fft = Queue() 21 | self.modem_service = Queue() 22 | self.event_manager = EventManager(self, [self.modem_events]) 23 | self.state_manager = StateManager(self.state_queue) 24 | self.schedule_manager = ScheduleManager(self) 25 | self.service_manager = ServiceManager(self) 26 | self.websocket_manager = WebsocketManager(self) 27 | 28 | self.socket_interface_manager = None # Socket interface instance, We start it as we need it 29 | self.rf_modem = None # Modem instnace, we start it as we need it 30 | self.message_system_db_manager = DatabaseManager(self) 31 | self.message_system_db_attachments = DatabaseManagerAttachments(self) 32 | 33 | self.TESTMODE = False 34 | self.TESTMODE_TRANSMIT_QUEUE = Queue() # This is a helper queue which holds bursts to be transmitted for helping using tests 35 | self.TESTMODE_RECEIVE_QUEUE = Queue() # This is a helper queue which holds received bursts for helping using tests 36 | self.TESTMODE_EVENTS = Queue() # This is a helper queue which holds events 37 | 38 | def startup(self): 39 | 40 | # initially read config 41 | self.config_manager.read() 42 | 43 | self.websocket_manager.startWorkerThreads(self) 44 | 45 | # start modem service 46 | self.modem_service.put("start") 47 | 48 | # DB setup 49 | db = DatabaseManager(self.event_manager) 50 | db.check_database_version() 51 | db.initialize_default_values() 52 | db.database_repair_and_cleanup() 53 | DatabaseManagerAttachments(self).clean_orphaned_attachments() 54 | 55 | def shutdown(self): 56 | try: 57 | for s in self.state_manager.arq_irs_sessions.values(): 58 | s.transmission_aborted() 59 | for s in self.state_manager.arq_iss_sessions.values(): 60 | s.abort_transmission(send_stop=False) 61 | s.transmission_aborted() 62 | except Exception: 63 | pass 64 | self.websocket_manager.shutdown() 65 | self.schedule_manager.stop() 66 | self.service_manager.shutdown() 67 | #self._audio.terminate() 68 | import os 69 | os._exit(0) 70 | # Dependency provider for FastAPI (HTTP & WebSocket) 71 | def get_ctx(request: Request = None, websocket: WebSocket = None) -> AppContext: 72 | """ 73 | Provide the application context for HTTP requests or WebSocket connections. 74 | 75 | FastAPI will pass either a Request or WebSocket instance. 76 | 77 | Returns: 78 | AppContext: The application context stored in state. 79 | """ 80 | conn = request or websocket 81 | return conn.app.state.ctx 82 | -------------------------------------------------------------------------------- /freedata_server/exceptions.py: -------------------------------------------------------------------------------- 1 | """ 2 | Custom exceptions for FreeDATA Python code 3 | """ 4 | 5 | 6 | class NoCallsign(UserWarning): 7 | """Raised when a required callsign is not provided""" 8 | 9 | 10 | class MessageStatusError(UserWarning): 11 | pass -------------------------------------------------------------------------------- /freedata_server/frame_handler_beacon.py: -------------------------------------------------------------------------------- 1 | import frame_handler 2 | import datetime 3 | from message_system_db_beacon import DatabaseManagerBeacon 4 | from message_system_db_messages import DatabaseManagerMessages 5 | 6 | 7 | from message_system_db_manager import DatabaseManager 8 | class BeaconFrameHandler(frame_handler.FrameHandler): 9 | """Handles received beacon frames. 10 | 11 | This class processes received beacon frames, stores them in the database, 12 | and checks for queued messages to be sent based on configuration and 13 | signal strength. 14 | """ 15 | 16 | def follow_protocol(self): 17 | """Processes the received beacon frame. 18 | 19 | This method adds the beacon information to the database and checks 20 | for queued messages to send if auto-repeat is enabled and the 21 | signal strength is above a certain threshold. 22 | """ 23 | DatabaseManagerBeacon(self.ctx).add_beacon(datetime.datetime.now(), 24 | self.details['frame']["origin"], 25 | self.details["snr"], 26 | self.details['frame']["gridsquare"] 27 | ) 28 | 29 | # only check for queued messages, if we have enabled this and if we have a minimum snr received 30 | if self.config["MESSAGES"]["enable_auto_repeat"] and self.details["snr"] >= -2: 31 | # set message to queued if beacon received 32 | DatabaseManagerMessages(self.ctx).set_message_to_queued_for_callsign(self.details['frame']["origin"]) 33 | -------------------------------------------------------------------------------- /freedata_server/frame_handler_cq.py: -------------------------------------------------------------------------------- 1 | import threading 2 | 3 | import frame_handler_ping 4 | import helpers 5 | import data_frame_factory 6 | import frame_handler 7 | from message_system_db_messages import DatabaseManagerMessages 8 | import numpy as np 9 | 10 | class CQFrameHandler(frame_handler.FrameHandler): 11 | """Handles received CQ frames. 12 | 13 | This class processes received CQ (Calling Any Station) frames and sends 14 | a QRV (Ready to Receive) frame as an acknowledgement if the station is 15 | not currently busy with ARQ. It also checks for queued messages to be 16 | sent based on the configuration. 17 | """ 18 | 19 | #def should_respond(self): 20 | # self.logger.debug(f"Respond to CQ: {self.ctx.config_manager.config['MODEM']['respond_to_cq']}") 21 | # return bool(self.ctx.config_manager.config['MODEM']['respond_to_cq'] and not self.ctx.state_manager.getARQ()) 22 | 23 | def follow_protocol(self): 24 | """Processes the received CQ frame. 25 | 26 | This method checks if the modem is currently busy with ARQ. If not, 27 | it sends a QRV frame as an acknowledgement and checks for queued 28 | messages to send. 29 | """ 30 | 31 | if self.ctx.state_manager.getARQ(): 32 | return 33 | 34 | self.logger.debug( 35 | f"[Modem] Responding to request from [{self.details['frame']['origin']}]", 36 | snr=self.details['snr'], 37 | ) 38 | 39 | self.send_ack() 40 | 41 | def send_ack(self): 42 | factory = data_frame_factory.DataFrameFactory(self.ctx) 43 | qrv_frame = factory.build_qrv(self.details['snr']) 44 | 45 | # wait some random time and wait if we have an ongoing codec2 transmission 46 | # on our channel. This should prevent some packet collision 47 | random_delay = np.random.randint(0, 6) 48 | threading.Event().wait(random_delay) 49 | self.ctx.state_manager.channel_busy_condition_codec2.wait(5) 50 | 51 | self.transmit(qrv_frame) 52 | 53 | if self.ctx.config_manager.config["MESSAGES"]["enable_auto_repeat"]: 54 | # set message to queued if CQ received 55 | DatabaseManagerMessages(self.ctx).set_message_to_queued_for_callsign(self.details['frame']["origin"]) 56 | -------------------------------------------------------------------------------- /freedata_server/frame_handler_p2p_connection.py: -------------------------------------------------------------------------------- 1 | from queue import Queue 2 | import frame_handler 3 | from event_manager import EventManager 4 | from state_manager import StateManager 5 | from modem_frametypes import FRAME_TYPE as FR 6 | from p2p_connection import P2PConnection 7 | 8 | class P2PConnectionFrameHandler(frame_handler.FrameHandler): 9 | """Handles P2P connection frames. 10 | 11 | This class processes P2P connection frames, manages P2P connections, 12 | and dispatches frames to the appropriate connection based on their 13 | type and session ID. 14 | """ 15 | 16 | def follow_protocol(self): 17 | """Processes received P2P connection frames. 18 | 19 | This method handles different P2P frame types, including connection 20 | requests, acknowledgements, payload data, disconnections, and payload 21 | acknowledgements. It manages connection creation, retrieval, and 22 | updates based on the frame type and session ID. 23 | """ 24 | 25 | if not self.should_respond(): 26 | return 27 | 28 | frame = self.details['frame'] 29 | session_id = frame['session_id'] 30 | snr = self.details["snr"] 31 | frequency_offset = self.details["frequency_offset"] 32 | 33 | if frame['frame_type_int'] == FR.P2P_CONNECTION_CONNECT.value: 34 | 35 | # Lost OPEN_ACK case .. ISS will retry opening a session 36 | if session_id in self.ctx.state_manager.arq_irs_sessions: 37 | session = self.ctx.state_manager.p2p_connection_sessions[session_id] 38 | 39 | # Normal case when receiving a SESSION_OPEN for the first time 40 | else: 41 | # if self.ctx.state_manager.check_if_running_arq_session(): 42 | # self.logger.warning("DISCARDING SESSION OPEN because of ongoing ARQ session ", frame=frame) 43 | # return 44 | session = P2PConnection(self.ctx, 45 | frame['origin'], 46 | frame['destination'], 47 | ) 48 | session.session_id = session_id 49 | self.ctx.state_manager.register_p2p_connection_session(session) 50 | 51 | elif frame['frame_type_int'] in [ 52 | FR.P2P_CONNECTION_CONNECT_ACK.value, 53 | FR.P2P_CONNECTION_DISCONNECT.value, 54 | FR.P2P_CONNECTION_DISCONNECT_ACK.value, 55 | FR.P2P_CONNECTION_PAYLOAD.value, 56 | FR.P2P_CONNECTION_PAYLOAD_ACK.value, 57 | FR.P2P_CONNECTION_HEARTBEAT.value, 58 | FR.P2P_CONNECTION_HEARTBEAT_ACK.value, 59 | ]: 60 | session = self.ctx.state_manager.get_p2p_connection_session(session_id) 61 | 62 | else: 63 | self.logger.warning("DISCARDING FRAME", frame=frame) 64 | return 65 | 66 | session.set_details(snr, frequency_offset) 67 | session.on_frame_received(frame) 68 | -------------------------------------------------------------------------------- /freedata_server/frame_handler_ping.py: -------------------------------------------------------------------------------- 1 | import frame_handler 2 | import helpers 3 | import data_frame_factory 4 | from message_system_db_messages import DatabaseManagerMessages 5 | 6 | 7 | class PingFrameHandler(frame_handler.FrameHandler): 8 | """Handles received PING frames. 9 | 10 | This class processes received PING frames, sends acknowledgements, and 11 | checks for queued messages to be sent based on configuration. 12 | """ 13 | 14 | #def is_frame_for_me(self): 15 | # call_with_ssid = self.config['STATION']['mycall'] + "-" + str(self.config['STATION']['myssid']) 16 | # valid, mycallsign = helpers.check_callsign( 17 | # call_with_ssid, 18 | # self.details["frame"]["destination_crc"], 19 | # self.config['STATION']['ssid_list']) 20 | 21 | # if not valid: 22 | # ft = self.details['frame']['frame_type'] 23 | # self.logger.info(f"[Modem] {ft} received but not for us.") 24 | # return valid 25 | 26 | def follow_protocol(self): 27 | """Processes the received PING frame. 28 | 29 | This method checks if the frame is for the current station and if 30 | the modem is not busy with ARQ. If both conditions are met, it sends 31 | a PING acknowledgement and checks for queued messages to send. 32 | """ 33 | if not bool(self.is_frame_for_me() and not self.ctx.state_manager.getARQ()): 34 | return 35 | self.logger.debug( 36 | f"[Modem] Responding to request from [{self.details['frame']['origin']}]", 37 | snr=self.details['snr'], 38 | ) 39 | self.send_ack() 40 | 41 | self.check_for_queued_message() 42 | 43 | def send_ack(self): 44 | """Sends a PING acknowledgement frame. 45 | 46 | This method builds a PING acknowledgement frame using the received 47 | frame's origin CRC and SNR, and transmits it using the modem. 48 | """ 49 | factory = data_frame_factory.DataFrameFactory(self.ctx) 50 | ping_ack_frame = factory.build_ping_ack( 51 | self.details['frame']['origin_crc'], 52 | self.details['snr'] 53 | ) 54 | self.transmit(ping_ack_frame) 55 | 56 | def check_for_queued_message(self): 57 | """Checks for queued messages to send. 58 | 59 | This method checks if auto-repeat is enabled in the configuration 60 | and if the received signal strength is above a certain threshold. 61 | If both conditions are met, it sets any messages addressed to the 62 | originating station to 'queued' status in the message database. 63 | """ 64 | 65 | # only check for queued messages, if we have enabled this and if we have a minimum snr received 66 | if self.config["MESSAGES"]["enable_auto_repeat"] and self.details["snr"] >= -2: 67 | # set message to queued if beacon received 68 | DatabaseManagerMessages(self.ctx).set_message_to_queued_for_callsign( 69 | self.details['frame']["origin"]) -------------------------------------------------------------------------------- /freedata_server/lib/codec2/libcodec2.1.2.dylib: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DJ2LS/FreeDATA/9fb1c63b4924c5cee3141dbd6928b2f6b66a5e8c/freedata_server/lib/codec2/libcodec2.1.2.dylib -------------------------------------------------------------------------------- /freedata_server/lib/codec2/libcodec2.dll: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DJ2LS/FreeDATA/9fb1c63b4924c5cee3141dbd6928b2f6b66a5e8c/freedata_server/lib/codec2/libcodec2.dll -------------------------------------------------------------------------------- /freedata_server/lib/codec2/libcodec2.so: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DJ2LS/FreeDATA/9fb1c63b4924c5cee3141dbd6928b2f6b66a5e8c/freedata_server/lib/codec2/libcodec2.so -------------------------------------------------------------------------------- /freedata_server/log_handler.py: -------------------------------------------------------------------------------- 1 | import logging.config 2 | import structlog 3 | 4 | # https://www.structlog.org/en/stable/standard-library.html 5 | def setup_logging(filename: str = "", level: str = "DEBUG"): 6 | """ 7 | Args: 8 | filename: 9 | level:str: Log level to output, possible values are: 10 | "CRITICAL", "FATAL", "ERROR", "WARNING", "WARN", "INFO", "DEBUG" 11 | """ 12 | 13 | timestamper = structlog.processors.TimeStamper(fmt="iso") 14 | pre_chain = [ 15 | structlog.stdlib.add_log_level, 16 | timestamper, 17 | ] 18 | 19 | config_dict = { 20 | "version": 1, 21 | "disable_existing_loggers": False, 22 | "formatters": { 23 | "plain": { 24 | "()": structlog.stdlib.ProcessorFormatter, 25 | "processor": structlog.dev.ConsoleRenderer(colors=False), 26 | "foreign_pre_chain": pre_chain, 27 | }, 28 | "colored": { 29 | "()": structlog.stdlib.ProcessorFormatter, 30 | "processor": structlog.dev.ConsoleRenderer(colors=True), 31 | "foreign_pre_chain": pre_chain, 32 | }, 33 | }, 34 | "handlers": { 35 | "default": { 36 | "level": level, 37 | "class": "logging.StreamHandler", 38 | "formatter": "colored", 39 | }, 40 | }, 41 | "loggers": { 42 | "": { 43 | "handlers": ["default"], 44 | "level": level, 45 | "propagate": True, 46 | }, 47 | }, 48 | } 49 | 50 | if filename: 51 | config_dict["handlers"]["file"] = { 52 | "level": level, 53 | "class": "logging.handlers.WatchedFileHandler", 54 | "filename": f"{filename}.log", 55 | "formatter": "plain", 56 | } 57 | config_dict["loggers"][""]["handlers"].append("file") 58 | 59 | logging.config.dictConfig(config_dict) 60 | structlog.configure( 61 | processors=[ 62 | structlog.stdlib.add_log_level, 63 | structlog.stdlib.PositionalArgumentsFormatter(), 64 | timestamper, 65 | structlog.processors.StackInfoRenderer(), 66 | structlog.processors.format_exc_info, 67 | structlog.stdlib.ProcessorFormatter.wrap_for_formatter, 68 | ], 69 | logger_factory=structlog.stdlib.LoggerFactory(), 70 | wrapper_class=structlog.stdlib.BoundLogger, 71 | cache_logger_on_first_use=True, 72 | ) 73 | -------------------------------------------------------------------------------- /freedata_server/modem_frametypes.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # -*- coding: utf-8 -*- 3 | 4 | from enum import Enum 5 | 6 | 7 | class FRAME_TYPE(Enum): 8 | """Lookup for frame types""" 9 | ARQ_STOP = 10 10 | ARQ_STOP_ACK = 11 11 | ARQ_SESSION_OPEN = 12 12 | ARQ_SESSION_OPEN_ACK = 13 13 | ARQ_SESSION_INFO = 14 14 | ARQ_SESSION_INFO_ACK = 15 15 | ARQ_BURST_FRAME = 20 16 | ARQ_BURST_ACK = 21 17 | P2P_CONNECTION_CONNECT = 30 18 | P2P_CONNECTION_CONNECT_ACK = 31 19 | P2P_CONNECTION_HEARTBEAT = 32 20 | P2P_CONNECTION_HEARTBEAT_ACK = 33 21 | P2P_CONNECTION_PAYLOAD = 34 22 | P2P_CONNECTION_PAYLOAD_ACK = 35 23 | P2P_CONNECTION_DISCONNECT = 36 24 | P2P_CONNECTION_DISCONNECT_ACK = 37 25 | #MESH_BROADCAST = 100 26 | #MESH_SIGNALLING_PING = 101 27 | #MESH_SIGNALLING_PING_ACK = 102 28 | CQ = 200 29 | QRV = 201 30 | PING = 210 31 | PING_ACK = 211 32 | #IS_WRITING = 215 33 | BEACON = 250 34 | #FEC = 251 35 | #FEC_WAKEUP = 252 36 | IDENT = 254 37 | TEST_FRAME = 255 38 | -------------------------------------------------------------------------------- /freedata_server/radio_manager.py: -------------------------------------------------------------------------------- 1 | import rigctld 2 | import flrig 3 | import rigdummy 4 | import serial_ptt 5 | import threading 6 | 7 | class RadioManager: 8 | def __init__(self, ctx): 9 | 10 | self.ctx = ctx 11 | 12 | self.radiocontrol = self.ctx.config_manager.config['RADIO']['control'] 13 | 14 | 15 | self.refresh_rate = 1 16 | self.stop_event = threading.Event() 17 | self.update_thread = threading.Thread(target=self.update_parameters, daemon=True) 18 | self._init_rig_control() 19 | 20 | def _init_rig_control(self): 21 | # Check how we want to control the radio 22 | if self.radiocontrol in ["rigctld", "rigctld_bundle"]: 23 | self.radio = rigctld.radio(self.ctx) 24 | elif self.radiocontrol == "serial_ptt": 25 | self.radio = serial_ptt.radio(self.ctx) 26 | elif self.radiocontrol == "flrig": 27 | self.radio = flrig.radio(self.ctx) 28 | else: 29 | self.radio = rigdummy.radio() 30 | 31 | self.update_thread.start() 32 | 33 | def set_ptt(self, state): 34 | self.radio.set_ptt(state) 35 | # set disabled ptt twice for reducing chance of stuck PTT 36 | if not state: 37 | self.radio.set_ptt(state) 38 | 39 | # send ptt state via socket interface 40 | try: 41 | if self.ctx.config_manager.config['SOCKET_INTERFACE']['enable'] and self.ctx.socket_interface_manager.command_server.command_handler: 42 | self.socket_interface_manager.command_server.command_handler.socket_respond_ptt(state) 43 | except Exception as e: 44 | print(e) 45 | 46 | def set_tuner(self, state): 47 | self.radio.set_tuner(state) 48 | self.ctx.state_manager.set_radio("radio_tuner", state) 49 | 50 | def set_frequency(self, frequency): 51 | self.radio.set_frequency(frequency) 52 | self.ctx.state_manager.set_radio("radio_frequency", frequency) 53 | 54 | def set_mode(self, mode): 55 | self.radio.set_mode(mode) 56 | self.ctx.state_manager.set_radio("radio_mode", mode) 57 | 58 | def set_rf_level(self, level): 59 | self.radio.set_rf_level(level) 60 | self.ctx.state_manager.set_radio("radio_rf_level", level) 61 | 62 | def update_parameters(self): 63 | while not self.stop_event.is_set(): 64 | parameters = self.radio.get_parameters() 65 | 66 | self.ctx.state_manager.set_radio("radio_frequency", parameters['frequency']) 67 | self.ctx.state_manager.set_radio("radio_mode", parameters['mode']) 68 | self.ctx.state_manager.set_radio("radio_bandwidth", parameters['bandwidth']) 69 | self.ctx.state_manager.set_radio("radio_rf_level", parameters['rf']) 70 | self.ctx.state_manager.set_radio("radio_tuner", parameters['tuner']) 71 | 72 | if self.ctx.state_manager.isTransmitting(): 73 | self.radio_alc = parameters['alc'] 74 | self.ctx.state_manager.set_radio("radio_swr", parameters['swr']) 75 | 76 | self.ctx.state_manager.set_radio("s_meter_strength", parameters['strength']) 77 | threading.Event().wait(self.refresh_rate) 78 | 79 | def stop(self): 80 | self.stop_event.set() 81 | self.radio.disconnect() 82 | self.radio.stop_service() -------------------------------------------------------------------------------- /freedata_server/rigdummy.py: -------------------------------------------------------------------------------- 1 | 2 | class radio: 3 | """ """ 4 | 5 | def __init__(self): 6 | self.parameters = { 7 | 'frequency': '---', 8 | 'mode': '---', 9 | 'alc': '---', 10 | 'strength': '---', 11 | 'bandwidth': '---', 12 | 'rf': '---', 13 | 'ptt': False, # Initial PTT state is set to False 14 | 'tuner': False, 15 | 'swr': '---' 16 | } 17 | 18 | def connect(self, **kwargs): 19 | """ 20 | 21 | Args: 22 | **kwargs: 23 | 24 | Returns: 25 | 26 | """ 27 | return True 28 | 29 | def disconnect(self, **kwargs): 30 | """ 31 | 32 | Args: 33 | **kwargs: 34 | 35 | Returns: 36 | 37 | """ 38 | return True 39 | 40 | def get_frequency(self): 41 | """ """ 42 | return self.parameters['frequency'] 43 | 44 | def get_mode(self): 45 | """ """ 46 | return self.parameters['mode'] 47 | 48 | def get_level(self): 49 | """ """ 50 | return None 51 | 52 | def get_alc(self): 53 | """ """ 54 | return None 55 | 56 | def get_meter(self): 57 | """ """ 58 | return None 59 | 60 | def get_bandwidth(self): 61 | """ """ 62 | return None 63 | 64 | def get_strength(self): 65 | """ """ 66 | return None 67 | 68 | def get_tuner(self): 69 | """ """ 70 | return None 71 | 72 | def get_swr(self): 73 | """ """ 74 | return None 75 | 76 | def set_bandwidth(self): 77 | """ """ 78 | return None 79 | def set_mode(self, mode): 80 | """ 81 | 82 | Args: 83 | mode: 84 | 85 | Returns: 86 | 87 | """ 88 | self.parameters['mode'] = mode 89 | return None 90 | 91 | def set_tuner(self, state): 92 | """ 93 | 94 | Args: 95 | mode: 96 | 97 | Returns: 98 | 99 | """ 100 | return None 101 | 102 | def set_frequency(self, frequency): 103 | """ 104 | 105 | Args: 106 | mode: 107 | 108 | Returns: 109 | 110 | """ 111 | self.parameters['frequency'] = frequency 112 | 113 | return None 114 | def get_status(self): 115 | """ 116 | 117 | Args: 118 | mode: 119 | 120 | Returns: 121 | 122 | """ 123 | return True 124 | def get_ptt(self): 125 | """ """ 126 | return None 127 | 128 | def set_ptt(self, state): 129 | """ 130 | 131 | Args: 132 | state: 133 | 134 | Returns: 135 | 136 | """ 137 | return state 138 | 139 | def close_rig(self): 140 | """ """ 141 | return 142 | 143 | 144 | def get_parameters(self): 145 | return self.parameters 146 | 147 | def stop_service(self): 148 | pass -------------------------------------------------------------------------------- /freedata_server/serial_ports.py: -------------------------------------------------------------------------------- 1 | import serial.tools.list_ports 2 | import helpers 3 | import sys 4 | 5 | 6 | def get_ports(): 7 | """Retrieves a list of available serial ports. 8 | 9 | This function retrieves a list of available serial ports on the system, 10 | including their names and descriptions. On Windows, it uses a specific 11 | registry lookup to get detailed port information. For other platforms, 12 | it uses the standard serial.tools.list_ports function. It calculates a 13 | CRC-16 checksum of the hardware ID (HWID) for each port and appends it 14 | to the description to ensure unique entries. 15 | 16 | Windows part taken from https://github.com/pyserial/pyserial/pull/70 as a temporary fix 17 | 18 | Returns: 19 | list: A list of dictionaries, where each dictionary represents a 20 | serial port and contains 'port' (str) and 'description' (str) keys. 21 | """ 22 | 23 | serial_devices = [] 24 | if sys.platform == 'win32': 25 | import list_ports_winreg 26 | ports = list_ports_winreg.comports(include_links=False) 27 | else: 28 | 29 | ports = serial.tools.list_ports.comports(include_links=False) 30 | 31 | for port, desc, hwid in ports: 32 | # calculate hex of hwid if we have unique names 33 | crc_hwid = helpers.get_crc_16(bytes(hwid, encoding="utf-8")) 34 | crc_hwid = crc_hwid.hex() 35 | description = f"{desc} [{crc_hwid}]" 36 | serial_devices.append( 37 | {"port": str(port), "description": str(description)} 38 | ) 39 | return serial_devices 40 | -------------------------------------------------------------------------------- /freedata_server/socket_interface_data.py: -------------------------------------------------------------------------------- 1 | """ WORK IN PROGRESS by DJ2LS""" 2 | 3 | 4 | class SocketDataHandler: 5 | 6 | def __init__(self, cmd_request, ctx): 7 | self.cmd_request = cmd_request 8 | self.ctx = ctx 9 | self.session = None 10 | 11 | def send_response(self, message): 12 | full_message = f"{message}\r" 13 | self.cmd_request.sendall(full_message.encode()) 14 | 15 | def send_data_to_client(self, data): 16 | self.cmd_request.sendall(data + b'\r') 17 | -------------------------------------------------------------------------------- /freedata_server/stats.py: -------------------------------------------------------------------------------- 1 | # -*- coding: UTF-8 -*- 2 | """ 3 | Created on 05.11.23 4 | 5 | @author: DJ2LS 6 | """ 7 | # pylint: disable=invalid-name, line-too-long, c-extension-no-member 8 | # pylint: disable=import-outside-toplevel, attribute-defined-outside-init 9 | import requests 10 | import json 11 | import structlog 12 | from constants import MODEM_VERSION, STATS_API_URL 13 | 14 | log = structlog.get_logger("stats") 15 | 16 | class stats: 17 | """Handles the collection and submission of FreeData session statistics. 18 | 19 | This class collects various statistics about FreeData sessions, including 20 | SNR, data rate, file size, and duration. It then pushes these statistics 21 | to a remote API endpoint for aggregation and analysis. 22 | """ 23 | def __init__(self, ctx): 24 | self.api_url = STATS_API_URL 25 | self.ctx = ctx 26 | def push(self, status, session_statistics, dxcall, receiving=True): 27 | """Pushes session statistics to the remote API endpoint. 28 | 29 | This method collects session statistics, including average SNR, bytes 30 | per minute, file size, duration, and status, and sends them as a JSON 31 | payload to the configured API endpoint. It also includes histogram data 32 | for time, SNR, and BPM. 33 | 34 | Args: 35 | status (str): The status of the session (e.g., 'ENDED', 'FAILED'). 36 | session_statistics (dict): A dictionary containing session statistics. 37 | dxcall (str): The callsign of the remote station. 38 | receiving (bool, optional): True if the session was receiving data, False otherwise. Defaults to True. 39 | """ 40 | try: 41 | snr_raw = [item["snr"] for item in self.ctx.state_manager.arq_speed_list] 42 | avg_snr = round(sum(snr_raw) / len(snr_raw), 2) 43 | except Exception: 44 | avg_snr = 0 45 | 46 | if receiving: 47 | station = "IRS" 48 | else: 49 | station = "ISS" 50 | 51 | mycallsign = self.ctx.config_manager.config['STATION']['mycall'] 52 | ssid = self.ctx.config_manager.config['STATION']['myssid'] 53 | full_callsign = f"{mycallsign}-{ssid}" 54 | 55 | headers = {"Content-Type": "application/json"} 56 | station_data = { 57 | 'callsign': full_callsign, 58 | 'dxcallsign': dxcall, 59 | 'gridsquare': self.ctx.config_manager.config['STATION']['mygrid'], 60 | 'dxgridsquare': str(self.ctx.state_manager.dxgrid, "utf-8"), 61 | 'frequency': 0 if self.ctx.state_manager.radio_frequency is None else self.ctx.state_manager.radio_frequency, 62 | 'avgsnr': avg_snr, 63 | 'bytesperminute': session_statistics['bytes_per_minute'], 64 | 'filesize': session_statistics['total_bytes'], 65 | 'duration': session_statistics['duration'], 66 | 'status': status, 67 | 'direction': station, 68 | 'version': MODEM_VERSION, 69 | 'time_histogram': session_statistics['time_histogram'], # Adding new histogram data 70 | 'snr_histogram': session_statistics['snr_histogram'], # Adding new histogram data 71 | 'bpm_histogram': session_statistics['bpm_histogram'], # Adding new histogram data 72 | } 73 | 74 | station_data = json.dumps(station_data) 75 | try: 76 | response = requests.post(self.api_url, json=station_data, headers=headers) 77 | log.info("[STATS] push", code=response.status_code) 78 | 79 | # print(response.status_code) 80 | # print(response.content) 81 | 82 | except Exception as e: 83 | log.warning("[API] connection lost") -------------------------------------------------------------------------------- /freedata_server/wavelog_api_logger.py: -------------------------------------------------------------------------------- 1 | import requests 2 | import threading 3 | import structlog 4 | 5 | 6 | def send_wavelog_qso_data(config, event_manager, wavelog_data): 7 | """ 8 | Sends wavelog QSO data to the specified server via API call. 9 | 10 | Parameters: 11 | server_host:port (str) 12 | server_api_key (str) 13 | wavelog_data (str): wavelog-formatted ADIF QSO data. 14 | """ 15 | 16 | log = structlog.get_logger() 17 | 18 | # If False then exit the function 19 | wavelog = config['QSO_LOGGING'].get('enable_adif_wavelog', 'False') 20 | 21 | if not wavelog: 22 | return # exit as we don't want to log Wavelog 23 | 24 | wavelog_host = config['QSO_LOGGING'].get('adif_wavelog_host', 'http://localhost/') 25 | wavelog_api_key = config['QSO_LOGGING'].get('adif_wavelog_api_key', '') 26 | 27 | # check if the last part in the HOST URL from the config is correct 28 | if wavelog_host.endswith("/"): 29 | url = wavelog_host + "index.php/api/qso" 30 | else: 31 | url = wavelog_host + "/" + "index.php/api/qso" 32 | 33 | headers = { 34 | "Content-Type": "application/json", 35 | "Accept": "application/json" 36 | } 37 | 38 | data = { 39 | "key": wavelog_api_key, 40 | "station_profile_id": "1", 41 | "type": "adif", 42 | "string": wavelog_data 43 | } 44 | 45 | def send_api(): 46 | try: 47 | response = requests.post(url, headers=headers, json=data) 48 | response.raise_for_status() # Raise an error for bad status codes 49 | log.info(f"[CHAT] Wavelog API: {wavelog_data}") 50 | 51 | callsign_start = wavelog_data.find(f">") + 1 52 | callsign_end = wavelog_data.find(f"= nin: 49 | # demodulate audio 50 | nbytes = api.freedv_rawdatarx( 51 | freedv, bytes_out, audiobuffer.buffer.ctypes 52 | ) 53 | # get current freedata_server states and write to list 54 | # 1 trial 55 | # 2 sync 56 | # 3 trial sync 57 | # 6 decoded 58 | # 10 error decoding == NACK 59 | rx_status = api.freedv_get_rx_status(freedv) 60 | #print(rx_status) 61 | 62 | # decrement codec traffic counter for making state smoother 63 | 64 | audiobuffer.pop(nin) 65 | nin = api.freedv_nin(freedv) 66 | if nbytes == bytes_per_frame: 67 | print("DECODED!!!!") 68 | 69 | print("---------------------------------") 70 | print("ENDED") 71 | print(nin) 72 | print(audiobuffer.nbuffer) 73 | 74 | config = config.CONFIG('config.ini') 75 | modulator = modulator.Modulator(config.read()) 76 | #freedv = open_instance(FREEDV_MODE.data_ofdm_2438.value) 77 | #freedv = open_instance(FREEDV_MODE.datac14.value) 78 | #freedv = open_instance(FREEDV_MODE.datac1.value) 79 | freedv = open_instance(MODE.value) 80 | print(f"MODULATE: {MODE}") 81 | #freedv = open_instance(FREEDV_MODE.data_ofdm_500.value) 82 | #freedv = open_instance(FREEDV_MODE.qam16c2.value) 83 | 84 | 85 | frames = 1 86 | txbuffer = bytearray() 87 | 88 | for frame in range(0,frames): 89 | #txbuffer = modulator.transmit_add_silence(txbuffer, 1000) 90 | txbuffer = modulator.transmit_add_preamble(txbuffer, freedv) 91 | txbuffer = modulator.transmit_create_frame(txbuffer, freedv, b'123') 92 | txbuffer = modulator.transmit_add_postamble(txbuffer, freedv) 93 | txbuffer = modulator.transmit_add_silence(txbuffer, 1000) 94 | 95 | #sys.stdout.buffer.flush() 96 | #sys.stdout.buffer.write(txbuffer) 97 | #sys.stdout.buffer.flush() 98 | demod(txbuffer) 99 | 100 | -------------------------------------------------------------------------------- /tools/custom_mode_tests/over_the_air_mode_test.py: -------------------------------------------------------------------------------- 1 | import sys 2 | sys.path.append('freedata_server') 3 | 4 | import threading 5 | import ctypes 6 | from codec2 import open_instance, api, audio_buffer, FREEDV_MODE, resampler 7 | import modulator 8 | import config 9 | import helpers 10 | import numpy as np 11 | 12 | 13 | class FreeDV: 14 | def __init__(self, mode, config_file): 15 | self.mode = mode 16 | self.config = config.CONFIG(config_file) 17 | self.modulator = modulator.Modulator(self.config.read()) 18 | self.freedv = open_instance(self.mode.value) 19 | 20 | def demodulate(self, txbuffer): 21 | print(f"DEMOD: {self.mode} {self.mode.value}") 22 | c2instance = open_instance(self.mode.value) 23 | bytes_per_frame = int(api.freedv_get_bits_per_modem_frame(c2instance) / 8) 24 | bytes_out = ctypes.create_string_buffer(bytes_per_frame) 25 | api.freedv_set_frames_per_burst(c2instance, 1) 26 | audiobuffer = audio_buffer(len(txbuffer)) 27 | nin = api.freedv_nin(c2instance) 28 | audiobuffer.push(txbuffer) 29 | threading.Event().wait(0.01) 30 | 31 | while audiobuffer.nbuffer >= nin: 32 | nbytes = api.freedv_rawdatarx(self.freedv, bytes_out, audiobuffer.buffer.ctypes) 33 | rx_status = api.freedv_get_rx_status(self.freedv) 34 | nin = api.freedv_nin(self.freedv) 35 | print(f"{rx_status} - {nin}") 36 | 37 | audiobuffer.pop(nin) 38 | 39 | if nbytes == bytes_per_frame: 40 | print("DECODED!!!!") 41 | api.freedv_set_sync(self.freedv, 0) 42 | 43 | print("---------------------------------") 44 | print("ENDED") 45 | print(nin) 46 | print(audiobuffer.nbuffer) 47 | 48 | def write_to_file(self, txbuffer, filename): 49 | with open(filename, 'wb') as f: 50 | f.write(txbuffer) 51 | print(f"TX buffer written to {filename}") 52 | 53 | # Usage example 54 | if __name__ == "__main__": 55 | 56 | # geht 57 | MODE = FREEDV_MODE.data_ofdm_250 58 | RX_MODE = FREEDV_MODE.datac4 59 | 60 | # fail 61 | #MODE = FREEDV_MODE.datac4 62 | #RX_MODE = FREEDV_MODE.data_ofdm_250 63 | 64 | FRAMES = 1 65 | 66 | freedv_instance = FreeDV(MODE, 'config.ini') 67 | freedv_rx_instance = FreeDV(RX_MODE, 'config.ini') 68 | 69 | message = b'ABC' 70 | txbuffer = freedv_instance.modulator.create_burst(MODE, FRAMES, 100, message) 71 | freedv_instance.write_to_file(txbuffer, 'ota_audio.raw') 72 | txbuffer = np.frombuffer(txbuffer, dtype=np.int16) 73 | freedv_rx_instance.demodulate(txbuffer) 74 | 75 | 76 | # ./src/freedv_data_raw_rx --framesperburst 2 --testframes DATAC0 - /dev/null --vv 77 | # aplay -f S16_LE ../raw/test_datac1_006.raw 78 | # cat ota_audio.raw | ./freedata_server/lib/codec2/codec2/build_macos/src/freedv_data_raw_rx DATAC0 - /dev/null -vv 79 | """ 80 | Python --> Python --> C 81 | 82 | 83 | 84 | #x = np.frombuffer(txbuffer, dtype=np.int16) 85 | #resampler = resampler() 86 | #txbuffer = resampler.resample8_to_48(x) 87 | #txbuffer = resampler.resample48_to_8(txbuffer) 88 | #print(txbuffer) 89 | 90 | 91 | 92 | 93 | """ -------------------------------------------------------------------------------- /tools/custom_mode_tests/plot_speed_levels.py: -------------------------------------------------------------------------------- 1 | import matplotlib.pyplot as plt 2 | import numpy as np 3 | 4 | codec2_modes = { 5 | 'datac4': { 6 | 'min_snr': -4, 7 | 'bit_rate': 87, # Bit rate in bits per second 8 | 'bandwidth': 250, # Bandwidth in Hz 9 | }, 10 | 'data_ofdm_500': { 11 | 'min_snr': 1, 12 | 'bit_rate': 276, 13 | 'bandwidth': 500, 14 | }, 15 | 'datac1': { 16 | 'min_snr': 5, 17 | 'bit_rate': 980, 18 | 'bandwidth': 1700, 19 | }, 20 | #'datac2000': { 21 | # 'min_snr': 7.5, 22 | # 'bit_rate': 1280, 23 | # 'bandwidth': 2000, 24 | #}, 25 | 'data_ofdm_2438': { 26 | 'min_snr': 8.5, 27 | 'bit_rate': 1830, 28 | 'bandwidth': 2438, 29 | }, 30 | } 31 | 32 | 33 | # Extracting data from the dictionary 34 | snr_values = [info['min_snr'] for info in codec2_modes.values()] 35 | bit_rates = [info['bit_rate'] for info in codec2_modes.values()] 36 | bandwidths = [info['bandwidth'] for info in codec2_modes.values()] 37 | modes = list(codec2_modes.keys()) # Get the mode names 38 | 39 | 40 | 41 | # Plot bit/s vs SNR 42 | plt.figure(figsize=(12, 6)) 43 | plt.subplot(1, 2, 1) 44 | plt.scatter(snr_values, bit_rates, color='b') 45 | for i, txt in enumerate(modes): 46 | plt.annotate(txt, (snr_values[i], bit_rates[i])) # Annotate each point with mode name 47 | plt.plot(snr_values, bit_rates, '--', color='b') 48 | 49 | plt.yscale("log") 50 | 51 | plt.xlabel('SNR (dB)') 52 | plt.ylabel('Bit/s') 53 | plt.title('Bit Rate vs SNR') 54 | plt.grid(True) 55 | 56 | # Plot bandwidth vs SNR 57 | plt.subplot(1, 2, 2) 58 | plt.scatter(snr_values, bandwidths, color='g') 59 | for i, txt in enumerate(modes): 60 | plt.annotate(txt, (snr_values[i], bandwidths[i])) # Annotate each point with mode name 61 | plt.plot(snr_values, bandwidths, '--', color='g') 62 | plt.xlabel('SNR (dB)') 63 | plt.ylabel('Bandwidth (Hz)') 64 | plt.title('Bandwidth vs SNR') 65 | plt.grid(True) 66 | 67 | # Show plot 68 | plt.tight_layout() 69 | plt.show() 70 | -------------------------------------------------------------------------------- /tools/macOS/README.md: -------------------------------------------------------------------------------- 1 | ## FreeDATA Scripts for Apple macOS 2 | 3 | ### Preface 4 | 5 | The installation requires an already working MacPorts or Homebrew installation on your Mac, please follow the corresponding instrutions on https://www.macports.org/install.php or https://brew.sh 6 | The scripts run on Apple Silicon. It's not tested on Intel Macs.\ 7 | I include two short instruction how to install MacPorts or Homebrew. Please install only one of them! 8 | 9 | #### Short MacPorts installation instructions 10 | 11 | Install the Apple Command Line Tools\ 12 | Open the Terminal, you find it in the Utilities Folder inside the Applications Folder, and execute the following command: 13 | 14 | ``` 15 | % xcode-select --install 16 | ``` 17 | 18 | Download the required MacPorts version from the link above and install it as usual. (Double click the pkg and follow the instructions) 19 | If you have the Terminal open, please close it completely [command+q] to make shure that the MacPorts environment is loaded. 20 | 21 | #### Short Homebrew installation instructions 22 | 23 | Install the Apple Command Line Tools\ 24 | Open the Terminal, you find it in the Utilities Folder inside the Applications Folder, and execute the following command: 25 | 26 | ``` 27 | % xcode-select --install 28 | ``` 29 | 30 | This will take some time, depending on the speed of your mac and internet connections. After successfull installation install brew: 31 | 32 | ``` 33 | % /bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)" 34 | ``` 35 | 36 | brew tells you at the end of the installation, to execute some commands. please don't forget them. Close the Terminal completely [command+q]\ 37 | 38 | ### Install FreeDATA 39 | 40 | Open the Terminal and execute the following commands: 41 | 42 | ``` 43 | % mkdir ~/freedata 44 | % cd ~/freedata 45 | % curl -o install-freedata-macos.sh https://raw.githubusercontent.com/DJ2LS/FreeDATA/main/install-freedata-macos.sh 46 | % curl -o run-freedata-macos.sh https://raw.githubusercontent.com/DJ2LS/FreeDATA/main/run-freedata-macos.sh 47 | 48 | % bash install-freedata-macos.sh 49 | ``` 50 | 51 | ### Run FreeDATA 52 | 53 | As usual, open the Terminal and execute the following commands: 54 | 55 | ``` 56 | $ cd ~/freedata 57 | $ bash run-freedata-macos.sh 58 | ``` 59 | 60 | Your browser should open the FreeDATA webinterface. Please follow the instructions on https://wiki.freedata.app to configure FreeDATA. 61 | -------------------------------------------------------------------------------- /tools/run-server.sh: -------------------------------------------------------------------------------- 1 | FREEDATA_CONFIG=freedata_server/config.ini FREEDATA_DATABASE=freedata_server/freedata-messages.db python3 freedata_server/server.py 2 | -------------------------------------------------------------------------------- /tools/run-tests.sh: -------------------------------------------------------------------------------- 1 | #npm test --prefix freedata_gui 2 | python3 -m unittest discover tests 3 | 4 | -------------------------------------------------------------------------------- /tools/socket_interface/socket_client.py: -------------------------------------------------------------------------------- 1 | import socket 2 | import threading 3 | 4 | 5 | def receive_messages(sock): 6 | while True: 7 | try: 8 | # Receive messages from the server 9 | data = sock.recv(48) 10 | if not data: 11 | # If no data is received, break out of the loop 12 | print("Disconnected from server.") 13 | break 14 | print(f"\nReceived from server: {data.decode()}\n> ", end='') 15 | except Exception as e: 16 | print(f"Error receiving data: {e}") 17 | sock.close() 18 | break 19 | 20 | 21 | def tcp_client(server_ip, server_port): 22 | # Create a socket object 23 | client_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM) 24 | 25 | # Connect the client to the server 26 | client_socket.connect((server_ip, server_port)) 27 | 28 | print(f"Connected to server {server_ip} on port {server_port}") 29 | 30 | # Start the receiving thread 31 | threading.Thread(target=receive_messages, args=(client_socket,), daemon=True).start() 32 | 33 | try: 34 | while True: 35 | # Send data to the server 36 | message = input("> ") 37 | if message.lower() == 'quit': 38 | break 39 | message += '\r' 40 | client_socket.sendall(message.encode('utf-8')) 41 | except Exception as e: 42 | print(f"An error occurred: {e}") 43 | finally: 44 | # Close the connection when done 45 | client_socket.close() 46 | print("Connection closed.") 47 | 48 | 49 | # Example usage 50 | if __name__ == "__main__": 51 | SERVER_IP = "127.0.0.1" # Server IP address 52 | SERVER_PORT = 8300 # Server port number 53 | tcp_client(SERVER_IP, SERVER_PORT) 54 | -------------------------------------------------------------------------------- /tools/socket_interface/socket_data_client.py: -------------------------------------------------------------------------------- 1 | import socket 2 | import threading 3 | 4 | 5 | def receive_messages(sock): 6 | while True: 7 | try: 8 | # Receive messages from the server 9 | data = sock.recv(48) 10 | if not data: 11 | # If no data is received, break out of the loop 12 | print("Disconnected from server.") 13 | break 14 | print(f"\nReceived from server: {data.decode()}\n> ", end='') 15 | except Exception as e: 16 | print(f"Error receiving data: {e}") 17 | sock.close() 18 | break 19 | 20 | 21 | def tcp_client(server_ip, server_port): 22 | # Create a socket object 23 | client_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM) 24 | 25 | # Connect the client to the server 26 | client_socket.connect((server_ip, server_port)) 27 | 28 | print(f"Connected to server {server_ip} on port {server_port}") 29 | 30 | # Start the receiving thread 31 | threading.Thread(target=receive_messages, args=(client_socket,), daemon=True).start() 32 | 33 | try: 34 | while True: 35 | # Send data to the server 36 | message = input("> ") 37 | if message.lower() == 'quit': 38 | break 39 | client_socket.sendall(message.encode('utf-8')) 40 | except Exception as e: 41 | print(f"An error occurred: {e}") 42 | finally: 43 | # Close the connection when done 44 | client_socket.close() 45 | print("Connection closed.") 46 | 47 | 48 | # Example usage 49 | if __name__ == "__main__": 50 | SERVER_IP = "127.0.0.1" # Server IP address 51 | SERVER_PORT = 8301 # Server port number 52 | tcp_client(SERVER_IP, SERVER_PORT) 53 | --------------------------------------------------------------------------------