├── .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 | [](https://www.codefactor.io/repository/github/dj2ls/freedata)
8 | [](https://github.com/DJ2LS/FreeDATA/actions/workflows/modem_tests.yml)
9 |
10 | 
11 |
12 | 
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 |
66 | We're sorry but <%= htmlWebpackPlugin.options.title %> doesn't work
68 | properly without JavaScript enabled. Please enable it to
69 | continue.
71 |
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 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
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 |
2 |
6 |
10 |
18 |
22 |
23 |
24 |
25 |
26 | {{ getDate(item.timestamp) }}
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 |
42 |
43 |
44 |
45 |
83 |
--------------------------------------------------------------------------------
/freedata_gui/src/components/chat_messages_action_menu.vue:
--------------------------------------------------------------------------------
1 |
2 |
13 |
14 |
15 |
33 |
34 |
54 |
--------------------------------------------------------------------------------
/freedata_gui/src/components/chat_messages_image_preview.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
8 |
9 |
10 |
11 |
28 |
29 |
--------------------------------------------------------------------------------
/freedata_gui/src/components/grid/grid_CQ.vue:
--------------------------------------------------------------------------------
1 |
26 |
27 |
28 |
41 |
42 |
--------------------------------------------------------------------------------
/freedata_gui/src/components/grid/grid_active_heard_stations_mini.vue:
--------------------------------------------------------------------------------
1 |
34 |
35 |
36 |
37 |
44 |
45 |
46 |
47 |
48 |
49 |
50 |
51 |
52 | Tim{{ $t('grid.components.time') }}e
53 |
54 |
55 | {{ $t('grid.components.dxcall') }}
56 |
57 |
58 |
59 |
60 |
61 |
68 |
69 | {{ getDateTime(item.timestamp) }}
70 |
71 |
72 | {{ item.origin }}
73 |
74 |
75 |
76 |
77 |
78 |
79 |
80 |
81 |
--------------------------------------------------------------------------------
/freedata_gui/src/components/grid/grid_activities.vue:
--------------------------------------------------------------------------------
1 |
24 |
25 |
26 |
27 |
34 |
38 |
42 |
46 | {{ item[1].origin }}
47 | -
48 | {{ getDateTime(item[1].timestamp) }}
49 |
50 |
51 |
52 |
56 |
57 | {{ item[1].activity_type }} -
58 | {{ item[1].direction === 'received'
59 | ? $t('grid.components.received')
60 | : $t('grid.components.transmitted') }}
61 |
62 |
63 |
64 |
65 |
66 |
67 |
--------------------------------------------------------------------------------
/freedata_gui/src/components/grid/grid_beacon.vue:
--------------------------------------------------------------------------------
1 |
18 |
19 |
20 |
39 |
40 |
--------------------------------------------------------------------------------
/freedata_gui/src/components/grid/grid_button.vue:
--------------------------------------------------------------------------------
1 |
11 |
12 |
13 |
17 | {{ props.btnText }}
18 |
19 |
20 |
--------------------------------------------------------------------------------
/freedata_gui/src/components/grid/grid_dbfs.vue:
--------------------------------------------------------------------------------
1 |
9 |
10 |
11 |
15 |
24 |
28 | {{ state.dbfs_level }} dBFS
29 |
30 |
31 |
68 |
69 |
--------------------------------------------------------------------------------
/freedata_gui/src/components/grid/grid_frequency.vue:
--------------------------------------------------------------------------------
1 |
9 |
10 |
11 |
16 | {{ state.frequency / 1000 }} kHz
17 |
18 |
19 |
--------------------------------------------------------------------------------
/freedata_gui/src/components/grid/grid_mycall small.vue:
--------------------------------------------------------------------------------
1 |
8 |
9 |
10 |
32 |
33 |
--------------------------------------------------------------------------------
/freedata_gui/src/components/grid/grid_mycall.vue:
--------------------------------------------------------------------------------
1 |
8 |
9 |
10 |
13 |
14 | {{ settingsStore.remote.STATION.mycall }}-{{
15 | settingsStore.remote.STATION.myssid
16 | }}
17 |
18 |
19 |
20 |
--------------------------------------------------------------------------------
/freedata_gui/src/components/grid/grid_ping.vue:
--------------------------------------------------------------------------------
1 |
23 |
24 |
25 |
55 |
56 |
--------------------------------------------------------------------------------
/freedata_gui/src/components/grid/grid_ptt.vue:
--------------------------------------------------------------------------------
1 |
9 |
10 |
11 |
15 |
{{ $t('grid.components.onair').toUpperCase() }}
16 |
17 |
18 |
--------------------------------------------------------------------------------
/freedata_gui/src/components/grid/grid_s-meter.vue:
--------------------------------------------------------------------------------
1 |
9 |
10 |
11 |
15 |
24 |
28 | S-Meter(dB): {{ state.s_meter_strength_raw }}
29 |
30 |
31 |
68 |
69 |
--------------------------------------------------------------------------------
/freedata_gui/src/components/grid/grid_scatter.vue:
--------------------------------------------------------------------------------
1 |
91 |
92 |
93 |
94 |
95 |
102 |
103 |
107 |
108 |
109 |
110 |
111 |
112 |
113 |
114 |
--------------------------------------------------------------------------------
/freedata_gui/src/components/grid/grid_stats_chart.vue:
--------------------------------------------------------------------------------
1 |
95 |
96 |
97 |
98 |
99 |
106 |
107 |
111 |
112 |
113 |
114 |
--------------------------------------------------------------------------------
/freedata_gui/src/components/grid/grid_stop.vue:
--------------------------------------------------------------------------------
1 |
13 |
14 |
15 |
24 |
25 |
26 |
27 |
--------------------------------------------------------------------------------
/freedata_gui/src/components/grid/grid_swr_meter.vue:
--------------------------------------------------------------------------------
1 |
9 |
10 |
11 |
15 |
24 |
28 | SWR 1:{{ state.swr_raw }}
29 |
30 |
31 |
60 |
61 |
--------------------------------------------------------------------------------
/freedata_gui/src/components/grid/grid_tune.vue:
--------------------------------------------------------------------------------
1 |
8 |
9 |
10 |
22 |
23 |
--------------------------------------------------------------------------------
/freedata_gui/src/components/main_loading_screen.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
7 |
8 |
9 |
14 |
15 |
46 |
--------------------------------------------------------------------------------
/freedata_gui/src/components/main_screen.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
14 |
15 |
16 |
17 |
18 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
38 |
39 |
40 |
46 |
47 |
48 |
54 |
55 |
56 |
57 |
58 |
59 |
60 |
61 |
62 |
63 |
64 |
65 |
90 |
--------------------------------------------------------------------------------
/freedata_gui/src/components/settings_exp.vue:
--------------------------------------------------------------------------------
1 |
21 |
22 |
23 |
24 |
28 | {{ $t('settings.exp.introduction') }}
29 |
30 |
31 |
32 |
36 | {{ $t('settings.exp.info') }}
37 |
38 |
39 |
68 |
69 |
98 |
99 |
100 |
101 |
--------------------------------------------------------------------------------
/freedata_gui/src/components/settings_flrig.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 | {{ $t('settings.radio.flrighost') }}
7 |
13 |
14 |
15 |
16 |
17 |
26 |
27 |
28 |
29 |
30 | {{ $t('settings.radio.flrigport') }}
31 |
37 |
38 |
39 |
40 |
41 |
50 |
51 |
52 |
53 |
54 |
55 |
56 |
--------------------------------------------------------------------------------
/freedata_gui/src/components/settings_url.vue:
--------------------------------------------------------------------------------
1 |
10 |
11 |
12 |
96 |
97 |
--------------------------------------------------------------------------------
/freedata_gui/src/components/settings_web.vue:
--------------------------------------------------------------------------------
1 |
9 |
10 |
11 |
12 |
16 | {{ $t('settings.web.introduction') }}
17 |
18 |
19 |
23 | {{ $t('settings.web.description') }}
24 |
25 |
26 |
27 |
55 |
56 |
57 |
85 |
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 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 | ${content}
18 |
19 |
20 |
21 |
22 |
23 |
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 |
--------------------------------------------------------------------------------