├── .dictionary.txt ├── .dockerignore ├── .github ├── dependabot.yaml └── workflows │ ├── cancel_dupes.yml │ ├── deploy.yml │ ├── markdownlint.yml │ ├── on_pr.yaml │ ├── pre-commit-updates.yaml │ └── yamllint.yml ├── .gitignore ├── .pre-commit-config.yaml ├── BUILD.md ├── Cargo.lock ├── Cargo.toml ├── Dockerfile ├── Dockerfile.build_binary ├── Dockerfile.local ├── LICENSE ├── README.md ├── acars_router ├── README.md ├── acars_router.py ├── internals.drawio ├── internals.drawio.svg ├── requirements.txt └── viewacars ├── pre-commit-hooks.MD ├── rootfs ├── etc │ └── s6-overlay │ │ ├── s6-rc.d │ │ ├── acars_router │ │ │ ├── dependencies.d │ │ │ │ └── base │ │ │ ├── run │ │ │ └── type │ │ └── user │ │ │ └── contents.d │ │ │ └── acars_router │ │ └── scripts │ │ └── acars_router └── rename_current_arch_binary.sh ├── rust ├── bin │ └── acars_router │ │ ├── Cargo.toml │ │ └── src │ │ └── main.rs └── libraries │ ├── acars_config │ ├── Cargo.toml │ └── src │ │ ├── lib.rs │ │ └── sanity_checker.rs │ └── acars_connection_manager │ ├── Cargo.toml │ └── src │ ├── lib.rs │ ├── message_handler.rs │ ├── packet_handler.rs │ ├── service_init.rs │ ├── tcp_services.rs │ ├── udp_services.rs │ └── zmq_services.rs └── test_data ├── acars ├── acars_other ├── clean_up_after_test.sh ├── data_feeder_test_receiver_tcp.py ├── data_feeder_test_sender_tcp.py ├── data_feeder_test_udp.py ├── data_feeder_test_zmq.py ├── run_acars_ruster_test.sh ├── send_lines.sh ├── test_tcplisten_tcpsend.sh ├── test_tcpreceive_tcpserve.sh ├── test_udp.sh ├── test_udp_zmqserver.sh ├── vdlm2 └── vdlm2_other /.dictionary.txt: -------------------------------------------------------------------------------- 1 | crate 2 | -------------------------------------------------------------------------------- /.dockerignore: -------------------------------------------------------------------------------- 1 | .git 2 | .gitignore 3 | .github 4 | .gitattributes 5 | READMETEMPLATE.md 6 | README.md 7 | target 8 | -------------------------------------------------------------------------------- /.github/dependabot.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | version: 2 3 | 4 | updates: 5 | # Maintain dependencies for Docker 6 | - package-ecosystem: "docker" 7 | directory: "/" 8 | schedule: 9 | interval: "weekly" 10 | day: "saturday" 11 | time: "00:00" 12 | timezone: "Etc/UTC" 13 | assignees: 14 | - "fredclausen" 15 | 16 | # Maintain dependencies for GitHub Actions 17 | - package-ecosystem: "github-actions" 18 | directory: "/" 19 | schedule: 20 | interval: "weekly" 21 | day: "saturday" 22 | time: "00:00" 23 | timezone: "Etc/UTC" 24 | assignees: 25 | - "fredclausen" 26 | 27 | # Maintain pip packages for acars_router 28 | - package-ecosystem: "cargo" 29 | directory: "/" 30 | schedule: 31 | interval: "weekly" 32 | day: "saturday" 33 | time: "00:00" 34 | timezone: "Etc/UTC" 35 | assignees: 36 | - "fredclausen" 37 | -------------------------------------------------------------------------------- /.github/workflows/cancel_dupes.yml: -------------------------------------------------------------------------------- 1 | name: Cancelling Duplicates 2 | on: 3 | workflow_run: 4 | workflows: 5 | - "Deploy" 6 | types: ["requested"] 7 | 8 | jobs: 9 | cancel-duplicate-workflow-runs: 10 | name: "Cancel duplicate workflow runs" 11 | runs-on: ubuntu-latest 12 | steps: 13 | - uses: potiuk/cancel-workflow-runs@master 14 | name: "Cancel duplicate workflow runs" 15 | with: 16 | cancelMode: allDuplicates 17 | token: ${{ secrets.GITHUB_TOKEN }} 18 | sourceRunId: ${{ github.event.workflow_run.id }} 19 | -------------------------------------------------------------------------------- /.github/workflows/deploy.yml: -------------------------------------------------------------------------------- 1 | --- 2 | name: Deploy 3 | 4 | on: 5 | workflow_dispatch: 6 | inputs: 7 | reason: 8 | required: true 9 | description: "Reason for running this workflow" 10 | use_test_image: 11 | required: false 12 | type: boolean 13 | description: "Use base image testpr" 14 | default: false 15 | build_latest_as_test: 16 | required: false 17 | type: boolean 18 | description: "Build latest as test" 19 | default: false 20 | push: 21 | branches: 22 | - main 23 | 24 | paths: 25 | - "rust/**" 26 | - "Dockerfile" 27 | - "Dockerfile.build_binary" 28 | - "rootfs/**" 29 | 30 | jobs: 31 | workflow-dispatch: 32 | name: Triggered via Workflow Dispatch? 33 | # only run this step if workflow dispatch triggered 34 | # log the reason the workflow dispatch was triggered 35 | if: | 36 | github.event_name == 'workflow_dispatch' && 37 | github.event.inputs.reason != '' 38 | runs-on: ubuntu-22.04 39 | steps: 40 | - name: Log dispatch reason 41 | env: 42 | INPUTS_REASON: ${{ github.event.inputs.reason }} 43 | INPUTS_USE_TEST_IMAGE: ${{ github.event.inputs.use_test_image }} 44 | INPUTS_BUILD_LATEST_AS_TEST: ${{ github.event.inputs.build_latest_as_test }} 45 | run: | 46 | echo "Workflow dispatch reason: $INPUTS_REASON" 47 | echo "Use test image: $INPUTS_USE_TEST_IMAGE" 48 | echo "Build latest as test: $INPUTS_BUILD_LATEST_AS_TEST" 49 | 50 | binary_build_armv7: 51 | name: Build Binary - armv7 52 | runs-on: ubuntu-22.04 53 | # needs: test_rust_functionality 54 | 55 | steps: 56 | - name: Checkout 57 | uses: actions/checkout@v4.2.2 58 | with: 59 | fetch-depth: 0 60 | 61 | - name: Run Docker on tmpfs 62 | uses: JonasAlfredsson/docker-on-tmpfs@v1.0.1 63 | with: 64 | tmpfs_size: 5 65 | swap_size: 4 66 | swap_location: "/mnt/swapfile" 67 | 68 | - name: Set up QEMU 69 | uses: docker/setup-qemu-action@v3.6.0 70 | 71 | - name: Set up Docker Buildx 72 | uses: docker/setup-buildx-action@v3.10.0 73 | 74 | - name: Build armv7 75 | uses: docker/build-push-action@v6.17.0 76 | with: 77 | context: . 78 | push: false 79 | file: Dockerfile.build_binary 80 | tags: acars_router:armv7 81 | platforms: linux/arm/v7 82 | outputs: type=local,dest=./image_armv7/ 83 | 84 | - name: Upload artifact armv7 binary 85 | uses: actions/upload-artifact@v4.6.2 86 | with: 87 | name: acars_router.armv7 88 | path: ./image_armv7/acars_router 89 | 90 | binary_build_arm64: 91 | name: Build Binary - arm64 92 | runs-on: ubuntu-22.04 93 | # needs: test_rust_functionality 94 | 95 | steps: 96 | - name: Checkout 97 | uses: actions/checkout@v4.2.2 98 | with: 99 | fetch-depth: 0 100 | 101 | - name: Run Docker on tmpfs 102 | uses: JonasAlfredsson/docker-on-tmpfs@v1.0.1 103 | with: 104 | tmpfs_size: 5 105 | swap_size: 4 106 | swap_location: "/mnt/swapfile" 107 | 108 | - name: Set up QEMU 109 | uses: docker/setup-qemu-action@v3.6.0 110 | 111 | - name: Set up Docker Buildx 112 | uses: docker/setup-buildx-action@v3.10.0 113 | 114 | - name: Build arm64 115 | uses: docker/build-push-action@v6.17.0 116 | with: 117 | context: . 118 | push: false 119 | file: Dockerfile.build_binary 120 | tags: acars_router:arm64 121 | platforms: linux/arm64 122 | outputs: type=local,dest=./image_arm64/ 123 | 124 | - name: Upload artifact arm64 binary 125 | uses: actions/upload-artifact@v4.6.2 126 | with: 127 | name: acars_router.arm64 128 | path: ./image_arm64/acars_router 129 | 130 | binary_build_amd64: 131 | name: Build Binary - amd64 132 | runs-on: ubuntu-22.04 133 | # needs: test_rust_functionality 134 | 135 | steps: 136 | - name: Checkout 137 | uses: actions/checkout@v4.2.2 138 | with: 139 | fetch-depth: 0 140 | 141 | - name: Run Docker on tmpfs 142 | uses: JonasAlfredsson/docker-on-tmpfs@v1.0.1 143 | with: 144 | tmpfs_size: 5 145 | swap_size: 4 146 | swap_location: "/mnt/swapfile" 147 | 148 | - name: Set up QEMU 149 | uses: docker/setup-qemu-action@v3.6.0 150 | 151 | - name: Set up Docker Buildx 152 | uses: docker/setup-buildx-action@v3.10.0 153 | 154 | - name: Build amd64 155 | uses: docker/build-push-action@v6.17.0 156 | with: 157 | context: . 158 | push: false 159 | file: Dockerfile.build_binary 160 | tags: acars_router:amd64 161 | platforms: linux/amd64 162 | outputs: type=local,dest=./image_amd64/ 163 | 164 | - name: Upload artifact amd64 binary 165 | uses: actions/upload-artifact@v4.6.2 166 | with: 167 | name: acars_router.amd64 168 | path: ./image_amd64/acars_router 169 | 170 | consolidate_binaries: 171 | name: Consolidate & Cache Binaries 172 | runs-on: ubuntu-22.04 173 | needs: [binary_build_amd64, binary_build_arm64, binary_build_armv7] 174 | steps: 175 | - run: mkdir -p ./bin 176 | 177 | - uses: actions/download-artifact@v4.3.0 178 | with: 179 | name: acars_router.amd64 180 | path: ./bin/acars_router.amd64 181 | 182 | - uses: actions/download-artifact@v4.3.0 183 | with: 184 | name: acars_router.armv7 185 | path: ./bin/acars_router.armv7 186 | 187 | - uses: actions/download-artifact@v4.3.0 188 | with: 189 | name: acars_router.arm64 190 | path: ./bin/acars_router.arm64 191 | 192 | - run: ls -la ./bin/* 193 | 194 | - name: Cache Binaries 195 | uses: actions/cache@v4.2.3 196 | with: 197 | path: ./bin/ 198 | key: ${{ github.run_id }} 199 | 200 | release_binaries: 201 | name: Release Binaries 202 | if: | 203 | (github.event.inputs.build_latest_as_test == 'false' || 204 | github.event.inputs.build_latest_as_test == '') && 205 | (github.event.inputs.use_test_image == 'false' || github.event.inputs.use_test_image == '') 206 | needs: 207 | [ 208 | binary_build_amd64, 209 | binary_build_arm64, 210 | binary_build_armv7, 211 | consolidate_binaries, 212 | ] 213 | runs-on: ubuntu-22.04 214 | steps: 215 | - name: Checkout 216 | uses: actions/checkout@v4.2.2 217 | with: 218 | fetch-depth: 0 219 | 220 | - name: Cache cargo build output 221 | id: get_cache 222 | uses: actions/cache@v4.2.3 223 | with: 224 | path: ./bin/ 225 | key: ${{ github.run_id }} 226 | 227 | - name: Prepare binary release tarballs 228 | run: | 229 | ORIGDIR=$(pwd) 230 | # Make release tarballs 231 | mkdir -vp ./release 232 | pushd ./bin 233 | tar cJvf "$ORIGDIR/release/acars_router.amd64.tar.xz" ./acars_router.amd64 234 | tar cJvf "$ORIGDIR/release/acars_router.armv7.tar.xz" ./acars_router.armv7 235 | tar cJvf "$ORIGDIR/release/acars_router.arm64.tar.xz" ./acars_router.arm64 236 | popd 237 | 238 | - name: Get binary version from Cargo.toml 239 | if: steps.get_cache.outputs.cache-hit == 'true' 240 | id: release_version 241 | run: | 242 | # Get version from Cargo.toml 243 | RELEASE_VERSION=$(cat ./Cargo.toml | grep '\[workspace.package\]' -A9999 | grep -m 1 'version = ' | tr -d " " | tr -d '"' | tr -d "'" | cut -d = -f 2) 244 | echo "RELEASE_VERSION=$RELEASE_VERSION" >> $GITHUB_OUTPUT 245 | 246 | - name: Create binary release 247 | uses: ncipollo/release-action@v1.16.0 248 | with: 249 | body: "See Commits" 250 | allowUpdates: true 251 | commit: ${{ github.ref }} 252 | name: ${{ steps.release_version.outputs.RELEASE_VERSION }} Build ${{ github.run_number }} 253 | tag: ${{ steps.release_version.outputs.RELEASE_VERSION }} 254 | token: ${{ secrets.GITHUB_TOKEN }} 255 | 256 | deploy: 257 | name: Deploy 258 | if: | 259 | github.event.inputs.build_latest_as_test == 'false' || 260 | github.event.inputs.build_latest_as_test == '' 261 | needs: [consolidate_binaries] 262 | uses: sdr-enthusiasts/common-github-workflows/.github/workflows/build_and_push_image.yml@main 263 | with: 264 | push_enabled: true 265 | push_destinations: ghcr.io 266 | ghcr_repo_owner: ${{ github.repository_owner }} 267 | ghcr_repo: ${{ github.repository }} 268 | build_with_tmpfs: true 269 | get_version_method: cargo_toml_file_in_repo:file=/Cargo.toml 270 | cache_enabled: true 271 | cache_path: ./bin/ 272 | cache_key: ${{ github.run_id }} 273 | # set build_latest to true if github.event.inputs.use_test_image is false 274 | build_latest: ${{ github.event.inputs.use_test_image == 'false' || github.event.inputs.use_test_image == '' }} 275 | build_baseimage_test: ${{ github.event.inputs.use_test_image == 'true' }} 276 | # only build the entire stack if we are not using the test image 277 | build_version_specific: false 278 | build_platform_specific: false 279 | build_nohealthcheck: false 280 | build_baseimage_url: :base/:base-test-pr 281 | secrets: 282 | ghcr_token: ${{ secrets.GITHUB_TOKEN }} 283 | 284 | deploy_test: 285 | name: Deploy as test 286 | if: | 287 | github.event.inputs.build_latest_as_test == 'true' && 288 | (github.event.inputs.use_test_image == 'false' || github.event.inputs.use_test_image == '') 289 | needs: [consolidate_binaries] 290 | uses: sdr-enthusiasts/common-github-workflows/.github/workflows/build_and_push_image.yml@main 291 | with: 292 | push_enabled: true 293 | push_destinations: ghcr.io 294 | ghcr_repo_owner: ${{ github.repository_owner }} 295 | ghcr_repo: ${{ github.repository }} 296 | build_with_tmpfs: true 297 | get_version_method: cargo_toml_file_in_repo:file=/Cargo.toml 298 | cache_enabled: true 299 | cache_path: ./bin/ 300 | cache_key: ${{ github.run_id }} 301 | # set build_latest to true if github.event.inputs.use_test_image is false 302 | build_latest: true 303 | docker_latest_tag: test 304 | build_baseimage_test: false 305 | # only build the entire stack if we are not using the test image 306 | build_version_specific: false 307 | build_platform_specific: false 308 | build_nohealthcheck: false 309 | build_baseimage_url: :base/:base-test-pr 310 | secrets: 311 | ghcr_token: ${{ secrets.GITHUB_TOKEN }} 312 | -------------------------------------------------------------------------------- /.github/workflows/markdownlint.yml: -------------------------------------------------------------------------------- 1 | --- 2 | name: Linting (Markdown) 3 | 4 | on: 5 | pull_request: 6 | branches: 7 | - main 8 | # only run these if markdown files are updated 9 | paths: 10 | - "**.md" 11 | - "**.MD" 12 | 13 | jobs: 14 | markdownlint: 15 | name: Run markdownlint against markdown files 16 | runs-on: ubuntu-latest 17 | 18 | steps: 19 | - uses: actions/checkout@v4.2.2 20 | - name: Pull markdownlint/markdownlint:latest Image 21 | run: docker pull markdownlint/markdownlint:latest 22 | - name: Run markdownlint against *.md files 23 | run: docker run --rm -i -v "$(pwd)":/workdir --workdir /workdir markdownlint/markdownlint:latest --rules ~MD013,~MD033,~MD026,~MD002,~MD022,~MD007 $(find . -type f -iname '*.md' | grep -v '/.git/') 24 | -------------------------------------------------------------------------------- /.github/workflows/on_pr.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | name: Pull Request 3 | 4 | on: 5 | # Enable manual running of action if necessary 6 | workflow_dispatch: 7 | # Build and test deployment the image on pushes to main branch 8 | pull_request: 9 | # # Only publish on push to main branch 10 | # branches: 11 | # - main 12 | # Only run if the PR yaml, Dockerfile, sh, py or rs files have changed 13 | paths: 14 | - Dockerfile** 15 | - "**on_pr.yaml" 16 | - "**.py" 17 | - "**.rs" 18 | - "**.sh" 19 | - "**.toml" 20 | 21 | jobs: 22 | check: 23 | name: Check 24 | runs-on: ubuntu-22.04 25 | steps: 26 | - uses: actions/checkout@v4.2.2 27 | - uses: actions-rs/toolchain@v1.0.7 28 | with: 29 | profile: minimal 30 | toolchain: stable 31 | override: true 32 | - uses: actions-rs/cargo@v1.0.3 33 | with: 34 | command: check 35 | 36 | fmt: 37 | name: Rustfmt 38 | runs-on: ubuntu-22.04 39 | steps: 40 | - uses: actions/checkout@v4.2.2 41 | - uses: actions-rs/toolchain@v1.0.7 42 | with: 43 | profile: minimal 44 | toolchain: stable 45 | override: true 46 | - run: rustup component add rustfmt 47 | - uses: actions-rs/cargo@v1.0.3 48 | with: 49 | command: fmt 50 | args: --all -- --check 51 | 52 | clippy: 53 | name: Clippy 54 | runs-on: ubuntu-22.04 55 | steps: 56 | - uses: actions/checkout@v4.2.2 57 | - uses: actions-rs/toolchain@v1.0.7 58 | with: 59 | toolchain: stable 60 | components: clippy 61 | override: true 62 | - uses: actions-rs/clippy-check@v1.0.7 63 | with: 64 | token: ${{ secrets.GITHUB_TOKEN }} 65 | args: --all-features 66 | name: Clippy Output 67 | 68 | hadolint: 69 | name: "Linting: hadolint" 70 | runs-on: ubuntu-22.04 71 | steps: 72 | - uses: actions/checkout@v4.2.2 73 | - name: Pull hadolint/hadolint:latest Image 74 | run: docker pull hadolint/hadolint:latest 75 | - name: Run hadolint against Dockerfiles 76 | run: docker run --rm -i -v "$PWD":/workdir --workdir /workdir --entrypoint hadolint hadolint/hadolint --ignore DL3013 --ignore DL3008 $(find . -type f -iname "Dockerfile*") 77 | 78 | test_rust_functionality: 79 | name: Build and test rust functionality 80 | runs-on: ubuntu-22.04 81 | 82 | steps: 83 | - name: Checkout 84 | uses: actions/checkout@v4.2.2 85 | with: 86 | fetch-depth: 0 87 | 88 | - name: Install Rust and deps 89 | run: | 90 | curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y 91 | sudo apt-get update 92 | sudo apt-get install -y --no-install-recommends libzmq3-dev 93 | python3 -m pip install zmq 94 | 95 | - name: Get binary version from Cargo.toml 96 | id: release_version 97 | run: | 98 | # Get version from Cargo.toml 99 | RELEASE_VERSION=$(cat ./rust/bin/acars_router/Cargo.toml | grep '\[package\]' -A9999 | grep -m 1 'version = ' | tr -d " " | tr -d '"' | tr -d "'" | cut -d = -f 2) 100 | echo "$RELEASE_VERSION" 101 | 102 | - name: Run tests 103 | run: | 104 | cd test_data 105 | ./run_acars_ruster_test.sh 106 | 107 | binary_build_armv7: 108 | name: Build Binary - armv7 109 | runs-on: ubuntu-22.04 110 | # needs: test_rust_functionality 111 | 112 | steps: 113 | - name: Checkout 114 | uses: actions/checkout@v4.2.2 115 | with: 116 | fetch-depth: 0 117 | 118 | - name: Run Docker on tmpfs 119 | uses: JonasAlfredsson/docker-on-tmpfs@v1.0.1 120 | with: 121 | tmpfs_size: 5 122 | swap_size: 4 123 | swap_location: "/mnt/swapfile" 124 | 125 | - name: Set up QEMU 126 | uses: docker/setup-qemu-action@v3.6.0 127 | 128 | - name: Set up Docker Buildx 129 | uses: docker/setup-buildx-action@v3.10.0 130 | 131 | - name: Build armv7 132 | uses: docker/build-push-action@v6.17.0 133 | with: 134 | context: . 135 | push: false 136 | file: Dockerfile.build_binary 137 | tags: acars_router:armv7 138 | platforms: linux/arm/v7 139 | outputs: type=local,dest=./image_armv7/ 140 | 141 | - name: Upload artifact armv7 binary 142 | uses: actions/upload-artifact@v4.6.2 143 | with: 144 | name: acars_router.armv7 145 | path: ./image_armv7/acars_router 146 | 147 | binary_build_arm64: 148 | name: Build Binary - arm64 149 | runs-on: ubuntu-22.04 150 | # needs: test_rust_functionality 151 | 152 | steps: 153 | - name: Checkout 154 | uses: actions/checkout@v4.2.2 155 | with: 156 | fetch-depth: 0 157 | 158 | - name: Run Docker on tmpfs 159 | uses: JonasAlfredsson/docker-on-tmpfs@v1.0.1 160 | with: 161 | tmpfs_size: 5 162 | swap_size: 4 163 | swap_location: "/mnt/swapfile" 164 | 165 | - name: Set up QEMU 166 | uses: docker/setup-qemu-action@v3.6.0 167 | 168 | - name: Set up Docker Buildx 169 | uses: docker/setup-buildx-action@v3.10.0 170 | 171 | - name: Build arm64 172 | uses: docker/build-push-action@v6.17.0 173 | with: 174 | context: . 175 | push: false 176 | file: Dockerfile.build_binary 177 | tags: acars_router:arm64 178 | platforms: linux/arm64 179 | outputs: type=local,dest=./image_arm64/ 180 | 181 | - name: Upload artifact arm64 binary 182 | uses: actions/upload-artifact@v4.6.2 183 | with: 184 | name: acars_router.arm64 185 | path: ./image_arm64/acars_router 186 | 187 | binary_build_amd64: 188 | name: Build Binary - amd64 189 | runs-on: ubuntu-22.04 190 | needs: test_rust_functionality 191 | 192 | steps: 193 | - name: Checkout 194 | uses: actions/checkout@v4.2.2 195 | with: 196 | fetch-depth: 0 197 | 198 | - name: Run Docker on tmpfs 199 | uses: JonasAlfredsson/docker-on-tmpfs@v1.0.1 200 | with: 201 | tmpfs_size: 5 202 | swap_size: 4 203 | swap_location: "/mnt/swapfile" 204 | 205 | - name: Set up QEMU 206 | uses: docker/setup-qemu-action@v3.6.0 207 | 208 | - name: Set up Docker Buildx 209 | uses: docker/setup-buildx-action@v3.10.0 210 | 211 | - name: Build amd64 212 | uses: docker/build-push-action@v6.17.0 213 | with: 214 | context: . 215 | push: false 216 | file: Dockerfile.build_binary 217 | tags: acars_router:amd64 218 | platforms: linux/amd64 219 | outputs: type=local,dest=./image_amd64/ 220 | 221 | - name: Upload artifact amd64 binary 222 | uses: actions/upload-artifact@v4.6.2 223 | with: 224 | name: acars_router.amd64 225 | path: ./image_amd64/acars_router 226 | 227 | consolidate_binaries: 228 | name: Consolidate & Cache Binaries 229 | runs-on: ubuntu-22.04 230 | needs: [binary_build_amd64, binary_build_arm64, binary_build_armv7] 231 | steps: 232 | - run: mkdir -p ./bin 233 | 234 | - uses: actions/download-artifact@v4.3.0 235 | with: 236 | name: acars_router.amd64 237 | path: ./bin/acars_router.amd64 238 | 239 | - uses: actions/download-artifact@v4.3.0 240 | with: 241 | name: acars_router.armv7 242 | path: ./bin/acars_router.armv7 243 | 244 | - uses: actions/download-artifact@v4.3.0 245 | with: 246 | name: acars_router.arm64 247 | path: ./bin/acars_router.arm64 248 | 249 | - run: ls -la ./bin/* 250 | 251 | - name: Cache Binaries 252 | uses: actions/cache@v4.2.3 253 | with: 254 | path: ./bin/ 255 | key: ${{ github.run_id }} 256 | 257 | test_docker_image_build: 258 | name: Test Docker Image Build 259 | needs: [hadolint, consolidate_binaries, test_rust_functionality] 260 | uses: sdr-enthusiasts/common-github-workflows/.github/workflows/build_and_push_image.yml@main 261 | with: 262 | get_version_method: cargo_toml_file_in_repo:file=/Cargo.toml 263 | build_with_tmpfs: true 264 | build_nohealthcheck: false 265 | cache_enabled: true 266 | cache_path: ./bin/ 267 | cache_key: ${{ github.run_id }} 268 | -------------------------------------------------------------------------------- /.github/workflows/pre-commit-updates.yaml: -------------------------------------------------------------------------------- 1 | name: Update pre-commit hooks 2 | 3 | on: 4 | workflow_dispatch: 5 | schedule: 6 | - cron: 0 0 * * 0 7 | 8 | jobs: 9 | pre-commit-update: 10 | runs-on: ubuntu-latest 11 | name: Updates 12 | steps: 13 | - name: Checkout 14 | uses: actions/checkout@v4.2.2 15 | - name: Update pre-commit hooks 16 | uses: brokenpip3/action-pre-commit-update@0.0.2 17 | with: 18 | github-token: ${{ secrets.GITHUB_TOKEN }} 19 | -------------------------------------------------------------------------------- /.github/workflows/yamllint.yml: -------------------------------------------------------------------------------- 1 | --- 2 | name: Linting (YAML) 3 | 4 | on: 5 | pull_request: 6 | branches: 7 | - main 8 | # only run when yaml files are updated 9 | paths: 10 | - "**.yml" 11 | - "**.yaml" 12 | 13 | jobs: 14 | yamllint: 15 | name: Run yamllint against YAML files 16 | runs-on: ubuntu-latest 17 | steps: 18 | - uses: actions/checkout@v4.2.2 19 | - name: yaml-lint 20 | uses: ibiqlik/action-yamllint@v3.1.1 21 | with: 22 | config_data: | 23 | extends: default 24 | rules: 25 | line-length: 26 | max: 120 27 | level: warning 28 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # File created using '.gitignore Generator' for Visual Studio Code: https://bit.ly/vscode-gig 2 | 3 | # Created by https://www.gitignore.io/api/visualstudiocode,python,macos,virtualenv 4 | # Edit at https://www.gitignore.io/?templates=visualstudiocode,python,macos,virtualenv 5 | 6 | ### macOS ### 7 | # General 8 | .DS_Store 9 | .AppleDouble 10 | .LSOverride 11 | 12 | # Icon must end with two \r 13 | Icon 14 | 15 | # Thumbnails 16 | ._* 17 | 18 | # Files that might appear in the root of a volume 19 | .DocumentRevisions-V100 20 | .fseventsd 21 | .Spotlight-V100 22 | .TemporaryItems 23 | .Trashes 24 | .VolumeIcon.icns 25 | .com.apple.timemachine.donotpresent 26 | 27 | # Directories potentially created on remote AFP share 28 | .AppleDB 29 | .AppleDesktop 30 | Network Trash Folder 31 | Temporary Items 32 | .apdisk 33 | 34 | ### Python ### 35 | # Byte-compiled / optimized / DLL files 36 | __pycache__/ 37 | *.py[cod] 38 | *$py.class 39 | 40 | # C extensions 41 | *.so 42 | 43 | # Distribution / packaging 44 | .Python 45 | build/ 46 | develop-eggs/ 47 | dist/ 48 | downloads/ 49 | eggs/ 50 | .eggs/ 51 | lib/ 52 | lib64/ 53 | parts/ 54 | sdist/ 55 | var/ 56 | wheels/ 57 | pip-wheel-metadata/ 58 | share/python-wheels/ 59 | *.egg-info/ 60 | .installed.cfg 61 | *.egg 62 | MANIFEST 63 | 64 | # PyInstaller 65 | # Usually these files are written by a python script from a template 66 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 67 | *.manifest 68 | *.spec 69 | 70 | # Installer logs 71 | pip-log.txt 72 | pip-delete-this-directory.txt 73 | 74 | # Unit test / coverage reports 75 | htmlcov/ 76 | .tox/ 77 | .nox/ 78 | .coverage 79 | .coverage.* 80 | .cache 81 | nosetests.xml 82 | coverage.xml 83 | *.cover 84 | .hypothesis/ 85 | .pytest_cache/ 86 | 87 | # Translations 88 | *.mo 89 | *.pot 90 | 91 | # Scrapy stuff: 92 | .scrapy 93 | 94 | # Sphinx documentation 95 | docs/_build/ 96 | 97 | # PyBuilder 98 | target/ 99 | 100 | # pyenv 101 | .python-version 102 | 103 | # pipenv 104 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. 105 | # However, in case of collaboration, if having platform-specific dependencies or dependencies 106 | # having no cross-platform support, pipenv may install dependencies that don't work, or not 107 | # install all needed dependencies. 108 | #Pipfile.lock 109 | 110 | # celery beat schedule file 111 | celerybeat-schedule 112 | 113 | # SageMath parsed files 114 | *.sage.py 115 | 116 | # Spyder project settings 117 | .spyderproject 118 | .spyproject 119 | 120 | # Rope project settings 121 | .ropeproject 122 | 123 | # Mr Developer 124 | .mr.developer.cfg 125 | .project 126 | .pydevproject 127 | 128 | # mkdocs documentation 129 | /site 130 | 131 | # mypy 132 | .mypy_cache/ 133 | .dmypy.json 134 | dmypy.json 135 | 136 | # Pyre type checker 137 | .pyre/ 138 | 139 | ### VirtualEnv ### 140 | # Virtualenv 141 | # http://iamzed.com/2009/05/07/a-primer-on-virtualenv/ 142 | pyvenv.cfg 143 | .env 144 | .venv 145 | env/ 146 | venv/ 147 | ENV/ 148 | env.bak/ 149 | venv.bak/ 150 | pip-selfcheck.json 151 | 152 | ### VisualStudioCode ### 153 | .vscode/* 154 | !.vscode/settings.json 155 | !.vscode/tasks.json 156 | !.vscode/launch.json 157 | !.vscode/extensions.json 158 | 159 | ### VisualStudioCode Patch ### 160 | # Ignore all local history of files 161 | .history 162 | 163 | # End of https://www.gitignore.io/api/visualstudiocode,python,macos,virtualenv 164 | 165 | # Custom rules (everything added below won't be overriden by 'Generate .gitignore File' if you use 'Update' option) 166 | 167 | .vscode/* 168 | 169 | # ignore patched test files 170 | test_data/*.patched 171 | 172 | # Added by cargo 173 | 174 | /target 175 | .idea 176 | -------------------------------------------------------------------------------- /.pre-commit-config.yaml: -------------------------------------------------------------------------------- 1 | repos: 2 | # lint yaml, line and whitespace 3 | - repo: https://github.com/pre-commit/pre-commit-hooks 4 | rev: cef0300fd0fc4d2a87a85fa2093c6b283ea36f4b # frozen: v5.0.0 5 | hooks: 6 | - id: check-yaml 7 | - id: end-of-file-fixer 8 | - id: trailing-whitespace 9 | - id: requirements-txt-fixer 10 | - id: mixed-line-ending 11 | - id: check-executables-have-shebangs 12 | - id: check-shebang-scripts-are-executable 13 | 14 | # lint the dockerfiles 15 | - repo: https://github.com/hadolint/hadolint 16 | rev: c3dc18df7a501f02a560a2cc7ba3c69a85ca01d3 # frozen: v2.13.1-beta 17 | hooks: 18 | - id: hadolint 19 | 20 | # prettier 21 | - repo: https://github.com/pre-commit/mirrors-prettier 22 | rev: "f12edd9c7be1c20cfa42420fd0e6df71e42b51ea" # frozen: v4.0.0-alpha.8 23 | hooks: 24 | - id: prettier 25 | types_or: [file, bash, sh, javascript, jsx, ts, tsx] 26 | additional_dependencies: 27 | - prettier@2.5.1 28 | exclude: ^(Dockerfile*) 29 | 30 | - repo: https://github.com/codespell-project/codespell.git 31 | rev: "63c8f8312b7559622c0d82815639671ae42132ac" # frozen: v2.4.1 32 | hooks: 33 | - id: codespell 34 | types: [text] 35 | args: [--ignore-words=.dictionary.txt] 36 | exclude: ^(Dockerfile*) 37 | 38 | - repo: https://github.com/shellcheck-py/shellcheck-py 39 | rev: a23f6b85d0fdd5bb9d564e2579e678033debbdff # frozen: v0.10.0.1 40 | hooks: 41 | - id: shellcheck 42 | - repo: https://github.com/sirosen/check-jsonschema 43 | rev: 06e4cc849d03f3a59ca223a4046f4bb5bb2aba6d # frozen: 0.33.0 44 | hooks: 45 | - id: check-github-actions 46 | - id: check-github-workflows 47 | 48 | - repo: https://github.com/doublify/pre-commit-rust 49 | rev: eeee35a89e69d5772bdee97db1a6a898467b686e # frozen: v1.0 50 | hooks: 51 | - id: fmt 52 | - id: cargo-check 53 | 54 | # lint python formatting 55 | - repo: https://github.com/psf/black 56 | rev: 8a737e727ac5ab2f1d4cf5876720ed276dc8dc4b # frozen: 25.1.0 57 | hooks: 58 | - id: black 59 | exclude: ^(acars_router/) 60 | 61 | - repo: https://github.com/pycqa/flake8 62 | rev: "4b5e89b4b108a6c1a000c591d334a99a80d34c7b" # frozen: 7.2.0 63 | hooks: 64 | - id: flake8 65 | args: ["--extend-ignore=W503,W504,E501"] 66 | exclude: ^(acars_router/) 67 | -------------------------------------------------------------------------------- /BUILD.md: -------------------------------------------------------------------------------- 1 | # How to build the project 2 | 3 | ## Docker 4 | 5 | Building with Docker is super easy. 6 | 7 | ```shell 8 | docker build -f Dockerfile.local . -t name, organization, or whatever you like < your > /acars_router:test 9 | ``` 10 | 11 | And the project should build with no issues. 12 | 13 | ## Building acars_router from source 14 | 15 | If you desire to build `acars_router` from source, ensure you have [rust](https://www.rust-lang.org/tools/install) installed and up to date. `acars_router` build target is always the most recent Rust version, so you should not have to roll back to previous versions to get it built. 16 | 17 | Once you clone the repository and enter the repo's directory using a shell... 18 | 19 | Debugging: 20 | 21 | ```shell 22 | cargo build 23 | ``` 24 | 25 | Release 26 | 27 | ```shell 28 | cargo build --release 29 | ``` 30 | 31 | To run the project using cargo (useful for debugging): 32 | 33 | ```shell 34 | 35 | cargo run -- 36 | ``` 37 | 38 | Or you can directly run/find the binary in the `target` directory. 39 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [workspace] 2 | members = [ 3 | "rust/bin/acars_router", 4 | "rust/libraries/acars_config", 5 | "rust/libraries/acars_connection_manager", 6 | ] 7 | resolver = "2" 8 | 9 | 10 | [workspace.package] 11 | edition = "2021" 12 | version = "1.3.1" 13 | authors = ["Fred Clausen", "Mike Nye", "Alex Austin"] 14 | description = "ACARS Router: A Utility to ingest ACARS/VDLM2/HFDL/IMSL/IRDM from many sources, process, and feed out to many consumers." 15 | documentation = "https://github.com/sdr-enthusiasts/acars_router" 16 | homepage = "https://github.com/sdr-enthusiasts/acars_router" 17 | repository = "https://github.com/sdr-enthusiasts/acars_router" 18 | readme = "README.md" 19 | license = "MIT" 20 | rust-version = "1.66.1" 21 | 22 | [workspace.dependencies] 23 | log = "0.4.27" 24 | tokio = { version = "1.45.0", features = ["full", "tracing"] } 25 | serde = { version = "1.0.219", features = ["derive"] } 26 | serde_json = "1.0.140" 27 | sdre-rust-logging = "0.3.18" 28 | clap = { version = "4.5.38", features = ["derive", "env"] } 29 | sdre-stubborn-io = "0.6.8" 30 | tokio-util = { version = "0.7.15", features = ["full"] } 31 | tokio-stream = "0.1.17" 32 | futures = "0.3.31" 33 | async-trait = "0.1.88" 34 | zmq = "0.10.0" 35 | tmq = "0.5.0" 36 | acars_vdlm2_parser = { git = "https://github.com/jcdeimos/acars_vdlm2_parser", version = "0.4.0" } 37 | #acars_vdlm2_parser = { git = "https://github.com/fredclausen/acars_vdlm2_parser", branch = "hfdl-and-dependency-updates" } 38 | acars_config = { path = "../acars_config" } 39 | #acars_vdlm2_parser = { git = "https://github.com/rpatel3001/acars_vdlm2_parser", branch = "add_imsl_irdm" } 40 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM ghcr.io/sdr-enthusiasts/docker-baseimage:base 2 | 3 | ENV AR_LISTEN_UDP_ACARS=5550 \ 4 | AR_LISTEN_TCP_ACARS=5550 \ 5 | AR_LISTEN_UDP_VDLM2=5555 \ 6 | AR_LISTEN_TCP_VDLM2=5555 \ 7 | AR_LISTEN_UDP_HFDL=5556 \ 8 | AR_LISTEN_TCP_HFDL=5556 \ 9 | AR_LISTEN_UDP_IMSL=5557 \ 10 | AR_LISTEN_TCP_IMSL=5557 \ 11 | AR_LISTEN_UDP_IRDM=5558 \ 12 | AR_LISTEN_TCP_IRDM=5558 \ 13 | AR_LISTEN_ZMQ_ACARS=35550 \ 14 | AR_LISTEN_ZMQ_VDLM2=35555 \ 15 | AR_LISTEN_ZMQ_HFDL=35556 \ 16 | AR_LISTEN_ZMQ_IMSL=35557 \ 17 | AR_LISTEN_ZMQ_IRDM=35558 \ 18 | AR_SERVE_TCP_ACARS=15550 \ 19 | AR_SERVE_TCP_VDLM2=15555 \ 20 | AR_SERVE_TCP_HFDL=15556 \ 21 | AR_SERVE_TCP_IMSL=15557 \ 22 | AR_SERVE_TCP_IRDM=15558 \ 23 | AR_SERVE_ZMQ_ACARS=45550 \ 24 | AR_SERVE_ZMQ_VDLM2=45555 \ 25 | AR_SERVE_ZMQ_HFDL=45556 \ 26 | AR_SERVE_ZMQ_IMSL=45557 \ 27 | AR_SERVE_ZMQ_IRDM=45558 \ 28 | AR_ADD_PROXY_ID=true \ 29 | AR_DISABLE_ACARS=false \ 30 | AR_DISABLE_VDLM2=false \ 31 | AR_DISABLE_HFDL=false \ 32 | AR_DISABLE_IMSL=false \ 33 | AR_DISABLE_IRDM=false 34 | 35 | SHELL ["/bin/bash", "-o", "pipefail", "-c"] 36 | COPY ./rootfs / 37 | COPY ./bin/acars_router.armv7/acars_router /opt/acars_router.armv7 38 | COPY ./bin/acars_router.arm64/acars_router /opt/acars_router.arm64 39 | COPY ./bin/acars_router.amd64/acars_router /opt/acars_router.amd64 40 | 41 | # hadolint ignore=DL3008,DL3003,SC1091 42 | RUN set -x && \ 43 | KEPT_PACKAGES=() && \ 44 | TEMP_PACKAGES=() && \ 45 | KEPT_PACKAGES+=(libzmq5) && \ 46 | apt-get update && \ 47 | apt-get install -y --no-install-recommends \ 48 | "${KEPT_PACKAGES[@]}" \ 49 | "${TEMP_PACKAGES[@]}"\ 50 | && \ 51 | # ensure binaries are executable 52 | chmod -v a+x \ 53 | /opt/acars_router.armv7 \ 54 | /opt/acars_router.arm64 \ 55 | /opt/acars_router.amd64 \ 56 | && \ 57 | # remove foreign architecture binaries 58 | /rename_current_arch_binary.sh && \ 59 | rm -fv \ 60 | /opt/acars_router.* \ 61 | && \ 62 | # clean up 63 | apt-get remove -y "${TEMP_PACKAGES[@]}" && \ 64 | apt-get autoremove -y && \ 65 | rm -rf /src/* /tmp/* /var/lib/apt/lists/* && \ 66 | # test 67 | /opt/acars_router --version 68 | -------------------------------------------------------------------------------- /Dockerfile.build_binary: -------------------------------------------------------------------------------- 1 | FROM rust:1.87.0 AS builder 2 | ENV CARGO_NET_GIT_FETCH_WITH_CLI=true 3 | WORKDIR /tmp/acars_router 4 | # hadolint ignore=DL3008,DL3003,SC1091,DL3009 5 | RUN set -x && \ 6 | apt-get update && \ 7 | apt-get install -y --no-install-recommends libzmq3-dev 8 | COPY . . 9 | 10 | RUN cargo build --release 11 | 12 | FROM scratch 13 | COPY --from=builder /tmp/acars_router/target/release/acars_router /acars_router 14 | -------------------------------------------------------------------------------- /Dockerfile.local: -------------------------------------------------------------------------------- 1 | FROM rust:1.87.0 as builder 2 | WORKDIR /tmp/acars_router 3 | # hadolint ignore=DL3008,DL3003,SC1091 4 | RUN set -x && \ 5 | apt-get update && \ 6 | apt-get install -y --no-install-recommends libzmq3-dev 7 | COPY . . 8 | 9 | RUN cargo build --release 10 | 11 | FROM ghcr.io/sdr-enthusiasts/docker-baseimage:base 12 | 13 | ENV AR_LISTEN_UDP_ACARS=5550 \ 14 | AR_LISTEN_TCP_ACARS=5550 \ 15 | AR_LISTEN_UDP_VDLM2=5555 \ 16 | AR_LISTEN_TCP_VDLM2=5555 \ 17 | AR_LISTEN_UDP_HFDL=5556 \ 18 | AR_LISTEN_TCP_HFDL=5556 \ 19 | AR_LISTEN_UDP_IMSL=5557 \ 20 | AR_LISTEN_TCP_IMSL=5557 \ 21 | AR_LISTEN_UDP_IRDM=5558 \ 22 | AR_LISTEN_TCP_IRDM=5558 \ 23 | AR_LISTEN_ZMQ_ACARS=35550 \ 24 | AR_LISTEN_ZMQ_VDLM2=35555 \ 25 | AR_LISTEN_ZMQ_HFDL=35556 \ 26 | AR_LISTEN_ZMQ_IMSL=35557 \ 27 | AR_LISTEN_ZMQ_IRDM=35558 \ 28 | AR_SERVE_TCP_ACARS=15550 \ 29 | AR_SERVE_TCP_VDLM2=15555 \ 30 | AR_SERVE_TCP_HFDL=15556 \ 31 | AR_SERVE_TCP_IMSL=15557 \ 32 | AR_SERVE_TCP_IRDM=15558 \ 33 | AR_SERVE_ZMQ_ACARS=45550 \ 34 | AR_SERVE_ZMQ_VDLM2=45555 \ 35 | AR_SERVE_ZMQ_HFDL=45556 \ 36 | AR_SERVE_ZMQ_IMSL=45557 \ 37 | AR_SERVE_ZMQ_IRDM=45558 \ 38 | AR_ADD_PROXY_ID=true \ 39 | AR_DISABLE_ACARS=false \ 40 | AR_DISABLE_VDLM2=false \ 41 | AR_DISABLE_HFDL=false \ 42 | AR_DISABLE_IMSL=false \ 43 | AR_DISABLE_IRDM=false 44 | 45 | SHELL ["/bin/bash", "-o", "pipefail", "-c"] 46 | COPY rootfs / 47 | COPY --from=builder /tmp/acars_router/target/release/acars_router /opt/acars_router 48 | # hadolint ignore=DL3008,DL3003,SC1091 49 | RUN set -x && \ 50 | KEPT_PACKAGES=() && \ 51 | TEMP_PACKAGES=() && \ 52 | KEPT_PACKAGES+=(libzmq5) && \ 53 | apt-get update && \ 54 | apt-get install -y --no-install-recommends \ 55 | "${KEPT_PACKAGES[@]}" \ 56 | "${TEMP_PACKAGES[@]}"\ 57 | && \ 58 | # ensure binaries are executable 59 | chmod -v a+x \ 60 | /opt/acars_router \ 61 | && \ 62 | # clean up 63 | apt-get remove -y "${TEMP_PACKAGES[@]}" && \ 64 | apt-get autoremove -y && \ 65 | rm -rf /src/* /tmp/* /var/lib/apt/lists/* 66 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022 SDR Enthusiasts 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 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # sdr-enthusiasts/acars_router 2 | 3 | `acars_router` receives, validates, deduplicates, modifies and routes ACARS/VDLM2/HFDL/IMSL/IRDM JSON messages. 4 | 5 | ## Example Feeder `docker-compose.yml` 6 | 7 | ```yaml 8 | volumes: 9 | - acarshub_run 10 | 11 | services: 12 | acarshub: 13 | image: ghcr.io/sdr-enthusiasts/docker-acarshub:latest 14 | container_name: acarshub 15 | restart: always 16 | ports: 17 | - 8080:80 18 | environment: 19 | - ENABLE_VDLM=EXTERNAL 20 | - ENABLE_ACARS=EXTERNAL 21 | - FEED=true 22 | - IATA_OVERRIDE=UP|UPS|United Parcel Service;GS|FTH|Mountain Aviation (Foothills);GS|EJA|ExecJet 23 | - ENABLE_ADSB=true 24 | - ADSB_LAT=${FEEDER_LAT} 25 | - ADSB_LON=${FEEDER_LON} 26 | - ADSB_URL=${ACARS_TAR_HOST} 27 | volumes: 28 | - "acars_run:/run/acars" 29 | tmpfs: 30 | - /database:exec,size=64M 31 | - /run:exec,size=64M 32 | - /var/log 33 | 34 | acars_router: 35 | image: ghcr.io/sdr-enthusiasts/acars_router:latest 36 | container_name: acars_router 37 | restart: always 38 | environment: 39 | - TZ=${FEEDER_TZ} 40 | - AR_ENABLE_DEDUPE=true 41 | - AR_SEND_UDP_ACARS=acarshub:5550;feed.airframes.io:5550 42 | - AR_SEND_UDP_VDLM2=acarshub:5555 43 | - AR_SEND_TCP_VDLM2=feed.airframes.io:5553 44 | - AR_SEND_TCP_HFDL=feed.airframes.io:5556 45 | - AR_RECV_ZMQ_VDLM2=dumpvdl2:45555 46 | tmpfs: 47 | - /run:exec,size=64M 48 | - /var/log 49 | ``` 50 | 51 | Change the `GAIN`, `FREQUENCIES`, `SERIAL`, `TZ`, `ADSB_LAT`, `ADSB_LON` and `AR_OVERRIDE_STATION_NAME` to suit your environment. 52 | 53 | With the above deployment: 54 | 55 | - JSON messages generated by `acarsdec` will be sent to `acars_router` via UDP. 56 | - `acars_router` will connect to `dumpvdl2` via TCP/ZMQ and receive JSON messages. 57 | - `acars_router` will set your station ID on all messages. 58 | - `acars_router` will then output to `acarshub`. 59 | 60 | ## Ports 61 | 62 | All ports are configurable. By default, the following ports will be used: 63 | 64 | | Port | Protocol | Description | 65 | | ------- | -------- | -------------------------------------------------------------------------------- | 66 | | `5550` | `UDP` | ACARS ingest. Clients will send ACARS data to this port via UDP. | 67 | | `5550` | `TCP` | ACARS ingest. Clients will send ACARS data to this port via TCP. | 68 | | `5555` | `UDP` | VDLM2 ingest. Clients will send VDLM2 data to this port via UDP. | 69 | | `5555` | `TCP` | VDLM2 ingest. Clients will send VDLM2 data to this port via TCP. | 70 | | `5556` | `UDP` | HFDL ingest. Clients will send HFDL data to this port via UDP. | 71 | | `5556` | `TCP` | HFDL ingest. Clients will send HFDL data to this port via TCP. | 72 | | `5557` | `UDP` | IMSL ingest. Clients will send IMSL data to this port via UDP. | 73 | | `5557` | `TCP` | IMSL ingest. Clients will send IMSL data to this port via TCP. | 74 | | `5558` | `UDP` | IRDM ingest. Clients will send IRDM data to this port via UDP. | 75 | | `5558` | `TCP` | IRDM ingest. Clients will send IRDM data to this port via TCP. | 76 | | `15550` | `TCP` | ACARS server. Can be used for other clients to connect and get messages | 77 | | `15555` | `TCP` | VDLM2 server. Can be used for other clients to connect and get messages | 78 | | `15556` | `TCP` | HFDL server. Can be used for other clients to connect and get messages | 79 | | `15557` | `TCP` | IMSL server. Can be used for other clients to connect and get messages | 80 | | `15558` | `TCP` | IRDM server. Can be used for other clients to connect and get messages | 81 | | `35550` | `ZMQ` | ACARS ingest. Clients will connect to this port and send ACARS messages over ZMQ | 82 | | `35555` | `ZMQ` | VDLM2 ingest. Clients will connect to this port and send VDLM2 messages over ZMQ | 83 | | `35556` | `ZMQ` | HFDL ingest. Clients will connect to this port and send HFDL messages over ZMQ | 84 | | `35557` | `ZMQ` | IMSL ingest. Clients will connect to this port and send IMSL messages over ZMQ | 85 | | `35558` | `ZMQ` | IRDM ingest. Clients will connect to this port and send IRDM messages over ZMQ | 86 | | `45550` | `ZMQ` | ACARS server. Can be used for other ZMQ clients to connect and get messages | 87 | | `45555` | `ZMQ` | VDLM2 server. Can be used for other ZMQ clients to connect and get messages | 88 | | `45556` | `ZMQ` | HFDL server. Can be used for other ZMQ clients to connect and get messages | 89 | | `45557` | `ZMQ` | IMSL server. Can be used for other ZMQ clients to connect and get messages | 90 | | `45558` | `ZMQ` | IRDM server. Can be used for other ZMQ clients to connect and get messages | 91 | 92 | If you want any port(s) to be exposed outside of the docker network, please be sure to append them to the ports section of the `docker-compose.yml` file. 93 | 94 | ## Environment Variables 95 | 96 | All env variables with `SEND` or `RECV` in them can have multiple destinations. Each destination should be separated by a `;`. For example, `SEND_UDP_ACARS=acarshub:5550;acarshub2:5550` will send UDP ACARS messages to both `acarshub` and `acarshub2`. 97 | 98 | The nomenclature for the environment variables is as follows: 99 | 100 | ### Nomenclature used in variable naming 101 | 102 | #### Input/Inbound data 103 | 104 | - Receiver: ACARS router will connect out to a remote host and receive data from it. (TCP/ZMQ) 105 | - Listener: ACARS router will listen on a port for incoming data (UDP) or incoming connection based on socket type (TCP/ZMQ) 106 | 107 | #### Output/Outbound data 108 | 109 | - Sender: ACARS router will connect out to a remote host and send data to it. (TCP/ZMQ) 110 | - Server: ACARS router will send data to a remote host (UDP) or listen for incoming connection (TCP/ZMQ) and send data to it. 111 | 112 | ### Outbound data 113 | 114 | | Env Variable | Command Line Switch | Default | Description | 115 | | ------------------ | ------------------- | ------- | --------------------------------------- | 116 | | AR_SEND_UDP_ACARS | --send-udp-acars | `unset` | UDP host:port to send ACARS messages to | 117 | | AR_SEND_UDP_VDLM2 | --send-udp-vdlm2 | `unset` | UDP host:port to send VDLM2 messages to | 118 | | AR_SEND_UDP_HFDL | --send-udp-hfdl | `unset` | UDP host:port to send HFDL messages to | 119 | | AR_SEND_UDP_IMSL | --send-udp-imsl | `unset` | UDP host:port to send IMSL messages to | 120 | | AR_SEND_UDP_IRDM | --send-udp-irdm | `unset` | UDP host:port to send IRDM messages to | 121 | | AR_SEND_TCP_ACARS | --send-tcp-acars | `unset` | TCP host:port to send ACARS messages to | 122 | | AR_SEND_TCP_VDLM2 | --send-tcp-vdlm2 | `unset` | TCP host:port to send VDLM2 messages to | 123 | | AR_SEND_TCP_HFDL | --send-tcp-hfdl | `unset` | TCP host:port to send HFDL messages to | 124 | | AR_SEND_TCP_IMSL | --send-tcp-imsl | `unset` | TCP host:port to send IMSL messages to | 125 | | AR_SEND_TCP_IRDM | --send-tcp-irdm | `unset` | TCP host:port to send IRDM messages to | 126 | | AR_SERVE_TCP_ACARS | --serve-tcp-acars | `15550` | TCP port to serve ACARS messages to | 127 | | AR_SERVE_TCP_VDLM2 | --serve-tcp-vdlm2 | `15555` | TCP port to serve VDLM2 messages to | 128 | | AR_SERVE_TCP_HFDL | --serve-tcp-hfdl | `15556` | TCP port to serve HFDL messages to | 129 | | AR_SERVE_TCP_IMSL | --serve-tcp-imsl | `15557` | TCP port to serve IMSL messages to | 130 | | AR_SERVE_TCP_IRDM | --serve-tcp-irdm | `15558` | TCP port to serve IRDM messages to | 131 | | AR_SERVE_ZMQ_ACARS | --serve-zmq-acars | `45550` | ZMQ port to serve ACARS messages to | 132 | | AR_SERVE_ZMQ_VDLM2 | --serve-zmq-vdlm2 | `45555` | ZMQ port to serve VDLM2 messages to | 133 | | AR_SERVE_ZMQ_HFDL | --serve-zmq-hfdl | `45556` | ZMQ port to serve HFDL messages to | 134 | | AR_SERVE_ZMQ_IMSL | --serve-zmq-imsl | `45557` | ZMQ port to serve IMSL messages to | 135 | | AR_SERVE_ZMQ_IRDM | --serve-zmq-irdm | `45558` | ZMQ port to serve IRDM messages to | 136 | 137 | ### Inbound data 138 | 139 | | Env Variable | Command Line Switch | Default | Description | 140 | | ------------------- | ------------------- | ------- | -------------------------------------------- | 141 | | AR_RECV_ZMQ_ACARS | --recv-zmq-acars | `unset` | ZMQ host:port to receive ACARS messages from | 142 | | AR_RECV_ZMQ_VDLM2 | --recv-zmq-vdlm2 | `unset` | ZMQ host:port to receive VDLM2 messages from | 143 | | AR_RECV_ZMQ_HFDL | --recv-zmq-hfdl | `unset` | ZMQ host:port to receive HFDL messages from | 144 | | AR_RECV_ZMQ_IMSL | --recv-zmq-imsl | `unset` | ZMQ host:port to receive IMSL messages from | 145 | | AR_RECV_ZMQ_IRDM | --recv-zmq-irdm | `unset` | ZMQ host:port to receive IRDM messages from | 146 | | AR_RECV_TCP_ACARS | --recv-tcp-acars | `unset` | TCP host:port to receive ACARS messages from | 147 | | AR_RECV_TCP_VDLM2 | --recv-tcp-vdlm2 | `unset` | TCP host:port to receive VDLM2 messages from | 148 | | AR_RECV_TCP_HFDL | --recv-tcp-hfdl | `unset` | TCP host:port to receive HFDL messages from | 149 | | AR_RECV_TCP_IMSL | --recv-tcp-imsl | `unset` | TCP host:port to receive IMSL messages from | 150 | | AR_RECV_TCP_IRDM | --recv-tcp-irdm | `unset` | TCP host:port to receive IRDM messages from | 151 | | AR_LISTEN_TCP_ACARS | --listen-tcp-acars | `5550` | TCP port to listen for ACARS messages from | 152 | | AR_LISTEN_TCP_VDLM2 | --listen-tcp-vdlm2 | `5555` | TCP port to listen for VDLM2 messages from | 153 | | AR_LISTEN_TCP_HFDL | --listen-tcp-hfdl | `5556` | TCP port to listen for HFDL messages from | 154 | | AR_LISTEN_TCP_IMSL | --listen-tcp-imsl | `5557` | TCP port to listen for IMSL messages from | 155 | | AR_LISTEN_TCP_IRDM | --listen-tcp-irdm | `5558` | TCP port to listen for IRDM messages from | 156 | | AR_LISTEN_UDP_ACARS | --listen-udp-acars | `5550` | UDP port to listen for ACARS messages from | 157 | | AR_LISTEN_UDP_VDLM2 | --listen-udp-vdlm2 | `5555` | UDP port to listen for VDLM2 messages from | 158 | | AR_LISTEN_UDP_HFDL | --listen-udp-hfdl | `5556` | UDP port to listen for HFDL messages from | 159 | | AR_LISTEN_UDP_IMSL | --listen-udp-imsl | `5557` | UDP port to listen for IMSL messages from | 160 | | AR_LISTEN_UDP_IRDM | --listen-udp-irdm | `5558` | UDP port to listen for IRDM messages from | 161 | | AR_LISTEN_ZMQ_ACARS | --listen-zmq-acars | `35550` | ZMQ port to listen for ACARS messages from | 162 | | AR_LISTEN_ZMQ_VDLM2 | --listen-zmq-vdlm2 | `35555` | ZMQ port to listen for VDLM2 messages from | 163 | | AR_LISTEN_ZMQ_HFDL | --listen-zmq-hfdl | `35556` | ZMQ port to listen for HFDL messages from | 164 | | AR_LISTEN_ZMQ_IMSL | --listen-zmq-imsl | `35557` | ZMQ port to listen for IMSL messages from | 165 | | AR_LISTEN_ZMQ_IRDM | --listen-zmq-irdm | `35558` | ZMQ port to listen for IRDM messages from | 166 | 167 | ### General Options 168 | 169 | | Env Variable | Command Line Switch | Default | Description | 170 | | ------------------------ | ----------------------- | ------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | 171 | | AR_VERBOSITY | --verbose | `info` | Verbosity level. Valid values are `debug`, `info`, `warning`, `error` | 172 | | AR_ENABLE_DEDUPE | --enable-dedupe | `false` | Enable message deduplication. Valid values are `true` or `false` | 173 | | AR_DEDUPE_WINDOW | --dedupe-window | `2` | Window for how long a message will be considered a duplicate if the same message is received again. | 174 | | AR_SKEW_WINDOW | --skew-window | `5` | If a message is older then the skew window it will be automatically rejected, in seconds. If you are receiving only ACARS/VDLM2 1 or 2 seconds is a good value. With HFDL you will need to increase the window. | 175 | | AR_MAX_UDP_PACKET_SIZE | --max-udp-packet-size | `60000` | Maximum UDP packet size. Messages greater then this will be send in multiple parts | 176 | | AR_ADD_PROXY_ID | --add-proxy-id | `true` | Append to the message a header indicating that acars router processed the message | 177 | | AR_OVERRIDE_STATION_NAME | --override-station-name | `unset` | Change the station id field (identifying your station name for unstream clients) is set to this instead of what was in the message originally | 178 | | AR_STATS_EVERY | --stats-every | `5` | How often to print stats to the log in minutes | 179 | | AR_STATS_VERBOSE | --stats-verbose | `false` | Print verbose stats to the log | 180 | | AR_REASSEMBLY_WINDOW | --reassemble-window | `1.0` | If a message comes in, but part of the message is missing, this value will be used to keep the partial message fragment around while attempting to wait for the second (or subsequent) part(s) | 181 | | AR_DISABLE_ACARS | --disable-acars | `false` | Disable ACARS processing. Valid values are `true` or `false` | 182 | | AR_DISABLE_VDLM2 | --disable-vdlm | `false` | Disable VDLM processing. Valid values are `true` or `false` | 183 | | AR_DISABLE_HFDL | --disable-hfdl | `false` | Disable HFDL processing. Valid values are `true` or `false` | 184 | | AR_DISABLE_IMSL | --disable-imsl | `false` | Disable IMSL processing. Valid values are `true` or `false` | 185 | | AR_DISABLE_IRDM | --disable-irdm | `false` | Disable IRDM processing. Valid values are `true` or `false` | 186 | -------------------------------------------------------------------------------- /acars_router/README.md: -------------------------------------------------------------------------------- 1 | # ACARS Router 2 | 3 | `acars_router` receives, validates, deduplicates, modifies and routes ACARS and VDLM2 JSON messages. 4 | 5 | ## Runtime Configuration 6 | 7 | `acars_router` can be configured via command line arguments or environment variables. Command line arguments take preference over environment variables. Environment variables should only be used for running in Docker as internally in acars_router not every environment variable will be read in. 8 | 9 | When using environment variables use `;` to separate entries, for example: `AR_SEND_UDP_ACARS="1.2.3.4:5550;5.6.7.8:5550"` 10 | 11 | ### Input 12 | 13 | #### ACARS Input 14 | 15 | | Argument | Environment Variable | Description | Default | 16 | | --------------------- | --------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------- | 17 | | `--listen-udp-acars` | `AR_LISTEN_UDP_ACARS` | UDP port to listen for ACARS JSON messages. Can be specified multiple times (separated by `;`) to listen on multiple ports. | | 18 | | `--listen-tcp-acars` | `AR_LISTEN_TCP_ACARS` | TCP port to listen for ACARS JSON messages. Can be specified multiple times (separated by `;`) to listen on multiple ports. | | 19 | | `--receive-tcp-acars` | `AR_RECV_TCP_ACARS` | Connect to "host:port" (over TCP) and receive ACARS JSON messages. Can be specified multiple times (separated by `;`) to receive from multiple sources. | | 20 | | `--receive-zmq-acars` | `AR_RECV_ZMQ_ACARS` | Connect to a ZeroMQ publisher at `host:port` (over TCP) and receive ACARS JSON messages as a subscriber. Can be specified multiple times to receive from multiple sources. | | 21 | 22 | #### VDLM2 Input 23 | 24 | | Argument | Environment Variable | Description | Default | 25 | | --------------------- | --------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------- | 26 | | `--listen-udp-vdlm2` | `AR_LISTEN_UDP_VDLM2` | UDP port to listen for VDLM2 JSON messages. Can be specified multiple times (separated by `;`) to listen on multiple ports. | | 27 | | `--listen-tcp-vdlm2` | `AR_LISTEN_TCP_VDLM2` | TCP port to listen for VDLM2 JSON messages. Can be specified multiple times (separated by `;`) to listen on multiple ports. | | 28 | | `--receive-tcp-vdlm2` | `AR_RECV_TCP_VDLM2` | Connect to `host:port` (over TCP) and receive VDLM2 JSON messages. Can be specified multiple times (separated by `;`) to receive from multiple sources. | | 29 | | `--receive-zmq-vdlm2` | `AR_RECV_ZMQ_VDLM2` | Connect to a ZeroMQ publisher at `host:port` (over TCP) and receive VDLM2 JSON messages as a subscriber. Can be specified multiple times to receive from multiple sources. | | 30 | 31 | ### Output 32 | 33 | #### ACARS Output 34 | 35 | | Argument | Environment Variable | Description | Default | 36 | | ------------------- | -------------------- | ----------------------------------------------------------------------------------------------------------------------------------------- | ------- | 37 | | `--send-udp-acars` | `AR_SEND_UDP_ACARS` | Send ACARS JSON messages via UDP datagram to `host:port`. Can be specified multiple times (separated by `;`) to send to multiple clients. | | 38 | | `--send-tcp-acars` | `AR_SEND_TCP_ACARS` | Send ACARS JSON messages via TCP to `host:port`. Can be specified multiple times (separated by `;`) to send to multiple clients. | | 39 | | `--serve-tcp-acars` | `AR_SERVE_TCP_ACARS` | Serve ACARS JSON messages on TCP `port`. Can be specified multiple times (separated by `;`) to serve on multiple ports. | | 40 | | `--serve-zmq-acars` | `AR_SERVE_ZMQ_ACARS` | Serve ACARS messages as a ZeroMQ publisher on TCP `port`. Can be specified multiple times (separated by `;`) to serve on multiple ports. | | 41 | 42 | #### VDLM2 Output 43 | 44 | | Argument | Environment Variable | Description | Default | 45 | | ------------------- | -------------------- | ----------------------------------------------------------------------------------------------------------------------------------------- | ------- | 46 | | `--send-udp-vdlm2` | `AR_SEND_UDP_VDLM2` | Send VDLM2 JSON messages via UDP datagram to `host:port`. Can be specified multiple times (separated by `;`) to send to multiple clients. | | 47 | | `--send-tcp-vdlm2` | `AR_SEND_TCP_VDLM2` | Send VDLM2 JSON messages via TCP to `host:port`. Can be specified multiple times (separated by `;`) to send to multiple clients. | | 48 | | `--serve-tcp-vdlm2` | `AR_SERVE_TCP_VDLM2` | Serve VDLM2 JSON messages on TCP `port`. Can be specified multiple times (separated by `;`) to serve on multiple ports. | | 49 | | `--serve-zmq-vdlm2` | `AR_SERVE_ZMQ_VDLM2` | Serve VDLM2 messages as a ZeroMQ publisher on TCP `port`. Can be specified multiple times (separated by `;`) to serve on multiple ports. | | 50 | 51 | ### Logging 52 | 53 | | Argument | Environment Variable | Description | Default | 54 | | ---------------- | -------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | ------- | 55 | | `--stats-every` | `AR_STATS_EVERY` | Print statistics every `N` minutes | `5` | 56 | | `--stats-file` | `AR_STATS_FILE` | Logs statistics (in JSON format) to this file every 10 seconds | | 57 | | `-v` `--verbose` | `AR_VERBOSITY` | Increase log verbosity. `No flag`/`AR_VERBOSITY=`/`AR_VERBOSITY=info` = info `-v`/`AR_VERBOSITY=debug` = Debug. `-vv`/`AR_VERBOSITY=trace` = Trace (raw packets printed) | `info` | 58 | 59 | ### Deduplication 60 | 61 | | Argument | Environment Variable | Description | Default | 62 | | ----------------- | -------------------- | ----------------------------------------------------------- | ------- | 63 | | `--enable-dedupe` | `AR_ENABLE_DEDUPE` | Enables message deduplication. | False | 64 | | `--dedupe-window` | `AR_DEDUPE_WINDOW` | The window in seconds for duplicate messages to be dropped. | `2` | 65 | 66 | ### Message Modification 67 | 68 | | Argument | Environment Variable | Description | Default | 69 | | ------------------------- | -------------------------- | ----------------------------------------- | ---------------------------------------------- | 70 | | `--override-station-name` | `AR_OVERRIDE_STATION_NAME` | Overrides station id/name with this value | | 71 | | `--add-proxy-id` | `AR_ADD_PROXY_ID` | If set, will add in `proxy` data. | `env`: `true`, if run via command line `false` | 72 | 73 | ### Advanced Settings 74 | 75 | You should not have to modify any of these for normal operation. 76 | 77 | | Argument | Environment Variable | Description | Default | 78 | | ----------------------- | ------------------------ | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------- | 79 | | `--skew-window` | `AR_SKEW_WINDOW` | Reject messages with a timestamp greater than +/- this many seconds. | 1 | 80 | | `--max-udp-packet-size` | `AR_MAX_UDP_PACKET_SIZE` | If message size exceeds this for any UDP clients, split the message and send in multiple chunks. | 60000 | 81 | | `--reassembly-window` | `AR_REASSEMBLY_WINDOW` | For any message that comes in and cannot be deserialized into JSON, acars_router will retain that message for the specified number of seconds and attempt to use that chunk to reassemble future incomplete messages. | 1 | 82 | 83 | ## Internals 84 | 85 | A high-level overview of the `acars_router` internals: 86 | 87 | - Input 88 | - `acars_router` can receive data from ACARS/VDLM2 providers: 89 | - As a UDP server. `acarsdec`/`dumpvdlm2` can be configured to output UDP JSON by using the `--listen-udp-acars` and `--listen-udp-vdlm2` arguments respectively. This is the recommended way to receive data from `acarsdec` 90 | - As a TCP server for applications that output TCP JSON. The `--listen-tcp-acars` and `--listen-tcp-vdlm2` arguments can be used. 91 | - As a TCP client. `acars_router` can connect to a TCP server, and receive JSON. The arguments `--receive-tcp-acars` and `--receive-tcp-vdlm2` arguments can be used. 92 | - For `dumpvdl2`, `acars_router` supports ZeroMQ, and can connect to `dumpvdl2` and receive zmq JSON messages. This is the recommended way to receive data from `dumpvdl2`. 93 | - For each message received, a message object containing the raw JSON message (and some metadata) is then put into the `inbound_acars_message_queue` / `inbound_vdlm2_message_queue` for further processing. 94 | - Validation 95 | - A pool of `json_validator` threads get messages from the inbound queues, and attempt to deserialise the JSON. The deserialised data is added to the message object, and the message object is then put into the `deserialised_acars_message_queue` / `deserialised_vdlm2_message_queue` for further processing. 96 | - Hashing 97 | - The receive timestamp of the message object is reviewed. If the timestamp is outside +/- `--skew-window` seconds, the message is dropped. 98 | - A pool of `acars_hasher`/`vdlm2_hasher` threads then get messages from the deserialised queues, and hash the non-feeder-specific data in each message object. 99 | - The hash is added to the message object, and the message object is then put into the `hashed_acars_message_queue` / `hashed_vdlm2_message_queue` for further processing. 100 | - Deduplicating 101 | - A pool of `deduper` threads then get messages from the hashed queues. 102 | - The hash in the message object is checked against a list of hashes of all messages received in the last _N_ seconds. 103 | - If there is a match, the message is considered a duplicate, and is dropped. 104 | - If there is not a match, the message hash is added to the list of messages received in the last _N_ seconds, and then the message object is placed into the `deduped_acars_message_queue` / `deduped_vdlm2_message_queue`. 105 | - An "evictor" process runs constantly, ensuring that message hashes in the recent message hash list do not exceed the `--dedupe-window` settings. 106 | - Outbound Queue Population 107 | - When an output is configured with `--send-udp-acars`, `--send-tcp-acars`, `--send-udp-vdlm2` or `--send-tcp-vdlm2`, an output queue is created for each destination. 108 | - When an output is configured with `--serve-tcp-acars` or `--serve-tcp-vdlm2`, an output queue is created for each host that connects. 109 | - When an output is configured with `--serve-zmq-acars` or `--serve-zmq-vdlm2`, ZMQ handles per-connection queueing, so a single output queue for each is created. 110 | - All of these output queues are kept in lists: `output_acars_queues` and `output_vdlm2_queues`. 111 | - A pool of `output_queue_populator` threads receive messages from the dedupe message queues. The message object is duplicated for each output queue in the output queue lists, and placed onto the queues. If `--override-station-name` has been set, the JSON is modified accordingly. 112 | - Each output queue is then processed by a TCP/UDP client/server and the processed JSON message is sent out. 113 | 114 | I have attempted to show this in a flow diagram: 115 | 116 | ![Flowchart internals diagram](./internals.drawio.svg) 117 | 118 | ## Interpreting Logs 119 | 120 | - With the default logging, the log level is informational and above. Log entries of levels `INFO`, `WARNING`, `ERROR` and `CRITICAL` are logged. 121 | - With `-v`/`AR_VERBOSITY=debug` the `DEBUG` level is added, and log entries include the originating thread. 122 | - Queue depths are logged in this level during scheduled statistics logging. During normal operation, all of these should be `0` (zero), with the exception of `recent_message_queue_acars` and `recent_message_queue_vdlm2`, which may be higher than zero - this is OK (as they contain a copy of message objects received in the past `--dedupe-window` / `AR_DEDUPE_WINDOW` seconds). 123 | - With `-vv`/`AR_VERBOSITY=trace` the `TRACE` level is added, which prints the contents of the message object as it traverses through each process. This level is very noisy and is intended for application troubleshooting. 124 | - As messages are received, they are given a UUID. This way, if odd message routing/processing behaviour is observed, the message object UUID can be found, the logs can be grepped for this UUID, and the logs will show how the message has been received and processed all the way through to being dropped or sent. 125 | 126 | ## Telegraf Integration 127 | 128 | [Telegraf](https://www.influxdata.com/time-series-platform/telegraf/) can ingest statistics from the file produced by `--stats-file` / `AR_STATS_FILE`. 129 | 130 | An example telegraf configuration is: 131 | 132 | ```toml 133 | [[inputs.file]] 134 | files = [ "/run/acars_router/stats.json" ] 135 | name_override = "acars_router" 136 | data_format = "json" 137 | ``` 138 | -------------------------------------------------------------------------------- /acars_router/internals.drawio: -------------------------------------------------------------------------------- 1 | 7V1bd5tIEv41Omf2IT40t4bHWB7PZDfe48TJZGdefBjRlsggoQHky/z6bcRFQANqiUA1djsPMS3AiK++6qqvqmGmzdfPv4TOdnUTuMSfqYr7PNOuZqqKMNbof8nISzqimygdWIaem+10GLjz/iHZoJKN7jyXRJUd4yDwY29bHVwEmw1ZxJUxJwyDp+puD4Ff/atbZ0mYgbuF47Oj3zw3XqWjlooP478Sb7nK/zIy7fSTtZPvnH2TaOW4wVNpSPt5ps3DIIjT39bPc+InNy+/L98+vHzzP/5l/vLvT9HfztfL/3z572/v0pNdn3JI8RVCsonPPvX3u80m/vXDp/f3n75FD8Zn0/Au35nZV4tf8vsVBruNS5JjlJl2GYTxKlgGG8f/GARbOojo4HcSxy8Z0s4uDujQKl772afk2Yv/lxx+oRrZ5u+lj66es1PvN17yjU0cvpSPSrZ/L394OG6/lR8YxWHwF5kHfhDur1+bzxX6Qz958Hy/NH59Pac/dDz9xsRlzObI3c32i4JduCAd+2V8iZ1wSeKOW38wIco9EqwJ/Vr0uJD4Tuw9Vi/OyUiwLPY7AE1/ybA+AXc8JO4S9o47j0aCvesiHx1/l/0lZ+GEkUsWHfaQQPi08mJyt3X2t+CJzhRV7B+CTZwZBqLf7nLpO1FUgSr3vcnehSPtgyQvcI8kjMlz6bay9z77VM/8/Et18+kwa6hKNrYqzRj5fj+co7kVlEBJzPcu29wEG/rfZW/eglKp3TPyUMmGdKHUYdXJBINXgtb1NYtXgtZ+XBS8VNApT2Hg+rD5MwHj69XtDYmiJBB0Nq5Pwplq+vR7XP6Z/LZMfotXIXHcCMxJ1pD92Uz+tdnCnrstzo8X9VYnqVadJLJZL4msMb2kNVoEW0Qvx0IZXAllkMihTDNPWT63RxNg7hfI24Kjg1h0RJwdTTk7noYX7OyIGLiy2fGLnB1PmB2RaNMjYmk3UKbfPT2Kx8uGWa5xP70nLbNDbwOPXvYh2dSqlmLVDCC9rOygmg0UV9HDLNiwaQhvfLAU3MtU5nPT7BNjMTZ2fb33EKLYmAE6VWsj2wK/PjhdrwE7m6usvvs2gmNeeICDYybY+jK//UwWhP5lGV91SrTCxVes84TRH8RzlbwVkb4BVj8q6nLy++GI9p78mmNmtcZ+zRg3aGarZ3/cfJJu+7jbNmzh3LbSAcu5HD9kJ/1q2eA81zl5DqtYseVRCeHJEI7VjtB1kSWH6u7W20fX7xKtRnWaBV7tCA/lNAtJSJR+BGSMFupydw3hvjTNlCUogciYhKcdtEtQ4i6qe2YbXPbdYvcU/DgJeEVx0Rlm7SgPFteqgrlonZ04x8tmlQvL6uYv3bgloUe/KwnbqAkuISKbk5uqBeqUbYadXlqxvU9Zuk6rtvd/7wjdoW4WVY6pNV135WyT/dbPy2RRwMWDHzwtVk4YX6x3fuy9c4PFbr2/2400b0OuHeuhCFoLoYrtMkF1iyWoORhBx+npHJKg4KlN7nSPElQzIQmaX2YDQWmGs1bFIShgkqObojFUtRjYvkfB5p5ue64TB8LEPYB6HlLsCmoahtbzNFablajVUUM11Bq4NjJqrAAkUaujptZQA9fOdVYHEqVkdq5+u98SPzXReGUDHbQ1W2OFgzqtZa2sle9ale9FywOcl2ZbuaWXrsextiVaRIQlakf1O6WGGnxEJLOP46ihGmrwERFbIHkbERG4FqTxirU6aJeZxoq1MiLir7JU+Q4fERk2IN9fSZlF524PBS2z6Kyw5JKI3l3K3IjIWsuBprUeP71JycUNLB1Myc1PPGGWgs+vOm8fmQG6hElv6CQrs1QWXIpEFYtGU51NeVKnunKilThtJoCetV5uMbANHQCpErPTii2GBY5Zy8O/JGZtpRbDhsbMFHdp7isvtRi8gY8J26LJBj6S1J31FFOBJrXR1lcrFmaQQaqtiRbwsDUwiVmtllLDDD7gYStgErNaJaWGGXzAI+5y7FdeSTEs3oBHBQ14WLUgFXckqYtySZXU8AEPhsxizhJiS1w3afz4+tKb/CUOx9kOquvml1li+57nsu4ya6u7mA3dEOMKuhhyDu9Nd4Rf4eRuqrx0B30uVn6ZLN1lAaetgAPPd3MaeoZA62UweJ+hOQ09Q6DVMhi8y9Cchp4h0FoZC7xTDI/zIE7x9Az4DIdXz8CwGQ6rZ1BL2G3lA+U6iV4r6lgImuj5WwOlc+Yt6sAHQXmULDHjLerAB0GYVYQkZp1FHQGCoLFfNiBKEASu+2Be3QeDPiolv0wZBPUp9MAHQRZktvNK1sVg3jX+GFSqxazol1JWlmZmbaUZC1yqtSBn4leyJAabnAS1QB/CgVmFNyeoLKa0FVPgGYqljnBiMcUG1xEsqSOcWEyxwXUES+oIJxZTkAIuJKD8CoaOXyZdGjkemPTNHLJD66/nYQym/n6eNLYa7qWWCuTTdd90tc3iFZps0GobUlRpIaJbCKgUmV9mKS4IdvF2F6d50v022O58+cSe08qz1DFDhw7WNFY/C1SfFSCvmsbiVoHqswLkVbIZ9MT6rBB51Vt9zgC4mmzxqsk2hg2c5cJM4S0E9n1Eylt9LPyELAS0ZGyxFSmZWvVt+hAgtUIIUlTpu+Cv34sk4WUV3gcgIwVWV2HrmmXys5T/KbEaeqOSnEFJd/0XY2Zvo43kXe0lWQg1ZQz6mGVqhKa8pn/inLe53x6rgCYNNqsFSM6f1zomBOWnvK5/6pTnDfGRApoF2t0xvqR8B+WLCF0kzkMm9bCcB0/rbczNedC83mbXpU+L85ANqKqAnId8m9Fb5zzvqnWUyj5gnGeXrUvO80p4WEDOT27l1iviPLeEh0CfNW5PXcID5DwWcJ5X2Fzt69XtHQkfZZ/GLFFdURUytREyxEI2YJ8GG2p/mUvI2kSzZsTG7axhA6U9YhQZiViD5iECZOw8J/1iR8oqgF/Ma+DSL7ZkHFxT2bhNA+wCPOkYywGjgJCxjvH9/P3nOzby/0wWhOYjIduxQ/+Ot41IKbZf+MHOPY5qOwRtoA0FTRG6F9A0dV2rDdBYg0GTnAAsf+9uvhOvmFZkPhwZ90BLJZMbWLWhum20rJVsOFX1RJqKL9TqqdKvOdyyS1Xc7nERjY+7rDOQ8SGbMT7OhbrsqVTjQldUzUDJugbTttWaMSLtQtVpMKhguo8ytl2K27Muol3ylx6GsUstv4LCLPGZZqlTs7PLZlmzSg3UKtmuqN+uPt6obyyKMqx6gCtAFAVZ+TzRYYCXLQqd5LjDUPsu+m+hOaP3nTuP0TMxc6I9fhwl7sPiBTS/XPMBMz9DYY3mXAM0FbMUSOF8TmqLo9DIhinuA3xFNEze5/AMZZimUZ9azzbL+pk0RflBpkc3wyCIy7snJd6bwCXJHv8H 2 | -------------------------------------------------------------------------------- /acars_router/requirements.txt: -------------------------------------------------------------------------------- 1 | pyzmq==23.2.0 2 | -------------------------------------------------------------------------------- /acars_router/viewacars: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | import socket 4 | import argparse 5 | import logging 6 | import time 7 | import json 8 | from pprint import pprint 9 | import sys 10 | import zmq 11 | 12 | if __name__ == "__main__": 13 | 14 | parser = argparse.ArgumentParser(description="View ACARS/VDLM2 frames received via TCP/ZMQ") 15 | 16 | parser.add_argument( 17 | '--host', 18 | nargs='?', 19 | default="127.0.0.1", 20 | type=str, 21 | help="Hostname (default: 127.0.0.1)", 22 | ) 23 | 24 | parser.add_argument( 25 | '--port', 26 | nargs='?', 27 | default=15550, 28 | type=str, 29 | help="Port (default: 15550)", 30 | ) 31 | 32 | parser.add_argument( 33 | '--protocol', 34 | nargs='?', 35 | default="tcp", 36 | type=str, 37 | choices=["tcp", "zmq"], 38 | help="Protocol (default: tcp)", 39 | ) 40 | 41 | parser.add_argument( 42 | '--pretty', 43 | action='store_true', 44 | default=False, 45 | ) 46 | 47 | args = parser.parse_args() 48 | 49 | host = args.host 50 | port = args.port 51 | proto = args.protocol 52 | 53 | # configure logging: base logger 54 | logging.basicConfig(level=logging.DEBUG, format='%(asctime)s [%(levelname)s] [%(name)s] %(message)s') 55 | logger = logging.getLogger(f'input.{proto}.{host}:{port}') 56 | logger.setLevel(logging.DEBUG) 57 | 58 | if proto == "tcp": 59 | 60 | # loop until the session is disconnected 61 | while True: 62 | 63 | # prepare socket 64 | with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as sock: 65 | 66 | # set up socket 67 | sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) 68 | 69 | # attempt connection 70 | logger.debug("attempting to connect") 71 | 72 | # set socket connect timeout 73 | sock.settimeout(1) 74 | 75 | # attempt to connect 76 | try: 77 | sock.connect((host, port)) 78 | except Exception as e: 79 | logger.error(f"connection error: {e}") 80 | time.sleep(1) 81 | 82 | # if connected with no exception 83 | else: 84 | logger.info("connection established") 85 | sock.settimeout(1) 86 | 87 | # while connected 88 | while True: 89 | 90 | # try to receive data 91 | try: 92 | data = sock.recv(16384) 93 | 94 | except socket.timeout: 95 | continue 96 | 97 | except Exception as e: 98 | logger.error(f"error receiving data: {e}") 99 | 100 | # if data received with no exception 101 | else: 102 | 103 | # if we received something 104 | if data: 105 | 106 | # initially, we attempt to decode the whole message 107 | decode_to_char = len(data) 108 | 109 | # counter to prevent getting stuck in an infinite loop (shouldn't happen as everything is in try/except, but just to be sure) 110 | decode_attempts = 0 111 | 112 | # while there is data left to decode: 113 | while len(data) > 0: 114 | 115 | # attempt to deserialise 116 | try: 117 | deserialised_json = json.loads(data[:decode_to_char]) 118 | 119 | # if there is extra data, attempt to decode as much as we can next iteration of loop 120 | except json.decoder.JSONDecodeError as e: 121 | decode_to_char = e.pos 122 | 123 | # if an exception, log and continue 124 | except Exception as e: 125 | logger.error(f"invalid JSON received: {data}, exception: {e}") 126 | break 127 | 128 | # if there was no exception: 129 | else: 130 | 131 | # ensure json.loads resulted in a dict 132 | if type(deserialised_json) != dict: 133 | logger.debug(f"invalid JSON received: json.loads on raw_json returned non-dict object: {data}") 134 | 135 | # if it is a dict... 136 | else: 137 | 138 | # print json 139 | if args.pretty: 140 | pprint(deserialised_json) 141 | else: 142 | print(json.dumps( 143 | deserialised_json, 144 | separators=(',', ':'), 145 | sort_keys=True, 146 | )) 147 | 148 | # remove the json we've already serialised from the input 149 | data = data[decode_to_char:] 150 | decode_to_char = len(data) 151 | 152 | # ensure we're not stuck in an infinite loop (unlikely there'd be 100 parts of json in a message. I've only ever seen up to 2.) 153 | if decode_attempts > 100: 154 | logger.error(f"infinite loop deserialising: {data}") 155 | break 156 | else: 157 | decode_attempts += 1 158 | 159 | # if we received nothing, then "connection lost" 160 | else: 161 | logger.info("connection lost") 162 | 163 | # close the socket 164 | sock.close() 165 | 166 | # sleep for a second (slow down reconnection attempts) 167 | time.sleep(1) 168 | 169 | # break out of inner loop, so reconnection can happen 170 | break 171 | 172 | elif proto == "zmq": 173 | 174 | # Prepare zmq context and sockets 175 | context = zmq.Context() 176 | 177 | # This program is a SUBscripber. dumpvdl2 is a PUBlisher. 178 | subscriber = context.socket(zmq.SUB) 179 | 180 | # Connect to dumpvdl2 zmq server 181 | subscriber.connect(f"tcp://{host}:{port}") 182 | 183 | # Subscribe for everything 184 | subscriber.setsockopt(zmq.SUBSCRIBE, b'') 185 | 186 | # Receive messages forever 187 | while True: 188 | 189 | # Receive all parts of a zmq message 190 | message = subscriber.recv_multipart() 191 | 192 | # For each json message inside the zmq message 193 | for data in message: 194 | 195 | # deserialise json 196 | deserialised_json = json.loads(data) 197 | 198 | # print json 199 | if args.pretty: 200 | pprint(deserialised_json) 201 | else: 202 | print(json.dumps( 203 | deserialised_json, 204 | separators=(',', ':'), 205 | sort_keys=True, 206 | )) 207 | 208 | else: 209 | 210 | logger.critical(f"Unsupported protocol: {proto}") 211 | sys.exit(1) 212 | -------------------------------------------------------------------------------- /pre-commit-hooks.MD: -------------------------------------------------------------------------------- 1 | # Pre-Commit Hooks 2 | 3 | [Setup](https://pre-commit.com/#install) 4 | 5 | ## Commands 6 | 7 | To run outside of the pre-commit: 8 | 9 | ```shell 10 | pre-commit run --all-files 11 | ``` 12 | -------------------------------------------------------------------------------- /rootfs/etc/s6-overlay/s6-rc.d/acars_router/dependencies.d/base: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sdr-enthusiasts/acars_router/b9a81cbf894b076f1506e14a407bcaba89e7fa21/rootfs/etc/s6-overlay/s6-rc.d/acars_router/dependencies.d/base -------------------------------------------------------------------------------- /rootfs/etc/s6-overlay/s6-rc.d/acars_router/run: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | exec /etc/s6-overlay/scripts/acars_router 3 | -------------------------------------------------------------------------------- /rootfs/etc/s6-overlay/s6-rc.d/acars_router/type: -------------------------------------------------------------------------------- 1 | longrun 2 | -------------------------------------------------------------------------------- /rootfs/etc/s6-overlay/s6-rc.d/user/contents.d/acars_router: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sdr-enthusiasts/acars_router/b9a81cbf894b076f1506e14a407bcaba89e7fa21/rootfs/etc/s6-overlay/s6-rc.d/user/contents.d/acars_router -------------------------------------------------------------------------------- /rootfs/etc/s6-overlay/scripts/acars_router: -------------------------------------------------------------------------------- 1 | #!/command/with-contenv bash 2 | # shellcheck shell=bash 3 | 4 | AR_COMMAND=() 5 | 6 | # ACARS Input 7 | 8 | if [[ -n $AR_LISTEN_UDP_ACARS ]]; then 9 | AR_COMMAND+=("--listen-udp-acars" "$AR_LISTEN_UDP_ACARS") 10 | fi 11 | 12 | if [[ -n $AR_LISTEN_TCP_ACARS ]]; then 13 | AR_COMMAND+=("--listen-tcp-acars" "$AR_LISTEN_TCP_ACARS") 14 | fi 15 | 16 | if [[ -n $AR_LISTEN_ZMQ_ACARS ]]; then 17 | AR_COMMAND+=("--listen-zmq-acars" "$AR_LISTEN_ZMQ_ACARS") 18 | fi 19 | 20 | if [[ -n $AR_RECV_TCP_ACARS ]]; then 21 | AR_COMMAND+=("--receive-tcp-acars" "$AR_RECV_TCP_ACARS") 22 | fi 23 | 24 | if [[ -n $AR_RECV_ZMQ_ACARS ]]; then 25 | AR_COMMAND+=("--receive-zmq-acars" "$AR_RECV_ZMQ_ACARS") 26 | fi 27 | 28 | # VDLM2 input 29 | 30 | if [[ -n $AR_LISTEN_UDP_VDLM2 ]]; then 31 | AR_COMMAND+=("--listen-udp-vdlm2" "$AR_LISTEN_UDP_VDLM2") 32 | fi 33 | 34 | if [[ -n $AR_LISTEN_TCP_VDLM2 ]]; then 35 | AR_COMMAND+=("--listen-tcp-vdlm2" "$AR_LISTEN_TCP_VDLM2") 36 | fi 37 | 38 | if [[ -n $AR_LISTEN_ZMQ_VDLM2 ]]; then 39 | AR_COMMAND+=("--listen-zmq-vdlm2" "$AR_LISTEN_ZMQ_VDLM2") 40 | fi 41 | 42 | if [[ -n $AR_RECV_TCP_VDLM2 ]]; then 43 | AR_COMMAND+=("--receive-tcp-vdlm2" "$AR_RECV_TCP_VDLM2") 44 | fi 45 | 46 | if [[ -n $AR_RECV_ZMQ_VDLM2 ]]; then 47 | AR_COMMAND+=("--receive-zmq-vdlm2" "$AR_RECV_ZMQ_VDLM2") 48 | fi 49 | 50 | # HFDL input 51 | 52 | if [[ -n $AR_LISTEN_UDP_HFDL ]]; then 53 | AR_COMMAND+=("--listen-udp-hfdl" "$AR_LISTEN_UDP_HFDL") 54 | fi 55 | 56 | if [[ -n $AR_LISTEN_TCP_HFDL ]]; then 57 | AR_COMMAND+=("--listen-tcp-hfdl" "$AR_LISTEN_TCP_HFDL") 58 | fi 59 | 60 | if [[ -n $AR_LISTEN_ZMQ_HFDL ]]; then 61 | AR_COMMAND+=("--listen-zmq-hfdl" "$AR_LISTEN_ZMQ_HFDL") 62 | fi 63 | 64 | if [[ -n $AR_RECV_TCP_HFDL ]]; then 65 | AR_COMMAND+=("--receive-tcp-hfdl" "$AR_RECV_TCP_HFDL") 66 | fi 67 | 68 | if [[ -n $AR_RECV_ZMQ_HFDL ]]; then 69 | AR_COMMAND+=("--receive-zmq-hfdl" "$AR_RECV_ZMQ_HFDL") 70 | fi 71 | 72 | # IMSL input 73 | 74 | if [[ -n $AR_LISTEN_UDP_IMSL ]]; then 75 | AR_COMMAND+=("--listen-udp-imsl" "$AR_LISTEN_UDP_IMSL") 76 | fi 77 | 78 | if [[ -n $AR_LISTEN_TCP_IMSL ]]; then 79 | AR_COMMAND+=("--listen-tcp-imsl" "$AR_LISTEN_TCP_IMSL") 80 | fi 81 | 82 | if [[ -n $AR_LISTEN_ZMQ_IMSL ]]; then 83 | AR_COMMAND+=("--listen-zmq-imsl" "$AR_LISTEN_ZMQ_IMSL") 84 | fi 85 | 86 | if [[ -n $AR_RECV_TCP_IMSL ]]; then 87 | AR_COMMAND+=("--receive-tcp-imsl" "$AR_RECV_TCP_IMSL") 88 | fi 89 | 90 | if [[ -n $AR_RECV_ZMQ_IMSL ]]; then 91 | AR_COMMAND+=("--receive-zmq-imsl" "$AR_RECV_ZMQ_IMSL") 92 | fi 93 | 94 | # IRDM input 95 | 96 | if [[ -n $AR_LISTEN_UDP_IRDM ]]; then 97 | AR_COMMAND+=("--listen-udp-irdm" "$AR_LISTEN_UDP_IRDM") 98 | fi 99 | 100 | if [[ -n $AR_LISTEN_TCP_IRDM ]]; then 101 | AR_COMMAND+=("--listen-tcp-irdm" "$AR_LISTEN_TCP_IRDM") 102 | fi 103 | 104 | if [[ -n $AR_LISTEN_ZMQ_IRDM ]]; then 105 | AR_COMMAND+=("--listen-zmq-irdm" "$AR_LISTEN_ZMQ_IRDM") 106 | fi 107 | 108 | if [[ -n $AR_RECV_TCP_IRDM ]]; then 109 | AR_COMMAND+=("--receive-tcp-irdm" "$AR_RECV_TCP_IRDM") 110 | fi 111 | 112 | if [[ -n $AR_RECV_ZMQ_IRDM ]]; then 113 | AR_COMMAND+=("--receive-zmq-irdm" "$AR_RECV_ZMQ_IRDM") 114 | fi 115 | 116 | # ACARS Output 117 | 118 | if [[ -n $AR_SEND_UDP_ACARS ]]; then 119 | AR_COMMAND+=("--send-udp-acars" "$AR_SEND_UDP_ACARS") 120 | fi 121 | 122 | if [[ -n $AR_SEND_TCP_ACARS ]]; then 123 | AR_COMMAND+=("--send-tcp-acars" "$AR_SEND_TCP_ACARS") 124 | fi 125 | 126 | if [[ -n $AR_SERVE_TCP_ACARS ]]; then 127 | AR_COMMAND+=("--serve-tcp-acars" "$AR_SERVE_TCP_ACARS") 128 | fi 129 | 130 | if [[ -n $AR_SERVE_ZMQ_ACARS ]]; then 131 | AR_COMMAND+=("--serve-zmq-acars" "$AR_SERVE_ZMQ_ACARS") 132 | fi 133 | 134 | # VDLM2 Output 135 | 136 | if [[ -n $AR_SEND_UDP_VDLM2 ]]; then 137 | AR_COMMAND+=("--send-udp-vdlm2" "$AR_SEND_UDP_VDLM2") 138 | fi 139 | 140 | if [[ -n $AR_SEND_TCP_VDLM2 ]]; then 141 | AR_COMMAND+=("--send-tcp-vdlm2" "$AR_SEND_TCP_VDLM2") 142 | fi 143 | 144 | if [[ -n $AR_SERVE_TCP_VDLM2 ]]; then 145 | AR_COMMAND+=("--serve-tcp-vdlm2" "$AR_SERVE_TCP_VDLM2") 146 | fi 147 | 148 | if [[ -n $AR_SERVE_ZMQ_VDLM2 ]]; then 149 | AR_COMMAND+=("--serve-zmq-vdlm2" "$AR_SERVE_ZMQ_VDLM2") 150 | fi 151 | 152 | # HFDL Output 153 | 154 | if [[ -n $AR_SEND_UDP_HFDL ]]; then 155 | AR_COMMAND+=("--send-udp-hfdl" "$AR_SEND_UDP_HFDL") 156 | fi 157 | 158 | if [[ -n $AR_SEND_TCP_HFDL ]]; then 159 | AR_COMMAND+=("--send-tcp-hfdl" "$AR_SEND_TCP_HFDL") 160 | fi 161 | 162 | if [[ -n $AR_SERVE_TCP_HFDL ]]; then 163 | AR_COMMAND+=("--serve-tcp-hfdl" "$AR_SERVE_TCP_HFDL") 164 | fi 165 | 166 | if [[ -n $AR_SERVE_ZMQ_HFDL ]]; then 167 | AR_COMMAND+=("--serve-zmq-hfdl" "$AR_SERVE_ZMQ_HFDL") 168 | fi 169 | 170 | # IMSL Output 171 | 172 | if [[ -n $AR_SEND_UDP_IMSL ]]; then 173 | AR_COMMAND+=("--send-udp-imsl" "$AR_SEND_UDP_IMSL") 174 | fi 175 | 176 | if [[ -n $AR_SEND_TCP_IMSL ]]; then 177 | AR_COMMAND+=("--send-tcp-imsl" "$AR_SEND_TCP_IMSL") 178 | fi 179 | 180 | if [[ -n $AR_SERVE_TCP_IMSL ]]; then 181 | AR_COMMAND+=("--serve-tcp-imsl" "$AR_SERVE_TCP_IMSL") 182 | fi 183 | 184 | if [[ -n $AR_SERVE_ZMQ_IMSL ]]; then 185 | AR_COMMAND+=("--serve-zmq-imsl" "$AR_SERVE_ZMQ_IMSL") 186 | fi 187 | 188 | # IRDM Output 189 | 190 | if [[ -n $AR_SEND_UDP_IRDM ]]; then 191 | AR_COMMAND+=("--send-udp-irdm" "$AR_SEND_UDP_IRDM") 192 | fi 193 | 194 | if [[ -n $AR_SEND_TCP_IRDM ]]; then 195 | AR_COMMAND+=("--send-tcp-irdm" "$AR_SEND_TCP_IRDM") 196 | fi 197 | 198 | if [[ -n $AR_SERVE_TCP_IRDM ]]; then 199 | AR_COMMAND+=("--serve-tcp-irdm" "$AR_SERVE_TCP_IRDM") 200 | fi 201 | 202 | if [[ -n $AR_SERVE_ZMQ_IRDM ]]; then 203 | AR_COMMAND+=("--serve-zmq-irdm" "$AR_SERVE_ZMQ_IRDM") 204 | fi 205 | 206 | /opt/acars_router "${AR_COMMAND[@]}" 207 | -------------------------------------------------------------------------------- /rootfs/rename_current_arch_binary.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | set -x 4 | 5 | ls -la /opt/ 6 | 7 | # determine which binary to keep 8 | if /opt/acars_router.amd64 --version > /dev/null 2>&1; then 9 | mv -v /opt/acars_router.amd64 /opt/acars_router 10 | elif /opt/acars_router.arm64 --version > /dev/null 2>&1; then 11 | mv -v /opt/acars_router.arm64 /opt/acars_router 12 | elif /opt/acars_router.armv7 --version > /dev/null 2>&1; then 13 | mv -v /opt/acars_router.armv7 /opt/acars_router 14 | else 15 | >&2 echo "ERROR: Unsupported architecture" 16 | exit 1 17 | fi 18 | -------------------------------------------------------------------------------- /rust/bin/acars_router/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "acars_router" 3 | version.workspace = true 4 | edition.workspace = true 5 | authors.workspace = true 6 | description.workspace = true 7 | documentation.workspace = true 8 | homepage.workspace = true 9 | repository.workspace = true 10 | readme.workspace = true 11 | license.workspace = true 12 | rust-version.workspace = true 13 | 14 | [dependencies] 15 | acars_config = { path = "../../libraries/acars_config" } 16 | acars_connection_manager = { path = "../../libraries/acars_connection_manager" } 17 | log.workspace = true 18 | tokio.workspace = true 19 | serde.workspace = true 20 | serde_json.workspace = true 21 | sdre-rust-logging.workspace = true 22 | -------------------------------------------------------------------------------- /rust/bin/acars_router/src/main.rs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Mike Nye, Fred Clausen 2 | // 3 | // Licensed under the MIT license: https://opensource.org/licenses/MIT 4 | // Permission is granted to use, copy, modify, and redistribute the work. 5 | // Full license information available in the project LICENSE file. 6 | // 7 | 8 | #[macro_use] 9 | extern crate log; 10 | extern crate acars_config; 11 | extern crate acars_connection_manager; 12 | extern crate sdre_rust_logging; 13 | extern crate serde; 14 | extern crate serde_json; 15 | 16 | use acars_config::clap::Parser; 17 | use acars_config::Input; 18 | use acars_connection_manager::service_init::start_processes; 19 | use sdre_rust_logging::SetupLogging; 20 | use std::error::Error; 21 | use std::process; 22 | 23 | #[tokio::main] 24 | async fn main() -> Result<(), Box> { 25 | let args: Input = Input::parse(); 26 | args.verbose.enable_logging(); 27 | match args.check_config_option_sanity() { 28 | Ok(_) => { 29 | trace!("Config options are sane"); 30 | } 31 | Err(e) => { 32 | error!("{}", e); 33 | process::exit(1); 34 | } 35 | } 36 | start_processes(args).await; 37 | Ok(()) 38 | } 39 | -------------------------------------------------------------------------------- /rust/libraries/acars_config/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "acars_config" 3 | version.workspace = true 4 | edition.workspace = true 5 | 6 | # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html 7 | 8 | [dependencies] 9 | clap.workspace = true 10 | log.workspace = true 11 | sdre-rust-logging.workspace = true 12 | -------------------------------------------------------------------------------- /rust/libraries/acars_connection_manager/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "acars_connection_manager" 3 | version.workspace = true 4 | edition.workspace = true 5 | 6 | # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html 7 | 8 | [dependencies] 9 | acars_vdlm2_parser = { git = "https://github.com/rpatel3001/acars_vdlm2_parser", branch = "add_imsl_irdm" } 10 | acars_config = { path = "../acars_config" } 11 | sdre-stubborn-io.workspace = true 12 | #sdre-stubborn-io = "0.4.4" 13 | log.workspace = true 14 | tokio.workspace = true 15 | tokio-util.workspace = true 16 | tokio-stream.workspace = true 17 | futures.workspace = true 18 | async-trait.workspace = true 19 | zmq.workspace = true 20 | tmq.workspace = true 21 | #acars_vdlm2_parser = { git = "https://github.com/jcdeimos/acars_vdlm2_parser", version = "0.2.1" } 22 | #acars_vdlm2_parser = { git = "https://github.com/fredclausen/acars_vdlm2_parser", branch = "hfdl-and-dependency-updates" } 23 | -------------------------------------------------------------------------------- /rust/libraries/acars_connection_manager/src/lib.rs: -------------------------------------------------------------------------------- 1 | #[macro_use] 2 | extern crate log; 3 | extern crate acars_config; 4 | extern crate acars_vdlm2_parser; 5 | extern crate async_trait; 6 | extern crate futures; 7 | extern crate sdre_stubborn_io; 8 | extern crate tmq; 9 | pub extern crate tokio as tokio; 10 | extern crate tokio_stream; 11 | extern crate tokio_util; 12 | extern crate zmq; 13 | 14 | pub mod message_handler; 15 | pub mod packet_handler; 16 | pub mod service_init; 17 | pub mod tcp_services; 18 | pub mod udp_services; 19 | pub mod zmq_services; 20 | 21 | use acars_vdlm2_parser::AcarsVdlm2Message; 22 | use sdre_stubborn_io::ReconnectOptions; 23 | use std::collections::HashMap; 24 | use std::fmt; 25 | use std::fmt::Formatter; 26 | use std::net::SocketAddr; 27 | use std::time::Duration; 28 | use tokio::sync::mpsc; 29 | use tokio::sync::mpsc::Receiver; 30 | 31 | /// Shorthand for the transmit half of the message channel. 32 | pub type Tx = mpsc::UnboundedSender; 33 | 34 | /// Shorthand for the receive half of the message channel. 35 | pub type Rx = mpsc::UnboundedReceiver; 36 | 37 | pub type DurationIterator = Box + Send + Sync>; 38 | 39 | #[allow(dead_code)] 40 | #[derive(Debug)] 41 | pub(crate) struct SenderServer { 42 | pub(crate) host: String, 43 | pub(crate) proto_name: String, 44 | pub(crate) socket: T, 45 | pub(crate) channel: Receiver, 46 | } 47 | 48 | #[derive(Debug, Default)] 49 | pub(crate) struct Shared { 50 | pub(crate) peers: HashMap, 51 | } 52 | 53 | #[derive(Debug, Clone)] 54 | pub(crate) struct SocketListenerServer { 55 | pub(crate) proto_name: String, 56 | pub(crate) port: u16, 57 | pub(crate) reassembly_window: f64, 58 | pub(crate) socket_type: SocketType, 59 | } 60 | 61 | #[allow(dead_code)] 62 | #[derive(Debug, Clone)] 63 | pub(crate) enum SocketType { 64 | Tcp, 65 | Udp, 66 | } 67 | 68 | #[derive(Debug, Clone)] 69 | pub(crate) enum ServerType { 70 | Acars, 71 | Vdlm2, 72 | Hfdl, 73 | Imsl, 74 | Irdm, 75 | } 76 | 77 | #[derive(Debug, Clone, Default)] 78 | pub(crate) struct SenderServerConfig { 79 | pub(crate) send_udp: Option>, 80 | pub(crate) send_tcp: Option>, 81 | pub(crate) serve_tcp: Option>, 82 | pub(crate) serve_zmq: Option>, 83 | pub(crate) max_udp_packet_size: usize, 84 | } 85 | 86 | #[derive(Debug, Clone)] 87 | pub(crate) struct OutputServerConfig { 88 | pub(crate) listen_udp: Option>, 89 | pub(crate) listen_tcp: Option>, 90 | pub(crate) listen_zmq: Option>, 91 | pub(crate) receive_tcp: Option>, 92 | pub(crate) receive_zmq: Option>, 93 | pub(crate) reassembly_window: f64, 94 | pub(crate) output_server_type: ServerType, 95 | } 96 | 97 | // create ReconnectOptions. We want the TCP stuff that goes out and connects to clients 98 | // to attempt to reconnect 99 | // See: https://docs.rs/stubborn-io/latest/src/stubborn_io/config.rs.html#93 100 | 101 | pub fn reconnect_options(host: &str) -> ReconnectOptions { 102 | ReconnectOptions::new() 103 | .with_exit_if_first_connect_fails(false) 104 | .with_retries_generator(get_our_standard_reconnect_strategy) 105 | .with_connection_name(host) 106 | } 107 | 108 | fn get_our_standard_reconnect_strategy() -> DurationIterator { 109 | let initial_attempts = vec![ 110 | Duration::from_secs(5), 111 | Duration::from_secs(5), 112 | Duration::from_secs(5), 113 | Duration::from_secs(5), 114 | Duration::from_secs(5), 115 | Duration::from_secs(5), 116 | Duration::from_secs(5), 117 | Duration::from_secs(5), 118 | Duration::from_secs(5), 119 | Duration::from_secs(5), 120 | Duration::from_secs(5), 121 | Duration::from_secs(5), 122 | Duration::from_secs(5), 123 | Duration::from_secs(5), 124 | Duration::from_secs(10), 125 | Duration::from_secs(20), 126 | Duration::from_secs(30), 127 | Duration::from_secs(40), 128 | Duration::from_secs(50), 129 | Duration::from_secs(60), 130 | ]; 131 | 132 | let repeat = std::iter::repeat(Duration::from_secs(60)); 133 | 134 | let forever_iterator = initial_attempts.into_iter().chain(repeat); 135 | 136 | Box::new(forever_iterator) 137 | } 138 | 139 | impl fmt::Display for ServerType { 140 | fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { 141 | match self { 142 | ServerType::Acars => write!(f, "ACARS"), 143 | ServerType::Vdlm2 => write!(f, "VDLM"), 144 | ServerType::Hfdl => write!(f, "HFDL"), 145 | ServerType::Imsl => write!(f, "IMSL"), 146 | ServerType::Irdm => write!(f, "IRDM"), 147 | } 148 | } 149 | } 150 | 151 | impl fmt::Display for SocketType { 152 | fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { 153 | match self { 154 | SocketType::Tcp => write!(f, "TCP"), 155 | SocketType::Udp => write!(f, "UDP"), 156 | } 157 | } 158 | } 159 | -------------------------------------------------------------------------------- /rust/libraries/acars_connection_manager/src/packet_handler.rs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Mike Nye, Fred Clausen 2 | // 3 | // Licensed under the MIT license: https://opensource.org/licenses/MIT 4 | // Permission is granted to use, copy, modify, and redistribute the work. 5 | // Full license information available in the project LICENSE file. 6 | // 7 | 8 | use acars_vdlm2_parser::{AcarsVdlm2Message, DecodeMessage, MessageResult}; 9 | use async_trait::async_trait; 10 | use std::collections::HashMap; 11 | use std::net::SocketAddr; 12 | use std::sync::Arc; 13 | use std::time::{SystemTime, UNIX_EPOCH}; 14 | use tokio::sync::mpsc::Sender; 15 | use tokio::sync::Mutex; 16 | 17 | pub struct PacketHandler { 18 | name: String, 19 | listener_type: String, 20 | // Hashmap key is peer, stores a tuple of time and message 21 | queue: Arc>>, 22 | reassembly_window: f64, 23 | } 24 | 25 | #[async_trait] 26 | pub trait ProcessAssembly { 27 | async fn process_reassembly( 28 | &self, 29 | proto_name: &str, 30 | channel: &Sender, 31 | listener_type: &str, 32 | ); 33 | } 34 | 35 | #[async_trait] 36 | impl ProcessAssembly for Option { 37 | async fn process_reassembly( 38 | &self, 39 | proto_name: &str, 40 | channel: &Sender, 41 | listener_type: &str, 42 | ) { 43 | match self { 44 | Some(reassembled_msg) => { 45 | let parsed_msg: MessageResult = reassembled_msg.to_string_newline(); 46 | match parsed_msg { 47 | Err(parse_error) => error!( 48 | "[{} Listener Server: {}] {}", 49 | listener_type, proto_name, parse_error 50 | ), 51 | Ok(msg) => { 52 | trace!( 53 | "[{} Listener SERVER: {}] Received message: {:?}", 54 | listener_type, 55 | proto_name, 56 | msg 57 | ); 58 | match channel.send(msg).await { 59 | Ok(_) => debug!( 60 | "[{} Listener SERVER: {}] Message sent to channel", 61 | listener_type, proto_name 62 | ), 63 | Err(e) => error!( 64 | "[{} Listener SERVER: {}] sending message to channel: {}", 65 | listener_type, proto_name, e 66 | ), 67 | }; 68 | } 69 | } 70 | } 71 | None => trace!( 72 | "[{} Listener SERVER: {}] Invalid Message", 73 | listener_type, 74 | proto_name 75 | ), 76 | } 77 | } 78 | } 79 | 80 | impl PacketHandler { 81 | pub fn new(name: &str, listener_type: &str, reassembly_window: f64) -> PacketHandler { 82 | PacketHandler { 83 | name: name.to_string(), 84 | queue: Arc::new(Mutex::new(HashMap::new())), 85 | listener_type: listener_type.to_string(), 86 | reassembly_window, 87 | } 88 | } 89 | 90 | pub async fn attempt_message_reassembly( 91 | &self, 92 | new_message_string: String, 93 | peer: SocketAddr, 94 | ) -> Option { 95 | // FIXME: This is a hack to get this concept working. Ideally we aren't cleaning the queue while 96 | // Processing a message 97 | self.clean_queue().await; 98 | // FIXME: Ideally this entire function should not lock the mutex all the time 99 | 100 | if let Ok(msg) = new_message_string.decode_message() { 101 | self.queue.lock().await.remove(&peer); 102 | return Some(msg); 103 | } 104 | 105 | let mut output_message: Option = None; 106 | let mut message_for_peer: String = String::new(); 107 | let mut old_time: Option = None; // Save the time of the first message for this peer 108 | 109 | // TODO: Handle message reassembly for out of sequence messages 110 | // TODO: Handle message reassembly for a peer where the peer is sending multiple fragmented messages 111 | // Maybe on those two? This could get really tricky to know if the message we've reassembled is all the same message 112 | // Because we could end up in a position where the packet splits in the same spot and things look right but the packets belong to different messages 113 | 114 | if self.queue.lock().await.contains_key(&peer) { 115 | info!( 116 | "[{} SERVER: {}] Message received from {} is being reassembled", 117 | self.listener_type, self.name, peer 118 | ); 119 | let (time, message_to_test) = self.queue.lock().await.get(&peer).unwrap().clone(); 120 | old_time = Some(time); // We have a good peer, save the time 121 | message_for_peer = format!("{}{}", message_to_test, new_message_string); 122 | match message_for_peer.decode_message() { 123 | Err(e) => info!("{e}"), 124 | Ok(msg_deserialized) => { 125 | info!( 126 | "[{} SERVER: {}] Reassembled a message from peer {}", 127 | self.listener_type, self.name, peer 128 | ); 129 | // The default skew_window and are the same (1 second, but it doesn't matter) 130 | // So we shouldn't see any weird issues where the message is reassembled 131 | // BUT the time is off and causes the message to be rejected 132 | // Below we use the FIRST non-reasssembled time to base the expiration of the entire queue off of. 133 | output_message = Some(msg_deserialized); 134 | } 135 | }; 136 | } 137 | 138 | match output_message { 139 | Some(_) => { 140 | self.queue.lock().await.remove(&peer); 141 | } 142 | None => { 143 | // If the len is 0 then it's the first non-reassembled message, so we'll save the new message in to the queue 144 | // Otherwise message_for_peer should already have the old messages + the new one already in it. 145 | if message_for_peer.is_empty() { 146 | message_for_peer = new_message_string; 147 | } 148 | 149 | // We want the peer's message queue to expire once the FIRST message received from the peer is older 150 | // than the reassembly window. Therefore we use the old_time we grabbed from the queue above, or if it's the first 151 | // message we get the current time. 152 | 153 | let message_queue_time: f64 = match old_time { 154 | Some(t) => t, 155 | None => match SystemTime::now().duration_since(UNIX_EPOCH) { 156 | Ok(n) => n.as_secs_f64(), 157 | Err(_) => 0.0, 158 | }, 159 | }; 160 | 161 | self.queue 162 | .lock() 163 | .await 164 | .insert(peer, (message_queue_time, message_for_peer)); 165 | } 166 | } 167 | 168 | output_message 169 | } 170 | 171 | pub async fn clean_queue(&self) { 172 | let current_time: f64 = match SystemTime::now().duration_since(UNIX_EPOCH) { 173 | Ok(n) => n.as_secs_f64(), 174 | Err(_) => 0.0, 175 | }; 176 | 177 | if current_time == 0.0 { 178 | error!( 179 | "[{} SERVER: {}] Error getting current time", 180 | self.listener_type, self.name 181 | ); 182 | return; 183 | } 184 | 185 | self.queue.lock().await.retain(|peer, old_messages| { 186 | let (time, _) = old_messages; 187 | let time_diff: f64 = current_time - *time; 188 | if time_diff > self.reassembly_window { 189 | debug!("[{} SERVER {}] Peer {peer} has been idle for {time_diff} seconds, removing from queue", self.listener_type, self.name); 190 | false 191 | } else { 192 | debug!("[{} SERVER {}] Peer {peer} has been idle for {time_diff} seconds, keeping in queue", self.listener_type, self.name); 193 | true 194 | } 195 | }); 196 | } 197 | } 198 | -------------------------------------------------------------------------------- /rust/libraries/acars_connection_manager/src/tcp_services.rs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Mike Nye, Fred Clausen 2 | // 3 | // Licensed under the MIT license: https://opensource.org/licenses/MIT 4 | // Permission is granted to use, copy, modify, and redistribute the work. 5 | // Full license information available in the project LICENSE file. 6 | // 7 | 8 | use acars_vdlm2_parser::AcarsVdlm2Message; 9 | use futures::SinkExt; 10 | use sdre_stubborn_io::tokio::StubbornIo; 11 | use sdre_stubborn_io::StubbornTcpStream; 12 | use std::collections::HashMap; 13 | use std::error::Error; 14 | use std::io; 15 | use std::net::SocketAddr; 16 | use std::sync::Arc; 17 | use tokio::io::AsyncWriteExt; 18 | use tokio::net::{TcpListener, TcpStream}; 19 | use tokio::sync::mpsc::Receiver; 20 | use tokio::sync::mpsc::Sender; 21 | use tokio::sync::{mpsc, Mutex, MutexGuard}; 22 | use tokio_stream::StreamExt; 23 | use tokio_util::codec::{Framed, LinesCodec}; 24 | 25 | use crate::packet_handler::{PacketHandler, ProcessAssembly}; 26 | use crate::{reconnect_options, Rx, SenderServer, Shared}; 27 | 28 | /// TCP Listener server. This is used to listen for incoming TCP connections and process them. 29 | /// Used for incoming TCP data for ACARS Router to process 30 | pub(crate) struct TCPListenerServer { 31 | pub(crate) proto_name: String, 32 | pub(crate) reassembly_window: f64, 33 | } 34 | 35 | /// TCP Listener server. This is used to listen for incoming TCP connections and process them. 36 | /// Used for incoming TCP data for ACARS Router to process 37 | impl TCPListenerServer { 38 | pub(crate) fn new(proto_name: &str, reassembly_window: &f64) -> Self { 39 | Self { 40 | proto_name: proto_name.to_string(), 41 | reassembly_window: *reassembly_window, 42 | } 43 | } 44 | 45 | pub(crate) async fn run( 46 | self, 47 | listen_acars_udp_port: String, 48 | channel: Sender, 49 | ) -> Result<(), io::Error> { 50 | let listener: TcpListener = 51 | TcpListener::bind(format!("0.0.0.0:{}", listen_acars_udp_port)).await?; 52 | info!( 53 | "[TCP Listener SERVER: {}]: Listening on: {}", 54 | self.proto_name, 55 | listener.local_addr()? 56 | ); 57 | 58 | loop { 59 | trace!( 60 | "[TCP Listener SERVER: {}]: Waiting for connection", 61 | self.proto_name 62 | ); 63 | // Asynchronously wait for an inbound TcpStream. 64 | let (stream, addr) = listener.accept().await?; 65 | let new_channel = channel.clone(); 66 | let new_proto_name = format!("{}:{}", self.proto_name, addr); 67 | info!( 68 | "[TCP Listener SERVER: {}]:accepted connection from {}", 69 | self.proto_name, addr 70 | ); 71 | // Spawn our handler to be run asynchronously. 72 | tokio::spawn(async move { 73 | match process_tcp_sockets( 74 | stream, 75 | &new_proto_name, 76 | new_channel, 77 | addr, 78 | self.reassembly_window, 79 | ) 80 | .await 81 | { 82 | Ok(_) => debug!( 83 | "[TCP Listener SERVER: {}] connection closed", 84 | new_proto_name 85 | ), 86 | Err(e) => error!( 87 | "[TCP Listener SERVER: {}] connection error: {}", 88 | new_proto_name.clone(), 89 | e 90 | ), 91 | }; 92 | }); 93 | } 94 | } 95 | } 96 | 97 | /// This function is used to process the TCP socket. It will read the socket and send the messages to the channel. 98 | /// Used for incoming TCP data for ACARS Router to process 99 | async fn process_tcp_sockets( 100 | stream: TcpStream, 101 | proto_name: &str, 102 | channel: Sender, 103 | peer: SocketAddr, 104 | reassembly_window: f64, 105 | ) -> Result<(), Box> { 106 | let mut lines = Framed::new(stream, LinesCodec::new_with_max_length(8000)); 107 | 108 | let packet_handler = PacketHandler::new(proto_name, "TCP", reassembly_window); 109 | 110 | while let Some(Ok(line)) = lines.next().await { 111 | let split_messages_by_newline: Vec<&str> = line.split_terminator('\n').collect(); 112 | 113 | for msg_by_newline in split_messages_by_newline { 114 | let split_messages_by_brackets: Vec<&str> = 115 | msg_by_newline.split_terminator("}{").collect(); 116 | if split_messages_by_brackets.len().eq(&1) { 117 | packet_handler 118 | .attempt_message_reassembly(split_messages_by_brackets[0].to_string(), peer) 119 | .await 120 | .process_reassembly(proto_name, &channel, "TCP") 121 | .await; 122 | } else { 123 | // We have a message that was split by brackets if the length is greater than one 124 | for (count, msg_by_brackets) in split_messages_by_brackets.iter().enumerate() { 125 | let final_message = if count == 0 { 126 | // First case is the first element, which should only ever need a single closing bracket 127 | trace!( 128 | "[TCP Listener SERVER: {}] Multiple messages received in a packet.", 129 | proto_name 130 | ); 131 | format!("{}}}", msg_by_brackets) 132 | } else if count == split_messages_by_brackets.len() - 1 { 133 | // This case is for the last element, which should only ever need a single opening bracket 134 | trace!( 135 | "[TCP Listener SERVER: {}] End of a multiple message packet", 136 | proto_name 137 | ); 138 | format!("{{{}", msg_by_brackets) 139 | } else { 140 | // This case is for any middle elements, which need both an opening and closing bracket 141 | trace!( 142 | "[TCP Listener SERVER: {}] Middle of a multiple message packet", 143 | proto_name 144 | ); 145 | format!("{{{}}}", msg_by_brackets) 146 | }; 147 | packet_handler 148 | .attempt_message_reassembly(final_message, peer) 149 | .await 150 | .process_reassembly(proto_name, &channel, "TCP") 151 | .await; 152 | } 153 | } 154 | } 155 | } 156 | 157 | Ok(()) 158 | } 159 | 160 | /// TCP Receiver server. This is used to connect to a remote TCP server and process the messages. 161 | /// Used for incoming TCP data for ACARS Router to process 162 | pub struct TCPReceiverServer { 163 | pub host: String, 164 | pub proto_name: String, 165 | pub reassembly_window: f64, 166 | } 167 | 168 | /// TCP Receiver server. This is used to connect to a remote TCP server and process the messages. 169 | /// Used for incoming TCP data for ACARS Router to process 170 | impl TCPReceiverServer { 171 | pub(crate) fn new(server_host: &str, proto_name: &str, reassembly_window: f64) -> Self { 172 | Self { 173 | host: server_host.to_string(), 174 | proto_name: proto_name.to_string(), 175 | reassembly_window, 176 | } 177 | } 178 | 179 | pub async fn run(self, channel: Sender) -> Result<(), Box> { 180 | trace!("[TCP Receiver Server {}] Starting", self.proto_name); 181 | // create a SocketAddr from host 182 | let addr = match self.host.parse::() { 183 | Ok(addr) => addr, 184 | Err(e) => { 185 | error!( 186 | "[TCP Receiver Server {}] Error parsing host: {}", 187 | self.proto_name, e 188 | ); 189 | return Ok(()); 190 | } 191 | }; 192 | 193 | let stream = match StubbornTcpStream::connect_with_options( 194 | addr, 195 | reconnect_options(&self.proto_name), 196 | ) 197 | .await 198 | { 199 | Ok(stream) => stream, 200 | Err(e) => { 201 | error!( 202 | "[TCP Receiver Server {}] Error connecting to {}: {}", 203 | self.proto_name, self.host, e 204 | ); 205 | Err(e)? 206 | } 207 | }; 208 | 209 | // create a buffered reader and send the messages to the channel 210 | 211 | let reader = tokio::io::BufReader::new(stream); 212 | let mut lines = Framed::new(reader, LinesCodec::new()); 213 | let packet_handler = PacketHandler::new(&self.proto_name, "TCP", self.reassembly_window); 214 | 215 | while let Some(Ok(line)) = lines.next().await { 216 | // Clean up the line endings. This is probably unnecessary but it's here for safety. 217 | let split_messages_by_newline: Vec<&str> = line.split_terminator('\n').collect(); 218 | 219 | for msg_by_newline in split_messages_by_newline { 220 | let split_messages_by_brackets: Vec<&str> = 221 | msg_by_newline.split_terminator("}{").collect(); 222 | 223 | for (count, msg_by_brackets) in split_messages_by_brackets.iter().enumerate() { 224 | let final_message: String; 225 | // FIXME: This feels very non-rust idomatic and is ugly 226 | 227 | // Our message had no brackets, so we can just send it 228 | if split_messages_by_brackets.len() == 1 { 229 | final_message = msg_by_brackets.to_string(); 230 | } 231 | // We have a message that was split by brackets if the length is greater than one 232 | // First case is the first element, which should only ever need a single closing bracket 233 | else if count == 0 { 234 | trace!( 235 | "[TCP Receiver Server {}]Multiple messages received in a packet.", 236 | self.proto_name 237 | ); 238 | final_message = format!("{}{}", "}", msg_by_brackets); 239 | } else if count == split_messages_by_brackets.len() - 1 { 240 | // This case is for the last element, which should only ever need a single opening bracket 241 | trace!( 242 | "[TCP Receiver Server {}] End of a multiple message packet", 243 | self.proto_name 244 | ); 245 | final_message = format!("{}{}", "{", msg_by_brackets); 246 | } else { 247 | // This case is for any middle elements, which need both an opening and closing bracket 248 | trace!( 249 | "[TCP Receiver Server {}] Middle of a multiple message packet", 250 | self.proto_name 251 | ); 252 | final_message = format!("{}{}{}", "{", msg_by_brackets, "}"); 253 | } 254 | match packet_handler 255 | .attempt_message_reassembly(final_message, addr) 256 | .await 257 | { 258 | Some(encoded_msg) => { 259 | let parse_msg = encoded_msg.to_string(); 260 | match parse_msg { 261 | Err(parse_error) => error!("{}", parse_error), 262 | Ok(msg) => { 263 | trace!( 264 | "[TCP Receiver Server {}] Received message: {}", 265 | self.proto_name, 266 | msg 267 | ); 268 | match channel.send(msg).await { 269 | Ok(_) => trace!("[TCP SERVER {}] Message sent to channel", self.proto_name), 270 | Err(e) => error!("[TCP Receiver Server {}]Error sending message to channel: {}", self.proto_name, e) 271 | } 272 | } 273 | } 274 | } 275 | None => trace!("[TCP Receiver Server {}] Invalid Message", self.proto_name), 276 | } 277 | } 278 | } 279 | } 280 | 281 | Ok(()) 282 | } 283 | } 284 | 285 | /// TCP Sender server. This is used to connect to a remote TCP server and send the messages. 286 | /// Used for outgoing TCP data for ACARS Router to a client 287 | impl SenderServer> { 288 | pub async fn send_message(mut self) { 289 | tokio::spawn(async move { 290 | while let Some(message) = self.channel.recv().await { 291 | match message.to_bytes_newline() { 292 | Err(encode_error) => error!( 293 | "[TCP SENDER {}]: Error converting message: {}", 294 | self.proto_name, encode_error 295 | ), 296 | Ok(encoded_message) => match self.socket.write_all(&encoded_message).await { 297 | Ok(_) => trace!("[TCP SENDER {}]: sent message", self.proto_name), 298 | Err(e) => error!( 299 | "[TCP SENDER {}]: Error sending message: {}", 300 | self.proto_name, e 301 | ), 302 | }, 303 | } 304 | } 305 | }); 306 | } 307 | } 308 | 309 | /// TCP Serve Server. This is used to listen for incoming TCP connections and process them. 310 | /// Used for outgoing TCP data for ACARS Router to a client 311 | pub struct TCPServeServer { 312 | pub socket: TcpListener, 313 | pub proto_name: String, 314 | } 315 | 316 | /// The state for each connected client. 317 | struct Peer { 318 | /// The TCP socket wrapped with the `Lines` codec, defined below. 319 | /// 320 | /// This handles sending and receiving data on the socket. When using 321 | /// `Lines`, we can work at the line level instead of having to manage the 322 | /// raw byte operations. 323 | lines: Framed, 324 | 325 | /// Receive half of the message channel. 326 | /// 327 | /// This is used to receive messages from peers. When a message is received 328 | /// off of this `Rx`, it will be written to the socket. 329 | rx: Rx, 330 | } 331 | 332 | impl Shared { 333 | /// Create a new, empty, instance of `Shared`. 334 | pub(crate) fn new() -> Self { 335 | Shared { 336 | peers: HashMap::new(), 337 | } 338 | } 339 | 340 | /// Send a `LineCodec` encoded message to every peer, except 341 | /// for the sender. 342 | async fn broadcast(&mut self, message: &str) { 343 | for peer in self.peers.iter_mut() { 344 | let _ = peer.1.send(message.into()); 345 | } 346 | } 347 | } 348 | 349 | impl Peer { 350 | /// Create a new instance of `Peer`. 351 | async fn new( 352 | state: Arc>, 353 | lines: Framed, 354 | ) -> io::Result { 355 | // Get the client socket address 356 | let addr: SocketAddr = lines.get_ref().peer_addr()?; 357 | 358 | // Create a channel for this peer 359 | let (tx, rx) = mpsc::unbounded_channel(); 360 | 361 | // Add an entry for this `Peer` in the shared state map. 362 | state.lock().await.peers.insert(addr, tx); 363 | 364 | Ok(Peer { lines, rx }) 365 | } 366 | } 367 | 368 | /// TCP Serve Server. This is used to listen for incoming TCP connections and process them. 369 | /// Used for outgoing TCP data for ACARS Router to a client 370 | impl TCPServeServer { 371 | pub(crate) fn new(socket: TcpListener, proto_name: &str) -> Self { 372 | Self { 373 | socket, 374 | proto_name: proto_name.to_string(), 375 | } 376 | } 377 | pub(crate) async fn watch_for_connections( 378 | self, 379 | channel: Receiver, 380 | state: &Arc>, 381 | ) { 382 | let new_state: Arc> = Arc::clone(state); 383 | tokio::spawn(async move { handle_message(new_state, channel).await }); 384 | loop { 385 | let new_proto: String = self.proto_name.to_string(); 386 | match self.socket.accept().await { 387 | Ok((stream, addr)) => { 388 | // Clone a handle to the `Shared` state for the new connection. 389 | let state: Arc> = Arc::clone(state); 390 | 391 | // Spawn our handler to be run asynchronously. 392 | tokio::spawn(async move { 393 | info!("[TCP SERVER {new_proto}] accepted connection"); 394 | if let Err(e) = process(&state, stream, addr).await { 395 | info!( 396 | "[TCP SERVER {new_proto}] an error occurred; error = {:?}", 397 | e 398 | ); 399 | } 400 | }); 401 | } 402 | Err(e) => { 403 | error!("[TCP SERVER {new_proto}]: Error accepting connection: {e}"); 404 | continue; 405 | } 406 | }; 407 | } 408 | } 409 | } 410 | 411 | async fn handle_message(state: Arc>, mut channel: Receiver) { 412 | loop { 413 | if let Some(received_message) = channel.recv().await { 414 | // state.lock().await.broadcast(&format!("{}\n",received_message)).await; 415 | match received_message.to_string_newline() { 416 | Err(message_parse_error) => { 417 | error!("Failed to parse message to string: {}", message_parse_error) 418 | } 419 | Ok(message) => state.lock().await.broadcast(&message).await, 420 | } 421 | } 422 | } 423 | } 424 | 425 | async fn process( 426 | state: &Arc>, 427 | stream: TcpStream, 428 | addr: SocketAddr, 429 | ) -> Result<(), Box> { 430 | // If this section is reached it means that the client was disconnected! 431 | // Let's let everyone still connected know about it. 432 | let lines: Framed = Framed::new(stream, LinesCodec::new()); 433 | let mut peer: Peer = match Peer::new(state.clone(), lines).await { 434 | Ok(peer) => peer, 435 | Err(e) => { 436 | error!("[TCP SERVER {addr}]: Error creating peer: {}", e); 437 | return Ok(()); 438 | } 439 | }; 440 | loop { 441 | tokio::select! { 442 | // A message was received from a peer. Send it to the current user. 443 | Some(msg) = peer.rx.recv() => { 444 | match peer.lines.send(&msg).await { 445 | Ok(_) => { 446 | debug!("[TCP SERVER {addr}]: Sent message"); 447 | } 448 | Err(e) => { 449 | error!("[TCP SERVER {addr}]: Error sending message: {}", e); 450 | } 451 | }; 452 | } 453 | result = peer.lines.next() => match result { 454 | // We received a message on this socket. Why? Dunno. Do nothing. 455 | Some(Ok(_)) => (), 456 | // An error occurred. 457 | Some(Err(e)) => { 458 | error!( 459 | "[TCP SERVER {addr}]: [YOU SHOULD NEVER SEE THIS!] an error occurred while processing messages; error = {:?}", e 460 | ); 461 | } 462 | // The stream has been exhausted. 463 | None => break, 464 | }, 465 | } 466 | } 467 | { 468 | info!("[TCP SERVER {addr}]: Client disconnected"); 469 | let mut state: MutexGuard = state.lock().await; 470 | state.peers.remove(&addr); 471 | Ok(()) 472 | } 473 | } 474 | -------------------------------------------------------------------------------- /rust/libraries/acars_connection_manager/src/udp_services.rs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Mike Nye, Fred Clausen 2 | // 3 | // Licensed under the MIT license: https://opensource.org/licenses/MIT 4 | // Permission is granted to use, copy, modify, and redistribute the work. 5 | // Full license information available in the project LICENSE file. 6 | // 7 | 8 | // Server used to receive UDP data 9 | 10 | use crate::packet_handler::{PacketHandler, ProcessAssembly}; 11 | use acars_vdlm2_parser::AcarsVdlm2Message; 12 | use std::io; 13 | use std::net::SocketAddr; 14 | use tokio::net::UdpSocket; 15 | use tokio::sync::mpsc::{Receiver, Sender}; 16 | use tokio::time::{sleep, Duration}; 17 | 18 | /// UDPListenerServer is a struct that contains the configuration for a UDP server 19 | /// that will listen for incoming UDP packets and process them 20 | #[derive(Debug, Clone)] 21 | pub(crate) struct UDPListenerServer { 22 | pub(crate) proto_name: String, 23 | pub(crate) reassembly_window: f64, 24 | } 25 | 26 | /// UDPSenderServer is a struct that contains the configuration for a UDP server 27 | /// that will send out UDP packets 28 | #[derive(Debug)] 29 | pub(crate) struct UDPSenderServer { 30 | pub(crate) host: Vec, 31 | pub(crate) proto_name: String, 32 | pub(crate) socket: UdpSocket, 33 | pub(crate) max_udp_packet_size: usize, 34 | pub(crate) channel: Receiver, 35 | } 36 | 37 | /// UDPListenerServer is a struct that contains the configuration for a UDP server 38 | /// that will listen for incoming UDP packets and process them 39 | impl UDPListenerServer { 40 | pub(crate) fn new(proto_name: &str, reassembly_window: &f64) -> Self { 41 | Self { 42 | proto_name: proto_name.to_string(), 43 | reassembly_window: *reassembly_window, 44 | } 45 | } 46 | 47 | pub(crate) async fn run( 48 | self, 49 | listen_udp_port: &str, 50 | channel: Sender, 51 | ) -> Result<(), io::Error> { 52 | let mut buf: Vec = vec![0; 5000]; 53 | let mut to_send: Option<(usize, SocketAddr)> = None; 54 | 55 | let s = UdpSocket::bind(listen_udp_port).await; 56 | 57 | match s { 58 | Err(e) => error!( 59 | "[UDP SERVER: {}] Error listening on port: {}", 60 | self.proto_name, e 61 | ), 62 | Ok(socket) => { 63 | info!( 64 | "[UDP SERVER: {}]: Listening on: {}", 65 | self.proto_name, 66 | socket.local_addr()? 67 | ); 68 | 69 | let packet_handler: PacketHandler = 70 | PacketHandler::new(&self.proto_name, "UDP", self.reassembly_window); 71 | 72 | loop { 73 | if let Some((size, peer)) = to_send { 74 | let msg_string = match std::str::from_utf8(buf[..size].as_ref()) { 75 | Ok(s) => s, 76 | Err(_) => { 77 | warn!( 78 | "[UDP SERVER: {}] Invalid message received from {}", 79 | self.proto_name, peer 80 | ); 81 | continue; 82 | } 83 | }; 84 | let split_messages_by_newline: Vec<&str> = 85 | msg_string.split_terminator('\n').collect(); 86 | 87 | for msg_by_newline in split_messages_by_newline { 88 | let split_messages_by_brackets: Vec<&str> = 89 | msg_by_newline.split_terminator("}{").collect(); 90 | if split_messages_by_brackets.len().eq(&1) { 91 | packet_handler 92 | .attempt_message_reassembly( 93 | split_messages_by_brackets[0].to_string(), 94 | peer, 95 | ) 96 | .await 97 | .process_reassembly(&self.proto_name, &channel, "UDP") 98 | .await; 99 | } else { 100 | // We have a message that was split by brackets if the length is greater than one 101 | for (count, msg_by_brackets) in 102 | split_messages_by_brackets.iter().enumerate() 103 | { 104 | let final_message = if count == 0 { 105 | // First case is the first element, which should only ever need a single closing bracket 106 | trace!("[UDP SERVER: {}] Multiple messages received in a packet.", self.proto_name); 107 | format!("{}{}", "}", msg_by_brackets) 108 | } else if count == split_messages_by_brackets.len() - 1 { 109 | // This case is for the last element, which should only ever need a single opening bracket 110 | trace!( 111 | "[UDP SERVER: {}] End of a multiple message packet", 112 | self.proto_name 113 | ); 114 | format!("{}{}", "{", msg_by_brackets) 115 | } else { 116 | // This case is for any middle elements, which need both an opening and closing bracket 117 | trace!( 118 | "[UDP SERVER: {}] Middle of a multiple message packet", 119 | self.proto_name 120 | ); 121 | format!("{}{}{}", "{", msg_by_brackets, "}") 122 | }; 123 | packet_handler 124 | .attempt_message_reassembly(final_message, peer) 125 | .await 126 | .process_reassembly(&self.proto_name, &channel, "UDP") 127 | .await; 128 | } 129 | } 130 | } 131 | } 132 | to_send = Some(socket.recv_from(&mut buf).await?); 133 | } 134 | } 135 | }; 136 | Ok(()) 137 | } 138 | } 139 | 140 | /// UDPSenderServer is a struct that contains the configuration for a UDP server 141 | /// that will send out UDP packets 142 | impl UDPSenderServer { 143 | pub(crate) fn new( 144 | send_udp: &[String], 145 | server_type: &str, 146 | socket: UdpSocket, 147 | max_udp_packet_size: &usize, 148 | rx_processed: Receiver, 149 | ) -> Self { 150 | Self { 151 | host: send_udp.to_vec(), 152 | proto_name: server_type.to_string(), 153 | socket, 154 | max_udp_packet_size: *max_udp_packet_size, 155 | channel: rx_processed, 156 | } 157 | } 158 | 159 | pub(crate) async fn send_message(mut self) { 160 | // send the message to the socket 161 | // Loop through all of the sockets in the host list 162 | // We will send out a configured max amount bytes at a time until the buffer is exhausted 163 | 164 | while let Some(message) = self.channel.recv().await { 165 | match message.to_bytes_newline() { 166 | Err(bytes_error) => error!( 167 | "[UDP SENDER {}] Failed to encode to bytes: {}", 168 | self.proto_name, bytes_error 169 | ), 170 | Ok(message_as_bytes) => { 171 | let message_size: usize = message_as_bytes.len(); 172 | for addr in &self.host { 173 | let mut keep_sending: bool = true; 174 | let mut buffer_position: usize = 0; 175 | let mut buffer_end: usize = 176 | match message_as_bytes.len() < self.max_udp_packet_size { 177 | true => message_as_bytes.len(), 178 | false => self.max_udp_packet_size, 179 | }; 180 | 181 | while keep_sending { 182 | trace!("[UDP SENDER {}] Sending {buffer_position} to {buffer_end} of {message_size} to {addr}", self.proto_name); 183 | let bytes_sent = self 184 | .socket 185 | .send_to(&message_as_bytes[buffer_position..buffer_end], addr) 186 | .await; 187 | 188 | match bytes_sent { 189 | Ok(bytes_sent) => debug!( 190 | "[UDP SENDER {}] sent {} bytes to {}", 191 | self.proto_name, bytes_sent, addr 192 | ), 193 | Err(e) => warn!( 194 | "[UDP SENDER {}] failed to send message to {}: {:?}", 195 | self.proto_name, addr, e 196 | ), 197 | } 198 | 199 | if buffer_end == message_size { 200 | keep_sending = false; 201 | } else { 202 | buffer_position = buffer_end; 203 | buffer_end = match buffer_position + self.max_udp_packet_size 204 | < message_size 205 | { 206 | true => buffer_position + self.max_udp_packet_size, 207 | false => message_size, 208 | }; 209 | 210 | // Slow the sender down! 211 | sleep(Duration::from_millis(100)).await; 212 | } 213 | trace!( 214 | "[UDP SENDER {}] New buffer start: {}, end: {}", 215 | self.proto_name, 216 | buffer_position, 217 | buffer_end 218 | ); 219 | } 220 | } 221 | } 222 | } 223 | } 224 | } 225 | } 226 | -------------------------------------------------------------------------------- /rust/libraries/acars_connection_manager/src/zmq_services.rs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Mike Nye, Fred Clausen 2 | // 3 | // Licensed under the MIT license: https://opensource.org/licenses/MIT 4 | // Permission is granted to use, copy, modify, and redistribute the work. 5 | // Full license information available in the project LICENSE file. 6 | // 7 | 8 | // NOTE: WE **SUB** to a *PUB* socket. 9 | 10 | use crate::SenderServer; 11 | use acars_vdlm2_parser::AcarsVdlm2Message; 12 | use futures::SinkExt; 13 | use futures::StreamExt; 14 | use tmq::publish::Publish; 15 | use tmq::{subscribe, Context, Result}; 16 | use tokio::sync::mpsc::{Receiver, Sender}; 17 | 18 | /// ZMQ Receiver server. This is used to connect to a remote ZMQ server and process the messages. 19 | /// Used for incoming ZMQ data for ACARS Router to process 20 | pub struct ZMQReceiverServer { 21 | pub host: String, 22 | pub proto_name: String, 23 | } 24 | 25 | /// ZMQ Receiver server. This is used to connect to a remote ZMQ server and process the messages. 26 | /// Used for incoming ZMQ data for ACARS Router to process 27 | impl ZMQReceiverServer { 28 | pub async fn run(self, channel: Sender) -> Result<()> { 29 | debug!("[ZMQ RECEIVER SERVER {}] Starting", self.proto_name); 30 | let address = format!("tcp://{}", self.host); 31 | let mut socket = subscribe(&Context::new()) 32 | .connect(&address)? 33 | .subscribe(b"")?; 34 | 35 | while let Some(msg) = socket.next().await { 36 | let message = match msg { 37 | Ok(message) => message, 38 | Err(e) => { 39 | error!("[ZMQ RECEIVER SERVER {}] Error: {:?}", self.proto_name, e); 40 | continue; 41 | } 42 | }; 43 | 44 | let composed_message = message 45 | .iter() 46 | .map(|item| item.as_str().unwrap_or("invalid text")) 47 | .collect::>() 48 | .join(" "); 49 | trace!( 50 | "[ZMQ RECEIVER SERVER {}] Received: {}", 51 | self.proto_name, 52 | composed_message 53 | ); 54 | let stripped = composed_message 55 | .strip_suffix("\r\n") 56 | .or_else(|| composed_message.strip_suffix('\n')) 57 | .unwrap_or(&composed_message); 58 | 59 | match channel.send(stripped.to_string()).await { 60 | Ok(_) => trace!( 61 | "[ZMQ RECEIVER SERVER {}] Message sent to channel", 62 | self.proto_name 63 | ), 64 | Err(e) => error!( 65 | "[ZMQ RECEIVER SERVER {}] Error sending message to channel: {}", 66 | self.proto_name, e 67 | ), 68 | } 69 | } 70 | Ok(()) 71 | } 72 | } 73 | 74 | /// ZMQ Listener server. This is used to listen for incoming ZMQ data for ACARS Router to process 75 | /// Used for incoming ZMQ data for ACARS Router to process 76 | pub struct ZMQListenerServer { 77 | pub(crate) proto_name: String, 78 | } 79 | 80 | /// ZMQ Listener server. This is used to listen for incoming ZMQ data for ACARS Router to process 81 | /// Used for incoming ZMQ data for ACARS Router to process 82 | impl ZMQListenerServer { 83 | pub fn new(proto_name: &str) -> Self { 84 | Self { 85 | proto_name: proto_name.to_string(), 86 | } 87 | } 88 | pub async fn run(self, listen_acars_zmq_port: String, channel: Sender) -> Result<()> { 89 | debug!("[ZMQ LISTENER SERVER {}] Starting", self.proto_name); 90 | let address = format!("tcp://0.0.0.0:{}", listen_acars_zmq_port); 91 | debug!( 92 | "[ZMQ LISTENER SERVER {}] Listening on {}", 93 | self.proto_name, address 94 | ); 95 | let mut socket = subscribe(&Context::new()).bind(&address)?.subscribe(b"")?; 96 | 97 | while let Some(msg) = socket.next().await { 98 | match msg { 99 | Ok(message) => { 100 | for item in message { 101 | let composed_message = item 102 | .as_str() 103 | .unwrap_or("invalid text") 104 | .strip_suffix("\r\n") 105 | .or_else(|| item.as_str().unwrap_or("invalid text").strip_suffix('\n')) 106 | .unwrap_or(item.as_str().unwrap_or("invalid text")); 107 | trace!( 108 | "[ZMQ LISTENER SERVER {}] Received: {}", 109 | self.proto_name, 110 | composed_message 111 | ); 112 | match channel.send(composed_message.to_string()).await { 113 | Ok(_) => trace!( 114 | "[ZMQ LISTENER SERVER {}] Message sent to channel", 115 | self.proto_name 116 | ), 117 | Err(e) => error!( 118 | "[ZMQ LISTENER SERVER {}] Error sending message to channel: {}", 119 | self.proto_name, e 120 | ), 121 | } 122 | } 123 | } 124 | Err(e) => error!("[ZMQ LISTENER SERVER {}] Error: {:?}", self.proto_name, e), 125 | } 126 | } 127 | 128 | Ok(()) 129 | } 130 | } 131 | 132 | /// ZMQ Sender server. This is used to send messages to a remote ZMQ server. 133 | /// Used for outgoing ZMQ data for ACARS Router to process 134 | impl SenderServer { 135 | pub(crate) fn new( 136 | server_address: &str, 137 | name: &str, 138 | socket: Publish, 139 | channel: Receiver, 140 | ) -> Self { 141 | Self { 142 | host: server_address.to_string(), 143 | proto_name: name.to_string(), 144 | socket, 145 | channel, 146 | } 147 | } 148 | pub async fn send_message(mut self) { 149 | tokio::spawn(async move { 150 | while let Some(message) = self.channel.recv().await { 151 | match message.to_string_newline() { 152 | Err(decode_error) => error!( 153 | "[ZMQ SENDER]: Error parsing message to string: {}", 154 | decode_error 155 | ), 156 | // For some Subscribers it appears that a "blank" topic causes it to never receive the message 157 | // This needs more investigation... 158 | // Right now it seems that any Sub who listens to the blank topic gets all of the messages, even 159 | // if they aren't sub'd to the topic we're broadcasting on. This should fix the issue with moronic (hello node) 160 | // zmq implementations not getting the message if the topic is blank. 161 | // TODO: verify this doesn't break other kinds of zmq implementations....Like perhaps acars_router itself? 162 | Ok(payload) => match self.socket.send(vec![&payload]).await { 163 | Ok(_) => (), 164 | Err(e) => error!( 165 | "[ZMQ SENDER]: Error sending message on 'acars' topic: {:?}", 166 | e 167 | ), 168 | }, 169 | } 170 | } 171 | }); 172 | } 173 | } 174 | -------------------------------------------------------------------------------- /test_data/acars: -------------------------------------------------------------------------------- 1 | {"assstat":"skipped","channel":2,"error":0,"freq":131.55,"label":"SQ","level":-25.5,"mode":"2","station_id":"MN-YPPH","text":"02XSPERYPPH03155S11557EV136975/","timestamp":1645287877.534974} 2 | {"assstat":"skipped","channel":2,"error":0,"freq":131.55,"label":"SQ","level":-14.2,"mode":"2","station_id":"MN-YPPH","text":"02XSPERYPPH03156S11558EV136975/","timestamp":1645287880.393597} 3 | {"assstat":"skipped","channel":2,"error":0,"freq":131.55,"label":"SQ","level":-25.8,"mode":"2","station_id":"MN-YPPH","text":"00XS","timestamp":1645287883.260193} 4 | {"assstat":"skipped","channel":1,"error":0,"freq":131.45,"label":"SQ","level":-12.7,"mode":"2","station_id":"MN-YPPH","text":"01XAPERYPPH1ARINC","timestamp":1645287972.39217} 5 | {"assstat":"skipped","channel":2,"error":0,"freq":131.55,"label":"SQ","level":-25.2,"mode":"2","station_id":"MN-YPPH","text":"02XSPERYPPH03155S11557EV136975/","timestamp":1645287998.527162} 6 | {"assstat":"skipped","channel":2,"error":0,"freq":131.55,"label":"SQ","level":-14.4,"mode":"2","station_id":"MN-YPPH","text":"02XSPERYPPH03156S11558EV136975/","timestamp":1645288001.394173} 7 | {"assstat":"skipped","channel":2,"error":0,"freq":131.55,"label":"SQ","level":-14.3,"mode":"2","station_id":"MN-YPPH","text":"00XS","timestamp":1645288028.263864} 8 | {"assstat":"skipped","channel":1,"error":0,"freq":131.45,"label":"SQ","level":-12.9,"mode":"2","station_id":"MN-YPPH","text":"01XAPERYPPH1ARINC","timestamp":1645288086.834706} 9 | {"assstat":"skipped","channel":2,"error":0,"freq":131.55,"label":"SQ","level":-25.4,"mode":"2","station_id":"MN-YPPH","text":"02XSPERYPPH03155S11557EV136975/","timestamp":1645288119.517793} 10 | {"assstat":"skipped","channel":2,"error":0,"freq":131.55,"label":"SQ","level":-14.2,"mode":"2","station_id":"MN-YPPH","text":"02XSPERYPPH03156S11558EV136975/","timestamp":1645288122.390297} 11 | {"assstat":"skipped","channel":1,"error":0,"freq":131.45,"label":"SQ","level":-12.8,"mode":"2","station_id":"MN-YPPH","text":"01XAPERYPPH1ARINC","timestamp":1645288197.345744} 12 | {"assstat":"skipped","channel":2,"error":0,"freq":131.55,"label":"SQ","level":-25.3,"mode":"2","station_id":"MN-YPPH","text":"02XSPERYPPH03155S11557EV136975/","timestamp":1645288240.759683} 13 | {"assstat":"skipped","channel":2,"error":0,"freq":131.55,"label":"SQ","level":-14.2,"mode":"2","station_id":"MN-YPPH","text":"02XSPERYPPH03156S11558EV136975/","timestamp":1645288243.386479} 14 | {"assstat":"skipped","channel":1,"error":0,"freq":131.45,"label":"SQ","level":-12.7,"mode":"2","station_id":"MN-YPPH","text":"01XAPERYPPH1ARINC","timestamp":1645288306.137946} 15 | {"assstat":"skipped","channel":2,"error":0,"freq":131.55,"label":"SQ","level":-25.2,"mode":"2","station_id":"MN-YPPH","text":"02XSPERYPPH03155S11557EV136975/","timestamp":1645288361.553705} 16 | {"assstat":"skipped","channel":2,"error":0,"freq":131.55,"label":"SQ","level":-14.5,"mode":"2","station_id":"MN-YPPH","text":"02XSPERYPPH03156S11558EV136975/","timestamp":1645288364.333972} 17 | {"assstat":"skipped","channel":1,"error":0,"freq":131.45,"label":"SQ","level":-12.9,"mode":"2","station_id":"MN-YPPH","text":"01XAPERYPPH1ARINC","timestamp":1645288410.698817} 18 | -------------------------------------------------------------------------------- /test_data/acars_other: -------------------------------------------------------------------------------- 1 | {"timestamp":1611612170.6690149,"station_id":"CS-KABQ-VDLM2","channel":2,"freq":136.975,"icao":11117965,"toaddr":1057098,"is_response":1,"is_onground":0} 2 | {"timestamp":1611612173.3643351,"station_id":"CS-KABQ-ACARS","channel":0,"freq":130.025,"level":-18,"error":0,"mode":"2","label":"_d","block_id":"K","ack":"4","tail":"N1902U"} 3 | {"timestamp":1611612179.696661,"station_id":"CS-KABQ-VDLM2","channel":2,"freq":136.975,"icao":11363733,"toaddr":1057098,"is_response":1,"is_onground":0} 4 | {"timestamp":1611612183.441473,"station_id":"CS-KABQ-ACARS","channel":0,"freq":130.025,"level":-18,"error":0,"mode":"2","label":"_d","block_id":"M","ack":"0","tail":"N465UA"} 5 | {"timestamp":1611612184.597754,"station_id":"CS-KABQ-ACARS","channel":3,"freq":131.550,"level":-12,"error":0,"mode":"2","label":"SQ","text":"02XAABQKABQ13502N10637WV136975/ARINC"} 6 | {"timestamp":1611612186.89314,"station_id":"CS-KABQ-ACARS","channel":3,"freq":131.550,"level":-13,"error":0,"mode":"2","label":"SQ","text":"02XAABQKABQ13502N10636WV136975/ARINC"} 7 | {"timestamp":1611612191.3944161,"station_id":"CS-KABQ-VDLM2","channel":2,"freq":136.975,"icao":11363733,"toaddr":1057098,"is_response":0,"is_onground":0,"mode":"2","label":"_d","block_id":"5","ack":"W","tail":"N962WN","flight":"WN0184","msgno":"S06A"} 8 | {"timestamp":1611612193.279712,"station_id":"CS-KABQ-VDLM2","channel":2,"freq":136.975,"icao":11363733,"toaddr":1057098,"is_response":0,"is_onground":0,"mode":"2","label":"_d","block_id":"6","ack":"A","tail":"N962WN","flight":"WN0184","msgno":"S07A"} 9 | {"timestamp":1611612195.507102,"station_id":"CS-KABQ-VDLM2","channel":2,"freq":136.975,"icao":11363733,"toaddr":1057098,"is_response":0,"is_onground":0,"mode":"2","label":"_d","block_id":"7","ack":"B","tail":"N962WN","flight":"WN0184","msgno":"S08A"} 10 | {"timestamp":1611612197.3093519,"station_id":"CS-KABQ-VDLM2","channel":2,"freq":136.975,"icao":11363733,"toaddr":1057098,"is_response":0,"is_onground":0,"mode":"2","label":"_d","block_id":"8","ack":"C","tail":"N962WN","flight":"WN0184","msgno":"S09A"} 11 | {"timestamp":1611612207.2800119,"station_id":"CS-KABQ-ACARS","channel":0,"freq":130.025,"level":-15,"error":0,"mode":"2","label":"_d","block_id":"5","ack":"L","tail":"N1902U","flight":"UA0338","msgno":"S63A"} 12 | {"timestamp":1611612211.0062449,"station_id":"CS-KABQ-VDLM2","channel":2,"freq":136.975,"icao":10635095,"toaddr":1057098,"is_response":0,"is_onground":0} 13 | {"timestamp":1611612212.3990171,"station_id":"CS-KABQ-VDLM2","channel":2,"freq":136.975,"icao":10934719,"toaddr":2513527,"is_response":0,"is_onground":0} 14 | {"timestamp":1611612212.75912,"station_id":"CS-KABQ-VDLM2","channel":2,"freq":136.975,"icao":11183684,"toaddr":1065514,"is_response":0,"is_onground":0,"mode":"2","label":"37","block_id":"9","ack":"!","tail":"N7856A","flight":"WN2621","msgno":"M64A","text":"04QE638\r\nS6OB2SBOW\r\n-O/SR//0)HOT6HBOT70O7)\r\n"} 15 | -------------------------------------------------------------------------------- /test_data/clean_up_after_test.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | set -x 4 | 5 | # Kill any socat instances 6 | killall -9 socat 7 | 8 | # Kill any python3 instances 9 | killall -9 python3 10 | 11 | # Clean up /tmp files 12 | rm -rf /tmp/acars* 13 | rm -rf /tmp/vdlm2* 14 | 15 | exit 0 16 | -------------------------------------------------------------------------------- /test_data/data_feeder_test_receiver_tcp.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | # Copyright (c) Mike Nye, Fred Clausen 4 | # 5 | # Licensed under the MIT license: https://opensource.org/licenses/MIT 6 | # Permission is granted to use, copy, modify, and redistribute the work. 7 | # Full license information available in the project LICENSE file. 8 | # 9 | 10 | # This program tests the LISTEN aspect of acars_router 11 | # We have to CONNECT to the acars_router and send a message 12 | 13 | import json 14 | import random 15 | import socket 16 | import time 17 | import sys 18 | import argparse 19 | from threading import Thread, Event # noqa: E402 20 | from collections import deque # noqa: E402 21 | 22 | thread_stop_event = Event() 23 | 24 | 25 | def TCPSocketListener(sock, queue): 26 | global thread_stop_event 27 | 28 | while not thread_stop_event.is_set(): 29 | try: 30 | data = sock.recv(3000) 31 | if data: 32 | try: 33 | data = json.loads(data.decode("utf-8")) 34 | queue.append(data) 35 | except Exception as e: 36 | print(f"Invalid data received: {e}") 37 | print(f"{data}") 38 | except socket.timeout: 39 | pass 40 | except Exception: 41 | return 42 | 43 | 44 | if __name__ == "__main__": 45 | parser = argparse.ArgumentParser(description="Test data feeder") 46 | parser.add_argument( 47 | "--check-for-dupes", action="store_true", help="Check for duplicate packets" 48 | ) 49 | 50 | args = parser.parse_args() 51 | TEST_PASSED = True 52 | test_messages = [] 53 | received_messages_queue_acars = deque() 54 | received_messages_queue_vdlm = deque() 55 | number_of_expected_acars_messages = 0 56 | number_of_expected_vdlm_messages = 0 57 | check_for_dupes = args.check_for_dupes 58 | 59 | with open("acars_other", "r") as acars: 60 | for line in acars: 61 | test_messages.append(json.loads(line)) 62 | number_of_expected_acars_messages += 1 63 | 64 | with open("vdlm2_other", "r") as vdlm: 65 | for line in vdlm: 66 | test_messages.append(json.loads(line)) 67 | number_of_expected_vdlm_messages += 1 68 | 69 | # sort the test_messages array randomly 70 | 71 | random.shuffle(test_messages) 72 | 73 | # Socket ports 74 | # inputs ACARS 75 | tcp_acars_port = 15550 76 | # inputs VDLM 77 | tcp_vdlm_port = 15555 78 | 79 | # Remote listening ports 80 | # ACARS 81 | tcp_acars_remote_port = 15551 82 | # VDLM 83 | tcp_vdlm_remote_port = 15556 84 | 85 | remote_ip = "127.0.0.1" 86 | 87 | # This is slow, but we're going to start each socket one at a time, and then move on to the next 88 | # the idea here is that the router will not be started at first and won't be actively connecting until 89 | # some indeterminate amount of time 90 | 91 | # TODO: Thread the socket connects, and then we wait for the sockets to all connect 92 | 93 | # VDLM2 94 | vdlm_sock_receive = socket.socket(socket.AF_INET, socket.SOCK_STREAM) 95 | vdlm_sock_receive.bind((remote_ip, tcp_vdlm_port)) 96 | vdlm_sock_receive.listen(5) 97 | vdlm_client, addr = vdlm_sock_receive.accept() 98 | 99 | thread_vdlm2_tcp_listener = Thread( 100 | target=TCPSocketListener, 101 | args=(vdlm_client, received_messages_queue_vdlm), 102 | ) 103 | thread_vdlm2_tcp_listener.start() 104 | 105 | # ACARS 106 | acars_sock_receive = socket.socket(socket.AF_INET, socket.SOCK_STREAM) 107 | acars_sock_receive.bind((remote_ip, tcp_acars_port)) 108 | acars_sock_receive.listen(5) 109 | acars_client, addr = acars_sock_receive.accept() 110 | thread_acars_tcp_listener = Thread( 111 | target=TCPSocketListener, 112 | args=(acars_client, received_messages_queue_acars), 113 | ) 114 | thread_acars_tcp_listener.start() 115 | 116 | # create all of the output sockets 117 | 118 | acars_sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) 119 | acars_sock.bind((remote_ip, tcp_acars_remote_port)) 120 | acars_sock.listen(5) 121 | acars_client_remote, addr = acars_sock.accept() 122 | 123 | vdlm_sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) 124 | vdlm_sock.bind((remote_ip, tcp_vdlm_remote_port)) 125 | vdlm_sock.listen(5) 126 | vdlm_client_remote, addr = vdlm_sock.accept() 127 | 128 | print( 129 | f"STARTING TCP SEND/RECEIVE {'DUPLICATION ' if check_for_dupes else ''}TEST\n\n" 130 | ) 131 | message_count = 0 132 | duplicated = 0 133 | for message in test_messages: 134 | print(f"Sending message {message_count + 1}") 135 | # Randomly decide if the message should be sent twice 136 | if random.randint(0, 10) < 3: 137 | send_twice = True 138 | duplicated += 1 139 | else: 140 | send_twice = False 141 | 142 | if "vdl2" in message: 143 | # replace message["vdlm"]["t"]["sec"] with current unix epoch time 144 | message["vdl2"]["t"]["sec"] = int(time.time()) 145 | vdlm_client_remote.sendto( 146 | json.dumps(message).encode() + b"\n", (remote_ip, tcp_vdlm_remote_port) 147 | ) 148 | if send_twice: 149 | time.sleep(0.2) 150 | print("Sending VDLM duplicate") 151 | vdlm_client_remote.sendto( 152 | json.dumps(message).encode() + b"\n", 153 | (remote_ip, tcp_vdlm_remote_port), 154 | ) 155 | else: 156 | message["timestamp"] = float(time.time()) 157 | acars_client_remote.sendto( 158 | json.dumps(message).encode() + b"\n", (remote_ip, tcp_acars_remote_port) 159 | ) 160 | if send_twice: 161 | time.sleep(0.2) 162 | print("Sending ACARS duplicate") 163 | acars_client_remote.sendto( 164 | json.dumps(message).encode() + b"\n", 165 | (remote_ip, tcp_acars_remote_port), 166 | ) 167 | 168 | message_count += 1 169 | time.sleep(0.5) 170 | 171 | time.sleep(5) 172 | print( 173 | f"TCP SEND/RECEIVE {'DUPLICATION' if check_for_dupes else ''}TEST COMPLETE\n\n" 174 | ) 175 | print(f"Sent {message_count} original messages") 176 | print(f"Sent {duplicated} duplicates") 177 | print(f"Sent {number_of_expected_acars_messages} of non dup ACARS messages") 178 | print(f"Sent {number_of_expected_vdlm_messages} of non dup VDLM messages") 179 | print(f"Sent {message_count + duplicated} total messages") 180 | print( 181 | f"Expected number of messages {number_of_expected_acars_messages + number_of_expected_vdlm_messages + (duplicated if not check_for_dupes else 0)}" 182 | ) 183 | print( 184 | f"Received number of messages {len(received_messages_queue_acars) + len(received_messages_queue_vdlm)}" 185 | ) 186 | 187 | if len(received_messages_queue_acars) + len( 188 | received_messages_queue_vdlm 189 | ) == number_of_expected_acars_messages + number_of_expected_vdlm_messages + ( 190 | duplicated if not check_for_dupes else 0 191 | ): 192 | print( 193 | f"TCP SEND/RECEIVE {'DUPLICATION ' if check_for_dupes else ''}TEST PASSED" 194 | ) 195 | else: 196 | print( 197 | f"TCP SEND/RECEIVE {'DUPLICATION ' if check_for_dupes else ''}TEST FAILED" 198 | ) 199 | TEST_PASSED = False 200 | 201 | # Clean up 202 | 203 | vdlm_client.close() 204 | vdlm_client = None 205 | acars_client.close() 206 | acars_client = None 207 | acars_client_remote.close() 208 | acars_client_remote = None 209 | vdlm_client_remote.close() 210 | vdlm_client_remote = None 211 | 212 | # stop all threads 213 | 214 | thread_stop_event.set() 215 | 216 | sys.exit(0 if TEST_PASSED else 1) 217 | -------------------------------------------------------------------------------- /test_data/data_feeder_test_sender_tcp.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | # Copyright (c) Mike Nye, Fred Clausen 4 | # 5 | # Licensed under the MIT license: https://opensource.org/licenses/MIT 6 | # Permission is granted to use, copy, modify, and redistribute the work. 7 | # Full license information available in the project LICENSE file. 8 | # 9 | 10 | # This program tests the LISTEN aspect of acars_router 11 | # We have to CONNECT to the acars_router and send a message 12 | 13 | import json 14 | import random 15 | import socket 16 | import time 17 | import sys 18 | import argparse 19 | from threading import Thread, Event # noqa: E402 20 | from collections import deque # noqa: E402 21 | 22 | thread_stop_event = Event() 23 | 24 | 25 | def TCPSocketListener(host, port, queue): 26 | global thread_stop_event 27 | 28 | sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) 29 | sock.settimeout(5) 30 | try: 31 | sock.connect((host, port)) 32 | except socket.error as msg: 33 | print("Error: " + str(msg)) 34 | 35 | return 36 | 37 | while not thread_stop_event.is_set(): 38 | try: 39 | data = sock.recv(3000) 40 | if data: 41 | try: 42 | data = json.loads(data.decode("utf-8")) 43 | queue.append(data) 44 | except Exception as e: 45 | print(f"Invalid data received: {e}") 46 | print(f"{data}") 47 | except socket.timeout: 48 | pass 49 | except Exception as e: 50 | print(e) 51 | 52 | 53 | if __name__ == "__main__": 54 | parser = argparse.ArgumentParser(description="Test data feeder") 55 | parser.add_argument( 56 | "--check-for-dupes", action="store_true", help="Check for duplicate packets" 57 | ) 58 | 59 | args = parser.parse_args() 60 | TEST_PASSED = True 61 | test_messages = [] 62 | received_messages_queue_acars = deque() 63 | received_messages_queue_vdlm = deque() 64 | number_of_expected_acars_messages = 0 65 | number_of_expected_vdlm_messages = 0 66 | check_for_dupes = args.check_for_dupes 67 | 68 | with open("acars_other", "r") as acars: 69 | for line in acars: 70 | test_messages.append(json.loads(line)) 71 | number_of_expected_acars_messages += 1 72 | 73 | with open("vdlm2_other", "r") as vdlm: 74 | for line in vdlm: 75 | test_messages.append(json.loads(line)) 76 | number_of_expected_vdlm_messages += 1 77 | 78 | # sort the test_messages array randomly 79 | 80 | random.shuffle(test_messages) 81 | 82 | # Socket ports 83 | # inputs ACARS 84 | tcp_acars_port = 15550 85 | # inputs VDLM 86 | tcp_vdlm_port = 15555 87 | 88 | # Remote listening ports 89 | # ACARS 90 | tcp_acars_remote_port = 15551 91 | # VDLM 92 | tcp_vdlm_remote_port = 15556 93 | 94 | remote_ip = "127.0.0.1" 95 | 96 | # VDLM2 97 | thread_vdlm2_tcp_listener = Thread( 98 | target=TCPSocketListener, 99 | args=(remote_ip, tcp_vdlm_port, received_messages_queue_vdlm), 100 | ) 101 | thread_vdlm2_tcp_listener.start() 102 | 103 | # ACARS 104 | 105 | thread_acars_tcp_listener = Thread( 106 | target=TCPSocketListener, 107 | args=(remote_ip, tcp_acars_port, received_messages_queue_acars), 108 | ) 109 | thread_acars_tcp_listener.start() 110 | 111 | # create all of the output sockets 112 | 113 | acars_sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) 114 | acars_sock.settimeout(1) 115 | acars_sock.connect((remote_ip, tcp_acars_remote_port)) 116 | 117 | vdlm_sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) 118 | vdlm_sock.settimeout(1) 119 | vdlm_sock.connect((remote_ip, tcp_vdlm_remote_port)) 120 | 121 | print( 122 | f"STARTING TCP SEND/RECEIVE {'DUPLICATION ' if check_for_dupes else ''}TEST\n\n" 123 | ) 124 | message_count = 0 125 | duplicated = 0 126 | for message in test_messages: 127 | print(f"Sending message {message_count + 1}") 128 | # Randomly decide if the message should be sent twice 129 | if random.randint(0, 10) < 3: 130 | send_twice = True 131 | duplicated += 1 132 | else: 133 | send_twice = False 134 | 135 | if "vdl2" in message: 136 | # replace message["vdlm"]["t"]["sec"] with current unix epoch time 137 | message["vdl2"]["t"]["sec"] = int(time.time()) 138 | vdlm_sock.sendto( 139 | json.dumps(message).encode() + b"\n", (remote_ip, tcp_vdlm_remote_port) 140 | ) 141 | if send_twice: 142 | time.sleep(0.2) 143 | print("Sending VDLM duplicate") 144 | vdlm_sock.sendto( 145 | json.dumps(message).encode() + b"\n", 146 | (remote_ip, tcp_vdlm_remote_port), 147 | ) 148 | else: 149 | message["timestamp"] = float(time.time()) 150 | acars_sock.sendto( 151 | json.dumps(message).encode() + b"\n", (remote_ip, tcp_acars_remote_port) 152 | ) 153 | if send_twice: 154 | time.sleep(0.2) 155 | print("Sending ACARS duplicate") 156 | acars_sock.sendto( 157 | json.dumps(message).encode() + b"\n", 158 | (remote_ip, tcp_acars_remote_port), 159 | ) 160 | 161 | message_count += 1 162 | time.sleep(0.5) 163 | 164 | time.sleep(5) 165 | print( 166 | f"TCP SEND/RECEIVE {'DUPLICATION' if check_for_dupes else ''}TEST COMPLETE\n\n" 167 | ) 168 | print(f"Sent {message_count} original messages") 169 | print(f"Sent {duplicated} duplicates") 170 | print(f"Sent {number_of_expected_acars_messages} of non dup ACARS messages") 171 | print(f"Sent {number_of_expected_vdlm_messages} of non dup VDLM messages") 172 | print(f"Sent {message_count + duplicated} total messages") 173 | print( 174 | f"Expected number of messages {number_of_expected_acars_messages + number_of_expected_vdlm_messages + (duplicated if not check_for_dupes else 0)}" 175 | ) 176 | print( 177 | f"Received number of messages {len(received_messages_queue_acars) + len(received_messages_queue_vdlm)}" 178 | ) 179 | 180 | if len(received_messages_queue_acars) + len( 181 | received_messages_queue_vdlm 182 | ) == number_of_expected_acars_messages + number_of_expected_vdlm_messages + ( 183 | duplicated if not check_for_dupes else 0 184 | ): 185 | print( 186 | f"TCP SEND/RECEIVE {'DUPLICATION ' if check_for_dupes else ''}TEST PASSED" 187 | ) 188 | else: 189 | print( 190 | f"TCP SEND/RECEIVE {'DUPLICATION ' if check_for_dupes else ''}TEST FAILED" 191 | ) 192 | TEST_PASSED = False 193 | 194 | # Clean up 195 | 196 | acars_sock.close() 197 | vdlm_sock.close() 198 | 199 | # stop all threads 200 | 201 | thread_stop_event.set() 202 | 203 | sys.exit(0 if TEST_PASSED else 1) 204 | -------------------------------------------------------------------------------- /test_data/data_feeder_test_udp.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | # Copyright (c) Mike Nye, Fred Clausen 4 | # 5 | # Licensed under the MIT license: https://opensource.org/licenses/MIT 6 | # Permission is granted to use, copy, modify, and redistribute the work. 7 | # Full license information available in the project LICENSE file. 8 | # 9 | 10 | import json 11 | import random 12 | import socket 13 | import time 14 | import sys 15 | import argparse 16 | from threading import Thread, Event # noqa: E402 17 | from collections import deque # noqa: E402 18 | 19 | thread_stop_event = Event() 20 | 21 | 22 | def UDPSocketListener(port, queue): 23 | global thread_stop_event 24 | invalid_packets = [b""] 25 | while not thread_stop_event.is_set(): 26 | try: 27 | sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) 28 | sock.settimeout(5) 29 | sock.bind(("", port)) 30 | data, _ = sock.recvfrom(25000) 31 | # this is totally crap logic. I'm going to assume there is no way 32 | # packets are going to be misordered or from another message 33 | if data: 34 | good_data = False 35 | 36 | if data.endswith(b"\n"): 37 | # This is the end of a message sequence. Try deserializing it 38 | try: 39 | data = json.loads(data.decode("utf-8")) 40 | queue.append(data) 41 | good_data = True 42 | invalid_packets = [b""] 43 | except Exception: 44 | pass 45 | finally: 46 | # if good_data is false that means we didn't successfully reassemble from the packet 47 | # loop through old packets to see if we can build a good message 48 | if not good_data: 49 | all_packets = b"" 50 | for packet in invalid_packets: 51 | try: 52 | all_packets += packet 53 | data_try = json.loads(all_packets + data) 54 | queue.append(data_try) 55 | break 56 | except Exception: 57 | pass 58 | invalid_packets = [b""] 59 | else: 60 | invalid_packets.append(data) 61 | except socket.timeout: 62 | pass 63 | except Exception as e: 64 | print(e) 65 | 66 | 67 | if __name__ == "__main__": 68 | parser = argparse.ArgumentParser(description="Test data feeder") 69 | parser.add_argument( 70 | "--check-for-dupes", action="store_true", help="Check for duplicate packets" 71 | ) 72 | 73 | parser.add_argument( 74 | "--check-for-no-proxy-id", action="store_true", help="No proxy id" 75 | ) 76 | parser.add_argument("--check-for-station-id", type=str, nargs="*", default="") 77 | parser.add_argument("--check-data-continuity", action="store_true") 78 | parser.add_argument("--fragment-packets", action="store_true") 79 | 80 | args = parser.parse_args() 81 | TEST_PASSED = True 82 | test_messages = [] 83 | received_messages_queue_acars = deque() 84 | received_messages_queue_vdlm = deque() 85 | number_of_expected_acars_messages = 0 86 | number_of_expected_vdlm_messages = 0 87 | check_for_dupes = args.check_for_dupes 88 | check_for_station_id = ( 89 | args.check_for_station_id[0] if args.check_for_station_id else None 90 | ) 91 | check_for_no_proxy_id = args.check_for_no_proxy_id 92 | check_data_continuity = args.check_data_continuity 93 | 94 | with open("acars_other", "r") as acars: 95 | for line in acars: 96 | test_messages.append(json.loads(line)) 97 | number_of_expected_acars_messages += 1 98 | 99 | with open("vdlm2_other", "r") as vdlm: 100 | for line in vdlm: 101 | test_messages.append(json.loads(line)) 102 | number_of_expected_vdlm_messages += 1 103 | 104 | # sort the test_messages array randomly 105 | 106 | random.shuffle(test_messages) 107 | 108 | # Socket ports 109 | # inputs ACARS 110 | udp_acars_port = 15550 111 | # inputs VDLM 112 | udp_vdlm_port = 15555 113 | 114 | # Remote listening ports 115 | # ACARS 116 | udp_acars_remote_port = 15551 117 | # VDLM 118 | udp_vdlm_remote_port = 15556 119 | 120 | remote_ip = "127.0.0.1" 121 | 122 | # VDLM2 123 | thread_vdlm2_udp_listener = Thread( 124 | target=UDPSocketListener, args=(udp_vdlm_port, received_messages_queue_vdlm) 125 | ) 126 | thread_vdlm2_udp_listener.start() 127 | 128 | # ACARS 129 | 130 | thread_acars_udp_listener = Thread( 131 | target=UDPSocketListener, args=(udp_acars_port, received_messages_queue_acars) 132 | ) 133 | thread_acars_udp_listener.start() 134 | 135 | # create all of the output sockets 136 | # UDP 137 | acars_sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) 138 | acars_sock.bind((remote_ip, 0)) 139 | 140 | vdlm_sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) 141 | vdlm_sock.bind((remote_ip, 0)) 142 | 143 | # # # TCP 144 | # sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) 145 | # sock.bind((remote_ip, 25555)) 146 | 147 | print( 148 | f"STARTING UDP SEND/RECEIVE {'DUPLICATION ' if check_for_dupes else ' '}TEST\n\n" 149 | ) 150 | message_count = 0 151 | duplicated = 0 152 | for message in test_messages: 153 | # UDP 154 | print(f"Sending message {message_count + 1}") 155 | 156 | # Randomly decide if the message should be sent twice 157 | if random.randint(0, 10) < 3: 158 | send_twice = True 159 | duplicated += 1 160 | else: 161 | send_twice = False 162 | 163 | if "vdl2" in message: 164 | # replace message["vdlm"]["t"]["sec"] with current unix epoch time 165 | message["vdl2"]["t"]["sec"] = int(time.time()) 166 | message_as_bytes = json.dumps(message).encode("utf-8") + b"\n" 167 | 168 | if not args.fragment_packets: 169 | vdlm_sock.sendto(message_as_bytes, (remote_ip, udp_vdlm_remote_port)) 170 | else: 171 | # fragment the message into packets of size 512 bytes 172 | for i in range(0, len(message_as_bytes), 512): 173 | # time.sleep(0.2) 174 | vdlm_sock.sendto( 175 | message_as_bytes[i : i + 512], # noqa 176 | (remote_ip, udp_vdlm_remote_port), 177 | ) 178 | if send_twice: 179 | time.sleep(0.2) 180 | print("Sending VDLM duplicate") 181 | if not args.fragment_packets: 182 | vdlm_sock.sendto( 183 | message_as_bytes, (remote_ip, udp_vdlm_remote_port) 184 | ) 185 | else: 186 | for i in range(0, len(message_as_bytes), 512): 187 | # time.sleep(0.2) 188 | vdlm_sock.sendto( 189 | message_as_bytes[i : i + 512], # noqa 190 | (remote_ip, udp_vdlm_remote_port), 191 | ) 192 | else: 193 | # We are rounding to avoid an issue where acars_router will truncate the time 194 | # and thus the continunity check will fail even though it's good (sort of) 195 | 196 | message["timestamp"] = round(float(time.time()), 3) 197 | message_as_bytes = json.dumps(message).encode("utf-8") + b"\n" 198 | 199 | if not args.fragment_packets: 200 | acars_sock.sendto(message_as_bytes, (remote_ip, udp_acars_remote_port)) 201 | else: 202 | # fragment the message into packets of size 512 bytes 203 | for i in range(0, len(message_as_bytes), 512): 204 | # time.sleep(0.2) 205 | acars_sock.sendto( 206 | message_as_bytes[i : i + 512], # noqa 207 | (remote_ip, udp_acars_remote_port), 208 | ) 209 | if send_twice: 210 | time.sleep(0.2) 211 | print("Sending ACARS duplicate") 212 | if not args.fragment_packets: 213 | acars_sock.sendto( 214 | message_as_bytes, 215 | (remote_ip, udp_acars_remote_port), 216 | ) 217 | else: 218 | for i in range(0, len(message_as_bytes), 512): 219 | # time.sleep(0.2) 220 | acars_sock.sendto( 221 | message_as_bytes[i : i + 512], # noqa 222 | (remote_ip, udp_acars_remote_port), 223 | ) 224 | 225 | message_count += 1 226 | time.sleep(0.5) 227 | 228 | time.sleep(5) 229 | print( 230 | f"UDP SEND/RECEIVE {'DUPLICATION' if check_for_dupes else ' '}TEST COMPLETE\n\n" 231 | ) 232 | print(f"Sent {message_count} original messages") 233 | print(f"Sent {duplicated} duplicates") 234 | print(f"Sent {number_of_expected_acars_messages} of non dup ACARS messages") 235 | print(f"Sent {number_of_expected_vdlm_messages} of non dup VDLM messages") 236 | print(f"Sent {message_count + duplicated} total messages") 237 | print( 238 | f"Expected number of messages {number_of_expected_acars_messages + number_of_expected_vdlm_messages + (duplicated if not check_for_dupes else 0)}" 239 | ) 240 | print( 241 | f"Received number of messages {len(received_messages_queue_acars) + len(received_messages_queue_vdlm)}" 242 | ) 243 | 244 | if len(received_messages_queue_acars) + len( 245 | received_messages_queue_vdlm 246 | ) == number_of_expected_acars_messages + number_of_expected_vdlm_messages + ( 247 | duplicated if not check_for_dupes else 0 248 | ): 249 | print( 250 | f"UDP SEND/RECEIVE {'DUPLICATION ' if check_for_dupes else ' '}TEST PASSED" 251 | ) 252 | else: 253 | print( 254 | f"UDP SEND/RECEIVE {'DUPLICATION ' if check_for_dupes else ' '}TEST FAILED" 255 | ) 256 | TEST_PASSED = False 257 | 258 | if check_for_no_proxy_id: 259 | proxy_pass = True 260 | print("Checking for no proxy ID") 261 | for message in received_messages_queue_acars: 262 | if "app" in message and "proxied" in message: 263 | print("Proxy ID found in ACARS message") 264 | TEST_PASSED = False 265 | proxy_pass = False 266 | for message in received_messages_queue_vdlm: 267 | if "proxied" in message["vdl2"]["app"]: 268 | print("Proxy ID found in VDLM message") 269 | TEST_PASSED = False 270 | proxy_pass = False 271 | 272 | if proxy_pass: 273 | print("Proxy ID check passed") 274 | else: 275 | print("Proxy ID check failed") 276 | else: 277 | proxy_pass = True 278 | print("Checking for proxy ID") 279 | for message in received_messages_queue_acars: 280 | if "app" not in message and "proxied" not in message: 281 | print("Proxy ID not found in ACARS message") 282 | TEST_PASSED = False 283 | proxy_pass = False 284 | for message in received_messages_queue_vdlm: 285 | if "proxied" not in message["vdl2"]["app"]: 286 | print("Proxy ID not found in VDLM message") 287 | TEST_PASSED = False 288 | proxy_pass = False 289 | 290 | if proxy_pass: 291 | print("Proxy ID check passed") 292 | else: 293 | print("Proxy ID check failed") 294 | 295 | if check_for_station_id: 296 | station_pass = True 297 | print("Checking for station ID") 298 | for message in received_messages_queue_acars: 299 | if "station_id" not in message: 300 | print("Station ID not found in ACARS message") 301 | TEST_PASSED = False 302 | station_pass = False 303 | elif message["station_id"] != check_for_station_id: 304 | print( 305 | "Station ID does not match expected value in ACARS message. Found {}".format( 306 | message["station_id"] 307 | ) 308 | ) 309 | TEST_PASSED = False 310 | station_pass = False 311 | for message in received_messages_queue_vdlm: 312 | if "station" not in message["vdl2"]: 313 | print("Station ID not found in VDLM message") 314 | TEST_PASSED = False 315 | station_pass = False 316 | elif message["vdl2"]["station"] != check_for_station_id: 317 | print( 318 | "Station ID does not match expected value in VDLM message. Found {}".format( 319 | message["vdl2"]["station"] 320 | ) 321 | ) 322 | TEST_PASSED = False 323 | station_pass = False 324 | 325 | if station_pass: 326 | print("Station ID check passed") 327 | else: 328 | print("Station ID check failed") 329 | 330 | if check_data_continuity: 331 | data_is_good = True 332 | print("Checking data continuity") 333 | for message in received_messages_queue_acars: 334 | if message not in test_messages: 335 | print("ACARS message not found in test messages") 336 | TEST_PASSED = False 337 | data_is_good = False 338 | for message in received_messages_queue_vdlm: 339 | if message not in test_messages: 340 | print("VDLM message not found in test messages") 341 | TEST_PASSED = False 342 | data_is_good = False 343 | 344 | if data_is_good: 345 | print("Data continuity check passed") 346 | else: 347 | print("Data continuity check failed") 348 | 349 | # Clean up 350 | 351 | print("Cleaning up sockets....standby") 352 | acars_sock.close() 353 | vdlm_sock.close() 354 | 355 | # stop all threads 356 | 357 | thread_stop_event.set() 358 | print("Done") 359 | sys.exit(0 if TEST_PASSED else 1) 360 | -------------------------------------------------------------------------------- /test_data/data_feeder_test_zmq.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | # Copyright (c) Mike Nye, Fred Clausen 4 | # 5 | # Licensed under the MIT license: https://opensource.org/licenses/MIT 6 | # Permission is granted to use, copy, modify, and redistribute the work. 7 | # Full license information available in the project LICENSE file. 8 | # 9 | 10 | # This program tests the LISTEN aspect of acars_router 11 | # We have to CONNECT to the acars_router and send a message 12 | 13 | import json 14 | import random 15 | import socket 16 | import time 17 | import sys 18 | import argparse 19 | import zmq 20 | from threading import Thread, Event # noqa: E402 21 | from collections import deque # noqa: E402 22 | 23 | thread_stop_event = Event() 24 | 25 | 26 | def ZMQSocketSender(host, port, queue): 27 | global thread_stop_event 28 | 29 | try: 30 | context = zmq.Context() 31 | publisher = context.socket(zmq.PUB) 32 | publisher.bind(f"tcp://{host}:{port}") 33 | except Exception as e: 34 | print("Error: %s" % e) 35 | return 36 | 37 | while not thread_stop_event.is_set(): 38 | try: 39 | while len(queue) > 0: 40 | data = queue.popleft() 41 | publisher.send_multipart( 42 | [ 43 | json.dumps(data).encode(), 44 | ] 45 | ) 46 | 47 | time.sleep(0.1) 48 | except Exception as e: 49 | print(e) 50 | time.sleep(0.1) 51 | 52 | publisher.close() 53 | 54 | 55 | def ZMQSocketListener(subscriber, queue): 56 | global thread_stop_event 57 | 58 | while not thread_stop_event.is_set(): 59 | try: 60 | data = subscriber.recv_multipart() 61 | if data: 62 | try: 63 | queue.append(data) 64 | except Exception as e: 65 | print(f"Invalid data received: {e}") 66 | print(f"{data}") 67 | except socket.timeout: 68 | pass 69 | except Exception as e: 70 | print(e) 71 | 72 | 73 | if __name__ == "__main__": 74 | parser = argparse.ArgumentParser(description="Test data feeder") 75 | parser.add_argument( 76 | "--check-for-dupes", action="store_true", help="Check for duplicate packets" 77 | ) 78 | 79 | args = parser.parse_args() 80 | TEST_PASSED = True 81 | test_messages = [] 82 | received_messages_queue_acars = deque() 83 | received_messages_queue_vdlm = deque() 84 | output_messages_queue_vdlm = deque() 85 | number_of_expected_acars_messages = 0 86 | number_of_expected_vdlm_messages = 0 87 | check_for_dupes = args.check_for_dupes 88 | 89 | with open("acars_other", "r") as acars: 90 | for line in acars: 91 | test_messages.append(json.loads(line)) 92 | number_of_expected_acars_messages += 1 93 | 94 | with open("vdlm2_other", "r") as vdlm: 95 | for line in vdlm: 96 | test_messages.append(json.loads(line)) 97 | number_of_expected_vdlm_messages += 1 98 | 99 | # sort the test_messages array randomly 100 | 101 | random.shuffle(test_messages) 102 | 103 | # Socket ports 104 | # inputs ACARS 105 | tcp_acars_port = 15550 106 | # inputs VDLM 107 | tcp_vdlm_port = 15555 108 | 109 | # Remote listening ports 110 | # ACARS 111 | udp_acars_remote_port = 15551 112 | # VDLM 113 | tcp_vdlm_remote_port = 15556 114 | 115 | remote_ip = "127.0.0.1" 116 | 117 | try: 118 | vdlm_context = zmq.Context() 119 | vdlm_subscriber = vdlm_context.socket(zmq.SUB) 120 | vdlm_subscriber.connect("tcp://%s:%s" % (remote_ip, tcp_vdlm_port)) 121 | vdlm_subscriber.setsockopt(zmq.SUBSCRIBE, b"") 122 | except Exception as e: 123 | print("Error: %s" % e) 124 | 125 | try: 126 | acars_context = zmq.Context() 127 | acars_subscriber = acars_context.socket(zmq.SUB) 128 | acars_subscriber.connect("tcp://%s:%s" % (remote_ip, tcp_acars_port)) 129 | acars_subscriber.setsockopt(zmq.SUBSCRIBE, b"") 130 | except Exception as e: 131 | print("Error: %s" % e) 132 | 133 | # VDLM2 134 | thread_vdlm2_zmq_listener = Thread( 135 | target=ZMQSocketListener, 136 | args=(vdlm_subscriber, received_messages_queue_vdlm), 137 | ) 138 | thread_vdlm2_zmq_listener.start() 139 | 140 | # ACARS 141 | 142 | thread_acars_zmq_listener = Thread( 143 | target=ZMQSocketListener, 144 | args=(acars_subscriber, received_messages_queue_acars), 145 | ) 146 | thread_acars_zmq_listener.start() 147 | 148 | # create all of the output sockets 149 | # ACARS Router doesn't have a ZMQ ACARS input socket. We'll use UDP 150 | 151 | acars_sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) 152 | acars_sock.bind((remote_ip, 0)) 153 | 154 | vdlm_zmq_pub = Thread( 155 | target=ZMQSocketSender, 156 | args=("0.0.0.0", tcp_vdlm_remote_port, output_messages_queue_vdlm), 157 | ) 158 | vdlm_zmq_pub.start() 159 | # a hack to make sure acars_router has connected 160 | # Can we do this smarter? 161 | time.sleep(5) 162 | print( 163 | f"STARTING TCP SEND/RECEIVE {'DUPLICATION ' if check_for_dupes else ''}TEST\n\n" 164 | ) 165 | message_count = 0 166 | duplicated = 0 167 | for message in test_messages: 168 | print(f"Sending message {message_count + 1}") 169 | # Randomly decide if the message should be sent twice 170 | if random.randint(0, 10) < 3: 171 | send_twice = True 172 | duplicated += 1 173 | else: 174 | send_twice = False 175 | 176 | if "vdl2" in message: 177 | # replace message["vdlm"]["t"]["sec"] with current unix epoch time 178 | message["vdl2"]["t"]["sec"] = int(time.time()) 179 | output_messages_queue_vdlm.append(message) 180 | if send_twice: 181 | time.sleep(0.2) 182 | print("Sending VDLM duplicate") 183 | output_messages_queue_vdlm.append(message) 184 | else: 185 | message["timestamp"] = float(time.time()) 186 | acars_sock.sendto( 187 | json.dumps(message).encode() + b"\n", 188 | (remote_ip, udp_acars_remote_port), 189 | ) 190 | if send_twice: 191 | time.sleep(0.2) 192 | print("Sending ACARS duplicate") 193 | acars_sock.sendto( 194 | json.dumps(message).encode() + b"\n", 195 | (remote_ip, udp_acars_remote_port), 196 | ) 197 | 198 | message_count += 1 199 | time.sleep(0.5) 200 | 201 | time.sleep(5) 202 | print( 203 | f"TCP SEND/RECEIVE {'DUPLICATION' if check_for_dupes else ''}TEST COMPLETE\n\n" 204 | ) 205 | print(f"Sent {message_count} original messages") 206 | print(f"Sent {duplicated} duplicates") 207 | print(f"Sent {number_of_expected_acars_messages} of non dup ACARS messages") 208 | print(f"Sent {number_of_expected_vdlm_messages} of non dup VDLM messages") 209 | print(f"Sent {message_count + duplicated} total messages") 210 | print( 211 | f"Expected number of messages {number_of_expected_acars_messages + number_of_expected_vdlm_messages + (duplicated if not check_for_dupes else 0)}" 212 | ) 213 | print( 214 | f"Received number of messages {len(received_messages_queue_acars) + len(received_messages_queue_vdlm)}" 215 | ) 216 | 217 | if len(received_messages_queue_acars) + len( 218 | received_messages_queue_vdlm 219 | ) == number_of_expected_acars_messages + number_of_expected_vdlm_messages + ( 220 | duplicated if not check_for_dupes else 0 221 | ): 222 | print( 223 | f"TCP SEND/RECEIVE {'DUPLICATION ' if check_for_dupes else ''}TEST PASSED" 224 | ) 225 | else: 226 | print( 227 | f"TCP SEND/RECEIVE {'DUPLICATION ' if check_for_dupes else ''}TEST FAILED" 228 | ) 229 | TEST_PASSED = False 230 | 231 | thread_stop_event.set() 232 | # clean up 233 | acars_sock.close() 234 | acars_sock = None 235 | # destroy is def not the best way but it works 236 | vdlm_context.destroy() 237 | vdlm_context = None 238 | acars_context.destroy() 239 | acars_context = None 240 | # stop all threads 241 | 242 | sys.exit(0 if TEST_PASSED else 1) 243 | -------------------------------------------------------------------------------- /test_data/run_acars_ruster_test.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | # Copyright (c) Mike Nye, Fred Clausen 4 | # 5 | # Licensed under the MIT license: https://opensource.org/licenses/MIT 6 | # Permission is granted to use, copy, modify, and redistribute the work. 7 | # Full license information available in the project LICENSE file. 8 | # 9 | 10 | task_failed() { 11 | echo "Test failed" 12 | pkill acars_router 13 | exit 1 14 | } 15 | 16 | # run cargo test 17 | 18 | cargo test 19 | 20 | # UDP Tests 21 | # We will also check primary program logic with UDP here. All other tests will not check program logic 22 | # but will just ensure the router accepts / sends data as appropriate. 23 | 24 | # run acars_router with out deduping. Connection checking here 25 | 26 | echo "UDP Send/Receive without deduping" 27 | cargo run -- --add-proxy-id --listen-udp-acars 15551 --listen-udp-vdlm2 15556 --send-udp-acars 127.0.0.1:15550 --send-udp-vdlm2 127.0.0.1:15555 & 28 | sleep 30 29 | python3 data_feeder_test_udp.py || task_failed 30 | 31 | pkill acars_router 32 | echo "UDP Send/Receive without deduping PASS" 33 | 34 | echo "UDP Send/Receive with deduping AND small UDP packet size AND data continuity" 35 | 36 | cargo run -- --enable-dedupe --max-udp-packet-size 512 --listen-udp-acars 15551 --listen-udp-vdlm2 15556 --send-udp-acars 127.0.0.1:15550 --send-udp-vdlm2 127.0.0.1:15555 & 37 | sleep 3 38 | python3 data_feeder_test_udp.py --check-data-continuity --check-for-no-proxy-id --check-for-dupes || task_failed 39 | 40 | pkill acars_router 41 | 42 | echo "UDP Send/Receive without deduping AND small UDP packet size AND data continuity PASS" 43 | 44 | echo "UDP Send/Receive with deduping AND fragmented packets AND data continuity" 45 | cargo run -- --enable-dedupe --listen-udp-acars 15551 --listen-udp-vdlm2 15556 --send-udp-acars 127.0.0.1:15550 --send-udp-vdlm2 127.0.0.1:15555 & 46 | sleep 3 47 | python3 data_feeder_test_udp.py --check-data-continuity --check-for-no-proxy-id --check-for-dupes --fragment-packets || task_failed 48 | pkill acars_router 49 | echo "UDP Send/Receive with deduping AND fragmented packets AND data continuity PASS" 50 | 51 | # primary program logic checks 52 | 53 | # run acars_router with deduping 54 | 55 | echo "UDP Send/Receive with deduping" 56 | cargo run -- --add-proxy-id --listen-udp-acars 15551 --listen-udp-vdlm2 15556 --send-udp-acars 127.0.0.1:15550 --send-udp-vdlm2 127.0.0.1:15555 --enable-dedupe & 57 | sleep 3 58 | python3 data_feeder_test_udp.py --check-for-dupes || task_failed 59 | 60 | pkill acars_router 61 | echo "UDP Send/Receive with deduping PASS" 62 | 63 | echo "UDP Send/Receive and verify no proxied field" 64 | cargo run -- --listen-udp-acars 15551 --listen-udp-vdlm2 15556 --send-udp-acars 127.0.0.1:15550 --send-udp-vdlm2 127.0.0.1:15555 --enable-dedupe & 65 | sleep 3 66 | python3 data_feeder_test_udp.py --check-for-dupes --check-for-no-proxy-id || task_failed 67 | 68 | pkill acars_router 69 | echo "UDP Send/Receive and verify no proxied field PASS" 70 | 71 | echo "UDP Send/Receive and verify station id is replaced" 72 | 73 | cargo run -- --add-proxy-id --listen-udp-acars 15551 --listen-udp-vdlm2 15556 --send-udp-acars 127.0.0.1:15550 --send-udp-vdlm2 127.0.0.1:15555 --enable-dedupe --override-station-name TEST & 74 | sleep 3 75 | python3 data_feeder_test_udp.py --check-for-dupes --check-for-station-id TEST || task_failed 76 | 77 | pkill acars_router 78 | echo "UDP Send/Receive and verify station id is replaced PASS" 79 | 80 | echo "UDP Send/Receive data continuity" 81 | 82 | cargo run -- --listen-udp-acars 15551 --listen-udp-vdlm2 15556 --send-udp-acars 127.0.0.1:15550 --send-udp-vdlm2 127.0.0.1:15555 --enable-dedupe & 83 | sleep 3 84 | python3 data_feeder_test_udp.py --check-for-dupes --check-data-continuity --check-for-no-proxy-id || task_failed 85 | 86 | pkill acars_router 87 | echo "UDP Send/Receive data continuity PASS" 88 | 89 | ### UDP COMPLETE 90 | 91 | # TCP SEND / LISTEN 92 | 93 | echo "TCP Send/Receive with deduping" 94 | 95 | cargo run -- --add-proxy-id --listen-tcp-acars 15551 --listen-tcp-vdlm2 15556 --serve-tcp-acars 15550 --serve-tcp-vdlm2 15555 --enable-dedupe & 96 | sleep 3 97 | python3 data_feeder_test_sender_tcp.py --check-for-dupes || task_failed 98 | 99 | pkill acars_router 100 | echo "TCP Send/Receive with deduping PASS" 101 | 102 | # echo "TCP Listen/Send with deduping" 103 | 104 | # cargo run -- --add-proxy-id --receive-tcp-acars 127.0.0.1:15551 --receive-tcp-vdlm2 127.0.0.1:15556 --send-tcp-acars 127.0.0.1:15550 --send-tcp-vdlm2 127.0.0.1:15555 --enable-dedupe & 105 | 106 | # python3 data_feeder_test_receiver_tcp.py --check-for-dupes || task_failed 107 | 108 | # pkill acars_router 109 | # echo "TCP Listen/Send with deduping PASS" 110 | 111 | # ZMQ SEND / LISTEN 112 | 113 | echo "ZMQ VDLM Send/UDP ACARS Send and ZMQ Receive" 114 | 115 | cargo run -- --add-proxy-id --listen-udp-acars 15551 --receive-zmq-vdlm2 127.0.0.1:15556 --serve-zmq-acars 15550 --serve-zmq-vdlm2 15555 --enable-dedupe & 116 | 117 | python3 data_feeder_test_zmq.py --check-for-dupes || task_failed 118 | sleep 3 119 | pkill acars_router 120 | echo "ZMQ VDLM Send/UDP ACARS Send and ZMQ Receive PASS" 121 | 122 | echo "ALL TESTS PASSED" 123 | exit 0 124 | -------------------------------------------------------------------------------- /test_data/send_lines.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | while IFS="" read -r p || [ -n "$p" ]; do 3 | #printf '%s\n' "$p" 4 | echo "$p" 5 | sleep 1 6 | done < "$1" 7 | -------------------------------------------------------------------------------- /test_data/test_tcplisten_tcpsend.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | set -xe 3 | 4 | PORT1=$(shuf -i 3000-3999 -n 1) 5 | PORT2=$(shuf -i 4000-4999 -n 1) 6 | 7 | # Start fake destination server for acars_router output 8 | socat -d -t5 TCP-LISTEN:"${PORT1}",fork OPEN:/tmp/"$1".tcplisten.tcpsend.out,create,append & 9 | sleep 1 10 | 11 | # Start acars_router 12 | python3 ./acars_router/acars_router.py -vv --add-proxy-id false --skew-window 300 --listen-tcp-"$1" "${PORT2}" --send-tcp-"$1" 127.0.0.1:"${PORT1}" & 13 | sleep 1 14 | 15 | # Send test data thru acars_router 16 | while IFS="" read -r p || [ -n "$p" ]; do 17 | printf '%s\n' "$p" | socat - TCP:127.0.0.1:"${PORT2}" 18 | done < ./test_data/"$1".patched 19 | 20 | # Re-format output files 21 | jq -M . < ./test_data/"$1".patched > /tmp/"$1".tcplisten.tcpsend.out.reference.reformatted 22 | jq -M . < /tmp/"$1".tcplisten.tcpsend.out > /tmp/"$1".tcplisten.tcpsend.out.reformatted 23 | 24 | # Check output 25 | diff /tmp/"$1".tcplisten.tcpsend.out.reference.reformatted /tmp/"$1".tcplisten.tcpsend.out.reformatted 26 | exit $? 27 | -------------------------------------------------------------------------------- /test_data/test_tcpreceive_tcpserve.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | PORT1=$(shuf -i 5000-5999 -n 1) 4 | PORT2=$(shuf -i 6000-6999 -n 1) 5 | 6 | # Start acars_router 7 | timeout 30s python3 ./acars_router/acars_router.py -vv --add-proxy-id false --skew-window 300 --receive-tcp-"$1"="127.0.0.1:${PORT1}" --serve-tcp-"$1" "${PORT2}" & 8 | sleep 1 9 | 10 | # Start fake destination server for acars_router output 11 | socat -d -t10 TCP:127.0.0.1:"${PORT2}" OPEN:/tmp/"$1".tcpreceive.tcpsserve.out,create,append & 12 | sleep 1 13 | 14 | # Start fake source server(s) 15 | while IFS="" read -r p || [ -n "$p" ]; do 16 | printf '%s' "$p" | socat -d TCP-LISTEN:"${PORT1}",reuseaddr STDIN 17 | done < ./test_data/"$1".patched 18 | sleep 10 19 | 20 | # Re-format output files 21 | jq -M . < ./test_data/"$1".patched > /tmp/"$1".tcpreceive.tcpsserve.out.reference.reformatted 22 | jq -M . < /tmp/"$1".tcpreceive.tcpsserve.out > /tmp/"$1".tcpreceive.tcpsserve.out.reformatted 23 | 24 | # Check output 25 | diff /tmp/"$1".tcpreceive.tcpsserve.out.reference.reformatted /tmp/"$1".tcpreceive.tcpsserve.out.reformatted 26 | exit $? 27 | -------------------------------------------------------------------------------- /test_data/test_udp.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | set -xe 3 | 4 | PORT1=$(shuf -i 1025-1999 -n 1) 5 | PORT2=$(shuf -i 2000-2999 -n 1) 6 | 7 | # Start fake destination server for acars_router output 8 | socat -d -t5 UDP-LISTEN:"${PORT1}",fork OPEN:/tmp/"$1".udp.out,create,append & 9 | sleep 1 10 | 11 | # Start acars_router 12 | python3 ./acars_router/acars_router.py -vv --add-proxy-id false --skew-window 300 --listen-udp-"$1" "${PORT2}" --send-udp-"$1" 127.0.0.1:"${PORT1}" & 13 | sleep 1 14 | 15 | # Send test data thru acars_router 16 | while IFS="" read -r p || [ -n "$p" ]; do 17 | printf '%s\n' "$p" | socat - UDP-DATAGRAM:127.0.0.1:"${PORT2}" 18 | done < ./test_data/"$1".patched 19 | 20 | # Re-format output files 21 | jq -M . < ./test_data/"$1".patched > /tmp/"$1".udp.out.reference.reformatted 22 | jq -M . < /tmp/"$1".udp.out > /tmp/"$1".udp.out.reformatted 23 | 24 | # Check output 25 | diff /tmp/"$1".udp.out.reference.reformatted /tmp/"$1".udp.out.reformatted 26 | exit $? 27 | -------------------------------------------------------------------------------- /test_data/test_udp_zmqserver.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | # TODO: Fix this test. 4 | 5 | set -xe 6 | 7 | PORT1=$(shuf -i 7000-7999 -n 1) 8 | PORT2=$(shuf -i 8000-8999 -n 1) 9 | 10 | # Start acars_router 11 | python3 ./acars_router/acars_router.py -vv --add-proxy-id false --skew-window 300 --listen-udp-"$1" "${PORT2}" --serve-zmq-"$1" "${PORT1}" & 12 | sleep 1 13 | 14 | # Process to receive output 15 | python3 ./acars_router/viewacars --port "${PORT1}" --protocol zmq | tee /tmp/"$1".zmqserver.out & 16 | sleep 1 17 | 18 | # Send test data thru acars_router 19 | while IFS="" read -r p || [ -n "$p" ]; do 20 | printf '%s\n' "$p" | socat - UDP-DATAGRAM:127.0.0.1:"${PORT2}" 21 | done < ./test_data/"$1".patched 22 | 23 | # Re-format output files 24 | jq -M . < ./test_data/"$1".patched > /tmp/"$1".udp.out.reference.reformatted 25 | jq -M . < /tmp/"$1".zmqserver.out > /tmp/"$1".zmqserver.out.reformatted 26 | 27 | echo "DEBUGGING" 28 | cat /tmp/"$1".zmqserver.out 29 | echo "END DEBUGGING" 30 | 31 | # Check output 32 | diff /tmp/"$1".udp.out.reference.reformatted /tmp/"$1".zmqserver.out.reformatted 33 | exit $? 34 | -------------------------------------------------------------------------------- /test_data/vdlm2: -------------------------------------------------------------------------------- 1 | {"vdl2":{"app":{"name":"dumpvdl2","ver":"2.1.1"},"avlc":{"cmd":"XID","cr":"Command","dst":{"addr":"FFFFFF","type":"Aircraft"},"frame_type":"U","pf":false,"src":{"addr":"281658","status":"On ground","type":"Ground station"},"xid":{"err":false,"pub_params":[{"name":"param_set_id","value":"8885:1993"},{"name":"procedure_classes","value":[0,1]},{"name":"hdlc_options","value":[32,164,136]}],"type":"GSIF","type_descr":"Ground Station Information Frame","vdl_params":[{"name":"param_set_id","value":"V"},{"name":"avlc_specific_options","value":[32]},{"name":"airport_coverage","value":"YPPH"},{"name":"atn_router_nets","value":[0,0,0,0,0,0]},{"name":"system_mask","value":["200000"]},{"name":"gs_location","value":{"lat":-31.9,"lon":116.0}}]}},"burst_len_octets":72,"freq":136975000,"freq_skew":2.708673,"hdr_bits_fixed":0,"idx":0,"noise_level":-45.031429,"octets_corrected_by_fec":0,"sig_level":-20.861259,"station":"MN-YPPH","t":{"sec":1645287903,"usec":757630}}} 2 | {"vdl2":{"app":{"name":"dumpvdl2","ver":"2.1.1"},"avlc":{"cmd":"XID","cr":"Command","dst":{"addr":"FFFFFF","type":"Aircraft"},"frame_type":"U","pf":false,"src":{"addr":"281657","status":"On ground","type":"Ground station"},"xid":{"err":false,"pub_params":[{"name":"param_set_id","value":"8885:1993"},{"name":"procedure_classes","value":[0,1]},{"name":"hdlc_options","value":[32,164,136]}],"type":"GSIF","type_descr":"Ground Station Information Frame","vdl_params":[{"name":"param_set_id","value":"V"},{"name":"avlc_specific_options","value":[36]},{"name":"airport_coverage","value":"YPPH"},{"name":"atn_router_nets","value":[0,0,0,0,0,0]},{"name":"system_mask","value":["200000"]},{"name":"gs_location","value":{"lat":-31.9,"lon":116.0}}]}},"burst_len_octets":72,"freq":136975000,"freq_skew":2.632803,"hdr_bits_fixed":0,"idx":0,"noise_level":-45.538578,"octets_corrected_by_fec":0,"sig_level":-27.019394,"station":"MN-YPPH","t":{"sec":1645288009,"usec":53580}}} 3 | {"vdl2":{"app":{"name":"dumpvdl2","ver":"2.1.1"},"avlc":{"cmd":"XID","cr":"Command","dst":{"addr":"FFFFFF","type":"Aircraft"},"frame_type":"U","pf":false,"src":{"addr":"281658","status":"On ground","type":"Ground station"},"xid":{"err":false,"pub_params":[{"name":"param_set_id","value":"8885:1993"},{"name":"procedure_classes","value":[0,1]},{"name":"hdlc_options","value":[32,164,136]}],"type":"GSIF","type_descr":"Ground Station Information Frame","vdl_params":[{"name":"param_set_id","value":"V"},{"name":"avlc_specific_options","value":[32]},{"name":"airport_coverage","value":"YPPH"},{"name":"atn_router_nets","value":[0,0,0,0,0,0]},{"name":"system_mask","value":["200000"]},{"name":"gs_location","value":{"lat":-31.9,"lon":116.0}}]}},"burst_len_octets":72,"freq":136975000,"freq_skew":2.638039,"hdr_bits_fixed":0,"idx":0,"noise_level":-45.410618,"octets_corrected_by_fec":0,"sig_level":-20.896025,"station":"MN-YPPH","t":{"sec":1645288016,"usec":826846}}} 4 | {"vdl2":{"app":{"name":"dumpvdl2","ver":"2.1.1"},"avlc":{"cmd":"XID","cr":"Command","dst":{"addr":"FFFFFF","type":"Aircraft"},"frame_type":"U","pf":false,"src":{"addr":"281657","status":"On ground","type":"Ground station"},"xid":{"err":false,"pub_params":[{"name":"param_set_id","value":"8885:1993"},{"name":"procedure_classes","value":[0,1]},{"name":"hdlc_options","value":[32,164,136]}],"type":"GSIF","type_descr":"Ground Station Information Frame","vdl_params":[{"name":"param_set_id","value":"V"},{"name":"avlc_specific_options","value":[36]},{"name":"airport_coverage","value":"YPPH"},{"name":"atn_router_nets","value":[0,0,0,0,0,0]},{"name":"system_mask","value":["200000"]},{"name":"gs_location","value":{"lat":-31.9,"lon":116.0}}]}},"burst_len_octets":72,"freq":136975000,"freq_skew":2.620316,"hdr_bits_fixed":0,"idx":0,"noise_level":-45.562218,"octets_corrected_by_fec":0,"sig_level":-26.891216,"station":"MN-YPPH","t":{"sec":1645288109,"usec":939004}}} 5 | {"vdl2":{"app":{"name":"dumpvdl2","ver":"2.1.1"},"avlc":{"cmd":"XID","cr":"Command","dst":{"addr":"FFFFFF","type":"Aircraft"},"frame_type":"U","pf":false,"src":{"addr":"281658","status":"On ground","type":"Ground station"},"xid":{"err":false,"pub_params":[{"name":"param_set_id","value":"8885:1993"},{"name":"procedure_classes","value":[0,1]},{"name":"hdlc_options","value":[32,164,136]}],"type":"GSIF","type_descr":"Ground Station Information Frame","vdl_params":[{"name":"param_set_id","value":"V"},{"name":"avlc_specific_options","value":[32]},{"name":"airport_coverage","value":"YPPH"},{"name":"atn_router_nets","value":[0,0,0,0,0,0]},{"name":"system_mask","value":["200000"]},{"name":"gs_location","value":{"lat":-31.9,"lon":116.0}}]}},"burst_len_octets":72,"freq":136975000,"freq_skew":2.67743,"hdr_bits_fixed":0,"idx":0,"noise_level":-45.26786,"octets_corrected_by_fec":0,"sig_level":-20.938797,"station":"MN-YPPH","t":{"sec":1645288123,"usec":796890}}} 6 | {"vdl2":{"app":{"name":"dumpvdl2","ver":"2.1.1"},"avlc":{"cmd":"XID","cr":"Command","dst":{"addr":"FFFFFF","type":"Aircraft"},"frame_type":"U","pf":false,"src":{"addr":"281657","status":"On ground","type":"Ground station"},"xid":{"err":false,"pub_params":[{"name":"param_set_id","value":"8885:1993"},{"name":"procedure_classes","value":[0,1]},{"name":"hdlc_options","value":[32,164,136]}],"type":"GSIF","type_descr":"Ground Station Information Frame","vdl_params":[{"name":"param_set_id","value":"V"},{"name":"avlc_specific_options","value":[36]},{"name":"airport_coverage","value":"YPPH"},{"name":"atn_router_nets","value":[0,0,0,0,0,0]},{"name":"system_mask","value":["200000"]},{"name":"gs_location","value":{"lat":-31.9,"lon":116.0}}]}},"burst_len_octets":72,"freq":136975000,"freq_skew":2.538019,"hdr_bits_fixed":0,"idx":0,"noise_level":-45.177952,"octets_corrected_by_fec":0,"sig_level":-27.120708,"station":"MN-YPPH","t":{"sec":1645288210,"usec":959160}}} 7 | {"vdl2":{"app":{"name":"dumpvdl2","ver":"2.1.1"},"avlc":{"cmd":"XID","cr":"Command","dst":{"addr":"FFFFFF","type":"Aircraft"},"frame_type":"U","pf":false,"src":{"addr":"281658","status":"On ground","type":"Ground station"},"xid":{"err":false,"pub_params":[{"name":"param_set_id","value":"8885:1993"},{"name":"procedure_classes","value":[0,1]},{"name":"hdlc_options","value":[32,164,136]}],"type":"GSIF","type_descr":"Ground Station Information Frame","vdl_params":[{"name":"param_set_id","value":"V"},{"name":"avlc_specific_options","value":[32]},{"name":"airport_coverage","value":"YPPH"},{"name":"atn_router_nets","value":[0,0,0,0,0,0]},{"name":"system_mask","value":["200000"]},{"name":"gs_location","value":{"lat":-31.9,"lon":116.0}}]}},"burst_len_octets":72,"freq":136975000,"freq_skew":2.581854,"hdr_bits_fixed":0,"idx":0,"noise_level":-45.680626,"octets_corrected_by_fec":0,"sig_level":-21.023952,"station":"MN-YPPH","t":{"sec":1645288231,"usec":691200}}} 8 | {"vdl2":{"app":{"name":"dumpvdl2","ver":"2.1.1"},"avlc":{"cmd":"XID","cr":"Command","dst":{"addr":"FFFFFF","type":"Aircraft"},"frame_type":"U","pf":false,"src":{"addr":"281657","status":"On ground","type":"Ground station"},"xid":{"err":false,"pub_params":[{"name":"param_set_id","value":"8885:1993"},{"name":"procedure_classes","value":[0,1]},{"name":"hdlc_options","value":[32,164,136]}],"type":"GSIF","type_descr":"Ground Station Information Frame","vdl_params":[{"name":"param_set_id","value":"V"},{"name":"avlc_specific_options","value":[36]},{"name":"airport_coverage","value":"YPPH"},{"name":"atn_router_nets","value":[0,0,0,0,0,0]},{"name":"system_mask","value":["200000"]},{"name":"gs_location","value":{"lat":-31.9,"lon":116.0}}]}},"burst_len_octets":72,"freq":136975000,"freq_skew":2.397069,"hdr_bits_fixed":1,"idx":0,"noise_level":-45.633335,"octets_corrected_by_fec":0,"sig_level":-26.966627,"station":"MN-YPPH","t":{"sec":1645288311,"usec":992160}}} 9 | {"vdl2":{"app":{"name":"dumpvdl2","ver":"2.1.1"},"avlc":{"cmd":"XID","cr":"Command","dst":{"addr":"FFFFFF","type":"Aircraft"},"frame_type":"U","pf":false,"src":{"addr":"281658","status":"On ground","type":"Ground station"},"xid":{"err":false,"pub_params":[{"name":"param_set_id","value":"8885:1993"},{"name":"procedure_classes","value":[0,1]},{"name":"hdlc_options","value":[32,164,136]}],"type":"GSIF","type_descr":"Ground Station Information Frame","vdl_params":[{"name":"param_set_id","value":"V"},{"name":"avlc_specific_options","value":[32]},{"name":"airport_coverage","value":"YPPH"},{"name":"atn_router_nets","value":[0,0,0,0,0,0]},{"name":"system_mask","value":["200000"]},{"name":"gs_location","value":{"lat":-31.9,"lon":116.0}}]}},"burst_len_octets":72,"freq":136975000,"freq_skew":2.731552,"hdr_bits_fixed":0,"idx":0,"noise_level":-45.573734,"octets_corrected_by_fec":0,"sig_level":-20.742586,"station":"MN-YPPH","t":{"sec":1645288338,"usec":808442}}} 10 | {"vdl2":{"app":{"name":"dumpvdl2","ver":"2.1.1"},"avlc":{"cmd":"XID","cr":"Command","dst":{"addr":"FFFFFF","type":"Aircraft"},"frame_type":"U","pf":false,"src":{"addr":"281657","status":"On ground","type":"Ground station"},"xid":{"err":false,"pub_params":[{"name":"param_set_id","value":"8885:1993"},{"name":"procedure_classes","value":[0,1]},{"name":"hdlc_options","value":[32,164,136]}],"type":"GSIF","type_descr":"Ground Station Information Frame","vdl_params":[{"name":"param_set_id","value":"V"},{"name":"avlc_specific_options","value":[36]},{"name":"airport_coverage","value":"YPPH"},{"name":"atn_router_nets","value":[0,0,0,0,0,0]},{"name":"system_mask","value":["200000"]},{"name":"gs_location","value":{"lat":-31.9,"lon":116.0}}]}},"burst_len_octets":72,"freq":136975000,"freq_skew":2.52021,"hdr_bits_fixed":0,"idx":0,"noise_level":-45.52002,"octets_corrected_by_fec":1,"sig_level":-27.088142,"station":"MN-YPPH","t":{"sec":1645288424,"usec":38515}}} 11 | -------------------------------------------------------------------------------- /test_data/vdlm2_other: -------------------------------------------------------------------------------- 1 | {"vdl2":{"app":{"name":"dumpvdl2","ver":"2.1.1"},"station":"CS-KABQ-VDLM","t":{"sec":1641834946,"usec":141558},"freq":136975000,"burst_len_octets":101,"hdr_bits_fixed":0,"octets_corrected_by_fec":0,"idx":0,"sig_level":-2.879317,"noise_level":-40.696533,"freq_skew":2.540755,"avlc":{"src":{"addr":"AA73A0","type":"Aircraft","status":"Airborne"},"dst":{"addr":"10214A","type":"Ground station"},"cr":"Command","frame_type":"I","rseq":4,"sseq":2,"poll":false,"acars":{"err":false,"crc_ok":true,"more":false,"reg":".N7726A","mode":"2","label":"H1","blk_id":"9","ack":"!","flight":"WN0720","msg_num":"F79","msg_num_seq":"A","sublabel":"M1","mfi":"B6","msg_text":"QXHADS2.ADS.N7726A033B07191935A0EF88C98EC00F03E7","arinc622":{"msg_type":"adsc_msg","crc_ok":true,"gs_addr":"QXHADS2","air_addr":".N7726A","adsc":{"tags":[{"ack":{"contract_num":59}},{"basic_report":{"lat":35.294609,"lon":-106.710548,"alt":35992,"ts_sec":944.000000,"pos_accuracy_nm":0.050000,"nav_redundancy":true,"tcas_avail":false}}],"err":false}}}}}} 2 | {"vdl2":{"app":{"name":"dumpvdl2","ver":"2.1.1"},"station":"CS-KABQ-VDLM","t":{"sec":1641834948,"usec":432861},"freq":136975000,"burst_len_octets":13,"hdr_bits_fixed":0,"octets_corrected_by_fec":0,"idx":0,"sig_level":-2.104055,"noise_level":-41.189682,"freq_skew":2.608414,"avlc":{"src":{"addr":"AA73A0","type":"Aircraft","status":"Airborne"},"dst":{"addr":"10214A","type":"Ground station"},"cr":"Response","frame_type":"S","cmd":"Receive Ready","pf":false,"rseq":3}}} 3 | {"vdl2":{"app":{"name":"dumpvdl2","ver":"2.1.1"},"station":"CS-KABQ-VDLM","t":{"sec":1641834949,"usec":344696},"freq":136975000,"burst_len_octets":117,"hdr_bits_fixed":0,"octets_corrected_by_fec":0,"idx":0,"sig_level":-4.741450,"noise_level":-41.072136,"freq_skew":-0.149714,"avlc":{"src":{"addr":"ABB94F","type":"Aircraft","status":"Airborne"},"dst":{"addr":"10214A","type":"Ground station"},"cr":"Command","frame_type":"I","rseq":1,"sseq":1,"poll":false,"acars":{"err":false,"crc_ok":true,"more":false,"reg":".N8545V","mode":"2","label":"H1","blk_id":"5","ack":"!","flight":"WN0209","msg_num":"D54","msg_num_seq":"A","sublabel":"DF","msg_text":"76401\r\n02E10KDALKLAX\r\nN35156W10725617153600M056094027G000X2300B87GX\r\n"}}}} 4 | {"vdl2":{"app":{"name":"dumpvdl2","ver":"2.1.1"},"station":"CS-KABQ-VDLM","t":{"sec":1641834951,"usec":933002},"freq":136975000,"burst_len_octets":44,"hdr_bits_fixed":0,"octets_corrected_by_fec":0,"idx":0,"sig_level":-18.949692,"noise_level":-40.930534,"freq_skew":0.190822,"avlc":{"src":{"addr":"A44917","type":"Aircraft","status":"Airborne"},"dst":{"addr":"10214A","type":"Ground station"},"cr":"Command","frame_type":"I","rseq":5,"sseq":5,"poll":false,"acars":{"err":false,"crc_ok":true,"more":false,"reg":".N37522","mode":"2","label":"_d","blk_id":"3","ack":"V","flight":"UA2445","msg_num":"S30","msg_num_seq":"A","msg_text":""}}}} 5 | {"vdl2":{"app":{"name":"dumpvdl2","ver":"2.1.1"},"station":"CS-KABQ-VDLM","t":{"sec":1641834953,"usec":454857},"freq":136975000,"burst_len_octets":44,"hdr_bits_fixed":0,"octets_corrected_by_fec":0,"idx":0,"sig_level":-18.903336,"noise_level":-41.222797,"freq_skew":-0.010151,"avlc":{"src":{"addr":"A44917","type":"Aircraft","status":"Airborne"},"dst":{"addr":"10214A","type":"Ground station"},"cr":"Command","frame_type":"I","rseq":6,"sseq":6,"poll":false,"acars":{"err":false,"crc_ok":true,"more":false,"reg":".N37522","mode":"2","label":"_d","blk_id":"4","ack":"W","flight":"UA2445","msg_num":"S31","msg_num_seq":"A","msg_text":""}}}} 6 | {"vdl2":{"app":{"name":"dumpvdl2","ver":"2.1.1"},"station":"CS-KABQ-VDLM","t":{"sec":1641834955,"usec":133812},"freq":136975000,"burst_len_octets":44,"hdr_bits_fixed":0,"octets_corrected_by_fec":0,"idx":0,"sig_level":-18.929493,"noise_level":-40.905464,"freq_skew":0.053589,"avlc":{"src":{"addr":"A44917","type":"Aircraft","status":"Airborne"},"dst":{"addr":"10214A","type":"Ground station"},"cr":"Command","frame_type":"I","rseq":7,"sseq":7,"poll":false,"acars":{"err":false,"crc_ok":true,"more":false,"reg":".N37522","mode":"2","label":"_d","blk_id":"5","ack":"A","flight":"UA2445","msg_num":"S32","msg_num_seq":"A","msg_text":""}}}} 7 | {"vdl2":{"app":{"name":"dumpvdl2","ver":"2.1.1"},"station":"CS-KABQ-VDLM","t":{"sec":1641834962,"usec":446094},"freq":136975000,"burst_len_octets":44,"hdr_bits_fixed":0,"octets_corrected_by_fec":0,"idx":0,"sig_level":-19.076710,"noise_level":-41.261475,"freq_skew":0.174641,"avlc":{"src":{"addr":"A44917","type":"Aircraft","status":"Airborne"},"dst":{"addr":"10214A","type":"Ground station"},"cr":"Command","frame_type":"I","rseq":0,"sseq":0,"poll":false,"acars":{"err":false,"crc_ok":true,"more":false,"reg":".N37522","mode":"2","label":"_d","blk_id":"6","ack":"B","flight":"UA2445","msg_num":"S33","msg_num_seq":"A","msg_text":""}}}} 8 | {"vdl2":{"app":{"name":"dumpvdl2","ver":"2.1.1"},"station":"CS-KABQ-VDLM","t":{"sec":1641834963,"usec":667166},"freq":136975000,"burst_len_octets":44,"hdr_bits_fixed":0,"octets_corrected_by_fec":0,"idx":0,"sig_level":-19.492603,"noise_level":-41.019703,"freq_skew":0.255131,"avlc":{"src":{"addr":"A44917","type":"Aircraft","status":"Airborne"},"dst":{"addr":"10214A","type":"Ground station"},"cr":"Command","frame_type":"I","rseq":1,"sseq":1,"poll":false,"acars":{"err":false,"crc_ok":true,"more":false,"reg":".N37522","mode":"2","label":"_d","blk_id":"7","ack":"C","flight":"UA2445","msg_num":"S34","msg_num_seq":"A","msg_text":""}}}} 9 | {"vdl2":{"app":{"name":"dumpvdl2","ver":"2.1.1"},"station":"CS-KABQ-VDLM","t":{"sec":1641834971,"usec":900620},"freq":136975000,"burst_len_octets":44,"hdr_bits_fixed":0,"octets_corrected_by_fec":0,"idx":0,"sig_level":-17.370626,"noise_level":-41.467407,"freq_skew":-0.029637,"avlc":{"src":{"addr":"A44917","type":"Aircraft","status":"Airborne"},"dst":{"addr":"10214A","type":"Ground station"},"cr":"Command","frame_type":"I","rseq":3,"sseq":3,"poll":false,"acars":{"err":false,"crc_ok":true,"more":false,"reg":".N37522","mode":"2","label":"_d","blk_id":"9","ack":"E","flight":"UA2445","msg_num":"S36","msg_num_seq":"A","msg_text":""}}}} 10 | --------------------------------------------------------------------------------