From 5c2454b64b67c85323d86cde726bb416da718d8e Mon Sep 17 00:00:00 2001 From: Ken Andries Date: Sat, 2 Apr 2022 12:08:39 +0200 Subject: [PATCH 01/39] Remove duplicate fields from track, make register available * Removes duplicate fields from track. * Specific information such as accuracy made available through the new registerXX fields which map onto BDS X,X registers. These fields will contain specific data not relevant to a default tracks but could be useful for analyses or debugging. * Closes #13 Register 05 * Adds Register 07 (Closes #15) * Adds Register 08 (Closes #16) * Adds Register 09 (Closes #17) * Adds Register 17 * Adds Register 20 (Closes #20) * Adds Register 21 (Closes #21) --- example/.gitignore | 1 + example/build.gradle | 2 +- .../classes/java/main/example/Demo$1.class | Bin 656 -> 0 bytes .../classes/java/main/example/Demo$2.class | Bin 515 -> 0 bytes .../classes/java/main/example/Demo.class | Bin 5178 -> 0 bytes .../java/main/example/FlightsTable.class | Bin 2354 -> 0 bytes .../main/example/flight/FlightFrame$1.class | Bin 645 -> 0 bytes .../flight/FlightFrame$GcibTable.class | Bin 3927 -> 0 bytes .../main/example/flight/FlightFrame.class | Bin 12724 -> 0 bytes .../compileJava/source-classes-mapping.txt | 10 - .../src/main/java/example/FlightsTable.java | 22 +- .../main/java/example/flight/FlightFrame.form | 174 +++++++++++--- .../main/java/example/flight/FlightFrame.java | 158 ++----------- .../java/aero/t2s/modes/CapabilityReport.java | 31 +++ src/main/java/aero/t2s/modes/RadiusLimit.java | 89 ------- src/main/java/aero/t2s/modes/Track.java | 220 +++++++----------- .../t2s/modes/constants/AircraftCategory.java | 33 +++ .../t2s/modes/constants/AltitudeSource.java | 8 + .../java/aero/t2s/modes/constants/Angle.java | 3 +- .../constants/HorizontalProtectionLimit.java | 25 ++ .../NavigationAccuracyCategoryVelocity.java | 10 + .../aero/t2s/modes/constants/RocdSource.java | 3 +- .../java/aero/t2s/modes/constants/Speed.java | 2 +- .../t2s/modes/constants/TransmissionRate.java | 9 + .../aero/t2s/modes/decoder/df/bds/Bds17.java | 2 +- .../aero/t2s/modes/decoder/df/bds/Bds20.java | 1 + .../aero/t2s/modes/decoder/df/bds/Bds21.java | 4 + .../decoder/df/df17/AirbornePosition.java | 98 +++++--- .../df17/AirborneVelocityAirspeedHeading.java | 32 ++- .../df/df17/AirborneVelocityGroundspeed.java | 31 ++- .../df/df17/AircraftIdentification.java | 48 ++++ .../df/df17/TargetStatusMessageType0.java | 3 - .../df/df17/TargetStatusMessageType1.java | 6 - .../t2s/modes/examples/StdOutExample.java | 58 ++++- .../aero/t2s/modes/registers/Register.java | 15 ++ .../aero/t2s/modes/registers/Register05.java | 103 ++++++++ .../t2s/modes/registers/Register05V0.java | 36 +++ .../t2s/modes/registers/Register05V2.java | 23 ++ .../aero/t2s/modes/registers/Register06.java | 19 ++ .../aero/t2s/modes/registers/Register07.java | 42 ++++ .../aero/t2s/modes/registers/Register08.java | 41 ++++ .../aero/t2s/modes/registers/Register09.java | 142 +++++++++++ .../t2s/modes/registers/Register09V0.java | 26 +++ .../aero/t2s/modes/registers/Register17.java | 26 +++ .../aero/t2s/modes/registers/Register20.java | 23 ++ .../aero/t2s/modes/registers/Register21.java | 29 +++ 46 files changed, 1132 insertions(+), 476 deletions(-) delete mode 100644 example/build/classes/java/main/example/Demo$1.class delete mode 100644 example/build/classes/java/main/example/Demo$2.class delete mode 100644 example/build/classes/java/main/example/Demo.class delete mode 100644 example/build/classes/java/main/example/FlightsTable.class delete mode 100644 example/build/classes/java/main/example/flight/FlightFrame$1.class delete mode 100644 example/build/classes/java/main/example/flight/FlightFrame$GcibTable.class delete mode 100644 example/build/classes/java/main/example/flight/FlightFrame.class delete mode 100644 example/build/tmp/compileJava/source-classes-mapping.txt delete mode 100644 src/main/java/aero/t2s/modes/RadiusLimit.java create mode 100644 src/main/java/aero/t2s/modes/constants/AircraftCategory.java create mode 100644 src/main/java/aero/t2s/modes/constants/AltitudeSource.java create mode 100644 src/main/java/aero/t2s/modes/constants/HorizontalProtectionLimit.java create mode 100644 src/main/java/aero/t2s/modes/constants/NavigationAccuracyCategoryVelocity.java create mode 100644 src/main/java/aero/t2s/modes/constants/TransmissionRate.java create mode 100644 src/main/java/aero/t2s/modes/registers/Register.java create mode 100644 src/main/java/aero/t2s/modes/registers/Register05.java create mode 100644 src/main/java/aero/t2s/modes/registers/Register05V0.java create mode 100644 src/main/java/aero/t2s/modes/registers/Register05V2.java create mode 100644 src/main/java/aero/t2s/modes/registers/Register06.java create mode 100644 src/main/java/aero/t2s/modes/registers/Register07.java create mode 100644 src/main/java/aero/t2s/modes/registers/Register08.java create mode 100644 src/main/java/aero/t2s/modes/registers/Register09.java create mode 100644 src/main/java/aero/t2s/modes/registers/Register09V0.java create mode 100644 src/main/java/aero/t2s/modes/registers/Register17.java create mode 100644 src/main/java/aero/t2s/modes/registers/Register20.java create mode 100644 src/main/java/aero/t2s/modes/registers/Register21.java diff --git a/example/.gitignore b/example/.gitignore index c379362..dd40b00 100644 --- a/example/.gitignore +++ b/example/.gitignore @@ -1,3 +1,4 @@ build/ .gradle/ +out/ diff --git a/example/build.gradle b/example/build.gradle index 78563b7..91970fd 100644 --- a/example/build.gradle +++ b/example/build.gradle @@ -8,5 +8,5 @@ repositories { } dependencies { - compile ('aero.t2s:mode-s:0.2.0-SNAPSHOT') + compile ('aero.t2s:mode-s:0.2.5-SNAPSHOT') } 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 c222b03cc6ba04cd55468f37c72d4cc3d9dfcde7..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 656 zcmYjO!A{#i5PcimI5CE}0aDu10&S#*P&MVk#T7zwii#AtG|GKRmSkaLBd>$_Eu>PU z9{K_OD8!5jX>57NGxO%{o0)$PfBpgpQ7@x_1s9Hw7bprWs~b7KGOE>9T(PoQ!wP5s>H;V3&v2FeK3X5gbdY`gNzsL+FtB^S#+DhLEtF66ar zO)_m-T|HK5SK3Ru@tclyb|A3O*y(wwV#UR(k2S7Xebz8fOW|{!s{D$!%w#l@@lXu} zJ~d`CxSeq3%~yvzJpm^o-PA}&$Le%4K4UE;SfJWT`qK1ds@b0g9iE9mFyng~R!EZ5 zNi5*~OeSex9cgALbMgy!O8npBzDcZ(hbJlV(h5PqV+Y96&NxC}1vLn~qQ8hY%8};o9Ypi<9o*(U7H@OlI-NxV0*81_ zXKh;i25-q@8m1KVZXlQv?&m&GYW{-z8!!Jed3kjN7ww$QCnVwU#OB}~c?G=3CRv9X SSi*-qvW1UiJwmf|dL99<280X% 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 c1d57f124635a0f2d51c8c1c3bc3c03b9370216f..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 515 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 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 0c4acb6fa6c5a9f9a3caf2651ceeb034d7b24bde..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 2354 zcmZ`)ZF3V<6n-{MvfFf9$^t(4K4Wo z?k8vTKj;U?55*TOjxc`qtNsWb{)6LlcQ(6!*IcPudUmDhHq4T{Bu;Zq*{Vv2Puq3dJt?6lm0s?J zf?gR36@5@8?6o${+BywSSMAE0+w|;cB0#P*B@7mX5548uRlQ(0-HfmwP_YYx66BIo zZPn^j+jG&^tD20&a2pIL(+3TEWb9S34@n6FJAouboA!0f3o0*EA5ifi4)9#DTnYP9 z`E&?!Vdb(_ax*dxGGxDxMd`5)>{vrE&sUydWbdGMkaG`+rLE zb=Rs`4GGDel?vp{Xb7iO%;JpCa<=N)ZmUc#hFFxY61G3m>MYL5CYQ1rTdrL?W0_^PUIOQ_D8o>(gqPc$=M{3!bk|tqhSRE-*@3LZ*#us}1sO$= z;;TE93r@Yl)=r%bb18&ga<8d)UHFgD|7=si8^p`!vI`1K2}&+oC>Z(Kc?Bi*NFlqV zK=gWHff9oDFD~R}MpCP;UbE|I1vW)(5`>tdD^x!-GpnG+a?BbdsVi<;La)J2HQiQ& zXh9=%?&fqit=`SZoXm(My6zd!S&Y)PRXn;n+;oxkw6%A z*KA((WZ_;TgFccwjjpY?4Ucp?zi*%-s08`UwdcydR65+-K_mMewdic*SVvxofsRQa z=m^MmSR=obVAO#9;MyoX(I?_x28C?qgV|##@wJ z;9KY?Mt^}cdgN!wH+bqngUQE8OMTTB)o%n1NSEaTH-Vwnh>FeU8h4-#5+{P@gCmyoPK~0eH`f$ z;vR{24}M1s;BW5<76H(?PH~;*66Acv^$pi=Tz`tRhi+ki1ixeGCLY@A54O+OMCX7-%qr^ciBC+jZ@uDUc8As)w{)W+jwak%iZFOUkV6GkmdK! zD7ym~<1o}Y`^T|}qpaTqzUDvFx0u3h9K&Dym-+|CJwiu(syc+;#YY~Yz+HUIT?C&H zuMfTBgM-7vdy>h$Z1zvdu-`K0*r?(+IjG#mWU)uPWE7*?n?^CFtr*3)RyK;VwrUg= fZOteS#`xwzoCaplp+zzflCpWiVCE{g{pVcn%0mwD0UFPg~UVRzz6VA zh;^t07l;&(cQren-TnA{dk4@!xriLbTsS_)k!M&J4?MaHMQ|O;!O#S)3|a|~M5WG< zH$$l_`^0PiO>B55pyJS=*n8S3Mn(&j(a@p@AlHC^Fa`aDN7BpnK z9aC&GUTyaoto91`G2_hBnI{ggjRiUvzgdJ!6cImDW5ig7KhioRv=xQg6W9wTf0Vqe mx&{}!nVPL2-bl^C3Pm}rVvW$Dy{=<3%S>U5&?C0>NZ%K*P=|Z~ 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 76f07773f51b11c0d79c67f5fc74eff2ef8172d7..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 3927 zcmd6qYjhP?703U3^0<>QfiMYyHYp4xlt3@!b#DMe#SjRj4dHQVXn>-Vn@JeBxs%S! zO+wYGwboayuUZSz`l_uDtf^9}we_)E9{0wb0&O2~i*EQqK}FlnkrdVn>ea;dg3W2U(C2I&^@1H8>8y2RE_90H&bSpk zR&vA8@h#i+$2`C2SkqqMhOSrCVG8c+NcXHVI4JoOc0N9~QPA0u-n+_Jcibt4?u47S zl~v0wj$3?mCO!YEjwXg;^=Uz9howNrCP8;cdjBeh%O1}-1uyT0S1q?V;rUb1Iy&wa z?CD7NtuoP>p+~#NeV6U>IW2?X6gv z8gqOdmS9VUW^zu!$%l;cjN^w4K!K|0_@{8qeUipSBH+ypN za#?xL_5(*pmtcLCdn|WM&@UYsMlj2*@gR2l&V=JT#k{($yc7*lR#TP3W@hAEG^vwh zTRZ01<4mGxbHTJjcNW5@O2LN=^x=@8@@W4cn}+j4D|4^628VGZg#iOcaf~OMTmS@B zD!Z>R+(WkGd+lLY&_2a1589*fvOqkDhf)|aa2&&e#?=PViAg8yx2Nqfx4`5$9UZk` zTiec+Mpq7^{gXJA!f69JoZ(5Vu)lJ=V2vB}&Dd^Xp~|rUozu~$W010 zilOrYsMQ`da8WIla)U$TK}S}{M_9;4kK}ZW3AEu8YS6d=CpM@@u}K4$Vr;KsmknHr zv3-h787Rise#NE@JQibpiUkJ3CDzrU*o=YM80%E*s)5IsSSA|xF#{iuv1r^U416-i zy42h!4SYJrx)uA3fv00EquA#Rd_Kmqiha?*GcgwJ?O6j~USiqEm#-T5T8u@$JZIn= zF&6pqO#?5)Smeuf1K*0V$d?xlycA=RFW)in-586`yg_HJJoPop`nN;~L^;+|O>UHLB>Uy)CdcE06 zZJOQG4Q7_Q(cDYD!Q4;XWcE^TG#|*lfrGC{8~HKyTIH;4MvYY9hHSx4$eOrGZU8s& zQ?}}fg_%t78h*C8&68|dqOf7tEyykO-^So=oVbSS`b{@0Z?dBTOXj!n^QsjMvLiBY zEKFQ9KOc>i2Ey0N*3*mDwJK#wi%eCkX_Aha&=ySyevV(TSB2kWtGsV*RyzT03Zh@x?HIv8OqN$+V#+%e! zymAkhVmP%s%Yt$pbuHIXZ9Xz@o}1@WEqf0(cg;7em9^KX)Za!*k@7UvDc|Pvbg=^5w55I~k)1Hs> zd;*_H;BEphBycZ*`w6_1z~>V9M-%uH3H(zD{HX-~*#!QD1pcK2{*?s&^#uNW0>75P zZzS+o*bApfg4#D4<%v7I;cIXQ}(atwcvLHtb~ z#J}VrF=R+u<+vP>VL2lwq#!5dDLExK8ahpFQfEi2I5zI(xOCt0Ye+L+JB?i*q6dv d`2W%L{%vNrhIhaWW_uXFWn0T?qMm;T?Z5rx$#Vbz 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 81f11cf2fb1bdecf80a7cf9c74102baefa65354b..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 12724 zcmd5jcYGYh)o)gJx99WN7Lswp*yoB|B^yk!C_Yu6I7#O2EF((7>TV@1KHZADm25*O z385F$!Svp%gO6U4kOb2^B&0w>Pk<0e_(IKlGrOm|RdMv!7yZoc&TI4L&Ac)*$5Y=u z_$UBWD{BLwK#2+-4qosvC_7T?*EUvWHyG)z%BF^l)}u4sb?$W1Q(<`bC?8^8I0-CjPx2Ut*01F z6hiZ(5YXxX%ALw#8tlSgd{(z=&9tSbEkZJwTp{()HdvK}Kv{aGdO;g8?aE;W%w(V< zQ_MITsl659u*A*cFdODD2q3P_$Qm6fM0+X@Av4WmFnxFv%v~0Mxv(3Vy@J8aipcP! zsD~FP3;);`Waz~lmcUX5<=I5Hp6pBMA*;dIs3)VEg}qO+hcYlZaV@*Px~YjYu7YJM zEa$L0?15$)%BT#+cA;^!R4Qw9rIDy&fiz7@!xa&;GPp3%mQ}DPhrLLOisa#hW|pCT z`*2tZ)o9KhZM}|mOla0R!|KMbtfi%`EQ7sm?Fyw0mOo^dV_`oI`@;bY{AlgCVUewO z9zx!NHd;d=4mD7VOSLOubVT)DGh@XyF+Z;5A)q*)VYnQmY@$n6Et&ZA@`fuX=HQ(tj5WermK55JqEOvaD>4u zx1Mf7RJj|{986m9e#?|@PzDT+q7{#EZ2nN|aPYz=<5?W~pdVv~mQ2>C^d1aCu65kn zaipHGMEL1|jT|};E{-*ayW?)oDfDj8SBPLEWc2w^|0 zrxm)H!#Qv+Y9m)@UyqSAtW7w*t|h8<=qa%T&gXCeTqxts>O470tnxe8ZsxDu`^!q=I7!r#??E!C&D;)r*ys4EODk^q&t z2Ch}%Iu6&v4Mh^%1{7qfm>C$ zjl=B})rwPZgzO}fPF&w;k%jN%a2HwFFDx9>ELsHjaM(&#oQ#thvoy;{gp5pF@9EVu zn$?%l{ctZXwOA-_o6!$Blpxxu0*Sq!!viF?T#6Mk)@1Z-7P+a1hd4Y;t6>WE5b8&< zUApYZA@akcIL$UH*lV+5f5`wm4nJ4n7c@7&v_ny)n7c40-!3WuCRKzBr^JR=`{7qO zU5-qrrZG;_x&nJRTJ;K>6WWWDtO$BqkO8@H{G3 z47EFXk;6;W$r#ZId5Rxi!GuCz#2T!`$9mD_YaCvuStzw#st&_=!ZOS>>GN9-zaxF} z{?@C@p#FOfe}F%tL|Gw;2prKnah**rb{bj%p^UDfS>9qLABSs0t*AgYP#cOwV&TRX z(O4I+74J~IeU11CRz&L?>1#n?osqP7*LwV1>NN$Z;WXeizq6^-WBy82)U zKdbgg255>UO>9kAplMMBLn%FDiDo_4Z(SIgHx;*n#MqY=Kw0tGkOJ0XxFdhb$bW%Q z$}+4zp?Gp$Hdv9h^xj|=3o`60xgb!ZWlZd2fF{tWn>ceBBM~$^JF#RSx~DA<9wQwP6w*>fWCG-(-9cxob@aM)SD~?*wXU>|5eVH& z;Hcw3JkzHa0URR~0e8?@)J&y

&rR)p9b0*Z~AF#_2e_!Gx%kus?y@fv{jB%@uiW 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 Qcmmvt64xkEB@R>n1H`H^=l}o! 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/src/main/java/example/FlightsTable.java b/example/src/main/java/example/FlightsTable.java index d93da7c..e85c0a3 100644 --- a/example/src/main/java/example/FlightsTable.java +++ b/example/src/main/java/example/FlightsTable.java @@ -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/example/src/main/java/example/flight/FlightFrame.form index faaf4f2..fd6ed8c 100644 --- a/example/src/main/java/example/flight/FlightFrame.form +++ b/example/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/example/src/main/java/example/flight/FlightFrame.java index 8f4ff43..b3c0cb2 100644 --- a/example/src/main/java/example/flight/FlightFrame.java +++ b/example/src/main/java/example/flight/FlightFrame.java @@ -12,9 +12,7 @@ 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 +21,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 +50,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 +84,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 +95,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 +109,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 +133,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 +143,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 +154,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 +209,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 +229,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/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/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..dc0c3e7 100644 --- a/src/main/java/aero/t2s/modes/Track.java +++ b/src/main/java/aero/t2s/modes/Track.java @@ -1,51 +1,48 @@ 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 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 +56,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; @@ -71,8 +67,6 @@ public class Track { private String registration; private String operator; - private boolean wasJustCreated = true; - public Track(String icao) { this.icao = icao; } @@ -85,143 +79,121 @@ public String getCallsign() { return callsign; } - public void setCategory(int category) { - this.category = category; - } - - public String getIcao() { - return icao; - } - - public boolean isExpired() { - return Instant.now().minusSeconds(15).isAfter(updated); + public Register05 register05() { + return register05; } - public Instant getUpdatedAt() { - return updated; + public void register05(Register05 register05) { + this.register05 = register05; } - public void setUpdatedAt(Instant time) { - this.updated = time; + public Register06 register06() { + return register06; } - public void touch() { - updated = Instant.now(); + public void register06(Register06 register06) { + this.register06 = register06; } - public void setGroundBit(boolean groundBit) { - this.groundBit = groundBit; + public Register07 register07() { + return register07; } - public boolean isGroundBit() { - return groundBit; + public void register07(Register07 register07) { + this.register07 = register07; } - public void setBaroAltitude(int baroAltitude) { - this.baroAltitude = baroAltitude; + public Register08 register08() { + return register08; } - public int getBaroAltitude() { - return baroAltitude; + public void register08(Register08 register08) { + this.register08 = register08; } - public int getGnssHeight() { - return gnssHeight; + public Register09 register09() { + return register09; } - public Track setGnssHeight(int gnssHeight) { - this.gnssHeight = gnssHeight; - return this; + public void register09(Register09 register09) { + this.register09 = register09; } - public CprPosition getCprPosition(boolean cprEven) { - return cprEven ? cprPositionEven : cprPositionOdd; + public Register17 register17() { + return register17; } - public void setLat(double lat) { - this.lat = lat; + public void register17(Register17 register17) { + this.register17 = register17; } - public double getLat() { - return lat; + public Register20 register20() { + return register20; } - public void setLon(double lon) { - this.lon = lon; + public void register20(Register20 register20) { + this.register20 = register20; } - public double getLon() { - return lon; + public Register21 register21() { + return register21; } - public void setSingleAntenna(boolean singleAntenna) { - this.singleAntenna = singleAntenna; + public void register21(Register21 register21) { + this.register21 = register21; } - public boolean getSingleAntenna() { - return singleAntenna; + public String getIcao() { + return icao; } - public Version getVersion() { - return version; + public boolean isExpired() { + return Instant.now().minusSeconds(15).isAfter(updated); } - public void setVersion(Version version) { - this.version = version; + public Instant getUpdatedAt() { + return updated; } - public int getNIC() { - return NIC; + public Track setUpdatedAt(Instant updated) { + this.updated = updated; + return this; } - public int getNICa() { - return NICa; + public void touch() { + updated = Instant.now(); } - public int getNICb() { - return NICb; + public void setGroundBit(boolean groundBit) { + this.groundBit = groundBit; } - public int getNICc() { - return NICc; + public boolean isGroundBit() { + return groundBit; } - public void setNIC(int NIC) { - if (this.NIC != NIC) { - this.NIC = NIC; - rc.determine(); - } + public void setLat(double lat) { + 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) { + 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) { @@ -276,14 +248,6 @@ public boolean isPositionAvailable() { return lat != 0 & lon != 0; } - public void setNACv(int naCv) { - this.NACv = naCv; - } - - public int getNACv() { - return NACv; - } - public void setGeometricHeightOffset(int geometricHeightOffset) { this.geometricHeightOffset = geometricHeightOffset; } @@ -340,14 +304,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 +320,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 +424,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 +526,6 @@ public Meteo getMeteo() { return meteo; } - public CapabilityReport getCapabilityReport() { - return capabilityReport; - } - @Override public String toString() { return String.format( 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/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/df17/AirbornePosition.java b/src/main/java/aero/t2s/modes/decoder/df/df17/AirbornePosition.java index 67129e2..c652f4a 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,9 +1,10 @@ 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.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; @@ -66,23 +67,42 @@ public void apply(Track track) { track.setSpi(surveillanceStatus == SurveillanceStatus.SPI); track.setTempAlert(surveillanceStatus == SurveillanceStatus.TEMPORARY_ALERT); track.setEmergency(surveillanceStatus == SurveillanceStatus.PERMANENT_ALERT); + track.setLat(lat); + track.setLon(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(); - if (altitudeSourceBaro) { - track.setBaroAltitude(altitude); + Register05 position = track.register05(); + + 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; } - track.setNIC(determineNIC(track, typeCode)); - track.setLat(lat); - track.setLon(lon); + if (track.getVersion() == Version.VERSION2 && track.register05().getVersion() != Version.VERSION2) { + return true; + } + + return false; } public int getSingleAntennaFlag() { @@ -131,47 +151,59 @@ 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 AltitudeSource determineAltitudeSource() { + if (typeCode < 19) { + return AltitudeSource.BARO; + } + + if (typeCode == 19) { + return AltitudeSource.BARO_GNSS_DIFF; + } + + return AltitudeSource.GNSS_HAE; + } + private void calculatePosition(boolean isEven, double lat, double lon, double time) { // CprPosition cprEven = track.getCprPosition(true); // CprPosition cprOdd = track.getCprPosition(false); 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..57ac261 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,30 @@ 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); + } } } 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/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/examples/StdOutExample.java b/src/main/java/aero/t2s/modes/examples/StdOutExample.java index 113034b..e47d028 100644 --- a/src/main/java/aero/t2s/modes/examples/StdOutExample.java +++ b/src/main/java/aero/t2s/modes/examples/StdOutExample.java @@ -1,19 +1,67 @@ package aero.t2s.modes.examples; import aero.t2s.modes.ModeS; +import aero.t2s.modes.decoder.df.DF20; +import aero.t2s.modes.decoder.df.DF21; + +import java.util.Timer; +import java.util.TimerTask; 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 + "192.168.178.190", // Host IP where the Dump1090 server is running 30002, // The port with raw output (default 30002) 51, // Decimal latitude - 0 // Decimal longitude + 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())); +// 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/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..b106451 --- /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}"; + } +} From 3df60ae6eb050e72f9e8373583c644898c2064c6 Mon Sep 17 00:00:00 2001 From: Ken Andries Date: Sat, 2 Apr 2022 12:11:18 +0200 Subject: [PATCH 02/39] Fix incorrect BDS60 detection When both IRS and Baro ROCD are available and the difference is greater than 500fpm the message should be considered invalid. this reduces mutli match detection. During testing with same raw ADS data log following reducion in multi matches was detected: Before fix Number of multiple matches 83811 / 5687090 (1,5 %) After fix Number of multiple matches 80824 / 5687090 (1,4 %) --- src/main/java/aero/t2s/modes/decoder/df/bds/Bds60.java | 7 +++++++ 1 file changed, 7 insertions(+) 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..9a15de0 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 @@ -116,6 +116,13 @@ public Bds60(short[] data) { } } } + + if (statusBaroRocd && statusIrsRocd) { + if (Math.abs(irsRocd - baroRocd) > 500) { + invalidate(); + return; + } + } } private double machToCas(double altitude) { From 8b973edacba5835c4129503103d4dcdd386f2ad2 Mon Sep 17 00:00:00 2001 From: Ken Andries Date: Sat, 2 Apr 2022 12:52:55 +0200 Subject: [PATCH 03/39] Reduce BDS17 / BDS45 mutliple matches Add extra assumtions on BDS 45 data to improve detection between BDS 17 and BDS 45 During testing following reduction was observed out of ~80,000 mutliple matches 24813 were BDS 17, BDS 45 multiple matches After this changes this went down to ~56,124 or 24700 less mtatches with only 132 multiple match detecions on Bds 17, Bds 45 --- .../aero/t2s/modes/decoder/df/bds/Bds45.java | 24 +++++++++++++++++++ 1 file changed, 24 insertions(+) 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..b824a4d 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,28 @@ 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; + } + } } From 0df89fdc68fcddb0f684fb528843f3273b6d1cf3 Mon Sep 17 00:00:00 2001 From: Ken Andries Date: Sat, 2 Apr 2022 14:15:01 +0200 Subject: [PATCH 04/39] Determine difference between Bds50, Bds60 matches Check the data of both messages to determine the likelyhood of the message being Bds50 or Bds60 --- .../aero/t2s/modes/decoder/df/bds/Bds.java | 4 +++ .../aero/t2s/modes/decoder/df/bds/Bds50.java | 36 ++++++++++++++++++- .../t2s/modes/decoder/df/bds/BdsDecoder.java | 12 +++++++ .../t2s/modes/decoder/df/bds/Bds50Test.java | 8 ++--- 4 files changed, 55 insertions(+), 5 deletions(-) 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/Bds50.java b/src/main/java/aero/t2s/modes/decoder/df/bds/Bds50.java index cf23e53..a8e50e9 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 @@ -150,7 +152,7 @@ public Bds50(short[] data) { return; } if (statusRollAngle) { - if (Math.abs(rollAngle) > 50) { + if (Math.abs(rollAngle) > 32) { invalidate(); rollAngle = 0; return; @@ -203,6 +205,38 @@ public Bds50(short[] data) { } } + 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 public void apply(Track track) { if (statusRollAngle) 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/test/java/aero/t2s/modes/decoder/df/bds/Bds50Test.java b/src/test/java/aero/t2s/modes/decoder/df/bds/Bds50Test.java index 5948577..e6c8940 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 @@ -43,8 +43,8 @@ public void it_decodes_bds50_roll_angle() 0b00000000, }); - assertTrue(bds.isValid()); - assertEquals(45, bds.getRollAngle()); + assertFalse(bds.isValid()); + assertEquals(0, bds.getRollAngle()); assertEquals(0, bds.getTrueTrack()); assertEquals(0, bds.getGs()); assertEquals(0, bds.getTrackAngleRate()); @@ -61,8 +61,8 @@ public void it_decodes_bds50_roll_angle() 0b00000000, }); - assertTrue(bds.isValid()); - assertEquals(-45, bds.getRollAngle()); + assertFalse(bds.isValid()); + assertEquals(0, bds.getRollAngle()); assertEquals(0, bds.getTrueTrack()); assertEquals(0, bds.getGs()); assertEquals(0, bds.getTrackAngleRate()); From 11deb667d03ce763e2ac062e762233af92b1b2a8 Mon Sep 17 00:00:00 2001 From: Jan Henke Date: Tue, 6 Sep 2022 10:46:15 +0700 Subject: [PATCH 05/39] Update Gradle wrapper to 7.5.1 --- build.gradle | 2 +- gradle/wrapper/gradle-wrapper.jar | Bin 58910 -> 60756 bytes gradle/wrapper/gradle-wrapper.properties | 2 +- gradlew | 263 ++++++++++++++--------- gradlew.bat | 33 +-- 5 files changed, 171 insertions(+), 129 deletions(-) diff --git a/build.gradle b/build.gradle index 6e32cbf..0ca1440 100644 --- a/build.gradle +++ b/build.gradle @@ -12,7 +12,7 @@ apply plugin: 'java-library' apply plugin: "com.vanniktech.maven.publish" 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' diff --git a/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar index 62d4c053550b91381bbd28b1afc82d634bf73a8a..249e5832f090a2944b7473328c07c9755baa3196 100644 GIT binary patch delta 21827 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 D86?w= diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index 6c9a224..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.6-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 From 6f797e7089d6f3c02b1b667a03ca270b5e687431 Mon Sep 17 00:00:00 2001 From: Jan Henke Date: Tue, 6 Sep 2022 10:52:17 +0700 Subject: [PATCH 06/39] Improve Java version handling Setting the release option on the Java compile task ensures not only the right version of the class files, but also the correct bootstrap classpath. --- build.gradle | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/build.gradle b/build.gradle index 0ca1440..eebc1ee 100644 --- a/build.gradle +++ b/build.gradle @@ -11,6 +11,10 @@ apply plugin: 'idea' apply plugin: 'java-library' apply plugin: "com.vanniktech.maven.publish" +def javaTargetVersion = 11 + +sourceCompatibility = javaTargetVersion + dependencies { api 'org.slf4j:slf4j-api:1.7.32' @@ -24,9 +28,8 @@ repositories { mavenLocal() } -compileJava { - sourceCompatibility = 11 - targetCompatibility = 11 +tasks.withType(JavaCompile.class) { + options.release = javaTargetVersion } test { From c9527fe8f000f816b634783f5770f5e81aa079ae Mon Sep 17 00:00:00 2001 From: Jan Henke Date: Tue, 6 Sep 2022 12:28:42 +0700 Subject: [PATCH 07/39] Add examples as subproject & move StdOutExample to own example subproject --- build.gradle | 6 + example/.gitignore | 4 - example/gradle/wrapper/gradle-wrapper.jar | Bin 58910 -> 0 bytes .../gradle/wrapper/gradle-wrapper.properties | 5 - example/gradlew | 185 ------------------ example/gradlew.bat | 104 ---------- {example => examples/gui}/build.gradle | 2 +- .../gui}/src/main/java/example/Demo.java | 16 +- .../src/main/java/example/FlightsTable.java | 6 +- .../main/java/example/flight/FlightFrame.form | 0 .../main/java/example/flight/FlightFrame.java | 9 +- examples/stdout/build.gradle | 12 ++ .../t2s/modes/examples/StdOutExample.java | 6 +- settings.gradle | 1 + 14 files changed, 38 insertions(+), 318 deletions(-) delete mode 100644 example/.gitignore delete mode 100644 example/gradle/wrapper/gradle-wrapper.jar delete mode 100644 example/gradle/wrapper/gradle-wrapper.properties delete mode 100755 example/gradlew delete mode 100644 example/gradlew.bat rename {example => examples/gui}/build.gradle (75%) rename {example => examples/gui}/src/main/java/example/Demo.java (100%) rename {example => examples/gui}/src/main/java/example/FlightsTable.java (100%) rename {example => examples/gui}/src/main/java/example/flight/FlightFrame.form (100%) rename {example => examples/gui}/src/main/java/example/flight/FlightFrame.java (99%) create mode 100644 examples/stdout/build.gradle rename {src => examples/stdout/src}/main/java/aero/t2s/modes/examples/StdOutExample.java (100%) create mode 100644 settings.gradle diff --git a/build.gradle b/build.gradle index eebc1ee..658e4b9 100644 --- a/build.gradle +++ b/build.gradle @@ -15,6 +15,12 @@ def javaTargetVersion = 11 sourceCompatibility = javaTargetVersion +subprojects { + tasks.withType(JavaCompile.class) { + options.release = javaTargetVersion + } +} + dependencies { api 'org.slf4j:slf4j-api:1.7.32' diff --git a/example/.gitignore b/example/.gitignore deleted file mode 100644 index dd40b00..0000000 --- a/example/.gitignore +++ /dev/null @@ -1,4 +0,0 @@ -build/ -.gradle/ -out/ - diff --git a/example/gradle/wrapper/gradle-wrapper.jar b/example/gradle/wrapper/gradle-wrapper.jar deleted file mode 100644 index 62d4c053550b91381bbd28b1afc82d634bf73a8a..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 58910 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 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 91970fd..89bb44a 100644 --- a/example/build.gradle +++ b/examples/gui/build.gradle @@ -8,5 +8,5 @@ repositories { } dependencies { - compile ('aero.t2s:mode-s:0.2.5-SNAPSHOT') + implementation project(':') } diff --git a/example/src/main/java/example/Demo.java b/examples/gui/src/main/java/example/Demo.java similarity index 100% rename from example/src/main/java/example/Demo.java rename to examples/gui/src/main/java/example/Demo.java index 71f82c1..6ded03b 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: diff --git a/example/src/main/java/example/FlightsTable.java b/examples/gui/src/main/java/example/FlightsTable.java similarity index 100% rename from example/src/main/java/example/FlightsTable.java rename to examples/gui/src/main/java/example/FlightsTable.java index e85c0a3..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 = { diff --git a/example/src/main/java/example/flight/FlightFrame.form b/examples/gui/src/main/java/example/flight/FlightFrame.form similarity index 100% rename from example/src/main/java/example/flight/FlightFrame.form rename to examples/gui/src/main/java/example/flight/FlightFrame.form diff --git a/example/src/main/java/example/flight/FlightFrame.java b/examples/gui/src/main/java/example/flight/FlightFrame.java similarity index 99% rename from example/src/main/java/example/flight/FlightFrame.java rename to examples/gui/src/main/java/example/flight/FlightFrame.java index b3c0cb2..5723f2d 100644 --- a/example/src/main/java/example/flight/FlightFrame.java +++ b/examples/gui/src/main/java/example/flight/FlightFrame.java @@ -1,12 +1,11 @@ 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(); 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/src/main/java/aero/t2s/modes/examples/StdOutExample.java b/examples/stdout/src/main/java/aero/t2s/modes/examples/StdOutExample.java similarity index 100% rename from src/main/java/aero/t2s/modes/examples/StdOutExample.java rename to examples/stdout/src/main/java/aero/t2s/modes/examples/StdOutExample.java index e47d028..336e666 100644 --- a/src/main/java/aero/t2s/modes/examples/StdOutExample.java +++ b/examples/stdout/src/main/java/aero/t2s/modes/examples/StdOutExample.java @@ -1,12 +1,12 @@ 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; -import java.util.Timer; -import java.util.TimerTask; - public class StdOutExample { public static void main(String[] args) { ModeS modes = new ModeS( 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') From f59acfb31b8ec6ff1a24872c2e9cfcac653eae4d Mon Sep 17 00:00:00 2001 From: Ken Andries Date: Thu, 6 Oct 2022 21:50:47 +0200 Subject: [PATCH 08/39] Add Aircraft object ot DF class when available --- src/main/java/aero/t2s/modes/decoder/Decoder.java | 2 +- .../aero/t2s/modes/decoder/df/DownlinkFormat.java | 12 ++++++++++++ 2 files changed, 13 insertions(+), 1 deletion(-) diff --git a/src/main/java/aero/t2s/modes/decoder/Decoder.java b/src/main/java/aero/t2s/modes/decoder/Decoder.java index 5b563f6..ab2792c 100644 --- a/src/main/java/aero/t2s/modes/decoder/Decoder.java +++ b/src/main/java/aero/t2s/modes/decoder/Decoder.java @@ -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/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, From f2f3e632629043f0b6b1b9449c6550b686953e1c Mon Sep 17 00:00:00 2001 From: Ken Andries Date: Thu, 6 Oct 2022 22:11:39 +0200 Subject: [PATCH 09/39] Improve BDS60 decoding real messages can contain larger gaps in IRS / Baro ROCD --- .../aero/t2s/modes/decoder/df/bds/Bds60.java | 2 +- .../modes/decoder/df/DfRealMessageTest.java | 43 +++++++++++++++++++ .../aero/t2s/modes/decoder/df/DfTEst.java | 22 ---------- 3 files changed, 44 insertions(+), 23 deletions(-) create mode 100644 src/test/java/aero/t2s/modes/decoder/df/DfRealMessageTest.java delete mode 100644 src/test/java/aero/t2s/modes/decoder/df/DfTEst.java 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 9a15de0..6991209 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 @@ -118,7 +118,7 @@ public Bds60(short[] data) { } if (statusBaroRocd && statusIrsRocd) { - if (Math.abs(irsRocd - baroRocd) > 500) { + if (Math.abs(irsRocd - baroRocd) > 700) { invalidate(); return; } 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..7689c3c --- /dev/null +++ b/src/test/java/aero/t2s/modes/decoder/df/DfRealMessageTest.java @@ -0,0 +1,43 @@ +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 aero.t2s.modes.decoder.df.bds.Bds60; +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()); + } + + 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()); - } -} From 30b6ea88178d9a10d1d7de786eeebc4f47c869a7 Mon Sep 17 00:00:00 2001 From: Ken Andries Date: Thu, 6 Oct 2022 22:14:06 +0200 Subject: [PATCH 10/39] Fix javadoc with US english encoding --- src/main/java/aero/t2s/modes/decoder/df/bds/Bds40.java | 4 ++-- src/main/java/aero/t2s/modes/registers/Register09.java | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) 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..ed7019b 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). *

* diff --git a/src/main/java/aero/t2s/modes/registers/Register09.java b/src/main/java/aero/t2s/modes/registers/Register09.java index b106451..6bf83d6 100644 --- a/src/main/java/aero/t2s/modes/registers/Register09.java +++ b/src/main/java/aero/t2s/modes/registers/Register09.java @@ -10,7 +10,7 @@ public class Register09 extends Register { /** * 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. + * The code shall remain set for 18 +/-1 second following an intent change. */ private boolean intentChangeFlag; private int heading = 0; From 518dd6bf25fc8a60b236b26011b42d03ee96541b Mon Sep 17 00:00:00 2001 From: Ken Andries Date: Fri, 7 Oct 2022 00:13:31 +0200 Subject: [PATCH 11/39] Improves high altitude flight detection of BDS40 data --- .../aero/t2s/modes/decoder/df/bds/Bds40.java | 4 +-- .../modes/decoder/df/DfRealMessageTest.java | 31 +++++++++++++++++++ 2 files changed, 33 insertions(+), 2 deletions(-) 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 ed7019b..a680d48 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 @@ -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/test/java/aero/t2s/modes/decoder/df/DfRealMessageTest.java b/src/test/java/aero/t2s/modes/decoder/df/DfRealMessageTest.java index 7689c3c..163d011 100644 --- a/src/test/java/aero/t2s/modes/decoder/df/DfRealMessageTest.java +++ b/src/test/java/aero/t2s/modes/decoder/df/DfRealMessageTest.java @@ -1,9 +1,12 @@ package aero.t2s.modes.decoder.df; import aero.t2s.modes.BinaryHelper; +import aero.t2s.modes.constants.SelectedAltitudeSource; 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.Bds40; +import aero.t2s.modes.decoder.df.bds.Bds50; import aero.t2s.modes.decoder.df.bds.Bds60; import org.junit.jupiter.api.Test; @@ -35,6 +38,34 @@ public void test_bds60() throws UnknownDownlinkFormatException { assertEquals(32, bds.getIrsRocd()); } + @Test + public void test() throws UnknownDownlinkFormatException { +// DownlinkFormat df = testMessage("5D4CA64"); + 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()); + } + private DownlinkFormat testMessage(String message) throws UnknownDownlinkFormatException { Decoder decoder = new Decoder(new HashMap<>(), 50, 2, ModeSDatabase.createDatabase()); From f56a7ef16cd1051e261ef9ab71e138b3a90743a6 Mon Sep 17 00:00:00 2001 From: Ken Andries Date: Fri, 7 Oct 2022 00:20:15 +0200 Subject: [PATCH 12/39] Update docs --- src/main/java/aero/t2s/modes/decoder/df/bds/Bds40.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) 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 a680d48..7e19c66 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 @@ -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,7 +54,7 @@ * 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

* From beea9e0464b5d793718f0551a38ede58b3c07d93 Mon Sep 17 00:00:00 2001 From: Ken Andries Date: Fri, 7 Oct 2022 00:21:35 +0200 Subject: [PATCH 13/39] Fix typo's --- src/main/java/aero/t2s/modes/decoder/df/bds/Bds40.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) 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 7e19c66..17e962e 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 @@ -58,9 +58,9 @@ * *

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

*

From 9d59086df417674b1b3bb49dda716f38b5fddbbe Mon Sep 17 00:00:00 2001 From: Arno Stalpaert Date: Sat, 8 Oct 2022 15:51:20 +0200 Subject: [PATCH 14/39] =?UTF-8?q?=F0=9F=92=84Improve=20readme?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 42 +++++++++++++++++++++--------------------- 1 file changed, 21 insertions(+), 21 deletions(-) diff --git a/README.md b/README.md index 257d828..0015a77 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 | ❌ | Not supported at this stage | ## DF17/DF18 - Extended Squitter @@ -68,7 +68,7 @@ Most features of the DF17/18 protocol have been implemented, some message lack s 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,7 +96,7 @@ 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 | @@ -146,7 +146,7 @@ We hope with more BDS implemented the guessing accuracy will improve. # Installation -This package is available through maven central +This package is available through Maven Central Pom ```xml @@ -159,7 +159,7 @@ Pom Gradle ``` - compile('aero.t2s:mode-s:0.2.0-SNAPSHOT') + compile('aero.t2s:mode-s:0.2.0-SNAPSHOT') ``` # Usage @@ -191,9 +191,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 +222,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). From 08a42c8d8524912ee5c72d2c6057e90685d712c3 Mon Sep 17 00:00:00 2001 From: Ken Andries Date: Sun, 9 Oct 2022 11:22:43 +0200 Subject: [PATCH 15/39] Allow greater diff between baro and IRS ROCD --- .../aero/t2s/modes/decoder/df/bds/Bds60.java | 2 +- .../modes/decoder/df/DfRealMessageTest.java | 31 +++++++++++++++++-- 2 files changed, 30 insertions(+), 3 deletions(-) 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 6991209..b3a1672 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 @@ -118,7 +118,7 @@ public Bds60(short[] data) { } if (statusBaroRocd && statusIrsRocd) { - if (Math.abs(irsRocd - baroRocd) > 700) { + if (Math.abs(irsRocd - baroRocd) > 1500) { invalidate(); return; } diff --git a/src/test/java/aero/t2s/modes/decoder/df/DfRealMessageTest.java b/src/test/java/aero/t2s/modes/decoder/df/DfRealMessageTest.java index 163d011..d09d5e8 100644 --- a/src/test/java/aero/t2s/modes/decoder/df/DfRealMessageTest.java +++ b/src/test/java/aero/t2s/modes/decoder/df/DfRealMessageTest.java @@ -39,8 +39,7 @@ public void test_bds60() throws UnknownDownlinkFormatException { } @Test - public void test() throws UnknownDownlinkFormatException { -// DownlinkFormat df = testMessage("5D4CA64"); + public void test_df_20_bds_40_a48e35() throws UnknownDownlinkFormatException { DownlinkFormat df = testMessage("A00006A1E3A71D30AA014672C8DF"); assertInstanceOf(DF20.class, df); @@ -66,6 +65,34 @@ public void test() throws UnknownDownlinkFormatException { 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("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()); + } + private DownlinkFormat testMessage(String message) throws UnknownDownlinkFormatException { Decoder decoder = new Decoder(new HashMap<>(), 50, 2, ModeSDatabase.createDatabase()); From f805f47125c83ea6cd7408a9f1fafe3206bcd12f Mon Sep 17 00:00:00 2001 From: Ken Andries Date: Sun, 9 Oct 2022 11:36:08 +0200 Subject: [PATCH 16/39] Update worklofw & dependencies --- .github/workflows/publish-release.yml | 14 +++++--------- .github/workflows/publish-snapshot.yml | 11 +++++++---- build.gradle | 8 +++++++- 3 files changed, 19 insertions(+), 14 deletions(-) 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/build.gradle b/build.gradle index 658e4b9..b6a5e66 100644 --- a/build.gradle +++ b/build.gradle @@ -3,7 +3,7 @@ buildscript { mavenCentral() } dependencies { - classpath 'com.vanniktech:gradle-maven-publish-plugin:0.18.0' + classpath 'com.vanniktech:gradle-maven-publish-plugin:0.22.0' } } @@ -42,6 +42,12 @@ test { useJUnitPlatform() } + +mavenPublishing { + publishToMavenCentral() + signAllPublications() +} + signing { if (hasProperty('SIGNING_PRIVATE_KEY')) { useInMemoryPgpKeys(SIGNING_PRIVATE_KEY, "") From 63dc9c1fb8c010458651ad775b1b29c1c2c6d911 Mon Sep 17 00:00:00 2001 From: Ken Andries Date: Sun, 9 Oct 2022 11:43:56 +0200 Subject: [PATCH 17/39] Fix tests --- .../modes/decoder/df/DfRealMessageTest.java | 30 +++++++++---------- 1 file changed, 14 insertions(+), 16 deletions(-) diff --git a/src/test/java/aero/t2s/modes/decoder/df/DfRealMessageTest.java b/src/test/java/aero/t2s/modes/decoder/df/DfRealMessageTest.java index d09d5e8..f36df3f 100644 --- a/src/test/java/aero/t2s/modes/decoder/df/DfRealMessageTest.java +++ b/src/test/java/aero/t2s/modes/decoder/df/DfRealMessageTest.java @@ -72,25 +72,23 @@ public void test_df20_bds60_800736() throws UnknownDownlinkFormatException { assertInstanceOf(DF20.class, df); DF20 df20 = (DF20) df; - assertEquals("A48E35", df.getIcao()); - assertEquals(51000, df20.getAltitude().getAltitude()); + assertEquals("800736", df.getIcao()); + assertEquals(41375, df20.getAltitude().getAltitude()); assertTrue(df20.isValid()); - assertInstanceOf(Bds40.class, df20.getBds()); + assertInstanceOf(Bds60.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()); + 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); } private DownlinkFormat testMessage(String message) throws UnknownDownlinkFormatException { From 64df1abeac6d364d24c4c60b26dd13033f8a94db Mon Sep 17 00:00:00 2001 From: Ken Andries Date: Sun, 9 Oct 2022 13:59:24 +0200 Subject: [PATCH 18/39] When BDS60 receives IRS ROCD as all 1's conver them to 0 as either all 0 or all 1 = 0fpm --- .../aero/t2s/modes/decoder/df/bds/Bds60.java | 2 +- .../modes/decoder/df/DfRealMessageTest.java | 25 +++++++++++++++++++ 2 files changed, 26 insertions(+), 1 deletion(-) 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 b3a1672..c8d1645 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 @@ -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(); diff --git a/src/test/java/aero/t2s/modes/decoder/df/DfRealMessageTest.java b/src/test/java/aero/t2s/modes/decoder/df/DfRealMessageTest.java index f36df3f..d996be6 100644 --- a/src/test/java/aero/t2s/modes/decoder/df/DfRealMessageTest.java +++ b/src/test/java/aero/t2s/modes/decoder/df/DfRealMessageTest.java @@ -91,6 +91,31 @@ public void test_df20_bds60_800736() throws UnknownDownlinkFormatException { 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); + } + private DownlinkFormat testMessage(String message) throws UnknownDownlinkFormatException { Decoder decoder = new Decoder(new HashMap<>(), 50, 2, ModeSDatabase.createDatabase()); From 183509d5a626686d6ef695b93bbb498f4569ebe2 Mon Sep 17 00:00:00 2001 From: Ken Andries Date: Mon, 10 Oct 2022 20:06:03 +0200 Subject: [PATCH 19/39] When all bits are set to 1 on baro ROCD, value is 0 --- .../aero/t2s/modes/decoder/df/bds/Bds60.java | 2 +- .../modes/decoder/df/DfRealMessageTest.java | 25 +++++++++++++++++++ 2 files changed, 26 insertions(+), 1 deletion(-) 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 c8d1645..7783d29 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(); diff --git a/src/test/java/aero/t2s/modes/decoder/df/DfRealMessageTest.java b/src/test/java/aero/t2s/modes/decoder/df/DfRealMessageTest.java index d996be6..785274e 100644 --- a/src/test/java/aero/t2s/modes/decoder/df/DfRealMessageTest.java +++ b/src/test/java/aero/t2s/modes/decoder/df/DfRealMessageTest.java @@ -116,6 +116,31 @@ public void test_df21_bds60_4CA708() throws UnknownDownlinkFormatException { 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); + } + private DownlinkFormat testMessage(String message) throws UnknownDownlinkFormatException { Decoder decoder = new Decoder(new HashMap<>(), 50, 2, ModeSDatabase.createDatabase()); From 88c1c56e6865403bb82597251bd1a1faf122cb65 Mon Sep 17 00:00:00 2001 From: Ken Andries Date: Mon, 10 Oct 2022 20:21:56 +0200 Subject: [PATCH 20/39] Remove IRS/INS vs Baro ROCD check improves detection changes At the cost of multi match. Since these are valid packets we should aim at reducing flase postives instead of rejecting correct messages --- .../aero/t2s/modes/decoder/df/bds/Bds60.java | 7 ------ .../modes/decoder/df/DfRealMessageTest.java | 25 +++++++++++++++++++ 2 files changed, 25 insertions(+), 7 deletions(-) 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 7783d29..482c12f 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 @@ -116,13 +116,6 @@ public Bds60(short[] data) { } } } - - if (statusBaroRocd && statusIrsRocd) { - if (Math.abs(irsRocd - baroRocd) > 1500) { - invalidate(); - return; - } - } } private double machToCas(double altitude) { diff --git a/src/test/java/aero/t2s/modes/decoder/df/DfRealMessageTest.java b/src/test/java/aero/t2s/modes/decoder/df/DfRealMessageTest.java index 785274e..7784c96 100644 --- a/src/test/java/aero/t2s/modes/decoder/df/DfRealMessageTest.java +++ b/src/test/java/aero/t2s/modes/decoder/df/DfRealMessageTest.java @@ -141,6 +141,31 @@ public void test_df21_bds60_00000() throws UnknownDownlinkFormatException { 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); + } + private DownlinkFormat testMessage(String message) throws UnknownDownlinkFormatException { Decoder decoder = new Decoder(new HashMap<>(), 50, 2, ModeSDatabase.createDatabase()); From 68d6bc1a0d8c050a2643f7cc4a2823630fd647e2 Mon Sep 17 00:00:00 2001 From: Ken Andries Date: Mon, 10 Oct 2022 20:35:08 +0200 Subject: [PATCH 21/39] Allow BDS 50 roll angle up to 50 degrees Other libraries use a similar value. This reduces number of undecodable message, but it will increase false positives in multi matches. --- .../aero/t2s/modes/decoder/df/bds/Bds50.java | 2 +- .../modes/decoder/df/DfRealMessageTest.java | 26 +++++++++++++++++++ 2 files changed, 27 insertions(+), 1 deletion(-) 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 a8e50e9..2129801 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 @@ -152,7 +152,7 @@ public Bds50(short[] data) { return; } if (statusRollAngle) { - if (Math.abs(rollAngle) > 32) { + if (Math.abs(rollAngle) > 50) { invalidate(); rollAngle = 0; return; diff --git a/src/test/java/aero/t2s/modes/decoder/df/DfRealMessageTest.java b/src/test/java/aero/t2s/modes/decoder/df/DfRealMessageTest.java index 7784c96..a096006 100644 --- a/src/test/java/aero/t2s/modes/decoder/df/DfRealMessageTest.java +++ b/src/test/java/aero/t2s/modes/decoder/df/DfRealMessageTest.java @@ -166,6 +166,32 @@ public void test_df21_bds60_406ECD() throws UnknownDownlinkFormatException { assertEquals(104.7, bds.getMagneticHeading(), 0.1); } + + @Test + public void test_df21_bds50_406ECD() 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); + } + private DownlinkFormat testMessage(String message) throws UnknownDownlinkFormatException { Decoder decoder = new Decoder(new HashMap<>(), 50, 2, ModeSDatabase.createDatabase()); From 8967609002cb8cc423d49d174bfb7326a8650c47 Mon Sep 17 00:00:00 2001 From: Ken Andries Date: Mon, 10 Oct 2022 21:21:04 +0200 Subject: [PATCH 22/39] Add DF24 decoding --- README.md | 170 +++++++++--------- .../java/aero/t2s/modes/decoder/df/DF24.java | 13 +- .../aero/t2s/modes/decoder/df/DF24Test.java | 18 ++ .../modes/decoder/df/DfRealMessageTest.java | 2 +- 4 files changed, 113 insertions(+), 90 deletions(-) create mode 100644 src/test/java/aero/t2s/modes/decoder/df/DF24Test.java diff --git a/README.md b/README.md index 0015a77..b5db560 100644 --- a/README.md +++ b/README.md @@ -7,17 +7,17 @@ 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 | +| 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 | +| 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 | ❌ | Not supported at this stage | +| DF24 | Comm-B ELM | ⚠️ | Implemented basic sequence no decoding. | ## DF17/DF18 - Extended Squitter @@ -27,41 +27,40 @@ 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 | ❌ | 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 | ## DF20/21 Comm-B @@ -99,50 +98,49 @@ At this moment the coded logic has too many incorrect matches and thus decided t 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 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/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 index a096006..c131e3f 100644 --- a/src/test/java/aero/t2s/modes/decoder/df/DfRealMessageTest.java +++ b/src/test/java/aero/t2s/modes/decoder/df/DfRealMessageTest.java @@ -168,7 +168,7 @@ public void test_df21_bds60_406ECD() throws UnknownDownlinkFormatException { @Test - public void test_df21_bds50_406ECD() throws UnknownDownlinkFormatException { + public void test_df21_bds50_44CD73() throws UnknownDownlinkFormatException { DownlinkFormat df = testMessage("A0000333E17987192250004423EC"); assertInstanceOf(DF20.class, df); From c54c450ef0787c098b6f1fd7c0c951e2b0569b99 Mon Sep 17 00:00:00 2001 From: Ken Andries Date: Mon, 10 Oct 2022 21:23:43 +0200 Subject: [PATCH 23/39] Remove invalid test (no longer required) --- .../t2s/modes/decoder/df/bds/Bds50Test.java | 40 ------------------- 1 file changed, 40 deletions(-) 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 e6c8940..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, - }); - - assertFalse(bds.isValid()); - assertEquals(0, 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, - }); - - assertFalse(bds.isValid()); - assertEquals(0, 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() { From c6daacd48924915c54d036848483e800a193d48d Mon Sep 17 00:00:00 2001 From: Ken Andries Date: Wed, 12 Oct 2022 20:44:56 +0200 Subject: [PATCH 24/39] Fix Altitude decoding --- src/main/java/aero/t2s/modes/decoder/AltitudeEncoding.java | 3 ++- src/test/java/aero/t2s/modes/decoder/df/DF0Test.java | 2 +- src/test/java/aero/t2s/modes/decoder/df/DfRealMessageTest.java | 2 +- 3 files changed, 4 insertions(+), 3 deletions(-) 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/test/java/aero/t2s/modes/decoder/df/DF0Test.java b/src/test/java/aero/t2s/modes/decoder/df/DF0Test.java index 3bfbba2..7d6d752 100644 --- a/src/test/java/aero/t2s/modes/decoder/df/DF0Test.java +++ b/src/test/java/aero/t2s/modes/decoder/df/DF0Test.java @@ -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/DfRealMessageTest.java b/src/test/java/aero/t2s/modes/decoder/df/DfRealMessageTest.java index c131e3f..9962111 100644 --- a/src/test/java/aero/t2s/modes/decoder/df/DfRealMessageTest.java +++ b/src/test/java/aero/t2s/modes/decoder/df/DfRealMessageTest.java @@ -73,7 +73,7 @@ public void test_df20_bds60_800736() throws UnknownDownlinkFormatException { assertInstanceOf(DF20.class, df); DF20 df20 = (DF20) df; assertEquals("800736", df.getIcao()); - assertEquals(41375, df20.getAltitude().getAltitude()); + assertEquals(40975, df20.getAltitude().getAltitude()); assertTrue(df20.isValid()); assertInstanceOf(Bds60.class, df20.getBds()); From 83bd0c220ebb097531f681cbc6d8279d85b0d347 Mon Sep 17 00:00:00 2001 From: Ken Andries Date: Wed, 12 Oct 2022 20:45:23 +0200 Subject: [PATCH 25/39] Improve BDS40 detection by allowing empty FMS altitude if status is available --- .../aero/t2s/modes/decoder/df/bds/Bds40.java | 2 +- .../modes/decoder/df/DfRealMessageTest.java | 27 +++++++++++++++++++ 2 files changed, 28 insertions(+), 1 deletion(-) 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 17e962e..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 @@ -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 > 52000) { + if (fmsAltitude < 0 || fmsAltitude > 52000) { invalidate(); return; } diff --git a/src/test/java/aero/t2s/modes/decoder/df/DfRealMessageTest.java b/src/test/java/aero/t2s/modes/decoder/df/DfRealMessageTest.java index 9962111..666ba5b 100644 --- a/src/test/java/aero/t2s/modes/decoder/df/DfRealMessageTest.java +++ b/src/test/java/aero/t2s/modes/decoder/df/DfRealMessageTest.java @@ -192,6 +192,33 @@ public void test_df21_bds50_44CD73() throws UnknownDownlinkFormatException { 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()); + } + private DownlinkFormat testMessage(String message) throws UnknownDownlinkFormatException { Decoder decoder = new Decoder(new HashMap<>(), 50, 2, ModeSDatabase.createDatabase()); From fb0b5df3663a46fbc2b83e94b24feeacb2c8a5a2 Mon Sep 17 00:00:00 2001 From: Ken Andries Date: Thu, 13 Oct 2022 19:50:34 +0200 Subject: [PATCH 26/39] Skip BSD44 unless wind information is given to reduce multi match --- .../aero/t2s/modes/decoder/df/bds/Bds44.java | 10 ++++++++-- .../modes/decoder/df/DfRealMessageTest.java | 19 +++++++++++++++++++ 2 files changed, 27 insertions(+), 2 deletions(-) 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/test/java/aero/t2s/modes/decoder/df/DfRealMessageTest.java b/src/test/java/aero/t2s/modes/decoder/df/DfRealMessageTest.java index 666ba5b..4e32292 100644 --- a/src/test/java/aero/t2s/modes/decoder/df/DfRealMessageTest.java +++ b/src/test/java/aero/t2s/modes/decoder/df/DfRealMessageTest.java @@ -5,6 +5,7 @@ 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.Bds10; import aero.t2s.modes.decoder.df.bds.Bds40; import aero.t2s.modes.decoder.df.bds.Bds50; import aero.t2s.modes.decoder.df.bds.Bds60; @@ -219,6 +220,24 @@ public void test_df21_bds40_407776() throws UnknownDownlinkFormatException { 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(); + } + + + private DownlinkFormat testMessage(String message) throws UnknownDownlinkFormatException { Decoder decoder = new Decoder(new HashMap<>(), 50, 2, ModeSDatabase.createDatabase()); From 5fea2a48f0635b92da6941b2f3be670313a9b6c0 Mon Sep 17 00:00:00 2001 From: Ken Andries Date: Thu, 13 Oct 2022 20:47:52 +0200 Subject: [PATCH 27/39] Check if turn rate is within expected range --- .../aero/t2s/modes/decoder/df/bds/Bds50.java | 19 +++++++++++++ .../modes/decoder/df/DfRealMessageTest.java | 28 +++++++++++++++++++ 2 files changed, 47 insertions(+) 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 2129801..35c3b19 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 @@ -203,6 +203,25 @@ public Bds50(short[] data) { invalidate(); return; } + + // If known check if roll angle & track angle rate matches expected / value + // Formula from SkyBrary TurnRate (1) = (TAS / 10) => Roll Angle + // Which we can rewrite to Roll Angle * 10 * Turn Rate = TAS + // When TAS is not known we can use GS instead allow for bigger margin + if (statusTrackAngle && statusRollAngle) { + double expectedTAS = Math.abs(rollAngle * 10d * (trackAngleRate / 3d)); + if (statusTas) { + if (Math.abs(expectedTAS - tas) > 50) { + invalidate(); + return; + } + } else if (statusGs) { + if (Math.abs(expectedTAS - gs) > 150) { + invalidate(); + return; + } + } + } } public Bds compareWithBds60(Bds60 bds60) { diff --git a/src/test/java/aero/t2s/modes/decoder/df/DfRealMessageTest.java b/src/test/java/aero/t2s/modes/decoder/df/DfRealMessageTest.java index 4e32292..65c2eed 100644 --- a/src/test/java/aero/t2s/modes/decoder/df/DfRealMessageTest.java +++ b/src/test/java/aero/t2s/modes/decoder/df/DfRealMessageTest.java @@ -237,6 +237,34 @@ public void test_df21_bds10_407776() throws UnknownDownlinkFormatException { } + @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()); + } + + private DownlinkFormat testMessage(String message) throws UnknownDownlinkFormatException { Decoder decoder = new Decoder(new HashMap<>(), 50, 2, ModeSDatabase.createDatabase()); From f87ebbd23bdb4bd85660e0127b0d1f1b9acd4b91 Mon Sep 17 00:00:00 2001 From: Ken Andries Date: Thu, 13 Oct 2022 21:06:54 +0200 Subject: [PATCH 28/39] Do not check turn angle when aircraft is not turning --- .../aero/t2s/modes/decoder/df/bds/Bds50.java | 2 +- .../modes/decoder/df/DfRealMessageTest.java | 24 +++++++++++++++++++ 2 files changed, 25 insertions(+), 1 deletion(-) 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 35c3b19..f88b2e1 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 @@ -208,7 +208,7 @@ public Bds50(short[] data) { // Formula from SkyBrary TurnRate (1) = (TAS / 10) => Roll Angle // Which we can rewrite to Roll Angle * 10 * Turn Rate = TAS // When TAS is not known we can use GS instead allow for bigger margin - if (statusTrackAngle && statusRollAngle) { + if (statusTrackAngle && statusRollAngle && trackAngleRate != 0) { double expectedTAS = Math.abs(rollAngle * 10d * (trackAngleRate / 3d)); if (statusTas) { if (Math.abs(expectedTAS - tas) > 50) { diff --git a/src/test/java/aero/t2s/modes/decoder/df/DfRealMessageTest.java b/src/test/java/aero/t2s/modes/decoder/df/DfRealMessageTest.java index 65c2eed..ec30df7 100644 --- a/src/test/java/aero/t2s/modes/decoder/df/DfRealMessageTest.java +++ b/src/test/java/aero/t2s/modes/decoder/df/DfRealMessageTest.java @@ -264,6 +264,30 @@ public void test_df21_bds40_4CA6F8() throws UnknownDownlinkFormatException { 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); + } private DownlinkFormat testMessage(String message) throws UnknownDownlinkFormatException { From 0333236914548505e326ea858a032257bb693936 Mon Sep 17 00:00:00 2001 From: Ken Andries Date: Sun, 16 Oct 2022 11:08:03 +0200 Subject: [PATCH 29/39] Check BDS50 on out of range values --- .../aero/t2s/modes/decoder/df/bds/Bds50.java | 23 +++------ .../modes/decoder/df/DfRealMessageTest.java | 50 +++++++++++++++++++ 2 files changed, 57 insertions(+), 16 deletions(-) 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 f88b2e1..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 @@ -204,22 +204,13 @@ public Bds50(short[] data) { return; } - // If known check if roll angle & track angle rate matches expected / value - // Formula from SkyBrary TurnRate (1) = (TAS / 10) => Roll Angle - // Which we can rewrite to Roll Angle * 10 * Turn Rate = TAS - // When TAS is not known we can use GS instead allow for bigger margin - if (statusTrackAngle && statusRollAngle && trackAngleRate != 0) { - double expectedTAS = Math.abs(rollAngle * 10d * (trackAngleRate / 3d)); - if (statusTas) { - if (Math.abs(expectedTAS - tas) > 50) { - invalidate(); - return; - } - } else if (statusGs) { - if (Math.abs(expectedTAS - gs) > 150) { - 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; } } } diff --git a/src/test/java/aero/t2s/modes/decoder/df/DfRealMessageTest.java b/src/test/java/aero/t2s/modes/decoder/df/DfRealMessageTest.java index ec30df7..fa55aa0 100644 --- a/src/test/java/aero/t2s/modes/decoder/df/DfRealMessageTest.java +++ b/src/test/java/aero/t2s/modes/decoder/df/DfRealMessageTest.java @@ -289,6 +289,56 @@ public void test_df21_bds50_485209() throws UnknownDownlinkFormatException { 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); + } + private DownlinkFormat testMessage(String message) throws UnknownDownlinkFormatException { Decoder decoder = new Decoder(new HashMap<>(), 50, 2, ModeSDatabase.createDatabase()); From 9bc8047f65656535a6e45a0626d5d5ebed339baa Mon Sep 17 00:00:00 2001 From: Ken Andries Date: Sun, 16 Oct 2022 11:14:03 +0200 Subject: [PATCH 30/39] Allow messages to start with --- src/main/java/aero/t2s/modes/ModeSHandler.java | 3 --- 1 file changed, 3 deletions(-) diff --git a/src/main/java/aero/t2s/modes/ModeSHandler.java b/src/main/java/aero/t2s/modes/ModeSHandler.java index fc91b4f..aae0391 100644 --- a/src/main/java/aero/t2s/modes/ModeSHandler.java +++ b/src/main/java/aero/t2s/modes/ModeSHandler.java @@ -35,9 +35,6 @@ protected short[] toData(final String input) throws EmptyMessageException, ModeA // example mode A/C: *21D2; *0200; *0101; throw new ModeAcMessageException(); } - if (input.startsWith("*0000")) { - throw new EmptyMessageException(); - } String hex = input.replace("*", "").replace(";", ""); From b433fb2d8b0771bc1efea7db5929d0b25102532c Mon Sep 17 00:00:00 2001 From: Ken Andries Date: Mon, 17 Oct 2022 20:21:00 +0200 Subject: [PATCH 31/39] Add getters on velocity message --- .../df/df17/AirborneVelocityGroundspeed.java | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) 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 57ac261..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 @@ -93,4 +93,20 @@ public void apply(Track track) { .setVy(yVelocity); } } + + public boolean isVxAvailable() { + return xVelocityAvailable; + } + + public int getVx() { + return xVelocity; + } + + public boolean isVyAvailable() { + return yVelocityAvailable; + } + + public int getVy() { + return yVelocity; + } } From cd1b23a1a0f9c1fcaef55f5aa5cda00fbe7771c7 Mon Sep 17 00:00:00 2001 From: Ken Andries Date: Mon, 17 Oct 2022 20:35:11 +0200 Subject: [PATCH 32/39] Remove Cross-Link Capability it is only present on DF0 not on Df16 --- src/main/java/aero/t2s/modes/decoder/df/DF16.java | 7 ------- 1 file changed, 7 deletions(-) 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; } From c08f043a41b08d587778f1dee297696ecf7baae4 Mon Sep 17 00:00:00 2001 From: Ken Andries Date: Mon, 17 Oct 2022 20:48:19 +0200 Subject: [PATCH 33/39] Improve DF16 decoding --- .../modes/constants/AcasReplyInformation.java | 6 ++-- .../aero/t2s/modes/decoder/df/DF0Test.java | 2 +- .../modes/decoder/df/DfRealMessageTest.java | 28 ++++++++++++++++++- 3 files changed, 31 insertions(+), 5 deletions(-) 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/test/java/aero/t2s/modes/decoder/df/DF0Test.java b/src/test/java/aero/t2s/modes/decoder/df/DF0Test.java index 7d6d752..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 diff --git a/src/test/java/aero/t2s/modes/decoder/df/DfRealMessageTest.java b/src/test/java/aero/t2s/modes/decoder/df/DfRealMessageTest.java index fa55aa0..13335ed 100644 --- a/src/test/java/aero/t2s/modes/decoder/df/DfRealMessageTest.java +++ b/src/test/java/aero/t2s/modes/decoder/df/DfRealMessageTest.java @@ -1,7 +1,7 @@ package aero.t2s.modes.decoder.df; import aero.t2s.modes.BinaryHelper; -import aero.t2s.modes.constants.SelectedAltitudeSource; +import aero.t2s.modes.constants.*; import aero.t2s.modes.database.ModeSDatabase; import aero.t2s.modes.decoder.Decoder; import aero.t2s.modes.decoder.UnknownDownlinkFormatException; @@ -339,6 +339,32 @@ public void test_df20_bds50_48418A() throws UnknownDownlinkFormatException { assertEquals(354, bds.getTrueTrack(), 0.1); } + @Test + public void test_df16_bds50_48418A() 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()); + } private DownlinkFormat testMessage(String message) throws UnknownDownlinkFormatException { Decoder decoder = new Decoder(new HashMap<>(), 50, 2, ModeSDatabase.createDatabase()); From e21059f8f20bde8043dbfa26e9623a36a54758f9 Mon Sep 17 00:00:00 2001 From: Ken Andries Date: Fri, 28 Oct 2022 13:19:48 +0200 Subject: [PATCH 34/39] Improve BDS50/60 detection based on inverse rocd on irs/baro --- .../aero/t2s/modes/decoder/df/bds/Bds60.java | 8 ++++++ .../modes/decoder/df/DfRealMessageTest.java | 28 ++++++++++++++++++- 2 files changed, 35 insertions(+), 1 deletion(-) 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 482c12f..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 @@ -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/test/java/aero/t2s/modes/decoder/df/DfRealMessageTest.java b/src/test/java/aero/t2s/modes/decoder/df/DfRealMessageTest.java index 13335ed..fafd41d 100644 --- a/src/test/java/aero/t2s/modes/decoder/df/DfRealMessageTest.java +++ b/src/test/java/aero/t2s/modes/decoder/df/DfRealMessageTest.java @@ -340,7 +340,7 @@ public void test_df20_bds50_48418A() throws UnknownDownlinkFormatException { } @Test - public void test_df16_bds50_48418A() throws UnknownDownlinkFormatException { + public void test_df16_02A198() throws UnknownDownlinkFormatException { DownlinkFormat df = testMessage("80C18819584195384EF8505941FD"); assertInstanceOf(DF16.class, df); @@ -366,6 +366,32 @@ public void test_df16_bds50_48418A() throws UnknownDownlinkFormatException { 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); + } + private DownlinkFormat testMessage(String message) throws UnknownDownlinkFormatException { Decoder decoder = new Decoder(new HashMap<>(), 50, 2, ModeSDatabase.createDatabase()); From 17aab205dbfe7970ee9ceded059be18d26c371a1 Mon Sep 17 00:00:00 2001 From: Ken Andries Date: Fri, 28 Oct 2022 13:59:00 +0200 Subject: [PATCH 35/39] Improve Bds17/Bds45 detection --- .../aero/t2s/modes/decoder/df/bds/Bds45.java | 25 ++++++++++ .../modes/decoder/df/DfRealMessageTest.java | 46 +++++++++++++++++-- 2 files changed, 67 insertions(+), 4 deletions(-) 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 b824a4d..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 @@ -129,6 +129,31 @@ public Bds45(short[] data) { 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/test/java/aero/t2s/modes/decoder/df/DfRealMessageTest.java b/src/test/java/aero/t2s/modes/decoder/df/DfRealMessageTest.java index fafd41d..039f009 100644 --- a/src/test/java/aero/t2s/modes/decoder/df/DfRealMessageTest.java +++ b/src/test/java/aero/t2s/modes/decoder/df/DfRealMessageTest.java @@ -5,10 +5,7 @@ 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.Bds10; -import aero.t2s.modes.decoder.df.bds.Bds40; -import aero.t2s.modes.decoder.df.bds.Bds50; -import aero.t2s.modes.decoder.df.bds.Bds60; +import aero.t2s.modes.decoder.df.bds.*; import org.junit.jupiter.api.Test; import java.util.HashMap; @@ -392,6 +389,47 @@ public void test_df21_bds50_02A185() throws UnknownDownlinkFormatException { 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()); From eeefc190b612b56ef2cf1c1db027842804b3ea45 Mon Sep 17 00:00:00 2001 From: Ian Beswick Date: Mon, 20 Feb 2023 20:53:18 +0000 Subject: [PATCH 36/39] Experimental use of even/odd DF17 airborne positions and use global or local calculation depending on prior received data. --- src/main/java/aero/t2s/modes/CprPosition.java | 17 +- src/main/java/aero/t2s/modes/Track.java | 29 ++- .../decoder/df/df17/AirbornePosition.java | 166 ++++++++++-------- 3 files changed, 136 insertions(+), 76 deletions(-) diff --git a/src/main/java/aero/t2s/modes/CprPosition.java b/src/main/java/aero/t2s/modes/CprPosition.java index 5326368..2db1767 100644 --- a/src/main/java/aero/t2s/modes/CprPosition.java +++ b/src/main/java/aero/t2s/modes/CprPosition.java @@ -3,8 +3,23 @@ public class CprPosition { private double lat; private double lon; + private boolean valid; private int time; + public CprPosition() { + this.lat = 0.0; + this.lon = 0.0; + this.valid = false; + } + public CprPosition(double lat, double lon) { + setLatLon(lat ,lon); + } + public void setLatLon(double lat, double lon) { + this.lat = lat; + this.lon = lon; + this.valid = true; + } + public void setLat(double lat) { this.lat = lat; } @@ -30,6 +45,6 @@ public int getTime() { } public boolean isValid() { - return lat != 0d && lon != 0; + return valid; } } diff --git a/src/main/java/aero/t2s/modes/Track.java b/src/main/java/aero/t2s/modes/Track.java index dc0c3e7..e4f52d1 100644 --- a/src/main/java/aero/t2s/modes/Track.java +++ b/src/main/java/aero/t2s/modes/Track.java @@ -9,8 +9,12 @@ public class Track { private String icao; private String callsign; private Altitude altitude = new Altitude(); + private boolean cprEvenValid = false; + private CprPosition cprEven = new CprPosition(); + private CprPosition cprOdd = new CprPosition(); private double lat; private double lon; + private boolean positionAvailable = false; private int vx; private int vy; private double gs; @@ -172,7 +176,13 @@ public boolean isGroundBit() { return groundBit; } + public void setLatLon(double lat, double lon) { + this.lon = lat; + this.lon = lon; + this.positionAvailable = true; + } 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; } @@ -181,6 +191,7 @@ public double getLat() { } 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; } @@ -188,6 +199,22 @@ public double getLon() { return lon; } + public CprPosition getCprEven() { + return cprEven; + } + + public void setCprEven(CprPosition cprEven) { + this.cprEven = cprEven; + } + + public CprPosition getCprOdd() { + return cprOdd; + } + + public void setCprOdd(CprPosition cprOdd) { + this.cprOdd = cprOdd; + } + public Version getVersion() { return version; } @@ -245,7 +272,7 @@ public int getModeA() { } public boolean isPositionAvailable() { - return lat != 0 & lon != 0; + return positionAvailable; } public void setGeometricHeightOffset(int geometricHeightOffset) { 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 c652f4a..e46075a 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,6 +1,7 @@ package aero.t2s.modes.decoder.df.df17; import aero.t2s.modes.Track; +import aero.t2s.modes.CprPosition; import aero.t2s.modes.constants.*; import aero.t2s.modes.registers.Register05; import aero.t2s.modes.registers.Register05V0; @@ -17,6 +18,10 @@ public class AirbornePosition extends ExtendedSquitter { private int altitude; private boolean positionAvailable; + + private CprPosition cprEven = new CprPosition(); + private CprPosition cprOdd = new CprPosition(); + private double lat; private double lon; @@ -43,10 +48,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); @@ -56,7 +59,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); + if (isCprEven) { + this.cprEven.setLatLon(cprLat/(double)(1 << 17), cprLon/(double)(1 << 17)); + } + else { + this.cprOdd.setLatLon(cprLat, cprLon); + } + + calculatePosition(isCprEven); return this; } @@ -67,8 +77,9 @@ public void apply(Track track) { track.setSpi(surveillanceStatus == SurveillanceStatus.SPI); track.setTempAlert(surveillanceStatus == SurveillanceStatus.TEMPORARY_ALERT); track.setEmergency(surveillanceStatus == SurveillanceStatus.PERMANENT_ALERT); - track.setLat(lat); - track.setLon(lon); + track.setCprEven(cprEven); + track.setCprOdd(cprOdd); + track.setLatLon(lat, lon); if (versionChanged(track)) { switch (track.getVersion()) { @@ -204,88 +215,95 @@ private AltitudeSource determineAltitudeSource() { return AltitudeSource.GNSS_HAE; } - 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 calculatePosition(boolean isEven) { + if (!positionAvailable) { + //TODO Could be other cases where we need to do global calculation, such as too much time elapsed since last position update + calculateGlobal(cprEven, cprOdd); + positionAvailable = true; + } + else { + if (isEven) { + if (cprOdd.isValid()) { + calculateLocal(cprEven, false, this.lat, this.lon); + } + } else { + if (cprEven.isValid()) { + calculateLocal(cprOdd, true, this.lat, this.lon); + } + } + } } - private void calculateLocal(boolean isEven, double lat, double lon, double time) { - boolean isOdd = !isEven; -// CprPosition cpr = track.getCprPosition(isEven); + private void calculateLocal(CprPosition cpr, boolean isOdd, double previousLat, double previousLon) { 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); + double j = Math.floor(previousLat / dlat) + Math.floor((previousLat % dlat) / dlat - cpr.getLat() + 0.5); - lat = dlat * (j + lat); + double newLat = dlat * (j + previousLat); - double nl = NL(lat) - (isOdd ? 1.0 : 0.0); + double nl = NL(newLat) - (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); + double m = Math.floor(previousLon / dlon) + Math.floor((previousLon % dlon) / dlon - cpr.getLon() + 0.5); + double newLon = dlon * (m + lon); + + //TODO Should be a sanity-check here to make sure the calculated position isn't outside receiver origin range + //TODO Should be a sanity-check here to see if the calculated movement since the last update is too far + this.lat = newLat; + this.lon = newLon; + } + + private void calculateGlobal(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; + } + + //TODO Should be a sanity-check here to make sure the calculated position isn't outside receiver origin range, this.lat = lat; this.lon = lon; } + private double cprN(double lat, double isOdd) { + double nl = NL(lat) - isOdd; -// 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; -// } + return nl > 1 ? nl : 1; + } private double NL(double lat) { if (lat == 0) return 59; From 48b9f12c61c668d927f6673bd9ed90634793ee79 Mon Sep 17 00:00:00 2001 From: Ken Andries Date: Tue, 21 Feb 2023 17:46:09 +0100 Subject: [PATCH 37/39] Improve global position updating --- gradle.properties | 2 +- src/main/java/aero/t2s/modes/CprPosition.java | 14 +- .../java/aero/t2s/modes/ModeSHandler.java | 5 +- src/main/java/aero/t2s/modes/Track.java | 19 --- .../java/aero/t2s/modes/decoder/Decoder.java | 2 +- .../java/aero/t2s/modes/decoder/df/DF17.java | 9 +- .../decoder/df/df17/AirbornePosition.java | 145 ++++++++++++------ 7 files changed, 120 insertions(+), 76 deletions(-) diff --git a/gradle.properties b/gradle.properties index a8f0d77..c47ae13 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1,5 +1,5 @@ GROUP=aero.t2s -VERSION_NAME=0.2.5-SNAPSHOT +VERSION_NAME=0.2.6-SNAPSHOT POM_ARTIFACT_ID=mode-s POM_NAME=Mode-S/ADS-B (1090Mhz) diff --git a/src/main/java/aero/t2s/modes/CprPosition.java b/src/main/java/aero/t2s/modes/CprPosition.java index 2db1767..184710c 100644 --- a/src/main/java/aero/t2s/modes/CprPosition.java +++ b/src/main/java/aero/t2s/modes/CprPosition.java @@ -1,10 +1,12 @@ package aero.t2s.modes; +import java.time.Instant; + public class CprPosition { private double lat; private double lon; private boolean valid; - private int time; + private long time; public CprPosition() { this.lat = 0.0; @@ -14,9 +16,11 @@ public CprPosition() { public CprPosition(double lat, double lon) { setLatLon(lat ,lon); } + public void setLatLon(double lat, double lon) { this.lat = lat; this.lon = lon; + this.time = Instant.now().toEpochMilli(); this.valid = true; } @@ -36,15 +40,19 @@ public double getLon() { return lon; } - public void setTime(int time) { + public void setTime(long time) { this.time = time; } - public int getTime() { + public long getTime() { return time; } public boolean isValid() { return valid; } + + public boolean isExpired() { + return time < Instant.now().minusSeconds(10).toEpochMilli(); + } } diff --git a/src/main/java/aero/t2s/modes/ModeSHandler.java b/src/main/java/aero/t2s/modes/ModeSHandler.java index aae0391..dafdb65 100644 --- a/src/main/java/aero/t2s/modes/ModeSHandler.java +++ b/src/main/java/aero/t2s/modes/ModeSHandler.java @@ -1,6 +1,7 @@ package aero.t2s.modes; import aero.t2s.modes.decoder.df.DownlinkFormat; +import aero.t2s.modes.decoder.df.df17.AirbornePosition; import java.util.function.Consumer; @@ -42,10 +43,10 @@ protected short[] toData(final String input) throws EmptyMessageException, ModeA } public void start() { - + AirbornePosition.start(); } public void stop() { - + AirbornePosition.stop(); } } diff --git a/src/main/java/aero/t2s/modes/Track.java b/src/main/java/aero/t2s/modes/Track.java index e4f52d1..fe75d76 100644 --- a/src/main/java/aero/t2s/modes/Track.java +++ b/src/main/java/aero/t2s/modes/Track.java @@ -9,9 +9,6 @@ public class Track { private String icao; private String callsign; private Altitude altitude = new Altitude(); - private boolean cprEvenValid = false; - private CprPosition cprEven = new CprPosition(); - private CprPosition cprOdd = new CprPosition(); private double lat; private double lon; private boolean positionAvailable = false; @@ -199,22 +196,6 @@ public double getLon() { return lon; } - public CprPosition getCprEven() { - return cprEven; - } - - public void setCprEven(CprPosition cprEven) { - this.cprEven = cprEven; - } - - public CprPosition getCprOdd() { - return cprOdd; - } - - public void setCprOdd(CprPosition cprOdd) { - this.cprOdd = cprOdd; - } - public Version getVersion() { return version; } diff --git a/src/main/java/aero/t2s/modes/decoder/Decoder.java b/src/main/java/aero/t2s/modes/decoder/Decoder.java index ab2792c..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); 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..9860eaa 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: 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 e46075a..891863e 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 @@ -3,14 +3,15 @@ import aero.t2s.modes.Track; import aero.t2s.modes.CprPosition; import aero.t2s.modes.constants.*; +import aero.t2s.modes.decoder.Common; 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; @@ -19,16 +20,14 @@ public class AirbornePosition extends ExtendedSquitter { private boolean positionAvailable; - private CprPosition cprEven = new CprPosition(); - private CprPosition cprOdd = new CprPosition(); - private double lat; private double lon; + private static Map cache = new HashMap<>(); + private static Timer cacheCleanup; - 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 @@ -59,14 +58,38 @@ public AirbornePosition decode() { cprLon = cprLon | (data[9] << 8); cprLon = cprLon | data[10]; + + if (!cache.containsKey(address)) { + if (!isCprEven) { + return this; + } + + synchronized (cache) { + cache.putIfAbsent(address, new PositionUpdate( + new CprPosition(cprLat / (double)(1 << 17), cprLon / (double)(1 << 17)) + )); + } + } + + PositionUpdate positionUpdate; + synchronized (cache) { + positionUpdate = cache.get(address); + } if (isCprEven) { - this.cprEven.setLatLon(cprLat/(double)(1 << 17), cprLon/(double)(1 << 17)); + positionUpdate.setEven(new CprPosition(cprLat / (double) (1 << 17), cprLon / (double) (1 << 17))); + } else { + positionUpdate.setOdd(new CprPosition(cprLat, cprLon)); } - else { - this.cprOdd.setLatLon(cprLat, cprLon); + + if (positionUpdate.isComplete()) { + calculateGlobal(positionUpdate.even, positionUpdate.odd); + } else if (positionUpdate.isPreviousPositionAvailable() && positionUpdate.isPreviousPositionAvailable()) { + calculateLocal(positionUpdate.odd, true, positionUpdate.previousLat, positionUpdate.previousLon); } - calculatePosition(isCprEven); + if (positionAvailable) { + positionUpdate.setPreviousPosition(this.lat, this.lon); + } return this; } @@ -77,9 +100,9 @@ public void apply(Track track) { track.setSpi(surveillanceStatus == SurveillanceStatus.SPI); track.setTempAlert(surveillanceStatus == SurveillanceStatus.TEMPORARY_ALERT); track.setEmergency(surveillanceStatus == SurveillanceStatus.PERMANENT_ALERT); - track.setCprEven(cprEven); - track.setCprOdd(cprOdd); - track.setLatLon(lat, lon); + if (positionAvailable) { + track.setLatLon(lat, lon); + } if (versionChanged(track)) { switch (track.getVersion()) { @@ -130,14 +153,6 @@ public BarometricAltitudeIntegrityCode getNICbaro() { } } - public double getOriginLat() { - return originLat; - } - - public double getOriginLon() { - return originLon; - } - public SurveillanceStatus getSurveillanceStatus() { return surveillanceStatus; } @@ -215,25 +230,6 @@ private AltitudeSource determineAltitudeSource() { return AltitudeSource.GNSS_HAE; } - private void calculatePosition(boolean isEven) { - if (!positionAvailable) { - //TODO Could be other cases where we need to do global calculation, such as too much time elapsed since last position update - calculateGlobal(cprEven, cprOdd); - positionAvailable = true; - } - else { - if (isEven) { - if (cprOdd.isValid()) { - calculateLocal(cprEven, false, this.lat, this.lon); - } - } else { - if (cprEven.isValid()) { - calculateLocal(cprOdd, true, this.lat, this.lon); - } - } - } - } - private void calculateLocal(CprPosition cpr, boolean isOdd, double previousLat, double previousLon) { double dlat = isOdd ? 360.0 / 59.0 : 360.0 / 60.0; @@ -298,6 +294,7 @@ private void calculateGlobal(CprPosition cprEven, CprPosition cprOdd) { //TODO Should be a sanity-check here to make sure the calculated position isn't outside receiver origin range, this.lat = lat; this.lon = lon; + this.positionAvailable = true; } private double cprN(double lat, double isOdd) { double nl = NL(lat) - isOdd; @@ -323,4 +320,66 @@ private int calculateAltitude(short[] data, int typeCode) { return (n * qBit) - 1000; } + + public static void start() { + AirbornePosition.cache.clear(); + AirbornePosition.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); + } + + public static void stop() { + AirbornePosition.cacheCleanup.cancel(); + AirbornePosition.cacheCleanup = null; + + AirbornePosition.cache.clear(); + } + + class PositionUpdate { + private CprPosition even; + private CprPosition odd; + + + private boolean previousPositionAvailable = false; + private double previousLat; + private double previousLon; + + public PositionUpdate(CprPosition even) { + this.even = even; + } + + public void setEven(CprPosition even) { + this.even = even; + this.odd = null; + } + + public void setOdd(CprPosition odd) { + this.odd = odd; + } + + public void setPreviousPosition(double lat, double lon) { + this.previousLat = lat; + this.previousLon = lon; + } + + public boolean isPreviousPositionAvailable() { + return this.previousPositionAvailable; + } + + public boolean isComplete() { + return even != null && odd != null; + } + + public boolean isExpired() { + return even.isExpired() || odd.isExpired(); + } + } } From 9d36cb0819e98175652d23f777fbd99490a5c142 Mon Sep 17 00:00:00 2001 From: Ken Andries Date: Tue, 21 Feb 2023 17:50:48 +0100 Subject: [PATCH 38/39] Fix lon decoding --- .../java/aero/t2s/modes/decoder/df/df17/AirbornePosition.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 891863e..ad59e82 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 @@ -78,7 +78,7 @@ public AirbornePosition decode() { if (isCprEven) { positionUpdate.setEven(new CprPosition(cprLat / (double) (1 << 17), cprLon / (double) (1 << 17))); } else { - positionUpdate.setOdd(new CprPosition(cprLat, cprLon)); + positionUpdate.setOdd(new CprPosition(cprLat / (double) (1 << 17), cprLon / (double) (1 << 17))); } if (positionUpdate.isComplete()) { From cca9a63660e9dd0934a74b397ceedb7e8d0415cf Mon Sep 17 00:00:00 2001 From: Ken Andries Date: Tue, 21 Feb 2023 20:08:11 +0100 Subject: [PATCH 39/39] Fix latitude assignment --- src/main/java/aero/t2s/modes/Track.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/java/aero/t2s/modes/Track.java b/src/main/java/aero/t2s/modes/Track.java index fe75d76..f76da27 100644 --- a/src/main/java/aero/t2s/modes/Track.java +++ b/src/main/java/aero/t2s/modes/Track.java @@ -174,7 +174,7 @@ public boolean isGroundBit() { } public void setLatLon(double lat, double lon) { - this.lon = lat; + this.lat = lat; this.lon = lon; this.positionAvailable = true; }