diff --git a/.github/workflows/publish-release.yml b/.github/workflows/publish-release.yml index 1923f8e..f9ad960 100644 --- a/.github/workflows/publish-release.yml +++ b/.github/workflows/publish-release.yml @@ -24,15 +24,11 @@ jobs: with: java-version: 11 - - name: Upload release - run: ./gradlew uploadArchives --no-daemon --no-parallel - env: - ORG_GRADLE_PROJECT_SONATYPE_NEXUS_USERNAME: ${{ secrets.SONATYPE_USERNAME }} - ORG_GRADLE_PROJECT_SONATYPE_NEXUS_PASSWORD: ${{ secrets.SONATYPE_PASSWORD }} - ORG_GRADLE_PROJECT_SIGNING_PRIVATE_KEY: ${{ secrets.GPG_SECRET_KEYS }} + - uses: gradle/gradle-build-action@v2 - name: Publish release - run: ./gradlew closeAndReleaseRepository --no-daemon --no-parallel + run: ./gradlew publishAllPublicationsToMavenCentralRepository env: - ORG_GRADLE_PROJECT_SONATYPE_NEXUS_USERNAME: ${{ secrets.SONATYPE_USERNAME }} - ORG_GRADLE_PROJECT_SONATYPE_NEXUS_PASSWORD: ${{ secrets.SONATYPE_PASSWORD }} + ORG_GRADLE_PROJECT_mavenCentralUsername: ${{ secrets.SONATYPE_USERNAME }} + ORG_GRADLE_PROJECT_mavenCentralPassword: ${{ secrets.SONATYPE_PASSWORD }} + ORG_GRADLE_PROJECT_signingInMemoryKey: ${{ secrets.GPG_SECRET_KEYS }} diff --git a/.github/workflows/publish-snapshot.yml b/.github/workflows/publish-snapshot.yml index 2941df7..787802d 100644 --- a/.github/workflows/publish-snapshot.yml +++ b/.github/workflows/publish-snapshot.yml @@ -19,12 +19,15 @@ jobs: with: java-version: 11 + - uses: gradle/gradle-build-action@v2 + - name: Retrieve version run: | - echo "VERSION_NAME=$(cat gradle.properties | grep -w 'VERSION_NAME' | cut -d'=' -f2)" >> $GITHUB_ENV + echo "VERSION_NAME=$(cat gradle.properties | grep -w "VERSION_NAME" | cut -d'=' -f2)" >> $GITHUB_ENV + - name: Publish snapshot - run: ./gradlew uploadArchives --no-daemon --no-parallel + run: ./gradlew publishAllPublicationsToMavenCentralRepository if: endsWith(env.VERSION_NAME, '-SNAPSHOT') env: - ORG_GRADLE_PROJECT_SONATYPE_NEXUS_USERNAME: ${{ secrets.SONATYPE_USERNAME }} - ORG_GRADLE_PROJECT_SONATYPE_NEXUS_PASSWORD: ${{ secrets.SONATYPE_PASSWORD }} + ORG_GRADLE_PROJECT_mavenCentralUsername: ${{ secrets.SONATYPE_USERNAME }} + ORG_GRADLE_PROJECT_mavenCentralPassword: ${{ secrets.SONATYPE_PASSWORD }} diff --git a/README.md b/README.md index 257d828..adc20a8 100644 --- a/README.md +++ b/README.md @@ -5,19 +5,19 @@ This library decodes ADS-B Messages and creates an easy to work with track objec Message support status -| Downlink Format | Human Readable | Supported | Note | -|-----------------|------------------------------------------|-----------|------| -| DF0 | Short Air-Air Surveillance | ✅ | | -| DF4 | Surveillance, Altitude request | ✅ | Updates Altitude information + SPI (Ident) | -| DF5 | Surveillance, Identity request | ✅ | Updates Callsign + SPI (Ident) | -| DF11 | MODE S only all-call | ✅ | Broadcasts ICAO Mode-S address | -| DF16 | Long Air-Air Surveillance | ✅ | Logs warning when ACAS RA is active | -| DF17 | Extended Squitter (Most ADS-B Data) | ⚠️ | Partial supported see DF17/18 status below | -| DF18 | Extended Squitter/Supplementary (Ground) | ⚠️ | Partial supported see DF17/18 status below | -| DF19 | Extended Squitter Military | ❌ | Not supported at this stage | -| DF20 | Comm-B Altitude | ⚠️ | Partial supported see DF20/21 status below | -| DF21 | Comm-B Identity | ⚠️ | Partial supported see DF20/21 status below | -| DF24 | Comm-B ELM | ❌ | Not supported at this stage | +| Downlink Format | Human Readable | Supported | Note | +|-----------------|------------------------------------------|-----------|-----------------------------------------------| +| DF0 | Short Air-Air Surveillance | ✅ | | +| DF4 | Surveillance, Altitude request | ✅ | Updates Altitude information + SPI (Ident) | +| DF5 | Surveillance, Identity request | ✅ | Updates Callsign + SPI (Ident) | +| DF11 | MODE S only all-call | ✅ | Broadcasts ICAO Mode-S address | +| DF16 | Long Air-Air Surveillance | ✅ | Logs warning when ACAS RA is active | +| DF17 | Extended Squitter (Most ADS-B Data) | ⚠️ | Partially supported, see DF17/18 status below | +| DF18 | Extended Squitter/Supplementary (Ground) | ⚠️ | Partially supported, see DF17/18 status below | +| DF19 | Extended Squitter Military | ❌ | Not supported at this stage | +| DF20 | Comm-B Altitude | ⚠️ | Partially supported, see DF20/21 status below | +| DF21 | Comm-B Identity | ⚠️ | Partially supported, see DF20/21 status below | +| DF24 | Comm-B ELM | ⚠️ | Implemented basic sequence no decoding. | ## DF17/DF18 - Extended Squitter @@ -27,48 +27,47 @@ DF17 is used for aircraft, while DF18 is used for other vessels (ground vehicles Most features of the DF17/18 protocol have been implemented, some message lack support for specific fields. -| Type Code | Human Readable | Supported | Note | -|-----------|------------------------------|-----------|------| -| 0 | Airborne/Surface No altitude | ✅ | -| 1 | Aircraft Identification | ✅ | -| 2 | Aircraft Identification | ✅ | -| 3 | Aircraft Identification | ✅ | -| 4 | Aircraft Identification | ✅ | -| 5 | Surface Position | ❌ | Not implemented yet -| 6 | Surface Position | ❌ | Not implemented yet -| 7 | Surface Position | ❌ | Not implemented yet -| 8 | Surface Position | ❌ | Not implemented yet -| 9 | Airborne Position | ✅ | -| 10 | Airborne Position | ✅ | -| 11 | Airborne Position | ✅ | -| 12 | Airborne Position | ✅ | -| 13 | Airborne Position | ✅ | -| 14 | Airborne Position | ✅ | -| 15 | Airborne Position | ✅ | -| 16 | Airborne Position | ✅ | -| 17 | Airborne Position | ✅ | -| 18 | Airborne Position | ✅ | -| 19 | Airborne Velocity | ✅ | -| 20 | Airborne Position | ✅ | -| 21 | Airborne Position | ✅ | -| 22 | Airborne Position | ✅ | -| 23 | Test Message | ✅ | -| 24 | Surface System Status | ❌ | Not implemented yet -| 25 | Reserved Message | ✅ | -| 26 | Reserved Message | ✅ | -| 27 | Reserved (Trajectory Change) | ✅ | -| 28 | Aircraft Status Message | ✅ | Priority mode A code (emergency) + TCAS/ACAS RA Broadcast -| 29 | Target Status Message | ✅ | Partial support -| 30 | Reserved Message | ✅ | -| 31 | Aircraft Operational Status | ✅ | Partial support - +| Type Code | Human Readable | Supported | Note | +|-----------|------------------------------|-----------|-----------------------------------------------------------| +| 0 | Airborne/Surface No altitude | ✅ | | +| 1 | Aircraft Identification | ✅ | | +| 2 | Aircraft Identification | ✅ | | +| 3 | Aircraft Identification | ✅ | | +| 4 | Aircraft Identification | ✅ | | +| 5 | Surface Position | ✅ | | +| 6 | Surface Position | ✅ | | +| 7 | Surface Position | ✅ | | +| 8 | Surface Position | ✅ | | +| 9 | Airborne Position | ✅ | | +| 10 | Airborne Position | ✅ | | +| 11 | Airborne Position | ✅ | | +| 12 | Airborne Position | ✅ | | +| 13 | Airborne Position | ✅ | | +| 14 | Airborne Position | ✅ | | +| 15 | Airborne Position | ✅ | | +| 16 | Airborne Position | ✅ | | +| 17 | Airborne Position | ✅ | | +| 18 | Airborne Position | ✅ | | +| 19 | Airborne Velocity | ✅ | | +| 20 | Airborne Position | ✅ | | +| 21 | Airborne Position | ✅ | | +| 22 | Airborne Position | ✅ | | +| 23 | Test Message | ✅ | | +| 24 | Surface System Status | ❌ | Not implemented yet | +| 25 | Reserved Message | ✅ | | +| 26 | Reserved Message | ✅ | | +| 27 | Reserved (Trajectory Change) | ✅ | | +| 28 | Aircraft Status Message | ✅ | Priority mode A code (emergency) + TCAS/ACAS RA Broadcast | +| 29 | Target Status Message | ✅ | Partial support | +| 30 | Reserved Message | ✅ | | +| 31 | Aircraft Operational Status | ✅ | Partial support | ## DF20/21 Comm-B DF20/21 messages are replies to data requests from a radar station, you'll only receive these messages if Mode-S radar is actively requesting this information. You will only receive messages requested by the radar. -Message is structure as follows +Message is structured as follows: ``` LSB |1----|6--|9----|14----|20-----------|33------------------------------------------------------|89-----------------------| @@ -96,57 +95,56 @@ You run through each BDS and pass the message to the decoder, if the message doe Repeat this process until you have a match. At this moment the coded logic has too many incorrect matches and thus decided to disable BDS guessing. -We are actively looking for a fix, or at least ability to enable this with a experimental flag. +We are actively looking for a fix, or at least the ability to enable this with an experimental flag. We hope with more BDS implemented the guessing accuracy will improve. -| BDS | Human Readable | Supported | Note | -|-----|--------------------------------------------|-----------|------| -| 1,0 | Data link capability report | ❌ | Detection implemented, decoding missing -| 1,7 | Common usage GICB capability report | ✅ | -| 1,8 | Mode S services GICB capability report | ❌ | -| 1,9 | Mode S services GICB capability report | ❌ | -| 1,A | Mode S services GICB capability report | ❌ | -| 1,B | Mode S services GICB capability report | ❌ | -| 1,C | Mode S services GICB capability report | ❌ | -| 1,D | Mode S services GICB capability report | ❌ | -| 1,E | Mode S services GICB capability report | ❌ | -| 1,F | Mode S services GICB capability report | ❌ | -| 2,0 | Aircraft Identification | ✅ | -| 2,1 | Aircraft and Airline registration marking | ✅️ | Experimental -| 2,2 | Antenna positions | ❌ | -| 2,5 | Antenna type | ❌ | -| 3,0 | ACAS Active resolution advisory | ❌ | Detection implemented, decoding missing -| 4,0 | Selected vertical intention | ✅️ | -| 4,1 | Next waypoint details | ❌ | 9 Characters -| 4,2 | Next waypoint details | ❌ | Waypoint lat/lon + crossing altitude -| 4,3 | Next waypoint details | ❌ | Bearing, time and distance to waypoint -| 4,4 | Meteorological routine air report | ✅ | -| 4,5 | Meteorological hazard report | ✅ | -| 4,8 | VHF Channel report | ❌ | Info on VHF 1/2/3 (frequency + status) & Guard status -| 5,0 | Track and turn report | ✅ | -| 5,1 | Position report coarse | ❌ | -| 5,2 | Position report fine | ❌ | -| 5,3 | Air-reference state vector | ✅ | -| 5,4 | Waypoint 1 | ❌ | 5 Chars, ETA, Estimated level, time to go -| 5,5 | Waypoint 2 | ❌ | 5 Chars, ETA, Estimated level, time to go -| 5,5 | Waypoint 3 | ❌ | 5 Chars, ETA, Estimated level, time to go -| 5,F | Quasi-static parameter monitoring | ❌ | -| 6,0 | Heading and speed report | ✅ | -| 6,1 | Priority/emergency status | ❌ | -| 6,5 | Aircraft operational status | ❌ | -| E,3 | Transponder type/part number | ❌ | -| E,4 | Transponder software revision number | ❌ | -| E,5 | ACAS type/part number | ❌ | -| E,6 | ACAS software revision number | ❌ | -| E,7 | Transponder status and diagnostics | ❌ | -| E,A | Vendor specific status and diagnostics | ❌ | -| F,1 | Military application | ❌ | -| F,2 | Military application | ❌ | - +| BDS | Human Readable | Supported | Note | +|-----|-------------------------------------------|-----------|-------------------------------------------------------| +| 1,0 | Data link capability report | ❌ | Detection implemented, decoding missing | +| 1,7 | Common usage GICB capability report | ✅ | | +| 1,8 | Mode S services GICB capability report | ❌ | | +| 1,9 | Mode S services GICB capability report | ❌ | | +| 1,A | Mode S services GICB capability report | ❌ | | +| 1,B | Mode S services GICB capability report | ❌ | | +| 1,C | Mode S services GICB capability report | ❌ | | +| 1,D | Mode S services GICB capability report | ❌ | | +| 1,E | Mode S services GICB capability report | ❌ | | +| 1,F | Mode S services GICB capability report | ❌ | | +| 2,0 | Aircraft Identification | ✅ | | +| 2,1 | Aircraft and Airline registration marking | ✅️ | Experimental | +| 2,2 | Antenna positions | ❌ | | +| 2,5 | Antenna type | ❌ | | +| 3,0 | ACAS Active resolution advisory | ❌ | Detection implemented, decoding missing | +| 4,0 | Selected vertical intention | ✅️ | | +| 4,1 | Next waypoint details | ❌ | 9 Characters | +| 4,2 | Next waypoint details | ❌ | Waypoint lat/lon + crossing altitude | +| 4,3 | Next waypoint details | ❌ | Bearing, time and distance to waypoint | +| 4,4 | Meteorological routine air report | ✅ | | +| 4,5 | Meteorological hazard report | ✅ | | +| 4,8 | VHF Channel report | ❌ | Info on VHF 1/2/3 (frequency + status) & Guard status | +| 5,0 | Track and turn report | ✅ | | +| 5,1 | Position report coarse | ❌ | | +| 5,2 | Position report fine | ❌ | | +| 5,3 | Air-reference state vector | ✅ | | +| 5,4 | Waypoint 1 | ❌ | 5 Chars, ETA, Estimated level, time to go | +| 5,5 | Waypoint 2 | ❌ | 5 Chars, ETA, Estimated level, time to go | +| 5,5 | Waypoint 3 | ❌ | 5 Chars, ETA, Estimated level, time to go | +| 5,F | Quasi-static parameter monitoring | ❌ | | +| 6,0 | Heading and speed report | ✅ | | +| 6,1 | Priority/emergency status | ❌ | | +| 6,5 | Aircraft operational status | ❌ | | +| E,3 | Transponder type/part number | ❌ | | +| E,4 | Transponder software revision number | ❌ | | +| E,5 | ACAS type/part number | ❌ | | +| E,6 | ACAS software revision number | ❌ | | +| E,7 | Transponder status and diagnostics | ❌ | | +| E,A | Vendor specific status and diagnostics | ❌ | | +| F,1 | Military application | ❌ | | +| F,2 | Military application | ❌ | | # Installation -This package is available through maven central +This package is available through Maven Central Pom ```xml @@ -159,7 +157,7 @@ Pom Gradle ``` - compile('aero.t2s:mode-s:0.2.0-SNAPSHOT') + compile('aero.t2s:mode-s:0.2.0-SNAPSHOT') ``` # Usage @@ -191,9 +189,9 @@ class Main ## Using Aircraft Database -This library is compatible with opensky dataset (https://opensky-network.org/datasets/metadata/). +This library is compatible with [OpenSky dataset](https://opensky-network.org/datasets/metadata/). -In order to use the database version you can start ModeS plugin as follows +In order to use the database version you can start ModeS plugin as follows: ```java class Main @@ -222,9 +220,9 @@ class Main # Contributing -You can contribute to this project by reporting/fixing bugs or implemented a new packet. +You can contribute to this project by reporting/fixing bugs or implement a new packet. We are always looking for help on this project. # License -This library is Apache 2.0 licensed read the full license [here](LICENSE). +This library is Apache 2.0 licensed. You can read the full license [here](LICENSE). diff --git a/build.gradle b/build.gradle index 366f138..b6a5e66 100644 --- a/build.gradle +++ b/build.gradle @@ -3,7 +3,7 @@ buildscript { mavenCentral() } dependencies { - classpath 'com.vanniktech:gradle-maven-publish-plugin:0.13.0' + classpath 'com.vanniktech:gradle-maven-publish-plugin:0.22.0' } } @@ -11,8 +11,18 @@ apply plugin: 'idea' apply plugin: 'java-library' apply plugin: "com.vanniktech.maven.publish" +def javaTargetVersion = 11 + +sourceCompatibility = javaTargetVersion + +subprojects { + tasks.withType(JavaCompile.class) { + options.release = javaTargetVersion + } +} + dependencies { - compile 'org.slf4j:slf4j-api:1.7.32' + api 'org.slf4j:slf4j-api:1.7.32' // Use JUnit test framework testImplementation 'org.junit.jupiter:junit-jupiter-api:5.8.2' @@ -24,15 +34,20 @@ repositories { mavenLocal() } -compileJava { - sourceCompatibility = 11 - targetCompatibility = 11 +tasks.withType(JavaCompile.class) { + options.release = javaTargetVersion } test { useJUnitPlatform() } + +mavenPublishing { + publishToMavenCentral() + signAllPublications() +} + signing { if (hasProperty('SIGNING_PRIVATE_KEY')) { useInMemoryPgpKeys(SIGNING_PRIVATE_KEY, "") diff --git a/example/.gitignore b/example/.gitignore deleted file mode 100644 index c379362..0000000 --- a/example/.gitignore +++ /dev/null @@ -1,3 +0,0 @@ -build/ -.gradle/ - diff --git a/example/build/classes/java/main/example/Demo$1.class b/example/build/classes/java/main/example/Demo$1.class deleted file mode 100644 index c222b03..0000000 Binary files a/example/build/classes/java/main/example/Demo$1.class and /dev/null differ diff --git a/example/build/classes/java/main/example/Demo$2.class b/example/build/classes/java/main/example/Demo$2.class deleted file mode 100644 index c1d57f1..0000000 Binary files a/example/build/classes/java/main/example/Demo$2.class and /dev/null differ diff --git a/example/build/classes/java/main/example/Demo.class b/example/build/classes/java/main/example/Demo.class deleted file mode 100644 index b902e3b..0000000 Binary files a/example/build/classes/java/main/example/Demo.class and /dev/null differ diff --git a/example/build/classes/java/main/example/FlightsTable.class b/example/build/classes/java/main/example/FlightsTable.class deleted file mode 100644 index 0c4acb6..0000000 Binary files a/example/build/classes/java/main/example/FlightsTable.class and /dev/null differ diff --git a/example/build/classes/java/main/example/flight/FlightFrame$1.class b/example/build/classes/java/main/example/flight/FlightFrame$1.class deleted file mode 100644 index d899d5b..0000000 Binary files a/example/build/classes/java/main/example/flight/FlightFrame$1.class and /dev/null differ diff --git a/example/build/classes/java/main/example/flight/FlightFrame$GcibTable.class b/example/build/classes/java/main/example/flight/FlightFrame$GcibTable.class deleted file mode 100644 index 76f0777..0000000 Binary files a/example/build/classes/java/main/example/flight/FlightFrame$GcibTable.class and /dev/null differ diff --git a/example/build/classes/java/main/example/flight/FlightFrame.class b/example/build/classes/java/main/example/flight/FlightFrame.class deleted file mode 100644 index 81f11cf..0000000 Binary files a/example/build/classes/java/main/example/flight/FlightFrame.class and /dev/null differ diff --git a/example/build/tmp/compileJava/source-classes-mapping.txt b/example/build/tmp/compileJava/source-classes-mapping.txt deleted file mode 100644 index a1ed673..0000000 --- a/example/build/tmp/compileJava/source-classes-mapping.txt +++ /dev/null @@ -1,10 +0,0 @@ -example/FlightsTable.java - example.FlightsTable -example/flight/FlightFrame.java - example.flight.FlightFrame - example.flight.FlightFrame$1 - example.flight.FlightFrame$GcibTable -example/Demo.java - example.Demo - example.Demo$1 - example.Demo$2 diff --git a/example/gradle/wrapper/gradle-wrapper.jar b/example/gradle/wrapper/gradle-wrapper.jar deleted file mode 100644 index 62d4c05..0000000 Binary files a/example/gradle/wrapper/gradle-wrapper.jar and /dev/null differ diff --git a/example/gradle/wrapper/gradle-wrapper.properties b/example/gradle/wrapper/gradle-wrapper.properties deleted file mode 100644 index 622ab64..0000000 --- a/example/gradle/wrapper/gradle-wrapper.properties +++ /dev/null @@ -1,5 +0,0 @@ -distributionBase=GRADLE_USER_HOME -distributionPath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-6.5-bin.zip -zipStoreBase=GRADLE_USER_HOME -zipStorePath=wrapper/dists diff --git a/example/gradlew b/example/gradlew deleted file mode 100755 index fbd7c51..0000000 --- a/example/gradlew +++ /dev/null @@ -1,185 +0,0 @@ -#!/usr/bin/env sh - -# -# Copyright 2015 the original author or authors. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# https://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -# - -############################################################################## -## -## Gradle start up script for UN*X -## -############################################################################## - -# Attempt to set APP_HOME -# Resolve links: $0 may be a link -PRG="$0" -# Need this for relative symlinks. -while [ -h "$PRG" ] ; do - ls=`ls -ld "$PRG"` - link=`expr "$ls" : '.*-> \(.*\)$'` - if expr "$link" : '/.*' > /dev/null; then - PRG="$link" - else - PRG=`dirname "$PRG"`"/$link" - fi -done -SAVED="`pwd`" -cd "`dirname \"$PRG\"`/" >/dev/null -APP_HOME="`pwd -P`" -cd "$SAVED" >/dev/null - -APP_NAME="Gradle" -APP_BASE_NAME=`basename "$0"` - -# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. -DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' - -# Use the maximum available, or set MAX_FD != -1 to use that value. -MAX_FD="maximum" - -warn () { - echo "$*" -} - -die () { - echo - echo "$*" - echo - exit 1 -} - -# OS specific support (must be 'true' or 'false'). -cygwin=false -msys=false -darwin=false -nonstop=false -case "`uname`" in - CYGWIN* ) - cygwin=true - ;; - Darwin* ) - darwin=true - ;; - MINGW* ) - msys=true - ;; - NONSTOP* ) - nonstop=true - ;; -esac - -CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar - - -# Determine the Java command to use to start the JVM. -if [ -n "$JAVA_HOME" ] ; then - if [ -x "$JAVA_HOME/jre/sh/java" ] ; then - # IBM's JDK on AIX uses strange locations for the executables - JAVACMD="$JAVA_HOME/jre/sh/java" - else - JAVACMD="$JAVA_HOME/bin/java" - fi - if [ ! -x "$JAVACMD" ] ; then - die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME - -Please set the JAVA_HOME variable in your environment to match the -location of your Java installation." - fi -else - JAVACMD="java" - which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. - -Please set the JAVA_HOME variable in your environment to match the -location of your Java installation." -fi - -# Increase the maximum file descriptors if we can. -if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then - MAX_FD_LIMIT=`ulimit -H -n` - if [ $? -eq 0 ] ; then - if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then - MAX_FD="$MAX_FD_LIMIT" - fi - ulimit -n $MAX_FD - if [ $? -ne 0 ] ; then - warn "Could not set maximum file descriptor limit: $MAX_FD" - fi - else - warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" - fi -fi - -# For Darwin, add options to specify how the application appears in the dock -if $darwin; then - GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" -fi - -# For Cygwin or MSYS, switch paths to Windows format before running java -if [ "$cygwin" = "true" -o "$msys" = "true" ] ; then - APP_HOME=`cygpath --path --mixed "$APP_HOME"` - CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` - - JAVACMD=`cygpath --unix "$JAVACMD"` - - # We build the pattern for arguments to be converted via cygpath - ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` - SEP="" - for dir in $ROOTDIRSRAW ; do - ROOTDIRS="$ROOTDIRS$SEP$dir" - SEP="|" - done - OURCYGPATTERN="(^($ROOTDIRS))" - # Add a user-defined pattern to the cygpath arguments - if [ "$GRADLE_CYGPATTERN" != "" ] ; then - OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" - fi - # Now convert the arguments - kludge to limit ourselves to /bin/sh - i=0 - for arg in "$@" ; do - CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` - CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option - - if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition - eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` - else - eval `echo args$i`="\"$arg\"" - fi - i=`expr $i + 1` - done - case $i in - 0) set -- ;; - 1) set -- "$args0" ;; - 2) set -- "$args0" "$args1" ;; - 3) set -- "$args0" "$args1" "$args2" ;; - 4) set -- "$args0" "$args1" "$args2" "$args3" ;; - 5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; - 6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; - 7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; - 8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; - 9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; - esac -fi - -# Escape application args -save () { - for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done - echo " " -} -APP_ARGS=`save "$@"` - -# Collect all arguments for the java command, following the shell quoting and substitution rules -eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS" - -exec "$JAVACMD" "$@" diff --git a/example/gradlew.bat b/example/gradlew.bat deleted file mode 100644 index a9f778a..0000000 --- a/example/gradlew.bat +++ /dev/null @@ -1,104 +0,0 @@ -@rem -@rem Copyright 2015 the original author or authors. -@rem -@rem Licensed under the Apache License, Version 2.0 (the "License"); -@rem you may not use this file except in compliance with the License. -@rem You may obtain a copy of the License at -@rem -@rem https://www.apache.org/licenses/LICENSE-2.0 -@rem -@rem Unless required by applicable law or agreed to in writing, software -@rem distributed under the License is distributed on an "AS IS" BASIS, -@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -@rem See the License for the specific language governing permissions and -@rem limitations under the License. -@rem - -@if "%DEBUG%" == "" @echo off -@rem ########################################################################## -@rem -@rem Gradle startup script for Windows -@rem -@rem ########################################################################## - -@rem Set local scope for the variables with windows NT shell -if "%OS%"=="Windows_NT" setlocal - -set DIRNAME=%~dp0 -if "%DIRNAME%" == "" set DIRNAME=. -set APP_BASE_NAME=%~n0 -set APP_HOME=%DIRNAME% - -@rem Resolve any "." and ".." in APP_HOME to make it shorter. -for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi - -@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. -set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" - -@rem Find java.exe -if defined JAVA_HOME goto findJavaFromJavaHome - -set JAVA_EXE=java.exe -%JAVA_EXE% -version >NUL 2>&1 -if "%ERRORLEVEL%" == "0" goto init - -echo. -echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. -echo. -echo Please set the JAVA_HOME variable in your environment to match the -echo location of your Java installation. - -goto fail - -:findJavaFromJavaHome -set JAVA_HOME=%JAVA_HOME:"=% -set JAVA_EXE=%JAVA_HOME%/bin/java.exe - -if exist "%JAVA_EXE%" goto init - -echo. -echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% -echo. -echo Please set the JAVA_HOME variable in your environment to match the -echo location of your Java installation. - -goto fail - -:init -@rem Get command-line arguments, handling Windows variants - -if not "%OS%" == "Windows_NT" goto win9xME_args - -:win9xME_args -@rem Slurp the command line arguments. -set CMD_LINE_ARGS= -set _SKIP=2 - -:win9xME_args_slurp -if "x%~1" == "x" goto execute - -set CMD_LINE_ARGS=%* - -:execute -@rem Setup the command line - -set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar - - -@rem Execute Gradle -"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS% - -:end -@rem End local scope for the variables with windows NT shell -if "%ERRORLEVEL%"=="0" goto mainEnd - -:fail -rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of -rem the _cmd.exe /c_ return code! -if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 -exit /b 1 - -:mainEnd -if "%OS%"=="Windows_NT" endlocal - -:omega diff --git a/example/build.gradle b/examples/gui/build.gradle similarity index 75% rename from example/build.gradle rename to examples/gui/build.gradle index 78563b7..89bb44a 100644 --- a/example/build.gradle +++ b/examples/gui/build.gradle @@ -8,5 +8,5 @@ repositories { } dependencies { - compile ('aero.t2s:mode-s:0.2.0-SNAPSHOT') + implementation project(':') } diff --git a/example/src/main/java/example/Demo.java b/examples/gui/src/main/java/example/Demo.java similarity index 98% rename from example/src/main/java/example/Demo.java rename to examples/gui/src/main/java/example/Demo.java index 71f82c1..57b1932 100644 --- a/example/src/main/java/example/Demo.java +++ b/examples/gui/src/main/java/example/Demo.java @@ -1,18 +1,18 @@ package example; -import aero.t2s.modes.ModeS; -import aero.t2s.modes.Track; -import example.flight.FlightFrame; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -import javax.swing.*; - import java.awt.*; import java.util.LinkedList; import java.util.List; import java.util.Timer; import java.util.TimerTask; +import javax.swing.*; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import aero.t2s.modes.ModeS; +import aero.t2s.modes.Track; +import example.flight.FlightFrame; /* * ButtonDemo.java requires the following files: @@ -23,7 +23,7 @@ public class Demo extends JPanel { private static final Logger logger = LoggerFactory.getLogger(Demo.class); - private static final String IP = "192.168.178.190"; + private static final String IP = "127.0.0.1"; private static final int PORT = 30002; private static final double LAT = 51; private static final double LON = 2; diff --git a/example/src/main/java/example/FlightsTable.java b/examples/gui/src/main/java/example/FlightsTable.java similarity index 55% rename from example/src/main/java/example/FlightsTable.java rename to examples/gui/src/main/java/example/FlightsTable.java index d93da7c..7c3cabc 100644 --- a/example/src/main/java/example/FlightsTable.java +++ b/examples/gui/src/main/java/example/FlightsTable.java @@ -1,9 +1,9 @@ package example; -import aero.t2s.modes.Track; - -import javax.swing.table.AbstractTableModel; import java.util.List; +import javax.swing.table.AbstractTableModel; + +import aero.t2s.modes.Track; class FlightsTable extends AbstractTableModel { private static String[] columns = { @@ -43,15 +43,19 @@ public String getColumnName(int column) { @Override public Object getValueAt(int rowIndex, int columnIndex) { - switch (columnIndex) { - case 0: return tracks.get(rowIndex).getIcao(); - case 1: return tracks.get(rowIndex).getCallsign(); - case 2: return String.format("%2.4f", tracks.get(rowIndex).getLat()); - case 3: return String.format("%03.4f", tracks.get(rowIndex).getLon()); - case 4: return tracks.get(rowIndex).getRocd(); - case 5: return tracks.get(rowIndex).getAltitude().getAltitude(); - case 6: return Math.round(tracks.get(rowIndex).getMagneticHeading()); - case 7: return (int)tracks.get(rowIndex).getGs(); + try { + switch (columnIndex) { + case 0: return tracks.get(rowIndex).getIcao(); + case 1: return tracks.get(rowIndex).getCallsign(); + case 2: return String.format("%2.4f", tracks.get(rowIndex).getLat()); + case 3: return String.format("%03.4f", tracks.get(rowIndex).getLon()); + case 4: return tracks.get(rowIndex).getRocd(); + case 5: return tracks.get(rowIndex).getAltitude().getAltitude(); + case 6: return Math.round(tracks.get(rowIndex).getMagneticHeading()); + case 7: return (int)tracks.get(rowIndex).getGs(); + } + } catch (IndexOutOfBoundsException ex) { + return null; } return null; diff --git a/example/src/main/java/example/flight/FlightFrame.form b/examples/gui/src/main/java/example/flight/FlightFrame.form similarity index 63% rename from example/src/main/java/example/flight/FlightFrame.form rename to examples/gui/src/main/java/example/flight/FlightFrame.form index faaf4f2..fd6ed8c 100644 --- a/example/src/main/java/example/flight/FlightFrame.form +++ b/examples/gui/src/main/java/example/flight/FlightFrame.form @@ -19,7 +19,7 @@ - + @@ -35,14 +35,6 @@ - - - - - - - - @@ -169,7 +161,7 @@ - + @@ -177,31 +169,47 @@ - + - + - + + - - - - - + + + + + + + + + + + + + + + + + + + + @@ -209,7 +217,7 @@ - + @@ -223,7 +231,7 @@ - + @@ -231,7 +239,7 @@ - + @@ -244,31 +252,143 @@ - + - + - + - + - + - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/example/src/main/java/example/flight/FlightFrame.java b/examples/gui/src/main/java/example/flight/FlightFrame.java similarity index 61% rename from example/src/main/java/example/flight/FlightFrame.java rename to examples/gui/src/main/java/example/flight/FlightFrame.java index 8f4ff43..5723f2d 100644 --- a/example/src/main/java/example/flight/FlightFrame.java +++ b/examples/gui/src/main/java/example/flight/FlightFrame.java @@ -1,20 +1,17 @@ package example.flight; -import aero.t2s.modes.Track; -import aero.t2s.modes.constants.Hazard; - -import javax.swing.*; -import javax.swing.table.AbstractTableModel; import java.util.Timer; import java.util.TimerTask; +import javax.swing.*; + +import aero.t2s.modes.Track; +import aero.t2s.modes.constants.Hazard; public class FlightFrame extends JFrame { java.util.Timer timer = new Timer(); private Track track; - private JTabbedPane tabbedPane1; - private JTable gcibReportTable; private JList meteoHazards; private JLabel meteoHumidity; private JLabel meteoSat; @@ -23,11 +20,16 @@ public class FlightFrame extends JFrame { private JLabel meteoRadioHeight; private JPanel mainPanel; private JTextArea flightModelLeft; - private JTextArea flightInfoRight; - private JTextArea infoAccuracy; private JTextArea infoAbds; private JTextArea acas; + private JTextArea register05; + private JTextArea register06; + private JTextArea register07; + private JTextArea register08; + private JTextArea register09; + private JTextArea register17; + public FlightFrame(Track track) { this.track = track; @@ -47,8 +49,6 @@ public void run() { private void updateContent() { setTitle(track.getCallsign() + " - " + track.getIcao()); - ((GcibTable)gcibReportTable.getModel()).fireTableDataChanged(); - // Meteo Hazard ((DefaultListModel)meteoHazards.getModel()).clear(); if (track.getMeteo().getTurbulence() != Hazard.NIL) { @@ -83,7 +83,6 @@ private void updateContent() { "CALLSIGN: %s\n" + "WTC: %s\n" + "ATYP: %s\n (REG: %s)\n" + - "Width & Length Code: %s\n" + "OPERATOR: %s\n" + "MODE A: %04d\n" + "State: %s\n" + @@ -95,9 +94,7 @@ private void updateContent() { "Flight Status SPI: %s\n" + "-------------------------------\n" + "Altitude: %d%s (step size: %d)\n" + - "Baro Altitude: %d\n" + "Geometric offset: %d\n" + - "GNSS Altitude: %d\n" + "Selected Altitude Source: %s\n" + "Selected Altitude: %d\n" + "Selected Altitude FMS: %s\n" + @@ -111,7 +108,6 @@ private void updateContent() { "ROCD Baro Available: %s\n" + "Baro ROCD: %dft/min\n" + "------------------------------\n" + - "Heading Source: %s\n" + "Magnetic Heading: %d\n" + "True Heading: %d\n" + "Selected Heading: %d\n" + @@ -136,7 +132,6 @@ private void updateContent() { track.getCallsign(), track.getWtc(), track.getAtype(), track.getRegistration(), - track.getLengthWidthCode().name(), track.getOperator(), track.getModeA(), track.isGroundBit() ? "GROUND" : "AIRBORNE", @@ -147,9 +142,7 @@ private void updateContent() { track.getFlightStatus().isAlert() ? "YES" : "NO", track.getFlightStatus().isSpi() ? "YES" : "NO", (int)track.getAltitude().getAltitude(), track.getAltitude().isMetric() ? "M" : "FT", track.getAltitude().getStep(), - track.getBaroAltitude(), track.getGeometricHeightOffset(), - track.getGnssHeight(), track.getSelectedAltitudeSource().toString(), track.getSelectedAltitude(), track.getSelectedAltitudeManagedFms() ? "YES" : "NO", @@ -160,7 +153,6 @@ private void updateContent() { track.getRocd(), track.getRocdSourceBaro() ? "YES" : "NO", (int)track.getBaroRocd(), - track.isMagneticHeading() ? "MAGNETIC" : "TRUE", (int)track.getMagneticHeading(), (int)track.getTrueHeading(), (int)track.getSelectedHeading(), @@ -216,30 +208,17 @@ private void updateContent() { track.getAcas().getTargetRange() )); - infoAccuracy.setText(String.format( - "NIC: %d\n" + - "NICa: %d\n" + - "NICb: %d\n" + - "NICc: %d\n" + - "NACv: %d\n" + - "NACp: %s\n" + - "SIL: %d\n" + - "----------------------------\n", - track.getNIC(), - track.getNICa(), - track.getNICb(), - track.getNICc(), - track.getNACv(), - track.getNACp(), - track.getSil() - )); - - infoAbds.setText(String.format( + infoAbds.setText( "Version: %s\n" + - "Single Antenna: %s\n", - track.getVersion().name(), - track.getSingleAntenna() ? "YES" : "NO" - )); + track.getVersion().name() + ); + + register05.setText(track.register05().toString()); + register06.setText(track.register06().toString()); + register07.setText(track.register07().toString()); + register08.setText(track.register08().toString()); + register09.setText(track.register09().toString()); + register17.setText(track.register17().toString()); } private String formatAcasResolution() { @@ -249,108 +228,4 @@ private String formatAcasResolution() { return track.getAcas().getResolutionAdvisory().toString(); } - - private void createUIComponents() { - gcibReportTable = new JTable(new GcibTable(track)); - gcibReportTable.setAutoResizeMode(JTable.AUTO_RESIZE_ALL_COLUMNS); - } - - private class GcibTable extends AbstractTableModel { - private final String[] GCIB = { - "0,5 Extended squitter airborne position", - "0,6 Extended squitter surface position", - "0,7 Extended squitter identification and category", - "0,7 Extended squitter status", - "0,9 Extended squitter airborne velocity information", - "0,A Extended squitter event-driven information", - "2,0 Aircraft identification", - "2,1 Aircraft registration number", - "4,0 Selected vertical intention", - "4,1 Next waypoint identifier", - "4,2 Next waypoint position", - "4,3 Next waypoint information", - "4,4 Meteorological routine report", - "4,5 Meteorological hazard report", - "4,8 VHF channel report", - "5,0 Track and turn report", - "5,1 Position coarse", - "5,2 Position fine", - "5,3 Air-referenced state vector", - "5,4 Waypoint 1", - "5,5 Waypoint 2", - "5,6 Waypoint 3", - "5,F Quasi-static parameter monitoring", - "6,0 Heading and speed report", - }; - - public GcibTable(Track track) { - } - - @Override - public int getRowCount() { - if (track.getCapabilityReport().isAvailable()) { - return 24; - } - - return 1; - } - - @Override - public int getColumnCount() { - return 2; - } - - @Override - public String getColumnName(int column) { - switch (column) { - case 0: return "GCIB"; - case 1: return "Available"; - } - - return null; - } - - @Override - public Object getValueAt(int rowIndex, int columnIndex) { - if (!track.getCapabilityReport().isAvailable()) { - if (columnIndex == 0) - return "Not Available"; - else - return ""; - } - - if (columnIndex == 0) { - return GCIB[rowIndex]; - } - - switch (rowIndex) { - case 0: return track.getCapabilityReport().isBds05() ? "YES" : "NO"; - case 1: return track.getCapabilityReport().isBds06() ? "YES" : "NO"; - case 2: return track.getCapabilityReport().isBds07() ? "YES" : "NO"; - case 3: return track.getCapabilityReport().isBds08() ? "YES" : "NO"; - case 4: return track.getCapabilityReport().isBds09() ? "YES" : "NO"; - case 5: return track.getCapabilityReport().isBds0A() ? "YES" : "NO"; - case 6: return track.getCapabilityReport().isBds20() ? "YES" : "NO"; - case 7: return track.getCapabilityReport().isBds21() ? "YES" : "NO"; - case 8: return track.getCapabilityReport().isBds40() ? "YES" : "NO"; - case 9: return track.getCapabilityReport().isBds41() ? "YES" : "NO"; - case 10: return track.getCapabilityReport().isBds42() ? "YES" : "NO"; - case 11: return track.getCapabilityReport().isBds43() ? "YES" : "NO"; - case 12: return track.getCapabilityReport().isBds44() ? "YES" : "NO"; - case 13: return track.getCapabilityReport().isBds45() ? "YES" : "NO"; - case 14: return track.getCapabilityReport().isBds48() ? "YES" : "NO"; - case 15: return track.getCapabilityReport().isBds50() ? "YES" : "NO"; - case 16: return track.getCapabilityReport().isBds51() ? "YES" : "NO"; - case 17: return track.getCapabilityReport().isBds52() ? "YES" : "NO"; - case 18: return track.getCapabilityReport().isBds53() ? "YES" : "NO"; - case 19: return track.getCapabilityReport().isBds54() ? "YES" : "NO"; - case 20: return track.getCapabilityReport().isBds55() ? "YES" : "NO"; - case 21: return track.getCapabilityReport().isBds56() ? "YES" : "NO"; - case 22: return track.getCapabilityReport().isBds5F() ? "YES" : "NO"; - case 23: return track.getCapabilityReport().isBds60() ? "YES" : "NO"; - } - - return null; - } - } } diff --git a/examples/stdout/build.gradle b/examples/stdout/build.gradle new file mode 100644 index 0000000..89bb44a --- /dev/null +++ b/examples/stdout/build.gradle @@ -0,0 +1,12 @@ +apply plugin: 'idea' +apply plugin: 'java' +apply plugin: 'application' + +repositories { + mavenCentral() + mavenLocal() +} + +dependencies { + implementation project(':') +} diff --git a/examples/stdout/src/main/java/aero/t2s/modes/examples/StdOutExample.java b/examples/stdout/src/main/java/aero/t2s/modes/examples/StdOutExample.java new file mode 100644 index 0000000..336e666 --- /dev/null +++ b/examples/stdout/src/main/java/aero/t2s/modes/examples/StdOutExample.java @@ -0,0 +1,67 @@ +package aero.t2s.modes.examples; + +import java.util.Timer; +import java.util.TimerTask; + +import aero.t2s.modes.ModeS; +import aero.t2s.modes.decoder.df.DF20; +import aero.t2s.modes.decoder.df.DF21; + +public class StdOutExample { + public static void main(String[] args) { + ModeS modes = new ModeS( + "192.168.178.190", // Host IP where the Dump1090 server is running + 30002, // The port with raw output (default 30002) + 51, // Decimal latitude + 4 // Decimal longitude + ); +// modes.onTrackCreated(track -> System.out.println("CREATED " + track.toString())); +// modes.onTrackUpdated(track -> System.out.println("UPDATED " + track.toString())); +// modes.onTrackDeleted(track -> System.out.println("DELETED " + track.toString())); + + MessageCount count = new MessageCount(); + + Timer timer = new Timer(); + timer.scheduleAtFixedRate(new TimerTask() { + @Override + public void run() { + if (count.total > 0) { + System.out.print("Multi BDS messages last 60 seconds: " + count.multi + " ( " + Math.round((float)count.multi / count.total) + "%)\n"); + count.reset(); + } + } + }, 0, 60000); + + modes.onMessage(df -> { + count.increment();; + if (df instanceof DF20) { + if (((DF20) df).isMultipleMatches()) + count.incrementMulti(); + } + if (df instanceof DF21) { + if (((DF21) df).isMultipleMatches()) + count.incrementMulti(); + } + }); + + modes.start(); + } + + static class MessageCount { + long total = 0; + long multi = 0; + + void reset() { + total = 0; + multi = 0; + } + + void increment() { + total++; + } + + void incrementMulti() { + multi++; + } + } +} diff --git a/gradle.properties b/gradle.properties index 25dcfce..5b03a12 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1,5 +1,5 @@ GROUP=aero.t2s -VERSION_NAME=0.2.2-SNAPSHOT +VERSION_NAME=0.2.8-SNAPSHOT POM_ARTIFACT_ID=mode-s POM_NAME=Mode-S/ADS-B (1090Mhz) @@ -8,10 +8,10 @@ POM_PACKAGING=jar POM_DESCRIPTION=Mode-S/ADS-B (1090Mhz) allows for both message based decoding and tracking aircraft. POM_INCEPTION_YEAR=2020 -POM_URL=https://github.com/terminal2/java-modes-library/ -POM_SCM_URL=https://github.com/terminal2/java-modes-library/ -POM_SCM_CONNECTION=scm:git:git://github.com/terminal2/java-modes-library.git -POM_SCM_DEV_CONNECTION=scm:git:ssh://git@github.com/terminal2/java-modes-library.git +POM_URL=https://github.com/terminal2/java-mode-s/ +POM_SCM_URL=https://github.com/terminal2/java-mode-s/ +POM_SCM_CONNECTION=scm:git:git://github.com/terminal2/java-mode-s.git +POM_SCM_DEV_CONNECTION=scm:git:ssh://git@github.com/terminal2/java-mode-s.git POM_LICENCE_NAME=The Apache Software License, Version 2.0 POM_LICENCE_URL=https://www.apache.org/licenses/LICENSE-2.0.txt diff --git a/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar index 62d4c05..249e583 100644 Binary files a/gradle/wrapper/gradle-wrapper.jar and b/gradle/wrapper/gradle-wrapper.jar differ diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index 622ab64..ae04661 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,5 +1,5 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-6.5-bin.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-7.5.1-bin.zip zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists diff --git a/gradlew b/gradlew index fbd7c51..a69d9cb 100755 --- a/gradlew +++ b/gradlew @@ -1,7 +1,7 @@ -#!/usr/bin/env sh +#!/bin/sh # -# Copyright 2015 the original author or authors. +# Copyright © 2015-2021 the original authors. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -17,67 +17,101 @@ # ############################################################################## -## -## Gradle start up script for UN*X -## +# +# Gradle start up script for POSIX generated by Gradle. +# +# Important for running: +# +# (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is +# noncompliant, but you have some other compliant shell such as ksh or +# bash, then to run this script, type that shell name before the whole +# command line, like: +# +# ksh Gradle +# +# Busybox and similar reduced shells will NOT work, because this script +# requires all of these POSIX shell features: +# * functions; +# * expansions «$var», «${var}», «${var:-default}», «${var+SET}», +# «${var#prefix}», «${var%suffix}», and «$( cmd )»; +# * compound commands having a testable exit status, especially «case»; +# * various built-in commands including «command», «set», and «ulimit». +# +# Important for patching: +# +# (2) This script targets any POSIX shell, so it avoids extensions provided +# by Bash, Ksh, etc; in particular arrays are avoided. +# +# The "traditional" practice of packing multiple parameters into a +# space-separated string is a well documented source of bugs and security +# problems, so this is (mostly) avoided, by progressively accumulating +# options in "$@", and eventually passing that to Java. +# +# Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS, +# and GRADLE_OPTS) rely on word-splitting, this is performed explicitly; +# see the in-line comments for details. +# +# There are tweaks for specific operating systems such as AIX, CygWin, +# Darwin, MinGW, and NonStop. +# +# (3) This script is generated from the Groovy template +# https://github.com/gradle/gradle/blob/master/subprojects/plugins/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt +# within the Gradle project. +# +# You can find Gradle at https://github.com/gradle/gradle/. +# ############################################################################## # Attempt to set APP_HOME + # Resolve links: $0 may be a link -PRG="$0" -# Need this for relative symlinks. -while [ -h "$PRG" ] ; do - ls=`ls -ld "$PRG"` - link=`expr "$ls" : '.*-> \(.*\)$'` - if expr "$link" : '/.*' > /dev/null; then - PRG="$link" - else - PRG=`dirname "$PRG"`"/$link" - fi +app_path=$0 + +# Need this for daisy-chained symlinks. +while + APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path + [ -h "$app_path" ] +do + ls=$( ls -ld "$app_path" ) + link=${ls#*' -> '} + case $link in #( + /*) app_path=$link ;; #( + *) app_path=$APP_HOME$link ;; + esac done -SAVED="`pwd`" -cd "`dirname \"$PRG\"`/" >/dev/null -APP_HOME="`pwd -P`" -cd "$SAVED" >/dev/null + +APP_HOME=$( cd "${APP_HOME:-./}" && pwd -P ) || exit APP_NAME="Gradle" -APP_BASE_NAME=`basename "$0"` +APP_BASE_NAME=${0##*/} # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' # Use the maximum available, or set MAX_FD != -1 to use that value. -MAX_FD="maximum" +MAX_FD=maximum warn () { echo "$*" -} +} >&2 die () { echo echo "$*" echo exit 1 -} +} >&2 # OS specific support (must be 'true' or 'false'). cygwin=false msys=false darwin=false nonstop=false -case "`uname`" in - CYGWIN* ) - cygwin=true - ;; - Darwin* ) - darwin=true - ;; - MINGW* ) - msys=true - ;; - NONSTOP* ) - nonstop=true - ;; +case "$( uname )" in #( + CYGWIN* ) cygwin=true ;; #( + Darwin* ) darwin=true ;; #( + MSYS* | MINGW* ) msys=true ;; #( + NONSTOP* ) nonstop=true ;; esac CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar @@ -87,9 +121,9 @@ CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar if [ -n "$JAVA_HOME" ] ; then if [ -x "$JAVA_HOME/jre/sh/java" ] ; then # IBM's JDK on AIX uses strange locations for the executables - JAVACMD="$JAVA_HOME/jre/sh/java" + JAVACMD=$JAVA_HOME/jre/sh/java else - JAVACMD="$JAVA_HOME/bin/java" + JAVACMD=$JAVA_HOME/bin/java fi if [ ! -x "$JAVACMD" ] ; then die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME @@ -98,7 +132,7 @@ Please set the JAVA_HOME variable in your environment to match the location of your Java installation." fi else - JAVACMD="java" + JAVACMD=java which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. Please set the JAVA_HOME variable in your environment to match the @@ -106,80 +140,101 @@ location of your Java installation." fi # Increase the maximum file descriptors if we can. -if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then - MAX_FD_LIMIT=`ulimit -H -n` - if [ $? -eq 0 ] ; then - if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then - MAX_FD="$MAX_FD_LIMIT" - fi - ulimit -n $MAX_FD - if [ $? -ne 0 ] ; then - warn "Could not set maximum file descriptor limit: $MAX_FD" - fi - else - warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" - fi +if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then + case $MAX_FD in #( + max*) + MAX_FD=$( ulimit -H -n ) || + warn "Could not query maximum file descriptor limit" + esac + case $MAX_FD in #( + '' | soft) :;; #( + *) + ulimit -n "$MAX_FD" || + warn "Could not set maximum file descriptor limit to $MAX_FD" + esac fi -# For Darwin, add options to specify how the application appears in the dock -if $darwin; then - GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" -fi +# Collect all arguments for the java command, stacking in reverse order: +# * args from the command line +# * the main class name +# * -classpath +# * -D...appname settings +# * --module-path (only if needed) +# * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables. # For Cygwin or MSYS, switch paths to Windows format before running java -if [ "$cygwin" = "true" -o "$msys" = "true" ] ; then - APP_HOME=`cygpath --path --mixed "$APP_HOME"` - CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` - - JAVACMD=`cygpath --unix "$JAVACMD"` - - # We build the pattern for arguments to be converted via cygpath - ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` - SEP="" - for dir in $ROOTDIRSRAW ; do - ROOTDIRS="$ROOTDIRS$SEP$dir" - SEP="|" - done - OURCYGPATTERN="(^($ROOTDIRS))" - # Add a user-defined pattern to the cygpath arguments - if [ "$GRADLE_CYGPATTERN" != "" ] ; then - OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" - fi +if "$cygwin" || "$msys" ; then + APP_HOME=$( cygpath --path --mixed "$APP_HOME" ) + CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" ) + + JAVACMD=$( cygpath --unix "$JAVACMD" ) + # Now convert the arguments - kludge to limit ourselves to /bin/sh - i=0 - for arg in "$@" ; do - CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` - CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option - - if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition - eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` - else - eval `echo args$i`="\"$arg\"" + for arg do + if + case $arg in #( + -*) false ;; # don't mess with options #( + /?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath + [ -e "$t" ] ;; #( + *) false ;; + esac + then + arg=$( cygpath --path --ignore --mixed "$arg" ) fi - i=`expr $i + 1` + # Roll the args list around exactly as many times as the number of + # args, so each arg winds up back in the position where it started, but + # possibly modified. + # + # NB: a `for` loop captures its iteration list before it begins, so + # changing the positional parameters here affects neither the number of + # iterations, nor the values presented in `arg`. + shift # remove old arg + set -- "$@" "$arg" # push replacement arg done - case $i in - 0) set -- ;; - 1) set -- "$args0" ;; - 2) set -- "$args0" "$args1" ;; - 3) set -- "$args0" "$args1" "$args2" ;; - 4) set -- "$args0" "$args1" "$args2" "$args3" ;; - 5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; - 6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; - 7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; - 8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; - 9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; - esac fi -# Escape application args -save () { - for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done - echo " " -} -APP_ARGS=`save "$@"` +# Collect all arguments for the java command; +# * $DEFAULT_JVM_OPTS, $JAVA_OPTS, and $GRADLE_OPTS can contain fragments of +# shell script including quotes and variable substitutions, so put them in +# double quotes to make sure that they get re-expanded; and +# * put everything else in single quotes, so that it's not re-expanded. + +set -- \ + "-Dorg.gradle.appname=$APP_BASE_NAME" \ + -classpath "$CLASSPATH" \ + org.gradle.wrapper.GradleWrapperMain \ + "$@" + +# Stop when "xargs" is not available. +if ! command -v xargs >/dev/null 2>&1 +then + die "xargs is not available" +fi + +# Use "xargs" to parse quoted args. +# +# With -n1 it outputs one arg per line, with the quotes and backslashes removed. +# +# In Bash we could simply go: +# +# readarray ARGS < <( xargs -n1 <<<"$var" ) && +# set -- "${ARGS[@]}" "$@" +# +# but POSIX shell has neither arrays nor command substitution, so instead we +# post-process each arg (as a line of input to sed) to backslash-escape any +# character that might be a shell metacharacter, then use eval to reverse +# that process (while maintaining the separation between arguments), and wrap +# the whole thing up as a single "set" statement. +# +# This will of course break if any of these variables contains a newline or +# an unmatched quote. +# -# Collect all arguments for the java command, following the shell quoting and substitution rules -eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS" +eval "set -- $( + printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" | + xargs -n1 | + sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' | + tr '\n' ' ' + )" '"$@"' exec "$JAVACMD" "$@" diff --git a/gradlew.bat b/gradlew.bat index a9f778a..53a6b23 100644 --- a/gradlew.bat +++ b/gradlew.bat @@ -14,7 +14,7 @@ @rem limitations under the License. @rem -@if "%DEBUG%" == "" @echo off +@if "%DEBUG%"=="" @echo off @rem ########################################################################## @rem @rem Gradle startup script for Windows @@ -25,7 +25,7 @@ if "%OS%"=="Windows_NT" setlocal set DIRNAME=%~dp0 -if "%DIRNAME%" == "" set DIRNAME=. +if "%DIRNAME%"=="" set DIRNAME=. set APP_BASE_NAME=%~n0 set APP_HOME=%DIRNAME% @@ -40,7 +40,7 @@ if defined JAVA_HOME goto findJavaFromJavaHome set JAVA_EXE=java.exe %JAVA_EXE% -version >NUL 2>&1 -if "%ERRORLEVEL%" == "0" goto init +if %ERRORLEVEL% equ 0 goto execute echo. echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. @@ -54,7 +54,7 @@ goto fail set JAVA_HOME=%JAVA_HOME:"=% set JAVA_EXE=%JAVA_HOME%/bin/java.exe -if exist "%JAVA_EXE%" goto init +if exist "%JAVA_EXE%" goto execute echo. echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% @@ -64,21 +64,6 @@ echo location of your Java installation. goto fail -:init -@rem Get command-line arguments, handling Windows variants - -if not "%OS%" == "Windows_NT" goto win9xME_args - -:win9xME_args -@rem Slurp the command line arguments. -set CMD_LINE_ARGS= -set _SKIP=2 - -:win9xME_args_slurp -if "x%~1" == "x" goto execute - -set CMD_LINE_ARGS=%* - :execute @rem Setup the command line @@ -86,17 +71,19 @@ set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar @rem Execute Gradle -"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS% +"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %* :end @rem End local scope for the variables with windows NT shell -if "%ERRORLEVEL%"=="0" goto mainEnd +if %ERRORLEVEL% equ 0 goto mainEnd :fail rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of rem the _cmd.exe /c_ return code! -if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 -exit /b 1 +set EXIT_CODE=%ERRORLEVEL% +if %EXIT_CODE% equ 0 set EXIT_CODE=1 +if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE% +exit /b %EXIT_CODE% :mainEnd if "%OS%"=="Windows_NT" endlocal diff --git a/settings.gradle b/settings.gradle new file mode 100644 index 0000000..f3f0a36 --- /dev/null +++ b/settings.gradle @@ -0,0 +1 @@ +include('examples:gui', 'examples:stdout') diff --git a/src/main/java/aero/t2s/modes/CapabilityReport.java b/src/main/java/aero/t2s/modes/CapabilityReport.java index df9a433..67bbf8c 100644 --- a/src/main/java/aero/t2s/modes/CapabilityReport.java +++ b/src/main/java/aero/t2s/modes/CapabilityReport.java @@ -214,4 +214,35 @@ public void all() { bds5F = true; bds60 = true; } + + + @Override + public String toString() { + return "Bds17{" + + "\nbds05=" + bds05 + + ",\n bds06=" + bds06 + + ",\n bds07=" + bds07 + + ",\n bds08=" + bds08 + + ",\n bds09=" + bds09 + + ",\n bds0A=" + bds0A + + ",\n bds20=" + bds20 + + ",\n bds21=" + bds21 + + ",\n bds40=" + bds40 + + ",\n bds41=" + bds41 + + ",\n bds42=" + bds42 + + ",\n bds43=" + bds43 + + ",\n bds44=" + bds44 + + ",\n bds45=" + bds45 + + ",\n bds48=" + bds48 + + ",\n bds50=" + bds50 + + ",\n bds51=" + bds51 + + ",\n bds52=" + bds52 + + ",\n bds53=" + bds53 + + ",\n bds54=" + bds54 + + ",\n bds55=" + bds55 + + ",\n bds56=" + bds56 + + ",\n bds5F=" + bds5F + + ",\n bds60=" + bds60 + + "\n}"; + } } diff --git a/src/main/java/aero/t2s/modes/CprPosition.java b/src/main/java/aero/t2s/modes/CprPosition.java index 5326368..2192335 100644 --- a/src/main/java/aero/t2s/modes/CprPosition.java +++ b/src/main/java/aero/t2s/modes/CprPosition.java @@ -1,9 +1,43 @@ package aero.t2s.modes; +import java.time.Instant; + public class CprPosition { private double lat; private double lon; - private int time; + private boolean surface = false; + private boolean valid = false; + private double latZone; + private double lonZone; + private long time; + + public CprPosition() { + this.lat = 0.0; + this.lon = 0.0; + this.surface = false; + this.valid = false; + } + + public CprPosition(double lat, double lon, boolean surface) { + setLatLon(lat ,lon); + this.surface = surface; + } + + public CprPosition(double lat, double lon) { + setLatLon(lat ,lon); + } + + public CprPosition(int cprLat, int cprLon, boolean surface) { + setLatLon(cprLat / (double) (1 << 17), cprLon / (double) (1 << 17)); + this.surface = surface; + } + + public void setLatLon(double lat, double lon) { + this.lat = lat; + this.lon = lon; + this.time = Instant.now().toEpochMilli(); + this.valid = true; + } public void setLat(double lat) { this.lat = lat; @@ -21,15 +55,51 @@ public double getLon() { return lon; } - public void setTime(int time) { + public void setSurface(boolean surface) { + this.surface = surface; + } + + public boolean getSurface() { + return this.surface; + } + + public void setZones(double latZone, double lonZone) { + this.latZone = latZone; + this.lonZone = lonZone; + } + public void setLatZone(double zone) { + this.latZone = zone; + } + + public double getLatZone() { + return this.latZone; + } + + public void setLonZone(double zone) { + this.lonZone = zone; + } + + public double getLonZone() { + return this.lonZone; + } + + public void setTime(long time) { this.time = time; } - public int getTime() { + public long getTime() { return time; } public boolean isValid() { - return lat != 0d && lon != 0; + return valid; + } + + public void setValid(boolean valid) { + this.valid = valid; + } + + public boolean isExpired() { + return time < Instant.now().minusSeconds(10).toEpochMilli(); } } diff --git a/src/main/java/aero/t2s/modes/ModeAcMessageException.java b/src/main/java/aero/t2s/modes/ModeAcMessageException.java new file mode 100644 index 0000000..5219775 --- /dev/null +++ b/src/main/java/aero/t2s/modes/ModeAcMessageException.java @@ -0,0 +1,5 @@ +package aero.t2s.modes; + +public class ModeAcMessageException extends Exception implements DownlinkException { + +} diff --git a/src/main/java/aero/t2s/modes/ModeS.java b/src/main/java/aero/t2s/modes/ModeS.java index ad9b448..d4cdffd 100644 --- a/src/main/java/aero/t2s/modes/ModeS.java +++ b/src/main/java/aero/t2s/modes/ModeS.java @@ -26,8 +26,8 @@ public ModeS(String host, int port, double originLat, double originLon, ModeSDat handler = new ModeSTrackHandler(tracks, originLat, originLon, database); handler.onTrackCreated(onTrackCreated); - handler.onTrackCreated(onTrackUpdated); - handler.onTrackCreated(onTrackDeleted); + handler.onTrackUpdated(onTrackUpdated); + handler.onTrackDeleted(onTrackDeleted); handler.onMessage(onMessage); listener = new ModeSListener(new InetSocketAddress(host, port), handler); diff --git a/src/main/java/aero/t2s/modes/ModeSHandler.java b/src/main/java/aero/t2s/modes/ModeSHandler.java index a4ea818..c752f03 100644 --- a/src/main/java/aero/t2s/modes/ModeSHandler.java +++ b/src/main/java/aero/t2s/modes/ModeSHandler.java @@ -1,10 +1,13 @@ package aero.t2s.modes; import aero.t2s.modes.decoder.df.DownlinkFormat; +import aero.t2s.modes.decoder.df.df17.PositionUpdate; import java.util.function.Consumer; abstract public class ModeSHandler { + protected double originLat; + protected double originLon; protected Consumer onDeleted = track -> {}; protected Consumer onCreated = track -> {}; protected Consumer onUpdated = track -> {}; @@ -30,9 +33,10 @@ public void onMessage(Consumer onMessage) { public abstract DownlinkFormat handleSync(String data); - protected short[] toData(final String input) throws EmptyMessageException { - if (input.startsWith("*0000")) { - throw new EmptyMessageException(); + protected short[] toData(final String input) throws EmptyMessageException, ModeAcMessageException { + if(input.length() == 6) { + // example mode A/C: *21D2; *0200; *0101; + throw new ModeAcMessageException(); } String hex = input.replace("*", "").replace(";", ""); @@ -41,10 +45,10 @@ protected short[] toData(final String input) throws EmptyMessageException { } public void start() { - + PositionUpdate.start(originLat, originLon); } public void stop() { - + PositionUpdate.stop(); } } diff --git a/src/main/java/aero/t2s/modes/ModeSMessageHandler.java b/src/main/java/aero/t2s/modes/ModeSMessageHandler.java index 5542209..ebb651b 100644 --- a/src/main/java/aero/t2s/modes/ModeSMessageHandler.java +++ b/src/main/java/aero/t2s/modes/ModeSMessageHandler.java @@ -22,6 +22,8 @@ public class ModeSMessageHandler extends ModeSHandler { private Consumer onMessage; public ModeSMessageHandler(double originLat, double originLon) { + this.originLat = originLat; + this.originLon = originLon; this.decoder = new Decoder(new HashMap<>(), originLat, originLon, ModeSDatabase.createDatabase()); } @@ -38,7 +40,7 @@ public DownlinkFormat handleSync(final String input) { } return df; - } catch (EmptyMessageException ignored) { + } catch (ModeAcMessageException | EmptyMessageException ignored) { } catch (InvalidExtendedSquitterTypeCodeException | UnknownDownlinkFormatException e) { LOGGER.error(e.getMessage()); } catch (Throwable throwable) { diff --git a/src/main/java/aero/t2s/modes/ModeSTrackHandler.java b/src/main/java/aero/t2s/modes/ModeSTrackHandler.java index db19904..f11b6fc 100644 --- a/src/main/java/aero/t2s/modes/ModeSTrackHandler.java +++ b/src/main/java/aero/t2s/modes/ModeSTrackHandler.java @@ -24,6 +24,8 @@ public class ModeSTrackHandler extends ModeSHandler { public ModeSTrackHandler(Map tracks, double originLat, double originLon, ModeSDatabase database) { this.tracks = tracks; + this.originLat = originLat; + this.originLon = originLon; this.decoder = new Decoder(tracks, originLat, originLon, database); timer = new Timer(); @@ -59,6 +61,12 @@ public void handle(final String input) { public DownlinkFormat handleSync(final String input) { try { DownlinkFormat df = decoder.decode(toData(input)); + if(df == null) { + // invalid packet (Mode A/C like *21D2; *0200; *0101;) + LOGGER.debug("DF Message could not be parsed: [{}]", input); + return null; + } + Track track = decoder.getTrack(df.getIcao()); if (track == null) { @@ -79,11 +87,11 @@ public DownlinkFormat handleSync(final String input) { } return df; - } catch (EmptyMessageException ignored) { + } catch (ModeAcMessageException | EmptyMessageException ignored) { } catch (InvalidExtendedSquitterTypeCodeException | UnknownDownlinkFormatException e) { LOGGER.error(e.getMessage()); } catch (Throwable throwable) { - LOGGER.error("Message could not be parsed: [" + input + "]", throwable); + LOGGER.error("DF Message could not be parsed: [" + input + "]", throwable); } return null; diff --git a/src/main/java/aero/t2s/modes/RadiusLimit.java b/src/main/java/aero/t2s/modes/RadiusLimit.java deleted file mode 100644 index 5fb3de7..0000000 --- a/src/main/java/aero/t2s/modes/RadiusLimit.java +++ /dev/null @@ -1,89 +0,0 @@ -package aero.t2s.modes; - -public class RadiusLimit { - private final Track track; - - private double radiusLimitMetres; - private boolean isUnknown; - - public RadiusLimit(Track track) { - this.track = track; - } - - public void determine() { - radiusLimitMetres = 0; - isUnknown = false; - - // Surface Position - if (track.isGroundBit()) { - determineSurface(); - return; - } - - determineAirborne(); - } - - private void determineAirborne() { - if (track.getNIC() == 11 && track.getNICa() == 0 && track.getNICb() == 0) { - radiusLimitMetres = 7.5; - } else if (track.getNIC() == 10 && track.getNICa() == 0 && track.getNICb() == 0) { - radiusLimitMetres = 25; - } else if (track.getNIC() == 9 && track.getNICa() == 0 && track.getNICb() == 0) { - radiusLimitMetres = 42; - } else if (track.getNIC() == 8 && track.getNICa() == 1 && track.getNICb() == 1) { - radiusLimitMetres = 185.2; - } else if (track.getNIC() == 7 && track.getNICa() == 0 && track.getNICb() == 0) { - radiusLimitMetres = 370.4; - } else if (track.getNIC() == 6 && track.getNICa() == 0 && track.getNICb() == 1) { - radiusLimitMetres = 555.6; - } else if (track.getNIC() == 6 && track.getNICa() == 0 && track.getNICb() == 0) { - radiusLimitMetres = 926; - } else if (track.getNIC() == 6 && track.getNICa() == 1 && track.getNICb() == 1) { - radiusLimitMetres = 1111.2; - } else if (track.getNIC() == 5 && track.getNICa() == 0 && track.getNICb() == 0) { - radiusLimitMetres = 1852; - } else if (track.getNIC() == 4 && track.getNICa() == 0 && track.getNICb() == 0) { - radiusLimitMetres = 3704; - } else if (track.getNIC() == 3 && track.getNICa() == 1 && track.getNICb() == 1) { - radiusLimitMetres = 7408; - } else if (track.getNIC() == 2 && track.getNICa() == 0 && track.getNICb() == 0) { - radiusLimitMetres = 14816; - } else if (track.getNIC() == 1 && track.getNICa() == 0 && track.getNICb() == 0) { - radiusLimitMetres = 37040; - } else if (track.getNIC() == 0 && track.getNICa() == 0 && track.getNICb() == 0) { - isUnknown = true; - } else { - isUnknown = true; - } - } - - private void determineSurface() { - if (track.getNIC() == 11 && track.getNICa() == 0 && track.getNICc() == 0) { - radiusLimitMetres = 7.5; - } else if (track.getNIC() == 10 && track.getNICa() == 0 && track.getNICc() == 0) { - radiusLimitMetres = 25; - } else if (track.getNIC() == 9 && track.getNICa() == 1 && track.getNICc() == 0) { - radiusLimitMetres = 75; - } else if (track.getNIC() == 8 && track.getNICa() == 0 && track.getNICc() == 0) { - radiusLimitMetres = 185.2; - } else if (track.getNIC() == 7 && track.getNICa() == 1 && track.getNICc() == 1) { - radiusLimitMetres = 370.4; - } else if (track.getNIC() == 6 && track.getNICa() == 1 && track.getNICc() == 0) { - radiusLimitMetres = 555.6; - } else if (track.getNIC() == 6 && track.getNICa() == 0 && track.getNICc() == 1) { - radiusLimitMetres = 1111.2; - } else if (track.getNIC() == 0 && track.getNICa() == 0 && track.getNICc() == 0) { - isUnknown = true; - } else { - isUnknown = true; - } - } - - public boolean isUnknown() { - return isUnknown; - } - - public double getRadiusLimitMetres() { - return radiusLimitMetres; - } -} diff --git a/src/main/java/aero/t2s/modes/Track.java b/src/main/java/aero/t2s/modes/Track.java index 51bed0f..a3b8184 100644 --- a/src/main/java/aero/t2s/modes/Track.java +++ b/src/main/java/aero/t2s/modes/Track.java @@ -1,51 +1,49 @@ package aero.t2s.modes; import aero.t2s.modes.constants.*; +import aero.t2s.modes.registers.*; import java.time.Instant; public class Track { private String icao; private String callsign; - private int category; - private boolean groundBit; - private int baroAltitude; - private int gnssHeight; + private Altitude altitude = new Altitude(); private double lat; private double lon; - private CprPosition cprPositionEven = new CprPosition(); - private CprPosition cprPositionOdd = new CprPosition(); + private boolean positionAvailable = false; + private int vx; + private int vy; + private double gs; + private Version version = Version.VERSION0; + private boolean groundBit; Instant updated = Instant.now(); - private boolean singleAntenna; - private int NIC; - private int NICb; - private int NICa; - private int NICc; - private RadiusLimit rc = new RadiusLimit(this); - private int NACv; - private NavigationAccuracyCategoryPosition NACp = NavigationAccuracyCategoryPosition.UNKNOWN; + private boolean wasJustCreated = true; + + private Register05 register05 = new Register05V0(); + private Register06 register06 = new Register06(); + private Register07 register07 = new Register07(); + private Register08 register08 = new Register08(); + private Register09 register09 = new Register09(); + private Register17 register17 = new Register17(); + private Register20 register20 = new Register20(); + private Register21 register21 = new Register21(); + private boolean spi; private boolean tempAlert; private boolean emergency; - private Version version = Version.VERSION0; private Acas acas = new Acas(); private FlightStatus flightStatus = new FlightStatus(); - private Altitude altitude = new Altitude(); private SelectedAltitudeSource selectedAltitudeSource = SelectedAltitudeSource.UNKNOWN; private Meteo meteo = new Meteo(); - private CapabilityReport capabilityReport = new CapabilityReport(); private int modeA; private int geometricHeightOffset; private int rocd; private boolean rocdAvailable; private boolean rocdSourceBaro; - private int vx; - private int vy; - private double gs; - private boolean headingSourceMagnetic; + private double magneticHeading; private double trueHeading; - private boolean iasAvailable; private int ias; private double tas; private boolean selectedAltitudeManagedFms; @@ -59,7 +57,6 @@ public class Track { private boolean altitudeHold; private boolean approachMode; private boolean lnav; - private LengthWidthCode lengthWidthCode = LengthWidthCode.CAT15; private EmergencyState emergencyState = EmergencyState.NONE; private int fmsSelectedAltitude; private double rollAngle; @@ -70,8 +67,7 @@ public class Track { private String wtc = ""; private String registration; private String operator; - - private boolean wasJustCreated = true; + private Angle horizontalSource = Angle.UNAVAILABLE; public Track(String icao) { this.icao = icao; @@ -85,143 +81,128 @@ public String getCallsign() { return callsign; } - public void setCategory(int category) { - this.category = category; + public Register05 register05() { + return register05; } - public String getIcao() { - return icao; + public void register05(Register05 register05) { + this.register05 = register05; } - public boolean isExpired() { - return Instant.now().minusSeconds(15).isAfter(updated); + public Register06 register06() { + return register06; } - public Instant getUpdatedAt() { - return updated; + public void register06(Register06 register06) { + this.register06 = register06; } - public void setUpdatedAt(Instant time) { - this.updated = time; + public Register07 register07() { + return register07; } - public void touch() { - updated = Instant.now(); - } - - public void setGroundBit(boolean groundBit) { - this.groundBit = groundBit; + public void register07(Register07 register07) { + this.register07 = register07; } - public boolean isGroundBit() { - return groundBit; + public Register08 register08() { + return register08; } - public void setBaroAltitude(int baroAltitude) { - this.baroAltitude = baroAltitude; + public void register08(Register08 register08) { + this.register08 = register08; } - public int getBaroAltitude() { - return baroAltitude; + public Register09 register09() { + return register09; } - public int getGnssHeight() { - return gnssHeight; + public void register09(Register09 register09) { + this.register09 = register09; } - public Track setGnssHeight(int gnssHeight) { - this.gnssHeight = gnssHeight; - return this; + public Register17 register17() { + return register17; } - public CprPosition getCprPosition(boolean cprEven) { - return cprEven ? cprPositionEven : cprPositionOdd; + public void register17(Register17 register17) { + this.register17 = register17; } - public void setLat(double lat) { - this.lat = lat; + public Register20 register20() { + return register20; } - public double getLat() { - return lat; + public void register20(Register20 register20) { + this.register20 = register20; } - public void setLon(double lon) { - this.lon = lon; + public Register21 register21() { + return register21; } - public double getLon() { - return lon; + public void register21(Register21 register21) { + this.register21 = register21; } - public void setSingleAntenna(boolean singleAntenna) { - this.singleAntenna = singleAntenna; + public String getIcao() { + return icao; } - public boolean getSingleAntenna() { - return singleAntenna; + public boolean isExpired() { + return Instant.now().minusSeconds(15).isAfter(updated); } - public Version getVersion() { - return version; + public Instant getUpdatedAt() { + return updated; } - public void setVersion(Version version) { - this.version = version; + public Track setUpdatedAt(Instant updated) { + this.updated = updated; + return this; } - public int getNIC() { - return NIC; + public void touch() { + updated = Instant.now(); } - public int getNICa() { - return NICa; + public void setGroundBit(boolean groundBit) { + this.groundBit = groundBit; } - public int getNICb() { - return NICb; + public boolean isGroundBit() { + return groundBit; } - public int getNICc() { - return NICc; + public void setLatLon(double lat, double lon) { + this.lat = lat; + this.lon = lon; + this.positionAvailable = true; } - - public void setNIC(int NIC) { - if (this.NIC != NIC) { - this.NIC = NIC; - rc.determine(); - } + public void setLat(double lat) { + //TODO How do we know if position really is available if we only set the lat? Can we remove this method? + this.lat = lat; } - - public void setNICa(int NICa) { - if (this.NICa != NICa) { - this.NICa = NICa; - rc.determine(); - } + public double getLat() { + return lat; } - public void setNICb(int niCb) { - if (niCb != this.NICb) { - this.NICb = niCb; - rc.determine(); - } + public void setLon(double lon) { + //TODO How do we know if position really is available if we only set the lon? Can we remove this method? + this.lon = lon; } - public void setNICc(int NICc) { - if (this.NICc != NICc) { - this.NICc = NICc; - rc.determine(); - } + public double getLon() { + return lon; } - public NavigationAccuracyCategoryPosition getNACp() { - return NACp; + public Version getVersion() { + return version; } - public Track setNACp(NavigationAccuracyCategoryPosition NACp) { - this.NACp = NACp; - return this; + public void setVersion(Version version) { + this.version = version; } public void setSpi(boolean spi) { @@ -273,15 +254,7 @@ public int getModeA() { } public boolean isPositionAvailable() { - return lat != 0 & lon != 0; - } - - public void setNACv(int naCv) { - this.NACv = naCv; - } - - public int getNACv() { - return NACv; + return positionAvailable; } public void setGeometricHeightOffset(int geometricHeightOffset) { @@ -340,14 +313,6 @@ public double getGs() { return gs; } - public boolean isMagneticHeading() { - return headingSourceMagnetic; - } - - public void setHeadingSource(boolean magneticHeading) { - this.headingSourceMagnetic = magneticHeading; - } - public void setMagneticHeading(double magneticHeading) { this.magneticHeading = magneticHeading; } @@ -364,14 +329,6 @@ public double getTrueHeading() { return trueHeading; } - public void setIasAvailable(boolean iasAvailable) { - this.iasAvailable = iasAvailable; - } - - public boolean isIasAvailable() { - return iasAvailable; - } - public void setIas(int ias) { this.ias = ias; } @@ -476,14 +433,6 @@ public boolean getLnav() { return lnav; } - public void setLengthWidthCode(LengthWidthCode lengthWidthCode) { - this.lengthWidthCode = lengthWidthCode; - } - - public LengthWidthCode getLengthWidthCode() { - return lengthWidthCode; - } - public void setEmergencyState(EmergencyState emergencyState) { this.emergencyState = emergencyState; } @@ -586,10 +535,6 @@ public Meteo getMeteo() { return meteo; } - public CapabilityReport getCapabilityReport() { - return capabilityReport; - } - @Override public String toString() { return String.format( @@ -604,4 +549,12 @@ public String toString() { lon ); } + + public void setHorizontalSource(Angle horizontalSource) { + this.horizontalSource = horizontalSource; + } + + public Angle getHorizontalSource() { + return horizontalSource; + } } diff --git a/src/main/java/aero/t2s/modes/constants/AcasReplyInformation.java b/src/main/java/aero/t2s/modes/constants/AcasReplyInformation.java index b903797..4dffee3 100644 --- a/src/main/java/aero/t2s/modes/constants/AcasReplyInformation.java +++ b/src/main/java/aero/t2s/modes/constants/AcasReplyInformation.java @@ -12,11 +12,11 @@ public enum AcasReplyInformation { /** * 2 - reserved for ACAS */ - RESERVED2, + ACAS_RA_INHIBIT, /** * 3 - reserved for ACAS */ - RESERVED3, + ACAS_RA_VERTICAL_ONLY, /** * 4 - reserved for ACAS */ @@ -32,7 +32,7 @@ public enum AcasReplyInformation { /** * 7 - reserved for ACAS */ - RESERVED7, + ACAS_RA_FULL, /** * 8 - no maximum airspeed data available */ diff --git a/src/main/java/aero/t2s/modes/constants/AircraftCategory.java b/src/main/java/aero/t2s/modes/constants/AircraftCategory.java new file mode 100644 index 0000000..663a8a1 --- /dev/null +++ b/src/main/java/aero/t2s/modes/constants/AircraftCategory.java @@ -0,0 +1,33 @@ +package aero.t2s.modes.constants; + +public enum AircraftCategory { + // Category A + NO_ADS_B_EMITTER, + LIGHT, + SMALL, + LARGE, + HIGH_VORTEX_LARGE, + HEAVY, + HIGH_PERFORMANCE, + ROTORCRAFT, + + // Category B + GLIDER, + LIGHTER_THAN_AIR, + SKYDIVER, + ULTRALIGHT, + UNMANNED_AERIAL_VEHICLE, + SPACE, + + // Category C + SURFACE_VEHICLE_EMERGENCY, + SURFACE_VEHICLE_SERVICE, + POINT_OBSTACLE, + CLUSTER_OBSTACLE, + LINE_OBSTACLE, + + // + RESERVED, + UNKNOWN, + ; +} diff --git a/src/main/java/aero/t2s/modes/constants/AltitudeSource.java b/src/main/java/aero/t2s/modes/constants/AltitudeSource.java new file mode 100644 index 0000000..ab5ec54 --- /dev/null +++ b/src/main/java/aero/t2s/modes/constants/AltitudeSource.java @@ -0,0 +1,8 @@ +package aero.t2s.modes.constants; + +public enum AltitudeSource { + UNAVAILABLE, + BARO, + GNSS_HAE, + BARO_GNSS_DIFF, +} diff --git a/src/main/java/aero/t2s/modes/constants/Angle.java b/src/main/java/aero/t2s/modes/constants/Angle.java index df9b473..fbda00b 100644 --- a/src/main/java/aero/t2s/modes/constants/Angle.java +++ b/src/main/java/aero/t2s/modes/constants/Angle.java @@ -8,5 +8,6 @@ public enum Angle { TRACK, MAGNETIC_TRACK, TRUE_TRACK, - ; + + UNAVAILABLE; } diff --git a/src/main/java/aero/t2s/modes/constants/HorizontalProtectionLimit.java b/src/main/java/aero/t2s/modes/constants/HorizontalProtectionLimit.java new file mode 100644 index 0000000..eb515bb --- /dev/null +++ b/src/main/java/aero/t2s/modes/constants/HorizontalProtectionLimit.java @@ -0,0 +1,25 @@ +package aero.t2s.modes.constants; + +public enum HorizontalProtectionLimit { + RC_7_5(7.5), + RC_25(25), + RC_75(75), + RC_185(185.2), + RC_370(370.4), + RC_555(555.6), + RC_926(926), + RC_1111(1111.2), + RC_1852(1852), + RC_3704(3704), + RC_7408(7408), + RC_14816(14816), + RC_37040(37040), + RC_UNKNOWN(-1), + ; + + private final double minAccuracyInMetres; + + HorizontalProtectionLimit(double minAccuracyInMetres) { + this.minAccuracyInMetres = minAccuracyInMetres; + } +} diff --git a/src/main/java/aero/t2s/modes/constants/NavigationAccuracyCategoryVelocity.java b/src/main/java/aero/t2s/modes/constants/NavigationAccuracyCategoryVelocity.java new file mode 100644 index 0000000..37c7f82 --- /dev/null +++ b/src/main/java/aero/t2s/modes/constants/NavigationAccuracyCategoryVelocity.java @@ -0,0 +1,10 @@ +package aero.t2s.modes.constants; + +public enum NavigationAccuracyCategoryVelocity { + UNKNOWN, + LESS_THAN_10_M_S, + LESS_THAN_3_M_S, + LESS_THAN_1_M_S, + LESS_THAN_0_3_M_S, + ; +} diff --git a/src/main/java/aero/t2s/modes/constants/RocdSource.java b/src/main/java/aero/t2s/modes/constants/RocdSource.java index 4493da9..b925837 100644 --- a/src/main/java/aero/t2s/modes/constants/RocdSource.java +++ b/src/main/java/aero/t2s/modes/constants/RocdSource.java @@ -3,5 +3,6 @@ public enum RocdSource { BARO, GNSS, - ; + DIFF_BARO_GNSS, + UNKNOWN; } diff --git a/src/main/java/aero/t2s/modes/constants/Speed.java b/src/main/java/aero/t2s/modes/constants/Speed.java index 4286fe6..0730c74 100644 --- a/src/main/java/aero/t2s/modes/constants/Speed.java +++ b/src/main/java/aero/t2s/modes/constants/Speed.java @@ -7,5 +7,5 @@ public enum Speed { IAS, TAS, GS, - ; + UNKNOWN; } diff --git a/src/main/java/aero/t2s/modes/constants/TransmissionRate.java b/src/main/java/aero/t2s/modes/constants/TransmissionRate.java new file mode 100644 index 0000000..2dfb611 --- /dev/null +++ b/src/main/java/aero/t2s/modes/constants/TransmissionRate.java @@ -0,0 +1,9 @@ +package aero.t2s.modes.constants; + +public enum TransmissionRate { + UNKNOWN, + HIGH_SURFACE_RATE, + LOW_SURFACE_RATE, + RESERVED + ; +} diff --git a/src/main/java/aero/t2s/modes/decoder/AltitudeEncoding.java b/src/main/java/aero/t2s/modes/decoder/AltitudeEncoding.java index f07b99c..0ffdb7f 100644 --- a/src/main/java/aero/t2s/modes/decoder/AltitudeEncoding.java +++ b/src/main/java/aero/t2s/modes/decoder/AltitudeEncoding.java @@ -23,7 +23,8 @@ public static Altitude decode(int encoded) { } private static Altitude decodeFeet(int encoded) { - int n = ((encoded & 0b1111110000000) >>> 2) | (encoded & 0b11111); + // Remove bits 7 & 8 and stitch the binary message together. + int n = ((encoded & 0b1111110000000) >>> 2) + ((encoded & 0b100000) >>> 1) + (encoded & 0b1111); int altitude = (25 * n) - 1000; diff --git a/src/main/java/aero/t2s/modes/decoder/Common.java b/src/main/java/aero/t2s/modes/decoder/Common.java index 995b7a8..b78786c 100644 --- a/src/main/java/aero/t2s/modes/decoder/Common.java +++ b/src/main/java/aero/t2s/modes/decoder/Common.java @@ -106,8 +106,10 @@ public static short[] getParity(short[] data) { } public static boolean isNotValid(short[] data) { + // fixes negative length bug where data is only 2 bytes (16bit Mode A/C replies, ACAS, ...) + // examples: *21D2; *0200; *0101; <- only the 2 bytes are in data variable if(data.length < 3) { - return false; // fixes negative length bug where data is only 2 bytes (16bit Mode A/C replies, ACAS, ...) + return true; } short[] payload = Arrays.copyOfRange(data, 0, data.length - 3); short[] parity = Arrays.copyOfRange(data, data.length - 3, data.length); diff --git a/src/main/java/aero/t2s/modes/decoder/Decoder.java b/src/main/java/aero/t2s/modes/decoder/Decoder.java index 5b563f6..b485453 100644 --- a/src/main/java/aero/t2s/modes/decoder/Decoder.java +++ b/src/main/java/aero/t2s/modes/decoder/Decoder.java @@ -50,7 +50,7 @@ public DownlinkFormat decode(short[] data) throws UnknownDownlinkFormatException df = new DF16(data); break; case 17: - df = new DF17(data, originLat, originLon); + df = new DF17(data); break; case 18: df = new DF18(data); @@ -71,7 +71,7 @@ public DownlinkFormat decode(short[] data) throws UnknownDownlinkFormatException throw new UnknownDownlinkFormatException(downlinkFormat, data); } - return df.decode(); + return df.decode().aircraft(modeSDatabase.find(df.getIcao())); } public Track getTrack(String icao) { diff --git a/src/main/java/aero/t2s/modes/decoder/df/DF16.java b/src/main/java/aero/t2s/modes/decoder/df/DF16.java index f5989c2..fd6f0b6 100644 --- a/src/main/java/aero/t2s/modes/decoder/df/DF16.java +++ b/src/main/java/aero/t2s/modes/decoder/df/DF16.java @@ -9,7 +9,6 @@ public class DF16 extends DownlinkFormat { private VerticalStatus verticalStatus; - private CrossLinkCapability crossLinkCapability; private AcasSensitivity sensitivity; private AcasReplyInformation replyInformation; private Altitude altitude; @@ -28,7 +27,6 @@ public DF16(short[] data) { @Override public DF16 decode() { verticalStatus = VerticalStatus.from((data[0] >>> 2) & 0x1); - crossLinkCapability = CrossLinkCapability.from((data[0] >>> 1) & 0x1); sensitivity = AcasSensitivity.from(data[1] >>> 5); replyInformation = AcasReplyInformation.from(((data[1] & 0x7) << 1) | ((data[2] >> 7) & 0x1)); altitude = AltitudeEncoding.decode((((data[2] << 8) | data[3])) & 0x1FFF); @@ -67,7 +65,6 @@ public DF16 decode() { public void apply(Track track) { Acas acas = track.getAcas(); acas.setVerticalStatus(verticalStatus); - acas.setCrossLinkCapability(crossLinkCapability); acas.setSensitivity(sensitivity); acas.setReplyInformation(replyInformation); acas.setAltitude(altitude); @@ -83,10 +80,6 @@ public VerticalStatus getVerticalStatus() { return verticalStatus; } - public CrossLinkCapability getCrossLinkCapability() { - return crossLinkCapability; - } - public AcasSensitivity getSensitivity() { return sensitivity; } diff --git a/src/main/java/aero/t2s/modes/decoder/df/DF17.java b/src/main/java/aero/t2s/modes/decoder/df/DF17.java index 622caf4..1bd10b5 100644 --- a/src/main/java/aero/t2s/modes/decoder/df/DF17.java +++ b/src/main/java/aero/t2s/modes/decoder/df/DF17.java @@ -4,15 +4,10 @@ import aero.t2s.modes.decoder.df.df17.*; public class DF17 extends DownlinkFormat { - private final double originLat; - private final double originLon; - private ExtendedSquitter extendedSquitter; - public DF17(short[] data, double originLat, double originLon) { + public DF17(short[] data) { super(data, IcaoAddress.FROM_MESSAGE); - this.originLat = originLat; - this.originLon = originLon; } @Override @@ -34,7 +29,7 @@ public DF17 decode() { case 20: case 21: case 22: - extendedSquitter = new AirbornePosition(data, originLat, originLon); + extendedSquitter = new AirbornePosition(data, getIcao()); break; case 1: case 2: @@ -46,7 +41,7 @@ public DF17 decode() { case 6: case 7: case 8: - extendedSquitter = new SurfacePosition(data); + extendedSquitter = new SurfacePosition(data, getIcao()); break; case 19: extendedSquitter = new AirborneVelocity(data); diff --git a/src/main/java/aero/t2s/modes/decoder/df/DF18.java b/src/main/java/aero/t2s/modes/decoder/df/DF18.java index e989c0b..60c766d 100644 --- a/src/main/java/aero/t2s/modes/decoder/df/DF18.java +++ b/src/main/java/aero/t2s/modes/decoder/df/DF18.java @@ -15,6 +15,22 @@ public DF18 decode() { int typeCode = data[4] >>> 3; switch (typeCode) { + case 0: + case 9: + case 10: + case 11: + case 12: + case 13: + case 14: + case 15: + case 16: + case 17: + case 18: + case 20: + case 21: + case 22: + extendedSquitter = new AirbornePosition(data, getIcao()); + break; case 1: case 2: case 3: @@ -25,7 +41,7 @@ public DF18 decode() { case 6: case 7: case 8: - extendedSquitter = new SurfacePosition(data); + extendedSquitter = new SurfacePosition(data, getIcao()); break; case 19: extendedSquitter = new AirborneVelocity(data); diff --git a/src/main/java/aero/t2s/modes/decoder/df/DF24.java b/src/main/java/aero/t2s/modes/decoder/df/DF24.java index 8503e6e..b7f8085 100644 --- a/src/main/java/aero/t2s/modes/decoder/df/DF24.java +++ b/src/main/java/aero/t2s/modes/decoder/df/DF24.java @@ -1,20 +1,27 @@ package aero.t2s.modes.decoder.df; -import aero.t2s.modes.NotImplementedException; import aero.t2s.modes.Track; public class DF24 extends DownlinkFormat { + private int sequenceNo = 0; + public DF24(short[] data) { super(data, IcaoAddress.FROM_PARITY); } @Override public DF24 decode() { - throw new NotImplementedException(getClass().getSimpleName() + ": Not implemented"); + sequenceNo = data[0] & 0b00000111; + + return this; } @Override public void apply(Track track) { - // + // Not implemented + } + + public int getSequenceNo() { + return sequenceNo; } } diff --git a/src/main/java/aero/t2s/modes/decoder/df/DownlinkFormat.java b/src/main/java/aero/t2s/modes/decoder/df/DownlinkFormat.java index 0e9bf08..6b567dd 100644 --- a/src/main/java/aero/t2s/modes/decoder/df/DownlinkFormat.java +++ b/src/main/java/aero/t2s/modes/decoder/df/DownlinkFormat.java @@ -1,6 +1,7 @@ package aero.t2s.modes.decoder.df; import aero.t2s.modes.Track; +import aero.t2s.modes.database.ModeSDatabase; import aero.t2s.modes.decoder.Common; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -12,6 +13,7 @@ public abstract class DownlinkFormat { protected final short[] data; private final String icao; + private ModeSDatabase.ModeSAircraft aircraft; public DownlinkFormat(short[] data, IcaoAddress icaoAddressFrom) { this.data = data; @@ -35,6 +37,16 @@ public short[] getData() { return data; } + public DownlinkFormat aircraft(ModeSDatabase.ModeSAircraft aircraft) { + this.aircraft = aircraft == null ? new ModeSDatabase.ModeSAircraft(this.getIcao(), null, null, null) : aircraft; + + return this; + } + + public ModeSDatabase.ModeSAircraft getAircraft() { + return this.aircraft; + } + protected enum IcaoAddress { FROM_MESSAGE, FROM_PARITY, diff --git a/src/main/java/aero/t2s/modes/decoder/df/bds/Bds.java b/src/main/java/aero/t2s/modes/decoder/df/bds/Bds.java index 4740bca..c83856c 100644 --- a/src/main/java/aero/t2s/modes/decoder/df/bds/Bds.java +++ b/src/main/java/aero/t2s/modes/decoder/df/bds/Bds.java @@ -23,4 +23,8 @@ protected void invalidate() { public boolean isValid() { return valid; } + + public short[] getData() { + return data; + } } diff --git a/src/main/java/aero/t2s/modes/decoder/df/bds/Bds17.java b/src/main/java/aero/t2s/modes/decoder/df/bds/Bds17.java index d677e1c..767618c 100644 --- a/src/main/java/aero/t2s/modes/decoder/df/bds/Bds17.java +++ b/src/main/java/aero/t2s/modes/decoder/df/bds/Bds17.java @@ -73,7 +73,7 @@ public Bds17(short[] data) { @Override public void apply(Track track) { - track.getCapabilityReport().update(this); + track.register17().update(this); } @Override diff --git a/src/main/java/aero/t2s/modes/decoder/df/bds/Bds20.java b/src/main/java/aero/t2s/modes/decoder/df/bds/Bds20.java index c1f6205..ded30ce 100644 --- a/src/main/java/aero/t2s/modes/decoder/df/bds/Bds20.java +++ b/src/main/java/aero/t2s/modes/decoder/df/bds/Bds20.java @@ -29,6 +29,7 @@ public Bds20(short[] data) { @Override public void apply(Track track) { track.setCallsign(acid); + track.register20().setAcid(acid); } @Override diff --git a/src/main/java/aero/t2s/modes/decoder/df/bds/Bds21.java b/src/main/java/aero/t2s/modes/decoder/df/bds/Bds21.java index 455178a..ec805fc 100644 --- a/src/main/java/aero/t2s/modes/decoder/df/bds/Bds21.java +++ b/src/main/java/aero/t2s/modes/decoder/df/bds/Bds21.java @@ -69,6 +69,10 @@ public void apply(Track track) { if (statusAirlineRegistration) { track.setOperator(airline); } + + if (statusAircraftRegistration || statusAirlineRegistration) { + track.register21().update(registration, airline); + } } @Override diff --git a/src/main/java/aero/t2s/modes/decoder/df/bds/Bds40.java b/src/main/java/aero/t2s/modes/decoder/df/bds/Bds40.java index 6767d71..2686301 100644 --- a/src/main/java/aero/t2s/modes/decoder/df/bds/Bds40.java +++ b/src/main/java/aero/t2s/modes/decoder/df/bds/Bds40.java @@ -27,10 +27,10 @@ *

MCP/FCU Selected altitude

* *

- * This information which represents the real “aircraft intent,” when available, + * This information which represents the real "aircraft intent," when available, * represented by the altitude control panel selected altitude, * the flight management system selected altitude, - * or the current aircraft altitude according to the aircraft’s mode of flight + * or the current aircraft altitude according to the aircraft's mode of flight * (the intent may not be available at all when the pilot is flying the aircraft). *

* @@ -39,7 +39,7 @@ * See {@link SelectedAltitudeSource} for more details *

* - * Note: LSB (1 bit) = 16feet with a range 0 - 65520 feet, this class considers altitudes above 50000ft as invalid / error. + * Note: LSB (1 bit) = 16feet with a range 0 - 65520 feet, this class considers altitudes above 52000ft as invalid / error. * *

FMS Selected altitude

* @@ -54,13 +54,13 @@ * The FMS selected altitude field is transmitting 32000ft, the Target Altitude Source flag is set to MCP. *

* - * Note: LSB (1 bit) = 16feet with a range 0 - 65520 feet, this class considers altitudes above 50000ft as invalid / error. + * Note: LSB (1 bit) = 16feet with a range 0 - 65520 feet, this class considers altitudes above 52000ft as invalid / error. * *

Barometric Pressure Setting

* - *

When status flag (bit 27) is set to false indicates the information is valid and van be used

+ *

When status flag (bit 27) is set to 1 indicates the information is valid and van be used

* - * Note: LSB (1 bit) = 0.1mb with a range 0 - 410mb. You need to add 800mb to receive the real baro steting + * Note: LSB (1 bit) = 0.1mb with a range 0 - 410mb. You need to add 800mb to receive the real baro setting * *

MCP/FCU Mode bits

*

@@ -113,7 +113,7 @@ public Bds40(short[] data) { selectedAltitude = (((data[4] & 0b01111111) << 5) | (data[5] & 0b11111000) >>> 3) * 16; if (statusMcp) { - if (selectedAltitude > 50000) { + if (selectedAltitude > 52000) { invalidate(); return; } @@ -126,7 +126,7 @@ public Bds40(short[] data) { fmsAltitude = (((data[5] & 0x3) << 10) | (data[6] << 2) | ((data[7] >>> 6) & 0x3)) * 16; if (statusFms) { - if (fmsAltitude <= 0 || fmsAltitude > 50000) { + if (fmsAltitude < 0 || fmsAltitude > 52000) { invalidate(); return; } diff --git a/src/main/java/aero/t2s/modes/decoder/df/bds/Bds44.java b/src/main/java/aero/t2s/modes/decoder/df/bds/Bds44.java index 26c6680..f202da7 100644 --- a/src/main/java/aero/t2s/modes/decoder/df/bds/Bds44.java +++ b/src/main/java/aero/t2s/modes/decoder/df/bds/Bds44.java @@ -33,6 +33,14 @@ public Bds44(short[] data) { } statusWindSpeed = (data[4] & 0b00001000) != 0; + statusAverageStaticPressure = (data[8] & 0b00100000) != 0; + statusTurbulence = (data[9] & 0b00000010) != 0; + + if (!statusWindSpeed) { + invalidate(); + return; + } + windSpeed = (data[4] & 0b00000111) << 6 | data[5] >> 2; windDirection = ((data[5] & 0b00000011) << 7 | data[6] >> 1) * WIND_DIRECTION_ACCURACY; if (!statusWindSpeed && windSpeed != 0) { @@ -52,14 +60,12 @@ public Bds44(short[] data) { return; } - statusAverageStaticPressure = (data[8] & 0b00100000) != 0; averageStaticPressure = ((data[8] & 0b00011111) << 6) | data[9] >> 2; if (!statusAverageStaticPressure && averageStaticPressure != 0) { invalidate(); return; } - statusTurbulence = (data[9] & 0b00000010) != 0; turbulence = Hazard.find(((data[9] & 0b00000001) << 1) | data[10] >>> 7); if (!statusTurbulence && turbulence != Hazard.NIL) { invalidate(); diff --git a/src/main/java/aero/t2s/modes/decoder/df/bds/Bds45.java b/src/main/java/aero/t2s/modes/decoder/df/bds/Bds45.java index 60d956d..7b6f8b4 100644 --- a/src/main/java/aero/t2s/modes/decoder/df/bds/Bds45.java +++ b/src/main/java/aero/t2s/modes/decoder/df/bds/Bds45.java @@ -1,8 +1,10 @@ package aero.t2s.modes.decoder.df.bds; +import aero.t2s.modes.Altitude; import aero.t2s.modes.Meteo; import aero.t2s.modes.Track; import aero.t2s.modes.constants.Hazard; +import aero.t2s.modes.decoder.AltitudeEncoding; public class Bds45 extends Bds { private static final double SAT_ACCURACY = 0.25d; @@ -105,6 +107,53 @@ public Bds45(short[] data) { invalidate(); return; } + + // Windshear + turbulence + microburst => Very unlikely to be a BDS 45 valid message let's flag it as invalid + if (statusTurbulence && statusWindShear && statusMicroBurst) { + invalidate(); + return; + } + + if (statusMicroBurst) { + // If message is DF20 (altitude encoding) and altitude is above 10000ft microburst is unlikely flag as invalid + if (data[0] >>> 3 == 20) { + Altitude altitude = AltitudeEncoding.decode((data[2] & 0x1F) << 8 | data[3]); + if (altitude.getAltitude() > 10_000) { + invalidate(); + return; + } + } + // DF 21 message since we do not have altitude info and message is likely to be BDS17 flag BDS45 on DF21 with microburst as invalid + else { + invalidate(); + return; + } + } + + if (statusTurbulence || statusWindShear || statusMicroBurst || statusIcing || statusWake) { + boolean nothing = true; + + if (statusTurbulence && turbulence != Hazard.NIL) { + nothing = false; + } + if (statusWindShear && windShear != Hazard.NIL) { + nothing = false; + } + if (statusMicroBurst && microBurst != Hazard.NIL) { + nothing = false; + } + if (statusIcing && icing != Hazard.NIL) { + nothing = false; + } + if (statusWake && wake != Hazard.NIL) { + nothing = false; + } + + if (nothing) { + invalidate(); + return; + } + } } diff --git a/src/main/java/aero/t2s/modes/decoder/df/bds/Bds50.java b/src/main/java/aero/t2s/modes/decoder/df/bds/Bds50.java index cf23e53..3098ae8 100644 --- a/src/main/java/aero/t2s/modes/decoder/df/bds/Bds50.java +++ b/src/main/java/aero/t2s/modes/decoder/df/bds/Bds50.java @@ -1,6 +1,8 @@ package aero.t2s.modes.decoder.df.bds; +import aero.t2s.modes.Altitude; import aero.t2s.modes.Track; +import aero.t2s.modes.decoder.AltitudeEncoding; /** * 56-bit MB Field is structured in the following format @@ -201,6 +203,48 @@ public Bds50(short[] data) { invalidate(); return; } + + // Check if values are way off th scale. + // We can only check large values + if ((statusTas || statusGs) && statusTrackAngle && statusRollAngle && Math.abs(trackAngleRate) > 0.25 && Math.abs(rollAngle) > 5) { + // We cannot have a rate one turn at 180 knots or greater at less than 30 degrees of bank + if ((gs > 180 || tas > 180) && rollAngle < 30 && trackAngleRate > 3) { + invalidate(); + return; + } + } + } + + public Bds compareWithBds60(Bds60 bds60) { + // average speed of sound between 0 - 45000ft + double speedOfSound = 617d; + + short[] data = getData(); + if (data[0] >>> 3 == 20) { + Altitude altitude = AltitudeEncoding.decode((data[2] & 0x1F) << 8 | data[3]); + // Estimated TAS + double bds60Tas = bds60.getIas() * (1 + (altitude.getAltitude() / 1000) * 0.02); + // Estimated Mach + double bds60Mach = bds60Tas / speedOfSound; + + if (Math.abs(bds60Mach - bds60.getMach()) < 0.1) { + return bds60; + } + + return this; + } else { + // No altitude information assume check in 2000ft increments + for (int altitude = 0; altitude < 45; altitude += 2) { + double bds60Tas = bds60.getIas() * (1 + altitude * 0.02); + double bds60Mach = bds60Tas / speedOfSound; + + if (Math.abs(bds60Mach - bds60.getMach()) < 0.01) { + return bds60; + } + } + + return this; + } } @Override diff --git a/src/main/java/aero/t2s/modes/decoder/df/bds/Bds60.java b/src/main/java/aero/t2s/modes/decoder/df/bds/Bds60.java index 55bf427..1d1f697 100644 --- a/src/main/java/aero/t2s/modes/decoder/df/bds/Bds60.java +++ b/src/main/java/aero/t2s/modes/decoder/df/bds/Bds60.java @@ -79,7 +79,7 @@ public Bds60(short[] data) { } double baroSign = ((data[8] >>> 4) & 0x1) == 1 ? -512.0 : 0.0; - baroRocd = ((((data[8] & 0b00001111) << 5) | (data[9] >>> 3)) + baroSign) * ROCD_ACCURCY; + baroRocd = (((((data[8] & 0b00001111) << 5) | (data[9] >>> 3)) + baroSign) * ROCD_ACCURCY) % 16384; if (statusBaroRocd) { if (baroRocd < -8000 || baroRocd > 8000) { invalidate(); @@ -93,7 +93,7 @@ public Bds60(short[] data) { } double irsSign = ((data[9] >> 1) & 0x1) == 1 ? -512.0 : 0.0; - irsRocd = (((data[9] & 0x1) << 8 | data[10]) + irsSign) * ROCD_ACCURCY; + irsRocd = ((((data[9] & 0x1) << 8 | data[10]) + irsSign) * ROCD_ACCURCY) % 16384; if (statusIrsRocd) { if (irsRocd < -8000 || irsRocd > 6000) { invalidate(); @@ -106,6 +106,14 @@ public Bds60(short[] data) { } } + if ((irsRocd > 0 && baroRocd < 0) || (irsRocd < 0 && baroRocd > 0)) { + // Aircraft cannot be climbing and descending at the same time + if (Math.abs(irsRocd) > 1000 && Math.abs(baroRocd) > 1000) { + invalidate(); + return; + } + } + if (statusMach && statusIas && data[0] >>> 3 == 20) { double altitude = AltitudeEncoding.decode((data[2] & 0x1F) << 8 | data[3]).getAltitude(); if (altitude > 0) { diff --git a/src/main/java/aero/t2s/modes/decoder/df/bds/BdsDecoder.java b/src/main/java/aero/t2s/modes/decoder/df/bds/BdsDecoder.java index bb7209d..0c57ca5 100644 --- a/src/main/java/aero/t2s/modes/decoder/df/bds/BdsDecoder.java +++ b/src/main/java/aero/t2s/modes/decoder/df/bds/BdsDecoder.java @@ -44,6 +44,18 @@ public Bds decode() throws MultipleBdsMatchesFoundException, EmptyMessageExcepti } if (valid.size() > 1) { + // Is BDS 50 / 60 pair + if (valid.size() == 2 && valid.stream().anyMatch(bds -> bds.getClass().equals(Bds50.class)) && valid.stream().anyMatch(bds -> bds.getClass().equals(Bds60.class))) { + Bds bds50 = valid.stream().filter(bds -> bds.getClass().equals(Bds50.class)).findFirst().get(); + Bds bds60 = valid.stream().filter(bds -> bds.getClass().equals(Bds60.class)).findFirst().get(); + + Bds likelyBds = ((Bds50)bds50).compareWithBds60((Bds60) bds60); + + if (likelyBds != null) { + return likelyBds; + } + } + throw new MultipleBdsMatchesFoundException(valid); } diff --git a/src/main/java/aero/t2s/modes/decoder/df/df17/AirbornePosition.java b/src/main/java/aero/t2s/modes/decoder/df/df17/AirbornePosition.java index 67129e2..eb37d3a 100644 --- a/src/main/java/aero/t2s/modes/decoder/df/df17/AirbornePosition.java +++ b/src/main/java/aero/t2s/modes/decoder/df/df17/AirbornePosition.java @@ -1,28 +1,29 @@ package aero.t2s.modes.decoder.df.df17; import aero.t2s.modes.Track; -import aero.t2s.modes.constants.BarometricAltitudeIntegrityCode; -import aero.t2s.modes.constants.SurveillanceStatus; -import aero.t2s.modes.constants.Version; +import aero.t2s.modes.CprPosition; +import aero.t2s.modes.constants.*; +import aero.t2s.modes.registers.Register05; +import aero.t2s.modes.registers.Register05V0; +import aero.t2s.modes.registers.Register05V2; -public class AirbornePosition extends ExtendedSquitter { - private final double originLat; - private final double originLon; +import java.util.*; +public class AirbornePosition extends ExtendedSquitter { + private final String address; private SurveillanceStatus surveillanceStatus; private int singleAntennaFlag; private boolean altitudeSourceBaro; private int altitude; - private boolean positionAvailable; private double lat; private double lon; + private boolean positionAvailable; - public AirbornePosition(short[] data, final double originLat, final double originLon) { + public AirbornePosition(short[] data, String address) { super(data); - this.originLat = originLat; - this.originLon = originLon; + this.address = address; } @Override @@ -42,10 +43,8 @@ public AirbornePosition decode() { return this; } - positionAvailable = true; - int time = (data[6] >>> 3) & 0x1; - boolean cprEven = ((data[6] >>> 2) & 0x1) == 0; + boolean isCprEven = ((data[6] >>> 2) & 0x1) == 0; int cprLat = (data[6] & 0x3) << 15; cprLat = cprLat | (data[7] << 7); @@ -55,8 +54,14 @@ public AirbornePosition decode() { cprLon = cprLon | (data[9] << 8); cprLon = cprLon | data[10]; - calculatePosition(cprEven, ((double)cprLat) / ((double)(1 << 17)), ((double)cprLon) / ((double)(1 << 17)), time); - + CprPosition newPosition = PositionUpdate.calculate(address, isCprEven, new CprPosition(cprLat, cprLon, false)); + if (newPosition != null) { + this.lat = newPosition.getLat(); + this.lon = newPosition.getLon(); + this.positionAvailable = true; + } else { + this.positionAvailable = false; + } return this; } @@ -66,23 +71,43 @@ public void apply(Track track) { track.setSpi(surveillanceStatus == SurveillanceStatus.SPI); track.setTempAlert(surveillanceStatus == SurveillanceStatus.TEMPORARY_ALERT); track.setEmergency(surveillanceStatus == SurveillanceStatus.PERMANENT_ALERT); + if (positionAvailable) { + track.setLatLon(lat, lon); + } - // Determine Antenna or Navigation Integrity Category - if (track.getVersion().ordinal() < Version.VERSION2.ordinal()) { - track.setSingleAntenna(singleAntennaFlag == 0); - } else { - track.setNICb(singleAntennaFlag); + if (versionChanged(track)) { + switch (track.getVersion()) { + case VERSION0: + case VERSION1: + track.register05(new Register05V0()); + break; + case VERSION2: + track.register05(new Register05V2()); + break; + } } + HorizontalProtectionLimit hpl = determineHorizontalProtection(track); + AltitudeSource altitudeSource = determineAltitudeSource(); + + Register05 position = track.register05(); - if (altitudeSourceBaro) { - track.setBaroAltitude(altitude); + if (position instanceof Register05V2) { + position.update(hpl, altitude, altitudeSource, lat, lon, surveillanceStatus); } else { - track.setGnssHeight(altitude); + ((Register05V0) position).update(hpl, altitude, altitudeSource, lat, lon, surveillanceStatus, singleAntennaFlag == 1); + } + } + + private boolean versionChanged(Track track) { + if (track.register05() == null) { + return true; + } + + if (track.getVersion() == Version.VERSION2 && track.register05().getVersion() != Version.VERSION2) { + return true; } - track.setNIC(determineNIC(track, typeCode)); - track.setLat(lat); - track.setLon(lon); + return false; } public int getSingleAntennaFlag() { @@ -99,14 +124,6 @@ public BarometricAltitudeIntegrityCode getNICbaro() { } } - public double getOriginLat() { - return originLat; - } - - public double getOriginLon() { - return originLon; - } - public SurveillanceStatus getSurveillanceStatus() { return surveillanceStatus; } @@ -131,137 +148,57 @@ public double getLon() { return lon; } - private int determineNIC(Track track, int typeCode) { + private HorizontalProtectionLimit determineHorizontalProtection(Track track) { switch (typeCode) { case 9: case 20: - return 11; + return HorizontalProtectionLimit.RC_7_5; case 10: case 21: - return 10; + return HorizontalProtectionLimit.RC_25; case 11: - if (track.getNICa() == 1 && track.getNICb() == 1) { - return 9; - } else if (track.getNICa() == 0 && track.getNICb() == 0) { - return 8; + if (singleAntennaFlag == 1 && track.getVersion() == Version.VERSION2) { + return HorizontalProtectionLimit.RC_75; } else { - return 0; + return HorizontalProtectionLimit.RC_185; } case 12: - return 7; + return HorizontalProtectionLimit.RC_370; case 13: - return 6; + if (singleAntennaFlag == 1 && track.getVersion() == Version.VERSION2) { + return HorizontalProtectionLimit.RC_555; + } else { + return HorizontalProtectionLimit.RC_926; + } case 14: - return 5; + return HorizontalProtectionLimit.RC_1852; case 15: - return 4; + return HorizontalProtectionLimit.RC_3704; case 16: - if (track.getNICa() == 0 && track.getNICb() == 0) { - return 2; - } else if (track.getNICa() == 1 && track.getNICb() == 1) { - return 3; + if (singleAntennaFlag == 1 && track.getVersion() == Version.VERSION2) { + return HorizontalProtectionLimit.RC_7408; } else { - return 0; + return HorizontalProtectionLimit.RC_14816; } case 17: - return 1; + return HorizontalProtectionLimit.RC_37040; case 18: case 22: default: - return 0; + return HorizontalProtectionLimit.RC_UNKNOWN; } } - private void calculatePosition(boolean isEven, double lat, double lon, double time) { -// CprPosition cprEven = track.getCprPosition(true); -// CprPosition cprOdd = track.getCprPosition(false); - -// if (! (cprEven.isValid() && cprOdd.isValid())) { - calculateLocal(isEven, lat, lon, time); -// return; -// } - -// calculateGlobal(track, cprEven, cprOdd); - } - - private void calculateLocal(boolean isEven, double lat, double lon, double time) { - boolean isOdd = !isEven; -// CprPosition cpr = track.getCprPosition(isEven); - - double dlat = isOdd ? 360.0 / 59.0 : 360.0 / 60.0; - - double j = Math.floor(originLat / dlat) + Math.floor((originLat % dlat) / dlat - lat + 0.5); - - lat = dlat * (j + lat); - - double nl = NL(lat) - (isOdd ? 1.0 : 0.0); - double dlon = nl > 0 ? 360.0 / nl : 360; - - double m = Math.floor(originLon / dlon) + Math.floor((originLon % dlon) / dlon - lon + 0.5); - lon = dlon * (m + lon); + private AltitudeSource determineAltitudeSource() { + if (typeCode < 19) { + return AltitudeSource.BARO; + } - this.lat = lat; - this.lon = lon; - } + if (typeCode == 19) { + return AltitudeSource.BARO_GNSS_DIFF; + } -// private void calculateGlobal(Track track, CprPosition cprEven, CprPosition cprOdd) { -// double dLat0 = 360.0 / 60.0; -// double dLat1 = 360.0 / 59.0; -// -// double j = Math.floor(59.0 * cprEven.getLat() - 60.0 * cprOdd.getLat() + 0.5); -// -// double latEven = dLat0 * (j % 60.0 + cprEven.getLat()); -// double latOdd = dLat1 * (j % 59.0 + cprOdd.getLat()); -// -// if (latEven >= 270.0 && latEven <= 360.0) { -// latEven -= 360.0; -// } -// -// if (latOdd >= 270.0 && latOdd <= 360.0) { -// latOdd -= 360.0; -// } -// -// if (NL(latEven) != NL(latOdd)) { -// return; -// } -// -// double lat; -// double lon; -// if (cprEven.getTime() > cprOdd.getTime()) { -// double ni = cprN(latEven, 0); -// double m = Math.floor(cprEven.getLon() * (NL(latEven) - 1) - cprOdd.getLon() * NL(latEven) + 0.5); -// -// lat = latEven; -// lon = (360d / ni) * (m % ni + cprEven.getLon()); -// } else { -// double ni = cprN(latOdd, 1); -// double m = Math.floor(cprEven.getLon() * (NL(latOdd) - 1) - cprOdd.getLon() * NL(latOdd) + 0.5); -// -// lat = latOdd; -// lon = (360d / ni) * (m % ni + cprOdd.getLon()); -// } -// -// if (lon > 180d) { -// lon -= 360d; -// } -// -// track.setLat(lat); -// track.setLon(lon); -// } - -// private double cprN(double lat, double isOdd) { -// double nl = NL(lat) - isOdd; -// -// return nl > 1 ? nl : 1; -// } - - private double NL(double lat) { - if (lat == 0) return 59; - else if (Math.abs(lat) == 87) return 2; - else if (Math.abs(lat) > 87) return 1; - - double tmp = 1 - (1 - Math.cos(Math.PI / (2.0 * 15.0))) / Math.pow(Math.cos(Math.PI / 180.0 * Math.abs(lat)), 2); - return Math.floor(2 * Math.PI / Math.acos(tmp)); + return AltitudeSource.GNSS_HAE; } private int calculateAltitude(short[] data, int typeCode) { diff --git a/src/main/java/aero/t2s/modes/decoder/df/df17/AirborneVelocityAirspeedHeading.java b/src/main/java/aero/t2s/modes/decoder/df/df17/AirborneVelocityAirspeedHeading.java index 4d819cf..7f67d26 100644 --- a/src/main/java/aero/t2s/modes/decoder/df/df17/AirborneVelocityAirspeedHeading.java +++ b/src/main/java/aero/t2s/modes/decoder/df/df17/AirborneVelocityAirspeedHeading.java @@ -1,8 +1,9 @@ package aero.t2s.modes.decoder.df.df17; import aero.t2s.modes.Track; -import aero.t2s.modes.constants.RocdSource; -import aero.t2s.modes.constants.Speed; +import aero.t2s.modes.constants.*; +import aero.t2s.modes.registers.Register09; +import aero.t2s.modes.registers.Register09V0; public class AirborneVelocityAirspeedHeading extends AirborneVelocity { private static final double HEADING_RESOLUTION = 360.0 / 1024.0; @@ -35,8 +36,6 @@ public AirborneVelocityAirspeedHeading decode() { @Override public void apply(Track track) { - track.setNACv(NACv.ordinal()); - if (isGnssAltitudeDifferenceFromBaroAvailable()) { track.setGeometricHeightOffset(getGnssAltitudeDifferenceFromBaro()); } @@ -50,6 +49,31 @@ public void apply(Track track) { track.setRocd(getRocd()); } } + + + if (track.getVersion().equals(Version.VERSION2) && track.register09() instanceof Register09V0) { + track.register09(new Register09V0()); + } + if (!track.getVersion().equals(Version.VERSION2) && !(track.register09() instanceof Register09V0)) { + track.register09(new Register09()); + } + + if (track.register09() instanceof Register09V0) { + ((Register09V0) track.register09()).setIfrCapability(isIfrCapability()); + } + + track.register09() + .setHeadingSource(isHeadingAvailable() ? Angle.HEADING : Angle.UNAVAILABLE) + .setHeading((int) Math.round(heading)) + .setAirspeedSource(isAirspeedAvailable() ? airspeedType : Speed.UNKNOWN) + .setAirspeed(airspeed) + .setVerticalRateSource(isRocdAvailable() ? getRocdSource() : RocdSource.UNKNOWN) + .setVerticalRate(isRocdAvailable() ? getRocd() : 0) + .setGnssDifferenceFromBaro(isGnssAltitudeDifferenceFromBaroAvailable() ? getGnssAltitudeDifferenceFromBaro() : 0) + .setIntentChangeFlag(isIntentChange()) + .setNACv(NavigationAccuracyCategoryVelocity.values()[NACv.ordinal()]) + .validate(); + ; } public boolean isHeadingAvailable() { diff --git a/src/main/java/aero/t2s/modes/decoder/df/df17/AirborneVelocityGroundspeed.java b/src/main/java/aero/t2s/modes/decoder/df/df17/AirborneVelocityGroundspeed.java index 8cc08aa..7cdeaac 100644 --- a/src/main/java/aero/t2s/modes/decoder/df/df17/AirborneVelocityGroundspeed.java +++ b/src/main/java/aero/t2s/modes/decoder/df/df17/AirborneVelocityGroundspeed.java @@ -1,7 +1,11 @@ package aero.t2s.modes.decoder.df.df17; import aero.t2s.modes.Track; +import aero.t2s.modes.constants.NavigationAccuracyCategoryVelocity; import aero.t2s.modes.constants.RocdSource; +import aero.t2s.modes.constants.Version; +import aero.t2s.modes.registers.Register09; +import aero.t2s.modes.registers.Register09V0; public class AirborneVelocityGroundspeed extends AirborneVelocity { private boolean xVelocityAvailable; @@ -38,8 +42,6 @@ public AirborneVelocityGroundspeed decode() { @Override public void apply(Track track) { - track.setNACv(NACv.ordinal()); - if (isGnssAltitudeDifferenceFromBaroAvailable()) { track.setGeometricHeightOffset(getGnssAltitudeDifferenceFromBaro()); } @@ -65,5 +67,46 @@ public void apply(Track track) { if (xVelocityAvailable && yVelocityAvailable) { track.setGs(Math.sqrt(xVelocity * xVelocity + yVelocity * yVelocity)); } + + if (track.getVersion().equals(Version.VERSION2) && track.register09() instanceof Register09V0) { + track.register09(new Register09V0()); + } + if (!track.getVersion().equals(Version.VERSION2) && !(track.register09() instanceof Register09V0)) { + track.register09(new Register09()); + } + + if (track.register09() instanceof Register09V0) { + ((Register09V0) track.register09()).setIfrCapability(isIfrCapability()); + } + + track.register09() + .setVerticalRateSource(isRocdAvailable() ? getRocdSource() : RocdSource.UNKNOWN) + .setVerticalRate(isRocdAvailable() ? getRocd() : 0) + .setGnssDifferenceFromBaro(isGnssAltitudeDifferenceFromBaroAvailable() ? getGnssAltitudeDifferenceFromBaro() : 0) + .setIntentChangeFlag(isIntentChange()) + .setNACv(NavigationAccuracyCategoryVelocity.values()[NACv.ordinal()]) + .validate(); + + if (xVelocityAvailable && yVelocityAvailable) { + track.register09() + .setVx(xVelocity) + .setVy(yVelocity); + } + } + + public boolean isVxAvailable() { + return xVelocityAvailable; + } + + public int getVx() { + return xVelocity; + } + + public boolean isVyAvailable() { + return yVelocityAvailable; + } + + public int getVy() { + return yVelocity; } } diff --git a/src/main/java/aero/t2s/modes/decoder/df/df17/AircraftIdentification.java b/src/main/java/aero/t2s/modes/decoder/df/df17/AircraftIdentification.java index 6b04cc4..d2fc21f 100644 --- a/src/main/java/aero/t2s/modes/decoder/df/df17/AircraftIdentification.java +++ b/src/main/java/aero/t2s/modes/decoder/df/df17/AircraftIdentification.java @@ -1,10 +1,12 @@ package aero.t2s.modes.decoder.df.df17; import aero.t2s.modes.Track; +import aero.t2s.modes.constants.AircraftCategory; import aero.t2s.modes.decoder.Common; public class AircraftIdentification extends ExtendedSquitter { private String acid; + private int aircraftEmitterCategory; public AircraftIdentification(short[] data) { super(data); @@ -21,6 +23,7 @@ public AircraftIdentification decode() { Common.charToString(((data[9] & 0xF) << 2) | (data[10] >>> 6)) + Common.charToString(data[10] & 0x3F); acid = acid.replace("_", ""); + aircraftEmitterCategory = data[4] >> 4; return this; } @@ -28,6 +31,51 @@ public AircraftIdentification decode() { @Override public void apply(Track track) { track.setCallsign(acid); + + track.register08().update(acid, determineAircraftCategory()); + } + + private AircraftCategory determineAircraftCategory() { + switch (typeCode) { + case 4: + switch (aircraftEmitterCategory) { + case 0: return AircraftCategory.NO_ADS_B_EMITTER; + case 1: return AircraftCategory.LIGHT; + case 2: return AircraftCategory.SMALL; + case 3: return AircraftCategory.LARGE; + case 4: return AircraftCategory.HIGH_VORTEX_LARGE; + case 5: return AircraftCategory.HEAVY; + case 6: return AircraftCategory.HIGH_PERFORMANCE; + case 7: return AircraftCategory.ROTORCRAFT; + default: return AircraftCategory.UNKNOWN; + } + case 3: + switch (aircraftEmitterCategory) { + case 0: return AircraftCategory.NO_ADS_B_EMITTER; + case 1: return AircraftCategory.GLIDER; + case 2: return AircraftCategory.LIGHTER_THAN_AIR; + case 3: return AircraftCategory.SKYDIVER; + case 4: return AircraftCategory.ULTRALIGHT; + case 5: return AircraftCategory.RESERVED; + case 6: return AircraftCategory.UNMANNED_AERIAL_VEHICLE; + case 7: return AircraftCategory.SPACE; + default: return AircraftCategory.UNKNOWN; + } + case 2: + switch (aircraftEmitterCategory) { + case 0: return AircraftCategory.NO_ADS_B_EMITTER; + case 1: return AircraftCategory.SURFACE_VEHICLE_EMERGENCY; + case 2: return AircraftCategory.SURFACE_VEHICLE_SERVICE; + case 3: return AircraftCategory.POINT_OBSTACLE; + case 4: return AircraftCategory.CLUSTER_OBSTACLE; + case 5: return AircraftCategory.LINE_OBSTACLE; + case 6: return AircraftCategory.RESERVED; + case 7: return AircraftCategory.RESERVED; + default: return AircraftCategory.UNKNOWN; + } + default: + return AircraftCategory.UNKNOWN; + } } public String getAcid() { diff --git a/src/main/java/aero/t2s/modes/decoder/df/df17/AircraftOperationalStatusVersion2Surface.java b/src/main/java/aero/t2s/modes/decoder/df/df17/AircraftOperationalStatusVersion2Surface.java index fe2b30a..4b25011 100644 --- a/src/main/java/aero/t2s/modes/decoder/df/df17/AircraftOperationalStatusVersion2Surface.java +++ b/src/main/java/aero/t2s/modes/decoder/df/df17/AircraftOperationalStatusVersion2Surface.java @@ -13,7 +13,7 @@ public class AircraftOperationalStatusVersion2Surface extends AircraftOperationa private Angle horizontalSource; private SourceIntegrityLevelSupplement SILsupp; private SourceIntegrityLevel SIL; - private Version verison; + private Version version; public AircraftOperationalStatusVersion2Surface(short[] data) { super(data); @@ -21,23 +21,27 @@ public AircraftOperationalStatusVersion2Surface(short[] data) { @Override public AircraftOperationalStatusVersion2Surface decode() { - verison = Version.VERSION2; - surfaceCapability = new SurfaceCapability((data[5] << 4) | (data[6] & 0b11110000) >>> 4, verison); + version = Version.VERSION2; + surfaceCapability = new SurfaceCapability((data[5] << 4) | (data[6] & 0b11110000) >>> 4, version); lengthWidthCode = LengthWidthCode.from(data[6] & 0b00001111); operationalMode = new SurfaceOperationalMode((data[7] << 8) | data[8]); - SIL = SourceIntegrityLevel.from((data[10] & 0b000110000) >>> 4); - SILsupp = SourceIntegrityLevelSupplement.from((data[10] & 0b00000010) >>> 1); - int NICsuppA = (data[9] & 0b00010000) >>> 4; int NACp = (data[9] & 0b00001111); - NICp = NavigationIntegrityCategory.surface(NACp, NICsuppA); + int GVA = (data[10] & 0b11000000) >>> 6; + int SIL = (data[10] & 0b00110000) >>> 4; + + int SILsupp = (data[10] & 0b00000010) >>> 1; + + this.SIL = SourceIntegrityLevel.from(SIL); + this.SILsupp = SourceIntegrityLevelSupplement.from(SILsupp); + this.NICp = NavigationIntegrityCategory.surface(NACp, NICsuppA); if ((data[10] & 0b00001000) != 0) { if ((data[10] & 0b00000100) != 0) { - horizontalSource = Angle.TRUE_TRACK; - } else { horizontalSource = Angle.MAGNETIC_TRACK; + } else { + horizontalSource = Angle.TRUE_TRACK; } } else { if ((data[10] & 0b00000100) != 0) { @@ -53,6 +57,7 @@ public AircraftOperationalStatusVersion2Surface decode() { @Override public void apply(Track track) { track.setVersion(Version.VERSION2); + track.setHorizontalSource(horizontalSource); } public SurfaceCapability getSurfaceCapability() { @@ -83,7 +88,7 @@ public SourceIntegrityLevel getSIL() { return SIL; } - public Version getVerison() { - return verison; + public Version getVersion() { + return version; } } diff --git a/src/main/java/aero/t2s/modes/decoder/df/df17/PositionUpdate.java b/src/main/java/aero/t2s/modes/decoder/df/df17/PositionUpdate.java new file mode 100644 index 0000000..a744711 --- /dev/null +++ b/src/main/java/aero/t2s/modes/decoder/df/df17/PositionUpdate.java @@ -0,0 +1,283 @@ +package aero.t2s.modes.decoder.df.df17; + +import aero.t2s.modes.CprPosition; +import org.slf4j.LoggerFactory; + +import java.util.*; + +public class PositionUpdate { + private static double originLat; // Origin is passed-in as a command-line argument as an indication of where the receiver is located + private static double originLon; + + private static final double updateThreshold = 0.5d; // Positions which move between updates by more than this (in degrees) should be rejected + private static final double receiverRange = 4.0d; // Positions further away from the receiver than this (in degrees) should be rejected + private static double receiverLat; // Receiver position is calculated based on data received + private static double receiverLon; + private static double receiverSumLat; // Running-total of valid positions that can be used to calculate the average receiver position + private static double receiverSumLon; + private static double receiverSumCount = 0; // number of valid positions received that are used to calculate the average + + private static Map cache = new HashMap<>(); + private static Timer cacheCleanup; + + private static final double dLatEven = 4.0 * 15.0; + private static final double dLatOdd = 4.0 * 15.0 - 1.0; + private static final double nlNumerator = 1 - Math.cos(Math.PI / (2.0 * 15.0)); + private static final double nlPi180 = Math.PI / 180.0; + + private CprPosition even; + private CprPosition odd; + private CprPosition previous; + private CprPosition current; + + static public CprPosition calculate(String address, boolean isCprEven, CprPosition newCpr) { + if (!cache.containsKey(address)) { + synchronized (cache) { + cache.putIfAbsent(address, new PositionUpdate(isCprEven, newCpr)); + } + return null; + } + + PositionUpdate positionUpdate; + synchronized (cache) { + positionUpdate = cache.get(address); + } + return positionUpdate.calculate(isCprEven, newCpr); + } + + private CprPosition calculate(boolean isCprEven, CprPosition newCpr) { + if (isCprEven) { + even = newCpr; + // If the even/odd frames were too many seconds apart, of if their surface bits do not match, discard the previously received frame + if ((odd != null) && (odd.isExpired() || (odd.getSurface() != even.getSurface()))) { + odd = null; + previous = null; + } + } else { + odd = newCpr; + if ((even != null) && (even.isExpired() || (even.getSurface() != odd.getSurface()))) { + even = null; + previous = null; + } + } + + if (isComplete() && previous == null) { + // We've now got a pair of odd/even frames but there was no previous position... so we must use Global calculation to get accurate position + calculateGlobal(); + // Update the receiver position only from Global updates (likely to be the first time updating from this aircraft) + PositionUpdate.receiverUpdate(current.getLat(), current.getLon()); + } else if (previous != null) { + // We had a previous accurate position, so we can use the most recent frame to do Local calculation + calculateLocal(!isCprEven); + } else { + current = null; + } + + if (current != null && current.isValid()) { + // Sanity Check: Is the position just received too far away from the receiver position? + if ((Math.abs(current.getLat() - receiverLat) > receiverRange) || (Math.abs(current.getLon() - receiverLon) > receiverRange)) { + LoggerFactory.getLogger(getClass()).info("Position Update discarded due to outside receiver range."); + current = null; + } + else { + previous = current; + } + } + + return current; + } + + private void calculateLocal(boolean isOdd) { + CprPosition cpr = isOdd ? odd : even; + CprPosition otherCpr = isOdd ? even : odd; + double degrees = cpr.getSurface() ? 90.0 : 360.0; + double dLat = degrees / (isOdd ? dLatOdd : dLatEven); + double j = Math.floor(previous.getLat() / dLat) + Math.floor(cprMod(previous.getLat(), dLat) / dLat - cpr.getLat() + 0.5); + double newLat = dLat * (j + cpr.getLat()); + if (newLat < 0) { + // Something screwed-up + current = null; + return; + } + + double nl = NL(newLat) - (isOdd ? 1.0 : 0.0); + double dLon = (nl > 0) ? degrees / nl : degrees; + double m = Math.floor(previous.getLon() / dLon) + Math.floor(cprMod(previous.getLon(), dLon) / dLon - cpr.getLon() + 0.5); + double newLon = dLon * (m + cpr.getLon()); + + cpr.setZones(j, m); + + current = new CprPosition(newLat, newLon, cpr.getSurface()); + if (current.getSurface()) { + validateSurface(current); + } + + if ((Math.abs(current.getLat() - previous.getLat()) > updateThreshold) || (Math.abs(current.getLon() - previous.getLon()) > updateThreshold)) { + LoggerFactory.getLogger(getClass()).info("Position Update discarded due to unreasonable distance between updates."); + current = null; + } + } + + private void calculateGlobal() { + double j = Math.floor(dLatOdd * even.getLat() - dLatEven * odd.getLat() + 0.5); + double degrees = even.getSurface() ? 90.0 : 360.0; // Doesn't matter whether we check odd or even as they must both match by now + + double latEven = (degrees / dLatEven) * (cprMod(j, dLatEven) + even.getLat()); + double latOdd = (degrees / dLatOdd) * (cprMod(j, dLatOdd) + odd.getLat()); + + latEven = normaliseLat(latEven); + latOdd = normaliseLat(latOdd); + + double nlLatEven = NL(latEven); + double nlLatOdd = NL(latOdd); + + if (nlLatEven != nlLatOdd) { + // The even and odd frames are in different Longitude zones, so we need to discard BOTH + odd = null; + even = null; + previous = null; + current = null; + return; + } + + double newLat, newLon, ni, m; + if (even.getTime() > odd.getTime()) { + ni = cprN(nlLatEven, 0); + m = Math.floor(even.getLon() * (nlLatEven - 1) - odd.getLon() * nlLatEven + 0.5); + + newLat = latEven; + newLon = (degrees / ni) * (cprMod(m, ni) + even.getLon()); + } else { + ni = cprN(nlLatOdd, 1); + m = Math.floor(even.getLon() * (nlLatOdd - 1) - odd.getLon() * nlLatOdd + 0.5); + + newLat = latOdd; + newLon = (degrees / ni) * (cprMod(m, ni) + odd.getLon()); + } + newLon = normaliseLon(newLon); + + even.setZones(nlLatEven, m); + odd.setZones(nlLatOdd, m); + + current = new CprPosition(newLat, newLon, even.getSurface()); + if (current.getSurface()) { + validateSurface(current); + } + } + + static private double cprMod(double a, double b) { + return a - b * Math.floor(a/b); + } + + static private double cprN(double nlLat, double isOdd) { + return Math.max(1, nlLat - isOdd); + } + + static private double NL(double lat) { + if (lat == 0) return 59; + else if (Math.abs(lat) == 87) return 2; + else if (Math.abs(lat) > 87) return 1; + + double tmpCos = Math.cos(nlPi180 * Math.abs(lat)); + double tmpArc = 1 - nlNumerator / (tmpCos * tmpCos); + return Math.floor(2 * Math.PI / Math.acos(tmpArc)); + } + + public PositionUpdate(boolean isEven, CprPosition cpr) { + if (isEven) { + this.even = cpr; + } else { + this.odd = cpr; + } + } + + public boolean isComplete() { + return even != null && odd != null; + } + + public boolean isExpired() { + return even.isExpired() || odd.isExpired(); + } + + static public double normaliseLat(double lat) { + if (lat >= 270.0 && lat <= 360.0) { + lat -= 360.0; + } + return lat; + } + + static public double normaliseLon(double lon) { + if (lon > 180.0) { + lon -= 360.0; + } + return lon; + } + + private void validateSurface(CprPosition pos) { + // For SurfacePositions we get 8 possible solutions: 2x latitude, 4x longitude zones at 90 degree increments + double diff; + double test; + double testDiff; + + // Pick the lat which is closest to the receiver + test = pos.getLat(); + diff = Math.abs(normaliseLat(test) - receiverLat); + for (int i = 0 ; i < 1 ; i++){ + test = normaliseLat(test + 90.0); + testDiff = Math.abs(test - receiverLat); + if (testDiff < diff) { + diff = testDiff; + pos.setLat(test); + } + } + + // Pick the lon which is closest to the receiver + test = pos.getLon(); + diff = Math.abs(normaliseLon(test) - receiverLon); + for (int i = 0 ; i < 3 ; i++){ + test = normaliseLon(test + 90.0); + testDiff = Math.abs(test - receiverLon); + if (testDiff < diff) { + diff = testDiff; + pos.setLon(test); + } + } + } + + static private void receiverUpdate(double lat, double lon) { + if (!((lat == 0) && (lon == 0))) { + receiverSumCount ++; + receiverSumLat += lat; + receiverSumLon += lon; + receiverLat = receiverSumLat / receiverSumCount; + receiverLon = receiverSumLon / receiverSumCount; + } + } + + static public void start(double originLat, double originLon) { + PositionUpdate.originLat = originLat; + PositionUpdate.originLon = originLon; + + receiverUpdate(originLat, originLon); + + cache.clear(); + cacheCleanup.schedule(new TimerTask() { + @Override + public void run() { + List expired = new LinkedList<>(); + + synchronized (cache) { + cache.entrySet().stream().filter(entry -> entry.getValue().isExpired()).forEach(entry -> expired.add(entry.getKey())); + expired.forEach(cache::remove); + } + } + }, 0, 10_000); + } + + static public void stop() { + cacheCleanup.cancel(); + cacheCleanup = null; + + cache.clear(); + } +} diff --git a/src/main/java/aero/t2s/modes/decoder/df/df17/SurfacePosition.java b/src/main/java/aero/t2s/modes/decoder/df/df17/SurfacePosition.java index df4097b..04e5d93 100644 --- a/src/main/java/aero/t2s/modes/decoder/df/df17/SurfacePosition.java +++ b/src/main/java/aero/t2s/modes/decoder/df/df17/SurfacePosition.java @@ -1,20 +1,155 @@ package aero.t2s.modes.decoder.df.df17; -import aero.t2s.modes.NotImplementedException; +import aero.t2s.modes.CprPosition; import aero.t2s.modes.Track; +import aero.t2s.modes.constants.Angle; public class SurfacePosition extends ExtendedSquitter { - public SurfacePosition(short[] data) { + private final String address; + private int velocityEncoded = 0; + private double velocity = 0; + + private boolean trackAvailable = false; + private int trackEncoded = 0; + private double track = 0; + + private double lat; + private double lon; + private boolean positionAvailable = false; + private boolean velocityAvailable = false; + + public SurfacePosition(short[] data, String address) { super(data); + this.address = address; } @Override public SurfacePosition decode() { - throw new NotImplementedException(getClass().getSimpleName() + " Not implemented"); + velocityEncoded = ((data[4] & 0x7) << 4) | (data[5] >>> 4); + velocityAvailable = velocityEncoded != 0; + velocity = decodeEncodedVelocity(); + + trackAvailable = ((data[5] >>> 3) & 0x1) == 0x1; + trackEncoded = ((data[5] & 0x7) << 4) | (data[6] >>> 4); + track = trackEncoded * 360.0 / 128.0; + + // Decode position + int time = (data[6] >>> 3) & 0x1; + boolean isCprEven = ((data[6] >>> 2) & 0x1) == 0; + + int cprLat = (data[6] & 0x3) << 15; + cprLat = cprLat | (data[7] << 7); + cprLat = cprLat | (data[8] >>> 1); + + int cprLon = ((data[8] & 0x1) << 16); + cprLon = cprLon | (data[9] << 8); + cprLon = cprLon | data[10]; + + CprPosition newPosition = PositionUpdate.calculate(address, isCprEven, new CprPosition(cprLat, cprLon, true)); + if (newPosition != null) { + this.lat = newPosition.getLat(); + this.lon = newPosition.getLon(); + this.positionAvailable = true; + } else { + this.positionAvailable = false; + } + + return this; } @Override public void apply(Track track) { - throw new NotImplementedException(getClass().getSimpleName() + " Not implemented"); + track.setGroundBit(true); + + if (positionAvailable) { + track.setLatLon(lat, lon); + } + + if (velocityAvailable) { + track.setGs(velocity); + } + + if (trackAvailable) { + if (track.getHorizontalSource() == Angle.TRUE_TRACK) { + track.setTrueHeading(this.track); + } else if (track.getHorizontalSource() == Angle.MAGNETIC_HEADING) { + track.setMagneticHeading(this.track); + } else { + track.setTrueHeading(this.track); + track.setMagneticHeading(this.track); + } + } + } + + public boolean isPositionAvailable() { + return positionAvailable; + } + + public boolean isVelocityAvailable() { + return this.velocityAvailable; + } + + public boolean isTrackAvailable() { + return trackAvailable; + } + + public double getLat() { + return lat; + } + + public double getLon() { + return lon; + } + + public String getAddress() { + return address; + } + + public int getVelocityEncoded() { + return velocityEncoded; + } + + public double getVelocity() { + return this.velocity; + } + + public int getTrackEncoded() { + return trackEncoded; + } + + public double getTrack() { + return track; + } + + private double decodeEncodedVelocity() { + if (this.velocityEncoded == 1) { + return 0; + } + if (this.velocityEncoded <= 8) { + return (this.velocityEncoded - 1) * 0.125 + 0; + } + if (this.velocityEncoded <= 12) { + return (this.velocityEncoded - 9) * 0.25 + 1.0; + } + if (this.velocityEncoded <= 38) { + return (this.velocityEncoded - 13) * 0.5 + 2.0; + } + if (this.velocityEncoded <= 93) { + return (this.velocityEncoded - 39) * 1.0 + 15.0; + } + if (this.velocityEncoded <= 93) { + return (this.velocityEncoded - 39) * 1.0 + 15.0; + } + if (this.velocityEncoded <= 108) { + return (this.velocityEncoded - 94) * 2.0 + 70.0; + } + if (this.velocityEncoded <= 123) { + return (this.velocityEncoded - 109) * 5.0 + 100.0; + } + if (this.velocityEncoded == 124) { + return 175.0; + } + + return 0; // Could be either "not available" or "reserved"... just assume zero speed for now } } diff --git a/src/main/java/aero/t2s/modes/decoder/df/df17/TargetStatusMessageType0.java b/src/main/java/aero/t2s/modes/decoder/df/df17/TargetStatusMessageType0.java index 03f1410..85989a9 100644 --- a/src/main/java/aero/t2s/modes/decoder/df/df17/TargetStatusMessageType0.java +++ b/src/main/java/aero/t2s/modes/decoder/df/df17/TargetStatusMessageType0.java @@ -1,6 +1,5 @@ package aero.t2s.modes.decoder.df.df17; -import aero.t2s.modes.NotImplementedException; import aero.t2s.modes.Track; import aero.t2s.modes.constants.*; @@ -61,8 +60,6 @@ public TargetStatusMessageType0 decode() { @Override public void apply(Track track) { - track.setNACp(NACp); - if (horizontalDataAvailable != HorizontalDataAvailable.NOT_VALID) track.setSelectedHeading(targetHeadingTrack); diff --git a/src/main/java/aero/t2s/modes/decoder/df/df17/TargetStatusMessageType1.java b/src/main/java/aero/t2s/modes/decoder/df/df17/TargetStatusMessageType1.java index 7a3adb5..80c4a5f 100644 --- a/src/main/java/aero/t2s/modes/decoder/df/df17/TargetStatusMessageType1.java +++ b/src/main/java/aero/t2s/modes/decoder/df/df17/TargetStatusMessageType1.java @@ -81,16 +81,10 @@ public void apply(Track track) { track.setSelectedAltitudeManagedMcp(selectedAltitudeType == SelectedAltitudeSource.MCP); track.setSelectedAltitude(selectedAltitudeType != SelectedAltitudeSource.UNKNOWN ? selectedAltitude : 0); - if (isBaroAvailable()) { - track.setBaroAltitude((int) Math.round(baroSetting)); - } - if (selectedHeadingAvailable) { track.setSelectedHeading(selectedHeading); } - track.setNACp(NACp); - track.setNICb(NICbaro.ordinal()); track.setSil(SIL.ordinal()); track.setAutopilot(autopilot); track.setVnav(autopilotVnav); diff --git a/src/main/java/aero/t2s/modes/decoder/df/df17/data/SurfaceOperationalMode.java b/src/main/java/aero/t2s/modes/decoder/df/df17/data/SurfaceOperationalMode.java index b775c51..8508fe0 100644 --- a/src/main/java/aero/t2s/modes/decoder/df/df17/data/SurfaceOperationalMode.java +++ b/src/main/java/aero/t2s/modes/decoder/df/df17/data/SurfaceOperationalMode.java @@ -27,7 +27,7 @@ public SurfaceOperationalMode(int data) { acasRA = (data & 0b0010000000000000) != 0 ? AcasState.RA_ACTIVE : AcasState.RA_NOT_ACTIVE; acasIdent = (data & 0b0001000000000000) != 0; singleAntennaFlag = (data & 0b0000010000000000) != 0; - systemDesignAssurance = SourceIntegrityLevel.from((data & 0b0000001100000000) >>> 7); + systemDesignAssurance = SourceIntegrityLevel.from((data & 0b0000001100000000) >>> 8); int gpsAntennaOffset = (data & 0b0000000011111111); gpsLateralOffset = (gpsAntennaOffset & 0b11100000) >>> 5; diff --git a/src/main/java/aero/t2s/modes/examples/StdOutExample.java b/src/main/java/aero/t2s/modes/examples/StdOutExample.java deleted file mode 100644 index 113034b..0000000 --- a/src/main/java/aero/t2s/modes/examples/StdOutExample.java +++ /dev/null @@ -1,19 +0,0 @@ -package aero.t2s.modes.examples; - -import aero.t2s.modes.ModeS; - -public class StdOutExample { - public static void main(String[] args) { - ModeS modes = new ModeS( - "127.0.0.1", // Host IP where the Dump1090 server is running - 30002, // The port with raw output (default 30002) - 51, // Decimal latitude - 0 // Decimal longitude - ); - modes.onTrackCreated(track -> System.out.println("CREATED " + track.toString())); - modes.onTrackUpdated(track -> System.out.println("UPDATED " + track.toString())); - modes.onTrackDeleted(track -> System.out.println("DELETED " + track.toString())); - - modes.start(); - } -} diff --git a/src/main/java/aero/t2s/modes/registers/Register.java b/src/main/java/aero/t2s/modes/registers/Register.java new file mode 100644 index 0000000..0007fb5 --- /dev/null +++ b/src/main/java/aero/t2s/modes/registers/Register.java @@ -0,0 +1,15 @@ +package aero.t2s.modes.registers; + +abstract public class Register { + private boolean valid = false; + + public void validate() { + valid = true; + } + + public boolean isValid() { + return valid; + } + + public abstract String toString(); +} diff --git a/src/main/java/aero/t2s/modes/registers/Register05.java b/src/main/java/aero/t2s/modes/registers/Register05.java new file mode 100644 index 0000000..b9ebaff --- /dev/null +++ b/src/main/java/aero/t2s/modes/registers/Register05.java @@ -0,0 +1,103 @@ +package aero.t2s.modes.registers; + +import aero.t2s.modes.constants.AltitudeSource; +import aero.t2s.modes.constants.HorizontalProtectionLimit; +import aero.t2s.modes.constants.SurveillanceStatus; +import aero.t2s.modes.constants.Version; + +import java.time.Instant; + +public abstract class Register05 extends Register { + private SurveillanceStatus surveillanceStatus = SurveillanceStatus.NO_CONDITION; + private int altitude = 0; + private AltitudeSource altitudeSource = AltitudeSource.BARO; + private double lat = 0; + private double lon = 0; + private HorizontalProtectionLimit hpl = HorizontalProtectionLimit.RC_UNKNOWN; + private Instant updated = Instant.MIN; + + public SurveillanceStatus getSurveillanceStatus() { + return surveillanceStatus; + } + + public Register05 setSurveillanceStatus(SurveillanceStatus surveillanceStatus) { + this.surveillanceStatus = surveillanceStatus; + return this; + } + + public int getAltitude() { + return altitude; + } + + public Register05 setAltitude(int altitude) { + this.altitude = altitude; + return this; + } + + public AltitudeSource getAltitudeSource() { + return altitudeSource; + } + + public Register05 setAltitudeSource(AltitudeSource altitudeSource) { + this.altitudeSource = altitudeSource; + return this; + } + + public double getLat() { + return lat; + } + + public Register05 setLat(double lat) { + this.lat = lat; + return this; + } + + public double getLon() { + return lon; + } + + public Register05 setLon(double lon) { + this.lon = lon; + return this; + } + + public HorizontalProtectionLimit getHpl() { + return hpl; + } + + public Register05 setHpl(HorizontalProtectionLimit hpl) { + this.hpl = hpl; + return this; + } + + public Instant getUpdated() { + return updated; + } + + public abstract Version getVersion(); + + + public void update(HorizontalProtectionLimit hpl, int altitude, AltitudeSource source, double lat, double lon, SurveillanceStatus surveillanceStatus) { + this.hpl = hpl; + this.altitude = altitude; + this.lat = lat; + this.lon = lon; + this.surveillanceStatus = surveillanceStatus; + this.updated = Instant.now(); + this.validate(); + } + + @Override + public String toString() { + return "Register05{\n" + + "valid=" + isValid() + + ",\n surveillanceStatus=" + surveillanceStatus + + ",\n altitude=" + altitude + + ",\n altitudeSource=" + altitudeSource.name() + + ",\n lat=" + lat + + ",\n lon=" + lon + + ",\n hpl=" + hpl + + ",\n updated=" + updated + + "\n}"; + } +} diff --git a/src/main/java/aero/t2s/modes/registers/Register05V0.java b/src/main/java/aero/t2s/modes/registers/Register05V0.java new file mode 100644 index 0000000..0f63bd2 --- /dev/null +++ b/src/main/java/aero/t2s/modes/registers/Register05V0.java @@ -0,0 +1,36 @@ +package aero.t2s.modes.registers; + +import aero.t2s.modes.constants.AltitudeSource; +import aero.t2s.modes.constants.HorizontalProtectionLimit; +import aero.t2s.modes.constants.SurveillanceStatus; +import aero.t2s.modes.constants.Version; + +public class Register05V0 extends Register05 { + private boolean singleAntennaFlag = false; + + public boolean isSingleAntennaFlag() { + return singleAntennaFlag; + } + + public Register05V0 setSingleAntennaFlag(boolean singleAntennaFlag) { + this.singleAntennaFlag = singleAntennaFlag; + return this; + } + + @Override + public Version getVersion() { + return Version.VERSION0; + } + + public void update(HorizontalProtectionLimit hpl, int altitude, AltitudeSource altitudeSource, double lat, double lon, SurveillanceStatus surveillanceStatus, boolean singleAntennaFlag) { + super.update(hpl, altitude, altitudeSource, lat, lon, surveillanceStatus); + this.singleAntennaFlag = singleAntennaFlag; + } + + @Override + public String toString() { + return super.toString() + "\n" + "Register05V0{" + + "\nsingleAntennaFlag=" + singleAntennaFlag + + "\n}"; + } +} diff --git a/src/main/java/aero/t2s/modes/registers/Register05V2.java b/src/main/java/aero/t2s/modes/registers/Register05V2.java new file mode 100644 index 0000000..60bcfde --- /dev/null +++ b/src/main/java/aero/t2s/modes/registers/Register05V2.java @@ -0,0 +1,23 @@ +package aero.t2s.modes.registers; + +import aero.t2s.modes.constants.AltitudeSource; +import aero.t2s.modes.constants.HorizontalProtectionLimit; +import aero.t2s.modes.constants.SurveillanceStatus; +import aero.t2s.modes.constants.Version; + +public class Register05V2 extends Register05 { + @Override + public Version getVersion() { + return Version.VERSION2; + } + + @Override + public void update(HorizontalProtectionLimit hpl, int altitude, AltitudeSource altitudeSource, double lat, double lon, SurveillanceStatus surveillanceStatus) { + super.update(hpl, altitude, altitudeSource, lat, lon, surveillanceStatus); + } + + @Override + public String toString() { + return super.toString() + "\n" + "Register05V2 {}"; + } +} diff --git a/src/main/java/aero/t2s/modes/registers/Register06.java b/src/main/java/aero/t2s/modes/registers/Register06.java new file mode 100644 index 0000000..2823235 --- /dev/null +++ b/src/main/java/aero/t2s/modes/registers/Register06.java @@ -0,0 +1,19 @@ +package aero.t2s.modes.registers; + +public class Register06 extends Register { + private double groundSpeed; + private double groundTrack; + private double lat; + private double lon; + + @Override + public String toString() { + return "Register06{\n" + + "valid=" + isValid() + + ",\n groundSpeed=" + groundSpeed + + ",\n groundTrack=" + groundTrack + + ",\n lat=" + lat + + ",\n lon=" + lon + + "\n}"; + } +} diff --git a/src/main/java/aero/t2s/modes/registers/Register07.java b/src/main/java/aero/t2s/modes/registers/Register07.java new file mode 100644 index 0000000..e378d7a --- /dev/null +++ b/src/main/java/aero/t2s/modes/registers/Register07.java @@ -0,0 +1,42 @@ +package aero.t2s.modes.registers; + +import aero.t2s.modes.constants.AltitudeSource; +import aero.t2s.modes.constants.TransmissionRate; + +public class Register07 extends Register { + private TransmissionRate transmissionRate = TransmissionRate.UNKNOWN; + private AltitudeSource altitudeType = AltitudeSource.BARO; + + public TransmissionRate getTransmissionRate() { + return transmissionRate; + } + + public Register07 setTransmissionRate(TransmissionRate transmissionRate) { + this.transmissionRate = transmissionRate; + return this; + } + + public AltitudeSource getAltitudeType() { + return altitudeType; + } + + public Register07 setAltitudeType(AltitudeSource altitudeType) { + this.altitudeType = altitudeType; + return this; + } + + public void update(TransmissionRate trs, AltitudeSource altitudeType) { + setTransmissionRate(trs); + setAltitudeType(altitudeType); + validate(); + } + + @Override + public String toString() { + return "Register07{\n" + + "valid=" + isValid() + + ",\ntransmissionRate=" + transmissionRate + + ",\n altitudeType=" + altitudeType + + "\n}"; + } +} diff --git a/src/main/java/aero/t2s/modes/registers/Register08.java b/src/main/java/aero/t2s/modes/registers/Register08.java new file mode 100644 index 0000000..8549868 --- /dev/null +++ b/src/main/java/aero/t2s/modes/registers/Register08.java @@ -0,0 +1,41 @@ +package aero.t2s.modes.registers; + +import aero.t2s.modes.constants.AircraftCategory; + +public class Register08 extends Register { + private String acid; + private AircraftCategory aircraftCategory = AircraftCategory.UNKNOWN; + + public String getAcid() { + return acid; + } + + public Register08 setAcid(String acid) { + this.acid = acid; + return this; + } + + public AircraftCategory getAircraftCategory() { + return aircraftCategory; + } + + public Register08 setAircraftCategory(AircraftCategory aircraftCategory) { + this.aircraftCategory = aircraftCategory; + return this; + } + + public void update(String acid, AircraftCategory aircraftCategory) { + setAcid(acid); + setAircraftCategory(aircraftCategory); + validate(); + } + + @Override + public String toString() { + return "Register08{\n" + + "valid=" + isValid() + + "\nacid='" + acid + '\'' + + ",\n aircraftCategory=" + aircraftCategory + + "\n}"; + } +} diff --git a/src/main/java/aero/t2s/modes/registers/Register09.java b/src/main/java/aero/t2s/modes/registers/Register09.java new file mode 100644 index 0000000..6bf83d6 --- /dev/null +++ b/src/main/java/aero/t2s/modes/registers/Register09.java @@ -0,0 +1,142 @@ +package aero.t2s.modes.registers; + +import aero.t2s.modes.constants.Angle; +import aero.t2s.modes.constants.NavigationAccuracyCategoryVelocity; +import aero.t2s.modes.constants.RocdSource; +import aero.t2s.modes.constants.Speed; + +public class Register09 extends Register { + private NavigationAccuracyCategoryVelocity NACv; + + /** + * An intent change event shall be triggered 4 seconds after the detection of new information being inserted in registers 4016 to 4216. + * The code shall remain set for 18 +/-1 second following an intent change. + */ + private boolean intentChangeFlag; + private int heading = 0; + private Angle headingSource = Angle.UNAVAILABLE; + private int airspeed = 0; + private Speed airspeedSource = Speed.IAS; + private int vx = 0; + private int vy = 0; + private int gnssDifferenceFromBaro = 0; + private int verticalRate; + private RocdSource verticalRateSource = RocdSource.GNSS; + + public NavigationAccuracyCategoryVelocity getNACv() { + return NACv; + } + + public Register09 setNACv(NavigationAccuracyCategoryVelocity NACv) { + this.NACv = NACv; + return this; + } + + public boolean isIntentChangeFlag() { + return intentChangeFlag; + } + + public Register09 setIntentChangeFlag(boolean intentChangeFlag) { + this.intentChangeFlag = intentChangeFlag; + return this; + } + + public int getHeading() { + return heading; + } + + public Register09 setHeading(int heading) { + this.heading = heading; + return this; + } + + public Angle getHeadingSource() { + return headingSource; + } + + public Register09 setHeadingSource(Angle headingSource) { + this.headingSource = headingSource; + return this; + } + + public int getAirspeed() { + return airspeed; + } + + public Register09 setAirspeed(int airspeed) { + this.airspeed = airspeed; + return this; + } + + public Speed getAirspeedSource() { + return airspeedSource; + } + + public Register09 setAirspeedSource(Speed airspeedSource) { + this.airspeedSource = airspeedSource; + return this; + } + + public int getVx() { + return vx; + } + + public Register09 setVx(int vx) { + this.vx = vx; + return this; + } + + public int getVy() { + return vy; + } + + public Register09 setVy(int vy) { + this.vy = vy; + return this; + } + + public int getGnssDifferenceFromBaro() { + return gnssDifferenceFromBaro; + } + + public Register09 setGnssDifferenceFromBaro(int gnssDifferenceFromBaro) { + this.gnssDifferenceFromBaro = gnssDifferenceFromBaro; + return this; + } + + public int getVerticalRate() { + return verticalRate; + } + + public Register09 setVerticalRate(int verticalRate) { + this.verticalRate = verticalRate; + return this; + } + + public RocdSource getVerticalRateSource() { + return verticalRateSource; + } + + public Register09 setVerticalRateSource(RocdSource verticalRateSource) { + this.verticalRateSource = verticalRateSource; + return this; + } + + @Override + public String toString() { + return "Register09{\n" + + "valid=" + isValid() + + ",\n NACv=" + NACv + + ",\n intentChangeFlag=" + intentChangeFlag + + ",\n heading=" + heading + + ",\n headingSource=" + headingSource + + ",\n airspeed=" + airspeed + + ",\n airspeedSource=" + airspeedSource + + ",\n vx=" + vx + + ",\n vy=" + vy + + ",\n gnssDifferenceFromBaro=" + gnssDifferenceFromBaro + + ",\n verticalRate=" + verticalRate + + ",\n verticalRateSource=" + verticalRateSource + + "\n}"; + } +} diff --git a/src/main/java/aero/t2s/modes/registers/Register09V0.java b/src/main/java/aero/t2s/modes/registers/Register09V0.java new file mode 100644 index 0000000..d11552f --- /dev/null +++ b/src/main/java/aero/t2s/modes/registers/Register09V0.java @@ -0,0 +1,26 @@ +package aero.t2s.modes.registers; + +public class Register09V0 extends Register09 { + /** + * The IFR capability flag shall be a 1-bit (bit 10) subfield in the subtypes 1, 2, 3 and 4 airborne velocity messages. + * IFR=1 shall signify that the transmitting aircraft has a capability for applications requiring ADS-B equipage class A1 or above. + * Otherwise, IFR shall be set to 0. + */ + private boolean ifrCapability; + + public boolean isIfrCapability() { + return ifrCapability; + } + + public Register09V0 setIfrCapability(boolean ifrCapability) { + this.ifrCapability = ifrCapability; + return this; + } + + @Override + public String toString() { + return super.toString() + "\nRegister09V0{\n" + + "ifrCapability=" + ifrCapability + + "\n}"; + } +} diff --git a/src/main/java/aero/t2s/modes/registers/Register17.java b/src/main/java/aero/t2s/modes/registers/Register17.java new file mode 100644 index 0000000..fc74e4e --- /dev/null +++ b/src/main/java/aero/t2s/modes/registers/Register17.java @@ -0,0 +1,26 @@ +package aero.t2s.modes.registers; + +import aero.t2s.modes.CapabilityReport; +import aero.t2s.modes.decoder.df.bds.Bds17; + +public class Register17 extends Register { + private CapabilityReport capabilityReport = new CapabilityReport(); + + public CapabilityReport getCapabilityReport() { + return capabilityReport; + } + + public Register17 update(Bds17 bds) { + capabilityReport.update(bds); + this.validate(); + return this; + } + + @Override + public String toString() { + return "Register17{\n" + + "valid=" + isValid() + + "\n" + capabilityReport.toString() + + "\n}"; + } +} diff --git a/src/main/java/aero/t2s/modes/registers/Register20.java b/src/main/java/aero/t2s/modes/registers/Register20.java new file mode 100644 index 0000000..93b46bb --- /dev/null +++ b/src/main/java/aero/t2s/modes/registers/Register20.java @@ -0,0 +1,23 @@ +package aero.t2s.modes.registers; + +public class Register20 extends Register { + private String acid; + + public String getAcid() { + return acid; + } + + public Register20 setAcid(String acid) { + this.acid = acid; + validate(); + return this; + } + + @Override + public String toString() { + return "Register20{\n" + + "valid=" + isValid() + + "\nacid='" + acid + '\'' + + "\n}"; + } +} diff --git a/src/main/java/aero/t2s/modes/registers/Register21.java b/src/main/java/aero/t2s/modes/registers/Register21.java new file mode 100644 index 0000000..00f1322 --- /dev/null +++ b/src/main/java/aero/t2s/modes/registers/Register21.java @@ -0,0 +1,29 @@ +package aero.t2s.modes.registers; + +public class Register21 extends Register { + private String aircraft; + private String airline; + + public String getAircraft() { + return aircraft; + } + + public String getAirline() { + return airline; + } + + public void update(String aircraft, String airline) { + this.aircraft = airline; + this.airline = airline; + validate(); + } + + @Override + public String toString() { + return "Register21{\n" + + "valid=" + isValid() + + ",\n aircraft='" + aircraft + '\'' + + ",\n airline='" + airline + '\'' + + "\n}"; + } +} diff --git a/src/test/java/aero/t2s/modes/decoder/df/DF0Test.java b/src/test/java/aero/t2s/modes/decoder/df/DF0Test.java index 3bfbba2..c27b083 100644 --- a/src/test/java/aero/t2s/modes/decoder/df/DF0Test.java +++ b/src/test/java/aero/t2s/modes/decoder/df/DF0Test.java @@ -43,7 +43,7 @@ void it_decodes_sensitivity_level() void it_decodes_reply_information() { df0 = new DF0(BinaryHelper.stringToByteArray("02E194979F2C4B")).decode(); - assertEquals(AcasReplyInformation.RESERVED3, df0.getReplyInformation()); + assertEquals(AcasReplyInformation.ACAS_RA_VERTICAL_ONLY, df0.getReplyInformation()); } @Test @@ -51,7 +51,7 @@ void it_decodes_altitude() { df0 = new DF0(BinaryHelper.stringToByteArray("02E1951DE7596A")).decode(); - assertEquals(33325, df0.getAltitude().getAltitude(), 0.1); + assertEquals(32925, df0.getAltitude().getAltitude(), 0.1); assertEquals(25, df0.getAltitude().getStep()); assertFalse(df0.getAltitude().isMetric()); } diff --git a/src/test/java/aero/t2s/modes/decoder/df/DF24Test.java b/src/test/java/aero/t2s/modes/decoder/df/DF24Test.java new file mode 100644 index 0000000..897a474 --- /dev/null +++ b/src/test/java/aero/t2s/modes/decoder/df/DF24Test.java @@ -0,0 +1,18 @@ +package aero.t2s.modes.decoder.df; + +import aero.t2s.modes.BinaryHelper; +import aero.t2s.modes.decoder.UnknownDownlinkFormatException; +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.*; + +public class DF24Test { + @Test + public void test_df24_elm() throws UnknownDownlinkFormatException { + DF24 df = new DF24(BinaryHelper.stringToByteArray("C33D2901090141AE21C600180121")); + df.decode(); + + assertEquals("76CEFA", df.getIcao()); + assertEquals(3, df.getSequenceNo()); + } +} diff --git a/src/test/java/aero/t2s/modes/decoder/df/DfRealMessageTest.java b/src/test/java/aero/t2s/modes/decoder/df/DfRealMessageTest.java new file mode 100644 index 0000000..039f009 --- /dev/null +++ b/src/test/java/aero/t2s/modes/decoder/df/DfRealMessageTest.java @@ -0,0 +1,438 @@ +package aero.t2s.modes.decoder.df; + +import aero.t2s.modes.BinaryHelper; +import aero.t2s.modes.constants.*; +import aero.t2s.modes.database.ModeSDatabase; +import aero.t2s.modes.decoder.Decoder; +import aero.t2s.modes.decoder.UnknownDownlinkFormatException; +import aero.t2s.modes.decoder.df.bds.*; +import org.junit.jupiter.api.Test; + +import java.util.HashMap; +import static org.junit.jupiter.api.Assertions.*; + +public class DfRealMessageTest { + @Test + public void test_bds60() throws UnknownDownlinkFormatException { + DownlinkFormat df = testMessage("A0001838E71A21357F640110A153"); + + assertInstanceOf(DF20.class, df); + + assertTrue(((DF20)df).isValid()); + assertFalse(((DF20)df).isMultipleMatches()); + assertInstanceOf(Bds60.class, ((DF20)df).getBds()); + assertEquals(38000, ((DF20) df).getAltitude().getAltitude()); + + Bds60 bds = (Bds60) ((DF20)df).getBds(); + assertTrue(bds.isStatusMagneticHeading()); + assertEquals(289.863, bds.getMagneticHeading(), 0.001); + assertTrue(bds.isStatusIas()); + assertEquals(272, bds.getIas()); + assertTrue(bds.isStatusMach()); + assertEquals(0.852, bds.getMach()); + assertTrue(bds.isStatusBaroRocd()); + assertEquals(-640, bds.getBaroRocd()); + assertTrue(bds.isStatusIrsRocd()); + assertEquals(32, bds.getIrsRocd()); + } + + @Test + public void test_df_20_bds_40_a48e35() throws UnknownDownlinkFormatException { + DownlinkFormat df = testMessage("A00006A1E3A71D30AA014672C8DF"); + + assertInstanceOf(DF20.class, df); + DF20 df20 = (DF20) df; + assertEquals("A48E35", df.getIcao()); + assertEquals(51000, df20.getAltitude().getAltitude()); + + assertTrue(df20.isValid()); + assertInstanceOf(Bds40.class, df20.getBds()); + + Bds40 bds = (Bds40) df20.getBds(); + assertTrue(bds.isStatusTargetSource()); + assertEquals(SelectedAltitudeSource.MCP, bds.getSelectedAltitudeSource()); + assertTrue(bds.isStatusMcp()); + assertEquals(51008, bds.getSelectedAltitude()); + assertTrue(bds.isStatusFms()); + assertEquals(51008, bds.getFmsAltitude()); + assertTrue(bds.isStatusBaro()); + assertEquals(1013.3, bds.getBaro(), 0.1); + assertTrue(bds.isStatusMcpMode()); + assertFalse(bds.isAutopilotApproach()); + assertFalse(bds.isAutopilotVnav()); + assertTrue(bds.isAutopilotAltitudeHold()); + } + + + @Test + public void test_df20_bds60_800736() throws UnknownDownlinkFormatException { + DownlinkFormat df = testMessage("A0001A1FA439F534BF07FFDE1ECC"); + + assertInstanceOf(DF20.class, df); + DF20 df20 = (DF20) df; + assertEquals("800736", df.getIcao()); + assertEquals(40975, df20.getAltitude().getAltitude()); + + assertTrue(df20.isValid()); + assertInstanceOf(Bds60.class, df20.getBds()); + + Bds60 bds = (Bds60) df20.getBds(); + assertTrue(bds.isStatusIrsRocd()); + assertEquals(-32, bds.getIrsRocd()); + assertTrue(bds.isStatusBaroRocd()); + assertEquals(-1024.0, bds.getBaroRocd()); + assertTrue(bds.isStatusIas()); + assertEquals(250, bds.getIas()); + assertTrue(bds.isStatusMach()); + assertEquals(0.84, bds.getMach(), 0.1); + assertTrue(bds.isStatusMagneticHeading()); + assertEquals(101.7, bds.getMagneticHeading(), 0.1); + } + + @Test + public void test_df21_bds60_4CA708() throws UnknownDownlinkFormatException { + DownlinkFormat df = testMessage("A8800337D439E730BFE600C28696"); + + assertInstanceOf(DF21.class, df); + DF21 df21 = (DF21) df; + assertEquals("4CA708", df.getIcao()); + assertEquals(2547, df21.getModeA()); + + assertTrue(df21.isValid()); + assertInstanceOf(Bds60.class, df21.getBds()); + + Bds60 bds = (Bds60) df21.getBds(); + assertTrue(bds.isStatusIrsRocd()); + assertEquals(0, bds.getIrsRocd(), 0.1); + assertTrue(bds.isStatusBaroRocd()); + assertEquals(-128, bds.getBaroRocd(), 0.1); + assertTrue(bds.isStatusIas()); + assertEquals(243, bds.getIas()); + assertTrue(bds.isStatusMach()); + assertEquals(0.77, bds.getMach(), 0.1); + assertTrue(bds.isStatusMagneticHeading()); + assertEquals(236.7, bds.getMagneticHeading(), 0.1); + } + + @Test + public void test_df21_bds60_00000() throws UnknownDownlinkFormatException { + DownlinkFormat df = testMessage("A800198EEABA2B30F0041257522A"); + + assertInstanceOf(DF21.class, df); + DF21 df21 = (DF21) df; + assertEquals("000000", df.getIcao()); // Military / corrupt transponder + assertEquals(5652, df21.getModeA()); + + assertTrue(df21.isValid()); + assertInstanceOf(Bds60.class, df21.getBds()); + + Bds60 bds = (Bds60) df21.getBds(); + assertTrue(bds.isStatusIrsRocd()); + assertEquals(576, bds.getIrsRocd(), 0.1); + assertTrue(bds.isStatusBaroRocd()); + assertEquals(0, bds.getBaroRocd(), 0.1); + assertTrue(bds.isStatusIas()); + assertEquals(277, bds.getIas()); + assertTrue(bds.isStatusMach()); + assertEquals(0.77, bds.getMach(), 0.1); + assertTrue(bds.isStatusMagneticHeading()); + assertEquals(300.0, bds.getMagneticHeading(), 0.1); + } + + @Test + public void test_df21_bds60_406ECD() throws UnknownDownlinkFormatException { + DownlinkFormat df = testMessage("A8000C98A549F33461E40552947D"); + + assertInstanceOf(DF21.class, df); + DF21 df21 = (DF21) df; + assertEquals("406ECD", df.getIcao()); // Military / corrupt transponder + assertEquals(5221, df21.getModeA()); + + assertTrue(df21.isValid()); + assertInstanceOf(Bds60.class, df21.getBds()); + + Bds60 bds = (Bds60) df21.getBds(); + assertTrue(bds.isStatusIrsRocd()); + assertEquals(160, bds.getIrsRocd(), 0.1); + assertTrue(bds.isStatusBaroRocd()); + assertEquals(1920, bds.getBaroRocd(), 0.1); + assertTrue(bds.isStatusIas()); + assertEquals(249, bds.getIas()); + assertTrue(bds.isStatusMach()); + assertEquals(0.77, bds.getMach(), 0.1); + assertTrue(bds.isStatusMagneticHeading()); + assertEquals(104.7, bds.getMagneticHeading(), 0.1); + } + + + @Test + public void test_df21_bds50_44CD73() throws UnknownDownlinkFormatException { + DownlinkFormat df = testMessage("A0000333E17987192250004423EC"); + + assertInstanceOf(DF20.class, df); + DF20 df20 = (DF20) df; + assertEquals("44CD73", df.getIcao()); // Military / corrupt transponder + assertEquals(4275, df20.getAltitude().getAltitude()); + + assertTrue(df20.isValid()); + assertInstanceOf(Bds50.class, df20.getBds()); + + Bds50 bds = (Bds50) df20.getBds(); + assertTrue(bds.isStatusGs()); + assertEquals(200, bds.getGs(), 0.1); + assertFalse(bds.isStatusTas()); + assertEquals(0, bds.getTas(), 0.1); + assertTrue(bds.isStatusRollAngle()); + assertEquals(-43, bds.getRollAngle(), 0.1); + assertTrue(bds.isStatusTrueAngleRate()); + assertEquals(2.3, bds.getTrackAngleRate(), 0.1); + assertTrue(bds.isStatusTrackAngle()); + assertEquals(214.2, bds.getTrueTrack(), 0.1); + } + + @Test + public void test_df21_bds40_407776() throws UnknownDownlinkFormatException { + DownlinkFormat df = testMessage("A000169EB2CC0030A80106C25083"); + + assertInstanceOf(DF20.class, df); + DF20 df20 = (DF20) df; + assertEquals("407776", df.getIcao()); // Military / corrupt transponder + assertEquals(35350, df20.getAltitude().getAltitude()); + + assertTrue(df20.isValid()); + assertInstanceOf(Bds40.class, df20.getBds()); + + Bds40 bds = (Bds40) df20.getBds(); + assertTrue(bds.isStatusMcp()); + assertEquals(26000, bds.getSelectedAltitude(), 0.1); + assertTrue(bds.isStatusFms()); + assertEquals(0, bds.getFmsAltitude(), 0.1); + assertTrue(bds.isStatusBaro()); + assertEquals(1013.2, bds.getBaro(), 0.1); + assertTrue(bds.isStatusTargetSource()); + assertEquals(SelectedAltitudeSource.MCP, bds.getSelectedAltitudeSource()); + assertTrue(bds.isStatusMcpMode()); + assertFalse(bds.isAutopilotVnav()); + assertFalse(bds.isAutopilotAltitudeHold()); + assertFalse(bds.isAutopilotApproach()); + } + + + @Test + public void test_df21_bds10_407776() throws UnknownDownlinkFormatException { + DownlinkFormat df = testMessage("A000042210000600B0000089B69E"); + + assertInstanceOf(DF20.class, df); + DF20 df20 = (DF20) df; + assertEquals("44CC63", df.getIcao()); // Military / corrupt transponder + assertEquals(2000, df20.getAltitude().getAltitude()); + + assertTrue(df20.isValid()); + assertInstanceOf(Bds10.class, df20.getBds()); + + Bds10 bds = (Bds10) df20.getBds(); + } + + + @Test + public void test_df21_bds40_4CA6F8() throws UnknownDownlinkFormatException { + DownlinkFormat df = testMessage("A000069E8BBC2F30A40000528AB8"); + + assertInstanceOf(DF20.class, df); + DF20 df20 = (DF20) df; + assertEquals("4CA6F8", df.getIcao()); // Military / corrupt transponder + assertEquals(9750, df20.getAltitude().getAltitude()); + + assertTrue(df20.isValid()); + assertInstanceOf(Bds40.class, df20.getBds()); + + Bds40 bds = (Bds40) df20.getBds(); + assertFalse(bds.isStatusTargetSource()); + assertNull(bds.getSelectedAltitudeSource()); + assertTrue(bds.isStatusMcp()); + assertEquals(6000, bds.getSelectedAltitude()); + assertTrue(bds.isStatusFms()); + assertEquals(3008, bds.getFmsAltitude()); + assertTrue(bds.isStatusBaro()); + assertEquals(1013.0, bds.getBaro(), 0.1); + assertFalse(bds.isStatusMcpMode()); + assertFalse(bds.isAutopilotApproach()); + assertFalse(bds.isAutopilotVnav()); + assertFalse(bds.isAutopilotAltitudeHold()); + } + + @Test + public void test_df21_bds50_485209() throws UnknownDownlinkFormatException { + DownlinkFormat df = testMessage("A000093BFFF16B276004997B748F"); + + assertInstanceOf(DF20.class, df); + DF20 df20 = (DF20) df; + assertEquals("485209", df.getIcao()); // Military / corrupt transponder + assertEquals(14075, df20.getAltitude().getAltitude()); + + assertTrue(df20.isValid()); + assertInstanceOf(Bds50.class, df20.getBds()); + + Bds50 bds = (Bds50) df20.getBds(); + assertTrue(bds.isStatusGs()); + assertEquals(314, bds.getGs(), 0.1); + assertTrue(bds.isStatusTas()); + assertEquals(306, bds.getTas(), 0.1); + assertTrue(bds.isStatusRollAngle()); + assertEquals(-0.1, bds.getRollAngle(), 0.1); + assertTrue(bds.isStatusTrueAngleRate()); + assertEquals(0, bds.getTrackAngleRate(), 0.1); + assertTrue(bds.isStatusTrackAngle()); + assertEquals(31.8, bds.getTrueTrack(), 0.1); + } + + @Test + public void test_df21_bds50_484FDF() throws UnknownDownlinkFormatException { + DownlinkFormat df = testMessage("A800080080502D2A600CA9DF5877"); + + assertInstanceOf(DF21.class, df); + DF21 df21 = (DF21) df; + assertEquals("484FDF", df.getIcao()); // Military / corrupt transponder + assertEquals(1000, df21.getModeA()); + + assertTrue(df21.isValid()); + assertInstanceOf(Bds50.class, df21.getBds()); + + Bds50 bds = (Bds50) df21.getBds(); + assertTrue(bds.isStatusGs()); + assertEquals(338, bds.getGs(), 0.1); + assertTrue(bds.isStatusTas()); + assertEquals(338, bds.getTas(), 0.1); + assertTrue(bds.isStatusRollAngle()); + assertEquals(0.35, bds.getRollAngle(), 0.1); + assertTrue(bds.isStatusTrueAngleRate()); + assertEquals(0, bds.getTrackAngleRate(), 0.1); + assertTrue(bds.isStatusTrackAngle()); + assertEquals(3.8, bds.getTrueTrack(), 0.1); + } + + @Test + public void test_df20_bds50_48418A() throws UnknownDownlinkFormatException { + DownlinkFormat df = testMessage("A0000E978F1FBD316114BDA0FFBF"); + + assertInstanceOf(DF20.class, df); + DF20 df20 = (DF20) df; + assertEquals("48418A", df.getIcao()); // Military / corrupt transponder + assertEquals(22375, df20.getAltitude().getAltitude()); + + assertTrue(df20.isValid()); + assertInstanceOf(Bds50.class, df20.getBds()); + + Bds50 bds = (Bds50) df20.getBds(); + assertTrue(bds.isStatusGs()); + assertEquals(394, bds.getGs(), 0.1); + assertTrue(bds.isStatusTas()); + assertEquals(378, bds.getTas(), 0.1); + assertTrue(bds.isStatusRollAngle()); + assertEquals(21, bds.getRollAngle(), 0.1); + assertTrue(bds.isStatusTrueAngleRate()); + assertEquals(1, bds.getTrackAngleRate(), 0.1); + assertTrue(bds.isStatusTrackAngle()); + assertEquals(354, bds.getTrueTrack(), 0.1); + } + + @Test + public void test_df16_02A198() throws UnknownDownlinkFormatException { + DownlinkFormat df = testMessage("80C18819584195384EF8505941FD"); + + assertInstanceOf(DF16.class, df); + DF16 df16 = (DF16) df; + assertEquals("02A198", df.getIcao()); // Military / corrupt transponder + assertEquals(12025, df16.getAltitude().getAltitude()); + + assertEquals(VerticalStatus.AIRBORNE, df16.getVerticalStatus()); + assertEquals(AcasSensitivity.LEVEL6, df16.getSensitivity()); + assertEquals(AcasReplyInformation.ACAS_RA_VERTICAL_ONLY, df16.getReplyInformation()); + assertFalse(df16.getResolutionAdvisory().isActive()); + assertFalse(df16.getResolutionAdvisory().isRequiresCorrectionUpwards()); + assertFalse(df16.getResolutionAdvisory().isRequiresCorrectionDownwards()); + assertFalse(df16.getResolutionAdvisory().isRequiresPositiveClimb()); + assertFalse(df16.getResolutionAdvisory().isRequiresPositiveDescend()); + assertFalse(df16.getResolutionAdvisory().isRequiresCrossing()); + assertFalse(df16.getResolutionAdvisory().isSenseReversal()); + + assertFalse(df16.isMultipleThreats()); + assertFalse(df16.isRANotPassAbove()); + assertFalse(df16.isRANotPassBelow()); + assertFalse(df16.isRANotTurnLeft()); + assertFalse(df16.isRANotTurnRight()); + } + + @Test + public void test_df21_bds50_02A185() throws UnknownDownlinkFormatException { + DownlinkFormat df = testMessage("A8001AA0F4596112BDFC569A3AEC"); + + assertInstanceOf(DF21.class, df); + DF21 df21 = (DF21) df; + assertEquals("02A185", df.getIcao()); // Military / corrupt transponder + assertEquals(7110, df21.getModeA()); + + assertFalse(df21.isMultipleMatches()); + assertTrue(df21.isValid()); + assertEquals(Bds50.class, df21.getBds().getClass()); + + Bds50 bds = (Bds50) df21.getBds(); + assertTrue(bds.isStatusGs()); + assertEquals(148, bds.getGs(), 0.1); + assertTrue(bds.isStatusTas()); + assertEquals(172, bds.getTas(), 0.1); + assertTrue(bds.isStatusRollAngle()); + assertEquals(-16.5, bds.getRollAngle(), 0.1); + assertTrue(bds.isStatusTrueAngleRate()); + assertEquals(-2.0, bds.getTrackAngleRate(), 0.1); + assertTrue(bds.isStatusTrackAngle()); + assertEquals(210.9, bds.getTrueTrack(), 0.1); + } + + @Test + public void test_df20_bds17_3D2C7C() throws UnknownDownlinkFormatException { + DownlinkFormat df = testMessage("A0280314020100000000004E25E8"); + + assertInstanceOf(DF20.class, df); + DF20 df20 = (DF20) df; + assertEquals("3D2C7C", df.getIcao()); // Military / corrupt transponder + assertEquals(3900, df20.getAltitude().getAltitude()); + + assertFalse(df20.isMultipleMatches()); + assertTrue(df20.isValid()); + assertEquals(Bds17.class, df20.getBds().getClass()); + + Bds17 bds = (Bds17) df20.getBds(); + assertFalse(bds.isBds0A()); + assertFalse(bds.isBds05()); + assertFalse(bds.isBds06()); + assertFalse(bds.isBds07()); + assertFalse(bds.isBds08()); + assertFalse(bds.isBds09()); + assertFalse(bds.isBds0A()); + assertTrue(bds.isBds20()); + assertFalse(bds.isBds21()); + assertFalse(bds.isBds40()); + assertFalse(bds.isBds41()); + assertFalse(bds.isBds42()); + assertFalse(bds.isBds43()); + assertFalse(bds.isBds44()); + assertFalse(bds.isBds45()); + assertFalse(bds.isBds48()); + assertTrue(bds.isBds50()); + assertFalse(bds.isBds51()); + assertFalse(bds.isBds52()); + assertFalse(bds.isBds53()); + assertFalse(bds.isBds54()); + assertFalse(bds.isBds55()); + assertFalse(bds.isBds56()); + assertFalse(bds.isBds5F()); + assertFalse(bds.isBds60()); + } + + private DownlinkFormat testMessage(String message) throws UnknownDownlinkFormatException { + Decoder decoder = new Decoder(new HashMap<>(), 50, 2, ModeSDatabase.createDatabase()); + + return decoder.decode(BinaryHelper.stringToByteArray(message)); + } +} diff --git a/src/test/java/aero/t2s/modes/decoder/df/DfTEst.java b/src/test/java/aero/t2s/modes/decoder/df/DfTEst.java deleted file mode 100644 index bc4f862..0000000 --- a/src/test/java/aero/t2s/modes/decoder/df/DfTEst.java +++ /dev/null @@ -1,22 +0,0 @@ -package aero.t2s.modes.decoder.df; - -import aero.t2s.modes.BinaryHelper; -import aero.t2s.modes.database.ModeSDatabase; -import aero.t2s.modes.decoder.Decoder; -import aero.t2s.modes.decoder.UnknownDownlinkFormatException; -import org.junit.jupiter.api.Test; - -import java.util.HashMap; - -public class DfTEst { - @Test - public void test() throws UnknownDownlinkFormatException { - String message = "A0001338000557F0A8000098FCDB"; - - Decoder decoder = new Decoder(new HashMap<>(), 50, 2, ModeSDatabase.createDatabase()); - - DownlinkFormat df = decoder.decode(BinaryHelper.stringToByteArray(message)); - - System.out.println(df.toString()); - } -} diff --git a/src/test/java/aero/t2s/modes/decoder/df/bds/Bds50Test.java b/src/test/java/aero/t2s/modes/decoder/df/bds/Bds50Test.java index 5948577..a7d10fd 100644 --- a/src/test/java/aero/t2s/modes/decoder/df/bds/Bds50Test.java +++ b/src/test/java/aero/t2s/modes/decoder/df/bds/Bds50Test.java @@ -29,46 +29,6 @@ public void it_does_nothing_with_all_zeros() assertEquals(0, bds.getTas()); } - @Test - public void it_decodes_bds50_roll_angle() - { - bds = new Bds50(new short[] { - 0x0, 0x0, 0x0, 0x0, - 0b10100000, - 0b00000000, - 0b00000000, - 0b00000000, - 0b00000000, - 0b00000000, - 0b00000000, - }); - - assertTrue(bds.isValid()); - assertEquals(45, bds.getRollAngle()); - assertEquals(0, bds.getTrueTrack()); - assertEquals(0, bds.getGs()); - assertEquals(0, bds.getTrackAngleRate()); - assertEquals(0, bds.getTas()); - - bds = new Bds50(new short[] { - 0x0, 0x0, 0x0, 0x0, - 0b11100000, - 0b00000000, - 0b00000000, - 0b00000000, - 0b00000000, - 0b00000000, - 0b00000000, - }); - - assertTrue(bds.isValid()); - assertEquals(-45, bds.getRollAngle()); - assertEquals(0, bds.getTrueTrack()); - assertEquals(0, bds.getGs()); - assertEquals(0, bds.getTrackAngleRate()); - assertEquals(0, bds.getTas()); - } - @Test public void it_is_not_bds50_when_roll_angle_is_not_available_and_bits_are_set() { diff --git a/src/test/java/aero/t2s/modes/decoder/df/df17/AirbornePositionTest.java b/src/test/java/aero/t2s/modes/decoder/df/df17/AirbornePositionTest.java new file mode 100644 index 0000000..64fe9ec --- /dev/null +++ b/src/test/java/aero/t2s/modes/decoder/df/df17/AirbornePositionTest.java @@ -0,0 +1,66 @@ +package aero.t2s.modes.decoder.df.df17; + +import aero.t2s.modes.BinaryHelper; +import aero.t2s.modes.database.ModeSDatabase; +import aero.t2s.modes.decoder.Decoder; +import aero.t2s.modes.decoder.UnknownDownlinkFormatException; +import aero.t2s.modes.decoder.df.DF17; +import aero.t2s.modes.decoder.df.DF18; +import aero.t2s.modes.decoder.df.DownlinkFormat; +import org.junit.jupiter.api.Test; + +import java.util.HashMap; + +import static org.junit.jupiter.api.Assertions.*; + +class AirbornePositionTest { + @Test + public void test_airborne_position_aircraft() throws UnknownDownlinkFormatException { + // This is an ODD frame with Airborne Position + DownlinkFormat dfA = testMessage("8d4076635883069b318845770698"); + + assertInstanceOf(DF17.class, dfA); + DF17 df17A = (DF17) dfA; + assertEquals("407663", df17A.getIcao()); + ExtendedSquitter exSqA = df17A.getExtendedSquitter(); + assertInstanceOf(AirbornePosition.class, exSqA); + AirbornePosition positionA = (AirbornePosition) exSqA; + // We should NOT have a valid position after only one frame + assertFalse(positionA.isPositionAvailable()); + + // This is an EVEN frame with Airborne Position... so we should be able to do a global calculation of position + DownlinkFormat dfB = testMessage("8d407663588303313d84f719b2de"); + + assertInstanceOf(DF17.class, dfB); + DF17 df17B = (DF17) dfB; + assertEquals("407663", df17B.getIcao()); + ExtendedSquitter exSqB = df17B.getExtendedSquitter(); + assertInstanceOf(AirbornePosition.class, exSqB); + AirbornePosition positionB = (AirbornePosition) exSqB; + // We should now have a valid position after receiving both even and odd frames + assertTrue(positionB.isPositionAvailable()); + assertEquals(52.789, positionB.getLat(), 0.01); + assertEquals(-2.405, positionB.getLon(), 0.01); + + // This is another EVEN frame with Airborne Position... so we should be able to do a local calculation of position + DownlinkFormat dfC = testMessage("8d407663588303312385138ddce6"); + + assertInstanceOf(DF17.class, dfC); + DF17 df17C = (DF17) dfC; + assertEquals("407663", df17C.getIcao()); + ExtendedSquitter exSqC = df17C.getExtendedSquitter(); + assertInstanceOf(AirbornePosition.class, exSqC); + AirbornePosition positionC = (AirbornePosition) exSqC; + // We should still have a valid position after receiving another even frame + assertTrue(positionC.isPositionAvailable()); + assertEquals(52.788, positionC.getLat(), 0.01); + assertEquals(-2.401, positionC.getLon(), 0.01); + } + + private DownlinkFormat testMessage(String message) throws UnknownDownlinkFormatException { + Decoder decoder = new Decoder(new HashMap<>(), 53, -2, ModeSDatabase.createDatabase()); + + return decoder.decode(BinaryHelper.stringToByteArray(message)); + } + +} diff --git a/src/test/java/aero/t2s/modes/decoder/df/df17/AircraftOperationalStatusMessageTest.java b/src/test/java/aero/t2s/modes/decoder/df/df17/AircraftOperationalStatusMessageTest.java new file mode 100644 index 0000000..b4fa927 --- /dev/null +++ b/src/test/java/aero/t2s/modes/decoder/df/df17/AircraftOperationalStatusMessageTest.java @@ -0,0 +1,43 @@ +package aero.t2s.modes.decoder.df.df17; + +import aero.t2s.modes.BinaryHelper; +import aero.t2s.modes.constants.*; +import aero.t2s.modes.decoder.df.df17.data.SurfaceOperationalMode; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; + +public class AircraftOperationalStatusMessageTest { + @Test + public void it_decodes_surface_version1_message() { + AircraftOperationalStatusMessage message = new AircraftOperationalStatusMessage(BinaryHelper.stringToByteArray("8F40622DF900260283493839FD1F")).decode(); + + Assertions.assertInstanceOf(AircraftOperationalStatusVersion2.class, message); + Assertions.assertInstanceOf(AircraftOperationalStatusVersion2Surface.class, message); + + AircraftOperationalStatusVersion2Surface surface = (AircraftOperationalStatusVersion2Surface) message; + Assertions.assertEquals(Version.VERSION2, surface.getVersion()); + Assertions.assertEquals(LengthWidthCode.CAT6, surface.getLengthWidthCode()); + + Assertions.assertFalse(surface.getSurfaceCapability().isCockpitDisplayOfTraffic()); + Assertions.assertFalse(surface.getSurfaceCapability().isUatReceive()); + Assertions.assertFalse(surface.getSurfaceCapability().isLowB2Power()); + Assertions.assertFalse(surface.getSurfaceCapability().isPositionOffsetApplied()); + Assertions.assertFalse(surface.getSurfaceCapability().isReceive1090ES()); + Assertions.assertEquals(NavigationUncertaintyCategory.NUC1, surface.getSurfaceCapability().getNACv()); + Assertions.assertEquals(0, surface.getSurfaceCapability().getNICsuppC()); + + Assertions.assertFalse(surface.getOperationalMode().isAcasIdent()); + Assertions.assertTrue(surface.getOperationalMode().isGpsLateralOffsetAvailable()); + Assertions.assertFalse(surface.getOperationalMode().isSingleAntennaFlag()); + Assertions.assertTrue(surface.getOperationalMode().isGpsLongitudinalOffsetAvailable()); + Assertions.assertFalse(surface.getOperationalMode().isGpsLongitudinalOffsetAppliedBySensor()); + Assertions.assertEquals(AcasState.RA_NOT_ACTIVE, surface.getOperationalMode().getAcasRA()); + Assertions.assertEquals(0, surface.getOperationalMode().getGpsLateralOffset()); + Assertions.assertEquals(0, surface.getOperationalMode().getGpsLateralOffset()); + Assertions.assertEquals(4, surface.getOperationalMode().getGpsLongitudinalOffset()); + Assertions.assertEquals(SourceIntegrityLevel.LESS_THEN_ONE_PER_HUNDRED_THOUSAND, surface.getOperationalMode().getSystemDesignAssurance()); + + Assertions.assertEquals(NavigationIntegrityCategory.RC_75_M, surface.getNICp()); + Assertions.assertEquals(Angle.TRUE_TRACK, surface.getHorizontalSource()); + } +} diff --git a/src/test/java/aero/t2s/modes/decoder/df/df17/SurfacePositionTest.java b/src/test/java/aero/t2s/modes/decoder/df/df17/SurfacePositionTest.java new file mode 100644 index 0000000..e7cbe8c --- /dev/null +++ b/src/test/java/aero/t2s/modes/decoder/df/df17/SurfacePositionTest.java @@ -0,0 +1,90 @@ +package aero.t2s.modes.decoder.df.df17; + +import aero.t2s.modes.BinaryHelper; +import aero.t2s.modes.database.ModeSDatabase; +import aero.t2s.modes.decoder.Decoder; +import aero.t2s.modes.decoder.UnknownDownlinkFormatException; +import aero.t2s.modes.decoder.df.DF17; +import aero.t2s.modes.decoder.df.DF18; +import aero.t2s.modes.decoder.df.DownlinkFormat; +import org.junit.jupiter.api.Test; + +import java.util.HashMap; + +import static org.junit.jupiter.api.Assertions.*; + +class SurfacePositionTest { + @Test + public void test_surface_position_aircraft() throws UnknownDownlinkFormatException { + // This is an EVEN frame with Surface Position + DownlinkFormat dfA = testMessage("8c4ca6a938dd224b663c964f058e"); + + assertInstanceOf(DF17.class, dfA); + DF17 df17A = (DF17) dfA; + assertEquals("4CA6A9", df17A.getIcao()); + ExtendedSquitter exSqA = df17A.getExtendedSquitter(); + assertInstanceOf(SurfacePosition.class, exSqA); + SurfacePosition positionA = (SurfacePosition) exSqA; + assertEquals(13, positionA.getVelocityEncoded()); + assertEquals(2.0, positionA.getVelocity()); + assertTrue(positionA.isTrackAvailable()); + assertEquals(82, positionA.getTrackEncoded()); + assertEquals(230.625, positionA.getTrack(), 0.001); + // We should NOT have a valid position after only one frame + assertFalse(positionA.isPositionAvailable()); + + // This is an ODD frame with Surface Position + DownlinkFormat dfB = testMessage("8c4ca6a938cd27ec46497d947b4d"); + + assertInstanceOf(DF17.class, dfB); + DF17 df17B = (DF17) dfB; + assertEquals("4CA6A9", df17B.getIcao()); + ExtendedSquitter exSqB = df17B.getExtendedSquitter(); + assertInstanceOf(SurfacePosition.class, exSqB); + SurfacePosition positionB = (SurfacePosition) exSqB; + // We should now have a valid position after receiving both even and odd frames + assertTrue(positionB.isPositionAvailable()); + assertEquals(53.3604, positionB.getLat(), 0.001); + assertEquals(-2.2671, positionB.getLon(), 0.001); + assertTrue(positionB.isVelocityAvailable()); + assertEquals(1.75, positionB.getVelocity(), 0.1); + assertTrue(positionB.isTrackAvailable()); + assertEquals(230.625, positionB.getTrack(), 0.1); + + // This is another ODD frame with Surface Position... so we should be able to do a local calculation of position + DownlinkFormat dfC = testMessage("8c4ca6a9389d27ec46497d8ea39e"); + + assertInstanceOf(DF17.class, dfC); + DF17 df17C = (DF17) dfC; + assertEquals("4CA6A9", df17C.getIcao()); + ExtendedSquitter exSqC = df17C.getExtendedSquitter(); + assertInstanceOf(SurfacePosition.class, exSqC); + SurfacePosition positionC = (SurfacePosition) exSqC; + // We should still have a valid position after receiving another odd frame + + assertTrue(positionC.isPositionAvailable()); + assertEquals(53.360, positionC.getLat(), 0.001); + assertEquals(-2.267, positionC.getLon(), 0.001); + } + + @Test + public void test_surface_position_vehicle() throws UnknownDownlinkFormatException { + DownlinkFormat df = testMessage("90425854281007f03e44e4ba62ea"); + + assertInstanceOf(DF18.class, df); + DF18 df18 = (DF18) df; + assertEquals("425854", df.getIcao()); + ExtendedSquitter exSq = df18.getExtendedSquitter(); + assertInstanceOf(SurfacePosition.class, exSq); + SurfacePosition position = (SurfacePosition) exSq; + assertEquals(1, position.getVelocityEncoded()); + assertEquals(0.0, position.getVelocity()); + assertFalse(position.isTrackAvailable()); + } + + private DownlinkFormat testMessage(String message) throws UnknownDownlinkFormatException { + Decoder decoder = new Decoder(new HashMap<>(), 53, -2, ModeSDatabase.createDatabase()); + + return decoder.decode(BinaryHelper.stringToByteArray(message)); + } +}