From 1ac835260e8a5eee9e175fff18c9f0e8c2937c59 Mon Sep 17 00:00:00 2001 From: Jon Beniston Date: Fri, 7 May 2021 21:50:27 +0100 Subject: [PATCH] Add AIS mod, demod and feature. --- debian/control | 4 +- doc/img/AISDemod_plugin.png | Bin 0 -> 11518 bytes doc/img/AISDemod_plugin_messages.png | Bin 0 -> 33093 bytes doc/img/AISMod_plugin.png | Bin 0 -> 16799 bytes doc/img/AIS_plugin.png | Bin 0 -> 26440 bytes doc/img/AIS_plugin_map.png | Bin 0 -> 115929 bytes plugins/channelrx/CMakeLists.txt | 1 + plugins/channelrx/demodais/CMakeLists.txt | 58 + plugins/channelrx/demodais/aisdemod.cpp | 500 +++++++ plugins/channelrx/demodais/aisdemod.h | 180 +++ .../channelrx/demodais/aisdemodbaseband.cpp | 175 +++ plugins/channelrx/demodais/aisdemodbaseband.h | 97 ++ plugins/channelrx/demodais/aisdemodgui.cpp | 627 +++++++++ plugins/channelrx/demodais/aisdemodgui.h | 134 ++ plugins/channelrx/demodais/aisdemodgui.ui | 898 ++++++++++++ plugins/channelrx/demodais/aisdemodplugin.cpp | 92 ++ plugins/channelrx/demodais/aisdemodplugin.h | 49 + .../channelrx/demodais/aisdemodsettings.cpp | 163 +++ plugins/channelrx/demodais/aisdemodsettings.h | 70 + plugins/channelrx/demodais/aisdemodsink.cpp | 473 +++++++ plugins/channelrx/demodais/aisdemodsink.h | 142 ++ .../demodais/aisdemodwebapiadapter.cpp | 52 + .../demodais/aisdemodwebapiadapter.h | 50 + plugins/channelrx/demodais/readme.md | 85 ++ plugins/channeltx/CMakeLists.txt | 1 + plugins/channeltx/modais/CMakeLists.txt | 64 + plugins/channeltx/modais/aismod.cpp | 824 +++++++++++ plugins/channeltx/modais/aismod.h | 209 +++ plugins/channeltx/modais/aismodbaseband.cpp | 201 +++ plugins/channeltx/modais/aismodbaseband.h | 99 ++ plugins/channeltx/modais/aismodgui.cpp | 686 ++++++++++ plugins/channeltx/modais/aismodgui.h | 119 ++ plugins/channeltx/modais/aismodgui.ui | 1212 +++++++++++++++++ plugins/channeltx/modais/aismodplugin.cpp | 92 ++ plugins/channeltx/modais/aismodplugin.h | 49 + .../channeltx/modais/aismodrepeatdialog.cpp | 54 + plugins/channeltx/modais/aismodrepeatdialog.h | 40 + .../channeltx/modais/aismodrepeatdialog.ui | 134 ++ plugins/channeltx/modais/aismodsettings.cpp | 224 +++ plugins/channeltx/modais/aismodsettings.h | 79 ++ plugins/channeltx/modais/aismodsource.cpp | 496 +++++++ plugins/channeltx/modais/aismodsource.h | 147 ++ .../modais/aismodtxsettingsdialog.cpp | 54 + .../channeltx/modais/aismodtxsettingsdialog.h | 48 + .../modais/aismodtxsettingsdialog.ui | 211 +++ .../channeltx/modais/aismodwebapiadapter.cpp | 53 + .../channeltx/modais/aismodwebapiadapter.h | 50 + plugins/channeltx/modais/readme.md | 128 ++ plugins/feature/CMakeLists.txt | 1 + plugins/feature/ais/CMakeLists.txt | 55 + plugins/feature/ais/ais.cpp | 301 ++++ plugins/feature/ais/ais.h | 116 ++ plugins/feature/ais/ais.qrc | 12 + plugins/feature/ais/aisgui.cpp | 604 ++++++++ plugins/feature/ais/aisgui.h | 107 ++ plugins/feature/ais/aisgui.ui | 188 +++ plugins/feature/ais/aisplugin.cpp | 80 ++ plugins/feature/ais/aisplugin.h | 49 + plugins/feature/ais/aissettings.cpp | 122 ++ plugins/feature/ais/aissettings.h | 54 + plugins/feature/ais/aiswebapiadapter.cpp | 52 + plugins/feature/ais/aiswebapiadapter.h | 50 + plugins/feature/ais/map/aircraft.png | Bin 0 -> 657 bytes plugins/feature/ais/map/anchor.png | Bin 0 -> 531 bytes plugins/feature/ais/map/bouy.png | Bin 0 -> 375 bytes plugins/feature/ais/map/cargo.png | Bin 0 -> 1191 bytes plugins/feature/ais/map/helicopter.png | Bin 0 -> 779 bytes plugins/feature/ais/map/ship.png | Bin 0 -> 1292 bytes plugins/feature/ais/map/tanker.png | Bin 0 -> 991 bytes plugins/feature/ais/map/tug.png | Bin 0 -> 856 bytes plugins/feature/ais/readme.md | 44 + .../demodanalyzer/demodanalyzersettings.cpp | 4 + plugins/feature/map/mapsettings.cpp | 2 + plugins/feature/map/mapsettings.h | 11 +- plugins/feature/map/mapsettingsdialog.ui | 8 + plugins/feature/map/readme.md | 3 +- plugins/feature/pertester/readme.md | 5 +- sdrbase/CMakeLists.txt | 3 + sdrbase/dsp/gaussian.h | 121 ++ sdrbase/util/ais.cpp | 757 ++++++++++ sdrbase/util/ais.h | 385 ++++++ sdrbase/webapi/webapirequestmapper.cpp | 29 +- sdrbase/webapi/webapiutils.cpp | 7 + swagger/sdrangel/api/swagger/include/AIS.yaml | 18 + .../api/swagger/include/AISDemod.yaml | 55 + .../sdrangel/api/swagger/include/AISMod.yaml | 125 ++ .../api/swagger/include/ChannelActions.yaml | 2 + .../api/swagger/include/ChannelReport.yaml | 4 + .../api/swagger/include/ChannelSettings.yaml | 4 + .../api/swagger/include/FeatureSettings.yaml | 2 + .../code/qt5/client/SWGAISDemodReport.cpp | 131 ++ .../code/qt5/client/SWGAISDemodReport.h | 64 + .../code/qt5/client/SWGAISDemodSettings.cpp | 459 +++++++ .../code/qt5/client/SWGAISDemodSettings.h | 149 ++ .../code/qt5/client/SWGAISModActions.cpp | 110 ++ .../code/qt5/client/SWGAISModActions.h | 59 + .../code/qt5/client/SWGAISModActions_tx.cpp | 110 ++ .../code/qt5/client/SWGAISModActions_tx.h | 59 + .../code/qt5/client/SWGAISModReport.cpp | 131 ++ .../code/qt5/client/SWGAISModReport.h | 64 + .../code/qt5/client/SWGAISModSettings.cpp | 969 +++++++++++++ .../code/qt5/client/SWGAISModSettings.h | 281 ++++ .../code/qt5/client/SWGAISSettings.cpp | 250 ++++ .../sdrangel/code/qt5/client/SWGAISSettings.h | 95 ++ .../code/qt5/client/SWGChannelActions.cpp | 25 + .../code/qt5/client/SWGChannelActions.h | 7 + .../code/qt5/client/SWGChannelReport.cpp | 50 + .../code/qt5/client/SWGChannelReport.h | 14 + .../code/qt5/client/SWGChannelSettings.cpp | 50 + .../code/qt5/client/SWGChannelSettings.h | 14 + .../code/qt5/client/SWGFeatureSettings.cpp | 25 + .../code/qt5/client/SWGFeatureSettings.h | 7 + .../code/qt5/client/SWGModelFactory.h | 28 + 113 files changed, 15643 insertions(+), 12 deletions(-) create mode 100644 doc/img/AISDemod_plugin.png create mode 100644 doc/img/AISDemod_plugin_messages.png create mode 100644 doc/img/AISMod_plugin.png create mode 100644 doc/img/AIS_plugin.png create mode 100644 doc/img/AIS_plugin_map.png create mode 100644 plugins/channelrx/demodais/CMakeLists.txt create mode 100644 plugins/channelrx/demodais/aisdemod.cpp create mode 100644 plugins/channelrx/demodais/aisdemod.h create mode 100644 plugins/channelrx/demodais/aisdemodbaseband.cpp create mode 100644 plugins/channelrx/demodais/aisdemodbaseband.h create mode 100644 plugins/channelrx/demodais/aisdemodgui.cpp create mode 100644 plugins/channelrx/demodais/aisdemodgui.h create mode 100644 plugins/channelrx/demodais/aisdemodgui.ui create mode 100644 plugins/channelrx/demodais/aisdemodplugin.cpp create mode 100644 plugins/channelrx/demodais/aisdemodplugin.h create mode 100644 plugins/channelrx/demodais/aisdemodsettings.cpp create mode 100644 plugins/channelrx/demodais/aisdemodsettings.h create mode 100644 plugins/channelrx/demodais/aisdemodsink.cpp create mode 100644 plugins/channelrx/demodais/aisdemodsink.h create mode 100644 plugins/channelrx/demodais/aisdemodwebapiadapter.cpp create mode 100644 plugins/channelrx/demodais/aisdemodwebapiadapter.h create mode 100644 plugins/channelrx/demodais/readme.md create mode 100644 plugins/channeltx/modais/CMakeLists.txt create mode 100644 plugins/channeltx/modais/aismod.cpp create mode 100644 plugins/channeltx/modais/aismod.h create mode 100644 plugins/channeltx/modais/aismodbaseband.cpp create mode 100644 plugins/channeltx/modais/aismodbaseband.h create mode 100644 plugins/channeltx/modais/aismodgui.cpp create mode 100644 plugins/channeltx/modais/aismodgui.h create mode 100644 plugins/channeltx/modais/aismodgui.ui create mode 100644 plugins/channeltx/modais/aismodplugin.cpp create mode 100644 plugins/channeltx/modais/aismodplugin.h create mode 100644 plugins/channeltx/modais/aismodrepeatdialog.cpp create mode 100644 plugins/channeltx/modais/aismodrepeatdialog.h create mode 100644 plugins/channeltx/modais/aismodrepeatdialog.ui create mode 100644 plugins/channeltx/modais/aismodsettings.cpp create mode 100644 plugins/channeltx/modais/aismodsettings.h create mode 100644 plugins/channeltx/modais/aismodsource.cpp create mode 100644 plugins/channeltx/modais/aismodsource.h create mode 100644 plugins/channeltx/modais/aismodtxsettingsdialog.cpp create mode 100644 plugins/channeltx/modais/aismodtxsettingsdialog.h create mode 100644 plugins/channeltx/modais/aismodtxsettingsdialog.ui create mode 100644 plugins/channeltx/modais/aismodwebapiadapter.cpp create mode 100644 plugins/channeltx/modais/aismodwebapiadapter.h create mode 100644 plugins/channeltx/modais/readme.md create mode 100644 plugins/feature/ais/CMakeLists.txt create mode 100644 plugins/feature/ais/ais.cpp create mode 100644 plugins/feature/ais/ais.h create mode 100644 plugins/feature/ais/ais.qrc create mode 100644 plugins/feature/ais/aisgui.cpp create mode 100644 plugins/feature/ais/aisgui.h create mode 100644 plugins/feature/ais/aisgui.ui create mode 100644 plugins/feature/ais/aisplugin.cpp create mode 100644 plugins/feature/ais/aisplugin.h create mode 100644 plugins/feature/ais/aissettings.cpp create mode 100644 plugins/feature/ais/aissettings.h create mode 100644 plugins/feature/ais/aiswebapiadapter.cpp create mode 100644 plugins/feature/ais/aiswebapiadapter.h create mode 100644 plugins/feature/ais/map/aircraft.png create mode 100644 plugins/feature/ais/map/anchor.png create mode 100644 plugins/feature/ais/map/bouy.png create mode 100644 plugins/feature/ais/map/cargo.png create mode 100644 plugins/feature/ais/map/helicopter.png create mode 100644 plugins/feature/ais/map/ship.png create mode 100644 plugins/feature/ais/map/tanker.png create mode 100644 plugins/feature/ais/map/tug.png create mode 100644 plugins/feature/ais/readme.md create mode 100644 sdrbase/dsp/gaussian.h create mode 100644 sdrbase/util/ais.cpp create mode 100644 sdrbase/util/ais.h create mode 100644 swagger/sdrangel/api/swagger/include/AIS.yaml create mode 100644 swagger/sdrangel/api/swagger/include/AISDemod.yaml create mode 100644 swagger/sdrangel/api/swagger/include/AISMod.yaml create mode 100644 swagger/sdrangel/code/qt5/client/SWGAISDemodReport.cpp create mode 100644 swagger/sdrangel/code/qt5/client/SWGAISDemodReport.h create mode 100644 swagger/sdrangel/code/qt5/client/SWGAISDemodSettings.cpp create mode 100644 swagger/sdrangel/code/qt5/client/SWGAISDemodSettings.h create mode 100644 swagger/sdrangel/code/qt5/client/SWGAISModActions.cpp create mode 100644 swagger/sdrangel/code/qt5/client/SWGAISModActions.h create mode 100644 swagger/sdrangel/code/qt5/client/SWGAISModActions_tx.cpp create mode 100644 swagger/sdrangel/code/qt5/client/SWGAISModActions_tx.h create mode 100644 swagger/sdrangel/code/qt5/client/SWGAISModReport.cpp create mode 100644 swagger/sdrangel/code/qt5/client/SWGAISModReport.h create mode 100644 swagger/sdrangel/code/qt5/client/SWGAISModSettings.cpp create mode 100644 swagger/sdrangel/code/qt5/client/SWGAISModSettings.h create mode 100644 swagger/sdrangel/code/qt5/client/SWGAISSettings.cpp create mode 100644 swagger/sdrangel/code/qt5/client/SWGAISSettings.h diff --git a/debian/control b/debian/control index f897e0d01..ad6bf58cb 100644 --- a/debian/control +++ b/debian/control @@ -61,9 +61,9 @@ Description: SDR/Analyzer/Generator front-end for various hardware Builds on Linux, Windows and Mac O/S Reception modes supported: Analog: AM, ATV, NFM, WFM, SSB, broadcast FM, APT - Digital: D-Star, Yaesu SF, DMR, dPMR, FreeDV, DAB, DVB-S, LoRa, ADS-B, Packet (AX.25/APRS) + Digital: D-Star, Yaesu SF, DMR, dPMR, FreeDV, DAB, DVB-S, LoRa, ADS-B, Packet (AX.25/APRS), AIS Analyzer: Generic channel Transmission modes supported: Analog: AM, ATV, NFM, SSB, WFM - Digital: DVB-S, Packet (AX.25), 802.15.4 + Digital: DVB-S, Packet (AX.25), AIS, 802.15.4 Homepage: https://github.com/f4exb/sdrangel diff --git a/doc/img/AISDemod_plugin.png b/doc/img/AISDemod_plugin.png new file mode 100644 index 0000000000000000000000000000000000000000..cf34b338e59ee9f9382261fa4931fe06b77b6478 GIT binary patch literal 11518 zcmYj%1yoy2)GZFdr9hDu3s&5rxD%u}6ff>hi@Vce!JXm`h2ri`aF^g#oZ|A*?|<*D z|E`tX%*xz5GiQ$Mv*$#re3HdNCq;*YgTn&JNvXlX!H2ALs$dJQd~(K4(?Yh#*;BJtPOCI({Y7^!|wg}fgf}%F@u9+P5?=XYj_x*WMO^Q zFkgSg2`bMsAxh2T4w<&INY_eroRmyIwqi|AXcZp)EyU79S8wwfB0?ByN}?sJ9+oIU>A;OrA~Y>dAFo( zSHao2ZpL&kM(}m<)L8ZPakQ!E=2+xO?(O4lO2=<(Aq};>G!X;u1p}e8h`&SeGpINn z%{GJ(8nhsf0;Uf~8LIgfX4vna^Uv}`{65Uz4U}Y%lKt$-njg|cu?t)Ed%U@H(JCCu zj}=+IIV^6?_5ttXAsud1EY?{Lm8eE7PkWuNGJPkOYysu#bpEC-YyJGWjl)^dwa&6K zUc*Ak{v_=6p29Hwx0(5RPpPTZ!)mkeS(Wz{yAnpvveB#8_L}<@CcEoQ9jdl(y^zf@ zZ!$!0D(fq^ytZVymYvbpK_p$DquF(=($;1yL}sJr;Nr;cVzT#ZEho4T6l9P7`hAPS zQ!PKdB|&XUN9nc7vw|v--a51(T;UeU;+0PS*O+&L9`Qr9=A(P#8Sa0w+{Kb6Xz4oI zi#*|~@sQ4Afx`0X9hc4807#`iSW2P6aU)yE6a{GZkSb6)k$X*&&5?EcA4C4bYwYQE}d{t(2KY(48CN+4Prl({T8>cIYx6bx}lAGVzx z7}JxzdJ{xRejY0pE9NB@;DHx9wy{B1%xA)bV1Jqk9T^?pe0B8U^Nj zbR|k*JU1D$HW7$e;MYmlS&TiZ(xZynX;P}Fap*77^ThM`Hu(l3R`+#F!Kom*|C^Yh zc64@)%{#%7znZz-ElN6jE!s!C(fP<|(-d%raIZt4B)^-w91h{Kl9D*v?*?EX_%xk& zFM%dJgL28uW3=U~BRCO1d}E!4SzlSGly)Ye$$^nmT~xD%7h@&n)WE5@F6Z}2)_6}) z@ERnY&wO|M`cOhx<6@X`{?mGtwwmZq>{(Z`3UwU$%U@qME2WRS%c-}%vfs|H6$&YZ zV!DY7+1xXqHy@~S@$FWbgqEJ6egET}M}PS~&d}F$?Oj2Zy`H80%hBxKzL+MV5prsc z&Dg1Iim6%nwWwIqy7!rIk$kSK>;8nfS6{jG8z6vxVRcOXy*0uhnoucCkdwCI`^d|J zrg8X)XM9=z2|(DKxlY7f-~gU)8!9GOUA()wZpPgFHu)>6aD zJ#N1?)gNKf)lhaMRW?EQQ?s(2PqA?3Aku`?A@z0Zk3M(feBEgeb9I9U^8N~-N@{K7f(!NHw@@RfON00t71Tn?1D&+b) z9)m6x<0AfRl8kGHcP{d0njL)hRztTsl!aREmG784QsB*I?zc8_k^DIt;r40y4w#jSrP5qbx*QS z{1Ad22I4mu?Qz<7i+S!KIXS;%K~IpdHLf^yR?h3w{_UDO#ht7g(jb!=13+^oV@19* zz2Rj}+sL3(j{l~jmFKvC*7G<=k27l&zhoh=6dj&I%0B;CggE6B_B&7Q)UuSw$uXs7 z)Z^MqkKrbvs~+UyiRa+mH*Zs9-_zmp635dzp`{R2>?%xf@l}>^6$?aValY30(F)pZ zBIG&zX8-<9#9>DF^iu9ucAHAbow)uti=woJkN%TIcOTHEW8<&Uws;tWTs3K1fjY6| zn~eBB>~&;CmyH_xJDOPdC~PJcuuZR|{KBbZHF!Cl zt>>;dJ~eFAx|7-r%mrXzUmn=4ElV4*jSgSKe%CK4SdEd<4)ylhC{q;I5 zIayY=n8LY(+S`5d{qbgAn9^?1J1Xt6(0x#10;V!IMOC_}(X)hfM8XflrO2^gGiD;9 z#ElJ-bFX2OW`jSBJH}%|(hOo&r)_Rnq)5`Q^}L$2o`LzS30v~}T{eHu!=d=Ka&1AO z4~OsfWoco4x|-Sl@x&TOD7PuQa^ZL%lH-3}+1Oxi{!5nj1dKF?({ZhK`GceiLJ=6o zqoE!AUVOREg#-A!<%uNzz?Um8Iru|$Hd(C1BA*m(%0MBAeI25LE* zqrhqRh$n`{l8~VJrY6MB2l{v*k~nUX7?9B(wMvo(4d2Tqd2X53dil4t`?@E6xysJL zRQ_PhyKNFA@i?(=x~vk+nE2sBFJ0xzBJ$Qn1E zPu;bv4R=XMRM0eqXi^nMkyVH%!+f%|=x|z0@&~}<`o#m71P3wAl&$0lt$%*YQtSd` zU*$}P@Z|sQz*%vm9BRY|@~C}g+xJVpaq6 zwjLz*!Lz@Xug5$!UMlv2xNEsXDdnWLC_!BV(PV{e+{R)==$->Hb9StZjFZPRwJfBy zsZvDWr=x{h%1SwkTm?Jhv@*%z9Y+^a6?bqD`H&**GHC%GhlMH2{lauq@H)xKak92! z;hV3TB-T#-huze?j-!Ocv)cD9Cuao+Pj~_pNCdH34w~pcM9MXud-mWc} zBit`oAwBL+Dy@)*Y`(|TiD4yDdivQhg#u3@pi({x+00I^YLf91w{3^Y)Z-#W!oi%l z$7U!ot2$-(cVGo=sEmS4OyqziPZ4=Hc$~<63|U4Gg^AZ3&AiZp0qB6EgAO6?>r<*A zKcdn)w*S>~cXuaxo(|}c!Z7|hhi-%N zq8+c=75)LQ;1zm)-5@_nsQq>+c(bAGvIpVG2}}F|$0iY`=MOB{{whI~X!tG>DT%ea z7TJ=xNPu@(Ov(hHhq?XO3j~;sPhK2V`uEhsde0(M(Ndx1v8sS^kyP;*KxjcFX#d`# zfh3O~MEQZ5J*R1Ie*W?ezFKWHg|mP(&o#9IwC;ZIL1|0BFAYQ^1um zY=4d2;ciZyMVlVj7qI)6ZxGUKEl19H+IF|wkE7h^t6&e;Ov0v?hgyj||AUwjuk4nW z)6*sw$jHTYRRBmE zozuoaX#4y6Ft1S-?@S^sKHZszQ5q|C0V9ow*3YN#$1~5@FvWY)13+qSZrrPd!o9qt z=QjVVpLKxaoz=g#!++1wIVUz%iOVf3-|pM&0bcbmD$?km99h0t zpyAIm$m1_HZgeP!j*eVLb^WE>r;nX@9#syz`hjWTIjZcCy*7RKr^7!hFSPjPd7F(z za)y58#DGkruX-NAb}vb#osYz=k@3EyTh5C_rhr1?hChGhb@A4I?#LtlcSb!qq+VW5 z9S@%~F-8jvJZZ!HStdV}I=KGcj7L+(amBFLKo`3`%tIHup4@q(;HQLwo-iBYvK>p0 zQi8+mB{SDmj3{g~#sq(QIihV5|L3WwmN0o&sLgipqbK(g#sh0XX-ygRm}=#}n#%vi z{6@5IP(8E?1u4E6R@1e?!`wOPCgYu~tbe`&L&KE(GmVqaTT2U*lGr~r{ssVg)jVP{ zJp2D=EnnJ7*ep7_o(uv0`)}~Y4>)JaM^amjSr{j0ghybeV~uP^Ap>4_{AI1Sv+#0f z7Cm0OVNi)V^@y;~6iO`cg@4HuA%?ACqV0wc@*_*v?Fc%9DPDJng=kHqG0Y z6Zf6{#zu4yiF@7khSpl9d^x<0fPO+(Ow+H3w~8$}nl+qJ%!dG*J=1?1i_L@4?R|x8 z3s@ShA&R1{s}q6BE;!sF~6=?Q#$Q( z=iut?o-c*;rQ^02aFwC}=mnl5G_zxa|91MF4N=hTUc^9`JD>c({~{O*Ee+AxF3L7W)*sN4I*(UK-QS-bZ9VOvm}fO5wqmGXtj zRSQTALg4!}%~Cts2}z&bmE(K!_3Agd%@T_n1_&V8((yMPIkC5LD*f7>^=es)@P=c|dAus01?^>6hA)|hh`!|6PQK-1*T zg?~en(xh1_z&jC4N}=Y%0wW3^p?(*MT+_Pv|b-YZg zZ=4T*qj_7=L2>ry0$Q$x`N+|8ANrH%_H_Za!B02)D93)^p?_}p<-vBP-x1OahEx6C zK|KEr=g%T~{=L(1hlFHo`e7l=hH8`e3V$G>cG2^SP6Rw@0$v&=%e?*W{|LSJXG-s> zb9u6XjgLv#!I#q&fn_|&vrWFYZ*}E5Z6E!YUD!p5&RGYPf#ZPA_=ZXsk+a7(l7ObV z9?^t8JwD7jjZ0%a6WjgH1>e9qdg||FzGj19Xf3z$RbxZLx5Kv9iF&LpPre_K?Uz%- zu3U66RLPO~Mt8R>mI;4##jWmJ4%h}?mlbC|S@awbzc@YqA@I*>e`YddnNENyNKwb_ zx335MODPFm9(&vtqe4_p(NQ51zQZ2HfhgL`&K?(|pa+7MEp*e5fI2%(nVWdBvBWKE zM3&$lSbX3N|MJmXYdSJdqa03=uf+?%YaUG^V78}|#f@T!clzU|7xCT~gtN{UZCIEc zV7qrN`pWY>$miAI#@>1rBPmK@vHZDRv+W${HJ$EFz1{v5s}s=41^^f#z#d!h>6KL& zwtG3@TV5B{n(lL%xLZlLg!Q75`%J(N+WJlcbINL*-i_C5g=hL-thJx)$GPG!)^ULq zfK(u(U&kKO|8nEe%ZPlC+%$qOFxQMF56&85myD~Si?ani)jHuU^6SNrcW|@#4ajAi zGNdw2UHub{6h!a1o&@Hi;S&lbCMK{Q1ER-jy;m2Hi3`BW^{rLY2zatwjn^ghUFpq* zvX#gK|AK>qls*V;!=?6|qqGbs{ zv0woJOIuqX8&({)@8vf8A_?Lb7-b5+VTCjQ3Gidlsg+^{J##ue9wrMr(V=-aJSC<8 z2o@p=YkSTmk7RbfM!O+B;8{Prq(&2X8uwwQg+PRl!ok-wED(B%$zGQIhLvknaE|5v zezD$%Q-X#mHE-iP@fKC(R`~WIJxO|x5Vz=qDU{7FR3T!#aIZR39Bv(DRKdopXjPY(J`PBb7X`Gp3% zx1IKzGLP3U}6-sg;r&VILD80x~fb$#`K5bXgudP;23B~gBjKc7_^+!H}Y&~B+<$>+n{ zhPo+YpJUYm1$=&tJ{_b20G7zk-R=BzZT-r?ez(9D;p|%not=>)cc!n~%RHR(63;}s z6&rLy4OGmnd&_K2FW@uZjv)OJ$OhBuW80q~>|3h*IP>JBp5#!bG_r0$b=7^4qwpmq zsKumXiU4S)5pw@++)2VCs)rM*h#p&M_We77zY9es=lu>E86}+(k=i@oEwlqtBtGib zzk%o7{!Y5Ym} z%7PfG$A{X8SwHy2`Wl+w&n}4t7sI5^je51B{4DteWcTvm;gVm8p^EV^@ajYk1Z7C@ z)o6(6D#$A^y>W@^@d<_Lbv+D9LCf4k{cz%6-fJT77*IZ_%$bO74X8dUX4;p}wF--;{f9c2;8t-F)|34oFu4hV#uAe}vvZ zR&bHT5yJCe1sJEt7(<8h0 zVr6Ch@S*H`PdOs`Kltqs3AD?E=%i!U!P-+LYWgvkoGlu5e^Nk6s6 z5L{*0zC1PMEDm4@*L4V|6wVV>uEC>`0n*Y6UyX6$2%QILmA~V%P?V79X>#7S7*9{i z$?5dC*y`((;s<19WjzImL7(^>($dl{_ggiw6(bfG7groReb1rYP{m6%d3X0)9MPxq z+s@ba%1W%n?35dY9*56!!4LGG&ryi4tuA+l6;fD36W{{LLhLB~?luG95iKCu&wm$; zg7KdZWJN!^pRWF+V2X<9*x&LyQHs_Q-(6leRHZsHf=U^R1RZU!m~1{rN!XFs5ektg zcA2()J6SFOB|zPLuEzOx>QqQn5O!5Cc*W7Z5W}>}nW_R3GOBlx-`I;V7SON#3WDka zOt)qO=rh9z2j=!AAF;Y~zL>qyM~xmw;bniSy|i@NTy_g1SE!N?hxrHqwV=z6+$2q$ zL0myoQ`0-aZ6% z!&DvzIIQxKrQurfg77yZX@(Mw`PZ~V-VSqVl|WCpecQ=g@eTi1;R1s|ml9+$xhqqR zNC!g)`9dE@gR)cFHFVRRWrj(**I;YwUuAnhvOMjs=1;=_wU3QGlPD3-V~ z)w3x&1D;toJ%T0zj&n~4V0d^qPBRWTP0Xvj>wCfJ{j94{n@(mIM2BoqMTnnAXT5KZ zM${@5C0@Zeen#Rk4+XE22Ht%Qg6pWkw+3h44< ze}#JC4XQtw5)A9A<+8EQsA1zhU#hk2*UahbvbG93LFYB%Au}Rhcv24PMv;RVe5SLa zH~PpOYjnetgSne*x;6ks{7nw<;`k%z24`#A2|T=`esokj^UBqH0)E&IIB;h;CUfwJ zC~n`rNt}tfR=1vhbV@urK0ZD=3d5nm)?qO74L5`d`5DUB9XL^D#=@vba66Fvuw{Sq zK!|AjHO;9-9%Q;V^1&3FkiJYV^fB}YHAi{r2iK+`lNP@_=)yt>)dI#^;I-C-QOieL zS297iwjpFK$~tMin5!)sKCpV|wo z+09)^G5*pD9n+W%q<(Qp9d9x5NCIp;D(4YzR$)o7-|hu7s>voYKiRB(oL7`lg=D*qFE`q&R*4*NQ(jM<>4_g z*j`#f2$rg*a#cHl83UhzIAswON7&9I%d(5wLpzj1&s;Dvhp;&k6g*yqf7vNlgQATk z`l8I9gOY*)&fm8k)K{?>WuL;RV(2wVO#AEOL_&&an{3S4lp#Y|oi7_}!HVw|clgQF zl;&WTrE%5e;oACuE-Bxk2EOY(Ig<(#!=};@B?e?NiM%^i>0!(IM(yeUg(Z(a+8b6h zGl^qRc@Nsusq~!-JF3X?6oof2cf~Bq(SoGn>#cVuHn^(_C|BRz6~D z6G2gc#<|=JDd)qE=;3<}byLOSBrqLbPE#h4a}ZCH&E186&^>q4dt^`m)7!G82L)pt zmzcMlw>C~BH*=B_4KqiD?{JCZ-p&iEkR%7>{$M$h3`z=M|AkaCU80(w`{-0r{z2z( zE4ibFvo7Mg|Ktzh#Z|Au7AD7Sw%0@0jwi4u_Y)pqm-9Qa3XpVZxQUbpXnGY|&Fa&8 z1&`1s#qTUt-hMv^wG5(V0iEl#NVhR=-7iaE;otrV;_qq0lQ#8Q zq^F|~2n=I}f_^NHR|EswoOz%_lwit_(oQ6W#l@prQt>gTso#tbBE$%YiBY4uvTooO z187C*W-&=P`{h4_5qIU75Pm_v1?G=KaSuGD%SWr( zBgeZcrG3mj4oxmD;7HT<_kySe9=iV73-k=>%Gqfgc}#&SpjbkmcnK?j9}k!5>Pk5X z+xftozeyN+xjV|6fz|e&C?${&i4>_*hX^WMETW;PI98~Tl9)3#y_>GMwN1P1vrgPt zm$JQ}i{F<+!fX1oWNHjK0T`h#gS#8gP^BE8&TBQ9J3l|a*&BYvwS$(}JB&aE^LizC zM7EM<5DDMCl9}N_tv5pgmbcOd9R5$Ex@wot?aq?*w6_Gnt&|UbsV# zBZINEwpauiDJfK_#*{WC3OY#~ai?P>1X*YI_aZ|g`?MMYAb_^Bd^FllD)Trf`XW5VEP0xK$YTyRw+FKOX$9N*9pecuwRudoV=Q&$~_XsG*rT0HugOX7-XfUGL+(5 z#Xa#I;N3-%@j2KzIG~(_NlfM&e*6(%^A0W(nV)BY5q`HsR*vm3hXC^dIR;`|-Tn<` z+>=G!!KlIWHF8)s;@_(~U%dFaa%DGsuCQik*bfTdbHH#5jG5%8WSyFtvYg0VYIfbf zIaz^S;=y1rSSFgZKO5tD>pL5zA>!)+Y@h$c8HU;68&fn<8Fm{-e+l~71bwMVs*R_S z4U!N~Opl9$`-p6nHUngLU=vIv-C3v&DmYhvI3% zvEPkwv6eWY33YCE6y)b`PY9kNZA70WlCk#n_Xk3}uMegIUa=}WLpJCg-d6vG&sMHQ z4;K@51dwB$$Mz`1!g=(bH)!J9g{gtN?!)z}wAmFUdoa*Kd$3>THN_Zc0iXIBMRLoo zLMgL<5_^2^5yZ7=lL!BHPeoh$_9ICw=?A(qfkcq0W^~&K{DUy6A+)GUF|D?ut);oy zYweAp*yn~S(h;<|-BGLlT9;&EMk5)l6Ga`_bojE-cj+$S8#nT2T%5V@Ttx`mng6hS za*cKTK}_z}&suqPoWa%lCHzM;a-GhvJxen82krek`iqdB#4cSoXq+6-7)^7{cW5Ns_33_)DmKz9e3c~RQNd|L{7LsEFk?)^TqaT;;oT|Wkx2t@rw!n3T#&Ip_35~1ZOyRkA(O4DvAyTzccc% z-dLGi+px&dnqa`PV`*m&u5+2~aHVkx6E#j?0>zU4#Wi{I;?q#3Z1%do_~H{mxXoQHpSQ+C#Mae&$itw@=&bNnfpZqsl4)n~SJbYko902#1!-jb*pI)Sup_WwA z#r)@iY~g-E;!!00s*aL0qe}PmAJ+0wLcJm0j< zu+KU7cmNd>@c^r5iT9vmGOK}Zjl$==9LbTrgB(R6eK=_a48xD!w+Xh^RKLg{r-}+Y z?|se37k`tG>Y_Bg=pCSE#k-xxgOzuBS>CvAUb zp{5_)+?E52d@r4P($p}%30~J27S5@#Xe&klm#3NmY~a3Ad}^m&XTCU~SnvuqBs9!? zcTP{uC|N5}YLx-M|GgI;#Sq7$dXLfL(C!^}uU)NB7XJ!m!GwKDK9dDZ)lpo|+QY?G zlZ~lDE+vZ8is63auc@fW;t@NHkmk?r~3*H#*p;X_#aUCHU9@DorQ))kE0i!s`d*7f zZDcYuNZ0O0D>bb-vh&NKy67m;1mPYydPLfcVoGo=BqixeK7yhFT{({KMuf`FptR;m z>F`(k^dlzys{htEpTX~sNJO3|SsOL!0!1+4;?Lu^PbGKn*{Igit_&i33(kXUU3iz6 zT-4<;SQ^}DGKsc)#f_2q^3E0XCFJz}77Yz}PuP9n<(V{T#lAoav>;U!G@^K%l&N9U z;XQpN^milDy@R;XkDlUSzke^Nbyt$!K!odoDxt*i`ln&8Q52f^s!BW5=2M9cK)x2Pwe{vb)cLA?$(G{N^8MWv#d(k+I4Yz+ek)<^uTt!;xOg9Nz);CKx{!QP#B! zW3y+G|2z6cpauM6scd0K!gsQkmX@&7Xm4*p$ZaB(WSEq+G!{93-2S75C)w)@jhgBv zg)5~m+yS24FpS|&%RN_nHO~8XxdXamDOH=K1zi8bZ!ob6#Yz#ugxVX1C8>F&?dImT z`gOs;N%H7<0>RnY8CpRu;3D;K)spMKh4O+|G7SfmFlJ?^uq*I~`-HU`!rHW<)8(Fv zXEnkIhgk(QZX>#Y;@&DZl{CSdwERl^DH(oA&3C?M2BnALzB|+ug4=?kB9ZehL>QNm z&25d(=7Qs>0c^4b-Dxi2#d>wzk{*&l^RflqtByB%Wn^WQ(>SGzXJ8Af`Hm3E+!cQP z^89FPYbz)ynD$G`$LHzstdmWNYM~xzz_E5D539gng4KSkp6u*ml7EU3!vzLyo<`H) z=8vaxAohj&9^lCaB8^HAnl+cww5j4Pbd4K}ey!CksFzZ{(UiqSqHrlxWj9@WlUWS73Bi zhJdSuni_tSbX-JaWSWp??e$+8!wXkWz5fb1pd#ie7-9NF>e49Dv>6^6g%UC!xg>Fc zF0hH>!?xq`<^++fd=3TzF(Pka)oIg(3T?W8<$`zGAbCDD`iRZiFb&OtZ_o6tKW){z zlu2_c;z#61%7$Lif}}Y074)HVZx~0j&n-}**tq~~wjjfWI2aw4Edc|)Jbow+thkN< z@wgWxcgMsp_24vhEj%rzDLoMW5t;pA+Q*jZ%5g|HtOnr_$4S6BP?Li!Orr#?E`G)q zRGa)J+4e_KdU|@4{?7_>Upk~nx28;nZW~&%;tFaNra=&R47qAlcG?w$jl&Q~>uCj` z>__?^ZWserU=;pOKS{aK@Hsd*{xHc>9xu43UN0+1;1=X`uaOZiudJAwno{_ji`O)t zG_356r6ET4e1lm;tq8IpO}=Cblvaq1V(PY6Y@BpEma>MACJ4c zS#iW6=Xcr}!gJU`aMVD0{X=v}cxT!8jMY?P34xvHPxoBKV7_G+dl2CTIK{8}R%=N} zNPPWTA1>O8#g3~CE2sDg(_G0W?|dXVoDj!P`t4px+DzSKgn%lbw5)7hJu8qpusf7$ zj|8Ur!Rs&wa0+>NMj#C=$^;@bB8qvT-O0kNUlfMXO`%)PM|<>clWkfu_D1OxVt;Wo#5{7?vh;kvwhD# zXP>|B^V~dvFyWn*$(orpvp(~EgFh)qq9EZT!N9`sMR){q8a7 z^UDiIWl0g3(hN~n2HGGM}4^GGNPRnz!3%pwe$DSi(cD&BN&*k^U`9% zs&2YRE#Oc}*|?PgU&FX6ytl7aqZ3MeYSIlG_0)P0G?s-*U7;v`i9}BAr z9vQGSC55kUJ;Gr-h0Z%|yFk8*q1;haAHf zbNJ}O_w>I%ce;onC%(U1`^V!~QnfDx7rL(Z2uhCpZua||{_)Vm zHitf*K%d&6^@z0W-9q=_NHJ}X!(~2gC|($C(4w}-+hn0}U{$&ogw^<0X|x%}-!)-O zBd@{GTVm)2^jr@TdK^q`_5&+vyu zrzYr`5G3e00hS`0R4wVINa>@tS_Ps|Z0`Ho*G*RH6Cfee9Wn-Qh>SnQy;_Yg1jRa0 zwA`+7i&5G#BN;f_YW_C*jy%NMhU9O{TqE_q5CrHd@naqIdd0~XmxL6hV7$CrUdZ=Y zv`vvfI%Sb_SFX)2bs66rb(KAzxNBwFr>sU3(7!lv+|M2# zux;`$+u(X%D#%gWQ{8Vrxh`>W?Z)`?4xRR}h&-q+!H@qq2H^1k1+qf?Wl9dl8m##N zVhcL}9`{{%q`9cCt(>XXw3+Rr;N6Q2+dh$O8QQQ{2Rw-sVTB8VFaB!&B3U;i@tsdV zvV!x+3dVJ}#p@=#dG`5{8kIthfz*Qj=xCV|H^ul=zSwa z?oC~=d1RcW9O~gD^FnxL7P4C?Ltk8!`GX01$&?Z9Jp9(Czv8r6^sR<2GJEbz#hOqVTOFM2*mCR0 z1^0&+Jn!vC1QLaieDQ9NKmKjmCS?nfcpLG@;DeqLU%s+BfAA+fg}f6|h5z%KuUQo} zzVHvhLgorWhzNhxvwL0c``7hrc8Q1^-=w(ad_-0m1%V{1Sc)y2=~- zy9ks23xfA|fg9!epK8C%|C9A31L&j1-(v*kR0CRg3?#UM+~KZ@KMwldF+<0M?j=1O ziWbGAP3@aQY+kYBz}Q)p54<>yO;09-c`0;176EaWIL5h?EcBVaH4WE!*ja&P{>}^0 zZoAU{Un*AmLiQAW@6k{iw0*{J?KhHbTj!d5aoFsbgrIj|C}ait@W0fa6UE98rQ z`qGJVR1@Y`MLQkBFOBau4lu91C?9rCmc8FRo_02!v`$4Ooh(0{nrLrSrd&`!u7}aI zB!eoA&zeqDcj!K^I%jMVX)nMLh#qaGYCLtK*;Xqw7K2pJ8Czg#91slnj**%H-(?*S zEV*yzf6=*O&Cf^cQXWnj9Mn*hNrfM0?djYEyz!E70>Xa$vMdCZ7ClZRKLS1A-yXSc zByo#auIzZD?r_^tvN^;h&o|O8H%v*D9D)T2BI}j53$aFcho>yNK7=|AJ)o!E}zl z_lfPS*Y~m4X@}4O`kBevd@Mzy@1P_wep#p~--yedRjBL{NN;Qgr_x)ij4Lok zqfD>OT~x6iqLTg8Ftd0I0eFt10`7~d1=L#>{S=S-0)8b^*0VcV_D+2*ICcS|ga$c~ zU%(Q+D0Yx+pTW^1K4%@*mE2@#j*A@mTFU5s)b8Pk^?L}dcKG_fD_Eu|Tk&~3eWDjI zZ@!0g_STi%tzO+UQ?iLkkg1#I$Pu*iPp-5*xI##7&ir?IVI@c@|uOrPnu0&&KI1?0HvR0PP!_&TjG1&M2?Pd)zf z)vhHnGeNV+tcqql`hB_zly2Zm`aLO}jwOB$YCYQ@4YXVmy1j-5lv-i5HRbTIjtmTFr@l$94BICe*@o{`LK9(ZZVo zfqEpjs#o!WHcmPySRhpMu5CYNqAeO2&>QuJthY0-0o*;t{(^Zk(cK2?8xx52dVf0S zIkL1U`Sc%b5byUBL!#S+Xm))2PQI|72eo-Ud{gkDRaQd_^u2vrK(qBVS6~_xddh=d zo;;?uWl?)(ENaz?6lJGM+}SbIG?(Y&QYN1DIF!-ay|qSD>WHJ%%hUa#af9*OS$HAq zq2ijvhXc}z)rfTDYgaa7RrS9IJk{b(^XtooUn=@7fiqb!UaXp1KSi{(FQby<(J_9O zQ5I`3P+7O{8O*@DhW`S#$|Dsy@}n=HW-sy)A~sryG#0amW0~M z^JR}go~%^!?=s07W#a6clo^JmTKqj{g)ZDr5Ukn#Vx#UA<)&J%hN}N)^?pg!bPR;&&SNOiykn3wR`K$ z(?y9RuHyPe3)7;o_z@q%J-^MvPLtbJZ!#s!>g<pfU{ICcKFWy#L%!?19Fzzz63Laznp5OiV?LS*~C zeZcKs%fwncI|AkN0)phqA|6rq^NVpah8e?O4mkai-hd;(#F!@_A}297HxKC9osuFxgR}+k3$fuQVSt;LhvLt~ z=WDi~*7gAYCm_!*t&6oNwa~5}T8~^FmGF?h$NbU0sYV@uf}ut<65mOI8$J-Mq!QfW zc)rMNwHe}&o|&(5$p=KT$=LS18Z>Os0Txv7iP~(|HllD+2VmIbt&u%U(LTnYADQ68 z9+%m39X!P4=UvB1n~CtLT%-f;i~WnJ{9paJIRP>G9@t*HXL%(BHLz4Y186X)EQ!v&2HjgUxcx(0QWwbLrBN~DgZ)*|>X<&|D0VP;x`nYZ zOe#e)ecM-XPjjejOPs<6uZ?Hcv0%|;0j-;Jwhvg<@65SY6T?ngVAxCRe4lp;O~muz zkj^HqmCDs7*P}`I8=VQv+^37g2|slPDup^P$jiR&NVu}lZD53Pf@y6r7W?MUDIE6-{+vVIa#=CTY<(h(43hK;lRtZk=8E3Dw^G$mQ7^ogb&Z_w3 zNV*W8q?-zkUYMNqCarTA)aac}!xC|iVA21n;ZykI^41nV#++zDXG6Dd?YwpCl-I1+ ztJ>JXLk3>|1^_=l^U8y(x{FB)DQb=mjAp9og^l4VG!oc*m~%B8go+q8|q>K9&O zuF(#H-)X=b#vj0`#+XQZ&ew&eh<@NtOWR4H5-bb_0g9kOgWmBTfpLJ+b}fGlN&ng z_H?`v#8q~?JC*E8yxd~bF>;{f;sIU6c@G)E9xf0}L?QCdr8T<oV|>R>DB&mW}994i7X%P4qXfH*wIBK>+<%}w$CF;gkZ`uO<`%V%|3akv(6Fs zTM|7~IB3QmJGbX&H~Lf;)~;1OP8Wl{mOIclAam&E9orzhG~O7~!)|G5R90Pdpe2jW zC9V3oB&Q)AX-rOwJ}Wz^xOU<}u=H!(v^js1I_E5S0PUq4*a_G2hbnB9lHeG!1?eug zF=g(i$d?I&@2)tpNL%73dkNOEU;TCg*rmHYs@!w?)MEz>{{532;Kn2hEUnNUnNBl+ z1Db@JG%aJGinieB-H<>%VD94RicOX1xQIhJNC zRuNPK)Se$uO1DdE+wCj4GIT-Zz;G%hXEbt!!vJ~Ty8LQI5=+V?gkkZTjrr?sU zq{-f38fBUtZfq3bexiDvyT_BS4d3MNz#4F%X7vWon1mAaw#=k`bu7qOG$(c!#M9cAy&Dv@NB2*{D67RRkzE&x=y#;2=~qcJwy$q?p-I>+yh z(_^Jw3JB0t81vrlahS9`DL;N_l^UEi;dzLjT)t9`2IupdvjQU{uD2TW%!>8CQcUC? zAkbOyI;4wS;7fLs?Zqp|?Th4vYlWq`-3xzWwGHw~?t&pu2aZK`9RIlVdc}BQ+od6G;Nk(^m=hcS{ zaqXBHGy9iGmPqpJhSSTUIBC`!vHqyjHQFa6S6h|9Xc-HT-eJ|4lWkrS!GJD7(eZ71olq4OUhP zsX%BwC7qI)zR&c^dkaf;0-N|MYH7U{54yuiRP))m8<8r`mE`AXnTEB{{G%qUYfhBU zaLe?ZS-IWM{hEn3HG5MEM`bW8Z&=<<8TivN#qtA(3tu*6OO&=V!8_Tib0W(aDnLE9 zeQx2|W(`dV{xibq<<+++le<=wR-f_&>Apdkhe8_nteH7PDnIq}L>StgM`+q6hinmd4Ahw+Q)1P6YASLs&HL94hkM@!KA!4XNG zxTd=3K~~c)L~o9G)vx!Ap2|puFY`)vv-2&I?PJ*x;w;~wf4};aW`M4P(uM17?QvAq z1TNm}xHc2_sl?nQTT;lN%A_XDj*T=zypcpNJ1r*exH3aw4dIBctbbQ5X2C?h{tR`( zulEFJ?vJWW7qV(tz?o6tADpgJ_H|f+=Wni}zH`zx&;~ek%_OJZZ=|E~?t55D0pRJ% zIVok*Wj`{U*~CSwxgUQjRt~lG*EDATU!aHX+nVi&vAtQr=oYKyv0Q@ zt|_F2hxl{JPAp!93j)$b z9QBFj1{~W)6KdUsr&ckl5Ck@YS{Aj|>L!gwtGJ|(D5QfXzhf#54JmZ*utq-o?A0)| zI3%o0N@#MNXp9M%>p4=@wh92Fn}*=`>v%IZRv*fd_i7(Ri56CHJ(VW~t19c>kT zW>P32#ZKSmOV%B#1_@Fa-|^BQ(>Ih0@Cxr21u?o#uG|le^Wx_uM_&FJw zh0@2XZ?k5i?)qIm73Ev+#-lefTj!%=H#~R)2}R$m749Ya6$;aCe%|hFfBSvaX!y#a zj|O(ij25-=cUrVuCNM2EOhkjGFD$EZ-s z5Dd^Mt1yMWE_(ERhhHLc1Heuax_wd!X=KY2TxXMkRnI0JDiX|y*2-P#ih|Tz>O*GQk9AnN)PagRS4)k8jLvR43N8O;scA3>l;!4>%l@GJ_{)#flnI64JEo84DLazIjhuSRLLn zWGc6p7gnEHJeDNiWjQu+5vsen*m<+7Q?i2t)Lz*aW8jP1wwDDtr^7wY8_nyq#5Ox; zIbdjuH1Qh^zDV**ea~iaHhwh~@CD6l5mWlkd6!DQdxX-TJp{u*rv1n6TH~a#_}T;W zXWVEZ^pb+I+-zl=&-pz5>VJf_o?z5kwa*9>HF0#=A)*~qOT#ODmcmzh~CE+w+n6{tr9eZe2A{Wpnz+O56qEzA%CXl z$4z8Dql+VqDZj`OoNR6-;8wf6&eTeuhP!25evTl3UQzKS)DM+h1^gO(L(^~yxpUPU zc!n;6H+69NRzo8yxhthg9JHMjei2Z2z0;U(f zh!7?SVS+H`3)GAL&rkaQPYXJp5!(MoFPNHr)vqp|zYSCF?%6bYaoE2|4k3}8<0pr; zCpK$8y*Eo)f590$QweNITtNMSUAHQ?FW!e91`K7WGZY0Tex*fGknRq|qcO;{t1|~dXqes24JA$Yyi2gtiI3eG0 z({8Am-_m8UHMA28$=Ir21z!F}56wVa!>XJu=aoVy`=tN`*3W@RR*N8VLZ(a8k)4Z* zCJk*yGOy~D$em@P1(Zdi7M{~vnAQJIgMt@lmplhctl#hy=)+xRu%Y9>?e@MB9L0Xe+6<%3$XNPd zl0!H5PA3rQP!;&vTYLWAF#!I^$hJ%-<9Rg*jOlc=MH@d@4?Y@OfQ>-=bD;em#MI1i z&)Dv7KK0xp4|4XW^3cgwyjdHMBDzv3!#BsZ*oXLc-OJr*OJ3Ub_usU2$R%wl0pa_> zwd`bE$pwup;ELNN#2FEjT{-H3k>a{UE$6kJ?d71`s!N&Ovouf|L)Rps@sKyboSg1w zmeNPSm_41?c{=+mg`RT?1L=-T9uarh%a_Bol)r>8i;*$b9sk$YZ4rc`w$z zGfa0Jz-88Vo&P>}#>uelgVI!=x(b8fF2VPlg=S_NzWf*k1>~KPcf1ZSU8b*D6eHZ3 z;|hVDZY2UiGPCm0d2r8({g(}6lq>K>H*n=WOS7jVN0ihqWJ1$$!=mDk9F6$z9F5Nh zqR529bG4hS;+N^%&d=Y0zjc&>Zg4(vsrXl>b{7xxcOU%M$=(+(a91gdbO+}VSo<1$ zNmm-=joHC4>_wfxE;aA6)(x$ZJ=v-%Qs!2kGBZZ>P(MbYik^;JKK8GQR0tXQFleIA z&o6immU1uJI~G~ia+nL65aFF0F^by1_po>InzJU@{*h-Ek5nP&@iE%V%Lj?CQ>u^* z7<6fcO^=voms3P3aMd+R&#SF~G9bw@XZDCHChhEr1<*VeAL zVC0+V!=Cndz$EfFHuXRAq9*)Os1W)EU?Q7PT&F^Mr4k!AL|_H(>88aVq=4VF-R(mm z^vTf5KgWpHH~{Zyc47StXKg=|i^XhuEus#P3lFqPMjZE$soRm}j#e+!<;rU-;Oek0 zAG=_FK|~DmN4WX-kf3etA1!x-zIJv0%7kRnB%4j7$?w*L=;_o)|GvVAwpjX#hDTOF z`v?Voyf4z((6<8^=@pT1v!p=+L93QiOGUgQchr)iUTBc(x6xtth}=VGfj3p8SUsFF zr0Zf$e9793Nhkv_O@lp{gJ}8(4VEgfG$cQdYwoq>=+9Nn0$ofz<(~wTlnV^71)wii z&tClcgC*BB_~z~y8+f^)g5AGA0|Sgh!!Uk<^oN_F&Y~b_3DueBdQGkuLYtITN~UQN z+f+WZEB#I>BdW-`sWPP}jiTsB(ML6&06TqMibg$^#9H^L(Yw@VI{2^Dat99K##D6WMc3lFmasYJMqur;IVeEM42@tSL{_X`B|iC{HBFJwuA@fF4xqb zk@#BJLy>%ob!6I{0NLU%mFH`gROy-RxLYcBe&TaG%DJpl`ixrX$w zk7}vqM)~*RNfwP?J)9q?e3l&FU0>XjFZ7A61m5(!?TJEC9w&FsWuo+pJ*R`cZ`pfp zbYbvk7KD5swwKV3Tp(*p$+l{jsUe!TseLi#P_J!oMHNaY8v}d5#2qpTt|*SZ%}sy5 ztRDkHwcW0khUK_L?PHxwF7=uFwipXtEs!|t&{6HDJ#Rm+0bV{+9tP7;CqbM};ZxH8 z0CU9-Sar5sI?(u!oiPWi;?L=sa)ih_9aWz0k|FE150SP(UbWI*$OMI!zSd4L4)`rQkaJbUxo=WwUhfl<4Yg!cis+AO zyPaJPmW+gZOWq>WWxD)gx~<ocr8M<1 zWSW9tvM$I(j>k$7T1Z;)nJDu6j)xF2K?K?W^o|F~Dn_u=IJW5GF(G@LKp^kz{R!v) z&4caCeApQ@XSg{jTRr+nBj9M?PuaU_Xf>7$b9qPG$Dg#=5r~SKr}BK7Lh2|}XeMN` zf?rZ`#KHiKcV5t~X7tk{d!ikrFC`@~wNS-b@rh$u%&-}cwX$!BP{%z6ZQ5Pghc4JxZkWT9k`sf8y(dnhEF`0CcS|~xDR3aNrKD;=N32aw({7s4hs%| zj3eW8DP0G=GPkMhDpDgJCIPw1o zG^(7sU$yXE1eZnR&Poe#HYL+31*+{_#*uP*tNMB+s+C%H_9llJwK!e(SU)*#3GJTp zmcAt7(W5;JL|7dyCMIS_+2xjbOhE0BDZ8LVMg&PVJ29_VvleUp&UdFg zKAS`37IvxyOzz{7(ovj*NL`LVM|^Vn7f(lIRUg)Q zj4n0-@2adDOt1}j#BUM_2FD=QJVDNRzHiN{QkJAy_Xxf@lH|y7BgN*5hRrx55e|8&6BHS!+ zTSQ!(W8>nj$;W6UQY|`b;tg3<7je_!v=AjBQcQxw+XT0@gmyrBn8-|&Kbqn_{bCKO zczM1TKZPBM33+Ls=Hbn*lU^1`c&0Y~R{~l>Uc2jjdzag$y4%)Wr86=@(fdPIE!dHKymz^KwaNrNvo?6UNpmr2*C&ZVht zue|Im`K^1LcSvwJm15O_xe=#9fEV|fnjj2P-)oFLi-yhM=x%A$1n|3%&j)N{BA|_%lH@tWgCh? zwIBZz9)Ot zO@A6OC#nFN&>GbD*dUV#(4{sVdk^$@7V)*7H!uw_>^e3-hyH`{Z5z%O+HW)oW#rv> zkFJPK*-joI5Z6UhdX!YMqMlVn#Zeh%}epi{7vHIH^S%k|DD7kB41eBjOd@4cI%g zF<)N&yRTyqqqjWCl!Zd~pXh5DToGEhrC@cDvifVOF^r@wuGKBF{YKGuHF`jLhMLBskdC!=-aR}@Q6o4ID^5&TrtT{c7A4p=GH{aVjHAH#6|Y;mNN3S@pe->#DNxY%fy`0Mc`k}h~6Wu8jf?hFmw`);ZY@l8KFS zzno#i4`?exR8rX%MjxfV041oO2$tx`cE#Sjb~&cbr4&)RS;2Oc7d(YgqN$8?`!T(H z7S!l+C!Gb9-XnyuLu|d=*jO_^pfuUp^@36OuB($wuzz~C?Ab1U%X-$H0B4%T? z7|S_c3J$WWil3CtNTqZPFf#e@#7BsD@eW=kl9fwv_R)3F1h_f*^v-;8B`-__AI>VrIX$qdZ3&u49qS2e>87DF$*_U8X>e3Zh(Prl=hxWvj*bIJp>?k zvkJ4yIMCgh`o}|97?u>%a zLWb-P%vag3y$JskB9sfaZo3@f1CTvVd0&;51*-vl*R-ad%A%n+8Tiqr7iPQQg(d@dOM8kp!}9 zTBJuOjX%Ftr9aUtPd{P$wpT;dCQGG+F{T>YKyqgW`u{Eju}(7i#K<+KdFXXehCj(9Pi z)|~>;+KI-unO>GNL*NQjQNyd6fCfZb(*iiCwBOdX{JF=zg z?#yf4_yY6FDFAuxEIUeEiB?&yu2e(NNv2qvTvgp^otaP)AwbZ$O1sInn5E0Ko7Z%h zc1L2dJNwU@Wn`w8|I(m6AAShgy0KA%1MW%h5iff+yRUbR6Vi&~O)QF?qxYir1b%_< zDh@?U9tN@QLUu`7WUxFoHA*#0%E+vryX z=->=6m=Zp_?FUD9sz!1U z2z{r_{^&lDCVeSonbi%Z=>&Tk#bmy=QivE6960VUbIWd+C;W2$A$wA`Qm(I8MQ3j- ze}#dk!qg+Vax|tfHP9fIDUoPadbCi!=9P2_4-bXZ^isk$%^Q;xKD|uAQi|kn>jqaw z9_kYU!0$l^X0}R<5qlx7K}bc6_n`5eNksSDoQ+#&mdG#FuxMLa~EOrSxP*4cB_R zYaz*x*ZCSeCXJd;^)r2DcK|6j?b=d@oeWT=@KkB+=UF031IE-Z`b4!DBMP~hi1TW| z%0p=-WL1eF%z*KfX0rJjh!U-T^E44rTjs^9oc*$x~eNl4MaFMzFBuh`Q3SR$Tc} zWpafOep$;<;O;xTm6^1S!>VtgtH#8+!9`F(blb<2c30lO<>X`L;0eEZ^49AWwOleD z2^o0Z<<7+ zaOfrZJ`jRa;UH?j=)`c-I)hy@-CbIBbiG91p9xx<{1Hc=THM7G2yX(9=BF>qL^c1j zNgkNem_%NbX4L3D5Uj8a;axE&8Zk=N$c}O}r1r7!Y22{`t`Kd|?U>ifiuPtq`phOz z6-E4+DXW!!XhIw;F!5CF+2N>~I5{uz7ZsY#42rR4vrT!bjS;z}FgKybV_D0J{dvv9 z%`SgGRQ{5$8d-%Klv%HUzZVzv)-6ie=u&tmS0Pe7H@sj+fh$Qw44SLHCCt#(yXAJ$ zvP$-^lPhJl%hTJw75i<3JbtS2tid#CCb_JOs(@F-AkcYHp#X^0WIt=0M{<_LZ5KYy zZeYk?!_cehvWtenyM_&lXd^QxMJuVPeX-;EERt6Wk}2qCwydPy5Y%R}@Oa>a>v_D@_G|z{VrK4&V=usOPKNL6!@Q?No*7Z`#!vDd1 zKYuxI>MF!~$c|}AZ{isvFk@*_mAw#J+v*V?N`0?IA;$q{7K;sxyQ&|^CSb(i#gnet z`52bq23qRxNREu2)jzoRO7E9bq{-}e@`_lvtMriNA`!wA*d@Ez^%%DpvO&%vxBnx4 z{7XJOSKLQLyEZmXM0B*a-odUVV85w-@R;CU))qKT{OOTyJn)qxP1bfp5iSG&B?J2! z#RXky&#g(KOcomNRT(3&(4KZHtU2cG<_2w2l8|YExtS5?rN&23y?%iCjM=EaO11K) zDsEBl>-GW9p7A%d<`Mvl4!q%TL8-a65C^%=;hE9|tQt@JJq67G6#i)!(QU))P9?U| z3hAL~ha;Zek6m^$+F1!XznF)M)f1l0<<)wL6_D0hq z(;Is_QRsC9mL={73vT;Yy${{G6vk=3y)dmyr1@UuQrI%C?*U@Gm8ojC@t(9^GO9$m zCjh@cjuEizU{8V|@j{8Dhy{2w#tuC((gCeC~af;Xdw_Y zbb#lph7aL3g4%9XUr@JLOh)kk9MOHn{7$eFh~<2x5Bo>d{48GQg{ zpaqL7rSnwu;#+w(BJZk5BXZhdUL>FdyNL7KAHRn%<3&Ox*IJl!kYFz)kG;{|K`w93 zdZ_!oyjW1Cu?uYsU#M=-M(%s#h%L-YYw$qm)&mKiGpD4Ua z%eMO@6kh@sMzsGSv<)%XD<_jPy2es4Dw>PukO4;XvoZ2qNr9gu2_qUfMLhHndOc6zaMzy2=Nkqe zM!qCXcD^!^R!)BP@Qz$@Pz-7>tvU5*`>fPrY7x7Bz^UdHB|3FIZZ0M2#sYRI}iw@Q5Iu)}ch*WgJL$ z13Pe^*^t!B zv5E**wak|V{ZeDl1PS&$6~|$vLd5YA1Ng+lNO1MF2|2m(^JGw*B0R@8(6IhOzre}+ z$S6tsxG3GCS5tol3^iq0B;Xmb*3t?!i_< z+F1e-UW_?C$M~(#n$1$`qiSPNk;$Ij!xg^Du!$nwgqllWT-=a))X^A@aM_RSnGQHo zo>7cijI^Y3d*cVQCdHEyCLS$>4yW~vb3~4+C}lKu zVcohE&Pr3VcO|T*?mqwlbA9^>F%yJe4~{@>-WSI~wZV6PoQPbfhGfbmT!T$V6w|iE zK)cp^X;cuv>C1$wNJ>c!LOl~2ZC(r62*Ul{V(F&7ohc#yPga{FEYu-N6{(N!p4;va zHX$8{E<0?zB6DGk?T|6-35bg3h}AdVnI^a}w0>Y2qG@Ii zxMN(x>il$>8KQoYtje$;aTkfPica}v{kuasIM$f@+=Nla9elsFl*Op>fDtpYHWt+o>=Yv)fL?|&7J zv%Vd%S+G3&Ylx8jz+7ccxKJL;c@n($OPm5vDZC_&Thw4URgxyY$y+0vWneDnU7< z@)K2?KrRB=R1Wcby7&p=LYu3r%KG;A%P#Y;X$zBhk;pC*OAvuFT{uNRYsIkp7khJ* zsPQeDY>E_wh{oy+X&wTUCU$d{#O8QzGBOj$%kcGFg8Q{jt0GB?DhT)$I{m&;9b`E! zSmxvB9@6}Y{mQWbQ_zv#pB|*vuWmN z)o<{=+~HYLZz2eGO)8imw9P8=Wz8t-sEtOE72!FWklFBGQD+UNT@x7}a$!DPW8jSZ`^q1Uev#BZp`_oaeHdVYvx5< ze>U#+kH(oo7hQU7m%Yst+n%M*b>Zjrz6yMYM^GrBP7pGiM!ZIm5svO{y?#wghUa26 zfdem(MQaUP9nJ=)9DcpY2!NN*a?w>BTiTXj8NeHteO)I;i3ElGgc#65zh+Rj{^tK& zWXF5j=7Pxm@!aX8j#{c>SV{2zO<44-!A`6lAU5|be%9@_)u#ITCSOk{KkSs(V60CE z-6#WuCHMR`A!={tnL$b-At%|JKd#l?CB^H_5t9SgQmp3fP?KbiS`spM5(o~)4CvEF zc{vAo$oqTV+K(>2>54&YiWD67QmfLa&ItZ*_cHxX!6n?Dt~Y{C^!5-zE#AWjWx8!s z#L{ivPc4W6$VZ@O!bpudKEmK;6Es$&f`q-?x1vRpGVIrU7BBU_JHXN=WQ<_SI zuBOe$+envJLEbOk)#6P*j7;kNJ~){3+RWhJZ@-E@I0Wf=LrD*RdP52O_znk;K>aaL zwAtD{45bO|&g-f{8!4sjwur=pf3%oKh_dqSj`Ewd9<>colKtViJ>>SC1O8*z3nLg6 z43a#lM(-=e8DL|#YrqO+JZ6C*ypAWwrp64OyQ3unPR0Q@jhmCZ%QP1y7@A(DyI43n zHiS_}TPjwvx4zh?D;9{*Ad^yd6b|ke{a+(`TpS*JpKvYa$HMluPe@mU%<&AxGWWRL z3u7r9(&TmBcwlblpE)`K^p}zLhxHKj?ZpoPJ9m9Z+fFQ@1m=xsmwKJZkTTLsktj0+>bFZ)s2J4}F53o8?lA4N3GN4%M zDb`phF=aPVx@!y$s&Xb~#BH4|c3M-|P7}tTP^Kv=Fia^wti4WCr)QmdPrhF}Mot12m4v{+a7@8gFp`<4jf<$r zmogFM+NFidGjfMl)jJKYWO^cbI+Lcjn#SYi<~*k{>z}`96iO`76$V799Sy94rc}_H zh&!?pz*w9GKe6uK1UK}MaGBin?_@rryf^K1WF<4_qYW70E-bzK!MD(rYNim!6VA8! zM@IPjzRV`(t&3xj`}Bh6-Cue=sbU&I@>?ADhl4x5$~~Trh(PEd9K;*oUdXQhSZdE@ zJ^uK~ye?bdhm1U`34)qftXi!Aad1|zj6kDMNVv&)$@mot>eNssJ+ME0*YRua>ex~Y z+TyJLLL#-c+F-}r+N%bgFuB<6>qs!?E^1~i+-DElxwFbxy`LKQ`^mcR8O89_XY+Aj zKB)-2wT>oel|Np|-LxHxLNdSidIOBu$@Khmu*yEN{w3Bey{7uhy$af}ykSW6#F+klUwQe1eX`XM(mMZOs>q>& zIRo{!C;{C_t?5bIphZShi@BypeN?2)LGfVPXOU9Q(#cf2Ka#osOEM}|m@gDQ59?ps zbC=j|`p7?;H+1RGDvbXj58zoHu2`()SZ$UtT196Kj8rIw(f5pD3IF6tGRJqP{(ss# z%b>W@wOxlK1Pvs(li)7Ff;+*w3GNWwA-F?GaCc~|gS)#23GVI$Z`|EZlbOlv*?Z5o zzpC?{Q|HH__(ydY-EXheYu)elTsQSxo@!MU%hhUgrvP^$; zD)9J5C*JmLKPF=8@c%i%Ib|>AeSG)9#jv9MU`JQeg9;+=w-j+xA`7PLd%B1xn?i>K z@I1qOIp0uWpo7ZJca4>JwFxdOOI~3vfCd9d*(_)G|zJO#87+PFjBg)wz3Uj zJk;rvF`LteeHd#U-@UI#T1&M>C$4s9{WL|KWMVZOkG^T;!Rdm#pSw$or0zoWa|olO z22cDy5E~tu_Aq$d^_BCiPt+7JQ5qoihto`p@aGHl)wQ)4j_dPZGPuOgNz2-B*`g)y zby}2J8@*<1-?Ixf7cLTl%U@;ZDObEWWw4e-Oy02=D1csggT{D&`xyLrPmfCz`CXeB zW|T#Ea7K1=QwB+1!Z$0z8~rd>-t5_itq2FBMHJIC%bM}Xn-ClWqW;wmiYLAr0g4uc zKXyOLA3`j*;evgrY!fYPHF#+W2gul3o{;5dKxi&SLR&u!&@tVRWNj9n=us>$u&uoR z(t#}q^N1GudgFV7Z(zUEMI&^$MzLQ5fzFYkB}z%(ea5%$Jtj{c-0P5Psei!u)~^BT7lM?!cRwpNL^@;!BJKUGxvyyuYSWV8xvAst><3h^^`e zb0LJiy5nCuGK1?JZ}G|EbBTR9o>sPdSs|?aB5#;NT`czJ>3QfM@lhn>h?~|(v zkQ3yC5cx%_DM#LjlH!T1S~3iu>c2XFk2?K9=8j2%|Jc%Rx zOpJI&r|Yh0T(yhv`W>A+vgcx@tDf`6cj1C=6qpGZ@+rIqBJSjR*ox69GI#hOA5%MS zNlkk)j#&H&Tn|H8@;L)d)8Sj}ZO*dJ)9fq5^~1O$-e+d)-K{#8TWyK%%=@@#mYF3| zSY(sZ%H|RdqtxOizOUHNW^Js2#a6z#7|J+(y6beKj9S6%DOt5O@EOpgKph-^lIgj5)DEgn@5j24Hs!TuM;Z@m+pBvlfKLfc#Pjk33B&XZ#%!{`+gj^ z+a2N|;=fl6KzGJQ*)P5i@jh6<9)7AnS;jwt zfA|g!iW9mv>W7%E{i1Py8592C?2q>wG5-F!|ID!dzrNr>UogK%X0{>9u_b_XOyfdd zFw>+x)rI*yj^ECM@^&D@O+DvQ>-I>+a#G)PXCNHEHrc)Y+0+EVM z*UE>Qs4L z5R#nc`4!4^3gdT3?W9Y+*KGQA&DhEv@>V@Vp-r3Iq7CB4s@H%D7nkm0^BvNz9_fM< zcM$J&nWY>f>0?Ip`C-L%Za;`qiH(D2Udc|Mu$90V5rcL2#A)6>$>*pNr*X+h*CM_E z`EGbXy91q-5oJswbu03wX3kG(*7DHx|dO#(f7GU1ynD${XdY z>}(F&U4?T;7LA)0bDBgJI)}$DGBgkJ1o&TWzK$GNGDwNHWXO3I}r(FW#SkeILCB7GnoIx@&?dd zM2I-5Hsh{k*^~{$BO{}R<39bk$XLMLw!&bnxH&x*^0{zZG*tdo>Zz+4c{i_P*KDpn z-YWlMprp-g=P4Z}bc{ALHoTrL(PHjH;1V^8+79Gz`2t zrR8!V_XE%*K@WWlm;3g`hO}-r8A_71CRQrJ@e2 zezCGBWsoZJ9_fg5sEy=lQt}Ywn$pEP$gQ9@WQ$iO+j4KQ9p7Yvl*GtVyB zeUOXA5DO}M0`(U>>E!d4MSR1N0Y}>K0k8km=5uc+FSr(tgSNQqYzvPD)lpOT_TtsP z9Raz=T#!KpDs-FCvnmKs5s3)Js}fY2JU$-i&=H8c&1t+7R+TM7_IkP&z`d-H5D%=EMOv(yS^ju%K8>^B3iG0<(bMB}z0HrK!N79X zh}-TLXy^vxfT)RO&og6ox^F%g?{wZ}*5i?S+z_m}u6|8ufX&qEwkVOhd z!&+>uKo{w;*`C2*H#V;i&*uJd#AEt!jv!yOz{>!?z!suoCz?8*i^>nY9)m?KE_W4kymAWP*GAKafh@IN`K8lo$BViTSE; zZKs&C=j}>8fd^v&^2RAZvea5}ZHv0$BiTY^_AhN8QpSW#@V|0eA!gw1+RJ|YoF&U$ zoDH$GO~+Y7Hj!P4%@JA3#^NnGVPbS}jUq@o-kb|jd__~Fxwv_sNRv*fgKOOUUMA2& zt38jz`fO-Gqk$YSrOxR}x|- zG^TRBMDZQex?$OVZVK6!R$S>elT~AjoDi~-o(kB0rh2ulkmJAVYP2Y4`yf2npX$zY zOf;n`g>Ub@N@lMUli3%zSl)sF`(Gp;T-g^{a@Hsjh-wH6P3P6 zB<|fxsO6n)uAxFdjKNwL;o+Y4UdBf7OV(Gm4Cdou_sS*D`J`bTd2{fID65T%pyp(O zD=>a7N#J1Wx$sx|>ccb+&pp%74aob#;?#&Mn9-|VSNT-~;$u~}FE7~Z^OgsR_gmOqg5oQ){OmMHj9yig$n6_<~sqWqY zU|SB$&`upR(qnx#3hzitT(#OYI9dRrGR^opoTVMa@GwFJDm;~IMZQMOo-W%cNpKg= zW@nYr%bdx6#aTjEPA)wWL{PzQFlIZ+0Pp}Ldu!?>`~WKSuA0;5MwR+g6W|+sUO|{Z z*_*mlMJg!gt^3?&mTB6^+45jH7#AD-WjQz#PtZo}|E%NFr;qst89GXzhkVx_9zYjrYEV8l-cai&JM`0JLzkJno%pPOFb8gWiV)JzZOiF z)HU#RxKDZ2$QqppCfu=^iMf4T7a8OoegoqLJi%j4xbsz4LVe7~Hcn(!)Oup-P6{5> z;z~4e^N59F1l#KmxHCy{)BVBHwX(}mKL=EA=XR|YHmgd|@DpJ= zhi%)EhKvqtP}yCS5P72*ddOZUOXCjYC*_C?-jda#L-%fPM&lugXzrokLx0*r4C|7q znqPJ^aF3D5vnjpR9BjP5w@o{esMHXGp81f!cuS4EKc_m|66L?Jt<$1LNXK}?$`pB& zX3fip4tZi@PAOG=okA$uTBNj%-_4LO2FyIX#MK^vG1joY=P?H#hk~B1SYGK~Z?K=w zL>i=J(faNt9?XXuu`Hp_{F$!PzxX$%gK?zv(fXcH=vB8OM~^`FoTAU0t5*e&lV2-1 zR3;O~gWjfpS(iMf%vHIfBJ>xH?gQR@0!7+VO5?0GPh?@XuT$9HL%-IN20uQiCqiGV zo06pW>Z03{avTYAnSQXR&X(H3%LKn7Q|ymDF2fL($HrizVBxF6b3KZQA#vehU#zxo zr3yz z5Qz)ipBVToe;BuZ)B(!X_`&D~(jK_&NH>kW8Nmb7P)aL7HA*p`lYrD<>Q!p!MZoO9 zu7ztV^>8wV!At_h#Cise>ev8pWzt-{(}ia3yu`%7mx{>oRtEZXM!7ub>Y7W>^6d7( zH{%8qX^25qLRm2XqCSwBUmvD|9PTX5^zH8Z=Di8>s-x{KtZ(9x*F!H{x6yU6{9c)} zN5X8Ekuy@o@%0GZyW4e=fB+cHrdA%{MDOC_F&vcSdXXZlOp8V&|1s<}LXhmlY$U!_ zu#e&MW+2Ex*^1n1J|}4^c8Gyx+UC>x2cynl6_A~~{p+bG#OES%9?55E`H1vv^47iK zVzNTg)HpABHR9^bY22*m^O1~Hsg%mpG18^B%nEU%cF&#`rH?IJTJV**+vUWZ+w<12*dfXG+PyLD5G~d5 zu{tHtBP97ny`GRFb*+>24{FurTXw}qj4Rb%U#(Q5C&|lzq!WiA+@F`5b)Ez2`pe_z zR{Ry5hfBLm(Ld32A&+he&@VLoYGg3x`9T*}pT?Iq|?nOib6+gtsc z$l}k$<^O3t!h;&J`B)B`$^MP50UFN~Y;An```E_{aMK^qJUmtDnPqVhlo2C&g}Emc<1{Y;|1cB6w!;j+IIzP#6+r2nt|~E`p9G(++6QV z2E@_46M(Gt|b(F4?8`UR5lqZxj1E#gq0{*_x=C3$l5BO7M@mg7aYv&v7C*s7i;qN9|Cg%Y%vP(Zr4 z+E#Zo>Vn5M4F_|!J52WcGAgowG}bGxsWAibEJHc)U2inSgI}uEn3V^+%sqE={F1H~ z5HgQ#LtA~sz{O1cRQ0)Z`Vo3D8*#pnGe?hJVntcpJlmcRfgBNiEJ2`3IGLY!Tx(jz zK*P|>*;aSJ59MCYx$LsxjVMC(b1yR2q6HvZh~eaC;3Y+!8>53PHt>1ENW`$<@k0!R zPd`I6kHEco3<^iMgE}n#eGXLLh`gst#vK`9+;b=yFJ*az)rzW+!__Fh@Klk=KgO2i z`S8nnM3zRh36TDvV7561x2<43yGIrCSo~vpMsgEFd0k;`(+Dp`&3bX`YdUtaG?S0D z^|O2ia){hM#DRCxj*{2QEvz69B+X==ewgtHcLW9TcLrhRCGQ`>F zxt_Ti(0nXXZj|E3b0vM;hxItc(L=Iawn^vvVwr9^9~{2o+ki>?Xg>z#Uj&X$;2t}1 zByn=5FuqMx*~g_d2}?0#x~mDCbY;HeSGew5-E|;!MghR|090nV1Uu)0CqdU0CJ^@J z{q#;ortqM71gcMW&cdY*FkrMN z@rD*7Qxk49=O*JRoYiRVnJav}hTWOg33WbmCiCWtJR=pPdb=E-c=C3MB@n zykX4xb=AoPZy~c`6%!nKsl66>Mia>xsK#>d4~^wG8&qRi*M4;BZ4Sy87*9t?{H9k0$!h$&-2=}Y2k;qUhEd1mGe{+IEZ@KsMA6XCC(rGINDu9wNYIACX9KhmSl%1;v?5_F< zu@2Y>?~gLl>8n~xQ1oBJm^ZSdd)Ih|vfWheV?=e19;NTn{$@yNaUhpOlbVaUjV4pI z#sdazZM`3w=Gd(Y_{4hr!zi^Wvy0zIm`kKH_sJ7XhkibH^BWiGkYwSGz~#cSt`NV0 zdQE&6PrHzr!H(qeB_6r)t|E$Rw;wU6rH@w`gQE#C0wqdcU@i6dST=u5!+dAq?&8wN zSackMXNt&wp5r}F=8X3mI>qY9+NNd0O!m^E`K2z|q$68|zbp7|K+K%Iq7+iKq1QoI zfn#4oWO&yi3ka@B0!X{x>qC{6l@kw2%ctkkIHMS;7YX%Stdt6+Go$3koxHA0w5X*R zE+z4%I{z{6nTW0yd~ueB15_2dphMFcH92%Y$KV3r*57r!CrV{OQ9>l89i$7ZqnG9H zuX}D=!JHxO6&qL9RZA0ljpI9Or0wKPH6J_TCEDMf7O>TTLB)*Z>v zxQNvmKfGV1PT;gUCc&axBys5I8MGS>P&mlrVT2Cix+xfNHokS4ne(P(+jKH-TLa@<-7U(X@EjNZu|d@dF4xkI>lIcgoXcF2H&a)ZY9F2pXh_GhtRU@|BsfPW%hGh)@(#9a4s&B3sgvvd9t~n5zlJ<4()(3;*Vw)U z#gtKY##Q|i6QJVY1)+jv;R(JZ42uDw<#ZXMS*%7cx!-4pYfs5g`^)@ z3-FVs>G`2tsi;Rkv=uREBkCkmRhbIHK?Op-Ue+=4F)d)H0N$!8Ucc$)@5+Q%;Tfyj zU+ErV1juJ}GbuWnZ>d>ptZ~M%QE+97-7ZvNIV*BhB|84RuE13>%(~dlj_^pfF=qGh zp=QZ~=;E?W;&?_l6Q3G}!Ub=*zDSK3Csa`WaIlmPNB}KQH~WMoJkzi=yGSO@X&?5- z^0krN?n7C@11T6FFU45lxh`|p%-}R6V$uWqyS=iNHij3+dlf8A|E(Z*Y_Ah4Ag^T( z=4;-NvN>Bv zAq-hKTX|Jj8|A|bQ|Oschyq03$X%^rU?Iju+9LLXzc!*iNhVxYab<@cHow4|^$nSL zbd)Z$Jl46V>Xe6$g!HEzNp_t<#*a`P5aTuuZNY?>((Byvm@9FVKDK;7i~E%$dld4Y zsSZ1$ReUx}3U>REn$(QayEf7(sCPL7lBWZNlsnDA;q%h^q)1>JM1}*ikT(>83TYc2 zk#hm8RKPnE=@UQzU#IT7pY!u&o)!|KoEX%BiqukH{E`&03;ag>FX??mtdFBi2lYuf zOsRZ7c0-rq2}tiMiNT;pYkFj{fgl5OIgeG83E(ishm8BRn3QCfh)#@}4qd!eza!e? znPheMdxJOww|&^J*57Y!8)ci0?m*I)p4-dJm~5KN=8IX|zC63|VCRmVd1}gRNI5p* z2Nq{NJlCQCO?=KQc8(*e+Tk&$43!ddI~QeVlSqShuJ_X%I!2hwf}!{lqsb#Jd7$f= z0c*OPPV+rBC8?lHzAen31M@gCQy-zJPNwmZ(6@gR>LC4Vq?2UXnS4uQYA%`(9#+_q z;%1QPPZS0~*B7seH`eSLnWCj!)7imRD$$L{w9_Pqi&ckBOAp8m_I8nq6AD=L{^_pd zs@f6$BCWVc_gxiVT1eLHF{47Zr@%35$V8A5GOCIpuZWED^UW!}m-aQ5-``mLK+XmD zLw*5U-h(h5EvY-^TnHMnbMMb;{`y@Owx{0R6_m)*;X!w&U7P14QMINb5k@& zNuer6_XKt`7m_)cl{7+%*e{yWTG1c$1;(R@Rmd&wZA(4Xu!gnPrzeA-WpE`oKlX3d<2#1w7^9OjxpeI44+Pn}egTVntbk^S&1*D>u) zJMhTQVEx=i)R5IQk$|4r#M_b|x&0Dx8o;gZTN}|t+V~iDeGqz%y(u$ z6$S?L2Z3}(X?A>X7KQ9JvB{eI9({#=dGYK-Ilm(mPlw*xsv7rp_-ss5{-!?ca#xNf z60e3O;{0mc#zFIlaXqNmX1AD9tIXDp?-6;^M!o%>X=CGUJ<9JMkwtfEu;V@5V*1P# z(d5R)&u}K%$ud_ptNkBEZBQ@f z)f`GWFTOu*zDp;Bh9sJQr<|-{ zzAIe3D)f`TIkUPyE4n@2Z}=~m8Yr)me|7oyp$l}{98}f9=({KTd*V|AYuEM^TH14M zblKaf^=KE&mR=n5OU(GM#UcL>R+2!Ko%BZ(Q~cV=fAfF;RoD36|8NMoE;PqnCZc<$ zA96zLNB$=&O6If4RYf&mX|9wgH5_UpC)N#E@N3hSj>3AfHytU~hHVn|#~H)c8~|Az zd&spBv(wdPpDX}+OGZt}zO#bG>A4{==aV&wS5?XbAY;*>9#ENjjj0f^4m9O?Lm8)6 zK8R144yxI{dSB@}hO;M4b+)s%{JFvSkYo2}G0A}Ok4P!rp}`;I#~eT5o;N5Pt6(VH zR4cBGk3xuVA@A;a zj)OV|@%hbe$~!n8O3pT=-!HglA93*Cyow}Va6d2X@;13|Fj|5i%4<0Zc*ny3Tf*~l zl7cT*C*`IrzG|%)0UDmr@bdE>rJDfFUC_WrK#X)_o~s;u8D4xx>!2IAs2dw}i&G3_ znE6}4llC*<`H^V5Au`+eCW&-z)Eh34`YKL8eK4D=YU650!7OldEeYT4($ul6rrn{Z zle&_Xbf7U|_P1E)@FpIKXGo^RTAO~_$?YQxIQ^Sg=i3bNC${;!ZrZd}aV@+-`hE|9 z&j9K(9R^NZ%aS+B;yDqmbbL}cZobih6ycUV)kf^4AEn@W^I2T1^!ZmzzJcFT!V?LV z0uwe1q-WeT3c`^k6dhMmy7`h@Ik5%9M$gEfC6>)7ZpD$v*gnFT=D$D5=D+pxy>aa; z%fJ2AQ1^Sjv;IZdiq8!?UZseBR4N_=71C*Cr(D#lvu>;_hC-cmk~6{UCfnJppxs3G zH-6W6!&F9ALI0g}C)yp~XE$R^gUrfQL_Alj{{55_3}UGh*`g5gez`_YMEmC(_!HJm zK151-LYMaeJO+Q*>24G2yLsGt9be|u9ueC)5i0AL?x+yR0;IhL*0;H(l9U!!wNt5f zx#Gn(joLV0-p(2jKZ*3C@oU6(mwa_en#kPzqGxnAha~o!!uqw35qs*i#e9w+bgOcK zW&S(fa$o4_XEk^(plSeh?Te|3_#erlnL^IPu3Uz*Xg^-LDEZLVF=H0BBlnBqC)DEpQ?QpK!&qahWu$P5~eo%Az`lggCb z%Fp=Qa#>b$rTfWtK4t61qdd+=H+1RglKhcAPM0%+r;L-Ko9$w%EZY91vnDx)5c zB>~z{U4CBP^|%9tGJ>;&D|$`DdWj=ep{&>Sd*ZL8IM3M(`+9UwhfO|nM}2McWyhL7 zTkVVY!AeJ&Z1ct{e1*-NZAx#DgC1wG*E!vG z*TbgKdju`+He%OW$Y)4!CgQm*(9RNd=7cymf4ctm9*g7Z8m{5$sN3Ld-vL>Je9)Fx z(?IB$R59nfbe33GdO4+@+C;o4nQVsz59dU7h`aI8w|DuS6Mz!v4bV@9=Nc6Z2fV-G zOfBy>GUCC~XBhcaRN{owSFQyuDyg9It|i{JAO9zIM|Jsu-BAQ#>GRHc^ucpS5s)f* zsbufqbDCk#-#gZ;Me*It`?I+`*0aktUT^3Ribq1*cshei%RdUBlxwlge1fnRIT8?- zN0*KLP9`z&AcKN1%%TRm|5+`pHUb4tJ?qizx-)l*WUo>$f@V$HoU`(Vd(cjTZ|f_{ zOvws;h_;gwjKyTX8!t>TY8*dE7lL0UF5JEkK1($pFnOKakfX;cyW^l{g#`048 zFnf(E*~uwX_CwR1C%tUG@GUM>GNt}S&z6Il*c;EL=Zzk2Rx6fQqmpfdrv%E?i!gUY z1E;TbOSEgGC0?g1L?rrDOtsksG0X+>U(vv3=8tN^iQ_~P$-tfuO<5WSjo^IgXGum0 z49&z)FkF?U0Rwm4(hf>ar|J;4&b1E4SFShsZ%6rWk2}DychIub__Hd;1^zI8eI>2o zBA4j>n_G)yr8a4Ds7ye7Py73)?2INcCO+pi|EyzH_4x{KK-)(kdm-KzsM(V-3&OeH z;ZiZfD`rCoj@Q?FDf=YGeWm2A!S+~Ao}d_QgppU{RO07sS#`6L zgeE+AxrM>nN{ zOiH76Jm5Ps+1`F?^R1ANZ0vPgIp|lfQ128)OOZwvO~NQd!BO9~S0X^f_+HuMnN z?3#NT)gG~orCFm$d-YZJo+gwoz5PeF4L*wwd*Y^S7mY8ikxM94#5(D-4(QG`FD8ny z<+XMw;GgaXJoJ==d*0Eh8_#pyPlIo}!8ZXr7K2m#QKlQvsuI43<`QHykEF~+4QOE} zcu>aTrLAm2XNxU&uvW5M&cT;9Ey;hA`m9x%no))|z3m=-si<-a;17P@FF5{~K8-OE znw8cK0v|d{@TR>}FbTy69vXmX#A99efc_*~A1rC|1@>ruf>-igWaSx-8?pHX{o+Ps zQ}A2aJNhe0!FhPX?KDaT{Za;h9)9)a2~Zy>HcPs6tgW2qmj74$e+1 zoGBP8Jt^ex*a^dOb2bBYu8T}KRyTnc!5oJBK+Z~0aO3+&>VoW8FoSHRW~7~C>taOY z90_<~t1-+WK&v;50I2s5F7GO~PgBi=eaR`lZ_;z{{Rib)VXA%~6g3LGd|18QKb?tM ziOaxvCduK|HQ#=~*4?du4<~r(A(tZ*t{M};qF5KRT>WA<_H9cJ7&pdd%AXWU<1Ne? z5HSH|dX)F5YYPf6hF3~I&f=#c%8f16v3fPYVrTcsz$=S4=FJ!7L(1pqMEy{^E1i$k z>oCrMWHRWQ#Son5dF>q{jn64kbJ)D%($IOng5t&Ut1^3^VAcCU5OQA=y3(bgOcttS z(W||jXOp*h7V?x|Y@+Cg2)6PlA4m2_MH|h3iG2$8GK|?@6W6s`_F$WZF{)rie|y#M zxv${AKvA5W=sp}6@xV5QtSZ1psup)e8K|42c%!|{|8d+N)Pmc(8=Xf{LZ=b5E=ilo zY+wCx`z|nl)D{uh!?0Q|I%VgT^M{o(zL)@8zCFV2GKct`FD6-dIZ4G(LUI}%hgh9M zkh32q{{G%*hO>A55*;f3`?6kLe4251wfhQQ!~DLl%4Rj~jE>}D*1^2nHim7s!XBKA zL*4ca!M=TP#bIv|w}9>6w3rZiMD3wR(LuLvKeNJ19m_?re9V@UF=^_OhgLUPGrbUU ze#}6+>K@!Wn@s&>AAI+XJSM70=KYD=%CIrU$HAMc0 z4inp@b#>Y7S!@Ql;-4iZ6SML)G_?%2c{!9DljN)047#f2A9G6J7Mh9&UKQHx1mz8d zg(@+PO_7{`V#WKB93OH*v#wP0C%towhDU{KKg%2cCL5}EHEM`nqg$ZW$W&k#G!_;t z*~Y@`Qg|*Q7^)!>DN;^+(`Q-V37GqM@=dn9ceT`1FJD`P4@o*Jm>L%R+Up39-e0wL zo~Uh5=BG~bV3C+2ajD~E1zLb-v~0+=59UYO9si(G*YxU_$MGh(K3V;(*E)V*=dQX zsq#D!eS2r{{eLIjA-Ue@V#MROx+xG$+-STu`aSb_^a#RNE4u^!RT<9o=^wkw|NO(J vQE2@1$Agl@&Id2P@c*0N^3#nt&$W9vZpN)X45vlF!y_a_fg(jhA3pytuJo;X literal 0 HcmV?d00001 diff --git a/doc/img/AISMod_plugin.png b/doc/img/AISMod_plugin.png new file mode 100644 index 0000000000000000000000000000000000000000..35306c050b1e45a4d343bc7e98f5a8e40c08aaa6 GIT binary patch literal 16799 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 literal 0 HcmV?d00001 diff --git a/doc/img/AIS_plugin.png b/doc/img/AIS_plugin.png new file mode 100644 index 0000000000000000000000000000000000000000..e4715b19f57829af6b984df64c2b6bba27ac6438 GIT binary patch literal 26440 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 literal 0 HcmV?d00001 diff --git a/doc/img/AIS_plugin_map.png b/doc/img/AIS_plugin_map.png new file mode 100644 index 0000000000000000000000000000000000000000..450a67d37737e1b59b2fad2e3c6463d2d6c0ab38 GIT binary patch literal 115929 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

;$qn9CRb11$ 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

    96kziM^M0vJiU(~LA1z?W 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�^ED~8F<3quE3Cp9|-+n;F5^IT8C z@mpM|Z4nd5o;TMtN8X`tdA*!%uH7LJfG<&Y#x_lAMX*_Ep&CYsdCi9 z^DX0-?5GDSP!w=R_(xGd{{~PeVHY~h!WzJ3-mw3+$?|)SPy-g?YKGsbl?_X;xox)K z>k|em_d|<)VL6rk}N)B=rQr=K*>vtqI`VLX>UHHUR zOw_`?isl2ZFdw%7YdnhI>`qQe&OFy52aChIcX^}72xN1zME{4abBvCx?bdb2wmY`b zVaK-Bv7K~mcWm3XZQHgw9owlmr}}-rz4ti#oQzRFQ&nrNWIhx3eNBBKL09Fy?N5sg zxLCjFVUCXm3zxhy|Ixf?Fn(*iv`0~~S27UMC)wp^!H5Nn181ECSvbn(DEhYQHSq~cO@7q(ErXG@Bc3u9?(pvu8(P5dK-{N^mY$|&OmxO z)_Z%Va9lJCfdZQrl~5dJ@J)CLQ#CNSWUg8;Z;X2JAlIL z%@jOOW6_#x2%TW@bbv-baGNh=HZlXq;&lI6#DD&`^7?mx8Q4MUSo5_C_bX(5KR1zmQC7O`n1oG0FnOx^Ms* zA4t3Dnd-Bs3G!f@7vJnqkcZh`-LFOMhOS<>)~c)?^U&&@ny*q2YXI6}-E3m$u{eUPyHQ9R`nx}OgZcE@-78vgPo@SRy-q4}4rHZp zjP=4y1T_rY#1qO5|Bk-EC?WsP1%w0qbX;zOZ|TJ|iieJOch}LNPw~f9OQm{Nfl~}y zx46otV)cC_{;Oio>B5T5riONBduwT4X>DF@F1t8=o%4sK?}(S&6awv;OL7t``r#?? zk(YF#R`HGSzZjOpjZyD3)YqdI2tgOGnBSSC=)|1E{UTCPZYTG?Li^){T=e}EZl)Og z&yaDF&_7KWc>_+zoBwXY^my%10_o95B_o}812{>Wbmm)WXu2Ht#yPOvE3MosU>9WR(;v z&nMn{1V0h3PBKg;S@0ie0B!Q%W7bvY*FFti`3(J@@vTOyBr%eaR?XI&n~N zv$yfF-{&namC3Uzt=!Tm9JWYD4h@H;tZ{!4=Ltf%hx0z+dG%p_rEBxNb0{f2%ED|lfFSxnlLn9II%@0K!CRWoBc4Hv zqK^L&>-h2{Fhg5KKag0l2VGcH~aw8Y<-I{>hwPSaW+WXbb zVHD4$?t}sx5MA9K&72Ivx*l`v_+ly$f201@XTbp!SQNunA(}kAJzIA6?s*ojQ$R)+ z+`W(hczpV}Qd%3JjfMUScdR}<9+f)U-dApz+w`{lR*C~ic&#bj$ON+dJQGMkA>+)? zU?Vt^6RXFTs1(N0h`>>0y)}lG4ZBXya|rs9Oxz*Z9DL8N7b7<~nOUq8)pV4};7V9T zMz^+{&pBj={hd1qsglGanbPDa6eY-Ruk!~5iowXp2bpYUW31ZmW>?RT9y zjVZ7ySdSRza3BrE{cf;?G?Y@)+Omfqn~G`!J$|-1;2R7+O@lWuF9-r)|9q4 z%Ykw-<^5`?dOyzb$bxXz;%lmFKR=HcMUmXjM!@B7wpe)IJ-A%gW@DgLk>+*jNFsuM zWUFtp8mDnhw1C-oD;1_M{!B{HoyN- z*xGYRqB3+0v2qH%bdq>a#Y*!o7$95{l6mq%=b?slavj|#-wb<>cH?~->wOZDs4i7) z7b;kw9st!%QiX5`!JRY^%$&=v#>*JLf^klA(h1ydoc1~v5HmK|S+C|*fONvW^|J|9 zlJq>RNc6tvf_HY|twVX6oyVJpqGJ``f}x|MOk%?h1aPK5_G@0ayS8HAd8%H)2>*iC|k z%t3->`B>h<3|sZvhbdVC5tb375S%uI;Uu(c8Q89;YB7SDzt*+hH+m|k}x>FT8qi& zwl?SaOo>4PJ)T`mzQ0y#j?H<7UFNz?lT?TcCyT=pNcXH0_9#Ojkz%H%N!z1TO)%W- z3_S|hP8&4MxJ@q@daMUSzOr&Tb)Wql31oSV3l6aw;;o|mV7E-KPZys$w9oOHIQ^Pc z_WE7b%iF}DZq~=t&2p4yg^ib=ouAw9z?PA5{3bfbN&f(1sG`v@#-ef#bsk%4MJj8= z6G}5vLb00NLp(YQ2RARXx$YgUyE7aCscKxAsBwf1upQ@`lbyem-`jiqJ8pMZjJ7#n zLRhBdt!)JpT3t&#I0ngaB2qj_%V=~guadrCk3Ioy%BWxj3JNi5(j5b0!>XJElM3wc zguR&`@H7|KmfQX`-agJH+8pEZdUk;^lrdheMt7hM8oxjc5q2-eEpGXrv>tX zt@Z#>I5P6{cJq((Ms@ON65Qd;1Y1cCKBWrfeCcA4R9YypaO0PuLH9Nj;7%NOlQ=QI z2o)u5fp0=_JTnUQ4J2xm@%y;2G)acuHYb{0DxtaQoAt8pn&#PVVCQG*D0!Xj#k()J zc=Qx1XAvu3RUq3^DX7s0OqiR3YbW*FGNdAClStB(^sHu&B)ez1&xI0fB!o9$>@bin)C5k_EK`kX zaL#SeHntP^r{6zcsi>VCL@l(R!)JL}V59-Cg)?S~u1DU@%>-j25gZfZ{6>ghdjYdvSIS?Y! zYT}+ZE-#8K3`tbP3itdTusa#OF2%0=Nq*b9rgZ5JS$CV7MM1@idu47o%5n$MOZ_!H zy^D;;JX&@=jUH2FZN9o^Fll@IrJU!BnDh1QjBanV7MLY^W)TH4|8S#A@o&3W8S#+{ zB&a02En|Wx(ah*Ux!+%*A}eb>=K~ofm{TPnMPWhP1nrS@A+pz^%{=f=H{Y1UKKIj# zi3fsvcG^u7z6RL(k=T8$WuOYapoS4gul~+1VOJsoZ$~VI`i58y0RK7MGlbU-`MrSJ zf9r}TaMxX;%{iP5l@771kziY9e4SJX<=dRZI3 zIqj~z--NjF#Yi-_KV~$Ol@8-Rv7iw>iJ;V98O8Q zgS@Oo5GWJ?`xT=<7`kP8+5>-iuNw=_OU)(!Y!tI2Ot4#h64fOlS4bTzr$>Yo`HP|$ z1B&njr41(36TVWML}ru#Ty3HwjD)6HM-{Z9&24roS+_>fKl=N>TI>H2e#K3PDWWDr z>0{~uM@#>}8z7wHq22K?H#W&=dOPdWUBwi`UQV+eT`iO2w% zQB992X6wuFrVMl`6Ky^L*1Mqu;M1f;3ALRpl z#-i|=>4!-eDCG;QqSFx63ZNjTfQtb1O#j;_^}qc|O;-H`85ZtacP>yzZz137 zU0>g=O9HarDeAdHr`jGI3vi1yFYg+y(Pm`f0g&d6rR97*rF4d$#Ncr>2v)RKHB}Xw zRcci6nUepAERxe=Op#%c;L{z`kC^I)8K$i`+|3*63NI4>*x}A`MTIhr9ZH!*OOV1U zKH8U9%VnwIW%tlpD~QbQy2xx;1EG3z2ll}dHq$4CxrY> z8w3*l-;;A^IE`F6$BqYx$1hyc-$hh_J%l9#{OlaUx;DJLQM`?#J_WuWvuIulxd=vV zB4N2lsFk<(cZQv0@ET8YpS{NB@K@xJrNc9EJiS8unnK zOuT92`ZN1#hkcE@wgaa|xKw=5D!jIWJ*}dg+sryuN$W?4X4O>KY7V*LUx9xh1#wJS zS}EG}2OpQ?r^c;};|+RnGzY}5N08P8 zArtewyu9a~t3>;07jFx3do^-gq(({LgA%@H+{p5)D^j(}!B7W2i`()QFpsm@&A{j=zj_0|67Iq$GKy2cr4br7#izX6k80O<2iCgzGXiM zuT(4)L%%d!U|-1BT{R+s9fd^4K*PXd3H9zTUBwXepsLC7=hJHA)(lf#uB(ITepd3r zjB|A4)0%qkY~hL(A^Y@W+Rx+$zp!GMkO3ZjRz%HAoA5mU?65f&e(t#)D+g$&1FbuB z!@S)XC$33~67(;Ak^9(|tg08(PKO9^V+KGu8 zg?hBtS(VYPFj~gV4~@pLZA5 zGkCUF4#H8I8W7WF3ZFRE!?o%gI6g-Ve>{q&%to zS5^Iw*ZHs7^FQ9Jy#jVQDs=eO$%f^yJVTuNZ)$O*LXNEE&ed^I3sb47{WRqHNtSuK zIPr&owZae&8AETuf#EF6Dj(`+#`r{um`JqsbdD9CvwpZ0ALp57TuZB(O1ed;!x*IK z4K|F7#mF^QpIo!tF>?a8jNwe;G|dW8;RQ5ce@GTleRAN=QJ8^MejjKZZ=Z?A0=D1u z+gS>=QYc&C*}B;cavIWKOiR)!rwHPh)foETiWk7%M}~`E5074C_%IdO{40NB{o+=&`B<~#Nd>GU3YES$8aCnu|qlVE%f z6}5{}{~|;0JY7*Y%Hj|?H~5ws`nhbj_tIIQw!-=(dHxi`F5CBao6|Qg(K%Ls)Lr)s zeQtb)r@y@_`6I3ALRDaR-vXXt5yyl~5$r3TAas3^MAYj4N~oRkM47$^kkE04d%xAO}sOYOT#o)~8(A4^ zpk3J>qo#;SGD)C8id9BS+qzwgH+__lnUJ~{ws!V~92;pIiV?mN?h*7Gw8XB0R9m&%O0)L-nw6}b4$a1-r1N`X8FE&}sK!}Od4kI8TT6-MNzE?&8!#uF6zVMoqC=Lex*V+s ze$X#*uv?)q0f)fAZMTb$2ply(Tjz;V>suvdGS%AddTck^nRz%};ms`uFpy3U^(=63 z92#tI5ur%>cS(UXP&}ifp7Q+Cl#xlYvBHVLY@bw&xAwH`@Ql$Bhz{m&u-B??ogop*j)RI)zuC&Km%> zTp)g46$Jt-sCHEh*Wwme|1GB7da-QwXr)?{QLU7ujqZY${x6?JWJm<4HV*c`M5a#) zRbbY4bvG^X)ca}a)#j=HYg(fSa~JhpHG|^^XGac?%i9cP`c6*sF#i&Ngb1-3EHOnM z$>@}cdXZ*HhC{Wb!p3LZ0I z#_5-9B0?2{2o6`##(X1IzpPTLT#DrA;o-4`3G(bEP8ITpc=sU$&LUzAeJha%)=p$J zR@dl2$uOQ}4Omm_QRjfi%1?67Rx`Vj)yyer_odTQZS^Kmgs zONR;GXVZIMr6la~22{C!v~2sls#>*%n)L$!t@WM;omKc**2dd9D_eKRU@g4$^@YeW z(?0kX<$)+tOfpnNTOUN!`a<9!*61>Hadu($W-wkx%*Tke)>&|I7&b3&U+t}qu0t8o z5ozX6lA0*ouoD^AD%={)21nt7i3th0Q9}rG{ zOCqeRL(sAK^tCJ4OCIFmuV>9~GkjwXTJo&c|t0YOTMkGX8e~S|F(I%>MDO34;^DjPHCV z%w>*q^_pAFw2sy7Vs)0}vM7ZMg5ybOk>;}m^ZQt8%hghgGLlG6y+5<82yl4Wc%F2% zCUC(V{ry#el0j!!ohCsp4!%_`TY}6iuH_|ryXifbsZQ_vYdt7a$^dg4L5+YPfeNe) zsKf-y8;L`Ldc_n+s$RwOJA>ser#RK!`Iu`sv_(`GIdM%b0bQI z?|n*zsE1=~R7G*Qvl3G7mHs@$zSpQ7cGY{B<^zJL=Q#&T0t`aCNKn>U4?*AQ6iPHQ zLY9xgIh7va(_EFi9P{=_w>dn#W|~_84_w@4Fw;yh3~osS7dgMeDORG0F{vJbUM1;o z2Id|P#=XqE_1+6FfAaH<#ABSDoP_eKrlyoJlcXFR6i_ITrA$<;z0@qY*tEhjq zfGj$pwHMJnsO=hLGfBHh&9)}Q<>chrK0{4hvhi@!8i1vy!IWNPo@8BrUY-?sU7UnZ z_uP63#&Cc+#BnbwDzUIExA2N)*mx*i(QC?YQnqRTKfqrgU+^r(ps`K~-CW3M-blV#_#5?UFjaH8Ly4`DMAXoa(!$E8TIX zCBnguij_h>mA_T_L3Fb_b5G^RxL1-=wJnu`+o9O@)d?@_cPA&~brQxN*NtH-DJR(K zdD6sFk;Wk&pW2q5H8Ctz*3CHu#onjL{sQ=0G@_Bg)D#-8o&Dj(^Aawo(@KDg-lT8Y3&JTt5&^)C@}G^25F4j2;M zpr>sTbzkXMh-&v~?4V)VU{%P)R8d7D^IMT&Vt=X~7G*v{LTFZ^{y42=MgV&bNLdYX z#BZSzg(vHC{j%~E#_%y43o*rK$R3F6a_ab90XFel(|@23t6z^(KJ0Kb$mFX*m>6_l zvBFq3_O-Zu2y!^eWc#QA+!BjCo4gyhnU}I0%%cnnZ1hME0MTh}=n48vOp9AwvtBa63hr%?9b4bH*dfJDCX%N* zYTC+~XNXd9GET4f**SOEuV|`u8thjHsNR76izzM{68zsLxBCVBjpZFSFUwJ^8}3LS zM4*o__-`kMW^dr;>Gu$zf{(;e-q&UNG1Vgx(^0N4kX9!cabnK>%oLI2XgSPXXFeKcKqH!;UxoCJIqtn ziTeZ3jW^(5*%W00>p5=pvxJzwaW`>b6k_9zUHc% zBYB;7k3N#e#0+MFtm7Ku`HbUMzxG=o`cu!9~ipe(zOWaN1`Bcrnfc zIeRiKaJpQvnph4nGy^C&(L!-VLnu@{zcrI#_O7g-sPeY(8(+*TfRuhJevGzGVtyAX zufQq7aTu6rK~AvfRaw?~AXmlOLhr;AX$M&YZYCd}FtGdcspA?ZS&@Z-bb9CZthU}M z!Bx+vz2-SkC2%h9*N+P23Y7*&N9MBq<_%j7Wz9v;)5VVudqtn;bZa#%%!Y-*n|G-Q zGV=M)+q2JRi}snWaboFYY(6JJDnw`uY8Xzq{vqOSHEjp3%D)I!AkN?qm&&RLHmMMD z(mj|z0+gg+twE*kIGPY1$ndU_rve%}Im}Eeqeu`5UU}=!4`jPl%WsVVGd=_4K1C1@ zpKkkN>bnw8G$BaTD_aj?jGrsE5o*>r8R_5B4pG20wrb0Bi9R1zw%_Ki2zL0HWye^9 zFr3|kickXn@yf?50-+igZD$F)uhi$*YUv0j@VyXqk;ljqPw9&sl}6waBO5iIn>LUG zw|933uD0$DQZlneI1~81?a{t7Gk+s7${8fmgh?(Dfm7{AMF2un_$Fx; z`zZ2(W{rg1NcGX*$8Fqemqfe=g&~uEx@ZeLyVe;nGOJ~r90&Jk#PR*#RQwAZ!X&8C z21=lXzY*i5GF_7sY1Y~*2HQf&rbN6fiEqDI?ZeUEnyP;JD!U2$1xIw5xdg0JM(nHQ z21b7LQWInsreF|=X07Sjl2!BhBqL2nY7O``v-=ANe`Fi}2ossCpks=6UklSM9VJUI z2MoWtpB2*jwHe_m6S`;}nr_I@XoogVIykt{w^yYly0U`-A8s4>>$ugztaKzmS7&O3 zD>YZF4VLtr-zpi(@cw9^CN&KjaoA;5+T@{j0CBxNAF^k3Br1a*^f-a`C?6^&OkAs$ zbtVhL&p1|!cQ@bset`hFpdKd!9f;AgG0!#+Fts|% z@Gj_bA>>A5=aw#WGY)18P1$DRM$ z6w>yepQD9KmT*Y@y9yhKmu(}~4H^`E01R zN_a7l(Q&{VWFEWSCzF;ZT>8Vjw(Z8B;EX9qXRYCQk{E9K^>#^&m+Bd=M+;$^>xi}_Q%%Zv?^{_snc6Q@*=>qiv*PJ zJH>Mo4Ye0xLNf2txpB<=G#W{*2sK+xkwu7X(0ZAm1>(lOB&Usj&!cbOO_6KKL{;&9 zHv)K@jl6I#*YtFYTx6c||B1aSo!kW z3h3PlmQ?P_RsB9FHqC(#yYyI2tS>;OJt*@RRn z3S~o~ev_Yg*?34WNw95^RgQsTVo+Z%R-0)rpJpmJelG1H-tDK>=7O|j;bq5Fb)3y* zwCJj)(=xmET+y~xV-w=$OlUNiq(q9CU~(_ktl^TTixVOpb7qTj%SeF=gPk{`hJ@#rIMEVwKcma#-BJOm?7Z^?bS-0@>L=j>; zKg8VzijXV_Gv(bjYCEOrzyK7OwLG4<3+&_%)q;Nsyo7=M-q;L}S)$a7KuGyi?)+mn z`Q?lnR6G-LrjIXlAA(d7YrX)U;fMzeMBy6y4)cXY>q~czw(kR$%g)m?*P+7MFY0<_ zyijC}Nx1UQ9m>nkxoMt`0IKS(C)W*EpNHs>Y@h2F+`F60Y7p0(Vn&^gBifbiTf&ZU zi;B99{oFeauPeXT4{vcDi=V2Y4$V$@Z{xN;fS~|V^P$I?%K0q(pmA{RyC|KK9fxEe z2kUI;l%yp+z`k0~%PGItX->IGoKC%QTj%e7)Ie67{8i^5w;;JxTJZyQFPM(Re!*xs zOGgsd4Ool_-orMx!>vS)!Qri;9^jVF1PP#c`(Mz%0u$(e0g2ibj!2Me2jBlLmq5GJ zkZnydY_-~K%lwYbEbz2Ntmv2Ar5bGD7OhEc$6XaRBOOJ|ffR6D)O9fakpFmO`*tI5 zTP>RzJ-mA zw{zOVh~Sr6WV*T;57wPH3~fSWtamLQN&)*x^Pw=TIE>9wK)+ZOi3irP%4 zQ)u@KBS5H?i*QFJP^chET8cxs_!qJtuz0%BeM&<0UV;O9rvEjZ|EouDIOYxo!n%Mb z1=i`NeCa8H_MZu^}L*`>~2tul4f^PRG~#rfBCegAV;i!_A?d_aU6E&$G0x*TI>1 zj~!sZ8KV5@2`JN>)(AmT#5~t-xrk7^=;V~2{q~)2M4|aPuczztl2&8@iLmp*RLA%> z7nq;W!Ma{Wci6WSXt$GKF)rzUEn{l7qng)`ZDc15$|5zT2KqN8i;ip>>{8fh~J$gH_$-hJ82YZEQPbsSoF zkCF#Ww&eyRJ2V5bUvgTIx;gP0X65|J`7YdRqbYgOHWuPUjdc;6!j_>Ec(TA)ld^-+Zlz9EhKlj$*CwV z-famYI7B4&4~MG`d4x4+ki8rC!%RN0K;`JPfs3e+RMk(9iRLDU@ATMQ^Sgh?V$vcm zkJevjH`kP-Y#EYzL1}>^v6)%{4rV#854x_lSIq#fu9(wLO5Tg}&2!+-_HqZH zj;93R=@Bnh@b3NK>0xrxKQzoDzkR#I-}d0^I>LjvrBGUkKcZf@g!Z&&^!fUc^LgH! z^L{d?vbH=PpHbcZ!ruOHB+utH;`<4J%Hdxm!j-NvrCGf{?(qEZYThW}{Z%V{$k^Q< z=k?sC_w(ZtVC#L~w59a?y7J@pWBa}2%q7R|os|FW^3>Mb@c`g+k2RBh-p3r+Cxf@> zBqS|E?oWh`EhY^=hZ4UASF$hvO0LeDAVvJhqmeFY(&?hVh=O*ANt$6YgW9Or*yGYM zuNShX9OuT9DskhnZpqB89w3PmEPx5YfENLUDw%DpW7@Da$`r^PlgW>IQ(~MjW=O1L zKd)qyuT+;SZ^jU07%2BUL2y-zhdj1&;HN{sFcLAj@|Zpy=Akp>@-Zt8J!?^lQr~yR zg-BJ4fhv*+V3#g*NJug}H%cyI%_bM-8%DSosjx2-c%B)2F$rA6k!zaNbtp`#WL`;2 zOXs=&a>bV5u1JkJvjS<>M`Qw9_+x1tvS~a>i;aFHL5G!~s#eesv z_cIq_IVK}sHgSi-p)2Q+k;SJE(F_J|Q8Uu{n7u|A%-IG^@}FD47`qtu z>oYCDJ`tSuw_Rp<<-br;%l86rMUFrmkBd{bMZGArUwDz?W)gA@9iY6!87AD$J_R(r>pgD?(lm55l;fb)2fS@KSesg*r2xKF>WR2 z^-n}cj_o7$09CJ~sK+)}?VX;k*WFBs9N*1!bWP9OvYhQpWTD%p@9*R75J34vHt*-_ zW%cLk0_t3MfzalPi!oDQ491yc-Y`EtPeTvKpamp6B#XwNkz&MJCAkV07&S}5Bxoph zN)M=51K1utb2>iG!agSPm+&I2sX}ID7yZV{lg6X5c^Mju0tz8}- zWi`FmHTtt=t@sCg{3HK7$BU_uj@PpcIeyQ47hgSecS{Y_{*UOD7($;v#_uNvW%0^! z*{*T~?r-WBD8Bv+{hD(-JDqofb!uNd+y9gmt`D(d;P5QX|2T^g3%Qqwbh@<0zlVt& zAcBlH|A6jx&3zTot8`mS(rf)#@6l{&C0V+W)B2^>5>lgIhrZ*sB>QQ%0DSE|zh6;ZrhFxfFsr<%AsR@Q49 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

    F>W#MVrF{u8 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 ASO5S3 literal 0 HcmV?d00001 diff --git a/plugins/channelrx/CMakeLists.txt b/plugins/channelrx/CMakeLists.txt index 82ed65df6..9ea109601 100644 --- a/plugins/channelrx/CMakeLists.txt +++ b/plugins/channelrx/CMakeLists.txt @@ -17,6 +17,7 @@ add_subdirectory(freqtracker) add_subdirectory(demodchirpchat) add_subdirectory(demodvorsc) add_subdirectory(demodpacket) +add_subdirectory(demodais) if(DAB_FOUND AND ZLIB_FOUND AND FAAD_FOUND) add_subdirectory(demoddab) diff --git a/plugins/channelrx/demodais/CMakeLists.txt b/plugins/channelrx/demodais/CMakeLists.txt new file mode 100644 index 000000000..13ab930f2 --- /dev/null +++ b/plugins/channelrx/demodais/CMakeLists.txt @@ -0,0 +1,58 @@ +project(demodais) + +set(demodais_SOURCES + aisdemod.cpp + aisdemodsettings.cpp + aisdemodbaseband.cpp + aisdemodsink.cpp + aisdemodplugin.cpp + aisdemodwebapiadapter.cpp +) + +set(demodais_HEADERS + aisdemod.h + aisdemodsettings.h + aisdemodbaseband.h + aisdemodsink.h + aisdemodplugin.h + aisdemodwebapiadapter.h +) + +include_directories( + ${CMAKE_SOURCE_DIR}/swagger/sdrangel/code/qt5/client +) + +if(NOT SERVER_MODE) + set(demodais_SOURCES + ${demodais_SOURCES} + aisdemodgui.cpp + aisdemodgui.ui + ) + set(demodais_HEADERS + ${demodais_HEADERS} + aisdemodgui.h + ) + + set(TARGET_NAME demodais) + set(TARGET_LIB "Qt5::Widgets") + set(TARGET_LIB_GUI "sdrgui") + set(INSTALL_FOLDER ${INSTALL_PLUGINS_DIR}) +else() + set(TARGET_NAME demodaissrv) + set(TARGET_LIB "") + set(TARGET_LIB_GUI "") + set(INSTALL_FOLDER ${INSTALL_PLUGINSSRV_DIR}) +endif() + +add_library(${TARGET_NAME} SHARED + ${demodais_SOURCES} +) + +target_link_libraries(${TARGET_NAME} + Qt5::Core + ${TARGET_LIB} + sdrbase + ${TARGET_LIB_GUI} +) + +install(TARGETS ${TARGET_NAME} DESTINATION ${INSTALL_FOLDER}) diff --git a/plugins/channelrx/demodais/aisdemod.cpp b/plugins/channelrx/demodais/aisdemod.cpp new file mode 100644 index 000000000..d426d346f --- /dev/null +++ b/plugins/channelrx/demodais/aisdemod.cpp @@ -0,0 +1,500 @@ +/////////////////////////////////////////////////////////////////////////////////// +// Copyright (C) 2015-2018 Edouard Griffiths, F4EXB. // +// Copyright (C) 2021 Jon Beniston, M7RCE // +// // +// This program is free software; you can redistribute it and/or modify // +// it under the terms of the GNU General Public License as published by // +// the Free Software Foundation as version 3 of the License, or // +// (at your option) any later version. // +// // +// This program is distributed in the hope that it will be useful, // +// but WITHOUT ANY WARRANTY; without even the implied warranty of // +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // +// GNU General Public License V3 for more details. // +// // +// You should have received a copy of the GNU General Public License // +// along with this program. If not, see . // +/////////////////////////////////////////////////////////////////////////////////// + +#include "aisdemod.h" + +#include +#include +#include +#include +#include +#include + +#include +#include + +#include "SWGChannelSettings.h" +#include "SWGChannelReport.h" + +#include "dsp/dspengine.h" +#include "dsp/dspcommands.h" +#include "device/deviceapi.h" +#include "feature/feature.h" +#include "util/ais.h" +#include "util/db.h" +#include "maincore.h" + +MESSAGE_CLASS_DEFINITION(AISDemod::MsgConfigureAISDemod, Message) +MESSAGE_CLASS_DEFINITION(AISDemod::MsgMessage, Message) + +const char * const AISDemod::m_channelIdURI = "sdrangel.channel.aisdemod"; +const char * const AISDemod::m_channelId = "AISDemod"; + +AISDemod::AISDemod(DeviceAPI *deviceAPI) : + ChannelAPI(m_channelIdURI, ChannelAPI::StreamSingleSink), + m_deviceAPI(deviceAPI), + m_basebandSampleRate(0) +{ + setObjectName(m_channelId); + + m_basebandSink = new AISDemodBaseband(this); + m_basebandSink->setMessageQueueToChannel(getInputMessageQueue()); + m_basebandSink->setChannel(this); + m_basebandSink->moveToThread(&m_thread); + + applySettings(m_settings, true); + + m_deviceAPI->addChannelSink(this); + m_deviceAPI->addChannelSinkAPI(this); + + m_networkManager = new QNetworkAccessManager(); + connect(m_networkManager, SIGNAL(finished(QNetworkReply*)), this, SLOT(networkManagerFinished(QNetworkReply*))); +} + +AISDemod::~AISDemod() +{ + qDebug("AISDemod::~AISDemod"); + disconnect(m_networkManager, SIGNAL(finished(QNetworkReply*)), this, SLOT(networkManagerFinished(QNetworkReply*))); + delete m_networkManager; + m_deviceAPI->removeChannelSinkAPI(this); + m_deviceAPI->removeChannelSink(this); + + if (m_basebandSink->isRunning()) { + stop(); + } + + delete m_basebandSink; +} + +uint32_t AISDemod::getNumberOfDeviceStreams() const +{ + return m_deviceAPI->getNbSourceStreams(); +} + +void AISDemod::feed(const SampleVector::const_iterator& begin, const SampleVector::const_iterator& end, bool firstOfBurst) +{ + (void) firstOfBurst; + m_basebandSink->feed(begin, end); +} + +void AISDemod::start() +{ + qDebug("AISDemod::start"); + + m_basebandSink->reset(); + m_basebandSink->startWork(); + m_thread.start(); + + DSPSignalNotification *dspMsg = new DSPSignalNotification(m_basebandSampleRate, m_centerFrequency); + m_basebandSink->getInputMessageQueue()->push(dspMsg); + + AISDemodBaseband::MsgConfigureAISDemodBaseband *msg = AISDemodBaseband::MsgConfigureAISDemodBaseband::create(m_settings, true); + m_basebandSink->getInputMessageQueue()->push(msg); +} + +void AISDemod::stop() +{ + qDebug("AISDemod::stop"); + m_basebandSink->stopWork(); + m_thread.quit(); + m_thread.wait(); +} + +bool AISDemod::handleMessage(const Message& cmd) +{ + if (MsgConfigureAISDemod::match(cmd)) + { + MsgConfigureAISDemod& cfg = (MsgConfigureAISDemod&) cmd; + qDebug() << "AISDemod::handleMessage: MsgConfigureAISDemod"; + applySettings(cfg.getSettings(), cfg.getForce()); + + return true; + } + else if (DSPSignalNotification::match(cmd)) + { + DSPSignalNotification& notif = (DSPSignalNotification&) cmd; + m_basebandSampleRate = notif.getSampleRate(); + m_centerFrequency = notif.getCenterFrequency(); + // Forward to the sink + DSPSignalNotification* rep = new DSPSignalNotification(notif); // make a copy + qDebug() << "AISDemod::handleMessage: DSPSignalNotification"; + m_basebandSink->getInputMessageQueue()->push(rep); + + return true; + } + else if (MsgMessage::match(cmd)) + { + // Forward to GUI + MsgMessage& report = (MsgMessage&)cmd; + if (getMessageQueueToGUI()) + { + MsgMessage *msg = new MsgMessage(report); + getMessageQueueToGUI()->push(msg); + } + + MessagePipes& messagePipes = MainCore::instance()->getMessagePipes(); + + // Forward to AIS feature + QList *aisMessageQueues = messagePipes.getMessageQueues(this, "ais"); + if (aisMessageQueues) + { + QList::iterator it = aisMessageQueues->begin(); + for (; it != aisMessageQueues->end(); ++it) + { + MainCore::MsgPacket *msg = MainCore::MsgPacket::create(this, report.getMessage(), report.getDateTime()); + (*it)->push(msg); + } + } + + // Forward via UDP + if (m_settings.m_udpEnabled) + { + if (m_settings.m_udpFormat == AISDemodSettings::Binary) + { + m_udpSocket.writeDatagram(report.getMessage().data(), report.getMessage().size(), + QHostAddress(m_settings.m_udpAddress), m_settings.m_udpPort); + } + else + { + QString nmea = AISMessage::toNMEA(report.getMessage().data()); + QByteArray bytes = nmea.toLatin1(); + m_udpSocket.writeDatagram(bytes.data(), bytes.size(), + QHostAddress(m_settings.m_udpAddress), m_settings.m_udpPort); + } + } + + return true; + } + else + { + return false; + } +} + +void AISDemod::setScopeSink(BasebandSampleSink* scopeSink) +{ + m_basebandSink->setScopeSink(scopeSink); +} + +void AISDemod::applySettings(const AISDemodSettings& settings, bool force) +{ + qDebug() << "AISDemod::applySettings:" + << " m_streamIndex: " << settings.m_streamIndex + << " m_useReverseAPI: " << settings.m_useReverseAPI + << " m_reverseAPIAddress: " << settings.m_reverseAPIAddress + << " m_reverseAPIPort: " << settings.m_reverseAPIPort + << " m_reverseAPIDeviceIndex: " << settings.m_reverseAPIDeviceIndex + << " m_reverseAPIChannelIndex: " << settings.m_reverseAPIChannelIndex + << " force: " << force; + + QList reverseAPIKeys; + + if ((settings.m_inputFrequencyOffset != m_settings.m_inputFrequencyOffset) || force) { + reverseAPIKeys.append("inputFrequencyOffset"); + } + if ((settings.m_rfBandwidth != m_settings.m_rfBandwidth) || force) { + reverseAPIKeys.append("rfBandwidth"); + } + if ((settings.m_fmDeviation != m_settings.m_fmDeviation) || force) { + reverseAPIKeys.append("fmDeviation"); + } + if ((settings.m_correlationThreshold != m_settings.m_correlationThreshold) || force) { + reverseAPIKeys.append("correlationThreshold"); + } + if ((settings.m_udpEnabled != m_settings.m_udpEnabled) || force) { + reverseAPIKeys.append("udpEnabled"); + } + if ((settings.m_udpAddress != m_settings.m_udpAddress) || force) { + reverseAPIKeys.append("udpAddress"); + } + if ((settings.m_udpPort != m_settings.m_udpPort) || force) { + reverseAPIKeys.append("udpPort"); + } + if ((settings.m_udpFormat != m_settings.m_udpFormat) || force) { + reverseAPIKeys.append("udpFormat"); + } + if (m_settings.m_streamIndex != settings.m_streamIndex) + { + if (m_deviceAPI->getSampleMIMO()) // change of stream is possible for MIMO devices only + { + m_deviceAPI->removeChannelSinkAPI(this); + m_deviceAPI->removeChannelSink(this, m_settings.m_streamIndex); + m_deviceAPI->addChannelSink(this, settings.m_streamIndex); + m_deviceAPI->addChannelSinkAPI(this); + } + + reverseAPIKeys.append("streamIndex"); + } + + AISDemodBaseband::MsgConfigureAISDemodBaseband *msg = AISDemodBaseband::MsgConfigureAISDemodBaseband::create(settings, force); + m_basebandSink->getInputMessageQueue()->push(msg); + + if (settings.m_useReverseAPI) + { + bool fullUpdate = ((m_settings.m_useReverseAPI != settings.m_useReverseAPI) && settings.m_useReverseAPI) || + (m_settings.m_reverseAPIAddress != settings.m_reverseAPIAddress) || + (m_settings.m_reverseAPIPort != settings.m_reverseAPIPort) || + (m_settings.m_reverseAPIDeviceIndex != settings.m_reverseAPIDeviceIndex) || + (m_settings.m_reverseAPIChannelIndex != settings.m_reverseAPIChannelIndex); + webapiReverseSendSettings(reverseAPIKeys, settings, fullUpdate || force); + } + + m_settings = settings; +} + +QByteArray AISDemod::serialize() const +{ + return m_settings.serialize(); +} + +bool AISDemod::deserialize(const QByteArray& data) +{ + if (m_settings.deserialize(data)) + { + MsgConfigureAISDemod *msg = MsgConfigureAISDemod::create(m_settings, true); + m_inputMessageQueue.push(msg); + return true; + } + else + { + m_settings.resetToDefaults(); + MsgConfigureAISDemod *msg = MsgConfigureAISDemod::create(m_settings, true); + m_inputMessageQueue.push(msg); + return false; + } +} + +int AISDemod::webapiSettingsGet( + SWGSDRangel::SWGChannelSettings& response, + QString& errorMessage) +{ + (void) errorMessage; + response.setAisDemodSettings(new SWGSDRangel::SWGAISDemodSettings()); + response.getAisDemodSettings()->init(); + webapiFormatChannelSettings(response, m_settings); + return 200; +} + +int AISDemod::webapiSettingsPutPatch( + bool force, + const QStringList& channelSettingsKeys, + SWGSDRangel::SWGChannelSettings& response, + QString& errorMessage) +{ + (void) errorMessage; + AISDemodSettings settings = m_settings; + webapiUpdateChannelSettings(settings, channelSettingsKeys, response); + + MsgConfigureAISDemod *msg = MsgConfigureAISDemod::create(settings, force); + m_inputMessageQueue.push(msg); + + qDebug("AISDemod::webapiSettingsPutPatch: forward to GUI: %p", m_guiMessageQueue); + if (m_guiMessageQueue) // forward to GUI if any + { + MsgConfigureAISDemod *msgToGUI = MsgConfigureAISDemod::create(settings, force); + m_guiMessageQueue->push(msgToGUI); + } + + webapiFormatChannelSettings(response, settings); + + return 200; +} + +void AISDemod::webapiUpdateChannelSettings( + AISDemodSettings& settings, + const QStringList& channelSettingsKeys, + SWGSDRangel::SWGChannelSettings& response) +{ + if (channelSettingsKeys.contains("inputFrequencyOffset")) { + settings.m_inputFrequencyOffset = response.getAisDemodSettings()->getInputFrequencyOffset(); + } + if (channelSettingsKeys.contains("rfBandwidth")) { + settings.m_rfBandwidth = response.getAisDemodSettings()->getRfBandwidth(); + } + if (channelSettingsKeys.contains("fmDeviation")) { + settings.m_fmDeviation = response.getAisDemodSettings()->getFmDeviation(); + } + if (channelSettingsKeys.contains("correlationThreshold")) { + settings.m_correlationThreshold = response.getAisDemodSettings()->getCorrelationThreshold(); + } + if (channelSettingsKeys.contains("udpEnabled")) { + settings.m_udpEnabled = response.getAisDemodSettings()->getUdpEnabled(); + } + if (channelSettingsKeys.contains("udpAddress")) { + settings.m_udpAddress = *response.getAisDemodSettings()->getUdpAddress(); + } + if (channelSettingsKeys.contains("udpPort")) { + settings.m_udpPort = response.getAisDemodSettings()->getUdpPort(); + } + if (channelSettingsKeys.contains("udpFormat")) { + settings.m_udpFormat = (AISDemodSettings::UDPFormat)response.getAisDemodSettings()->getUdpFormat(); + } + if (channelSettingsKeys.contains("rgbColor")) { + settings.m_rgbColor = response.getAisDemodSettings()->getRgbColor(); + } + if (channelSettingsKeys.contains("title")) { + settings.m_title = *response.getAisDemodSettings()->getTitle(); + } + if (channelSettingsKeys.contains("streamIndex")) { + settings.m_streamIndex = response.getAisDemodSettings()->getStreamIndex(); + } + if (channelSettingsKeys.contains("useReverseAPI")) { + settings.m_useReverseAPI = response.getAisDemodSettings()->getUseReverseApi() != 0; + } + if (channelSettingsKeys.contains("reverseAPIAddress")) { + settings.m_reverseAPIAddress = *response.getAisDemodSettings()->getReverseApiAddress(); + } + if (channelSettingsKeys.contains("reverseAPIPort")) { + settings.m_reverseAPIPort = response.getAisDemodSettings()->getReverseApiPort(); + } + if (channelSettingsKeys.contains("reverseAPIDeviceIndex")) { + settings.m_reverseAPIDeviceIndex = response.getAisDemodSettings()->getReverseApiDeviceIndex(); + } + if (channelSettingsKeys.contains("reverseAPIChannelIndex")) { + settings.m_reverseAPIChannelIndex = response.getAisDemodSettings()->getReverseApiChannelIndex(); + } +} + +void AISDemod::webapiFormatChannelSettings(SWGSDRangel::SWGChannelSettings& response, const AISDemodSettings& settings) +{ + response.getAisDemodSettings()->setInputFrequencyOffset(settings.m_inputFrequencyOffset); + response.getAisDemodSettings()->setRfBandwidth(settings.m_rfBandwidth); + response.getAisDemodSettings()->setFmDeviation(settings.m_fmDeviation); + response.getAisDemodSettings()->setCorrelationThreshold(settings.m_correlationThreshold); + response.getAisDemodSettings()->setUdpEnabled(settings.m_udpEnabled); + response.getAisDemodSettings()->setUdpAddress(new QString(settings.m_udpAddress)); + response.getAisDemodSettings()->setUdpPort(settings.m_udpPort); + response.getAisDemodSettings()->setUdpFormat((int)settings.m_udpFormat); + + response.getAisDemodSettings()->setRgbColor(settings.m_rgbColor); + if (response.getAisDemodSettings()->getTitle()) { + *response.getAisDemodSettings()->getTitle() = settings.m_title; + } else { + response.getAisDemodSettings()->setTitle(new QString(settings.m_title)); + } + + response.getAisDemodSettings()->setStreamIndex(settings.m_streamIndex); + response.getAisDemodSettings()->setUseReverseApi(settings.m_useReverseAPI ? 1 : 0); + + if (response.getAisDemodSettings()->getReverseApiAddress()) { + *response.getAisDemodSettings()->getReverseApiAddress() = settings.m_reverseAPIAddress; + } else { + response.getAisDemodSettings()->setReverseApiAddress(new QString(settings.m_reverseAPIAddress)); + } + + response.getAisDemodSettings()->setReverseApiPort(settings.m_reverseAPIPort); + response.getAisDemodSettings()->setReverseApiDeviceIndex(settings.m_reverseAPIDeviceIndex); + response.getAisDemodSettings()->setReverseApiChannelIndex(settings.m_reverseAPIChannelIndex); +} + +void AISDemod::webapiReverseSendSettings(QList& channelSettingsKeys, const AISDemodSettings& settings, bool force) +{ + SWGSDRangel::SWGChannelSettings *swgChannelSettings = new SWGSDRangel::SWGChannelSettings(); + webapiFormatChannelSettings(channelSettingsKeys, swgChannelSettings, settings, force); + + QString channelSettingsURL = QString("http://%1:%2/sdrangel/deviceset/%3/channel/%4/settings") + .arg(settings.m_reverseAPIAddress) + .arg(settings.m_reverseAPIPort) + .arg(settings.m_reverseAPIDeviceIndex) + .arg(settings.m_reverseAPIChannelIndex); + m_networkRequest.setUrl(QUrl(channelSettingsURL)); + m_networkRequest.setHeader(QNetworkRequest::ContentTypeHeader, "application/json"); + + QBuffer *buffer = new QBuffer(); + buffer->open((QBuffer::ReadWrite)); + buffer->write(swgChannelSettings->asJson().toUtf8()); + buffer->seek(0); + + // Always use PATCH to avoid passing reverse API settings + QNetworkReply *reply = m_networkManager->sendCustomRequest(m_networkRequest, "PATCH", buffer); + buffer->setParent(reply); + + delete swgChannelSettings; +} + +void AISDemod::webapiFormatChannelSettings( + QList& channelSettingsKeys, + SWGSDRangel::SWGChannelSettings *swgChannelSettings, + const AISDemodSettings& settings, + bool force +) +{ + swgChannelSettings->setDirection(0); // Single sink (Rx) + swgChannelSettings->setOriginatorChannelIndex(getIndexInDeviceSet()); + swgChannelSettings->setOriginatorDeviceSetIndex(getDeviceSetIndex()); + swgChannelSettings->setChannelType(new QString("AISDemod")); + swgChannelSettings->setAisDemodSettings(new SWGSDRangel::SWGAISDemodSettings()); + SWGSDRangel::SWGAISDemodSettings *swgAISDemodSettings = swgChannelSettings->getAisDemodSettings(); + + // transfer data that has been modified. When force is on transfer all data except reverse API data + + if (channelSettingsKeys.contains("fmDeviation") || force) { + swgAISDemodSettings->setFmDeviation(settings.m_fmDeviation); + } + if (channelSettingsKeys.contains("inputFrequencyOffset") || force) { + swgAISDemodSettings->setInputFrequencyOffset(settings.m_inputFrequencyOffset); + } + if (channelSettingsKeys.contains("rfBandwidth") || force) { + swgAISDemodSettings->setRfBandwidth(settings.m_rfBandwidth); + } + if (channelSettingsKeys.contains("correlationThreshold") || force) { + swgAISDemodSettings->setCorrelationThreshold(settings.m_correlationThreshold); + } + if (channelSettingsKeys.contains("udpEnabled") || force) { + swgAISDemodSettings->setUdpEnabled(settings.m_udpEnabled); + } + if (channelSettingsKeys.contains("udpAddress") || force) { + swgAISDemodSettings->setUdpAddress(new QString(settings.m_udpAddress)); + } + if (channelSettingsKeys.contains("udpPort") || force) { + swgAISDemodSettings->setUdpPort(settings.m_udpPort); + } + if (channelSettingsKeys.contains("udpFormat") || force) { + swgAISDemodSettings->setUdpPort((int)settings.m_udpFormat); + } + if (channelSettingsKeys.contains("rgbColor") || force) { + swgAISDemodSettings->setRgbColor(settings.m_rgbColor); + } + if (channelSettingsKeys.contains("title") || force) { + swgAISDemodSettings->setTitle(new QString(settings.m_title)); + } + if (channelSettingsKeys.contains("streamIndex") || force) { + swgAISDemodSettings->setStreamIndex(settings.m_streamIndex); + } +} + +void AISDemod::networkManagerFinished(QNetworkReply *reply) +{ + QNetworkReply::NetworkError replyError = reply->error(); + + if (replyError) + { + qWarning() << "AISDemod::networkManagerFinished:" + << " error(" << (int) replyError + << "): " << replyError + << ": " << reply->errorString(); + } + else + { + QString answer = reply->readAll(); + answer.chop(1); // remove last \n + qDebug("AISDemod::networkManagerFinished: reply:\n%s", answer.toStdString().c_str()); + } + + reply->deleteLater(); +} diff --git a/plugins/channelrx/demodais/aisdemod.h b/plugins/channelrx/demodais/aisdemod.h new file mode 100644 index 000000000..5216d332a --- /dev/null +++ b/plugins/channelrx/demodais/aisdemod.h @@ -0,0 +1,180 @@ +/////////////////////////////////////////////////////////////////////////////////// +// Copyright (C) 2015-2018 Edouard Griffiths, F4EXB. // +// Copyright (C) 2021 Jon Beniston, M7RCE // +// // +// This program is free software; you can redistribute it and/or modify // +// it under the terms of the GNU General Public License as published by // +// the Free Software Foundation as version 3 of the License, or // +// (at your option) any later version. // +// // +// This program is distributed in the hope that it will be useful, // +// but WITHOUT ANY WARRANTY; without even the implied warranty of // +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // +// GNU General Public License V3 for more details. // +// // +// You should have received a copy of the GNU General Public License // +// along with this program. If not, see . // +/////////////////////////////////////////////////////////////////////////////////// + +#ifndef INCLUDE_AISDEMOD_H +#define INCLUDE_AISDEMOD_H + +#include + +#include +#include +#include +#include + +#include "dsp/basebandsamplesink.h" +#include "channel/channelapi.h" +#include "util/message.h" + +#include "aisdemodbaseband.h" +#include "aisdemodsettings.h" + +class QNetworkAccessManager; +class QNetworkReply; +class QThread; +class DeviceAPI; + +class AISDemod : public BasebandSampleSink, public ChannelAPI { + Q_OBJECT +public: + class MsgConfigureAISDemod : public Message { + MESSAGE_CLASS_DECLARATION + + public: + const AISDemodSettings& getSettings() const { return m_settings; } + bool getForce() const { return m_force; } + + static MsgConfigureAISDemod* create(const AISDemodSettings& settings, bool force) + { + return new MsgConfigureAISDemod(settings, force); + } + + private: + AISDemodSettings m_settings; + bool m_force; + + MsgConfigureAISDemod(const AISDemodSettings& settings, bool force) : + Message(), + m_settings(settings), + m_force(force) + { } + }; + + class MsgMessage : public Message { + MESSAGE_CLASS_DECLARATION + + public: + QByteArray getMessage() const { return m_message; } + QDateTime getDateTime() const { return m_dateTime; } + + static MsgMessage* create(QByteArray message) + { + return new MsgMessage(message, QDateTime::currentDateTime()); + } + + private: + QByteArray m_message; + QDateTime m_dateTime; + + MsgMessage(QByteArray message, QDateTime dateTime) : + Message(), + m_message(message), + m_dateTime(dateTime) + { + } + }; + + AISDemod(DeviceAPI *deviceAPI); + virtual ~AISDemod(); + virtual void destroy() { delete this; } + + using BasebandSampleSink::feed; + virtual void feed(const SampleVector::const_iterator& begin, const SampleVector::const_iterator& end, bool po); + virtual void start(); + virtual void stop(); + virtual bool handleMessage(const Message& cmd); + + virtual void getIdentifier(QString& id) { id = objectName(); } + virtual const QString& getURI() const { return getName(); } + virtual void getTitle(QString& title) { title = m_settings.m_title; } + virtual qint64 getCenterFrequency() const { return 0; } + + virtual QByteArray serialize() const; + virtual bool deserialize(const QByteArray& data); + + virtual int getNbSinkStreams() const { return 1; } + virtual int getNbSourceStreams() const { return 0; } + + virtual qint64 getStreamCenterFrequency(int streamIndex, bool sinkElseSource) const + { + (void) streamIndex; + (void) sinkElseSource; + return 0; + } + + virtual int webapiSettingsGet( + SWGSDRangel::SWGChannelSettings& response, + QString& errorMessage); + + virtual int webapiSettingsPutPatch( + bool force, + const QStringList& channelSettingsKeys, + SWGSDRangel::SWGChannelSettings& response, + QString& errorMessage); + + static void webapiFormatChannelSettings( + SWGSDRangel::SWGChannelSettings& response, + const AISDemodSettings& settings); + + static void webapiUpdateChannelSettings( + AISDemodSettings& settings, + const QStringList& channelSettingsKeys, + SWGSDRangel::SWGChannelSettings& response); + + void setScopeSink(BasebandSampleSink* scopeSink); + double getMagSq() const { return m_basebandSink->getMagSq(); } + + void getMagSqLevels(double& avg, double& peak, int& nbSamples) { + m_basebandSink->getMagSqLevels(avg, peak, nbSamples); + } +/* void setMessageQueueToGUI(MessageQueue* queue) override { + ChannelAPI::setMessageQueueToGUI(queue); + m_basebandSink->setMessageQueueToGUI(queue); + }*/ + + uint32_t getNumberOfDeviceStreams() const; + + static const char * const m_channelIdURI; + static const char * const m_channelId; + +private: + DeviceAPI *m_deviceAPI; + QThread m_thread; + AISDemodBaseband* m_basebandSink; + AISDemodSettings m_settings; + int m_basebandSampleRate; //!< stored from device message used when starting baseband sink + qint64 m_centerFrequency; + QUdpSocket m_udpSocket; + + QNetworkAccessManager *m_networkManager; + QNetworkRequest m_networkRequest; + + void applySettings(const AISDemodSettings& settings, bool force = false); + void webapiReverseSendSettings(QList& channelSettingsKeys, const AISDemodSettings& settings, bool force); + void webapiFormatChannelSettings( + QList& channelSettingsKeys, + SWGSDRangel::SWGChannelSettings *swgChannelSettings, + const AISDemodSettings& settings, + bool force + ); + +private slots: + void networkManagerFinished(QNetworkReply *reply); + +}; + +#endif // INCLUDE_AISDEMOD_H diff --git a/plugins/channelrx/demodais/aisdemodbaseband.cpp b/plugins/channelrx/demodais/aisdemodbaseband.cpp new file mode 100644 index 000000000..c92bf67cf --- /dev/null +++ b/plugins/channelrx/demodais/aisdemodbaseband.cpp @@ -0,0 +1,175 @@ +/////////////////////////////////////////////////////////////////////////////////// +// Copyright (C) 2019 Edouard Griffiths, F4EXB // +// Copyright (C) 2021 Jon Beniston, M7RCE // +// // +// This program is free software; you can redistribute it and/or modify // +// it under the terms of the GNU General Public License as published by // +// the Free Software Foundation as version 3 of the License, or // +// (at your option) any later version. // +// // +// This program is distributed in the hope that it will be useful, // +// but WITHOUT ANY WARRANTY; without even the implied warranty of // +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // +// GNU General Public License V3 for more details. // +// // +// You should have received a copy of the GNU General Public License // +// along with this program. If not, see . // +/////////////////////////////////////////////////////////////////////////////////// + +#include + +#include "dsp/dspengine.h" +#include "dsp/dspcommands.h" +#include "dsp/downchannelizer.h" + +#include "aisdemodbaseband.h" + +MESSAGE_CLASS_DEFINITION(AISDemodBaseband::MsgConfigureAISDemodBaseband, Message) + +AISDemodBaseband::AISDemodBaseband(AISDemod *aisDemod) : + m_sink(aisDemod), + m_running(false), + m_mutex(QMutex::Recursive) +{ + qDebug("AISDemodBaseband::AISDemodBaseband"); + + m_sampleFifo.setSize(SampleSinkFifo::getSizePolicy(48000)); + m_channelizer = new DownChannelizer(&m_sink); +} + +AISDemodBaseband::~AISDemodBaseband() +{ + m_inputMessageQueue.clear(); + + delete m_channelizer; +} + +void AISDemodBaseband::reset() +{ + QMutexLocker mutexLocker(&m_mutex); + m_inputMessageQueue.clear(); + m_sampleFifo.reset(); +} + +void AISDemodBaseband::startWork() +{ + QMutexLocker mutexLocker(&m_mutex); + QObject::connect( + &m_sampleFifo, + &SampleSinkFifo::dataReady, + this, + &AISDemodBaseband::handleData, + Qt::QueuedConnection + ); + connect(&m_inputMessageQueue, SIGNAL(messageEnqueued()), this, SLOT(handleInputMessages())); + m_running = true; +} + +void AISDemodBaseband::stopWork() +{ + QMutexLocker mutexLocker(&m_mutex); + disconnect(&m_inputMessageQueue, SIGNAL(messageEnqueued()), this, SLOT(handleInputMessages())); + QObject::disconnect( + &m_sampleFifo, + &SampleSinkFifo::dataReady, + this, + &AISDemodBaseband::handleData + ); + m_running = false; +} + +void AISDemodBaseband::setChannel(ChannelAPI *channel) +{ + m_sink.setChannel(channel); +} + +void AISDemodBaseband::feed(const SampleVector::const_iterator& begin, const SampleVector::const_iterator& end) +{ + m_sampleFifo.write(begin, end); +} + +void AISDemodBaseband::handleData() +{ + QMutexLocker mutexLocker(&m_mutex); + + while ((m_sampleFifo.fill() > 0) && (m_inputMessageQueue.size() == 0)) + { + SampleVector::iterator part1begin; + SampleVector::iterator part1end; + SampleVector::iterator part2begin; + SampleVector::iterator part2end; + + std::size_t count = m_sampleFifo.readBegin(m_sampleFifo.fill(), &part1begin, &part1end, &part2begin, &part2end); + + // first part of FIFO data + if (part1begin != part1end) { + m_channelizer->feed(part1begin, part1end); + } + + // second part of FIFO data (used when block wraps around) + if(part2begin != part2end) { + m_channelizer->feed(part2begin, part2end); + } + + m_sampleFifo.readCommit((unsigned int) count); + } +} + +void AISDemodBaseband::handleInputMessages() +{ + Message* message; + + while ((message = m_inputMessageQueue.pop()) != nullptr) + { + if (handleMessage(*message)) { + delete message; + } + } +} + +bool AISDemodBaseband::handleMessage(const Message& cmd) +{ + if (MsgConfigureAISDemodBaseband::match(cmd)) + { + QMutexLocker mutexLocker(&m_mutex); + MsgConfigureAISDemodBaseband& cfg = (MsgConfigureAISDemodBaseband&) cmd; + qDebug() << "AISDemodBaseband::handleMessage: MsgConfigureAISDemodBaseband"; + + applySettings(cfg.getSettings(), cfg.getForce()); + + return true; + } + else if (DSPSignalNotification::match(cmd)) + { + QMutexLocker mutexLocker(&m_mutex); + DSPSignalNotification& notif = (DSPSignalNotification&) cmd; + qDebug() << "AISDemodBaseband::handleMessage: DSPSignalNotification: basebandSampleRate: " << notif.getSampleRate(); + setBasebandSampleRate(notif.getSampleRate()); + m_sampleFifo.setSize(SampleSinkFifo::getSizePolicy(notif.getSampleRate())); + + return true; + } + else + { + return false; + } +} + +void AISDemodBaseband::applySettings(const AISDemodSettings& settings, bool force) +{ + if ((settings.m_inputFrequencyOffset != m_settings.m_inputFrequencyOffset) || force) + { + m_channelizer->setChannelization(AISDEMOD_CHANNEL_SAMPLE_RATE, settings.m_inputFrequencyOffset); + m_sink.applyChannelSettings(m_channelizer->getChannelSampleRate(), m_channelizer->getChannelFrequencyOffset()); + } + + m_sink.applySettings(settings, force); + + m_settings = settings; +} + +void AISDemodBaseband::setBasebandSampleRate(int sampleRate) +{ + m_channelizer->setBasebandSampleRate(sampleRate); + m_sink.applyChannelSettings(m_channelizer->getChannelSampleRate(), m_channelizer->getChannelFrequencyOffset()); +} diff --git a/plugins/channelrx/demodais/aisdemodbaseband.h b/plugins/channelrx/demodais/aisdemodbaseband.h new file mode 100644 index 000000000..96729ffe3 --- /dev/null +++ b/plugins/channelrx/demodais/aisdemodbaseband.h @@ -0,0 +1,97 @@ +/////////////////////////////////////////////////////////////////////////////////// +// Copyright (C) 2019 Edouard Griffiths, F4EXB // +// Copyright (C) 2021 Jon Beniston, M7RCE // +// // +// This program is free software; you can redistribute it and/or modify // +// it under the terms of the GNU General Public License as published by // +// the Free Software Foundation as version 3 of the License, or // +// (at your option) any later version. // +// // +// This program is distributed in the hope that it will be useful, // +// but WITHOUT ANY WARRANTY; without even the implied warranty of // +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // +// GNU General Public License V3 for more details. // +// // +// You should have received a copy of the GNU General Public License // +// along with this program. If not, see . // +/////////////////////////////////////////////////////////////////////////////////// + +#ifndef INCLUDE_AISDEMODBASEBAND_H +#define INCLUDE_AISDEMODBASEBAND_H + +#include +#include + +#include "dsp/samplesinkfifo.h" +#include "util/message.h" +#include "util/messagequeue.h" + +#include "aisdemodsink.h" + +class DownChannelizer; +class ChannelAPI; +class AISDemod; + +class AISDemodBaseband : public QObject +{ + Q_OBJECT +public: + class MsgConfigureAISDemodBaseband : public Message { + MESSAGE_CLASS_DECLARATION + + public: + const AISDemodSettings& getSettings() const { return m_settings; } + bool getForce() const { return m_force; } + + static MsgConfigureAISDemodBaseband* create(const AISDemodSettings& settings, bool force) + { + return new MsgConfigureAISDemodBaseband(settings, force); + } + + private: + AISDemodSettings m_settings; + bool m_force; + + MsgConfigureAISDemodBaseband(const AISDemodSettings& settings, bool force) : + Message(), + m_settings(settings), + m_force(force) + { } + }; + + AISDemodBaseband(AISDemod *aisDemod); + ~AISDemodBaseband(); + void reset(); + void startWork(); + void stopWork(); + void feed(const SampleVector::const_iterator& begin, const SampleVector::const_iterator& end); + MessageQueue *getInputMessageQueue() { return &m_inputMessageQueue; } //!< Get the queue for asynchronous inbound communication + void getMagSqLevels(double& avg, double& peak, int& nbSamples) { + m_sink.getMagSqLevels(avg, peak, nbSamples); + } + void setMessageQueueToChannel(MessageQueue *messageQueue) { m_sink.setMessageQueueToChannel(messageQueue); } + void setBasebandSampleRate(int sampleRate); + void setScopeSink(BasebandSampleSink* scopeSink) { m_sink.setScopeSink(scopeSink); } + void setChannel(ChannelAPI *channel); + double getMagSq() const { return m_sink.getMagSq(); } + bool isRunning() const { return m_running; } + +private: + SampleSinkFifo m_sampleFifo; + DownChannelizer *m_channelizer; + AISDemodSink m_sink; + MessageQueue m_inputMessageQueue; //!< Queue for asynchronous inbound communication + AISDemodSettings m_settings; + bool m_running; + QMutex m_mutex; + + bool handleMessage(const Message& cmd); + void calculateOffset(AISDemodSink *sink); + void applySettings(const AISDemodSettings& settings, bool force = false); + +private slots: + void handleInputMessages(); + void handleData(); //!< Handle data when samples have to be processed +}; + +#endif // INCLUDE_AISDEMODBASEBAND_H diff --git a/plugins/channelrx/demodais/aisdemodgui.cpp b/plugins/channelrx/demodais/aisdemodgui.cpp new file mode 100644 index 000000000..992da993a --- /dev/null +++ b/plugins/channelrx/demodais/aisdemodgui.cpp @@ -0,0 +1,627 @@ +/////////////////////////////////////////////////////////////////////////////////// +// Copyright (C) 2016 Edouard Griffiths, F4EXB // +// Copyright (C) 2021 Jon Beniston, M7RCE // +// // +// This program is free software; you can redistribute it and/or modify // +// it under the terms of the GNU General Public License as published by // +// the Free Software Foundation as version 3 of the License, or // +// (at your option) any later version. // +// // +// This program is distributed in the hope that it will be useful, // +// but WITHOUT ANY WARRANTY; without even the implied warranty of // +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // +// GNU General Public License V3 for more details. // +// // +// You should have received a copy of the GNU General Public License // +// along with this program. If not, see . // +/////////////////////////////////////////////////////////////////////////////////// + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "aisdemodgui.h" + +#include "device/deviceuiset.h" +#include "dsp/dspengine.h" +#include "dsp/dspcommands.h" +#include "ui_aisdemodgui.h" +#include "plugin/pluginapi.h" +#include "util/simpleserializer.h" +#include "util/ais.h" +#include "util/db.h" +#include "gui/basicchannelsettingsdialog.h" +#include "gui/devicestreamselectiondialog.h" +#include "dsp/dspengine.h" +#include "gui/crightclickenabler.h" +#include "channel/channelwebapiutils.h" +#include "maincore.h" +#include "feature/featurewebapiutils.h" + +#include "aisdemod.h" +#include "aisdemodsink.h" + +#include "SWGMapItem.h" + +void AISDemodGUI::resizeTable() +{ + // Fill table with a row of dummy data that will size the columns nicely + // Trailing spaces are for sort arrow + int row = ui->messages->rowCount(); + ui->messages->setRowCount(row + 1); + ui->messages->setItem(row, MESSAGE_COL_DATE, new QTableWidgetItem("Fri Apr 15 2016-")); + ui->messages->setItem(row, MESSAGE_COL_TIME, new QTableWidgetItem("10:17:00")); + ui->messages->setItem(row, MESSAGE_COL_MMSI, new QTableWidgetItem("123456789")); + ui->messages->setItem(row, MESSAGE_COL_TYPE, new QTableWidgetItem("Position report")); + ui->messages->setItem(row, MESSAGE_COL_DATA, new QTableWidgetItem("ABCEDGHIJKLMNOPQRSTUVWXYZ")); + ui->messages->setItem(row, MESSAGE_COL_NMEA, new QTableWidgetItem("!AIVDM,1,1,,A,AAAAAAAAAAAAAAAAAAAAAAAAAAAA,0*00")); + ui->messages->setItem(row, MESSAGE_COL_HEX, new QTableWidgetItem("04058804002000069a0760728d9e00000040000000")); + ui->messages->resizeColumnsToContents(); + ui->messages->removeRow(row); +} + +// Columns in table reordered +void AISDemodGUI::messages_sectionMoved(int logicalIndex, int oldVisualIndex, int newVisualIndex) +{ + (void) oldVisualIndex; + + m_settings.m_messageColumnIndexes[logicalIndex] = newVisualIndex; +} + +// Column in table resized (when hidden size is 0) +void AISDemodGUI::messages_sectionResized(int logicalIndex, int oldSize, int newSize) +{ + (void) oldSize; + + m_settings.m_messageColumnSizes[logicalIndex] = newSize; +} + +// Right click in table header - show column select menu +void AISDemodGUI::messagesColumnSelectMenu(QPoint pos) +{ + messagesMenu->popup(ui->messages->horizontalHeader()->viewport()->mapToGlobal(pos)); +} + +// Hide/show column when menu selected +void AISDemodGUI::messagesColumnSelectMenuChecked(bool checked) +{ + (void) checked; + + QAction* action = qobject_cast(sender()); + if (action != nullptr) + { + int idx = action->data().toInt(nullptr); + ui->messages->setColumnHidden(idx, !action->isChecked()); + } +} + +// Create column select menu item +QAction *AISDemodGUI::createCheckableItem(QString &text, int idx, bool checked, const char *slot) +{ + QAction *action = new QAction(text, this); + action->setCheckable(true); + action->setChecked(checked); + action->setData(QVariant(idx)); + connect(action, SIGNAL(triggered()), this, slot); + return action; +} + +AISDemodGUI* AISDemodGUI::create(PluginAPI* pluginAPI, DeviceUISet *deviceUISet, BasebandSampleSink *rxChannel) +{ + AISDemodGUI* gui = new AISDemodGUI(pluginAPI, deviceUISet, rxChannel); + return gui; +} + +void AISDemodGUI::destroy() +{ + delete this; +} + +void AISDemodGUI::resetToDefaults() +{ + m_settings.resetToDefaults(); + displaySettings(); + applySettings(true); +} + +QByteArray AISDemodGUI::serialize() const +{ + return m_settings.serialize(); +} + +bool AISDemodGUI::deserialize(const QByteArray& data) +{ + if(m_settings.deserialize(data)) { + displaySettings(); + applySettings(true); + return true; + } else { + resetToDefaults(); + return false; + } +} + +// Add row to table +void AISDemodGUI::messageReceived(const AISDemod::MsgMessage& message) +{ + AISMessage *ais; + + // Decode the message + ais = AISMessage::decode(message.getMessage()); + + // Add to messages table + ui->messages->setSortingEnabled(false); + int row = ui->messages->rowCount(); + ui->messages->setRowCount(row + 1); + + QTableWidgetItem *dateItem = new QTableWidgetItem(); + QTableWidgetItem *timeItem = new QTableWidgetItem(); + QTableWidgetItem *mmsiItem = new QTableWidgetItem(); + QTableWidgetItem *typeItem = new QTableWidgetItem(); + QTableWidgetItem *dataItem = new QTableWidgetItem(); + QTableWidgetItem *nmeaItem = new QTableWidgetItem(); + QTableWidgetItem *hexItem = new QTableWidgetItem(); + ui->messages->setItem(row, MESSAGE_COL_DATE, dateItem); + ui->messages->setItem(row, MESSAGE_COL_TIME, timeItem); + ui->messages->setItem(row, MESSAGE_COL_MMSI, mmsiItem); + ui->messages->setItem(row, MESSAGE_COL_TYPE, typeItem); + ui->messages->setItem(row, MESSAGE_COL_DATA, dataItem); + ui->messages->setItem(row, MESSAGE_COL_NMEA, nmeaItem); + ui->messages->setItem(row, MESSAGE_COL_HEX, hexItem); + dateItem->setText(message.getDateTime().date().toString()); + timeItem->setText(message.getDateTime().time().toString()); + mmsiItem->setText(QString("%1").arg(ais->m_mmsi, 9, 10, QChar('0'))); + typeItem->setText(ais->getType()); + dataItem->setText(ais->toString()); + nmeaItem->setText(ais->toNMEA()); + hexItem->setText(ais->toHex()); + ui->messages->setSortingEnabled(true); + ui->messages->scrollToItem(dateItem); // Will only scroll if not hidden + filterRow(row); +} + +bool AISDemodGUI::handleMessage(const Message& message) +{ + if (AISDemod::MsgConfigureAISDemod::match(message)) + { + qDebug("AISDemodGUI::handleMessage: AISDemod::MsgConfigureAISDemod"); + const AISDemod::MsgConfigureAISDemod& cfg = (AISDemod::MsgConfigureAISDemod&) message; + m_settings = cfg.getSettings(); + blockApplySettings(true); + displaySettings(); + blockApplySettings(false); + return true; + } + else if (AISDemod::MsgMessage::match(message)) + { + AISDemod::MsgMessage& report = (AISDemod::MsgMessage&) message; + messageReceived(report); + return true; + } + + return false; +} + +void AISDemodGUI::handleInputMessages() +{ + Message* message; + + while ((message = getInputMessageQueue()->pop()) != 0) + { + if (handleMessage(*message)) + { + delete message; + } + } +} + +void AISDemodGUI::channelMarkerChangedByCursor() +{ + ui->deltaFrequency->setValue(m_channelMarker.getCenterFrequency()); + m_settings.m_inputFrequencyOffset = m_channelMarker.getCenterFrequency(); + applySettings(); +} + +void AISDemodGUI::channelMarkerHighlightedByCursor() +{ + setHighlighted(m_channelMarker.getHighlighted()); +} + +void AISDemodGUI::on_deltaFrequency_changed(qint64 value) +{ + m_channelMarker.setCenterFrequency(value); + m_settings.m_inputFrequencyOffset = m_channelMarker.getCenterFrequency(); + applySettings(); +} + +void AISDemodGUI::on_rfBW_valueChanged(int value) +{ + float bw = value * 100.0f; + ui->rfBWText->setText(QString("%1k").arg(value / 10.0, 0, 'f', 1)); + m_channelMarker.setBandwidth(bw); + m_settings.m_rfBandwidth = bw; + applySettings(); +} + +void AISDemodGUI::on_fmDev_valueChanged(int value) +{ + ui->fmDevText->setText(QString("%1k").arg(value / 10.0, 0, 'f', 1)); + m_settings.m_fmDeviation = value * 100.0; + applySettings(); +} + +void AISDemodGUI::on_threshold_valueChanged(int value) +{ + ui->thresholdText->setText(QString("%1").arg(value)); + m_settings.m_correlationThreshold = value; + applySettings(); +} + +void AISDemodGUI::on_filterMMSI_editingFinished() +{ + m_settings.m_filterMMSI = ui->filterMMSI->text(); + filter(); + applySettings(); +} + +void AISDemodGUI::on_clearTable_clicked() +{ + ui->messages->setRowCount(0); +} + +void AISDemodGUI::on_udpEnabled_clicked(bool checked) +{ + m_settings.m_udpEnabled = checked; + applySettings(); +} + +void AISDemodGUI::on_udpAddress_editingFinished() +{ + m_settings.m_udpAddress = ui->udpAddress->text(); + applySettings(); +} + +void AISDemodGUI::on_udpPort_editingFinished() +{ + m_settings.m_udpPort = ui->udpPort->text().toInt(); + applySettings(); +} + +void AISDemodGUI::on_udpFormat_currentIndexChanged(int value) +{ + m_settings.m_udpFormat = (AISDemodSettings::UDPFormat)value; + applySettings(); +} + +void AISDemodGUI::on_channel1_currentIndexChanged(int index) +{ + m_settings.m_scopeCh1 = index; + applySettings(); +} + +void AISDemodGUI::on_channel2_currentIndexChanged(int index) +{ + m_settings.m_scopeCh2 = index; + applySettings(); +} + +void AISDemodGUI::on_messages_cellDoubleClicked(int row, int column) +{ + // Get MMSI of message in row double clicked + QString mmsi = ui->messages->item(row, MESSAGE_COL_MMSI)->text(); + if (column == MESSAGE_COL_MMSI) + { + // Search for MMSI on www.vesselfinder.com + QDesktopServices::openUrl(QUrl(QString("https://www.vesselfinder.com/vessels?name=%1").arg(mmsi))); + } +} + +void AISDemodGUI::filterRow(int row) +{ + bool hidden = false; + if (m_settings.m_filterMMSI != "") + { + QRegExp re(m_settings.m_filterMMSI); + QTableWidgetItem *fromItem = ui->messages->item(row, MESSAGE_COL_MMSI); + if (!re.exactMatch(fromItem->text())) + hidden = true; + } + ui->messages->setRowHidden(row, hidden); +} + +void AISDemodGUI::filter() +{ + for (int i = 0; i < ui->messages->rowCount(); i++) + { + filterRow(i); + } +} + +void AISDemodGUI::onWidgetRolled(QWidget* widget, bool rollDown) +{ + (void) widget; + (void) rollDown; +} + +void AISDemodGUI::onMenuDialogCalled(const QPoint &p) +{ + if (m_contextMenuType == ContextMenuChannelSettings) + { + BasicChannelSettingsDialog dialog(&m_channelMarker, this); + dialog.setUseReverseAPI(m_settings.m_useReverseAPI); + dialog.setReverseAPIAddress(m_settings.m_reverseAPIAddress); + dialog.setReverseAPIPort(m_settings.m_reverseAPIPort); + dialog.setReverseAPIDeviceIndex(m_settings.m_reverseAPIDeviceIndex); + dialog.setReverseAPIChannelIndex(m_settings.m_reverseAPIChannelIndex); + dialog.move(p); + dialog.exec(); + + m_settings.m_rgbColor = m_channelMarker.getColor().rgb(); + m_settings.m_title = m_channelMarker.getTitle(); + m_settings.m_useReverseAPI = dialog.useReverseAPI(); + m_settings.m_reverseAPIAddress = dialog.getReverseAPIAddress(); + m_settings.m_reverseAPIPort = dialog.getReverseAPIPort(); + m_settings.m_reverseAPIDeviceIndex = dialog.getReverseAPIDeviceIndex(); + m_settings.m_reverseAPIChannelIndex = dialog.getReverseAPIChannelIndex(); + + setWindowTitle(m_settings.m_title); + setTitleColor(m_settings.m_rgbColor); + + applySettings(); + } + else if ((m_contextMenuType == ContextMenuStreamSettings) && (m_deviceUISet->m_deviceMIMOEngine)) + { + DeviceStreamSelectionDialog dialog(this); + dialog.setNumberOfStreams(m_aisDemod->getNumberOfDeviceStreams()); + dialog.setStreamIndex(m_settings.m_streamIndex); + dialog.move(p); + dialog.exec(); + + m_settings.m_streamIndex = dialog.getSelectedStreamIndex(); + m_channelMarker.clearStreamIndexes(); + m_channelMarker.addStreamIndex(m_settings.m_streamIndex); + displayStreamIndex(); + applySettings(); + } + + resetContextMenuType(); +} + +AISDemodGUI::AISDemodGUI(PluginAPI* pluginAPI, DeviceUISet *deviceUISet, BasebandSampleSink *rxChannel, QWidget* parent) : + ChannelGUI(parent), + ui(new Ui::AISDemodGUI), + m_pluginAPI(pluginAPI), + m_deviceUISet(deviceUISet), + m_channelMarker(this), + m_doApplySettings(true), + m_tickCount(0) +{ + ui->setupUi(this); + + setAttribute(Qt::WA_DeleteOnClose, true); + connect(this, SIGNAL(widgetRolled(QWidget*,bool)), this, SLOT(onWidgetRolled(QWidget*,bool))); + connect(this, SIGNAL(customContextMenuRequested(const QPoint &)), this, SLOT(onMenuDialogCalled(const QPoint &))); + + m_aisDemod = reinterpret_cast(rxChannel); + m_aisDemod->setMessageQueueToGUI(getInputMessageQueue()); + + connect(&MainCore::instance()->getMasterTimer(), SIGNAL(timeout()), this, SLOT(tick())); // 50 ms + + m_scopeVis = new ScopeVis(ui->glScope); + m_aisDemod->setScopeSink(m_scopeVis); + ui->glScope->connectTimer(MainCore::instance()->getMasterTimer()); + ui->scopeGUI->setBuddies(m_scopeVis->getInputMessageQueue(), m_scopeVis, ui->glScope); + + // Scope settings to display the IQ waveforms + ui->scopeGUI->setPreTrigger(1); + ScopeVis::TraceData traceDataI, traceDataQ; + traceDataI.m_projectionType = Projector::ProjectionReal; + traceDataI.m_amp = 1.0; // for -1 to +1 + traceDataI.m_ampIndex = 0; + traceDataI.m_ofs = 0.0; // vertical offset + traceDataI.m_ofsCoarse = 0; + traceDataQ.m_projectionType = Projector::ProjectionImag; + traceDataQ.m_amp = 1.0; + traceDataQ.m_ampIndex = 0; + traceDataQ.m_ofs = 0.0; + traceDataQ.m_ofsCoarse = 0; + ui->scopeGUI->changeTrace(0, traceDataI); + ui->scopeGUI->addTrace(traceDataQ); + ui->scopeGUI->setDisplayMode(GLScopeGUI::DisplayXYV); + ui->scopeGUI->focusOnTrace(0); // re-focus to take changes into account in the GUI + + ScopeVis::TriggerData triggerData; + triggerData.m_triggerLevel = 0.1; + triggerData.m_triggerLevelCoarse = 10; + triggerData.m_triggerPositiveEdge = true; + ui->scopeGUI->changeTrigger(0, triggerData); + ui->scopeGUI->focusOnTrigger(0); // re-focus to take changes into account in the GUI + + m_scopeVis->setLiveRate(9600*6); + //m_scopeVis->setFreeRun(false); // FIXME: add method rather than call m_scopeVis->configure() + + ui->deltaFrequencyLabel->setText(QString("%1f").arg(QChar(0x94, 0x03))); + ui->deltaFrequency->setColorMapper(ColorMapper(ColorMapper::GrayGold)); + ui->deltaFrequency->setValueRange(false, 7, -9999999, 9999999); + ui->channelPowerMeter->setColorTheme(LevelMeterSignalDB::ColorGreenAndBlue); + + m_channelMarker.blockSignals(true); + m_channelMarker.setColor(Qt::yellow); + m_channelMarker.setBandwidth(m_settings.m_rfBandwidth); + m_channelMarker.setCenterFrequency(m_settings.m_inputFrequencyOffset); + m_channelMarker.setTitle("AIS Demodulator"); + m_channelMarker.blockSignals(false); + m_channelMarker.setVisible(true); // activate signal on the last setting only + + setTitleColor(m_channelMarker.getColor()); + m_settings.setChannelMarker(&m_channelMarker); + + m_deviceUISet->addChannelMarker(&m_channelMarker); + m_deviceUISet->addRollupWidget(this); + + connect(&m_channelMarker, SIGNAL(changedByCursor()), this, SLOT(channelMarkerChangedByCursor())); + connect(&m_channelMarker, SIGNAL(highlightedByCursor()), this, SLOT(channelMarkerHighlightedByCursor())); + connect(getInputMessageQueue(), SIGNAL(messageEnqueued()), this, SLOT(handleInputMessages())); + + // Resize the table using dummy data + resizeTable(); + // Allow user to reorder columns + ui->messages->horizontalHeader()->setSectionsMovable(true); + // Allow user to sort table by clicking on headers + ui->messages->setSortingEnabled(true); + // Add context menu to allow hiding/showing of columns + messagesMenu = new QMenu(ui->messages); + for (int i = 0; i < ui->messages->horizontalHeader()->count(); i++) + { + QString text = ui->messages->horizontalHeaderItem(i)->text(); + messagesMenu->addAction(createCheckableItem(text, i, true, SLOT(messagesColumnSelectMenuChecked()))); + } + ui->messages->horizontalHeader()->setContextMenuPolicy(Qt::CustomContextMenu); + connect(ui->messages->horizontalHeader(), SIGNAL(customContextMenuRequested(QPoint)), SLOT(messagesColumnSelectMenu(QPoint))); + // Get signals when columns change + connect(ui->messages->horizontalHeader(), SIGNAL(sectionMoved(int, int, int)), SLOT(messages_sectionMoved(int, int, int))); + connect(ui->messages->horizontalHeader(), SIGNAL(sectionResized(int, int, int)), SLOT(messages_sectionResized(int, int, int))); + ui->messages->setContextMenuPolicy(Qt::CustomContextMenu); + connect(ui->messages, SIGNAL(customContextMenuRequested(QPoint)), SLOT(customContextMenuRequested(QPoint))); + + ui->scopeContainer->setVisible(false); + + displaySettings(); + applySettings(true); +} + +void AISDemodGUI::customContextMenuRequested(QPoint pos) +{ + QTableWidgetItem *item = ui->messages->itemAt(pos); + if (item) + { + QMenu* tableContextMenu = new QMenu(ui->messages); + connect(tableContextMenu, &QMenu::aboutToHide, tableContextMenu, &QMenu::deleteLater); + QAction* copyAction = new QAction("Copy", tableContextMenu); + const QString text = item->text(); + connect(copyAction, &QAction::triggered, this, [text]()->void { + QClipboard *clipboard = QGuiApplication::clipboard(); + clipboard->setText(text); + }); + tableContextMenu->addAction(copyAction); + tableContextMenu->popup(ui->messages->viewport()->mapToGlobal(pos)); + } +} + +AISDemodGUI::~AISDemodGUI() +{ + delete ui; +} + +void AISDemodGUI::blockApplySettings(bool block) +{ + m_doApplySettings = !block; +} + +void AISDemodGUI::applySettings(bool force) +{ + if (m_doApplySettings) + { + AISDemod::MsgConfigureAISDemod* message = AISDemod::MsgConfigureAISDemod::create( m_settings, force); + m_aisDemod->getInputMessageQueue()->push(message); + } +} + +void AISDemodGUI::displaySettings() +{ + m_channelMarker.blockSignals(true); + m_channelMarker.setBandwidth(m_settings.m_rfBandwidth); + m_channelMarker.setCenterFrequency(m_settings.m_inputFrequencyOffset); + m_channelMarker.setTitle(m_settings.m_title); + m_channelMarker.blockSignals(false); + m_channelMarker.setColor(m_settings.m_rgbColor); // activate signal on the last setting only + + setTitleColor(m_settings.m_rgbColor); + setWindowTitle(m_channelMarker.getTitle()); + + blockApplySettings(true); + + ui->deltaFrequency->setValue(m_channelMarker.getCenterFrequency()); + + ui->rfBWText->setText(QString("%1k").arg(m_settings.m_rfBandwidth / 1000.0, 0, 'f', 1)); + ui->rfBW->setValue(m_settings.m_rfBandwidth / 100.0); + + ui->fmDevText->setText(QString("%1k").arg(m_settings.m_fmDeviation / 1000.0, 0, 'f', 1)); + ui->fmDev->setValue(m_settings.m_fmDeviation / 100.0); + + ui->thresholdText->setText(QString("%1").arg(m_settings.m_correlationThreshold)); + ui->threshold->setValue(m_settings.m_correlationThreshold); + + displayStreamIndex(); + + ui->filterMMSI->setText(m_settings.m_filterMMSI); + + ui->udpEnabled->setChecked(m_settings.m_udpEnabled); + ui->udpAddress->setText(m_settings.m_udpAddress); + ui->udpPort->setText(QString::number(m_settings.m_udpPort)); + ui->udpFormat->setCurrentIndex((int)m_settings.m_udpFormat); + + ui->channel1->setCurrentIndex(m_settings.m_scopeCh1); + ui->channel2->setCurrentIndex(m_settings.m_scopeCh2); + + // Order and size columns + QHeaderView *header = ui->messages->horizontalHeader(); + for (int i = 0; i < AISDEMOD_MESSAGE_COLUMNS; i++) + { + bool hidden = m_settings.m_messageColumnSizes[i] == 0; + header->setSectionHidden(i, hidden); + messagesMenu->actions().at(i)->setChecked(!hidden); + if (m_settings.m_messageColumnSizes[i] > 0) + ui->messages->setColumnWidth(i, m_settings.m_messageColumnSizes[i]); + header->moveSection(header->visualIndex(i), m_settings.m_messageColumnIndexes[i]); + } + filter(); + + blockApplySettings(false); +} + +void AISDemodGUI::displayStreamIndex() +{ + if (m_deviceUISet->m_deviceMIMOEngine) { + setStreamIndicator(tr("%1").arg(m_settings.m_streamIndex)); + } else { + setStreamIndicator("S"); // single channel indicator + } +} + +void AISDemodGUI::leaveEvent(QEvent*) +{ + m_channelMarker.setHighlighted(false); +} + +void AISDemodGUI::enterEvent(QEvent*) +{ + m_channelMarker.setHighlighted(true); +} + +void AISDemodGUI::tick() +{ + double magsqAvg, magsqPeak; + int nbMagsqSamples; + m_aisDemod->getMagSqLevels(magsqAvg, magsqPeak, nbMagsqSamples); + double powDbAvg = CalcDb::dbPower(magsqAvg); + double powDbPeak = CalcDb::dbPower(magsqPeak); + + ui->channelPowerMeter->levelChanged( + (100.0f + powDbAvg) / 100.0f, + (100.0f + powDbPeak) / 100.0f, + nbMagsqSamples); + + if (m_tickCount % 4 == 0) { + ui->channelPower->setText(QString::number(powDbAvg, 'f', 1)); + } + + m_tickCount++; +} diff --git a/plugins/channelrx/demodais/aisdemodgui.h b/plugins/channelrx/demodais/aisdemodgui.h new file mode 100644 index 000000000..19222435a --- /dev/null +++ b/plugins/channelrx/demodais/aisdemodgui.h @@ -0,0 +1,134 @@ +/////////////////////////////////////////////////////////////////////////////////// +// Copyright (C) 2016 Edouard Griffiths, F4EXB // +// Copyright (C) 2021 Jon Beniston, M7RCE // +// // +// This program is free software; you can redistribute it and/or modify // +// it under the terms of the GNU General Public License as published by // +// the Free Software Foundation as version 3 of the License, or // +// (at your option) any later version. // +// // +// This program is distributed in the hope that it will be useful, // +// but WITHOUT ANY WARRANTY; without even the implied warranty of // +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // +// GNU General Public License V3 for more details. // +// // +// You should have received a copy of the GNU General Public License // +// along with this program. If not, see . // +/////////////////////////////////////////////////////////////////////////////////// + +#ifndef INCLUDE_AISDEMODGUI_H +#define INCLUDE_AISDEMODGUI_H + +#include +#include +#include +#include +#include + +#include "channel/channelgui.h" +#include "dsp/channelmarker.h" +#include "util/messagequeue.h" + +#include "aisdemodsettings.h" +#include "aisdemod.h" + +class PluginAPI; +class DeviceUISet; +class BasebandSampleSink; +class ScopeVis; +class ScopeVisXY; +class AISDemod; +class AISDemodGUI; + +namespace Ui { + class AISDemodGUI; +} +class AISDemodGUI; +class AISMessage; + +class AISDemodGUI : public ChannelGUI { + Q_OBJECT + +public: + static AISDemodGUI* create(PluginAPI* pluginAPI, DeviceUISet *deviceUISet, BasebandSampleSink *rxChannel); + virtual void destroy(); + + void resetToDefaults(); + QByteArray serialize() const; + bool deserialize(const QByteArray& data); + virtual MessageQueue *getInputMessageQueue() { return &m_inputMessageQueue; } + +public slots: + void channelMarkerChangedByCursor(); + void channelMarkerHighlightedByCursor(); + +private: + Ui::AISDemodGUI* ui; + PluginAPI* m_pluginAPI; + DeviceUISet* m_deviceUISet; + ChannelMarker m_channelMarker; + AISDemodSettings m_settings; + bool m_doApplySettings; + ScopeVis* m_scopeVis; + + AISDemod* m_aisDemod; + uint32_t m_tickCount; + MessageQueue m_inputMessageQueue; + + QMenu *messagesMenu; // Column select context menu + QMenu *copyMenu; + + explicit AISDemodGUI(PluginAPI* pluginAPI, DeviceUISet *deviceUISet, BasebandSampleSink *rxChannel, QWidget* parent = 0); + virtual ~AISDemodGUI(); + + void blockApplySettings(bool block); + void applySettings(bool force = false); + void displaySettings(); + void displayStreamIndex(); + void messageReceived(const AISDemod::MsgMessage& message); + bool handleMessage(const Message& message); + + void leaveEvent(QEvent*); + void enterEvent(QEvent*); + + void resizeTable(); + QAction *createCheckableItem(QString& text, int idx, bool checked, const char *slot); + + enum MessageCol { + MESSAGE_COL_DATE, + MESSAGE_COL_TIME, + MESSAGE_COL_MMSI, + MESSAGE_COL_TYPE, + MESSAGE_COL_DATA, + MESSAGE_COL_NMEA, + MESSAGE_COL_HEX + }; + +private slots: + void on_deltaFrequency_changed(qint64 value); + void on_rfBW_valueChanged(int index); + void on_fmDev_valueChanged(int value); + void on_threshold_valueChanged(int value); + void on_filterMMSI_editingFinished(); + void on_clearTable_clicked(); + void on_udpEnabled_clicked(bool checked); + void on_udpAddress_editingFinished(); + void on_udpPort_editingFinished(); + void on_udpFormat_currentIndexChanged(int value); + void on_channel1_currentIndexChanged(int index); + void on_channel2_currentIndexChanged(int index); + void on_messages_cellDoubleClicked(int row, int column); + void filterRow(int row); + void filter(); + void messages_sectionMoved(int logicalIndex, int oldVisualIndex, int newVisualIndex); + void messages_sectionResized(int logicalIndex, int oldSize, int newSize); + void messagesColumnSelectMenu(QPoint pos); + void messagesColumnSelectMenuChecked(bool checked = false); + void customContextMenuRequested(QPoint point); + void onWidgetRolled(QWidget* widget, bool rollDown); + void onMenuDialogCalled(const QPoint& p); + void handleInputMessages(); + void tick(); +}; + +#endif // INCLUDE_AISDEMODGUI_H diff --git a/plugins/channelrx/demodais/aisdemodgui.ui b/plugins/channelrx/demodais/aisdemodgui.ui new file mode 100644 index 000000000..26e273a96 --- /dev/null +++ b/plugins/channelrx/demodais/aisdemodgui.ui @@ -0,0 +1,898 @@ + + + AISDemodGUI + + + + 0 + 0 + 404 + 764 + + + + + 0 + 0 + + + + + 352 + 0 + + + + + Liberation Sans + 9 + + + + Qt::StrongFocus + + + AIS Demodulator + + + AIS Demodulator + + + + + 0 + 0 + 390 + 151 + + + + + 350 + 0 + + + + Settings + + + + 3 + + + 2 + + + 2 + + + 2 + + + 2 + + + + + 2 + + + + + + 16 + 0 + + + + Df + + + + + + + + 0 + 0 + + + + + 32 + 16 + + + + + Liberation Mono + 12 + + + + PointingHandCursor + + + Qt::StrongFocus + + + Demod shift frequency from center in Hz + + + + + + + Hz + + + + + + + Qt::Vertical + + + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + + + + Channel power + + + Qt::RightToLeft + + + 0.0 + + + + + + + dB + + + + + + + + + + + + + dB + + + + + + + + 0 + 0 + + + + + 0 + 24 + + + + + Liberation Mono + 8 + + + + Level meter (dB) top trace: average, bottom trace: instantaneous peak, tip: peak hold + + + + + + + + + Qt::Horizontal + + + + + + + + + Qt::Vertical + + + + + + + BW + + + + + + + + 0 + 0 + + + + + 0 + 0 + + + + RF bandwidth + + + 10 + + + 400 + + + 1 + + + 100 + + + Qt::Horizontal + + + + + + + + 30 + 0 + + + + 10.0k + + + Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter + + + + + + + Qt::Vertical + + + + + + + Dev + + + + + + + + 0 + 0 + + + + + 0 + 0 + + + + Frequency deviation + + + 10 + + + 50 + + + 1 + + + 24 + + + Qt::Horizontal + + + + + + + + 30 + 0 + + + + 2.4k + + + Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter + + + + + + + Qt::Vertical + + + + + + + TH + + + + + + + + 24 + 24 + + + + Correlation threshold + + + 0 + + + 60 + + + 1 + + + 5 + + + 0 + + + + + + + 60 + + + + + + + + + Qt::Horizontal + + + + + + + + + Send packets via UDP + + + Qt::RightToLeft + + + UDP + + + + + + + + 120 + 0 + + + + Qt::ClickFocus + + + Destination UDP address + + + 000.000.000.000 + + + 127.0.0.1 + + + + + + + : + + + Qt::AlignCenter + + + + + + + + 50 + 0 + + + + + 50 + 16777215 + + + + Qt::ClickFocus + + + Destination UDP port + + + 00000 + + + 9998 + + + + + + + Format + + + + + + + + 60 + 0 + + + + Format used to forward received AIS messages + + + + Binary + + + + + NMEA + + + + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + + + + Qt::Horizontal + + + + + + + + + Find + + + + + + + Display only messages where the MMSI matches the specified regular expression + + + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + + Clear messages from table + + + + + + + :/bin.png:/bin.png + + + + + + + + + + + 0 + 210 + 391 + 171 + + + + + 0 + 0 + + + + Received Messages + + + + 2 + + + 3 + + + 3 + + + 3 + + + 3 + + + + + Received packets + + + QAbstractItemView::NoEditTriggers + + + + Date + + + + + Time + + + + + MMSI + + + + + Type + + + + + Data + + + Packet data as ASCII + + + + + NMEA + + + + + Hex + + + Packet data as hex + + + + + + + + + + 20 + 400 + 351 + 341 + + + + Waveforms + + + + 2 + + + 3 + + + 3 + + + 3 + + + 3 + + + + + + + Real + + + + + + + + 0 + 0 + + + + + I + + + + + Q + + + + + Mag Sq + + + + + FM demod + + + + + Gaussian + + + + + RX buf + + + + + Correlation + + + + + Threshold met + + + + + DC offset + + + + + CRC + + + + + + + + + 0 + 0 + + + + Imag + + + + + + + + 0 + 0 + + + + + I + + + + + Q + + + + + Mag Sq + + + + + FM demod + + + + + Gaussian + + + + + RX buf + + + + + Correlation + + + + + Threshold met + + + + + DC offset + + + + + CRC + + + + + + + + + + + 200 + 250 + + + + + Liberation Mono + 8 + + + + + + + + + + + + + RollupWidget + QWidget +

    gui/rollupwidget.h
    + 1 + + + ValueDialZ + QWidget +
    gui/valuedialz.h
    + 1 +
    + + LevelMeterSignalDB + QWidget +
    gui/levelmeter.h
    + 1 +
    + + GLScope + QWidget +
    gui/glscope.h
    + 1 +
    + + GLScopeGUI + QWidget +
    gui/glscopegui.h
    + 1 +
    + + + + + + diff --git a/plugins/channelrx/demodais/aisdemodplugin.cpp b/plugins/channelrx/demodais/aisdemodplugin.cpp new file mode 100644 index 000000000..5101c081b --- /dev/null +++ b/plugins/channelrx/demodais/aisdemodplugin.cpp @@ -0,0 +1,92 @@ +/////////////////////////////////////////////////////////////////////////////////// +// Copyright (C) 2016 Edouard Griffiths, F4EXB // +// Copyright (C) 2021 Jon Beniston, M7RCE // +// // +// This program is free software; you can redistribute it and/or modify // +// it under the terms of the GNU General Public License as published by // +// the Free Software Foundation as version 3 of the License, or // +// (at your option) any later version. // +// // +// This program is distributed in the hope that it will be useful, // +// but WITHOUT ANY WARRANTY; without even the implied warranty of // +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // +// GNU General Public License V3 for more details. // +// // +// You should have received a copy of the GNU General Public License // +// along with this program. If not, see . // +/////////////////////////////////////////////////////////////////////////////////// + +#include +#include "plugin/pluginapi.h" + +#ifndef SERVER_MODE +#include "aisdemodgui.h" +#endif +#include "aisdemod.h" +#include "aisdemodwebapiadapter.h" +#include "aisdemodplugin.h" + +const PluginDescriptor AISDemodPlugin::m_pluginDescriptor = { + AISDemod::m_channelId, + QStringLiteral("AIS Demodulator"), + QStringLiteral("6.11.1"), + QStringLiteral("(c) Jon Beniston, M7RCE"), + QStringLiteral("https://github.com/f4exb/sdrangel"), + true, + QStringLiteral("https://github.com/f4exb/sdrangel") +}; + +AISDemodPlugin::AISDemodPlugin(QObject* parent) : + QObject(parent), + m_pluginAPI(0) +{ +} + +const PluginDescriptor& AISDemodPlugin::getPluginDescriptor() const +{ + return m_pluginDescriptor; +} + +void AISDemodPlugin::initPlugin(PluginAPI* pluginAPI) +{ + m_pluginAPI = pluginAPI; + + m_pluginAPI->registerRxChannel(AISDemod::m_channelIdURI, AISDemod::m_channelId, this); +} + +void AISDemodPlugin::createRxChannel(DeviceAPI *deviceAPI, BasebandSampleSink **bs, ChannelAPI **cs) const +{ + if (bs || cs) + { + AISDemod *instance = new AISDemod(deviceAPI); + + if (bs) { + *bs = instance; + } + + if (cs) { + *cs = instance; + } + } +} + +#ifdef SERVER_MODE +ChannelGUI* AISDemodPlugin::createRxChannelGUI( + DeviceUISet *deviceUISet, + BasebandSampleSink *rxChannel) const +{ + (void) deviceUISet; + (void) rxChannel; + return 0; +} +#else +ChannelGUI* AISDemodPlugin::createRxChannelGUI(DeviceUISet *deviceUISet, BasebandSampleSink *rxChannel) const +{ + return AISDemodGUI::create(m_pluginAPI, deviceUISet, rxChannel); +} +#endif + +ChannelWebAPIAdapter* AISDemodPlugin::createChannelWebAPIAdapter() const +{ + return new AISDemodWebAPIAdapter(); +} diff --git a/plugins/channelrx/demodais/aisdemodplugin.h b/plugins/channelrx/demodais/aisdemodplugin.h new file mode 100644 index 000000000..81e0d36ec --- /dev/null +++ b/plugins/channelrx/demodais/aisdemodplugin.h @@ -0,0 +1,49 @@ +/////////////////////////////////////////////////////////////////////////////////// +// Copyright (C) 2016 Edouard Griffiths, F4EXB // +// Copyright (C) 2021 Jon Beniston, M7RCE // +// // +// This program is free software; you can redistribute it and/or modify // +// it under the terms of the GNU General Public License as published by // +// the Free Software Foundation as version 3 of the License, or // +// (at your option) any later version. // +// // +// This program is distributed in the hope that it will be useful, // +// but WITHOUT ANY WARRANTY; without even the implied warranty of // +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // +// GNU General Public License V3 for more details. // +// // +// You should have received a copy of the GNU General Public License // +// along with this program. If not, see . // +/////////////////////////////////////////////////////////////////////////////////// + +#ifndef INCLUDE_AISDEMODPLUGIN_H +#define INCLUDE_AISDEMODPLUGIN_H + +#include +#include "plugin/plugininterface.h" + +class DeviceUISet; +class BasebandSampleSink; + +class AISDemodPlugin : public QObject, PluginInterface { + Q_OBJECT + Q_INTERFACES(PluginInterface) + Q_PLUGIN_METADATA(IID "sdrangel.channel.aisdemod") + +public: + explicit AISDemodPlugin(QObject* parent = NULL); + + const PluginDescriptor& getPluginDescriptor() const; + void initPlugin(PluginAPI* pluginAPI); + + virtual void createRxChannel(DeviceAPI *deviceAPI, BasebandSampleSink **bs, ChannelAPI **cs) const; + virtual ChannelGUI* createRxChannelGUI(DeviceUISet *deviceUISet, BasebandSampleSink *rxChannel) const; + virtual ChannelWebAPIAdapter* createChannelWebAPIAdapter() const; + +private: + static const PluginDescriptor m_pluginDescriptor; + + PluginAPI* m_pluginAPI; +}; + +#endif // INCLUDE_AISDEMODPLUGIN_H diff --git a/plugins/channelrx/demodais/aisdemodsettings.cpp b/plugins/channelrx/demodais/aisdemodsettings.cpp new file mode 100644 index 000000000..d2c4a1a75 --- /dev/null +++ b/plugins/channelrx/demodais/aisdemodsettings.cpp @@ -0,0 +1,163 @@ +/////////////////////////////////////////////////////////////////////////////////// +// Copyright (C) 2015 Edouard Griffiths, F4EXB. // +// Copyright (C) 2021 Jon Beniston, M7RCE // +// // +// This program is free software; you can redistribute it and/or modify // +// it under the terms of the GNU General Public License as published by // +// the Free Software Foundation as version 3 of the License, or // +// (at your option) any later version. // +// // +// This program is distributed in the hope that it will be useful, // +// but WITHOUT ANY WARRANTY; without even the implied warranty of // +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // +// GNU General Public License V3 for more details. // +// // +// You should have received a copy of the GNU General Public License // +// along with this program. If not, see . // +/////////////////////////////////////////////////////////////////////////////////// + +#include + +#include "dsp/dspengine.h" +#include "util/simpleserializer.h" +#include "settings/serializable.h" +#include "aisdemodsettings.h" + +AISDemodSettings::AISDemodSettings() : + m_channelMarker(0) +{ + resetToDefaults(); +} + +void AISDemodSettings::resetToDefaults() +{ + m_baud = 9600; // Fixed + m_inputFrequencyOffset = 0; + m_rfBandwidth = 16000.0f; + m_fmDeviation = 4800.0f; + m_correlationThreshold = 30; + m_filterMMSI = ""; + m_udpEnabled = false; + m_udpAddress = "127.0.0.1"; + m_udpPort = 9999; + m_udpFormat = Binary; + m_scopeCh1 = 5; + m_scopeCh2 = 6; + m_rgbColor = QColor(102, 0, 0).rgb(); + m_title = "AIS Demodulator"; + m_streamIndex = 0; + m_useReverseAPI = false; + m_reverseAPIAddress = "127.0.0.1"; + m_reverseAPIPort = 8888; + m_reverseAPIDeviceIndex = 0; + m_reverseAPIChannelIndex = 0; + + for (int i = 0; i < AISDEMOD_MESSAGE_COLUMNS; i++) + { + m_messageColumnIndexes[i] = i; + m_messageColumnSizes[i] = -1; // Autosize + } +} + +QByteArray AISDemodSettings::serialize() const +{ + SimpleSerializer s(1); + + s.writeS32(1, m_inputFrequencyOffset); + s.writeFloat(2, m_rfBandwidth); + s.writeFloat(3, m_fmDeviation); + s.writeFloat(4, m_correlationThreshold); + s.writeString(5, m_filterMMSI); + s.writeBool(6, m_udpEnabled); + s.writeString(7, m_udpAddress); + s.writeU32(8, m_udpPort); + s.writeS32(9, (int)m_udpFormat); + s.writeS32(10, m_scopeCh1); + s.writeS32(11, m_scopeCh2); + s.writeU32(12, m_rgbColor); + s.writeString(13, m_title); + if (m_channelMarker) { + s.writeBlob(14, m_channelMarker->serialize()); + } + s.writeS32(15, m_streamIndex); + s.writeBool(16, m_useReverseAPI); + s.writeString(17, m_reverseAPIAddress); + s.writeU32(18, m_reverseAPIPort); + s.writeU32(19, m_reverseAPIDeviceIndex); + s.writeU32(20, m_reverseAPIChannelIndex); + + for (int i = 0; i < AISDEMOD_MESSAGE_COLUMNS; i++) + s.writeS32(100 + i, m_messageColumnIndexes[i]); + for (int i = 0; i < AISDEMOD_MESSAGE_COLUMNS; i++) + s.writeS32(200 + i, m_messageColumnSizes[i]); + + return s.final(); +} + +bool AISDemodSettings::deserialize(const QByteArray& data) +{ + SimpleDeserializer d(data); + + if(!d.isValid()) + { + resetToDefaults(); + return false; + } + + if(d.getVersion() == 1) + { + QByteArray bytetmp; + uint32_t utmp; + QString strtmp; + + d.readS32(1, &m_inputFrequencyOffset, 0); + d.readFloat(2, &m_rfBandwidth, 16000.0f); + d.readFloat(3, &m_fmDeviation, 4800.0f); + d.readFloat(4, &m_correlationThreshold, 30); + d.readString(5, &m_filterMMSI, ""); + d.readBool(6, &m_udpEnabled); + d.readString(7, &m_udpAddress); + d.readU32(8, &utmp); + if ((utmp > 1023) && (utmp < 65535)) { + m_udpPort = utmp; + } else { + m_udpPort = 9999; + } + d.readS32(9, (int *)&m_udpFormat, (int)Binary); + d.readS32(10, &m_scopeCh1, 0); + d.readS32(11, &m_scopeCh2, 0); + d.readU32(12, &m_rgbColor, QColor(102, 0, 0).rgb()); + d.readString(13, &m_title, "AIS Demodulator"); + d.readBlob(14, &bytetmp); + if (m_channelMarker) { + m_channelMarker->deserialize(bytetmp); + } + d.readS32(15, &m_streamIndex, 0); + d.readBool(16, &m_useReverseAPI, false); + d.readString(17, &m_reverseAPIAddress, "127.0.0.1"); + d.readU32(18, &utmp, 0); + if ((utmp > 1023) && (utmp < 65535)) { + m_reverseAPIPort = utmp; + } else { + m_reverseAPIPort = 8888; + } + d.readU32(19, &utmp, 0); + m_reverseAPIDeviceIndex = utmp > 99 ? 99 : utmp; + d.readU32(20, &utmp, 0); + m_reverseAPIChannelIndex = utmp > 99 ? 99 : utmp; + + for (int i = 0; i < AISDEMOD_MESSAGE_COLUMNS; i++) + d.readS32(100 + i, &m_messageColumnIndexes[i], i); + for (int i = 0; i < AISDEMOD_MESSAGE_COLUMNS; i++) + d.readS32(200 + i, &m_messageColumnSizes[i], -1); + + return true; + } + else + { + resetToDefaults(); + return false; + } +} + + diff --git a/plugins/channelrx/demodais/aisdemodsettings.h b/plugins/channelrx/demodais/aisdemodsettings.h new file mode 100644 index 000000000..6e889566a --- /dev/null +++ b/plugins/channelrx/demodais/aisdemodsettings.h @@ -0,0 +1,70 @@ +/////////////////////////////////////////////////////////////////////////////////// +// Copyright (C) 2017 Edouard Griffiths, F4EXB. // +// Copyright (C) 2021 Jon Beniston, M7RCE // +// // +// This program is free software; you can redistribute it and/or modify // +// it under the terms of the GNU General Public License as published by // +// the Free Software Foundation as version 3 of the License, or // +// (at your option) any later version. // +// // +// This program is distributed in the hope that it will be useful, // +// but WITHOUT ANY WARRANTY; without even the implied warranty of // +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // +// GNU General Public License V3 for more details. // +// // +// You should have received a copy of the GNU General Public License // +// along with this program. If not, see . // +/////////////////////////////////////////////////////////////////////////////////// + +#ifndef INCLUDE_AISDEMODSETTINGS_H +#define INCLUDE_AISDEMODSETTINGS_H + +#include +#include + +#include "dsp/dsptypes.h" + +class Serializable; + +// Number of columns in the tables +#define AISDEMOD_MESSAGE_COLUMNS 7 + +struct AISDemodSettings +{ + qint32 m_baud; + qint32 m_inputFrequencyOffset; + Real m_rfBandwidth; + Real m_fmDeviation; + Real m_correlationThreshold; + QString m_filterMMSI; + bool m_udpEnabled; + QString m_udpAddress; + uint16_t m_udpPort; + enum UDPFormat { + Binary, + NMEA + } m_udpFormat; + int m_scopeCh1; + int m_scopeCh2; + + quint32 m_rgbColor; + QString m_title; + Serializable *m_channelMarker; + int m_streamIndex; //!< MIMO channel. Not relevant when connected to SI (single Rx). + bool m_useReverseAPI; + QString m_reverseAPIAddress; + uint16_t m_reverseAPIPort; + uint16_t m_reverseAPIDeviceIndex; + uint16_t m_reverseAPIChannelIndex; + + int m_messageColumnIndexes[AISDEMOD_MESSAGE_COLUMNS];//!< How the columns are ordered in the table + int m_messageColumnSizes[AISDEMOD_MESSAGE_COLUMNS]; //!< Size of the columns in the table + + AISDemodSettings(); + void resetToDefaults(); + void setChannelMarker(Serializable *channelMarker) { m_channelMarker = channelMarker; } + QByteArray serialize() const; + bool deserialize(const QByteArray& data); +}; + +#endif /* INCLUDE_AISDEMODSETTINGS_H */ diff --git a/plugins/channelrx/demodais/aisdemodsink.cpp b/plugins/channelrx/demodais/aisdemodsink.cpp new file mode 100644 index 000000000..7aa129511 --- /dev/null +++ b/plugins/channelrx/demodais/aisdemodsink.cpp @@ -0,0 +1,473 @@ +/////////////////////////////////////////////////////////////////////////////////// +// Copyright (C) 2019 Edouard Griffiths, F4EXB // +// Copyright (C) 2021 Jon Beniston, M7RCE // +// // +// This program is free software; you can redistribute it and/or modify // +// it under the terms of the GNU General Public License as published by // +// the Free Software Foundation as version 3 of the License, or // +// (at your option) any later version. // +// // +// This program is distributed in the hope that it will be useful, // +// but WITHOUT ANY WARRANTY; without even the implied warranty of // +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // +// GNU General Public License V3 for more details. // +// // +// You should have received a copy of the GNU General Public License // +// along with this program. If not, see . // +/////////////////////////////////////////////////////////////////////////////////// + +#include + +#include + +#include "dsp/dspengine.h" +#include "dsp/datafifo.h" +#include "util/db.h" +#include "util/stepfunctions.h" +#include "pipes/pipeendpoint.h" +#include "maincore.h" + +#include "aisdemod.h" +#include "aisdemodsink.h" + +#define AISDEMOD_MINS_MAXS 5 + +AISDemodSink::AISDemodSink(AISDemod *aisDemod) : + m_scopeSink(nullptr), + m_aisDemod(aisDemod), + m_channelSampleRate(AISDEMOD_CHANNEL_SAMPLE_RATE), + m_channelFrequencyOffset(0), + m_magsqSum(0.0f), + m_magsqPeak(0.0f), + m_magsqCount(0), + m_messageQueueToChannel(nullptr), + m_rxBuf(nullptr), + m_train(nullptr) +{ + m_magsq = 0.0; + + m_demodBuffer.resize(1<<12); + m_demodBufferFill = 0; + + applySettings(m_settings, true); + applyChannelSettings(m_channelSampleRate, m_channelFrequencyOffset, true); +} + +AISDemodSink::~AISDemodSink() +{ + delete[] m_rxBuf; + delete[] m_train; +} + +void AISDemodSink::sampleToScope(Complex sample) +{ + if (m_scopeSink) + { + Real r = std::real(sample) * SDR_RX_SCALEF; + Real i = std::imag(sample) * SDR_RX_SCALEF; + SampleVector m_sampleBuffer; + m_sampleBuffer.push_back(Sample(r, i)); + m_scopeSink->feed(m_sampleBuffer.begin(), m_sampleBuffer.end(), true); + m_sampleBuffer.clear(); + } +} + +void AISDemodSink::feed(const SampleVector::const_iterator& begin, const SampleVector::const_iterator& end) +{ + Complex ci; + + for (SampleVector::const_iterator it = begin; it != end; ++it) + { + Complex c(it->real(), it->imag()); + c *= m_nco.nextIQ(); + + if (m_interpolatorDistance < 1.0f) // interpolate + { + while (!m_interpolator.interpolate(&m_interpolatorDistanceRemain, c, &ci)) + { + processOneSample(ci); + m_interpolatorDistanceRemain += m_interpolatorDistance; + } + } + else // decimate + { + if (m_interpolator.decimate(&m_interpolatorDistanceRemain, c, &ci)) + { + processOneSample(ci); + m_interpolatorDistanceRemain += m_interpolatorDistance; + } + } + } +} + +void AISDemodSink::processOneSample(Complex &ci) +{ + Complex ca; + + // FM demodulation + double magsqRaw; + Real deviation; + Real fmDemod = m_phaseDiscri.phaseDiscriminatorDelta(ci, magsqRaw, deviation); + + // Calculate average and peak levels for level meter + Real magsq = magsqRaw / (SDR_RX_SCALED*SDR_RX_SCALED); + m_movingAverage(magsq); + m_magsq = m_movingAverage.asDouble(); + m_magsqSum += magsq; + if (magsq > m_magsqPeak) + { + m_magsqPeak = magsq; + } + m_magsqCount++; + + // Gaussian filter + Real filt = m_pulseShape.filter(fmDemod); + + // An input frequency offset corresponds to a DC offset after FM demodulation + // AIS spec allows up to +-1kHz offset + // We need to remove this, otherwise it may effect the sampling + // To calculate what it is, we sum the training sequence, which should be zero + + // Clip, as large noise can result in high correlation + // Don't clip to 1.0 - as there may be some DC offset (1k/4.8k max dev=0.2) + Real filtClipped; + filtClipped = std::fmax(-1.4, std::fmin(1.4, filt)); + + // Buffer filtered samples. We buffer enough samples for a max length message + // before trying to demod, so false triggering can't make us miss anything + m_rxBuf[m_rxBufIdx] = filtClipped; + m_rxBufIdx = (m_rxBufIdx + 1) % m_rxBufLength; + m_rxBufCnt = std::min(m_rxBufCnt + 1, m_rxBufLength); + + Real corr = 0.0f; + bool scopeCRCValid = false; + bool scopeCRCInvalid = false; + Real dcOffset = 0.0f; + bool thresholdMet = false; + Real peakFreq = 0.0f; + Real freqOffset = 0.0f; + if (m_rxBufCnt >= m_rxBufLength) + { + Real trainingSum = 0.0f; + + // Correlate with training sequence + // Note that DC offset doesn't matter for this + // Calculate sum to estimate DC offset + for (int i = 0; i < m_correlationLength; i++) + { + int j = (m_rxBufIdx + i) % m_rxBufLength; + corr += m_train[i] * m_rxBuf[j]; + trainingSum += m_rxBuf[j]; + } + + // If we meet threshold, try to demod + // Take abs value, to account for both initial phases + thresholdMet = fabs(corr) >= m_settings.m_correlationThreshold; + if (thresholdMet) + { + // Use mean of preamble as DC offset + dcOffset = trainingSum/m_correlationLength; + + // Start demod after (most of) preamble + int x = (m_rxBufIdx + m_correlationLength*3/4 + 4) % m_rxBufLength; + + // Attempt to demodulate + bool gotSOP = false; + int bits = 0; + int bitCount = 0; + int onesCount = 0; + int byteCount = 0; + int symbolPrev = 0; + for (int sampleIdx = 0; sampleIdx < m_rxBufLength; sampleIdx += m_samplesPerSymbol) + { + // Sum and slice + // Summing 3 samples seems to give a very small improvement vs just using 1 + int sampleCnt = 3; + int sampleOffset = -1; + Real sampleSum = 0.0f; + for (int i = 0; i < sampleCnt; i++) { + sampleSum += m_rxBuf[(x + sampleOffset + i) % m_rxBufLength] - dcOffset; + } + int sample = sampleSum >= 0.0f ? 1 : 0; + + // Move to next sample + x = (x + m_samplesPerSymbol) % m_rxBufLength; + + // HDLC deframing + + // NRZI decoding + int bit; + if (sample != symbolPrev) { + bit = 0; + } else { + bit = 1; + } + symbolPrev = sample; + + // Store in shift reg + bits |= bit << bitCount; + bitCount++; + + if (bit == 1) + { + onesCount++; + // Shouldn't ever get 7 1s in a row + if ((onesCount == 7) && gotSOP) + { + gotSOP = false; + byteCount = 0; + break; + } + } + else if (bit == 0) + { + if (onesCount == 5) + { + // Remove bit-stuffing (5 1s followed by a 0) + bitCount--; + } + else if (onesCount == 6) + { + // Start/end of packet + if (gotSOP && (bitCount == 8) && (bits == 0x7e) && (byteCount > 0)) + { + // End of packet + // Check CRC is valid + m_crc.init(); + m_crc.calculate(m_bytes, byteCount - 2); + uint16_t calcCrc = m_crc.get(); + uint16_t rxCrc = m_bytes[byteCount-2] | (m_bytes[byteCount-1] << 8); + if (calcCrc == rxCrc) + { + scopeCRCValid = true; + QByteArray rxPacket((char *)m_bytes, byteCount - 2); // Don't include CRC + //qDebug() << "RX: " << rxPacket.toHex(); + if (getMessageQueueToChannel()) + { + AISDemod::MsgMessage *msg = AISDemod::MsgMessage::create(rxPacket); + getMessageQueueToChannel()->push(msg); + } + + // Skip over received packet, so we don't try to re-demodulate it + m_rxBufCnt -= sampleIdx; + } + else + { + //qDebug() << QString("CRC mismatch: %1 %2").arg(calcCrc, 4, 16, QLatin1Char('0')).arg(rxCrc, 4, 16, QLatin1Char('0')); + scopeCRCInvalid = true; + } + break; + } + else if (gotSOP) + { + // Repeated start flag without data or misalignment, something not right + break; + } + else + { + // Start of packet + gotSOP = true; + bits = 0; + bitCount = 0; + byteCount = 0; + } + } + onesCount = 0; + } + + if (gotSOP) + { + if (bitCount == 8) + { + // Could also check count according to message ID as that varies + if (byteCount >= AISDEMOD_MAX_BYTES) + { + // Too many bytes + break; + } + else + { + // Got a complete byte + m_bytes[byteCount] = bits; + byteCount++; + } + bits = 0; + bitCount = 0; + } + } + + // Abort demod if we haven't found start flag within a couple of bytes of presumed preamble + if (!gotSOP && (sampleIdx >= 16 * m_samplesPerSymbol)) { + break; + } + } + } + } + + // Select signals to feed to scope + Complex scopeSample; + switch (m_settings.m_scopeCh1) + { + case 0: + scopeSample.real(ci.real() / SDR_RX_SCALEF); + break; + case 1: + scopeSample.real(ci.imag() / SDR_RX_SCALEF); + break; + case 2: + scopeSample.real(magsq); + break; + case 3: + scopeSample.real(fmDemod); + break; + case 4: + scopeSample.real(filt); + break; + case 5: + scopeSample.real(m_rxBuf[m_rxBufIdx]); + break; + case 6: + scopeSample.real(corr / 100.0); + break; + case 7: + scopeSample.real(thresholdMet); + break; + case 8: + scopeSample.real(dcOffset); + break; + case 9: + scopeSample.real(scopeCRCValid ? 1.0 : (scopeCRCInvalid ? -1.0 : 0)); + break; + } + switch (m_settings.m_scopeCh2) + { + case 0: + scopeSample.imag(ci.real() / SDR_RX_SCALEF); + break; + case 1: + scopeSample.imag(ci.imag() / SDR_RX_SCALEF); + break; + case 2: + scopeSample.imag(magsq); + break; + case 3: + scopeSample.imag(fmDemod); + break; + case 4: + scopeSample.imag(filt); + break; + case 5: + scopeSample.imag(m_rxBuf[m_rxBufIdx]); + break; + case 6: + scopeSample.imag(corr / 100.0); + break; + case 7: + scopeSample.imag(thresholdMet); + break; + case 8: + scopeSample.imag(dcOffset); + break; + case 9: + scopeSample.imag(scopeCRCValid ? 1.0 : (scopeCRCInvalid ? -1.0 : 0)); + break; + } + sampleToScope(scopeSample); + + // Send demod signal to Demod Analzyer feature + m_demodBuffer[m_demodBufferFill++] = fmDemod * std::numeric_limits::max(); + + if (m_demodBufferFill >= m_demodBuffer.size()) + { + QList *dataFifos = MainCore::instance()->getDataPipes().getFifos(m_channel, "demod"); + + if (dataFifos) + { + QList::iterator it = dataFifos->begin(); + + for (; it != dataFifos->end(); ++it) { + (*it)->write((quint8*) &m_demodBuffer[0], m_demodBuffer.size() * sizeof(qint16)); + } + } + + m_demodBufferFill = 0; + } +} + +void AISDemodSink::applyChannelSettings(int channelSampleRate, int channelFrequencyOffset, bool force) +{ + qDebug() << "AISDemodSink::applyChannelSettings:" + << " channelSampleRate: " << channelSampleRate + << " channelFrequencyOffset: " << channelFrequencyOffset; + + if ((m_channelFrequencyOffset != channelFrequencyOffset) || + (m_channelSampleRate != channelSampleRate) || force) + { + m_nco.setFreq(-channelFrequencyOffset, channelSampleRate); + } + + if ((m_channelSampleRate != channelSampleRate) || force) + { + m_interpolator.create(16, channelSampleRate, m_settings.m_rfBandwidth / 2.2); + m_interpolatorDistance = (Real) channelSampleRate / (Real) AISDEMOD_CHANNEL_SAMPLE_RATE; + m_interpolatorDistanceRemain = m_interpolatorDistance; + } + + m_channelSampleRate = channelSampleRate; + m_channelFrequencyOffset = channelFrequencyOffset; + m_samplesPerSymbol = AISDEMOD_CHANNEL_SAMPLE_RATE / m_settings.m_baud; + qDebug() << "AISDemodSink::applyChannelSettings: m_samplesPerSymbol: " << m_samplesPerSymbol; +} + +void AISDemodSink::applySettings(const AISDemodSettings& settings, bool force) +{ + qDebug() << "AISDemodSink::applySettings:" + << " force: " << force; + + if ((settings.m_rfBandwidth != m_settings.m_rfBandwidth) || force) + { + m_interpolator.create(16, m_channelSampleRate, settings.m_rfBandwidth / 2.2); + m_interpolatorDistance = (Real) m_channelSampleRate / (Real) AISDEMOD_CHANNEL_SAMPLE_RATE; + m_interpolatorDistanceRemain = m_interpolatorDistance; + m_lowpass.create(301, AISDEMOD_CHANNEL_SAMPLE_RATE, settings.m_rfBandwidth / 2.0f); + } + if ((settings.m_fmDeviation != m_settings.m_fmDeviation) || force) + { + m_phaseDiscri.setFMScaling(AISDEMOD_CHANNEL_SAMPLE_RATE / (2.0f * settings.m_fmDeviation)); + } + + if ((settings.m_baud != m_settings.m_baud) || force) + { + m_samplesPerSymbol = AISDEMOD_CHANNEL_SAMPLE_RATE / settings.m_baud; + qDebug() << "ISDemodSink::applySettings: m_samplesPerSymbol: " << m_samplesPerSymbol << " baud " << settings.m_baud; + m_pulseShape.create(0.5, 3, m_samplesPerSymbol); + + // Recieve buffer, long enough for one max length message + delete[] m_rxBuf; + m_rxBufLength = AISDEMOD_MAX_BYTES*8*m_samplesPerSymbol; + m_rxBuf = new Real[m_rxBufLength]; + m_rxBufIdx = 0; + m_rxBufCnt = 0; + + // Create 24-bit training sequence for correlation + delete[] m_train; + m_correlationLength = 24*m_samplesPerSymbol; + m_train = new Real[m_correlationLength](); + const int trainNRZ[24] = {1, 0, 0, 1, 1, 0, 0, 1, 1, 0, 0, 1, 1, 0, 0, 1, 1, 0, 0, 1, 1, 0, 0, 1}; + + // Pulse shape filter takes a few symbols before outputting expected shape + for (int j = 0; j < m_samplesPerSymbol; j++) + m_pulseShape.filter(-1.0f); + for (int j = 0; j < m_samplesPerSymbol; j++) + m_pulseShape.filter(1.0f); + for (int i = 0; i < 24; i++) + { + for (int j = 0; j < m_samplesPerSymbol; j++) + { + m_train[i*m_samplesPerSymbol+j] = m_pulseShape.filter(trainNRZ[i] * 2.0f - 1.0f); + } + } + } + + m_settings = settings; +} diff --git a/plugins/channelrx/demodais/aisdemodsink.h b/plugins/channelrx/demodais/aisdemodsink.h new file mode 100644 index 000000000..963881e1e --- /dev/null +++ b/plugins/channelrx/demodais/aisdemodsink.h @@ -0,0 +1,142 @@ +/////////////////////////////////////////////////////////////////////////////////// +// Copyright (C) 2019 Edouard Griffiths, F4EXB // +// Copyright (C) 2021 Jon Beniston, M7RCE // +// // +// This program is free software; you can redistribute it and/or modify // +// it under the terms of the GNU General Public License as published by // +// the Free Software Foundation as version 3 of the License, or // +// (at your option) any later version. // +// // +// This program is distributed in the hope that it will be useful, // +// but WITHOUT ANY WARRANTY; without even the implied warranty of // +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // +// GNU General Public License V3 for more details. // +// // +// You should have received a copy of the GNU General Public License // +// along with this program. If not, see . // +/////////////////////////////////////////////////////////////////////////////////// + +#ifndef INCLUDE_AISDEMODSINK_H +#define INCLUDE_AISDEMODSINK_H + +#include + +#include "dsp/channelsamplesink.h" +#include "dsp/phasediscri.h" +#include "dsp/nco.h" +#include "dsp/interpolator.h" +#include "dsp/firfilter.h" +#include "dsp/gaussian.h" +#include "dsp/fftfactory.h" +#include "dsp/fftengine.h" +#include "dsp/fftwindow.h" +#include "util/movingaverage.h" +#include "util/doublebufferfifo.h" +#include "util/messagequeue.h" +#include "util/crc.h" + +#include "aisdemodsettings.h" + +#include +#include +#include + +// 6x 9600 baud rate (use even multiple so Gausian filter has odd number of taps) +#define AISDEMOD_CHANNEL_SAMPLE_RATE 57600 + +#define AISDEMOD_MAX_BYTES (3+1+126+2+1+1) + +class ChannelAPI; +class AISDemod; + +class AISDemodSink : public ChannelSampleSink { +public: + AISDemodSink(AISDemod *aisDemod); + ~AISDemodSink(); + + virtual void feed(const SampleVector::const_iterator& begin, const SampleVector::const_iterator& end); + + void setScopeSink(BasebandSampleSink* scopeSink) { m_scopeSink = scopeSink; } + void applyChannelSettings(int channelSampleRate, int channelFrequencyOffset, bool force = false); + void applySettings(const AISDemodSettings& settings, bool force = false); + void setMessageQueueToChannel(MessageQueue *messageQueue) { m_messageQueueToChannel = messageQueue; } + void setChannel(ChannelAPI *channel) { m_channel = channel; } + + double getMagSq() const { return m_magsq; } + + void getMagSqLevels(double& avg, double& peak, int& nbSamples) + { + if (m_magsqCount > 0) + { + m_magsq = m_magsqSum / m_magsqCount; + m_magSqLevelStore.m_magsq = m_magsq; + m_magSqLevelStore.m_magsqPeak = m_magsqPeak; + } + + avg = m_magSqLevelStore.m_magsq; + peak = m_magSqLevelStore.m_magsqPeak; + nbSamples = m_magsqCount == 0 ? 1 : m_magsqCount; + + m_magsqSum = 0.0f; + m_magsqPeak = 0.0f; + m_magsqCount = 0; + } + + +private: + struct MagSqLevelsStore + { + MagSqLevelsStore() : + m_magsq(1e-12), + m_magsqPeak(1e-12) + {} + double m_magsq; + double m_magsqPeak; + }; + + BasebandSampleSink* m_scopeSink; // Scope GUI to display baseband waveform + AISDemod *m_aisDemod; + AISDemodSettings m_settings; + ChannelAPI *m_channel; + int m_channelSampleRate; + int m_channelFrequencyOffset; + int m_samplesPerSymbol; // Number of samples per symbol + + NCO m_nco; + Interpolator m_interpolator; + Real m_interpolatorDistance; + Real m_interpolatorDistanceRemain; + + double m_magsq; + double m_magsqSum; + double m_magsqPeak; + int m_magsqCount; + MagSqLevelsStore m_magSqLevelStore; + + MessageQueue *m_messageQueueToChannel; + + MovingAverageUtil m_movingAverage; + + Lowpass m_lowpass; // RF input filter + PhaseDiscriminators m_phaseDiscri; // FM demodulator + Gaussian m_pulseShape; // Pulse shaping filter + Real *m_rxBuf; // Receive sample buffer, large enough for one max length messsage + int m_rxBufLength; // Size in elements in m_rxBuf + int m_rxBufIdx; // Index in to circular buffer + int m_rxBufCnt; // Number of valid samples in buffer + Real *m_train; // Training sequence to look for + int m_correlationLength; + + int m_symbolPrev; + unsigned char m_bytes[AISDEMOD_MAX_BYTES]; + crc16x25 m_crc; + + QVector m_demodBuffer; + int m_demodBufferFill; + + void processOneSample(Complex &ci); + MessageQueue *getMessageQueueToChannel() { return m_messageQueueToChannel; } + void sampleToScope(Complex sample); +}; + +#endif // INCLUDE_AISDEMODSINK_H diff --git a/plugins/channelrx/demodais/aisdemodwebapiadapter.cpp b/plugins/channelrx/demodais/aisdemodwebapiadapter.cpp new file mode 100644 index 000000000..d52b31d02 --- /dev/null +++ b/plugins/channelrx/demodais/aisdemodwebapiadapter.cpp @@ -0,0 +1,52 @@ +/////////////////////////////////////////////////////////////////////////////////// +// Copyright (C) 2019 Edouard Griffiths, F4EXB. // +// Copyright (C) 2021 Jon Beniston, M7RCE // +// // +// This program is free software; you can redistribute it and/or modify // +// it under the terms of the GNU General Public License as published by // +// the Free Software Foundation as version 3 of the License, or // +// (at your option) any later version. // +// // +// This program is distributed in the hope that it will be useful, // +// but WITHOUT ANY WARRANTY; without even the implied warranty of // +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // +// GNU General Public License V3 for more details. // +// // +// You should have received a copy of the GNU General Public License // +// along with this program. If not, see . // +/////////////////////////////////////////////////////////////////////////////////// + +#include "SWGChannelSettings.h" +#include "aisdemod.h" +#include "aisdemodwebapiadapter.h" + +AISDemodWebAPIAdapter::AISDemodWebAPIAdapter() +{} + +AISDemodWebAPIAdapter::~AISDemodWebAPIAdapter() +{} + +int AISDemodWebAPIAdapter::webapiSettingsGet( + SWGSDRangel::SWGChannelSettings& response, + QString& errorMessage) +{ + (void) errorMessage; + response.setAisDemodSettings(new SWGSDRangel::SWGAISDemodSettings()); + response.getAisDemodSettings()->init(); + AISDemod::webapiFormatChannelSettings(response, m_settings); + + return 200; +} + +int AISDemodWebAPIAdapter::webapiSettingsPutPatch( + bool force, + const QStringList& channelSettingsKeys, + SWGSDRangel::SWGChannelSettings& response, + QString& errorMessage) +{ + (void) force; + (void) errorMessage; + AISDemod::webapiUpdateChannelSettings(m_settings, channelSettingsKeys, response); + + return 200; +} diff --git a/plugins/channelrx/demodais/aisdemodwebapiadapter.h b/plugins/channelrx/demodais/aisdemodwebapiadapter.h new file mode 100644 index 000000000..ba4060ee3 --- /dev/null +++ b/plugins/channelrx/demodais/aisdemodwebapiadapter.h @@ -0,0 +1,50 @@ +/////////////////////////////////////////////////////////////////////////////////// +// Copyright (C) 2019 Edouard Griffiths, F4EXB. // +// Copyright (C) 2020 Jon Beniston, M7RCE // +// // +// This program is free software; you can redistribute it and/or modify // +// it under the terms of the GNU General Public License as published by // +// the Free Software Foundation as version 3 of the License, or // +// (at your option) any later version. // +// // +// This program is distributed in the hope that it will be useful, // +// but WITHOUT ANY WARRANTY; without even the implied warranty of // +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // +// GNU General Public License V3 for more details. // +// // +// You should have received a copy of the GNU General Public License // +// along with this program. If not, see . // +/////////////////////////////////////////////////////////////////////////////////// + +#ifndef INCLUDE_AISDEMOD_WEBAPIADAPTER_H +#define INCLUDE_AISDEMOD_WEBAPIADAPTER_H + +#include "channel/channelwebapiadapter.h" +#include "aisdemodsettings.h" + +/** + * Standalone API adapter only for the settings + */ +class AISDemodWebAPIAdapter : public ChannelWebAPIAdapter { +public: + AISDemodWebAPIAdapter(); + virtual ~AISDemodWebAPIAdapter(); + + virtual QByteArray serialize() const { return m_settings.serialize(); } + virtual bool deserialize(const QByteArray& data) { return m_settings.deserialize(data); } + + virtual int webapiSettingsGet( + SWGSDRangel::SWGChannelSettings& response, + QString& errorMessage); + + virtual int webapiSettingsPutPatch( + bool force, + const QStringList& channelSettingsKeys, + SWGSDRangel::SWGChannelSettings& response, + QString& errorMessage); + +private: + AISDemodSettings m_settings; +}; + +#endif // INCLUDE_AISDEMOD_WEBAPIADAPTER_H diff --git a/plugins/channelrx/demodais/readme.md b/plugins/channelrx/demodais/readme.md new file mode 100644 index 000000000..e20ed49d1 --- /dev/null +++ b/plugins/channelrx/demodais/readme.md @@ -0,0 +1,85 @@ +

    AIS demodulator plugin

    + +

    Introduction

    + +This plugin can be used to demodulate AIS (Automatic Identification System) messages. AIS can be used to track ships and other marine vessels at sea, that are equiped with AIS transponders. It is also used by shore-side infrastructure known as base stations, aids-to-navigation such as buoys and some search and rescue aircraft. + +AIS is broadcast globally on 25kHz channels at 161.975MHz and 162.025MHz, with other frequencies being used regionally or for special purposes. This demodulator is single channel, so if you wish to decode multiple channels simulatenously, you will need to add one AIS demodulator per frequency. As most AIS messages are on 161.975MHz and 162.025MHz, you can set the center frequency as 162MHz, with a sample rate of 100k+Sa/s, with one AIS demod with an input offset -25kHz and another at +25kHz. + +The AIS demodulators can send received messages to the AIS feature, which displays a table combining the latest data for vessels amalgamated from multiple demodulators and display their position on the Map Feature. + +AIS uses GMSK/FM modulation at a baud rate of 9,600, with a modulation index of 0.5. The demodulator works at a sample rate of 57,600Sa/s. + +Received AIS messages can be NMEA encoded and forwarded via UDP to 3rd party applications. + +The AIS specification is ITU-R M.1371-5: https://www.itu.int/dms_pubrec/itu-r/rec/m/R-REC-M.1371-5-201402-I!!PDF-E.pdf + +

    Interface

    + +![AIS Demodulator plugin GUI](../../../doc/img/AISDemod_plugin.png) + +

    1: Frequency shift from center frequency of reception

    + +Use the wheels to adjust the frequency shift in Hz from the center frequency of reception. Left click on a digit sets the cursor position at this digit. Right click on a digit sets all digits on the right to zero. This effectively floors value at the digit position. Wheels are moved with the mousewheel while pointing at the wheel or by selecting the wheel with the left mouse click and using the keyboard arrows. Pressing shift simultaneously moves digit by 5 and pressing control moves it by 2. + +

    2: Channel power

    + +Average total power in dB relative to a +/- 1.0 amplitude signal received in the pass band. + +

    3: Level meter in dB

    + + - top bar (green): average value + - bottom bar (blue green): instantaneous peak value + - tip vertical bar (bright green): peak hold value + +

    4: BW - RF Bandwidth

    + +This specifies the bandwidth of a LPF that is applied to the input signal to limit the RF bandwidth. While AIS channels are 25kHz wide, more messages seem to be able to be received if this is around 16kHz. + +

    5: Dev - Frequency deviation

    + +Adjusts the expected frequency deviation in 0.1 kHz steps from 1 to 6 kHz. Typical values are 4.8 kHz, corresponding to a modulation index of 0.5 at 9,600 baud. + +

    6: TH - Correlation Threshold

    + +The correlation threshold between the received signal and the preamble (training sequence). A lower value should be able to demodulate weaker signals, but increases processor usage and may result in invalid messages if too low. + +

    7: Find

    + +Entering a regular expression in the Find field displays only messages where the source MMSI matches the given regular expression. + +

    8: Clear Messages from table

    + +Pressing this button clears all messages from the table. + +

    9: UDP

    + +When checked, received messages are forwarded to the specified UDP address (12) and port (13). + +

    10: UDP address

    + +IP address of the host to forward received messages to via UDP. + +

    11: UDP port

    + +UDP port number to forward received messages to. + +

    12: UDP format

    + +The format the messages are forwared via UDP in. This can be either binary (which is useful for SDRangel's PERTester feature) or NMEA (which is useful for 3rd party applications such as OpenCPN). + +

    Received Messages Table

    + +The received messages table displays information about each AIS message received. Only messages with valid CRCs are displayed. + +![AIS Demodulator plugin GUI](../../../doc/img/AISDemod_plugin_messages.png) + +* Date - The date the message was received. +* Time - The time the message was received. +* MMSI - The Maritime Mobile Service Identity number of the source of the message. Double clicking on this column will search for the MMSI on https://www.vesselfinder.com/ +* Type - The type of AIS message. E.g. Position report, Base station report or Ship static and voyage related data. +* Data - A textual decode of the message displaying the most interesting fields. +* NMEA - The message in NMEA format. +* Hex - The message in hex format. + +Right clicking on the table header allows you to select which columns to show. The columns can be reorderd by left clicking and dragging the column header. Right clicking on an item in the table allows you to copy the value to the clipboard. diff --git a/plugins/channeltx/CMakeLists.txt b/plugins/channeltx/CMakeLists.txt index dd9ae6ca8..27217a7af 100644 --- a/plugins/channeltx/CMakeLists.txt +++ b/plugins/channeltx/CMakeLists.txt @@ -1,5 +1,6 @@ project(mod) +add_subdirectory(modais) add_subdirectory(modam) add_subdirectory(modchirpchat) add_subdirectory(modnfm) diff --git a/plugins/channeltx/modais/CMakeLists.txt b/plugins/channeltx/modais/CMakeLists.txt new file mode 100644 index 000000000..07ee1e58e --- /dev/null +++ b/plugins/channeltx/modais/CMakeLists.txt @@ -0,0 +1,64 @@ +project(modais) + +set(modais_SOURCES + aismod.cpp + aismodbaseband.cpp + aismodsource.cpp + aismodplugin.cpp + aismodsettings.cpp + aismodwebapiadapter.cpp +) + +set(modais_HEADERS + aismod.h + aismodbaseband.h + aismodsource.h + aismodplugin.h + aismodsettings.h + aismodwebapiadapter.h +) + +include_directories( + ${CMAKE_SOURCE_DIR}/swagger/sdrangel/code/qt5/client +) + +if(NOT SERVER_MODE) + set(modais_SOURCES + ${modais_SOURCES} + aismodgui.cpp + aismodgui.ui + aismodrepeatdialog.cpp + aismodrepeatdialog.ui + aismodtxsettingsdialog.cpp + aismodtxsettingsdialog.ui + ) + set(modais_HEADERS + ${modais_HEADERS} + aismodgui.h + aismodrepeatdialog.h + aismodtxsettingsdialog.h + ) + set(TARGET_NAME modais) + set(TARGET_LIB "Qt5::Widgets") + set(TARGET_LIB_GUI "sdrgui") + set(INSTALL_FOLDER ${INSTALL_PLUGINS_DIR}) +else() + set(TARGET_NAME modaissrv) + set(TARGET_LIB "") + set(TARGET_LIB_GUI "") + set(INSTALL_FOLDER ${INSTALL_PLUGINSSRV_DIR}) +endif() + +add_library(${TARGET_NAME} SHARED + ${modais_SOURCES} +) + +target_link_libraries(${TARGET_NAME} + Qt5::Core + ${TARGET_LIB} + sdrbase + ${TARGET_LIB_GUI} + swagger +) + +install(TARGETS ${TARGET_NAME} DESTINATION ${INSTALL_FOLDER}) diff --git a/plugins/channeltx/modais/aismod.cpp b/plugins/channeltx/modais/aismod.cpp new file mode 100644 index 000000000..90b802cb2 --- /dev/null +++ b/plugins/channeltx/modais/aismod.cpp @@ -0,0 +1,824 @@ +/////////////////////////////////////////////////////////////////////////////////// +// Copyright (C) 2016 Edouard Griffiths, F4EXB // +// Copyright (C) 2020 Jon Beniston, M7RCE // +// // +// This program is free software; you can redistribute it and/or modify // +// it under the terms of the GNU General Public License as published by // +// the Free Software Foundation as version 3 of the License, or // +// (at your option) any later version. // +// // +// This program is distributed in the hope that it will be useful, // +// but WITHOUT ANY WARRANTY; without even the implied warranty of // +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // +// GNU General Public License V3 for more details. // +// // +// You should have received a copy of the GNU General Public License // +// along with this program. If not, see . // +/////////////////////////////////////////////////////////////////////////////////// + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "SWGChannelSettings.h" +#include "SWGChannelReport.h" +#include "SWGChannelActions.h" + +#include +#include +#include + +#include "dsp/dspengine.h" +#include "dsp/dspcommands.h" +#include "device/deviceapi.h" +#include "feature/feature.h" +#include "util/db.h" +#include "maincore.h" + +#include "aismodbaseband.h" +#include "aismod.h" + +MESSAGE_CLASS_DEFINITION(AISMod::MsgConfigureAISMod, Message) +MESSAGE_CLASS_DEFINITION(AISMod::MsgTXAISMod, Message) +MESSAGE_CLASS_DEFINITION(AISMod::MsgTXPacketBytes, Message) + +const char* const AISMod::m_channelIdURI = "sdrangel.channel.modais"; +const char* const AISMod::m_channelId = "AISMod"; + +AISMod::AISMod(DeviceAPI *deviceAPI) : + ChannelAPI(m_channelIdURI, ChannelAPI::StreamSingleSource), + m_deviceAPI(deviceAPI), + m_spectrumVis(SDR_TX_SCALEF), + m_settingsMutex(QMutex::Recursive), + m_udpSocket(nullptr) +{ + setObjectName(m_channelId); + + m_thread = new QThread(this); + m_basebandSource = new AISModBaseband(); + m_basebandSource->setSpectrumSampleSink(&m_spectrumVis); + m_basebandSource->setChannel(this); + m_basebandSource->moveToThread(m_thread); + + applySettings(m_settings, true); + + m_deviceAPI->addChannelSource(this); + m_deviceAPI->addChannelSourceAPI(this); + + m_networkManager = new QNetworkAccessManager(); + connect(m_networkManager, SIGNAL(finished(QNetworkReply*)), this, SLOT(networkManagerFinished(QNetworkReply*))); +} + +AISMod::~AISMod() +{ + closeUDP(); + disconnect(m_networkManager, SIGNAL(finished(QNetworkReply*)), this, SLOT(networkManagerFinished(QNetworkReply*))); + delete m_networkManager; + m_deviceAPI->removeChannelSourceAPI(this); + m_deviceAPI->removeChannelSource(this); + delete m_basebandSource; + delete m_thread; +} + +void AISMod::start() +{ + qDebug("AISMod::start"); + m_basebandSource->reset(); + m_thread->start(); +} + +void AISMod::stop() +{ + qDebug("AISMod::stop"); + m_thread->exit(); + m_thread->wait(); +} + +void AISMod::pull(SampleVector::iterator& begin, unsigned int nbSamples) +{ + m_basebandSource->pull(begin, nbSamples); +} + +bool AISMod::handleMessage(const Message& cmd) +{ + if (MsgConfigureAISMod::match(cmd)) + { + MsgConfigureAISMod& cfg = (MsgConfigureAISMod&) cmd; + qDebug() << "AISMod::handleMessage: MsgConfigureAISMod"; + + applySettings(cfg.getSettings(), cfg.getForce()); + + return true; + } + else if (MsgTXAISMod::match(cmd)) + { + // Forward a copy to baseband + MsgTXAISMod* rep = new MsgTXAISMod((MsgTXAISMod&)cmd); + qDebug() << "AISMod::handleMessage: MsgTXAISMod"; + m_basebandSource->getInputMessageQueue()->push(rep); + + return true; + } + else if (DSPSignalNotification::match(cmd)) + { + // Forward to the source + DSPSignalNotification& notif = (DSPSignalNotification&) cmd; + DSPSignalNotification* rep = new DSPSignalNotification(notif); // make a copy + qDebug() << "AISMod::handleMessage: DSPSignalNotification"; + m_basebandSource->getInputMessageQueue()->push(rep); + return true; + } + else + { + return false; + } +} + +void AISMod::setScopeSink(BasebandSampleSink* scopeSink) +{ + m_basebandSource->setScopeSink(scopeSink); +} + +void AISMod::applySettings(const AISModSettings& settings, bool force) +{ + qDebug() << "AISMod::applySettings:" + << " m_inputFrequencyOffset: " << settings.m_inputFrequencyOffset + << " m_rfBandwidth: " << settings.m_rfBandwidth + << " m_fmDeviation: " << settings.m_fmDeviation + << " m_gain: " << settings.m_gain + << " m_channelMute: " << settings.m_channelMute + << " m_repeat: " << settings.m_repeat + << " m_repeatDelay: " << settings.m_repeatDelay + << " m_repeatCount: " << settings.m_repeatCount + << " m_useReverseAPI: " << settings.m_useReverseAPI + << " m_reverseAPIAddress: " << settings.m_reverseAPIAddress + << " m_reverseAPIAddress: " << settings.m_reverseAPIPort + << " m_reverseAPIDeviceIndex: " << settings.m_reverseAPIDeviceIndex + << " m_reverseAPIChannelIndex: " << settings.m_reverseAPIChannelIndex + << " force: " << force; + + QList reverseAPIKeys; + + if ((settings.m_inputFrequencyOffset != m_settings.m_inputFrequencyOffset) || force) { + reverseAPIKeys.append("inputFrequencyOffset"); + } + + if ((settings.m_rfBandwidth != m_settings.m_rfBandwidth) || force) { + reverseAPIKeys.append("rfBandwidth"); + } + + if ((settings.m_fmDeviation != m_settings.m_fmDeviation) || force) { + reverseAPIKeys.append("fmDeviation"); + } + + if ((settings.m_gain != m_settings.m_gain) || force) { + reverseAPIKeys.append("gain"); + } + + if ((settings.m_channelMute != m_settings.m_channelMute) || force) { + reverseAPIKeys.append("channelMute"); + } + + if ((settings.m_repeat != m_settings.m_repeat) || force) { + reverseAPIKeys.append("repeat"); + } + + if ((settings.m_repeatDelay != m_settings.m_repeatDelay) || force) { + reverseAPIKeys.append("repeatDelay"); + } + + if ((settings.m_repeatCount != m_settings.m_repeatCount) || force) { + reverseAPIKeys.append("repeatCount"); + } + + if ((settings.m_rampUpBits != m_settings.m_rampUpBits) || force) { + reverseAPIKeys.append("rampUpBits"); + } + + if ((settings.m_rampDownBits != m_settings.m_rampDownBits) || force) { + reverseAPIKeys.append("rampDownBits"); + } + + if ((settings.m_rampRange != m_settings.m_rampRange) || force) { + reverseAPIKeys.append("rampRange"); + } + + if ((settings.m_rfNoise != m_settings.m_rfNoise) || force) { + reverseAPIKeys.append("rfNoise"); + } + + if ((settings.m_writeToFile != m_settings.m_writeToFile) || force) { + reverseAPIKeys.append("writeToFile"); + } + + if ((settings.m_msgId != m_settings.m_msgId) || force) { + reverseAPIKeys.append("msgId"); + } + + if ((settings.m_mmsi != m_settings.m_mmsi) || force) { + reverseAPIKeys.append("mmsi"); + } + + if ((settings.m_status != m_settings.m_status) || force) { + reverseAPIKeys.append("status"); + } + + if ((settings.m_latitude != m_settings.m_latitude) || force) { + reverseAPIKeys.append("latitude"); + } + + if ((settings.m_longitude != m_settings.m_longitude) || force) { + reverseAPIKeys.append("longitude"); + } + + if ((settings.m_course != m_settings.m_course) || force) { + reverseAPIKeys.append("course"); + } + + if ((settings.m_speed != m_settings.m_speed) || force) { + reverseAPIKeys.append("speed"); + } + + if ((settings.m_heading != m_settings.m_heading) || force) { + reverseAPIKeys.append("heading"); + } + + if ((settings.m_data != m_settings.m_data) || force) { + reverseAPIKeys.append("data"); + } + + if ((settings.m_bt != m_settings.m_bt) || force) { + reverseAPIKeys.append("bt"); + } + + if ((settings.m_symbolSpan != m_settings.m_symbolSpan) || force) { + reverseAPIKeys.append("symbolSpan"); + } + + if ((settings.m_udpEnabled != m_settings.m_udpEnabled) || force) { + reverseAPIKeys.append("udpEnabled"); + } + + if ((settings.m_udpAddress != m_settings.m_udpAddress) || force) { + reverseAPIKeys.append("udpAddress"); + } + + if ((settings.m_udpPort != m_settings.m_udpPort) || force) { + reverseAPIKeys.append("udpPort"); + } + + if ( (settings.m_udpEnabled != m_settings.m_udpEnabled) + || (settings.m_udpAddress != m_settings.m_udpAddress) + || (settings.m_udpPort != m_settings.m_udpPort) + || force) + { + if (settings.m_udpEnabled) + openUDP(settings); + else + closeUDP(); + } + + if (m_settings.m_streamIndex != settings.m_streamIndex) + { + if (m_deviceAPI->getSampleMIMO()) // change of stream is possible for MIMO devices only + { + m_deviceAPI->removeChannelSourceAPI(this); + m_deviceAPI->removeChannelSource(this, m_settings.m_streamIndex); + m_deviceAPI->addChannelSource(this, settings.m_streamIndex); + m_deviceAPI->addChannelSourceAPI(this); + } + + reverseAPIKeys.append("streamIndex"); + } + + AISModBaseband::MsgConfigureAISModBaseband *msg = AISModBaseband::MsgConfigureAISModBaseband::create(settings, force); + m_basebandSource->getInputMessageQueue()->push(msg); + + if (settings.m_useReverseAPI) + { + bool fullUpdate = ((m_settings.m_useReverseAPI != settings.m_useReverseAPI) && settings.m_useReverseAPI) || + (m_settings.m_reverseAPIAddress != settings.m_reverseAPIAddress) || + (m_settings.m_reverseAPIPort != settings.m_reverseAPIPort) || + (m_settings.m_reverseAPIDeviceIndex != settings.m_reverseAPIDeviceIndex) || + (m_settings.m_reverseAPIChannelIndex != settings.m_reverseAPIChannelIndex); + webapiReverseSendSettings(reverseAPIKeys, settings, fullUpdate || force); + } + + QList *messageQueues = MainCore::instance()->getMessagePipes().getMessageQueues(this, "settings"); + + if (messageQueues) { + sendChannelSettings(messageQueues, reverseAPIKeys, settings, force); + } + + m_settings = settings; +} + +QByteArray AISMod::serialize() const +{ + return m_settings.serialize(); +} + +bool AISMod::deserialize(const QByteArray& data) +{ + bool success = true; + + if (!m_settings.deserialize(data)) + { + m_settings.resetToDefaults(); + success = false; + } + + MsgConfigureAISMod *msg = MsgConfigureAISMod::create(m_settings, true); + m_inputMessageQueue.push(msg); + + return success; +} + +int AISMod::webapiSettingsGet( + SWGSDRangel::SWGChannelSettings& response, + QString& errorMessage) +{ + (void) errorMessage; + response.setAisModSettings(new SWGSDRangel::SWGAISModSettings()); + response.getAisModSettings()->init(); + webapiFormatChannelSettings(response, m_settings); + + return 200; +} + +int AISMod::webapiSettingsPutPatch( + bool force, + const QStringList& channelSettingsKeys, + SWGSDRangel::SWGChannelSettings& response, + QString& errorMessage) +{ + (void) errorMessage; + AISModSettings settings = m_settings; + webapiUpdateChannelSettings(settings, channelSettingsKeys, response); + + MsgConfigureAISMod *msg = MsgConfigureAISMod::create(settings, force); + m_inputMessageQueue.push(msg); + + if (m_guiMessageQueue) // forward to GUI if any + { + MsgConfigureAISMod *msgToGUI = MsgConfigureAISMod::create(settings, force); + m_guiMessageQueue->push(msgToGUI); + } + + webapiFormatChannelSettings(response, settings); + + return 200; +} + +void AISMod::webapiUpdateChannelSettings( + AISModSettings& settings, + const QStringList& channelSettingsKeys, + SWGSDRangel::SWGChannelSettings& response) +{ + if (channelSettingsKeys.contains("inputFrequencyOffset")) { + settings.m_inputFrequencyOffset = response.getAisModSettings()->getInputFrequencyOffset(); + } + if (channelSettingsKeys.contains("rfBandwidth")) { + settings.m_rfBandwidth = response.getAisModSettings()->getRfBandwidth(); + } + if (channelSettingsKeys.contains("fmDeviation")) { + settings.m_fmDeviation = response.getAisModSettings()->getFmDeviation(); + } + if (channelSettingsKeys.contains("gain")) { + settings.m_gain = response.getAisModSettings()->getGain(); + } + if (channelSettingsKeys.contains("channelMute")) { + settings.m_channelMute = response.getAisModSettings()->getChannelMute() != 0; + } + if (channelSettingsKeys.contains("repeat")) { + settings.m_repeat = response.getAisModSettings()->getRepeat() != 0; + } + if (channelSettingsKeys.contains("repeatDelay")) { + settings.m_repeatDelay = response.getAisModSettings()->getRepeatDelay(); + } + if (channelSettingsKeys.contains("repeatCount")) { + settings.m_repeatCount = response.getAisModSettings()->getRepeatCount(); + } + if (channelSettingsKeys.contains("rampUpBits")) { + settings.m_rampUpBits = response.getAisModSettings()->getRampUpBits(); + } + if (channelSettingsKeys.contains("rampDownBits")) { + settings.m_rampDownBits = response.getAisModSettings()->getRampDownBits(); + } + if (channelSettingsKeys.contains("rampRange")) { + settings.m_rampRange = response.getAisModSettings()->getRampRange(); + } + if (channelSettingsKeys.contains("rfNoise")) { + settings.m_rfNoise = response.getAisModSettings()->getRfNoise() != 0; + } + if (channelSettingsKeys.contains("writeToFile")) { + settings.m_writeToFile = response.getAisModSettings()->getWriteToFile() != 0; + } + if (channelSettingsKeys.contains("mmsi")) { + settings.m_mmsi = *response.getAisModSettings()->getMmsi(); + } + if (channelSettingsKeys.contains("status")) { + settings.m_status = response.getAisModSettings()->getStatus(); + } + if (channelSettingsKeys.contains("latitude")) { + settings.m_latitude = response.getAisModSettings()->getLatitude(); + } + if (channelSettingsKeys.contains("longitude")) { + settings.m_longitude = response.getAisModSettings()->getLongitude(); + } + if (channelSettingsKeys.contains("course")) { + settings.m_course = response.getAisModSettings()->getCourse(); + } + if (channelSettingsKeys.contains("speed")) { + settings.m_speed = response.getAisModSettings()->getSpeed(); + } + if (channelSettingsKeys.contains("heading")) { + settings.m_heading = response.getAisModSettings()->getHeading(); + } + if (channelSettingsKeys.contains("data")) { + settings.m_data = *response.getAisModSettings()->getData(); + } + if (channelSettingsKeys.contains("bt")) { + settings.m_bt = response.getAisModSettings()->getBt(); + } + if (channelSettingsKeys.contains("symbolSpan")) { + settings.m_symbolSpan = response.getAisModSettings()->getSymbolSpan(); + } + if (channelSettingsKeys.contains("rgbColor")) { + settings.m_rgbColor = response.getAisModSettings()->getRgbColor(); + } + if (channelSettingsKeys.contains("title")) { + settings.m_title = *response.getAisModSettings()->getTitle(); + } + if (channelSettingsKeys.contains("streamIndex")) { + settings.m_streamIndex = response.getAisModSettings()->getStreamIndex(); + } + if (channelSettingsKeys.contains("useReverseAPI")) { + settings.m_useReverseAPI = response.getAisModSettings()->getUseReverseApi() != 0; + } + if (channelSettingsKeys.contains("reverseAPIAddress")) { + settings.m_reverseAPIAddress = *response.getAisModSettings()->getReverseApiAddress(); + } + if (channelSettingsKeys.contains("reverseAPIPort")) { + settings.m_reverseAPIPort = response.getAisModSettings()->getReverseApiPort(); + } + if (channelSettingsKeys.contains("reverseAPIDeviceIndex")) { + settings.m_reverseAPIDeviceIndex = response.getAisModSettings()->getReverseApiDeviceIndex(); + } + if (channelSettingsKeys.contains("reverseAPIChannelIndex")) { + settings.m_reverseAPIChannelIndex = response.getAisModSettings()->getReverseApiChannelIndex(); + } + if (channelSettingsKeys.contains("udpEnabled")) { + settings.m_udpEnabled = response.getAisModSettings()->getUdpEnabled(); + } + if (channelSettingsKeys.contains("udpAddress")) { + settings.m_udpAddress = *response.getAisModSettings()->getUdpAddress(); + } + if (channelSettingsKeys.contains("udpPort")) { + settings.m_udpPort = response.getAisModSettings()->getUdpPort(); + } +} + +int AISMod::webapiReportGet( + SWGSDRangel::SWGChannelReport& response, + QString& errorMessage) +{ + (void) errorMessage; + response.setAisModReport(new SWGSDRangel::SWGAISModReport()); + response.getAisModReport()->init(); + webapiFormatChannelReport(response); + return 200; +} + +int AISMod::webapiActionsPost( + const QStringList& channelActionsKeys, + SWGSDRangel::SWGChannelActions& query, + QString& errorMessage) +{ + SWGSDRangel::SWGAISModActions *swgAISModActions = query.getAisModActions(); + + if (swgAISModActions) + { + if (channelActionsKeys.contains("tx")) + { + SWGSDRangel::SWGAISModActions_tx* tx = swgAISModActions->getTx(); + QString *dataP = tx->getData(); + if (dataP) + { + QString data(*dataP); + + AISMod::MsgTXAISMod *msg = AISMod::MsgTXAISMod::create(data); + m_basebandSource->getInputMessageQueue()->push(msg); + return 202; + } + else + { + errorMessage = "Message must contain data"; + return 400; + } + } + else + { + errorMessage = "Unknown action"; + return 400; + } + } + else + { + errorMessage = "Missing AISModActions in query"; + return 400; + } + return 400; +} + +void AISMod::webapiFormatChannelSettings(SWGSDRangel::SWGChannelSettings& response, const AISModSettings& settings) +{ + response.getAisModSettings()->setInputFrequencyOffset(settings.m_inputFrequencyOffset); + response.getAisModSettings()->setFmDeviation(settings.m_fmDeviation); + response.getAisModSettings()->setRfBandwidth(settings.m_rfBandwidth); + response.getAisModSettings()->setGain(settings.m_gain); + response.getAisModSettings()->setChannelMute(settings.m_channelMute ? 1 : 0); + response.getAisModSettings()->setRepeat(settings.m_repeat ? 1 : 0); + response.getAisModSettings()->setRepeatDelay(settings.m_repeatDelay); + response.getAisModSettings()->setRepeatCount(settings.m_repeatCount); + response.getAisModSettings()->setRampUpBits(settings.m_rampUpBits); + response.getAisModSettings()->setRampDownBits(settings.m_rampDownBits); + response.getAisModSettings()->setRampRange(settings.m_rampRange); + response.getAisModSettings()->setRfNoise(settings.m_rfNoise ? 1 : 0); + response.getAisModSettings()->setWriteToFile(settings.m_writeToFile ? 1 : 0); + + if (response.getAisModSettings()->getMmsi()) { + *response.getAisModSettings()->getMmsi() = settings.m_mmsi; + } else { + response.getAisModSettings()->setMmsi(new QString(settings.m_mmsi)); + } + response.getAisModSettings()->setStatus(settings.m_status); + response.getAisModSettings()->setLatitude(settings.m_latitude); + response.getAisModSettings()->setLongitude(settings.m_longitude); + response.getAisModSettings()->setCourse(settings.m_course); + response.getAisModSettings()->setSpeed(settings.m_speed); + response.getAisModSettings()->setHeading(settings.m_heading); + + if (response.getAisModSettings()->getData()) { + *response.getAisModSettings()->getData() = settings.m_data; + } else { + response.getAisModSettings()->setData(new QString(settings.m_data)); + } + + response.getAisModSettings()->setBt(settings.m_bt); + response.getAisModSettings()->setSymbolSpan(settings.m_symbolSpan); + response.getAisModSettings()->setRgbColor(settings.m_rgbColor); + + if (response.getAisModSettings()->getTitle()) { + *response.getAisModSettings()->getTitle() = settings.m_title; + } else { + response.getAisModSettings()->setTitle(new QString(settings.m_title)); + } + + response.getAisModSettings()->setUseReverseApi(settings.m_useReverseAPI ? 1 : 0); + + if (response.getAisModSettings()->getReverseApiAddress()) { + *response.getAisModSettings()->getReverseApiAddress() = settings.m_reverseAPIAddress; + } else { + response.getAisModSettings()->setReverseApiAddress(new QString(settings.m_reverseAPIAddress)); + } + + response.getAisModSettings()->setReverseApiPort(settings.m_reverseAPIPort); + response.getAisModSettings()->setReverseApiDeviceIndex(settings.m_reverseAPIDeviceIndex); + response.getAisModSettings()->setReverseApiChannelIndex(settings.m_reverseAPIChannelIndex); + + response.getAisModSettings()->setUdpEnabled(settings.m_udpEnabled); + response.getAisModSettings()->setUdpAddress(new QString(settings.m_udpAddress)); + response.getAisModSettings()->setUdpPort(settings.m_udpPort); +} + +void AISMod::webapiFormatChannelReport(SWGSDRangel::SWGChannelReport& response) +{ + response.getAisModReport()->setChannelPowerDb(CalcDb::dbPower(getMagSq())); + response.getAisModReport()->setChannelSampleRate(m_basebandSource->getChannelSampleRate()); +} + +void AISMod::webapiReverseSendSettings(QList& channelSettingsKeys, const AISModSettings& settings, bool force) +{ + SWGSDRangel::SWGChannelSettings *swgChannelSettings = new SWGSDRangel::SWGChannelSettings(); + webapiFormatChannelSettings(channelSettingsKeys, swgChannelSettings, settings, force); + + QString channelSettingsURL = QString("http://%1:%2/sdrangel/deviceset/%3/channel/%4/settings") + .arg(settings.m_reverseAPIAddress) + .arg(settings.m_reverseAPIPort) + .arg(settings.m_reverseAPIDeviceIndex) + .arg(settings.m_reverseAPIChannelIndex); + m_networkRequest.setUrl(QUrl(channelSettingsURL)); + m_networkRequest.setHeader(QNetworkRequest::ContentTypeHeader, "application/json"); + + QBuffer *buffer = new QBuffer(); + buffer->open((QBuffer::ReadWrite)); + buffer->write(swgChannelSettings->asJson().toUtf8()); + buffer->seek(0); + + // Always use PATCH to avoid passing reverse API settings + QNetworkReply *reply = m_networkManager->sendCustomRequest(m_networkRequest, "PATCH", buffer); + buffer->setParent(reply); + + delete swgChannelSettings; +} + +void AISMod::sendChannelSettings( + QList *messageQueues, + QList& channelSettingsKeys, + const AISModSettings& settings, + bool force) +{ + QList::iterator it = messageQueues->begin(); + + for (; it != messageQueues->end(); ++it) + { + SWGSDRangel::SWGChannelSettings *swgChannelSettings = new SWGSDRangel::SWGChannelSettings(); + webapiFormatChannelSettings(channelSettingsKeys, swgChannelSettings, settings, force); + MainCore::MsgChannelSettings *msg = MainCore::MsgChannelSettings::create( + this, + channelSettingsKeys, + swgChannelSettings, + force + ); + (*it)->push(msg); + } +} + +void AISMod::webapiFormatChannelSettings( + QList& channelSettingsKeys, + SWGSDRangel::SWGChannelSettings *swgChannelSettings, + const AISModSettings& settings, + bool force +) +{ + swgChannelSettings->setDirection(1); // single source (Tx) + swgChannelSettings->setOriginatorChannelIndex(getIndexInDeviceSet()); + swgChannelSettings->setOriginatorDeviceSetIndex(getDeviceSetIndex()); + swgChannelSettings->setChannelType(new QString(m_channelId)); + swgChannelSettings->setAisModSettings(new SWGSDRangel::SWGAISModSettings()); + SWGSDRangel::SWGAISModSettings *swgAISModSettings = swgChannelSettings->getAisModSettings(); + + // transfer data that has been modified. When force is on transfer all data except reverse API data + + if (channelSettingsKeys.contains("inputFrequencyOffset") || force) { + swgAISModSettings->setInputFrequencyOffset(settings.m_inputFrequencyOffset); + } + if (channelSettingsKeys.contains("fmDeviation") || force) { + swgAISModSettings->setFmDeviation(settings.m_fmDeviation); + } + if (channelSettingsKeys.contains("rfBandwidth") || force) { + swgAISModSettings->setRfBandwidth(settings.m_rfBandwidth); + } + if (channelSettingsKeys.contains("gain") || force) { + swgAISModSettings->setGain(settings.m_gain); + } + if (channelSettingsKeys.contains("channelMute") || force) { + swgAISModSettings->setChannelMute(settings.m_channelMute ? 1 : 0); + } + if (channelSettingsKeys.contains("repeat") || force) { + swgAISModSettings->setRepeat(settings.m_repeat ? 1 : 0); + } + if (channelSettingsKeys.contains("repeatDelay") || force) { + swgAISModSettings->setRepeatDelay(settings.m_repeatDelay); + } + if (channelSettingsKeys.contains("repeatCount") || force) { + swgAISModSettings->setRepeatCount(settings.m_repeatCount); + } + if (channelSettingsKeys.contains("rampUpBits")) { + swgAISModSettings->setRampUpBits(settings.m_rampUpBits); + } + if (channelSettingsKeys.contains("rampDownBits")) { + swgAISModSettings->setRampDownBits(settings.m_rampDownBits); + } + if (channelSettingsKeys.contains("rampRange")) { + swgAISModSettings->setRampRange(settings.m_rampRange); + } + if (channelSettingsKeys.contains("rfNoise")) { + swgAISModSettings->setRfNoise(settings.m_rfNoise ? 1 : 0); + } + if (channelSettingsKeys.contains("writeToFile")) { + swgAISModSettings->setWriteToFile(settings.m_writeToFile ? 1 : 0); + } + if (channelSettingsKeys.contains("mmsi")) { + swgAISModSettings->setMmsi(new QString(settings.m_mmsi)); + } + if (channelSettingsKeys.contains("status")) { + swgAISModSettings->setStatus(settings.m_status); + } + if (channelSettingsKeys.contains("latitude")) { + swgAISModSettings->setLatitude(settings.m_latitude); + } + if (channelSettingsKeys.contains("longitude")) { + swgAISModSettings->setLongitude(settings.m_longitude); + } + if (channelSettingsKeys.contains("course")) { + swgAISModSettings->setCourse(settings.m_course); + } + if (channelSettingsKeys.contains("speed")) { + swgAISModSettings->setSpeed(settings.m_speed); + } + if (channelSettingsKeys.contains("heading")) { + swgAISModSettings->setHeading(settings.m_heading); + } + if (channelSettingsKeys.contains("data")) { + swgAISModSettings->setData(new QString(settings.m_data)); + } + if (channelSettingsKeys.contains("bt")) { + swgAISModSettings->setBt(settings.m_bt); + } + if (channelSettingsKeys.contains("symbolSpan")) { + swgAISModSettings->setSymbolSpan(settings.m_symbolSpan); + } + if (channelSettingsKeys.contains("rgbColor") || force) { + swgAISModSettings->setRgbColor(settings.m_rgbColor); + } + if (channelSettingsKeys.contains("title") || force) { + swgAISModSettings->setTitle(new QString(settings.m_title)); + } + if (channelSettingsKeys.contains("streamIndex") || force) { + swgAISModSettings->setStreamIndex(settings.m_streamIndex); + } + if (channelSettingsKeys.contains("udpEnabled") || force) { + swgAISModSettings->setUdpEnabled(settings.m_udpEnabled); + } + if (channelSettingsKeys.contains("udpAddress") || force) { + swgAISModSettings->setUdpAddress(new QString(settings.m_udpAddress)); + } + if (channelSettingsKeys.contains("udpPort") || force) { + swgAISModSettings->setUdpPort(settings.m_udpPort); + } +} + +void AISMod::networkManagerFinished(QNetworkReply *reply) +{ + QNetworkReply::NetworkError replyError = reply->error(); + + if (replyError) + { + qWarning() << "AISMod::networkManagerFinished:" + << " error(" << (int) replyError + << "): " << replyError + << ": " << reply->errorString(); + } + else + { + QString answer = reply->readAll(); + answer.chop(1); // remove last \n + qDebug("AISMod::networkManagerFinished: reply:\n%s", answer.toStdString().c_str()); + } + + reply->deleteLater(); +} + +double AISMod::getMagSq() const +{ + return m_basebandSource->getMagSq(); +} + +void AISMod::setLevelMeter(QObject *levelMeter) +{ + connect(m_basebandSource, SIGNAL(levelChanged(qreal, qreal, int)), levelMeter, SLOT(levelChanged(qreal, qreal, int))); +} + +uint32_t AISMod::getNumberOfDeviceStreams() const +{ + return m_deviceAPI->getNbSinkStreams(); +} + +void AISMod::openUDP(const AISModSettings& settings) +{ + closeUDP(); + m_udpSocket = new QUdpSocket(); + if (!m_udpSocket->bind(QHostAddress(settings.m_udpAddress), settings.m_udpPort)) + qCritical() << "AISMod::openUDP: Failed to bind to port " << settings.m_udpAddress << ":" << settings.m_udpPort << ". Error: " << m_udpSocket->error(); + else + qDebug() << "AISMod::openUDP: Listening for messages on " << settings.m_udpAddress << ":" << settings.m_udpPort; + connect(m_udpSocket, &QUdpSocket::readyRead, this, &AISMod::udpRx); +} + +void AISMod::closeUDP() +{ + if (m_udpSocket != nullptr) + { + disconnect(m_udpSocket, &QUdpSocket::readyRead, this, &AISMod::udpRx); + delete m_udpSocket; + m_udpSocket = nullptr; + } +} + +void AISMod::udpRx() +{ + while (m_udpSocket->hasPendingDatagrams()) + { + QNetworkDatagram datagram = m_udpSocket->receiveDatagram(); + MsgTXPacketBytes *msg = MsgTXPacketBytes::create(datagram.data()); + m_basebandSource->getInputMessageQueue()->push(msg); + } +} diff --git a/plugins/channeltx/modais/aismod.h b/plugins/channeltx/modais/aismod.h new file mode 100644 index 000000000..6cc707987 --- /dev/null +++ b/plugins/channeltx/modais/aismod.h @@ -0,0 +1,209 @@ +/////////////////////////////////////////////////////////////////////////////////// +// Copyright (C) 2016-2019 Edouard Griffiths, F4EXB // +// Copyright (C) 2020 Jon Beniston, M7RCE // +// // +// This program is free software; you can redistribute it and/or modify // +// it under the terms of the GNU General Public License as published by // +// the Free Software Foundation as version 3 of the License, or // +// (at your option) any later version. // +// // +// This program is distributed in the hope that it will be useful, // +// but WITHOUT ANY WARRANTY; without even the implied warranty of // +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // +// GNU General Public License V3 for more details. // +// // +// You should have received a copy of the GNU General Public License // +// along with this program. If not, see . // +/////////////////////////////////////////////////////////////////////////////////// + +#ifndef PLUGINS_CHANNELTX_MODAIS_AISMOD_H_ +#define PLUGINS_CHANNELTX_MODAIS_AISMOD_H_ + +#include + +#include +#include + +#include "dsp/basebandsamplesource.h" +#include "dsp/spectrumvis.h" +#include "channel/channelapi.h" +#include "util/message.h" + +#include "aismodsettings.h" + +class QNetworkAccessManager; +class QNetworkReply; +class QThread; +class QUdpSocket; +class DeviceAPI; +class AISModBaseband; + +class AISMod : public BasebandSampleSource, public ChannelAPI { + Q_OBJECT + +public: + class MsgConfigureAISMod : public Message { + MESSAGE_CLASS_DECLARATION + + public: + const AISModSettings& getSettings() const { return m_settings; } + bool getForce() const { return m_force; } + + static MsgConfigureAISMod* create(const AISModSettings& settings, bool force) + { + return new MsgConfigureAISMod(settings, force); + } + + private: + AISModSettings m_settings; + bool m_force; + + MsgConfigureAISMod(const AISModSettings& settings, bool force) : + Message(), + m_settings(settings), + m_force(force) + { } + }; + + class MsgTXAISMod : public Message { + MESSAGE_CLASS_DECLARATION + + public: + static MsgTXAISMod* create(QString data) + { + return new MsgTXAISMod(data); + } + + QString m_data; + + private: + + MsgTXAISMod(QString data) : + Message(), + m_data(data) + { } + }; + + class MsgTXPacketBytes : public Message { + MESSAGE_CLASS_DECLARATION + + public: + static MsgTXPacketBytes* create(QByteArray data) + { + return new MsgTXPacketBytes(data); + } + + QByteArray m_data; + + private: + + MsgTXPacketBytes(QByteArray data) : + Message(), + m_data(data) + { } + }; + + //================================================================= + + AISMod(DeviceAPI *deviceAPI); + virtual ~AISMod(); + virtual void destroy() { delete this; } + + virtual void start(); + virtual void stop(); + virtual void pull(SampleVector::iterator& begin, unsigned int nbSamples); + virtual bool handleMessage(const Message& cmd); + + virtual void getIdentifier(QString& id) { id = objectName(); } + virtual void getTitle(QString& title) { title = m_settings.m_title; } + virtual qint64 getCenterFrequency() const { return m_settings.m_inputFrequencyOffset; } + + virtual QByteArray serialize() const; + virtual bool deserialize(const QByteArray& data); + + virtual int getNbSinkStreams() const { return 1; } + virtual int getNbSourceStreams() const { return 0; } + + virtual qint64 getStreamCenterFrequency(int streamIndex, bool sinkElseSource) const + { + (void) streamIndex; + (void) sinkElseSource; + return m_settings.m_inputFrequencyOffset; + } + + virtual int webapiSettingsGet( + SWGSDRangel::SWGChannelSettings& response, + QString& errorMessage); + + virtual int webapiSettingsPutPatch( + bool force, + const QStringList& channelSettingsKeys, + SWGSDRangel::SWGChannelSettings& response, + QString& errorMessage); + + virtual int webapiReportGet( + SWGSDRangel::SWGChannelReport& response, + QString& errorMessage); + + virtual int webapiActionsPost( + const QStringList& channelActionsKeys, + SWGSDRangel::SWGChannelActions& query, + QString& errorMessage); + + static void webapiFormatChannelSettings( + SWGSDRangel::SWGChannelSettings& response, + const AISModSettings& settings); + + static void webapiUpdateChannelSettings( + AISModSettings& settings, + const QStringList& channelSettingsKeys, + SWGSDRangel::SWGChannelSettings& response); + + SpectrumVis *getSpectrumVis() { return &m_spectrumVis; } + void setScopeSink(BasebandSampleSink* scopeSink); + double getMagSq() const; + void setLevelMeter(QObject *levelMeter); + uint32_t getNumberOfDeviceStreams() const; + + static const char* const m_channelIdURI; + static const char* const m_channelId; + +private: + + DeviceAPI* m_deviceAPI; + QThread *m_thread; + AISModBaseband* m_basebandSource; + AISModSettings m_settings; + SpectrumVis m_spectrumVis; + + QMutex m_settingsMutex; + + QNetworkAccessManager *m_networkManager; + QNetworkRequest m_networkRequest; + QUdpSocket *m_udpSocket; + + void applySettings(const AISModSettings& settings, bool force = false); + void webapiFormatChannelReport(SWGSDRangel::SWGChannelReport& response); + void webapiReverseSendSettings(QList& channelSettingsKeys, const AISModSettings& settings, bool force); + void sendChannelSettings( + QList *messageQueues, + QList& channelSettingsKeys, + const AISModSettings& settings, + bool force + ); + void webapiFormatChannelSettings( + QList& channelSettingsKeys, + SWGSDRangel::SWGChannelSettings *swgChannelSettings, + const AISModSettings& settings, + bool force + ); + void openUDP(const AISModSettings& settings); + void closeUDP(); + +private slots: + void networkManagerFinished(QNetworkReply *reply); + void udpRx(); +}; + + +#endif /* PLUGINS_CHANNELTX_MODAIS_AISMOD_H_ */ diff --git a/plugins/channeltx/modais/aismodbaseband.cpp b/plugins/channeltx/modais/aismodbaseband.cpp new file mode 100644 index 000000000..7cf4bb0d6 --- /dev/null +++ b/plugins/channeltx/modais/aismodbaseband.cpp @@ -0,0 +1,201 @@ +/////////////////////////////////////////////////////////////////////////////////// +// Copyright (C) 2019 Edouard Griffiths, F4EXB // +// Copyright (C) 2020 Jon Beniston, M7RCE // +// // +// This program is free software; you can redistribute it and/or modify // +// it under the terms of the GNU General Public License as published by // +// the Free Software Foundation as version 3 of the License, or // +// (at your option) any later version. // +// // +// This program is distributed in the hope that it will be useful, // +// but WITHOUT ANY WARRANTY; without even the implied warranty of // +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // +// GNU General Public License V3 for more details. // +// // +// You should have received a copy of the GNU General Public License // +// along with this program. If not, see . // +/////////////////////////////////////////////////////////////////////////////////// + +#include + +#include "dsp/upchannelizer.h" +#include "dsp/dspengine.h" +#include "dsp/dspcommands.h" + +#include "aismodbaseband.h" +#include "aismod.h" + +MESSAGE_CLASS_DEFINITION(AISModBaseband::MsgConfigureAISModBaseband, Message) + +AISModBaseband::AISModBaseband() : + m_mutex(QMutex::Recursive) +{ + m_sampleFifo.resize(SampleSourceFifo::getSizePolicy(48000)); + m_channelizer = new UpChannelizer(&m_source); + + qDebug("AISModBaseband::AISModBaseband"); + QObject::connect( + &m_sampleFifo, + &SampleSourceFifo::dataRead, + this, + &AISModBaseband::handleData, + Qt::QueuedConnection + ); + + connect(&m_inputMessageQueue, SIGNAL(messageEnqueued()), this, SLOT(handleInputMessages())); +} + +AISModBaseband::~AISModBaseband() +{ + delete m_channelizer; +} + +void AISModBaseband::reset() +{ + QMutexLocker mutexLocker(&m_mutex); + m_sampleFifo.reset(); +} + +void AISModBaseband::setChannel(ChannelAPI *channel) +{ + m_source.setChannel(channel); +} + +void AISModBaseband::pull(const SampleVector::iterator& begin, unsigned int nbSamples) +{ + unsigned int part1Begin, part1End, part2Begin, part2End; + m_sampleFifo.read(nbSamples, part1Begin, part1End, part2Begin, part2End); + SampleVector& data = m_sampleFifo.getData(); + + if (part1Begin != part1End) + { + std::copy( + data.begin() + part1Begin, + data.begin() + part1End, + begin + ); + } + + unsigned int shift = part1End - part1Begin; + + if (part2Begin != part2End) + { + std::copy( + data.begin() + part2Begin, + data.begin() + part2End, + begin + shift + ); + } +} + +void AISModBaseband::handleData() +{ + QMutexLocker mutexLocker(&m_mutex); + SampleVector& data = m_sampleFifo.getData(); + unsigned int ipart1begin; + unsigned int ipart1end; + unsigned int ipart2begin; + unsigned int ipart2end; + qreal rmsLevel, peakLevel; + int numSamples; + + unsigned int remainder = m_sampleFifo.remainder(); + + while ((remainder > 0) && (m_inputMessageQueue.size() == 0)) + { + m_sampleFifo.write(remainder, ipart1begin, ipart1end, ipart2begin, ipart2end); + + if (ipart1begin != ipart1end) { // first part of FIFO data + processFifo(data, ipart1begin, ipart1end); + } + + if (ipart2begin != ipart2end) { // second part of FIFO data (used when block wraps around) + processFifo(data, ipart2begin, ipart2end); + } + + remainder = m_sampleFifo.remainder(); + } + + m_source.getLevels(rmsLevel, peakLevel, numSamples); + emit levelChanged(rmsLevel, peakLevel, numSamples); +} + +void AISModBaseband::processFifo(SampleVector& data, unsigned int iBegin, unsigned int iEnd) +{ + m_channelizer->prefetch(iEnd - iBegin); + m_channelizer->pull(data.begin() + iBegin, iEnd - iBegin); +} + +void AISModBaseband::handleInputMessages() +{ + Message* message; + + while ((message = m_inputMessageQueue.pop()) != nullptr) + { + if (handleMessage(*message)) { + delete message; + } + } +} + +bool AISModBaseband::handleMessage(const Message& cmd) +{ + if (MsgConfigureAISModBaseband::match(cmd)) + { + QMutexLocker mutexLocker(&m_mutex); + MsgConfigureAISModBaseband& cfg = (MsgConfigureAISModBaseband&) cmd; + qDebug() << "AISModBaseband::handleMessage: MsgConfigureAISModBaseband"; + + applySettings(cfg.getSettings(), cfg.getForce()); + + return true; + } + else if (AISMod::MsgTXAISMod::match(cmd)) + { + AISMod::MsgTXAISMod& tx = (AISMod::MsgTXAISMod&) cmd; + m_source.addTXPacket(tx.m_data); + + return true; + } + else if (AISMod::MsgTXPacketBytes::match(cmd)) + { + AISMod::MsgTXPacketBytes& tx = (AISMod::MsgTXPacketBytes&) cmd; + m_source.addTXPacket(tx.m_data); + + return true; + } + else if (DSPSignalNotification::match(cmd)) + { + QMutexLocker mutexLocker(&m_mutex); + DSPSignalNotification& notif = (DSPSignalNotification&) cmd; + qDebug() << "AISModBaseband::handleMessage: DSPSignalNotification: basebandSampleRate: " << notif.getSampleRate(); + m_sampleFifo.resize(SampleSourceFifo::getSizePolicy(notif.getSampleRate())); + m_channelizer->setBasebandSampleRate(notif.getSampleRate()); + m_source.applyChannelSettings(m_channelizer->getChannelSampleRate(), m_channelizer->getChannelFrequencyOffset()); + + return true; + } + else + { + qDebug() << "AISModBaseband - Baseband got unknown message"; + return false; + } +} + +void AISModBaseband::applySettings(const AISModSettings& settings, bool force) +{ + if ((settings.m_inputFrequencyOffset != m_settings.m_inputFrequencyOffset) || force) + { + m_channelizer->setChannelization(m_channelizer->getChannelSampleRate(), settings.m_inputFrequencyOffset); + m_source.applyChannelSettings(m_channelizer->getChannelSampleRate(), m_channelizer->getChannelFrequencyOffset()); + } + + m_source.applySettings(settings, force); + + m_settings = settings; +} + +int AISModBaseband::getChannelSampleRate() const +{ + return m_channelizer->getChannelSampleRate(); +} diff --git a/plugins/channeltx/modais/aismodbaseband.h b/plugins/channeltx/modais/aismodbaseband.h new file mode 100644 index 000000000..f7cb20179 --- /dev/null +++ b/plugins/channeltx/modais/aismodbaseband.h @@ -0,0 +1,99 @@ +/////////////////////////////////////////////////////////////////////////////////// +// Copyright (C) 2019 Edouard Griffiths, F4EXB // +// Copyright (C) 2020 Jon Beniston, M7RCE // +// // +// This program is free software; you can redistribute it and/or modify // +// it under the terms of the GNU General Public License as published by // +// the Free Software Foundation as version 3 of the License, or // +// (at your option) any later version. // +// // +// This program is distributed in the hope that it will be useful, // +// but WITHOUT ANY WARRANTY; without even the implied warranty of // +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // +// GNU General Public License V3 for more details. // +// // +// You should have received a copy of the GNU General Public License // +// along with this program. If not, see . // +/////////////////////////////////////////////////////////////////////////////////// + +#ifndef INCLUDE_AISMODBASEBAND_H +#define INCLUDE_AISMODBASEBAND_H + +#include +#include + +#include "dsp/samplesourcefifo.h" +#include "util/message.h" +#include "util/messagequeue.h" + +#include "aismodsource.h" + +class UpChannelizer; +class ChannelAPI; + +class AISModBaseband : public QObject +{ + Q_OBJECT +public: + class MsgConfigureAISModBaseband : public Message { + MESSAGE_CLASS_DECLARATION + + public: + const AISModSettings& getSettings() const { return m_settings; } + bool getForce() const { return m_force; } + + static MsgConfigureAISModBaseband* create(const AISModSettings& settings, bool force) + { + return new MsgConfigureAISModBaseband(settings, force); + } + + private: + AISModSettings m_settings; + bool m_force; + + MsgConfigureAISModBaseband(const AISModSettings& settings, bool force) : + Message(), + m_settings(settings), + m_force(force) + { } + }; + + AISModBaseband(); + ~AISModBaseband(); + void reset(); + void pull(const SampleVector::iterator& begin, unsigned int nbSamples); + MessageQueue *getInputMessageQueue() { return &m_inputMessageQueue; } //!< Get the queue for asynchronous inbound communication + double getMagSq() const { return m_source.getMagSq(); } + int getChannelSampleRate() const; + void setSpectrumSampleSink(BasebandSampleSink* sampleSink) { m_source.setSpectrumSink(sampleSink); } + void setScopeSink(BasebandSampleSink* scopeSink) { m_source.setScopeSink(scopeSink); } + void setChannel(ChannelAPI *channel); + +signals: + /** + * Level changed + * \param rmsLevel RMS level in range 0.0 - 1.0 + * \param peakLevel Peak level in range 0.0 - 1.0 + * \param numSamples Number of audio samples analyzed + */ + void levelChanged(qreal rmsLevel, qreal peakLevel, int numSamples); + +private: + SampleSourceFifo m_sampleFifo; + UpChannelizer *m_channelizer; + AISModSource m_source; + MessageQueue m_inputMessageQueue; //!< Queue for asynchronous inbound communication + AISModSettings m_settings; + QMutex m_mutex; + + void processFifo(SampleVector& data, unsigned int iBegin, unsigned int iEnd); + bool handleMessage(const Message& cmd); + void applySettings(const AISModSettings& settings, bool force = false); + +private slots: + void handleInputMessages(); + void handleData(); //!< Handle data when samples have to be processed +}; + + +#endif // INCLUDE_AISMODBASEBAND_H diff --git a/plugins/channeltx/modais/aismodgui.cpp b/plugins/channeltx/modais/aismodgui.cpp new file mode 100644 index 000000000..d408ee2c0 --- /dev/null +++ b/plugins/channeltx/modais/aismodgui.cpp @@ -0,0 +1,686 @@ +/////////////////////////////////////////////////////////////////////////////////// +// Copyright (C) 2016 Edouard Griffiths, F4EXB // +// Copyright (C) 2020 Jon Beniston, M7RCE // +// // +// This program is free software; you can redistribute it and/or modify // +// it under the terms of the GNU General Public License as published by // +// the Free Software Foundation as version 3 of the License, or // +// (at your option) any later version. // +// // +// This program is distributed in the hope that it will be useful, // +// but WITHOUT ANY WARRANTY; without even the implied warranty of // +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // +// GNU General Public License V3 for more details. // +// // +// You should have received a copy of the GNU General Public License // +// along with this program. If not, see . // +/////////////////////////////////////////////////////////////////////////////////// + +#include +#include +#include +#include +#include + +#include "dsp/spectrumvis.h" +#include "dsp/scopevis.h" +#include "device/deviceuiset.h" +#include "plugin/pluginapi.h" +#include "util/simpleserializer.h" +#include "util/db.h" +#include "dsp/dspengine.h" +#include "gui/glspectrum.h" +#include "gui/crightclickenabler.h" +#include "gui/basicchannelsettingsdialog.h" +#include "gui/devicestreamselectiondialog.h" +#include "maincore.h" + +#include "ui_aismodgui.h" +#include "aismodgui.h" +#include "aismodrepeatdialog.h" +#include "aismodtxsettingsdialog.h" +#include "aismodsource.h" + +AISModGUI* AISModGUI::create(PluginAPI* pluginAPI, DeviceUISet *deviceUISet, BasebandSampleSource *channelTx) +{ + AISModGUI* gui = new AISModGUI(pluginAPI, deviceUISet, channelTx); + return gui; +} + +void AISModGUI::destroy() +{ + delete this; +} + +void AISModGUI::resetToDefaults() +{ + m_settings.resetToDefaults(); + displaySettings(); + applySettings(true); +} + +QByteArray AISModGUI::serialize() const +{ + return m_settings.serialize(); +} + +bool AISModGUI::deserialize(const QByteArray& data) +{ + if(m_settings.deserialize(data)) { + displaySettings(); + applySettings(true); + return true; + } else { + resetToDefaults(); + return false; + } +} + +bool AISModGUI::handleMessage(const Message& message) +{ + if (AISMod::MsgConfigureAISMod::match(message)) + { + const AISMod::MsgConfigureAISMod& cfg = (AISMod::MsgConfigureAISMod&) message; + m_settings = cfg.getSettings(); + blockApplySettings(true); + displaySettings(); + blockApplySettings(false); + return true; + } + else + { + return false; + } +} + +void AISModGUI::channelMarkerChangedByCursor() +{ + ui->deltaFrequency->setValue(m_channelMarker.getCenterFrequency()); + m_settings.m_inputFrequencyOffset = m_channelMarker.getCenterFrequency(); + applySettings(); +} + +void AISModGUI::handleSourceMessages() +{ + Message* message; + + while ((message = getInputMessageQueue()->pop()) != 0) + { + if (handleMessage(*message)) + { + delete message; + } + } +} + +void AISModGUI::on_deltaFrequency_changed(qint64 value) +{ + m_channelMarker.setCenterFrequency(value); + m_settings.m_inputFrequencyOffset = m_channelMarker.getCenterFrequency(); + applySettings(); +} + +void AISModGUI::on_mode_currentIndexChanged(int value) +{ + QString mode = ui->mode->currentText(); + + // If m_doApplySettings is set, we are here from a call to displaySettings, + // so we only want to display the current settings, not update them + // as though a user had selected a new mode + if (m_doApplySettings) + m_settings.setMode(mode); + + ui->rfBWText->setText(QString("%1k").arg(m_settings.m_rfBandwidth / 1000.0, 0, 'f', 1)); + ui->rfBW->setValue(m_settings.m_rfBandwidth / 100.0); + ui->fmDevText->setText(QString("%1k").arg(m_settings.m_fmDeviation / 1000.0, 0, 'f', 1)); + ui->fmDev->setValue(m_settings.m_fmDeviation / 100.0); + ui->btText->setText(QString("%1").arg(m_settings.m_bt, 0, 'f', 1)); + ui->bt->setValue(m_settings.m_bt * 10); + applySettings(); + + // Remove custom mode when deselected, as we no longer know how to set it + if (value < 2) + ui->mode->removeItem(2); +} + +void AISModGUI::on_rfBW_valueChanged(int value) +{ + float bw = value * 100.0f; + ui->rfBWText->setText(QString("%1k").arg(value / 10.0, 0, 'f', 1)); + m_channelMarker.setBandwidth(bw); + m_settings.m_rfBandwidth = bw; + applySettings(); +} + +void AISModGUI::on_fmDev_valueChanged(int value) +{ + ui->fmDevText->setText(QString("%1k").arg(value / 10.0, 0, 'f', 1)); + m_settings.m_fmDeviation = value * 100.0; + applySettings(); +} + +void AISModGUI::on_bt_valueChanged(int value) +{ + ui->btText->setText(QString("%1").arg(value / 10.0, 0, 'f', 1)); + m_settings.m_bt = value / 10.0; + applySettings(); +} + +void AISModGUI::on_gain_valueChanged(int value) +{ + ui->gainText->setText(QString("%1dB").arg(value)); + m_settings.m_gain = value; + applySettings(); +} + +void AISModGUI::on_channelMute_toggled(bool checked) +{ + m_settings.m_channelMute = checked; + applySettings(); +} + +void AISModGUI::on_insertPosition_clicked() +{ + float latitude = MainCore::instance()->getSettings().getLatitude(); + float longitude = MainCore::instance()->getSettings().getLongitude(); + + ui->latitude->setValue(latitude); + ui->longitude->setValue(longitude); +} + +void AISModGUI::on_txButton_clicked() +{ + transmit(); +} + +void AISModGUI::on_message_returnPressed() +{ + transmit(); +} + +void AISModGUI::on_msgId_currentIndexChanged(int index) +{ + m_settings.m_msgId = index + 1; + applySettings(); +} + +void AISModGUI::on_mmsi_editingFinished() +{ + m_settings.m_mmsi = ui->mmsi->text(); + applySettings(); +} + +void AISModGUI::on_status_currentIndexChanged(int index) +{ + m_settings.m_status = index; + applySettings(); +} + +void AISModGUI::on_latitude_valueChanged(double value) +{ + m_settings.m_latitude = (float)value; + applySettings(); +} + +void AISModGUI::on_longitude_valueChanged(double value) +{ + m_settings.m_longitude = (float)value; + applySettings(); +} + +void AISModGUI::on_course_valueChanged(double value) +{ + m_settings.m_course = (float)value; + applySettings(); +} + +void AISModGUI::on_speed_valueChanged(double value) +{ + m_settings.m_speed = (float)value; + applySettings(); +} + +void AISModGUI::on_heading_valueChanged(int value) +{ + m_settings.m_heading = value; + applySettings(); +} + +void AISModGUI::on_message_editingFinished() +{ + m_settings.m_data = ui->message->text(); + applySettings(); +} + +// Convert decimal degrees to 1/10000 minutes +static int degToMinFracs(float decimal) +{ + return std::round(decimal * 60.0f * 10000.0f); +} + +// Encode the message specified by the GUI controls in to a hex string and put in message field +void AISModGUI::on_encode_clicked() +{ + unsigned char bytes[168/8]; + int mmsi; + int latitude; + int longitude; + + mmsi = m_settings.m_mmsi.toInt(); + + latitude = degToMinFracs(m_settings.m_latitude); + longitude = degToMinFracs(m_settings.m_longitude); + + if (m_settings.m_msgId == 4) + { + // Base station report + QDateTime currentDateTime = QDateTime::currentDateTimeUtc(); + QDate currentDate = currentDateTime.date(); + QTime currentTime = currentDateTime.time(); + + int year = currentDate.year(); + int month = currentDate.month(); + int day = currentDate.day(); + int hour = currentTime.hour(); + int minute = currentTime.minute(); + int second = currentTime.second(); + + bytes[0] = (m_settings.m_msgId << 2); // Repeat indicator = 0 + bytes[1] = (mmsi >> 22) & 0xff; + bytes[2] = (mmsi >> 14) & 0xff; + bytes[3] = (mmsi >> 6) & 0xff; + bytes[4] = ((mmsi & 0x3f) << 2) | ((year >> 12) & 0x3); + bytes[5] = (year >> 4) & 0xff; + bytes[6] = ((year & 0xf) << 4) | month; + bytes[7] = (day << 3) | ((hour >> 2) & 0x7); + bytes[8] = ((hour & 0x3) << 6) | minute; + bytes[9] = (second << 2) | (0 << 1) | ((longitude >> 27) & 1); + bytes[10] = (longitude >> 19) & 0xff; + bytes[11] = (longitude >> 11) & 0xff; + bytes[12] = (longitude >> 3) & 0xff; + bytes[13] = ((longitude & 0x7) << 5) | ((latitude >> 22) & 0x1f); + bytes[14] = (latitude >> 14) & 0xff; + bytes[15] = (latitude >> 6) & 0xff; + bytes[16] = ((latitude & 0x3f) << 2); + bytes[17] = 0; + bytes[18] = 0; + bytes[19] = 0; + bytes[20] = 0; + } + else + { + // Position report + int status; + int rateOfTurn = 0x80; // Not available as not currently in GUI + int speedOverGround; + int courseOverGround; + int timestamp; + + timestamp = QDateTime::currentDateTimeUtc().time().second(); + + if (m_settings.m_speed >= 102.2) + speedOverGround = 1022; + else + speedOverGround = std::round(m_settings.m_speed * 10.0); + + courseOverGround = std::floor(m_settings.m_course * 10.0); + + if (m_settings.m_status == 9) // Not defined (last in combo box) + status = 15; + else + status = m_settings.m_status; + + bytes[0] = (m_settings.m_msgId << 2); // Repeat indicator = 0 + + bytes[1] = (mmsi >> 22) & 0xff; + bytes[2] = (mmsi >> 14) & 0xff; + bytes[3] = (mmsi >> 6) & 0xff; + bytes[4] = ((mmsi & 0x3f) << 2) | (status >> 2); + + bytes[5] = ((status & 0x3) << 6) | ((rateOfTurn >> 2) & 0x3f); + bytes[6] = ((rateOfTurn & 0x3) << 6) | ((speedOverGround >> 4) & 0x3f); + bytes[7] = ((speedOverGround & 0xf) << 4) | (0 << 3) | ((longitude >> 25) & 0x7); // Position accuracy = 0 + bytes[8] = (longitude >> 17) & 0xff; + bytes[9] = (longitude >> 9) & 0xff; + bytes[10] = (longitude >> 1) & 0xff; + bytes[11] = ((longitude & 0x1) << 7) | ((latitude >> 20) & 0x7f); + bytes[12] = (latitude >> 12) & 0xff; + bytes[13] = (latitude >> 4) & 0xff; + bytes[14] = ((latitude & 0xf) << 4) | ((courseOverGround >> 8) & 0xf); + bytes[15] = courseOverGround & 0xff; + bytes[16] = ((m_settings.m_heading >> 1) & 0xff); + bytes[17] = ((m_settings.m_heading & 0x1) << 7) | ((timestamp & 0x3f) << 1); + bytes[18] = 0; + bytes[19] = 0; + bytes[20] = 0; + } + + QByteArray ba((const char *)bytes, sizeof(bytes)); + ui->message->setText(ba.toHex()); + + m_settings.m_data = ui->message->text(); + applySettings(); +} + +void AISModGUI::on_repeat_toggled(bool checked) +{ + m_settings.m_repeat = checked; + applySettings(); +} + +void AISModGUI::repeatSelect() +{ + AISModRepeatDialog dialog(m_settings.m_repeatDelay, m_settings.m_repeatCount); + if (dialog.exec() == QDialog::Accepted) + { + m_settings.m_repeatDelay = dialog.m_repeatDelay; + m_settings.m_repeatCount = dialog.m_repeatCount; + applySettings(); + } +} + +void AISModGUI::txSettingsSelect() +{ + AISModTXSettingsDialog dialog(m_settings.m_rampUpBits, m_settings.m_rampDownBits, + m_settings.m_rampRange, + m_settings.m_baud, + m_settings.m_symbolSpan, + m_settings.m_rfNoise, + m_settings.m_writeToFile); + if (dialog.exec() == QDialog::Accepted) + { + m_settings.m_rampUpBits = dialog.m_rampUpBits; + m_settings.m_rampDownBits = dialog.m_rampDownBits; + m_settings.m_rampRange = dialog.m_rampRange; + m_settings.m_baud = dialog.m_baud; + m_settings.m_symbolSpan = dialog.m_symbolSpan; + m_settings.m_rfNoise = dialog.m_rfNoise; + m_settings.m_writeToFile = dialog.m_writeToFile; + displaySettings(); + applySettings(); + } +} + +void AISModGUI::on_udpEnabled_clicked(bool checked) +{ + m_settings.m_udpEnabled = checked; + applySettings(); +} + +void AISModGUI::on_udpAddress_editingFinished() +{ + m_settings.m_udpAddress = ui->udpAddress->text(); + applySettings(); +} + +void AISModGUI::on_udpPort_editingFinished() +{ + m_settings.m_udpPort = ui->udpPort->text().toInt(); + applySettings(); +} + +void AISModGUI::onWidgetRolled(QWidget* widget, bool rollDown) +{ + (void) widget; + (void) rollDown; +} + +void AISModGUI::onMenuDialogCalled(const QPoint &p) +{ + if (m_contextMenuType == ContextMenuChannelSettings) + { + BasicChannelSettingsDialog dialog(&m_channelMarker, this); + dialog.setUseReverseAPI(m_settings.m_useReverseAPI); + dialog.setReverseAPIAddress(m_settings.m_reverseAPIAddress); + dialog.setReverseAPIPort(m_settings.m_reverseAPIPort); + dialog.setReverseAPIDeviceIndex(m_settings.m_reverseAPIDeviceIndex); + dialog.setReverseAPIChannelIndex(m_settings.m_reverseAPIChannelIndex); + dialog.move(p); + dialog.exec(); + + m_settings.m_inputFrequencyOffset = m_channelMarker.getCenterFrequency(); + m_settings.m_rgbColor = m_channelMarker.getColor().rgb(); + m_settings.m_title = m_channelMarker.getTitle(); + m_settings.m_useReverseAPI = dialog.useReverseAPI(); + m_settings.m_reverseAPIAddress = dialog.getReverseAPIAddress(); + m_settings.m_reverseAPIPort = dialog.getReverseAPIPort(); + m_settings.m_reverseAPIDeviceIndex = dialog.getReverseAPIDeviceIndex(); + m_settings.m_reverseAPIChannelIndex = dialog.getReverseAPIChannelIndex(); + + setWindowTitle(m_settings.m_title); + setTitleColor(m_settings.m_rgbColor); + + applySettings(); + } + else if ((m_contextMenuType == ContextMenuStreamSettings) && (m_deviceUISet->m_deviceMIMOEngine)) + { + DeviceStreamSelectionDialog dialog(this); + dialog.setNumberOfStreams(m_aisMod->getNumberOfDeviceStreams()); + dialog.setStreamIndex(m_settings.m_streamIndex); + dialog.move(p); + dialog.exec(); + + m_settings.m_streamIndex = dialog.getSelectedStreamIndex(); + m_channelMarker.clearStreamIndexes(); + m_channelMarker.addStreamIndex(m_settings.m_streamIndex); + displayStreamIndex(); + applySettings(); + } + + resetContextMenuType(); +} + +AISModGUI::AISModGUI(PluginAPI* pluginAPI, DeviceUISet *deviceUISet, BasebandSampleSource *channelTx, QWidget* parent) : + ChannelGUI(parent), + ui(new Ui::AISModGUI), + m_pluginAPI(pluginAPI), + m_deviceUISet(deviceUISet), + m_channelMarker(this), + m_doApplySettings(true) +{ + ui->setupUi(this); + setAttribute(Qt::WA_DeleteOnClose, true); + + connect(this, SIGNAL(widgetRolled(QWidget*,bool)), this, SLOT(onWidgetRolled(QWidget*,bool))); + connect(this, SIGNAL(customContextMenuRequested(const QPoint &)), this, SLOT(onMenuDialogCalled(const QPoint &))); + + m_aisMod = (AISMod*) channelTx; + m_aisMod->setMessageQueueToGUI(getInputMessageQueue()); + + connect(&MainCore::instance()->getMasterTimer(), SIGNAL(timeout()), this, SLOT(tick())); + + m_scopeVis = new ScopeVis(ui->glScope); + m_aisMod->setScopeSink(m_scopeVis); + ui->glScope->connectTimer(MainCore::instance()->getMasterTimer()); + ui->scopeGUI->setBuddies(m_scopeVis->getInputMessageQueue(), m_scopeVis, ui->glScope); + + // Scope settings to display the IQ waveforms + ui->scopeGUI->setPreTrigger(1); + ScopeVis::TraceData traceDataI, traceDataQ; + traceDataI.m_projectionType = Projector::ProjectionReal; + traceDataI.m_amp = 1.0; // for -1 to +1 + traceDataI.m_ampIndex = 0; + traceDataI.m_ofs = 0.0; // vertical offset + traceDataI.m_ofsCoarse = 0; + traceDataQ.m_projectionType = Projector::ProjectionImag; + traceDataQ.m_amp = 1.0; + traceDataQ.m_ampIndex = 0; + traceDataQ.m_ofs = 0.0; + traceDataQ.m_ofsCoarse = 0; + ui->scopeGUI->changeTrace(0, traceDataI); + ui->scopeGUI->addTrace(traceDataQ); + ui->scopeGUI->setDisplayMode(GLScopeGUI::DisplayPol); + ui->scopeGUI->focusOnTrace(0); // re-focus to take changes into account in the GUI + + ScopeVis::TriggerData triggerData; + triggerData.m_triggerLevel = 0.1; + triggerData.m_triggerLevelCoarse = 10; + triggerData.m_triggerPositiveEdge = true; + ui->scopeGUI->changeTrigger(0, triggerData); + ui->scopeGUI->focusOnTrigger(0); // re-focus to take changes into account in the GUI + + m_scopeVis->setLiveRate(AISMOD_SAMPLE_RATE); + //m_scopeVis->setFreeRun(false); // FIXME: add method rather than call m_scopeVis->configure() + + m_spectrumVis = m_aisMod->getSpectrumVis(); + m_spectrumVis->setGLSpectrum(ui->glSpectrum); + + // Extra /2 here because SSB? + ui->glSpectrum->setCenterFrequency(0); + ui->glSpectrum->setSampleRate(AISMOD_SAMPLE_RATE); + ui->glSpectrum->setSsbSpectrum(true); + ui->glSpectrum->setDisplayCurrent(true); + ui->glSpectrum->setLsbDisplay(false); + ui->glSpectrum->setDisplayWaterfall(false); + ui->glSpectrum->setDisplayMaxHold(false); + ui->glSpectrum->setDisplayHistogram(false); + + CRightClickEnabler *repeatRightClickEnabler = new CRightClickEnabler(ui->repeat); + connect(repeatRightClickEnabler, SIGNAL(rightClick(const QPoint &)), this, SLOT(repeatSelect())); + + CRightClickEnabler *txRightClickEnabler = new CRightClickEnabler(ui->txButton); + connect(txRightClickEnabler, SIGNAL(rightClick(const QPoint &)), this, SLOT(txSettingsSelect())); + + ui->deltaFrequencyLabel->setText(QString("%1f").arg(QChar(0x94, 0x03))); + ui->deltaFrequency->setColorMapper(ColorMapper(ColorMapper::GrayGold)); + ui->deltaFrequency->setValueRange(false, 7, -9999999, 9999999); + + m_channelMarker.blockSignals(true); + m_channelMarker.setColor(Qt::red); + m_channelMarker.setBandwidth(12500); + m_channelMarker.setCenterFrequency(0); + m_channelMarker.setTitle("AIS Modulator"); + m_channelMarker.setSourceOrSinkStream(false); + m_channelMarker.blockSignals(false); + m_channelMarker.setVisible(true); // activate signal on the last setting only + + m_deviceUISet->addChannelMarker(&m_channelMarker); + m_deviceUISet->addRollupWidget(this); + + connect(&m_channelMarker, SIGNAL(changedByCursor()), this, SLOT(channelMarkerChangedByCursor())); + + connect(getInputMessageQueue(), SIGNAL(messageEnqueued()), this, SLOT(handleSourceMessages())); + m_aisMod->setLevelMeter(ui->volumeMeter); + + m_settings.setChannelMarker(&m_channelMarker); + + ui->spectrumGUI->setBuddies(m_spectrumVis, ui->glSpectrum); + + ui->scopeContainer->setVisible(false); + ui->spectrumContainer->setVisible(false); + + displaySettings(); + applySettings(); +} + +AISModGUI::~AISModGUI() +{ + delete ui; +} + +void AISModGUI::transmit() +{ + QString data = ui->message->text(); + ui->transmittedText->appendPlainText(data + "\n"); + AISMod::MsgTXAISMod *msg = AISMod::MsgTXAISMod::create(data); + m_aisMod->getInputMessageQueue()->push(msg); +} + +void AISModGUI::blockApplySettings(bool block) +{ + m_doApplySettings = !block; +} + +void AISModGUI::applySettings(bool force) +{ + if (m_doApplySettings) + { + AISMod::MsgConfigureAISMod *msg = AISMod::MsgConfigureAISMod::create(m_settings, force); + m_aisMod->getInputMessageQueue()->push(msg); + } +} + +void AISModGUI::displaySettings() +{ + m_channelMarker.blockSignals(true); + m_channelMarker.setCenterFrequency(m_settings.m_inputFrequencyOffset); + m_channelMarker.setTitle(m_settings.m_title); + m_channelMarker.setBandwidth(m_settings.m_rfBandwidth); + m_channelMarker.blockSignals(false); + m_channelMarker.setColor(m_settings.m_rgbColor); // activate signal on the last setting only + + setTitleColor(m_settings.m_rgbColor); + setWindowTitle(m_channelMarker.getTitle()); + displayStreamIndex(); + + blockApplySettings(true); + + ui->deltaFrequency->setValue(m_channelMarker.getCenterFrequency()); + if ((m_settings.m_rfBandwidth == 12500.0f) && (m_settings.m_bt == 0.3f)) + ui->mode->setCurrentIndex(0); + else if ((m_settings.m_rfBandwidth == 25000.0f) && (m_settings.m_bt == 0.4f)) + ui->mode->setCurrentIndex(1); + else + { + ui->mode->removeItem(2); + ui->mode->addItem(m_settings.getMode()); + ui->mode->setCurrentIndex(2); + } + + ui->rfBWText->setText(QString("%1k").arg(m_settings.m_rfBandwidth / 1000.0, 0, 'f', 1)); + ui->rfBW->setValue(m_settings.m_rfBandwidth / 100.0); + + ui->fmDevText->setText(QString("%1k").arg(m_settings.m_fmDeviation / 1000.0, 0, 'f', 1)); + ui->fmDev->setValue(m_settings.m_fmDeviation / 100.0); + + ui->btText->setText(QString("%1").arg(m_settings.m_bt, 0, 'f', 1)); + ui->bt->setValue(m_settings.m_bt * 10); + + ui->gainText->setText(QString("%1").arg((double)m_settings.m_gain, 0, 'f', 1)); + ui->gain->setValue(m_settings.m_gain); + + ui->udpEnabled->setChecked(m_settings.m_udpEnabled); + ui->udpAddress->setText(m_settings.m_udpAddress); + ui->udpPort->setText(QString::number(m_settings.m_udpPort)); + + ui->channelMute->setChecked(m_settings.m_channelMute); + ui->repeat->setChecked(m_settings.m_repeat); + + ui->msgId->setCurrentIndex(m_settings.m_msgId - 1); + ui->mmsi->setText(m_settings.m_mmsi); + ui->status->setCurrentIndex(m_settings.m_status); + ui->latitude->setValue(m_settings.m_latitude); + ui->longitude->setValue(m_settings.m_longitude); + ui->course->setValue(m_settings.m_course); + ui->speed->setValue(m_settings.m_speed); + ui->heading->setValue(m_settings.m_heading); + ui->message->setText(m_settings.m_data); + + blockApplySettings(false); +} + +void AISModGUI::displayStreamIndex() +{ + if (m_deviceUISet->m_deviceMIMOEngine) { + setStreamIndicator(tr("%1").arg(m_settings.m_streamIndex)); + } else { + setStreamIndicator("S"); // single channel indicator + } +} + +void AISModGUI::leaveEvent(QEvent*) +{ + m_channelMarker.setHighlighted(false); +} + +void AISModGUI::enterEvent(QEvent*) +{ + m_channelMarker.setHighlighted(true); +} + +void AISModGUI::tick() +{ + double powDb = CalcDb::dbPower(m_aisMod->getMagSq()); + m_channelPowerDbAvg(powDb); + ui->channelPower->setText(tr("%1 dB").arg(m_channelPowerDbAvg.asDouble(), 0, 'f', 1)); +} diff --git a/plugins/channeltx/modais/aismodgui.h b/plugins/channeltx/modais/aismodgui.h new file mode 100644 index 000000000..511947146 --- /dev/null +++ b/plugins/channeltx/modais/aismodgui.h @@ -0,0 +1,119 @@ +/////////////////////////////////////////////////////////////////////////////////// +// Copyright (C) 2016 Edouard Griffiths, F4EXB // +// Copyright (C) 2020 Jon Beniston, M7RCE // +// // +// This program is free software; you can redistribute it and/or modify // +// it under the terms of the GNU General Public License as published by // +// the Free Software Foundation as version 3 of the License, or // +// (at your option) any later version. // +// // +// This program is distributed in the hope that it will be useful, // +// but WITHOUT ANY WARRANTY; without even the implied warranty of // +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // +// GNU General Public License V3 for more details. // +// // +// You should have received a copy of the GNU General Public License // +// along with this program. If not, see . // +/////////////////////////////////////////////////////////////////////////////////// + +#ifndef PLUGINS_CHANNELTX_MODAIS_AISMODGUI_H_ +#define PLUGINS_CHANNELTX_MODAIS_AISMODGUI_H_ + +#include "channel/channelgui.h" +#include "dsp/channelmarker.h" +#include "util/movingaverage.h" +#include "util/messagequeue.h" + +#include "aismod.h" +#include "aismodsettings.h" + +class PluginAPI; +class DeviceUISet; +class BasebandSampleSource; +class SpectrumVis; +class ScopeVis; + +namespace Ui { + class AISModGUI; +} + +class AISModGUI : public ChannelGUI { + Q_OBJECT + +public: + static AISModGUI* create(PluginAPI* pluginAPI, DeviceUISet *deviceUISet, BasebandSampleSource *channelTx); + virtual void destroy(); + + void resetToDefaults(); + QByteArray serialize() const; + bool deserialize(const QByteArray& data); + virtual MessageQueue *getInputMessageQueue() { return &m_inputMessageQueue; } + +public slots: + void channelMarkerChangedByCursor(); + +private: + Ui::AISModGUI* ui; + PluginAPI* m_pluginAPI; + DeviceUISet* m_deviceUISet; + ChannelMarker m_channelMarker; + AISModSettings m_settings; + bool m_doApplySettings; + SpectrumVis* m_spectrumVis; + ScopeVis* m_scopeVis; + + AISMod* m_aisMod; + MovingAverageUtil m_channelPowerDbAvg; // Less than other mods, as messages are short + + MessageQueue m_inputMessageQueue; + + explicit AISModGUI(PluginAPI* pluginAPI, DeviceUISet *deviceUISet, BasebandSampleSource *channelTx, QWidget* parent = 0); + virtual ~AISModGUI(); + + void transmit(); + void blockApplySettings(bool block); + void applySettings(bool force = false); + void displaySettings(); + void displayStreamIndex(); + bool handleMessage(const Message& message); + + void leaveEvent(QEvent*); + void enterEvent(QEvent*); + +private slots: + void handleSourceMessages(); + + void on_deltaFrequency_changed(qint64 value); + void on_mode_currentIndexChanged(int value); + void on_rfBW_valueChanged(int index); + void on_fmDev_valueChanged(int value); + void on_bt_valueChanged(int value); + void on_gain_valueChanged(int value); + void on_channelMute_toggled(bool checked); + void on_txButton_clicked(); + void on_encode_clicked(); + void on_msgId_currentIndexChanged(int index); + void on_mmsi_editingFinished(); + void on_status_currentIndexChanged(int index); + void on_latitude_valueChanged(double value); + void on_longitude_valueChanged(double value); + void on_insertPosition_clicked(); + void on_course_valueChanged(double value); + void on_speed_valueChanged(double value); + void on_heading_valueChanged(int value); + void on_message_editingFinished(); + void on_message_returnPressed(); + void on_repeat_toggled(bool checked); + void repeatSelect(); + void txSettingsSelect(); + void on_udpEnabled_clicked(bool checked); + void on_udpAddress_editingFinished(); + void on_udpPort_editingFinished(); + + void onWidgetRolled(QWidget* widget, bool rollDown); + void onMenuDialogCalled(const QPoint& p); + + void tick(); +}; + +#endif /* PLUGINS_CHANNELTX_MODAIS_AISMODGUI_H_ */ diff --git a/plugins/channeltx/modais/aismodgui.ui b/plugins/channeltx/modais/aismodgui.ui new file mode 100644 index 000000000..7fcb9aa63 --- /dev/null +++ b/plugins/channeltx/modais/aismodgui.ui @@ -0,0 +1,1212 @@ + + + AISModGUI + + + + 0 + 0 + 350 + 925 + + + + + 0 + 0 + + + + + 350 + 0 + + + + + Liberation Sans + 9 + + + + Qt::StrongFocus + + + AIS Modulator + + + + true + + + + 2 + 2 + 341 + 271 + + + + + 280 + 0 + + + + Settings + + + + 3 + + + 2 + + + 2 + + + 2 + + + 2 + + + + + 2 + + + + + + + + 16 + 0 + + + + Df + + + + + + + + 0 + 0 + + + + + 32 + 16 + + + + + Liberation Mono + 12 + + + + PointingHandCursor + + + Qt::StrongFocus + + + Mod shift frequency from center in Hz + + + + + + + Hz + + + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + + + 60 + 0 + + + + Channel power + + + Qt::RightToLeft + + + -100.0 dB + + + Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter + + + + + + + + + Mute/Unmute channel + + + ... + + + + :/txon.png + :/txoff.png:/txon.png + + + true + + + + + + + + + + + + 70 + 0 + + + + Tranmission mode + + + + Narrow + + + + + Wide + + + + + + + + Qt::Vertical + + + + + + + BW + + + + + + + + 0 + 0 + + + + + 0 + 0 + + + + RF bandwidth + + + 10 + + + 400 + + + 1 + + + 100 + + + Qt::Horizontal + + + + + + + + 30 + 0 + + + + 25.0k + + + Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter + + + + + + + Qt::Vertical + + + + + + + Dev + + + + + + + + 0 + 0 + + + + + 0 + 0 + + + + Frequency deviation + + + 10 + + + 50 + + + 1 + + + 24 + + + Qt::Horizontal + + + + + + + + 30 + 0 + + + + 4.8k + + + Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter + + + + + + + Qt::Vertical + + + + + + + BT + + + + + + + + 0 + 0 + + + + + 0 + 0 + + + + Gaussian filter bandwidth-time parameter + + + 1 + + + 6 + + + 1 + + + 3 + + + Qt::Horizontal + + + + + + + + 30 + 0 + + + + 0.4 + + + Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter + + + + + + + + + + + Gain + + + + + + + + 24 + 24 + + + + Gain + + + -60 + + + 0 + + + 5 + + + 1 + + + 0 + + + + + + + + 30 + 0 + + + + Gain value + + + + + + -80.0dB + + + Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter + + + + + + + + 0 + 0 + + + + + Liberation Mono + 8 + + + + Level (% full range) top trace: average, bottom trace: instantaneous peak, tip: peak hold + + + + + + + + + Qt::Horizontal + + + + + + + + + Forward messages received via UDP + + + Qt::RightToLeft + + + UDP + + + + + + + + 120 + 0 + + + + Qt::ClickFocus + + + UDP address to listen for messages to forward on + + + 000.000.000.000 + + + 127.0.0.1 + + + + + + + : + + + Qt::AlignCenter + + + + + + + + 50 + 0 + + + + + 50 + 16777215 + + + + Qt::ClickFocus + + + UDP port to listen for messages to forward on + + + 00000 + + + 9997 + + + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + + + + Qt::Horizontal + + + + + + + + + Encode a message in hex using data from the following fields in to the message field + + + Encode + + + + + + + + 0 + 0 + + + + + 100 + 0 + + + + Message type + + + true + + + Scheduled position report + + + 0 + + + + Scheduled position report + + + + + Assigned position report + + + + + Special position report + + + + + Base station report + + + + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + + + + + + MMSI + + + + + + + + 0 + 0 + + + + Enter 9 digit Maritime Mobile Service Identity + + + 999999999 + + + 888888888 + + + 9 + + + + + + + Status + + + + + + + + 180 + 0 + + + + Status of vessel + + + + Under way using engine + + + + + At anchor + + + + + Not under command + + + + + Restricted manoeuvrability + + + + + Constrained by her draught + + + + + Moored + + + + + Aground + + + + + Engaged in fishing + + + + + Under way sailing + + + + + Not defined + + + + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + + + + + + Latitude + + + + + + + Latitude in decimal degrees (North positive) + + + 6 + + + -90.000000000000000 + + + 90.000000000000000 + + + -90.000000000000000 + + + + + + + Longitude + + + + + + + Longitude in decimal degress (East positive) + + + 6 + + + -180.000000000000000 + + + 180.000000000000000 + + + -180.000000000000000 + + + + + + + Set latitude and longitude to my position + + + ... + + + + :/gps.png + + + + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + + + + + + Course + + + + + + + Course over ground in degrees + + + 1 + + + 359.899999999999977 + + + 359.899999999999977 + + + + + + + Speed + + + + + + + Speed over ground in knots + + + 1 + + + 1023.000000000000000 + + + 1022.000000000000000 + + + + + + + Heading + + + + + + + Heading in degrees + + + 359 + + + 359 + + + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + + + + Qt::Horizontal + + + + + + + + + Qt::LeftToRight + + + Msg + + + + + + + Enter message to transmit as a hex encoded string. + + + + + + 63 + + + + + + + Repeatedly transmit the message. Right click for additional settings. + + + ... + + + + :/playloop.png:/playloop.png + + + + + + + Press to transmit the message + + + TX + + + + + + + + + + + 0 + 290 + 351 + 141 + + + + Transmitted Messages + + + + 2 + + + 3 + + + 3 + + + 3 + + + 3 + + + + + true + + + + + + + + + 0 + 450 + 351 + 331 + + + + Baseband Spectrum + + + + 2 + + + 3 + + + 3 + + + 3 + + + 3 + + + + + + 200 + 250 + + + + + Liberation Mono + 8 + + + + + + + + + + + + + 0 + 790 + 351 + 311 + + + + IQ Waveforms + + + + 2 + + + 3 + + + 3 + + + 3 + + + 3 + + + + + + 200 + 250 + + + + + Liberation Mono + 8 + + + + + + + + + + + + + RollupWidget + QWidget +
    gui/rollupwidget.h
    + 1 +
    + + ValueDialZ + QWidget +
    gui/valuedialz.h
    + 1 +
    + + GLSpectrum + QWidget +
    gui/glspectrum.h
    + 1 +
    + + GLSpectrumGUI + QWidget +
    gui/glspectrumgui.h
    + 1 +
    + + ButtonSwitch + QToolButton +
    gui/buttonswitch.h
    +
    + + LevelMeterVU + QWidget +
    gui/levelmeter.h
    + 1 +
    + + GLScope + QWidget +
    gui/glscope.h
    + 1 +
    + + GLScopeGUI + QWidget +
    gui/glscopegui.h
    + 1 +
    +
    + + deltaFrequency + channelMute + mode + rfBW + bt + gain + mmsi + msgId + message + txButton + transmittedText + + + + + +
    diff --git a/plugins/channeltx/modais/aismodplugin.cpp b/plugins/channeltx/modais/aismodplugin.cpp new file mode 100644 index 000000000..43e9c9210 --- /dev/null +++ b/plugins/channeltx/modais/aismodplugin.cpp @@ -0,0 +1,92 @@ +/////////////////////////////////////////////////////////////////////////////////// +// Copyright (C) 2016 Edouard Griffiths, F4EXB // +// Copyright (C) 2020 Jon Beniston, M7RCE // +// // +// This program is free software; you can redistribute it and/or modify // +// it under the terms of the GNU General Public License as published by // +// the Free Software Foundation as version 3 of the License, or // +// (at your option) any later version. // +// // +// This program is distributed in the hope that it will be useful, // +// but WITHOUT ANY WARRANTY; without even the implied warranty of // +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // +// GNU General Public License V3 for more details. // +// // +// You should have received a copy of the GNU General Public License // +// along with this program. If not, see . // +/////////////////////////////////////////////////////////////////////////////////// + +#include +#include "plugin/pluginapi.h" + +#ifndef SERVER_MODE +#include "aismodgui.h" +#endif +#include "aismod.h" +#include "aismodwebapiadapter.h" +#include "aismodplugin.h" + +const PluginDescriptor AISModPlugin::m_pluginDescriptor = { + AISMod::m_channelId, + QStringLiteral("AIS Modulator"), + QStringLiteral("6.11.1"), + QStringLiteral("(c) Jon Beniston, M7RCE"), + QStringLiteral("https://github.com/f4exb/sdrangel"), + true, + QStringLiteral("https://github.com/f4exb/sdrangel") +}; + +AISModPlugin::AISModPlugin(QObject* parent) : + QObject(parent), + m_pluginAPI(0) +{ +} + +const PluginDescriptor& AISModPlugin::getPluginDescriptor() const +{ + return m_pluginDescriptor; +} + +void AISModPlugin::initPlugin(PluginAPI* pluginAPI) +{ + m_pluginAPI = pluginAPI; + + m_pluginAPI->registerTxChannel(AISMod::m_channelIdURI, AISMod::m_channelId, this); +} + +void AISModPlugin::createTxChannel(DeviceAPI *deviceAPI, BasebandSampleSource **bs, ChannelAPI **cs) const +{ + if (bs || cs) + { + AISMod *instance = new AISMod(deviceAPI); + + if (bs) { + *bs = instance; + } + + if (cs) { + *cs = instance; + } + } +} + +#ifdef SERVER_MODE +ChannelGUI* AISModPlugin::createTxChannelGUI( + DeviceUISet *deviceUISet, + BasebandSampleSource *txChannel) const +{ + (void) deviceUISet; + (void) txChannel; + return nullptr; +} +#else +ChannelGUI* AISModPlugin::createTxChannelGUI(DeviceUISet *deviceUISet, BasebandSampleSource *txChannel) const +{ + return AISModGUI::create(m_pluginAPI, deviceUISet, txChannel); +} +#endif + +ChannelWebAPIAdapter* AISModPlugin::createChannelWebAPIAdapter() const +{ + return new AISModWebAPIAdapter(); +} diff --git a/plugins/channeltx/modais/aismodplugin.h b/plugins/channeltx/modais/aismodplugin.h new file mode 100644 index 000000000..85da4230a --- /dev/null +++ b/plugins/channeltx/modais/aismodplugin.h @@ -0,0 +1,49 @@ +/////////////////////////////////////////////////////////////////////////////////// +// Copyright (C) 2016 Edouard Griffiths, F4EXB // +// Copyright (C) 2020 Jon Beniston, M7RCE // +// // +// This program is free software; you can redistribute it and/or modify // +// it under the terms of the GNU General Public License as published by // +// the Free Software Foundation as version 3 of the License, or // +// (at your option) any later version. // +// // +// This program is distributed in the hope that it will be useful, // +// but WITHOUT ANY WARRANTY; without even the implied warranty of // +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // +// GNU General Public License V3 for more details. // +// // +// You should have received a copy of the GNU General Public License // +// along with this program. If not, see . // +/////////////////////////////////////////////////////////////////////////////////// + +#ifndef INCLUDE_AISMODPLUGIN_H +#define INCLUDE_AISMODPLUGIN_H + +#include +#include "plugin/plugininterface.h" + +class DeviceUISet; +class BasebandSampleSource; + +class AISModPlugin : public QObject, PluginInterface { + Q_OBJECT + Q_INTERFACES(PluginInterface) + Q_PLUGIN_METADATA(IID "sdrangel.channel.modais") + +public: + explicit AISModPlugin(QObject* parent = 0); + + const PluginDescriptor& getPluginDescriptor() const; + void initPlugin(PluginAPI* pluginAPI); + + virtual void createTxChannel(DeviceAPI *deviceAPI, BasebandSampleSource **bs, ChannelAPI **cs) const; + virtual ChannelGUI* createTxChannelGUI(DeviceUISet *deviceUISet, BasebandSampleSource *rxChannel) const; + virtual ChannelWebAPIAdapter* createChannelWebAPIAdapter() const; + +private: + static const PluginDescriptor m_pluginDescriptor; + + PluginAPI* m_pluginAPI; +}; + +#endif // INCLUDE_AISMODPLUGIN_H diff --git a/plugins/channeltx/modais/aismodrepeatdialog.cpp b/plugins/channeltx/modais/aismodrepeatdialog.cpp new file mode 100644 index 000000000..37eae2080 --- /dev/null +++ b/plugins/channeltx/modais/aismodrepeatdialog.cpp @@ -0,0 +1,54 @@ +/////////////////////////////////////////////////////////////////////////////////// +// Copyright (C) 2020 Jon Beniston, M7RCE // +// // +// This program is free software; you can redistribute it and/or modify // +// it under the terms of the GNU General Public License as published by // +// the Free Software Foundation as version 3 of the License, or // +// (at your option) any later version. // +// // +// This program is distributed in the hope that it will be useful, // +// but WITHOUT ANY WARRANTY; without even the implied warranty of // +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // +// GNU General Public License V3 for more details. // +// // +// You should have received a copy of the GNU General Public License // +// along with this program. If not, see . // +/////////////////////////////////////////////////////////////////////////////////// + +#include "aismodrepeatdialog.h" +#include "aismodsettings.h" +#include + +AISModRepeatDialog::AISModRepeatDialog(float repeatDelay, int repeatCount, QWidget* parent) : + QDialog(parent), + ui(new Ui::AISModRepeatDialog) +{ + ui->setupUi(this); + ui->repeatDelay->setValue(repeatDelay); + QLineEdit *edit = ui->repeatCount->lineEdit(); + if (edit) + { + if (repeatCount == AISModSettings::infinitePackets) { + edit->setText("Infinite"); + } else { + edit->setText(QString("%1").arg(repeatCount)); + } + } +} + +AISModRepeatDialog::~AISModRepeatDialog() +{ + delete ui; +} + +void AISModRepeatDialog::accept() +{ + m_repeatDelay = ui->repeatDelay->value(); + QString text = ui->repeatCount->currentText(); + if (!text.compare(QString("Infinite"), Qt::CaseInsensitive)) { + m_repeatCount = AISModSettings::infinitePackets; + } else { + m_repeatCount = text.toUInt(); + } + QDialog::accept(); +} diff --git a/plugins/channeltx/modais/aismodrepeatdialog.h b/plugins/channeltx/modais/aismodrepeatdialog.h new file mode 100644 index 000000000..db8be0e7a --- /dev/null +++ b/plugins/channeltx/modais/aismodrepeatdialog.h @@ -0,0 +1,40 @@ +/////////////////////////////////////////////////////////////////////////////////// +// Copyright (C) 2020 Jon Beniston, M7RCE // +// // +// This program is free software; you can redistribute it and/or modify // +// it under the terms of the GNU General Public License as published by // +// the Free Software Foundation as version 3 of the License, or // +// (at your option) any later version. // +// // +// This program is distributed in the hope that it will be useful, // +// but WITHOUT ANY WARRANTY; without even the implied warranty of // +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // +// GNU General Public License V3 for more details. // +// // +// You should have received a copy of the GNU General Public License // +// along with this program. If not, see . // +/////////////////////////////////////////////////////////////////////////////////// + +#ifndef INCLUDE_AISMODREPEATDIALOG_H +#define INCLUDE_AISMODREPEATDIALOG_H + +#include "ui_aismodrepeatdialog.h" + +class AISModRepeatDialog : public QDialog { + Q_OBJECT + +public: + explicit AISModRepeatDialog(float repeatDelay, int repeatCount, QWidget* parent = 0); + ~AISModRepeatDialog(); + + float m_repeatDelay; // Delay in seconds between messages + int m_repeatCount; // Number of messages to transmit (-1 = infinite) + +private slots: + void accept(); + +private: + Ui::AISModRepeatDialog* ui; +}; + +#endif // INCLUDE_AISMODREPEATDIALOG_H diff --git a/plugins/channeltx/modais/aismodrepeatdialog.ui b/plugins/channeltx/modais/aismodrepeatdialog.ui new file mode 100644 index 000000000..78b7945f1 --- /dev/null +++ b/plugins/channeltx/modais/aismodrepeatdialog.ui @@ -0,0 +1,134 @@ + + + AISModRepeatDialog + + + + 0 + 0 + 351 + 115 + + + + + Liberation Sans + 9 + + + + Packet Repeat Settings + + + + + + + + + Delay between messages (s) + + + + + + + Messages to transmit + + + + + + + Number of messages to transmit + + + true + + + + Infinite + + + + + 10 + + + + + 100 + + + + + 1000 + + + + + + + + 3 + + + 1000000.000000000000000 + + + + + + + + + + Qt::Horizontal + + + QDialogButtonBox::Cancel|QDialogButtonBox::Ok + + + + + + + repeatDelay + repeatCount + + + + + buttonBox + accepted() + AISModRepeatDialog + accept() + + + 248 + 254 + + + 157 + 274 + + + + + buttonBox + rejected() + AISModRepeatDialog + reject() + + + 316 + 260 + + + 286 + 274 + + + + + diff --git a/plugins/channeltx/modais/aismodsettings.cpp b/plugins/channeltx/modais/aismodsettings.cpp new file mode 100644 index 000000000..8899c71ad --- /dev/null +++ b/plugins/channeltx/modais/aismodsettings.cpp @@ -0,0 +1,224 @@ +/////////////////////////////////////////////////////////////////////////////////// +// Copyright (C) 2017 Edouard Griffiths, F4EXB // +// Copyright (C) 2020 Jon Beniston, M7RCE // +// // +// This program is free software; you can redistribute it and/or modify // +// it under the terms of the GNU General Public License as published by // +// the Free Software Foundation as version 3 of the License, or // +// (at your option) any later version. // +// // +// This program is distributed in the hope that it will be useful, // +// but WITHOUT ANY WARRANTY; without even the implied warranty of // +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // +// GNU General Public License V3 for more details. // +// // +// You should have received a copy of the GNU General Public License // +// along with this program. If not, see . // +/////////////////////////////////////////////////////////////////////////////////// + +#include +#include + +#include "dsp/dspengine.h" +#include "util/simpleserializer.h" +#include "settings/serializable.h" +#include "aismodsettings.h" + +AISModSettings::AISModSettings() +{ + resetToDefaults(); +} + +void AISModSettings::resetToDefaults() +{ + m_inputFrequencyOffset = 0; + m_baud = 9600; + m_rfBandwidth = 25000.0f; // 12.5k for narrow, 25k for wide (narrow is obsolete) + m_fmDeviation = 4800.0f; // To give modulation index of 0.5 for 9600 baud + m_gain = -1.0f; // To avoid overflow, which results in out-of-band RF + m_channelMute = false; + m_repeat = false; + m_repeatDelay = 1.0f; + m_repeatCount = infinitePackets; + m_rampUpBits = 0; + m_rampDownBits = 0; + m_rampRange = 60; + m_rfNoise = false; + m_writeToFile = false; + m_msgId = 1; + m_mmsi = "0000000000"; + m_status = 0; + m_latitude = 0.0f; + m_longitude = 0.0f; + m_course = 0.0f; + m_speed = 0.0f; + m_heading = 0; + m_data = ""; + m_rgbColor = QColor(102, 0, 0).rgb(); + m_title = "AIS Modulator"; + m_streamIndex = 0; + m_useReverseAPI = false; + m_reverseAPIAddress = "127.0.0.1"; + m_reverseAPIPort = 8888; + m_reverseAPIDeviceIndex = 0; + m_reverseAPIChannelIndex = 0; + m_bt = 0.4f; // 0.3 for narrow, 0.4 for wide + m_symbolSpan = 3; + m_udpEnabled = false; + m_udpAddress = "127.0.0.1"; + m_udpPort = 9998; +} + +bool AISModSettings::setMode(QString mode) +{ + if (mode.endsWith("Narrow")) + { + m_rfBandwidth = 12500.0f; + m_fmDeviation = m_baud * 0.25; + m_bt = 0.3f; + return true; + } + else if (mode.endsWith("Wide")) + { + m_rfBandwidth = 25000.0f; + m_fmDeviation = m_baud * 0.5; + m_bt = 0.4f; + return true; + } + else + { + return false; + } +} + +QString AISModSettings::getMode() const +{ + return QString("%1 %2 %3").arg(m_rfBandwidth).arg(m_fmDeviation).arg(m_bt); +} + +QByteArray AISModSettings::serialize() const +{ + SimpleSerializer s(1); + + s.writeS32(1, m_inputFrequencyOffset); + s.writeS32(2, m_baud); + s.writeReal(3, m_rfBandwidth); + s.writeReal(4, m_fmDeviation); + s.writeReal(5, m_gain); + s.writeBool(6, m_channelMute); + s.writeBool(7, m_repeat); + s.writeReal(8, m_repeatDelay); + s.writeS32(9, m_repeatCount); + s.writeS32(10, m_rampUpBits); + s.writeS32(11, m_rampDownBits); + s.writeS32(12, m_rampRange); + s.writeBool(14, m_rfNoise); + s.writeBool(15, m_writeToFile); + s.writeS32(17, m_msgId); + s.writeString(18, m_mmsi); + s.writeS32(19, m_status); + s.writeFloat(20, m_latitude); + s.writeFloat(21, m_longitude); + s.writeFloat(22, m_course); + s.writeFloat(23, m_speed); + s.writeS32(24, m_heading); + s.writeString(25, m_data); + s.writeReal(26, m_bt); + s.writeS32(27, m_symbolSpan); + s.writeU32(28, m_rgbColor); + s.writeString(29, m_title); + if (m_channelMarker) { + s.writeBlob(30, m_channelMarker->serialize()); + } + s.writeS32(31, m_streamIndex); + s.writeBool(32, m_useReverseAPI); + s.writeString(33, m_reverseAPIAddress); + s.writeU32(34, m_reverseAPIPort); + s.writeU32(35, m_reverseAPIDeviceIndex); + s.writeU32(36, m_reverseAPIChannelIndex); + s.writeBool(37, m_udpEnabled); + s.writeString(38, m_udpAddress); + s.writeU32(39, m_udpPort); + + return s.final(); +} + +bool AISModSettings::deserialize(const QByteArray& data) +{ + SimpleDeserializer d(data); + + if(!d.isValid()) + { + resetToDefaults(); + return false; + } + + if(d.getVersion() == 1) + { + QByteArray bytetmp; + qint32 tmp; + uint32_t utmp; + + d.readS32(1, &tmp, 0); + m_inputFrequencyOffset = tmp; + d.readS32(2, &m_baud, 9600); + d.readReal(3, &m_rfBandwidth, 25000.0f); + d.readReal(4, &m_fmDeviation, 4800.0f); + d.readReal(5, &m_gain, -1.0f); + d.readBool(6, &m_channelMute, false); + d.readBool(7, &m_repeat, false); + d.readReal(8, &m_repeatDelay, 1.0f); + d.readS32(9, &m_repeatCount, -1); + d.readS32(10, &m_rampUpBits, 8); + d.readS32(11, &m_rampDownBits, 8); + d.readS32(12, &m_rampRange, 8); + d.readBool(14, &m_rfNoise, false); + d.readBool(15, &m_writeToFile, false); + d.readS32(17, &m_msgId, 1); + d.readString(18, &m_mmsi, "0000000000"); + d.readS32(19, &m_status, 0); + d.readFloat(20, &m_latitude, 0.0f); + d.readFloat(21, &m_longitude, 0.0f); + d.readFloat(22, &m_course, 0.0f); + d.readFloat(23, &m_speed, 0.0f); + d.readS32(24, &m_heading, 0); + d.readString(25, &m_data, ""); + d.readReal(26, &m_bt, 0.3f); + d.readS32(27, &m_symbolSpan, 3); + d.readU32(28, &m_rgbColor, QColor(102, 0, 0).rgb()); + d.readString(29, &m_title, "AIS Modulator"); + if (m_channelMarker) { + d.readBlob(30, &bytetmp); + m_channelMarker->deserialize(bytetmp); + } + d.readS32(31, &m_streamIndex, 0); + d.readBool(32, &m_useReverseAPI, false); + d.readString(33, &m_reverseAPIAddress, "127.0.0.1"); + d.readU32(34, &utmp, 0); + if ((utmp > 1023) && (utmp < 65535)) { + m_reverseAPIPort = utmp; + } else { + m_reverseAPIPort = 8888; + } + d.readU32(35, &utmp, 0); + m_reverseAPIDeviceIndex = utmp > 99 ? 99 : utmp; + d.readU32(36, &utmp, 0); + m_reverseAPIChannelIndex = utmp > 99 ? 99 : utmp; + d.readBool(37, &m_udpEnabled); + d.readString(38, &m_udpAddress, "127.0.0.1"); + d.readU32(39, &utmp); + if ((utmp > 1023) && (utmp < 65535)) { + m_udpPort = utmp; + } else { + m_udpPort = 9998; + } + + return true; + } + else + { + qDebug() << "AISModSettings::deserialize: ERROR"; + resetToDefaults(); + return false; + } +} diff --git a/plugins/channeltx/modais/aismodsettings.h b/plugins/channeltx/modais/aismodsettings.h new file mode 100644 index 000000000..75061ad8d --- /dev/null +++ b/plugins/channeltx/modais/aismodsettings.h @@ -0,0 +1,79 @@ +/////////////////////////////////////////////////////////////////////////////////// +// Copyright (C) 2017 Edouard Griffiths, F4EXB // +// Copyright (C) 2020 Jon Beniston, M7RCE // +// // +// This program is free software; you can redistribute it and/or modify // +// it under the terms of the GNU General Public License as published by // +// the Free Software Foundation as version 3 of the License, or // +// (at your option) any later version. // +// // +// This program is distributed in the hope that it will be useful, // +// but WITHOUT ANY WARRANTY; without even the implied warranty of // +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // +// GNU General Public License V3 for more details. // +// // +// You should have received a copy of the GNU General Public License // +// along with this program. If not, see . // +/////////////////////////////////////////////////////////////////////////////////// + +#ifndef PLUGINS_CHANNELTX_MODAIS_AISMODSETTINGS_H +#define PLUGINS_CHANNELTX_MODAIS_AISMODSETTINGS_H + +#include +#include +#include "dsp/dsptypes.h" + +class Serializable; + +struct AISModSettings +{ + static const int infinitePackets = -1; + + qint64 m_inputFrequencyOffset; + int m_baud; + Real m_rfBandwidth; + Real m_fmDeviation; + Real m_gain; + bool m_channelMute; + bool m_repeat; + Real m_repeatDelay; + int m_repeatCount; + int m_rampUpBits; + int m_rampDownBits; + int m_rampRange; + bool m_rfNoise; + bool m_writeToFile; + int m_msgId; + QString m_mmsi; + int m_status; + float m_latitude; + float m_longitude; + float m_course; + float m_speed; + int m_heading; + QString m_data; + float m_bt; + int m_symbolSpan; + quint32 m_rgbColor; + QString m_title; + Serializable *m_channelMarker; + int m_streamIndex; + bool m_useReverseAPI; + QString m_reverseAPIAddress; + uint16_t m_reverseAPIPort; + uint16_t m_reverseAPIDeviceIndex; + uint16_t m_reverseAPIChannelIndex; + bool m_udpEnabled; + QString m_udpAddress; + uint16_t m_udpPort; + + AISModSettings(); + void resetToDefaults(); + void setChannelMarker(Serializable *channelMarker) { m_channelMarker = channelMarker; } + QByteArray serialize() const; + bool deserialize(const QByteArray& data); + bool setMode(QString mode); + QString getMode() const; +}; + +#endif /* PLUGINS_CHANNELTX_MODAIS_AISMODSETTINGS_H */ diff --git a/plugins/channeltx/modais/aismodsource.cpp b/plugins/channeltx/modais/aismodsource.cpp new file mode 100644 index 000000000..30a08896b --- /dev/null +++ b/plugins/channeltx/modais/aismodsource.cpp @@ -0,0 +1,496 @@ +/////////////////////////////////////////////////////////////////////////////////// +// Copyright (C) 2019 Edouard Griffiths, F4EXB // +// Copyright (C) 2020 Jon Beniston, M7RCE // +// // +// This program is free software; you can redistribute it and/or modify // +// it under the terms of the GNU General Public License as published by // +// the Free Software Foundation as version 3 of the License, or // +// (at your option) any later version. // +// // +// This program is distributed in the hope that it will be useful, // +// but WITHOUT ANY WARRANTY; without even the implied warranty of // +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // +// GNU General Public License V3 for more details. // +// // +// You should have received a copy of the GNU General Public License // +// along with this program. If not, see . // +/////////////////////////////////////////////////////////////////////////////////// + +#include + +#include "dsp/basebandsamplesink.h" +#include "dsp/datafifo.h" +#include "aismodsource.h" +#include "util/crc.h" +#include "util/messagequeue.h" +#include "maincore.h" +#include "channel/channelapi.h" + +AISModSource::AISModSource() : + m_channelSampleRate(AISMOD_SAMPLE_RATE), + m_channelFrequencyOffset(0), + m_fmPhase(0.0), + m_spectrumSink(nullptr), + m_scopeSink(nullptr), + m_magsq(0.0), + m_levelCalcCount(0), + m_peakLevel(0.0f), + m_levelSum(0.0f), + m_state(idle), + m_byteIdx(0), + m_bitIdx(0), + m_last5Bits(0), + m_bitCount(0) + { + m_demodBuffer.resize(1<<12); + m_demodBufferFill = 0; + + applySettings(m_settings, true); + applyChannelSettings(m_channelSampleRate, m_channelFrequencyOffset, true); +} + +AISModSource::~AISModSource() +{ +} + +void AISModSource::pull(SampleVector::iterator begin, unsigned int nbSamples) +{ + std::for_each( + begin, + begin + nbSamples, + [this](Sample& s) { + pullOne(s); + } + ); +} + +void AISModSource::pullOne(Sample& sample) +{ + if (m_settings.m_channelMute) + { + sample.m_real = 0.0f; + sample.m_imag = 0.0f; + return; + } + + Complex ci; + + if (m_interpolatorDistance > 1.0f) + { + modulateSample(); + + while (!m_interpolator.decimate(&m_interpolatorDistanceRemain, m_modSample, &ci)) + { + modulateSample(); + } + } + else + { + if (m_interpolator.interpolate(&m_interpolatorDistanceRemain, m_modSample, &ci)) + { + modulateSample(); + } + } + + m_interpolatorDistanceRemain += m_interpolatorDistance; + + ci *= m_carrierNco.nextIQ(); // shift to carrier frequency + + // Calculate power + double magsq = ci.real() * ci.real() + ci.imag() * ci.imag(); + m_movingAverage(magsq); + m_magsq = m_movingAverage.asDouble(); + + // Convert from float to fixed point + sample.m_real = (FixReal) (ci.real() * SDR_TX_SCALEF); + sample.m_imag = (FixReal) (ci.imag() * SDR_TX_SCALEF); +} + +void AISModSource::sampleToSpectrum(Complex sample) +{ + if (m_spectrumSink) + { + Real r = std::real(sample) * SDR_TX_SCALEF; + Real i = std::imag(sample) * SDR_TX_SCALEF; + m_sampleBuffer.push_back(Sample(r, i)); + m_spectrumSink->feed(m_sampleBuffer.begin(), m_sampleBuffer.end(), false); + m_sampleBuffer.clear(); + } +} + +void AISModSource::sampleToScope(Complex sample) +{ + if (m_scopeSink) + { + Real r = std::real(sample) * SDR_RX_SCALEF; + Real i = std::imag(sample) * SDR_RX_SCALEF; + m_sampleBuffer.push_back(Sample(r, i)); + m_scopeSink->feed(m_sampleBuffer.begin(), m_sampleBuffer.end(), true); + m_sampleBuffer.clear(); + } +} + +void AISModSource::modulateSample() +{ + Real mod; + Real linearRampGain; + + if ((m_state == idle) || (m_state == wait)) + { + m_modSample.real(0.0f); + m_modSample.imag(0.0f); + sampleToSpectrum(m_modSample); + sampleToScope(m_modSample); + Real s = std::abs(m_modSample); + calculateLevel(s); + if (m_state == wait) + { + m_waitCounter--; + if (m_waitCounter == 0) { + initTX(); + } + } + } + else + { + if (m_sampleIdx == 0) + { + if (bitsValid()) + { + // NRZI encoding - encode 0 as change of freq, 1 no change + if (getBit() == 0) { + m_nrziBit = m_nrziBit == 1 ? 0 : 1; + } + } + // Should we start ramping down power? + if ((m_bitCount < m_settings.m_rampDownBits) || ((m_bitCount == 0) && !m_settings.m_rampDownBits)) + { + m_state = ramp_down; + if (m_settings.m_rampDownBits > 0) { + m_powRamp = -m_settings.m_rampRange/(m_settings.m_rampDownBits * (Real)m_samplesPerSymbol); + } + } + } + m_sampleIdx++; + if (m_sampleIdx >= m_samplesPerSymbol) { + m_sampleIdx = 0; + } + + // Apply Gaussian pulse shaping filter + mod = m_pulseShape.filter(m_nrziBit ? 1.0f : -1.0f); + + // FM + m_fmPhase += m_phaseSensitivity * mod; + // Keep phase in range -pi,pi + if (m_fmPhase > M_PI) { + m_fmPhase -= 2.0f * M_PI; + } else if (m_fmPhase < -M_PI) { + m_fmPhase += 2.0f * M_PI; + } + + linearRampGain = powf(10.0f, m_pow/20.0f); + + m_modSample.real(m_linearGain * linearRampGain * cos(m_fmPhase)); + m_modSample.imag(m_linearGain * linearRampGain * sin(m_fmPhase)); + + if (m_iqFile.is_open()) { + m_iqFile << mod << "," << m_modSample.real() << "," << m_modSample.imag() << "\n"; + } + + if (m_settings.m_rfNoise) + { + // Noise to test filter frequency response + m_modSample.real(m_linearGain * ((Real)rand()/((Real)RAND_MAX)-0.5f)); + m_modSample.imag(m_linearGain * ((Real)rand()/((Real)RAND_MAX)-0.5f)); + } + + // Display baseband in spectrum analyser and scope + sampleToSpectrum(m_modSample); + sampleToScope(m_modSample); + + // Ramp up/down power at start/end of packet + if ((m_state == ramp_up) || (m_state == ramp_down)) + { + m_pow += m_powRamp; + if ((m_state == ramp_up) && (m_pow >= 0.0f)) + { + // Finished ramp up, transmit at full gain + m_state = tx; + m_pow = 0.0f; + } + else if ((m_state == ramp_down) && ( (m_settings.m_rampRange == 0) + || (m_settings.m_rampDownBits == 0) + || (m_pow <= -(Real)m_settings.m_rampRange) + )) + { + m_state = idle; + // Do we need to retransmit the packet? + if (m_settings.m_repeat) + { + if (m_packetRepeatCount > 0) + m_packetRepeatCount--; + if ((m_packetRepeatCount == AISModSettings::infinitePackets) || (m_packetRepeatCount > 0)) + { + if (m_settings.m_repeatDelay > 0.0f) + { + // Wait before retransmitting + m_state = wait; + m_waitCounter = m_settings.m_repeatDelay * AISMOD_SAMPLE_RATE; + } + else + { + // Retransmit immediately + initTX(); + } + } + } + } + } + + Real s = std::abs(m_modSample); + calculateLevel(s); + } + + // Send Gaussian filter output to mod analyzer + m_demodBuffer[m_demodBufferFill] = std::real(mod) * std::numeric_limits::max(); + ++m_demodBufferFill; + + if (m_demodBufferFill >= m_demodBuffer.size()) + { + QList *dataFifos = MainCore::instance()->getDataPipes().getFifos(m_channel, "demod"); + + if (dataFifos) + { + QList::iterator it = dataFifos->begin(); + + for (; it != dataFifos->end(); ++it) { + (*it)->write((quint8*) &m_demodBuffer[0], m_demodBuffer.size() * sizeof(qint16)); + } + } + + m_demodBufferFill = 0; + } +} + +void AISModSource::calculateLevel(Real& sample) +{ + if (m_levelCalcCount < m_levelNbSamples) + { + m_peakLevel = std::max(std::fabs(m_peakLevel), sample); + m_levelSum += sample * sample; + m_levelCalcCount++; + } + else + { + m_rmsLevel = sqrt(m_levelSum / m_levelNbSamples); + m_peakLevelOut = m_peakLevel; + m_peakLevel = 0.0f; + m_levelSum = 0.0f; + m_levelCalcCount = 0; + } +} + +void AISModSource::applySettings(const AISModSettings& settings, bool force) +{ + if ((settings.m_bt != m_settings.m_bt) || (settings.m_symbolSpan != m_settings.m_symbolSpan) || (settings.m_baud != m_settings.m_baud) || force) + { + qDebug() << "AISModSource::applySettings: Recreating pulse shaping filter: " + << " SampleRate:" << AISMOD_SAMPLE_RATE + << " bt: " << settings.m_bt + << " symbolSpan: " << settings.m_symbolSpan + << " baud:" << settings.m_baud; + m_pulseShape.create(settings.m_bt, settings.m_symbolSpan, AISMOD_SAMPLE_RATE/settings.m_baud); + } + + m_settings = settings; + + // Precalculate FM sensensity and linear gain to save doing it in the loop + m_samplesPerSymbol = AISMOD_SAMPLE_RATE / m_settings.m_baud; + Real modIndex = m_settings.m_fmDeviation / (Real)m_settings.m_baud; + m_phaseSensitivity = 2.0f * M_PI * modIndex / (Real)m_samplesPerSymbol; + m_linearGain = powf(10.0f, m_settings.m_gain/20.0f); +} + +void AISModSource::applyChannelSettings(int channelSampleRate, int channelFrequencyOffset, bool force) +{ + qDebug() << "AISModSource::applyChannelSettings:" + << " channelSampleRate: " << channelSampleRate + << " channelFrequencyOffset: " << channelFrequencyOffset + << " rfBandwidth: " << m_settings.m_rfBandwidth; + + if ((channelFrequencyOffset != m_channelFrequencyOffset) + || (channelSampleRate != m_channelSampleRate) || force) + { + m_carrierNco.setFreq(channelFrequencyOffset, channelSampleRate); + } + + if ((m_channelSampleRate != channelSampleRate) || force) + { + m_interpolatorDistanceRemain = 0; + m_interpolatorDistance = (Real) AISMOD_SAMPLE_RATE / (Real) channelSampleRate; + m_interpolator.create(48, AISMOD_SAMPLE_RATE, m_settings.m_rfBandwidth / 2.2, 3.0); + } + + m_channelSampleRate = channelSampleRate; + m_channelFrequencyOffset = channelFrequencyOffset; + + QList *messageQueues = MainCore::instance()->getMessagePipes().getMessageQueues(m_channel, "reportdemod"); + + if (messageQueues) + { + QList::iterator it = messageQueues->begin(); + + for (; it != messageQueues->end(); ++it) + { + MainCore::MsgChannelDemodReport *msg = MainCore::MsgChannelDemodReport::create(m_channel, m_channelSampleRate); + (*it)->push(msg); + } + } +} + +bool AISModSource::bitsValid() +{ + return m_bitCount > 0; +} + +int AISModSource::getBit() +{ + int bit; + + if (m_bitCount > 0) + { + bit = (m_bits[m_byteIdx] >> m_bitIdx) & 1; + m_bitIdx++; + m_bitCount--; + if (m_bitIdx == 8) + { + m_byteIdx++; + m_bitIdx = 0; + } + } + else + bit = 0; + return bit; +} + +void AISModSource::addBit(int bit) +{ + // Transmit LSB first + m_bits[m_byteIdx] |= bit << m_bitIdx; + m_bitIdx++; + m_bitCount++; + m_bitCountTotal++; + if (m_bitIdx == 8) + { + m_byteIdx++; + m_bits[m_byteIdx] = 0; + m_bitIdx = 0; + } + m_last5Bits = ((m_last5Bits << 1) | bit) & 0x1f; +} + +void AISModSource::initTX() +{ + m_byteIdx = 0; + m_bitIdx = 0; + m_bitCount = m_bitCountTotal; // Reset to allow retransmission + m_nrziBit = 1; + if (m_settings.m_rampUpBits == 0) + { + m_state = tx; + m_pow = 0.0f; + } + else + { + m_state = ramp_up; + m_pow = -(Real)m_settings.m_rampRange; + m_powRamp = m_settings.m_rampRange/(m_settings.m_rampUpBits * (Real)m_samplesPerSymbol); + } +} + +void AISModSource::addTXPacket(const QString& data) +{ + QByteArray ba = QByteArray::fromHex(data.toUtf8()); + addTXPacket(ba); +} + +void AISModSource::addTXPacket(QByteArray data) +{ + uint8_t packet[AIS_MAX_BYTES]; + uint8_t *crc_start; + uint8_t *packet_end; + uint8_t *p; + crc16x25 crc; + uint16_t crcValue; + int packet_length; + + // Create AIS message + p = packet; + // Training + *p++ = AIS_TRAIN; + *p++ = AIS_TRAIN; + *p++ = AIS_TRAIN; + // Flag + *p++ = AIS_FLAG; + crc_start = p; + // Copy packet payload + for (int i = 0; i < data.size(); i++) + *p++ = data[i]; + // CRC (do not include flags) + crc.calculate(crc_start, p-crc_start); + crcValue = crc.get(); + *p++ = crcValue & 0xff; + *p++ = (crcValue >> 8); + packet_end = p; + // Flag + *p++ = AIS_FLAG; + // Buffer + *p++ = 0; + + packet_length = p-&packet[0]; + + encodePacket(packet, packet_length, crc_start, packet_end); +} + +void AISModSource::encodePacket(uint8_t *packet, int packet_length, uint8_t *crc_start, uint8_t *packet_end) +{ + // HDLC bit stuffing + m_byteIdx = 0; + m_bitIdx = 0; + m_last5Bits = 0; + m_bitCount = 0; + m_bitCountTotal = 0; + for (int i = 0; i < packet_length; i++) + { + for (int j = 0; j < 8; j++) + { + int tx_bit = (packet[i] >> j) & 1; + // Stuff 0 if last 5 bits are 1s, unless transmitting flag + // Except for special case of when last 5 bits of CRC are 1s + if ( ( (packet[i] != AIS_FLAG) + || ( (&packet[i] >= crc_start) + && ( (&packet[i] < packet_end) + || ((&packet[i] == packet_end) && (j == 0)) + ) + ) + ) + && (m_last5Bits == 0x1f) + ) + addBit(0); + addBit(tx_bit); + } + } + //m_samplesPerSymbol = AISMOD_SAMPLE_RATE / m_settings.m_baud; + m_packetRepeatCount = m_settings.m_repeatCount; + initTX(); + // Only reset phases at start of new packet TX, not in initTX(), so that + // there isn't a discontinuity in phase when repeatedly transmitting a + // single tone + m_sampleIdx = 0; + m_fmPhase = 0.0; + + if (m_settings.m_writeToFile) + m_iqFile.open("aismod.csv", std::ofstream::out); + else if (m_iqFile.is_open()) + m_iqFile.close(); +} diff --git a/plugins/channeltx/modais/aismodsource.h b/plugins/channeltx/modais/aismodsource.h new file mode 100644 index 000000000..fd1954fa2 --- /dev/null +++ b/plugins/channeltx/modais/aismodsource.h @@ -0,0 +1,147 @@ +/////////////////////////////////////////////////////////////////////////////////// +// Copyright (C) 2019 Edouard Griffiths, F4EXB // +// Copyright (C) 2020 Jon Beniston, M7RCE // +// // +// This program is free software; you can redistribute it and/or modify // +// it under the terms of the GNU General Public License as published by // +// the Free Software Foundation as version 3 of the License, or // +// (at your option) any later version. // +// // +// This program is distributed in the hope that it will be useful, // +// but WITHOUT ANY WARRANTY; without even the implied warranty of // +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // +// GNU General Public License V3 for more details. // +// // +// You should have received a copy of the GNU General Public License // +// along with this program. If not, see . // +/////////////////////////////////////////////////////////////////////////////////// + +#ifndef INCLUDE_AISMODSOURCE_H +#define INCLUDE_AISMODSOURCE_H + +#include +#include +#include + +#include +#include + +#include "dsp/channelsamplesource.h" +#include "dsp/nco.h" +#include "dsp/ncof.h" +#include "dsp/interpolator.h" +#include "dsp/firfilter.h" +#include "dsp/gaussian.h" +#include "util/movingaverage.h" + +#include "aismodsettings.h" + +// Train, flag, data, crc, flag and a zero for ramp down. Longest message is 1008/8=126 bytes +#define AIS_MAX_BYTES (3+1+126+2+1+1) +// Add extra space for bit stuffing +#define AIS_MAX_BITS (AIS_MAX_BYTES*2) +#define AIS_TRAIN 0x55 +#define AIS_FLAG 0x7e + +// Sample rate is multiple of 9600 baud rate (use even multiple so Gausian filter has odd number of taps) +// Is there any benefit to having this higher? +#define AISMOD_SAMPLE_RATE (9600*6) + +class BasebandSampleSink; +class ChannelAPI; + +class AISModSource : public ChannelSampleSource +{ +public: + AISModSource(); + virtual ~AISModSource(); + + virtual void pull(SampleVector::iterator begin, unsigned int nbSamples); + virtual void pullOne(Sample& sample); + virtual void prefetch(unsigned int nbSamples) { (void) nbSamples; } + + double getMagSq() const { return m_magsq; } + void getLevels(qreal& rmsLevel, qreal& peakLevel, int& numSamples) const + { + rmsLevel = m_rmsLevel; + peakLevel = m_peakLevelOut; + numSamples = m_levelNbSamples; + } + void setSpectrumSink(BasebandSampleSink *sampleSink) { m_spectrumSink = sampleSink; } + void setScopeSink(BasebandSampleSink* scopeSink) { m_scopeSink = scopeSink; } + void applySettings(const AISModSettings& settings, bool force = false); + void applyChannelSettings(int channelSampleRate, int channelFrequencyOffset, bool force = false); + void addTXPacket(const QString& data); + void addTXPacket(QByteArray data); + void encodePacket(uint8_t *packet, int packet_length, uint8_t *crc_start, uint8_t *packet_end); + void setChannel(ChannelAPI *channel) { m_channel = channel; } + +private: + int m_channelSampleRate; + int m_channelFrequencyOffset; + AISModSettings m_settings; + ChannelAPI *m_channel; + + NCO m_carrierNco; + double m_fmPhase; // Double gives cleaner spectrum than Real + double m_phaseSensitivity; + Real m_linearGain; + Complex m_modSample; + + int m_nrziBit; // Output of NRZI coder + Gaussian m_pulseShape; // Pulse shaping filter + + BasebandSampleSink* m_spectrumSink; // Spectrum GUI to display baseband waveform + BasebandSampleSink* m_scopeSink; // Scope GUI to display baseband waveform + SampleVector m_sampleBuffer; + + Interpolator m_interpolator; // Interpolator to channel sample rate + Real m_interpolatorDistance; + Real m_interpolatorDistanceRemain; + + double m_magsq; + MovingAverageUtil m_movingAverage; + + quint32 m_levelCalcCount; + qreal m_rmsLevel; + qreal m_peakLevelOut; + Real m_peakLevel; + Real m_levelSum; + + static const int m_levelNbSamples = 480; // every 10ms assuming 48k Sa/s + + int m_sampleIdx; // Sample index in to symbol + int m_samplesPerSymbol; // Number of samples per symbol + Real m_pow; // In dB + Real m_powRamp; // In dB + enum AISModState { + idle, ramp_up, tx, ramp_down, wait + } m_state; // States for sample modulation + int m_packetRepeatCount; + uint64_t m_waitCounter; // Samples to wait before retransmission + + uint8_t m_bits[AIS_MAX_BITS]; // HDLC encoded bits to transmit + int m_byteIdx; // Index in to m_bits + int m_bitIdx; // Index in to current byte of m_bits + int m_last5Bits; // Last 5 bits to be HDLC encoded + int m_bitCount; // Count of number of valid bits in m_bits + int m_bitCountTotal; + + std::ofstream m_iqFile; // For debug output of baseband waveform + + QVector m_demodBuffer; + int m_demodBufferFill; + + bool bitsValid(); // Are there and bits to transmit + int getBit(); // Get bit from m_bits + void addBit(int bit); // Add bit to m_bits, with zero stuffing + void initTX(); + + void calculateLevel(Real& sample); + void modulateSample(); + void sampleToSpectrum(Complex sample); + void sampleToScope(Complex sample); + +}; + +#endif // INCLUDE_AISMODSOURCE_H diff --git a/plugins/channeltx/modais/aismodtxsettingsdialog.cpp b/plugins/channeltx/modais/aismodtxsettingsdialog.cpp new file mode 100644 index 000000000..9612afdfb --- /dev/null +++ b/plugins/channeltx/modais/aismodtxsettingsdialog.cpp @@ -0,0 +1,54 @@ +/////////////////////////////////////////////////////////////////////////////////// +// Copyright (C) 2020 Jon Beniston, M7RCE // +// // +// This program is free software; you can redistribute it and/or modify // +// it under the terms of the GNU General Public License as published by // +// the Free Software Foundation as version 3 of the License, or // +// (at your option) any later version. // +// // +// This program is distributed in the hope that it will be useful, // +// but WITHOUT ANY WARRANTY; without even the implied warranty of // +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // +// GNU General Public License V3 for more details. // +// // +// You should have received a copy of the GNU General Public License // +// along with this program. If not, see . // +/////////////////////////////////////////////////////////////////////////////////// + +#include "aismodtxsettingsdialog.h" + +AISModTXSettingsDialog::AISModTXSettingsDialog(int rampUpBits, int rampDownBits, + int rampRange, + int baud, int symbolSpan, + bool rfNoise, bool writeToFile, + QWidget* parent) : + QDialog(parent), + ui(new Ui::AISModTXSettingsDialog) +{ + ui->setupUi(this); + ui->rampUp->setValue(rampUpBits); + ui->rampDown->setValue(rampDownBits); + ui->rampRange->setValue(rampRange); + ui->baud->setValue(baud); + ui->symbolSpan->setValue(symbolSpan); + ui->rfNoise->setChecked(rfNoise); + ui->writeToFile->setChecked(writeToFile); +} + +AISModTXSettingsDialog::~AISModTXSettingsDialog() +{ + delete ui; +} + +void AISModTXSettingsDialog::accept() +{ + m_rampUpBits = ui->rampUp->value(); + m_rampDownBits = ui->rampDown->value(); + m_rampRange = ui->rampRange->value(); + m_baud = ui->baud->value(); + m_symbolSpan = ui->symbolSpan->value(); + m_rfNoise = ui->rfNoise->isChecked(); + m_writeToFile = ui->writeToFile->isChecked(); + + QDialog::accept(); +} diff --git a/plugins/channeltx/modais/aismodtxsettingsdialog.h b/plugins/channeltx/modais/aismodtxsettingsdialog.h new file mode 100644 index 000000000..28500d6fd --- /dev/null +++ b/plugins/channeltx/modais/aismodtxsettingsdialog.h @@ -0,0 +1,48 @@ +/////////////////////////////////////////////////////////////////////////////////// +// Copyright (C) 2020 Jon Beniston, M7RCE // +// // +// This program is free software; you can redistribute it and/or modify // +// it under the terms of the GNU General Public License as published by // +// the Free Software Foundation as version 3 of the License, or // +// (at your option) any later version. // +// // +// This program is distributed in the hope that it will be useful, // +// but WITHOUT ANY WARRANTY; without even the implied warranty of // +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // +// GNU General Public License V3 for more details. // +// // +// You should have received a copy of the GNU General Public License // +// along with this program. If not, see . // +/////////////////////////////////////////////////////////////////////////////////// + +#ifndef INCLUDE_AISMODTXSETTINGSDIALOG_H +#define INCLUDE_AISMODTXSETTINGSDIALOG_H + +#include "ui_aismodtxsettingsdialog.h" + +class AISModTXSettingsDialog : public QDialog { + Q_OBJECT + +public: + explicit AISModTXSettingsDialog(int rampUpBits, int rampDownBits, int rampRange, + int baud, int symbolSpan, + bool rfNoise, bool writeToFile, + QWidget* parent = 0); + ~AISModTXSettingsDialog(); + + int m_rampUpBits; + int m_rampDownBits; + int m_rampRange; + int m_baud; + int m_symbolSpan; + bool m_rfNoise; + bool m_writeToFile; + +private slots: + void accept(); + +private: + Ui::AISModTXSettingsDialog* ui; +}; + +#endif // INCLUDE_AISMODTXSETTINGSDIALOG_H diff --git a/plugins/channeltx/modais/aismodtxsettingsdialog.ui b/plugins/channeltx/modais/aismodtxsettingsdialog.ui new file mode 100644 index 000000000..f2cff93d8 --- /dev/null +++ b/plugins/channeltx/modais/aismodtxsettingsdialog.ui @@ -0,0 +1,211 @@ + + + AISModTXSettingsDialog + + + + 0 + 0 + 351 + 321 + + + + + Liberation Sans + 9 + + + + Packet TX Extra Settings + + + + + + Modulation + + + + + + Baud rate + + + + + + + Baud rate (symbols per second). + + + 100000 + + + 9600 + + + + + + + Filter symbol span + + + + + + + Number of symbols over which filter is applied + + + 1 + + + 20 + + + 3 + + + + + + + + + + Power Ramping + + + + + + Ramp up bits + + + + + + + Number of bits at start of frame during which output power is ramped up. + + + + + + + Ramp down bits + + + + + + + Number of bits at end of frame during which output power is ramped down. + + + + + + + Ramp range (dB) + + + + + + + Range in dB over which power is ramped up or down. E.g. a value of 60 causes power to be ramped from -60dB to 0dB. + + + 120 + + + + + + + + + + Debug + + + + + + Generate white noise as RF signal. + + + Generate RF noise + + + + + + + + 0 + 0 + + + + Write baseband signal to a CSV file named aismod.csv + + + Write baseband to CSV + + + + + + + + + + Qt::Horizontal + + + QDialogButtonBox::Cancel|QDialogButtonBox::Ok + + + + + + + + + buttonBox + accepted() + AISModTXSettingsDialog + accept() + + + 248 + 254 + + + 157 + 274 + + + + + buttonBox + rejected() + AISModTXSettingsDialog + reject() + + + 316 + 260 + + + 286 + 274 + + + + + diff --git a/plugins/channeltx/modais/aismodwebapiadapter.cpp b/plugins/channeltx/modais/aismodwebapiadapter.cpp new file mode 100644 index 000000000..176dec150 --- /dev/null +++ b/plugins/channeltx/modais/aismodwebapiadapter.cpp @@ -0,0 +1,53 @@ +/////////////////////////////////////////////////////////////////////////////////// +// Copyright (C) 2019 Edouard Griffiths, F4EXB. // +// Copyright (C) 2020 Jon Beniston, M7RCE // +// // +// This program is free software; you can redistribute it and/or modify // +// it under the terms of the GNU General Public License as published by // +// the Free Software Foundation as version 3 of the License, or // +// (at your option) any later version. // +// // +// This program is distributed in the hope that it will be useful, // +// but WITHOUT ANY WARRANTY; without even the implied warranty of // +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // +// GNU General Public License V3 for more details. // +// // +// You should have received a copy of the GNU General Public License // +// along with this program. If not, see . // +/////////////////////////////////////////////////////////////////////////////////// + +#include "SWGChannelSettings.h" +#include "aismod.h" +#include "aismodwebapiadapter.h" + +AISModWebAPIAdapter::AISModWebAPIAdapter() +{} + +AISModWebAPIAdapter::~AISModWebAPIAdapter() +{} + +int AISModWebAPIAdapter::webapiSettingsGet( + SWGSDRangel::SWGChannelSettings& response, + QString& errorMessage) +{ + (void) errorMessage; + response.setAisModSettings(new SWGSDRangel::SWGAISModSettings()); + response.getAisModSettings()->init(); + AISMod::webapiFormatChannelSettings(response, m_settings); + + return 200; +} + +int AISModWebAPIAdapter::webapiSettingsPutPatch( + bool force, + const QStringList& channelSettingsKeys, + SWGSDRangel::SWGChannelSettings& response, + QString& errorMessage) +{ + (void) force; // no action + (void) errorMessage; + AISMod::webapiUpdateChannelSettings(m_settings, channelSettingsKeys, response); + + AISMod::webapiFormatChannelSettings(response, m_settings); + return 200; +} diff --git a/plugins/channeltx/modais/aismodwebapiadapter.h b/plugins/channeltx/modais/aismodwebapiadapter.h new file mode 100644 index 000000000..ed04b918a --- /dev/null +++ b/plugins/channeltx/modais/aismodwebapiadapter.h @@ -0,0 +1,50 @@ +/////////////////////////////////////////////////////////////////////////////////// +// Copyright (C) 2019 Edouard Griffiths, F4EXB. // +// Copyright (C) 2020 Jon Beniston, M7RCE // +// // +// This program is free software; you can redistribute it and/or modify // +// it under the terms of the GNU General Public License as published by // +// the Free Software Foundation as version 3 of the License, or // +// (at your option) any later version. // +// // +// This program is distributed in the hope that it will be useful, // +// but WITHOUT ANY WARRANTY; without even the implied warranty of // +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // +// GNU General Public License V3 for more details. // +// // +// You should have received a copy of the GNU General Public License // +// along with this program. If not, see . // +/////////////////////////////////////////////////////////////////////////////////// + +#ifndef INCLUDE_AISMOD_WEBAPIADAPTER_H +#define INCLUDE_AISMOD_WEBAPIADAPTER_H + +#include "channel/channelwebapiadapter.h" +#include "aismodsettings.h" + +/** + * Standalone API adapter only for the settings + */ +class AISModWebAPIAdapter : public ChannelWebAPIAdapter { +public: + AISModWebAPIAdapter(); + virtual ~AISModWebAPIAdapter(); + + virtual QByteArray serialize() const { return m_settings.serialize(); } + virtual bool deserialize(const QByteArray& data) { return m_settings.deserialize(data); } + + virtual int webapiSettingsGet( + SWGSDRangel::SWGChannelSettings& response, + QString& errorMessage); + + virtual int webapiSettingsPutPatch( + bool force, + const QStringList& channelSettingsKeys, + SWGSDRangel::SWGChannelSettings& response, + QString& errorMessage); + +private: + AISModSettings m_settings; +}; + +#endif // INCLUDE_AISMOD_WEBAPIADAPTER_H diff --git a/plugins/channeltx/modais/readme.md b/plugins/channeltx/modais/readme.md new file mode 100644 index 000000000..59291653a --- /dev/null +++ b/plugins/channeltx/modais/readme.md @@ -0,0 +1,128 @@ +

    AIS modulator plugin

    + +

    Introduction

    + +This plugin can be used to transmit AIS (Automatic Identification System) messages using GMSK/FM modulation. AIS is used to track ships and other marine vessels at sea. + +You need an AIS license to transmit on the AIS VHF frequencies (161.975MHz and 162.025MHz). This plugin should not therefore be used on those frequencies unless the transmitter and receiver are directly connected via coax. If you have an amateur license, you should be able to transmit AIS within amateur bands. + +

    Interface

    + +![AIS Modulator plugin GUI](../../../doc/img/AISMod_plugin.png) + +

    1: Frequency shift from center frequency of transmission

    + +Use the wheels to adjust the frequency shift in Hz from the center frequency of transmission. Left click on a digit sets the cursor position at this digit. Right click on a digit sets all digits on the right to zero. This effectively floors value at the digit position. Wheels are moved with the mousewheel while pointing at the wheel or by selecting the wheel with the left mouse click and using the keyboard arrows. Pressing shift simultaneously moves digit by 5 and pressing control moves it by 2. + +

    2: Channel power

    + +Average total power in dB relative to a +/- 1.0 amplitude signal generated in the pass band. + +

    3: Channel mute

    + +Use this button to toggle mute for this channel. + +

    4: Mode

    + +Allows setting of RF bandwidth, FM deviation and BT values according to the chosen mode, which can be Narrow (BW=12.5kHz, Dev=2.4k, BT=0.3) or Wide (BW=25kHz, Dev=4.8k, BT=0.4). The latest specification for AIS, ITU-R M.1371-5, only specifies Wide operation. + +

    5: RF Bandwidth

    + +This specifies the bandwidth of a LPF that is applied to the output signal to limit the RF bandwidth. Typically this should be 25kHz. + +

    6: FM Deviation

    + +This specifies the maximum frequency deviation. Typically this should be 4.8kHz, giving a modulation index of 0.5 at 9,600 baud. + +

    7: BT Bandwidth

    + +Bandwidth-time product for the Gaussian filter, used for GMSK modulation. This should typically be 0.4. + +

    8: Gain

    + +Adjusts the gain in dB from -60 to 0dB. The gain should be set to ensure the level meter remains below 100%. + +

    9: Level meter in %

    + + - top bar (beige): average value + - bottom bar (brown): instantaneous peak value + - tip vertical bar (bright red): peak hold value + +

    10: UDP

    + +When checked, a UDP port is opened to receive messages from other features or applications that will be transmitted. These messages do not need to contain the CRC, as it is appended automatically. + +

    11: UDP address

    + +IP address of the interface open the UDP port on, to receive messages to be transmitted. + +

    12: UDP port

    + +UDP port number to receive messages to be transmitted on. + +

    13: Encode

    + +When pressed, the message field will be set to a hex encoded string that represents the message determined by the following controls. + +

    14: Message Type

    + +Select a message type: + + - Scheduled postion report + - Assigned position report + - Special position report + - Base station report + +

    15: MMSI

    + +Enter a 9 digit Maritime Mobile Service Identity, which uniquely identifies a vessel. + +

    16: Status

    + +For position reports, specify the status of the vessel. + +

    17: Latitude

    + +Specifiy the latitude of the vessel or station in decimal degrees, North positive. + +

    18: Longitude

    + +Specifiy the longitude of the vessel or station in decimal degrees, East positive. + +

    19: Insert position

    + +Sets the latitude and longitude fields to the values specified under Preferences > My position. + +

    20: Course

    + +For position reports, specify the vessel's course in degrees. This is the direction in which the vessel is moving. + +

    21: Speed

    + +For position reports, specify the vessel's speed in knots. + +

    22: Heading

    + +For position reports, specify the vessel's heading. This is the direction the vessel is pointing towards. + +

    23: Message

    + +The AIS message send. This should be a hex encoded string. + +

    24: Repeat

    + +Check this button to repeatedly transmit a message. Right click to open the dialog to adjust the delay between retransmission and number of times the message should be repeated. + +

    25: TX

    + +Transmits the message. + +

    API

    + +Full details of the API can be found in the Swagger documentation. Here is a quick example of how to transmit a message from the command line: + + curl -X POST "http://127.0.0.1:8091/sdrangel/deviceset/1/channel/0/actions" -d '{"channelType": "AISMod", "direction": 1, "AISModActions": { "tx": { "data": "000000000000000000000000000000000" }}}' + +Or to set the FM deviation: + + curl -X PATCH "http://127.0.0.1:8091/sdrangel/deviceset/1/channel/0/settings" -d '{"channelType": "AISMod", "direction": 1, "AISModSettings": {"fmDeviation": 4800}}' diff --git a/plugins/feature/CMakeLists.txt b/plugins/feature/CMakeLists.txt index 2e95e860d..6bc185fbf 100644 --- a/plugins/feature/CMakeLists.txt +++ b/plugins/feature/CMakeLists.txt @@ -12,6 +12,7 @@ if (Qt5Quick_FOUND AND Qt5QuickWidgets_FOUND AND Qt5Positioning_FOUND) endif() add_subdirectory(afc) +add_subdirectory(ais) add_subdirectory(aprs) add_subdirectory(demodanalyzer) add_subdirectory(pertester) diff --git a/plugins/feature/ais/CMakeLists.txt b/plugins/feature/ais/CMakeLists.txt new file mode 100644 index 000000000..36f0415d2 --- /dev/null +++ b/plugins/feature/ais/CMakeLists.txt @@ -0,0 +1,55 @@ +project(ais) + +set(ais_SOURCES + ais.cpp + aissettings.cpp + aisplugin.cpp + aiswebapiadapter.cpp +) + +set(ais_HEADERS + ais.h + aissettings.h + aisplugin.h + aiswebapiadapter.h +) + +include_directories( + ${CMAKE_SOURCE_DIR}/swagger/sdrangel/code/qt5/client +) + +if(NOT SERVER_MODE) + set(ais_SOURCES + ${ais_SOURCES} + aisgui.cpp + aisgui.ui + ais.qrc + ) + set(ais_HEADERS + ${ais_HEADERS} + aisgui.h + ) + + set(TARGET_NAME featureais) + set(TARGET_LIB Qt5::Widgets) + set(TARGET_LIB_GUI "sdrgui") + set(INSTALL_FOLDER ${INSTALL_PLUGINS_DIR}) +else() + set(TARGET_NAME featureaissrv) + set(TARGET_LIB "") + set(TARGET_LIB_GUI "") + set(INSTALL_FOLDER ${INSTALL_PLUGINSSRV_DIR}) +endif() + +add_library(${TARGET_NAME} SHARED + ${ais_SOURCES} +) + +target_link_libraries(${TARGET_NAME} + Qt5::Core + ${TARGET_LIB} + sdrbase + ${TARGET_LIB_GUI} +) + +install(TARGETS ${TARGET_NAME} DESTINATION ${INSTALL_FOLDER}) diff --git a/plugins/feature/ais/ais.cpp b/plugins/feature/ais/ais.cpp new file mode 100644 index 000000000..d0b1ee96c --- /dev/null +++ b/plugins/feature/ais/ais.cpp @@ -0,0 +1,301 @@ +/////////////////////////////////////////////////////////////////////////////////// +// Copyright (C) 2020 Jon Beniston, M7RCE // +// Copyright (C) 2020 Edouard Griffiths, F4EXB // +// // +// This program is free software; you can redistribute it and/or modify // +// it under the terms of the GNU General Public License as published by // +// the Free Software Foundation as version 3 of the License, or // +// (at your option) any later version. // +// // +// This program is distributed in the hope that it will be useful, // +// but WITHOUT ANY WARRANTY; without even the implied warranty of // +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // +// GNU General Public License V3 for more details. // +// // +// You should have received a copy of the GNU General Public License // +// along with this program. If not, see . // +/////////////////////////////////////////////////////////////////////////////////// + +#include +#include +#include +#include +#include + +#include "SWGFeatureSettings.h" +#include "SWGFeatureReport.h" +#include "SWGFeatureActions.h" +#include "SWGDeviceState.h" + +#include "dsp/dspengine.h" +#include "device/deviceset.h" +#include "channel/channelapi.h" +#include "feature/featureset.h" +#include "maincore.h" + +#include "ais.h" + +MESSAGE_CLASS_DEFINITION(AIS::MsgConfigureAIS, Message) + +const char* const AIS::m_featureIdURI = "sdrangel.feature.ais"; +const char* const AIS::m_featureId = "AIS"; + +AIS::AIS(WebAPIAdapterInterface *webAPIAdapterInterface) : + Feature(m_featureIdURI, webAPIAdapterInterface) +{ + qDebug("AIS::AIS: webAPIAdapterInterface: %p", webAPIAdapterInterface); + setObjectName(m_featureId); + m_state = StIdle; + m_errorMessage = "AIS error"; + connect(&m_updatePipesTimer, SIGNAL(timeout()), this, SLOT(updatePipes())); + m_updatePipesTimer.start(1000); + m_networkManager = new QNetworkAccessManager(); + connect(m_networkManager, SIGNAL(finished(QNetworkReply*)), this, SLOT(networkManagerFinished(QNetworkReply*))); +} + +AIS::~AIS() +{ + disconnect(m_networkManager, SIGNAL(finished(QNetworkReply*)), this, SLOT(networkManagerFinished(QNetworkReply*))); + delete m_networkManager; +} + +void AIS::start() +{ + qDebug("AIS::start"); + m_state = StRunning; +} + +void AIS::stop() +{ + qDebug("AIS::stop"); + m_state = StIdle; +} + +bool AIS::handleMessage(const Message& cmd) +{ + if (MsgConfigureAIS::match(cmd)) + { + MsgConfigureAIS& cfg = (MsgConfigureAIS&) cmd; + qDebug() << "AIS::handleMessage: MsgConfigureAIS"; + applySettings(cfg.getSettings(), cfg.getForce()); + + return true; + } + else if (MainCore::MsgPacket::match(cmd)) + { + MainCore::MsgPacket& report = (MainCore::MsgPacket&) cmd; + if (getMessageQueueToGUI()) + { + MainCore::MsgPacket *copy = new MainCore::MsgPacket(report); + getMessageQueueToGUI()->push(copy); + } + return true; + } + else + { + return false; + } +} + +void AIS::updatePipes() +{ + QList availablePipes = updateAvailablePipeSources("ais", AISSettings::m_pipeTypes, AISSettings::m_pipeURIs, this); + + if (availablePipes != m_availablePipes) { + m_availablePipes = availablePipes; + } +} + +QByteArray AIS::serialize() const +{ + return m_settings.serialize(); +} + +bool AIS::deserialize(const QByteArray& data) +{ + if (m_settings.deserialize(data)) + { + MsgConfigureAIS *msg = MsgConfigureAIS::create(m_settings, true); + m_inputMessageQueue.push(msg); + return true; + } + else + { + m_settings.resetToDefaults(); + MsgConfigureAIS *msg = MsgConfigureAIS::create(m_settings, true); + m_inputMessageQueue.push(msg); + return false; + } +} + +void AIS::applySettings(const AISSettings& settings, bool force) +{ + qDebug() << "AIS::applySettings:" + << " m_title: " << settings.m_title + << " m_rgbColor: " << settings.m_rgbColor + << " m_useReverseAPI: " << settings.m_useReverseAPI + << " m_reverseAPIAddress: " << settings.m_reverseAPIAddress + << " m_reverseAPIPort: " << settings.m_reverseAPIPort + << " m_reverseAPIFeatureSetIndex: " << settings.m_reverseAPIFeatureSetIndex + << " m_reverseAPIFeatureIndex: " << settings.m_reverseAPIFeatureIndex + << " force: " << force; + + QList reverseAPIKeys; + + if ((m_settings.m_title != settings.m_title) || force) { + reverseAPIKeys.append("title"); + } + if ((m_settings.m_rgbColor != settings.m_rgbColor) || force) { + reverseAPIKeys.append("rgbColor"); + } + + if (settings.m_useReverseAPI) + { + bool fullUpdate = ((m_settings.m_useReverseAPI != settings.m_useReverseAPI) && settings.m_useReverseAPI) || + (m_settings.m_reverseAPIAddress != settings.m_reverseAPIAddress) || + (m_settings.m_reverseAPIPort != settings.m_reverseAPIPort) || + (m_settings.m_reverseAPIFeatureSetIndex != settings.m_reverseAPIFeatureSetIndex) || + (m_settings.m_reverseAPIFeatureIndex != settings.m_reverseAPIFeatureIndex); + webapiReverseSendSettings(reverseAPIKeys, settings, fullUpdate || force); + } + + m_settings = settings; +} + +int AIS::webapiSettingsGet( + SWGSDRangel::SWGFeatureSettings& response, + QString& errorMessage) +{ + (void) errorMessage; + response.setAisSettings(new SWGSDRangel::SWGAISSettings()); + response.getAisSettings()->init(); + webapiFormatFeatureSettings(response, m_settings); + return 200; +} + +int AIS::webapiSettingsPutPatch( + bool force, + const QStringList& featureSettingsKeys, + SWGSDRangel::SWGFeatureSettings& response, + QString& errorMessage) +{ + (void) errorMessage; + AISSettings settings = m_settings; + webapiUpdateFeatureSettings(settings, featureSettingsKeys, response); + + MsgConfigureAIS *msg = MsgConfigureAIS::create(settings, force); + m_inputMessageQueue.push(msg); + + if (m_guiMessageQueue) // forward to GUI if any + { + MsgConfigureAIS *msgToGUI = MsgConfigureAIS::create(settings, force); + m_guiMessageQueue->push(msgToGUI); + } + + webapiFormatFeatureSettings(response, settings); + + return 200; +} + +void AIS::webapiFormatFeatureSettings( + SWGSDRangel::SWGFeatureSettings& response, + const AISSettings& settings) +{ + if (response.getAisSettings()->getTitle()) { + *response.getAisSettings()->getTitle() = settings.m_title; + } else { + response.getAisSettings()->setTitle(new QString(settings.m_title)); + } + + response.getAisSettings()->setRgbColor(settings.m_rgbColor); + response.getAisSettings()->setUseReverseApi(settings.m_useReverseAPI ? 1 : 0); + + if (response.getAisSettings()->getReverseApiAddress()) { + *response.getAisSettings()->getReverseApiAddress() = settings.m_reverseAPIAddress; + } else { + response.getAisSettings()->setReverseApiAddress(new QString(settings.m_reverseAPIAddress)); + } + + response.getAisSettings()->setReverseApiPort(settings.m_reverseAPIPort); +} + +void AIS::webapiUpdateFeatureSettings( + AISSettings& settings, + const QStringList& featureSettingsKeys, + SWGSDRangel::SWGFeatureSettings& response) +{ + if (featureSettingsKeys.contains("title")) { + settings.m_title = *response.getAisSettings()->getTitle(); + } + if (featureSettingsKeys.contains("rgbColor")) { + settings.m_rgbColor = response.getAisSettings()->getRgbColor(); + } + if (featureSettingsKeys.contains("useReverseAPI")) { + settings.m_useReverseAPI = response.getAisSettings()->getUseReverseApi() != 0; + } + if (featureSettingsKeys.contains("reverseAPIAddress")) { + settings.m_reverseAPIAddress = *response.getAisSettings()->getReverseApiAddress(); + } + if (featureSettingsKeys.contains("reverseAPIPort")) { + settings.m_reverseAPIPort = response.getAisSettings()->getReverseApiPort(); + } +} + +void AIS::webapiReverseSendSettings(QList& featureSettingsKeys, const AISSettings& settings, bool force) +{ + SWGSDRangel::SWGFeatureSettings *swgFeatureSettings = new SWGSDRangel::SWGFeatureSettings(); + // swgFeatureSettings->setOriginatorFeatureIndex(getIndexInDeviceSet()); + // swgFeatureSettings->setOriginatorFeatureSetIndex(getDeviceSetIndex()); + swgFeatureSettings->setFeatureType(new QString("AIS")); + swgFeatureSettings->setAisSettings(new SWGSDRangel::SWGAISSettings()); + SWGSDRangel::SWGAISSettings *swgAISSettings = swgFeatureSettings->getAisSettings(); + + // transfer data that has been modified. When force is on transfer all data except reverse API data + + if (featureSettingsKeys.contains("title") || force) { + swgAISSettings->setTitle(new QString(settings.m_title)); + } + if (featureSettingsKeys.contains("rgbColor") || force) { + swgAISSettings->setRgbColor(settings.m_rgbColor); + } + + QString channelSettingsURL = QString("http://%1:%2/sdrangel/featureset/%3/feature/%4/settings") + .arg(settings.m_reverseAPIAddress) + .arg(settings.m_reverseAPIPort) + .arg(settings.m_reverseAPIFeatureSetIndex) + .arg(settings.m_reverseAPIFeatureIndex); + m_networkRequest.setUrl(QUrl(channelSettingsURL)); + m_networkRequest.setHeader(QNetworkRequest::ContentTypeHeader, "application/json"); + + QBuffer *buffer = new QBuffer(); + buffer->open((QBuffer::ReadWrite)); + buffer->write(swgFeatureSettings->asJson().toUtf8()); + buffer->seek(0); + + // Always use PATCH to avoid passing reverse API settings + QNetworkReply *reply = m_networkManager->sendCustomRequest(m_networkRequest, "PATCH", buffer); + buffer->setParent(reply); + + delete swgFeatureSettings; +} + +void AIS::networkManagerFinished(QNetworkReply *reply) +{ + QNetworkReply::NetworkError replyError = reply->error(); + + if (replyError) + { + qWarning() << "AIS::networkManagerFinished:" + << " error(" << (int) replyError + << "): " << replyError + << ": " << reply->errorString(); + } + else + { + QString answer = reply->readAll(); + answer.chop(1); // remove last \n + qDebug("AIS::networkManagerFinished: reply:\n%s", answer.toStdString().c_str()); + } + + reply->deleteLater(); +} diff --git a/plugins/feature/ais/ais.h b/plugins/feature/ais/ais.h new file mode 100644 index 000000000..4e83fd970 --- /dev/null +++ b/plugins/feature/ais/ais.h @@ -0,0 +1,116 @@ +/////////////////////////////////////////////////////////////////////////////////// +// Copyright (C) 2020 Jon Beniston, M7RCE // +// Copyright (C) 2020 Edouard Griffiths, F4EXB // +// // +// This program is free software; you can redistribute it and/or modify // +// it under the terms of the GNU General Public License as published by // +// the Free Software Foundation as version 3 of the License, or // +// (at your option) any later version. // +// // +// This program is distributed in the hope that it will be useful, // +// but WITHOUT ANY WARRANTY; without even the implied warranty of // +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // +// GNU General Public License V3 for more details. // +// // +// You should have received a copy of the GNU General Public License // +// along with this program. If not, see . // +/////////////////////////////////////////////////////////////////////////////////// + +#ifndef INCLUDE_FEATURE_AIS_H_ +#define INCLUDE_FEATURE_AIS_H_ + +#include +#include +#include + +#include "feature/feature.h" +#include "util/message.h" + +#include "aissettings.h" + +class WebAPIAdapterInterface; +class QNetworkAccessManager; +class QNetworkReply; + +namespace SWGSDRangel { + class SWGDeviceState; +} + +class AIS : public Feature +{ + Q_OBJECT +public: + class MsgConfigureAIS : public Message { + MESSAGE_CLASS_DECLARATION + + public: + const AISSettings& getSettings() const { return m_settings; } + bool getForce() const { return m_force; } + + static MsgConfigureAIS* create(const AISSettings& settings, bool force) { + return new MsgConfigureAIS(settings, force); + } + + private: + AISSettings m_settings; + bool m_force; + + MsgConfigureAIS(const AISSettings& settings, bool force) : + Message(), + m_settings(settings), + m_force(force) + { } + }; + + AIS(WebAPIAdapterInterface *webAPIAdapterInterface); + virtual ~AIS(); + virtual void destroy() { delete this; } + virtual bool handleMessage(const Message& cmd); + + virtual void getIdentifier(QString& id) const { id = objectName(); } + virtual void getTitle(QString& title) const { title = m_settings.m_title; } + + virtual QByteArray serialize() const; + virtual bool deserialize(const QByteArray& data); + + virtual int webapiSettingsGet( + SWGSDRangel::SWGFeatureSettings& response, + QString& errorMessage); + + virtual int webapiSettingsPutPatch( + bool force, + const QStringList& featureSettingsKeys, + SWGSDRangel::SWGFeatureSettings& response, + QString& errorMessage); + + static void webapiFormatFeatureSettings( + SWGSDRangel::SWGFeatureSettings& response, + const AISSettings& settings); + + static void webapiUpdateFeatureSettings( + AISSettings& settings, + const QStringList& featureSettingsKeys, + SWGSDRangel::SWGFeatureSettings& response); + + static const char* const m_featureIdURI; + static const char* const m_featureId; + +private: + AISSettings m_settings; + QList m_availablePipes; + QTimer m_updatePipesTimer; + + QNetworkAccessManager *m_networkManager; + QNetworkRequest m_networkRequest; + + void start(); + void stop(); + void applySettings(const AISSettings& settings, bool force = false); + void webapiReverseSendSettings(QList& featureSettingsKeys, const AISSettings& settings, bool force); + +private slots: + void updatePipes(); + void networkManagerFinished(QNetworkReply *reply); +}; + +#endif // INCLUDE_FEATURE_AIS_H_ diff --git a/plugins/feature/ais/ais.qrc b/plugins/feature/ais/ais.qrc new file mode 100644 index 000000000..a3b220ef9 --- /dev/null +++ b/plugins/feature/ais/ais.qrc @@ -0,0 +1,12 @@ + + + map/aircraft.png + map/helicopter.png + map/ship.png + map/tanker.png + map/cargo.png + map/tug.png + map/bouy.png + map/anchor.png + + diff --git a/plugins/feature/ais/aisgui.cpp b/plugins/feature/ais/aisgui.cpp new file mode 100644 index 000000000..adf368e76 --- /dev/null +++ b/plugins/feature/ais/aisgui.cpp @@ -0,0 +1,604 @@ +/////////////////////////////////////////////////////////////////////////////////// +// Copyright (C) 2020 Jon Beniston, M7RCE // +// Copyright (C) 2020 Edouard Griffiths, F4EXB // +// // +// This program is free software; you can redistribute it and/or modify // +// it under the terms of the GNU General Public License as published by // +// the Free Software Foundation as version 3 of the License, or // +// (at your option) any later version. // +// // +// This program is distributed in the hope that it will be useful, // +// but WITHOUT ANY WARRANTY; without even the implied warranty of // +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // +// GNU General Public License V3 for more details. // +// // +// You should have received a copy of the GNU General Public License // +// along with this program. If not, see . // +/////////////////////////////////////////////////////////////////////////////////// + +#include +#include +#include +#include +#include + +#include "feature/featureuiset.h" +#include "feature/featurewebapiutils.h" +#include "gui/basicfeaturesettingsdialog.h" +#include "mainwindow.h" +#include "device/deviceuiset.h" + +#include "ui_aisgui.h" +#include "ais.h" +#include "aisgui.h" + +#include "SWGMapItem.h" + +AISGUI* AISGUI::create(PluginAPI* pluginAPI, FeatureUISet *featureUISet, Feature *feature) +{ + AISGUI* gui = new AISGUI(pluginAPI, featureUISet, feature); + return gui; +} + +void AISGUI::destroy() +{ + delete this; +} + +void AISGUI::resetToDefaults() +{ + m_settings.resetToDefaults(); + displaySettings(); + applySettings(true); +} + +QByteArray AISGUI::serialize() const +{ + return m_settings.serialize(); +} + +bool AISGUI::deserialize(const QByteArray& data) +{ + if (m_settings.deserialize(data)) + { + displaySettings(); + applySettings(true); + return true; + } + else + { + resetToDefaults(); + return false; + } +} + +bool AISGUI::handleMessage(const Message& message) +{ + if (AIS::MsgConfigureAIS::match(message)) + { + qDebug("AISGUI::handleMessage: AIS::MsgConfigureAIS"); + const AIS::MsgConfigureAIS& cfg = (AIS::MsgConfigureAIS&) message; + m_settings = cfg.getSettings(); + blockApplySettings(true); + displaySettings(); + blockApplySettings(false); + + return true; + } + else if (MainCore::MsgPacket::match(message)) + { + MainCore::MsgPacket& report = (MainCore::MsgPacket&) message; + + // Decode the message + AISMessage *ais = AISMessage::decode(report.getPacket()); + // Update table + updateVessels(ais); + } + + return false; +} + +void AISGUI::handleInputMessages() +{ + Message* message; + + while ((message = getInputMessageQueue()->pop())) + { + if (handleMessage(*message)) { + delete message; + } + } +} + +void AISGUI::onWidgetRolled(QWidget* widget, bool rollDown) +{ + (void) widget; + (void) rollDown; +} + +AISGUI::AISGUI(PluginAPI* pluginAPI, FeatureUISet *featureUISet, Feature *feature, QWidget* parent) : + FeatureGUI(parent), + ui(new Ui::AISGUI), + m_pluginAPI(pluginAPI), + m_featureUISet(featureUISet), + m_doApplySettings(true), + m_lastFeatureState(0) +{ + ui->setupUi(this); + setAttribute(Qt::WA_DeleteOnClose, true); + setChannelWidget(false); + connect(this, SIGNAL(widgetRolled(QWidget*,bool)), this, SLOT(onWidgetRolled(QWidget*,bool))); + m_ais = reinterpret_cast(feature); + m_ais->setMessageQueueToGUI(&m_inputMessageQueue); + + m_featureUISet->addRollupWidget(this); + + connect(this, SIGNAL(customContextMenuRequested(const QPoint &)), this, SLOT(onMenuDialogCalled(const QPoint &))); + connect(getInputMessageQueue(), SIGNAL(messageEnqueued()), this, SLOT(handleInputMessages())); + + connect(&m_statusTimer, SIGNAL(timeout()), this, SLOT(updateStatus())); + m_statusTimer.start(1000); + + // Resize the table using dummy data + resizeTable(); + // Allow user to reorder columns + ui->vessels->horizontalHeader()->setSectionsMovable(true); + // Allow user to sort table by clicking on headers + ui->vessels->setSortingEnabled(true); + // Add context menu to allow hiding/showing of columns + vesselsMenu = new QMenu(ui->vessels); + for (int i = 0; i < ui->vessels->horizontalHeader()->count(); i++) + { + QString text = ui->vessels->horizontalHeaderItem(i)->text(); + vesselsMenu->addAction(createCheckableItem(text, i, true, SLOT(vesselsColumnSelectMenuChecked()))); + } + ui->vessels->horizontalHeader()->setContextMenuPolicy(Qt::CustomContextMenu); + connect(ui->vessels->horizontalHeader(), SIGNAL(customContextMenuRequested(QPoint)), SLOT(vesselsColumnSelectMenu(QPoint))); + // Get signals when columns change + connect(ui->vessels->horizontalHeader(), SIGNAL(sectionMoved(int, int, int)), SLOT(vessels_sectionMoved(int, int, int))); + connect(ui->vessels->horizontalHeader(), SIGNAL(sectionResized(int, int, int)), SLOT(vessels_sectionResized(int, int, int))); + + displaySettings(); + applySettings(true); +} + +AISGUI::~AISGUI() +{ + delete ui; +} + +void AISGUI::blockApplySettings(bool block) +{ + m_doApplySettings = !block; +} + +void AISGUI::displaySettings() +{ + setTitleColor(m_settings.m_rgbColor); + setWindowTitle(m_settings.m_title); + blockApplySettings(true); + + // Order and size columns + QHeaderView *header = ui->vessels->horizontalHeader(); + for (int i = 0; i < AIS_VESSEL_COLUMNS; i++) + { + bool hidden = m_settings.m_vesselColumnSizes[i] == 0; + header->setSectionHidden(i, hidden); + vesselsMenu->actions().at(i)->setChecked(!hidden); + if (m_settings.m_vesselColumnSizes[i] > 0) { + ui->vessels->setColumnWidth(i, m_settings.m_vesselColumnSizes[i]); + } + header->moveSection(header->visualIndex(i), m_settings.m_vesselColumnIndexes[i]); + } + + blockApplySettings(false); + arrangeRollups(); +} + +void AISGUI::leaveEvent(QEvent*) +{ +} + +void AISGUI::enterEvent(QEvent*) +{ +} + +void AISGUI::onMenuDialogCalled(const QPoint &p) +{ + if (m_contextMenuType == ContextMenuChannelSettings) + { + BasicFeatureSettingsDialog dialog(this); + dialog.setTitle(m_settings.m_title); + dialog.setColor(m_settings.m_rgbColor); + dialog.setUseReverseAPI(m_settings.m_useReverseAPI); + dialog.setReverseAPIAddress(m_settings.m_reverseAPIAddress); + dialog.setReverseAPIPort(m_settings.m_reverseAPIPort); + dialog.setReverseAPIFeatureSetIndex(m_settings.m_reverseAPIFeatureSetIndex); + dialog.setReverseAPIFeatureIndex(m_settings.m_reverseAPIFeatureIndex); + + dialog.move(p); + dialog.exec(); + + m_settings.m_rgbColor = dialog.getColor().rgb(); + m_settings.m_title = dialog.getTitle(); + m_settings.m_useReverseAPI = dialog.useReverseAPI(); + m_settings.m_reverseAPIAddress = dialog.getReverseAPIAddress(); + m_settings.m_reverseAPIPort = dialog.getReverseAPIPort(); + m_settings.m_reverseAPIFeatureSetIndex = dialog.getReverseAPIFeatureSetIndex(); + m_settings.m_reverseAPIFeatureIndex = dialog.getReverseAPIFeatureIndex(); + + setWindowTitle(m_settings.m_title); + setTitleColor(m_settings.m_rgbColor); + + applySettings(); + } + + resetContextMenuType(); +} + +void AISGUI::updateStatus() +{ +} + +void AISGUI::applySettings(bool force) +{ + if (m_doApplySettings) + { + AIS::MsgConfigureAIS* message = AIS::MsgConfigureAIS::create(m_settings, force); + m_ais->getInputMessageQueue()->push(message); + } +} + +void AISGUI::resizeTable() +{ + // Fill table with a row of dummy data that will size the columns nicely + int row = ui->vessels->rowCount(); + ui->vessels->setRowCount(row + 1); + ui->vessels->setItem(row, VESSEL_COL_MMSI, new QTableWidgetItem("123456789")); + ui->vessels->setItem(row, VESSEL_COL_TYPE, new QTableWidgetItem("Base station")); + ui->vessels->setItem(row, VESSEL_COL_LATITUDE, new QTableWidgetItem("90.000000-")); + ui->vessels->setItem(row, VESSEL_COL_LONGITUDE, new QTableWidgetItem("180.00000-")); + ui->vessels->setItem(row, VESSEL_COL_COURSE, new QTableWidgetItem("360.0")); + ui->vessels->setItem(row, VESSEL_COL_SPEED, new QTableWidgetItem("120")); + ui->vessels->setItem(row, VESSEL_COL_HEADING, new QTableWidgetItem("360")); + ui->vessels->setItem(row, VESSEL_COL_STATUS, new QTableWidgetItem("Under way using engine")); + ui->vessels->setItem(row, VESSEL_COL_IMO, new QTableWidgetItem("123456789")); + ui->vessels->setItem(row, VESSEL_COL_NAME, new QTableWidgetItem("12345678901234567890")); + ui->vessels->setItem(row, VESSEL_COL_CALLSIGN, new QTableWidgetItem("1234567")); + ui->vessels->setItem(row, VESSEL_COL_SHIP_TYPE, new QTableWidgetItem("Passenger")); + ui->vessels->setItem(row, VESSEL_COL_DESTINATION, new QTableWidgetItem("12345678901234567890")); + ui->vessels->resizeColumnsToContents(); + ui->vessels->removeRow(row); +} + +// Columns in table reordered +void AISGUI::vessels_sectionMoved(int logicalIndex, int oldVisualIndex, int newVisualIndex) +{ + (void) oldVisualIndex; + + m_settings.m_vesselColumnIndexes[logicalIndex] = newVisualIndex; +} + +// Column in table resized (when hidden size is 0) +void AISGUI::vessels_sectionResized(int logicalIndex, int oldSize, int newSize) +{ + (void) oldSize; + + m_settings.m_vesselColumnSizes[logicalIndex] = newSize; +} + +// Right click in table header - show column select menu +void AISGUI::vesselsColumnSelectMenu(QPoint pos) +{ + vesselsMenu->popup(ui->vessels->horizontalHeader()->viewport()->mapToGlobal(pos)); +} + +// Hide/show column when menu selected +void AISGUI::vesselsColumnSelectMenuChecked(bool checked) +{ + (void) checked; + + QAction* action = qobject_cast(sender()); + if (action != nullptr) + { + int idx = action->data().toInt(nullptr); + ui->vessels->setColumnHidden(idx, !action->isChecked()); + } +} + +// Create column select menu item +QAction *AISGUI::createCheckableItem(QString &text, int idx, bool checked, const char *slot) +{ + QAction *action = new QAction(text, this); + action->setCheckable(true); + action->setChecked(checked); + action->setData(QVariant(idx)); + connect(action, SIGNAL(triggered()), this, slot); + return action; +} + +void AISGUI::updateVessels(AISMessage *ais) +{ + QTableWidgetItem *mmsiItem; + QTableWidgetItem *typeItem; + QTableWidgetItem *latitudeItem; + QTableWidgetItem *longitudeItem; + QTableWidgetItem *courseItem; + QTableWidgetItem *speedItem; + QTableWidgetItem *headingItem; + QTableWidgetItem *statusItem; + QTableWidgetItem *imoItem; + QTableWidgetItem *nameItem; + QTableWidgetItem *callsignItem; + QTableWidgetItem *shipTypeItem; + QTableWidgetItem *destinationItem; + + // See if vessel is already in table + QString messageMMSI = QString("%1").arg(ais->m_mmsi, 9, 10, QChar('0')); + bool found = false; + for (int row = 0; row < ui->vessels->rowCount(); row++) + { + QString itemMMSI = ui->vessels->item(row, VESSEL_COL_MMSI)->text(); + if (messageMMSI == itemMMSI) + { + // Update existing item + mmsiItem = ui->vessels->item(row, VESSEL_COL_MMSI); + typeItem = ui->vessels->item(row, VESSEL_COL_TYPE); + latitudeItem = ui->vessels->item(row, VESSEL_COL_LATITUDE); + longitudeItem = ui->vessels->item(row, VESSEL_COL_LONGITUDE); + courseItem = ui->vessels->item(row, VESSEL_COL_COURSE); + speedItem = ui->vessels->item(row, VESSEL_COL_SPEED); + headingItem = ui->vessels->item(row, VESSEL_COL_HEADING); + statusItem = ui->vessels->item(row, VESSEL_COL_STATUS); + imoItem = ui->vessels->item(row, VESSEL_COL_IMO); + nameItem = ui->vessels->item(row, VESSEL_COL_NAME); + callsignItem = ui->vessels->item(row, VESSEL_COL_CALLSIGN); + shipTypeItem = ui->vessels->item(row, VESSEL_COL_SHIP_TYPE); + destinationItem = ui->vessels->item(row, VESSEL_COL_DESTINATION); + found = true; + break; + } + } + if (!found) + { + // Add new vessel + ui->vessels->setSortingEnabled(false); + int row = ui->vessels->rowCount(); + ui->vessels->setRowCount(row + 1); + + mmsiItem = new QTableWidgetItem(); + typeItem = new QTableWidgetItem(); + latitudeItem = new QTableWidgetItem(); + longitudeItem = new QTableWidgetItem(); + courseItem = new QTableWidgetItem(); + speedItem = new QTableWidgetItem(); + headingItem = new QTableWidgetItem(); + statusItem = new QTableWidgetItem(); + imoItem = new QTableWidgetItem(); + nameItem = new QTableWidgetItem(); + callsignItem = new QTableWidgetItem(); + shipTypeItem = new QTableWidgetItem(); + destinationItem = new QTableWidgetItem(); + ui->vessels->setItem(row, VESSEL_COL_MMSI, mmsiItem); + ui->vessels->setItem(row, VESSEL_COL_TYPE, typeItem); + ui->vessels->setItem(row, VESSEL_COL_LATITUDE, latitudeItem); + ui->vessels->setItem(row, VESSEL_COL_LONGITUDE, longitudeItem); + ui->vessels->setItem(row, VESSEL_COL_COURSE, courseItem); + ui->vessels->setItem(row, VESSEL_COL_SPEED, speedItem); + ui->vessels->setItem(row, VESSEL_COL_HEADING, headingItem); + ui->vessels->setItem(row, VESSEL_COL_STATUS, statusItem); + ui->vessels->setItem(row, VESSEL_COL_IMO, imoItem); + ui->vessels->setItem(row, VESSEL_COL_NAME, nameItem); + ui->vessels->setItem(row, VESSEL_COL_CALLSIGN, callsignItem); + ui->vessels->setItem(row, VESSEL_COL_SHIP_TYPE, shipTypeItem); + ui->vessels->setItem(row, VESSEL_COL_DESTINATION, destinationItem); + } + + mmsiItem->setText(QString("%1").arg(ais->m_mmsi, 9, 10, QChar('0'))); + if ((ais->m_id <= 3) || (ais->m_id == 5) || (ais->m_id == 18) || (ais->m_id == 19)) { + typeItem->setText("Vessel"); + } else if (ais->m_id == 4) { + typeItem->setText("Base station"); + } else if (ais->m_id == 9) { + typeItem->setText("Aircraft"); + } else if (ais->m_id == 21) { + typeItem->setText("Aid-to-nav"); + } + if (ais->m_id == 21) + { + AISAidsToNavigationReport *aids = dynamic_cast(ais); + if (aids) { + nameItem->setText(aids->m_name); + } + } + if (ais->m_id == 5) + { + AISShipStaticAndVoyageData *vd = dynamic_cast(ais); + if (vd) + { + if (vd->m_imo != 0) { + imoItem->setData(Qt::DisplayRole, vd->m_imo); + } + nameItem->setText(vd->m_name); + callsignItem->setText(vd->m_callsign); + shipTypeItem->setText(AISMessage::typeToString(vd->m_type)); + destinationItem->setText(vd->m_destination); + } + } + else + { + if (ais->hasPosition()) + { + latitudeItem->setData(Qt::DisplayRole, ais->getLatitude()); + longitudeItem->setData(Qt::DisplayRole, ais->getLongitude()); + } + if (ais->hasCourse()) { + courseItem->setData(Qt::DisplayRole, ais->getCourse()); + } + if (ais->hasSpeed()) { + speedItem->setData(Qt::DisplayRole, ais->getSpeed()); + } + if (ais->hasHeading()) { + headingItem->setData(Qt::DisplayRole, ais->getHeading()); + } + AISPositionReport *pr = dynamic_cast(ais); + if (pr) { + statusItem->setText(AISPositionReport::getStatusString(pr->m_status)); + } + AISLongRangePositionReport *lrpr = dynamic_cast(ais); + if (lrpr) { + statusItem->setText(AISPositionReport::getStatusString(lrpr->m_status)); + } + } + if (ais->m_id == 24) + { + AISStaticDataReport *dr = dynamic_cast(ais); + if (dr) + { + if (dr->m_partNumber == 0) + { + nameItem->setText(dr->m_name); + } + else if (dr->m_partNumber == 1) + { + callsignItem->setText(dr->m_callsign); + shipTypeItem->setText(AISMessage::typeToString(dr->m_type)); + } + } + } + ui->vessels->setSortingEnabled(true); + + QVariant latitudeV = latitudeItem->data(Qt::DisplayRole); + QVariant longitudeV = longitudeItem->data(Qt::DisplayRole); + QString type = typeItem->text(); + + if (!latitudeV.isNull() && !longitudeV.isNull() && !type.isEmpty()) + { + // Send to Map feature + MessagePipes& messagePipes = MainCore::instance()->getMessagePipes(); + QList *mapMessageQueues = messagePipes.getMessageQueues(m_ais, "mapitems"); + if (mapMessageQueues) + { + QList::iterator it = mapMessageQueues->begin(); + + for (; it != mapMessageQueues->end(); ++it) + { + SWGSDRangel::SWGMapItem *swgMapItem = new SWGSDRangel::SWGMapItem(); + swgMapItem->setName(new QString(QString("%1").arg(mmsiItem->text()))); + swgMapItem->setLatitude(latitudeV.toFloat()); + swgMapItem->setLongitude(longitudeV.toFloat()); + swgMapItem->setAltitude(0); + QString image; + if (type == "Aircraft") { + // I presume search and rescue aircraft are more likely to be helicopters + image = "helicopter.png"; + } else if (type == "Base station") { + image = "anchor.png"; + } else if (type == "Aid-to-nav") { + image = "bouy.png"; + } else { + image = "ship.png"; + QString shipType = shipTypeItem->text(); + if (!shipType.isEmpty()) + { + if (shipType == "Tug") { + image = "tug.png"; + } else if (shipType == "Cargo") { + image = "cargo.png"; + } else if (shipType == "Tanker") { + image = "tanker.png"; + } + } + } + swgMapItem->setImage(new QString(QString("qrc:///ais/map/%1").arg(image))); + + swgMapItem->setImageMinZoom(11); + QStringList text; + QVariant courseV = courseItem->data(Qt::DisplayRole); + QVariant speedV = speedItem->data(Qt::DisplayRole); + QVariant headingV = headingItem->data(Qt::DisplayRole); + QString name = nameItem->text(); + QString callsign = callsignItem->text(); + QString destination = destinationItem->text(); + QString shipType = shipTypeItem->text(); + QString status = statusItem->text(); + if (!name.isEmpty()) { + text.append(QString("Name: %1").arg(name)); + } + if (!callsign.isEmpty()) { + text.append(QString("Callsign: %1").arg(callsign)); + } + if (!destination.isEmpty()) { + text.append(QString("Destination: %1").arg(destination)); + } + if (!courseV.isNull()) { + text.append(QString("Course: %1%2").arg(courseV.toFloat()).arg(QChar(0xb0))); + } + if (!speedV.isNull()) { + text.append(QString("Speed: %1 knts").arg(speedV.toFloat())); + } + if (!headingV.isNull()) + { + float heading = headingV.toFloat(); + text.append(QString("Heading: %1%2").arg(heading).arg(QChar(0xb0))); + swgMapItem->setImageRotation(heading); + } + if (!shipType.isEmpty()) { + text.append(QString("Ship type: %1").arg(shipType)); + } + if (!status.isEmpty()) { + text.append(QString("Status: %1").arg(status)); + } + swgMapItem->setText(new QString(text.join("\n"))); + + MainCore::MsgMapItem *msg = MainCore::MsgMapItem::create(m_ais, swgMapItem); + (*it)->push(msg); + } + } + } +} + +void AISGUI::on_vessels_cellDoubleClicked(int row, int column) +{ + if (column == VESSEL_COL_MMSI) + { + // Get MMSI of vessel in row double clicked + QString mmsi = ui->vessels->item(row, VESSEL_COL_MMSI)->text(); + // Search for MMSI on www.vesselfinder.com + QDesktopServices::openUrl(QUrl(QString("https://www.vesselfinder.com/vessels?name=%1").arg(mmsi))); + } + else if ((column == VESSEL_COL_LATITUDE) || (column == VESSEL_COL_LONGITUDE)) + { + // Get MMSI of vessel in row double clicked + QString mmsi = ui->vessels->item(row, VESSEL_COL_MMSI)->text(); + // Find MMSI on Map + FeatureWebAPIUtils::mapFind(mmsi); + } + else if (column == VESSEL_COL_IMO) + { + QString imo = ui->vessels->item(row, VESSEL_COL_IMO)->text(); + if (!imo.isEmpty() && (imo != "0")) + { + // Search for IMO on www.vesselfinder.com + QDesktopServices::openUrl(QUrl(QString("https://www.vesselfinder.com/vessels?name=%1").arg(imo))); + } + } + else if (column == VESSEL_COL_NAME) + { + QString name = ui->vessels->item(row, VESSEL_COL_NAME)->text(); + if (!name.isEmpty()) + { + // Search for name on www.vesselfinder.com + QDesktopServices::openUrl(QUrl(QString("https://www.vesselfinder.com/vessels?name=%1").arg(name))); + } + } + else if (column == VESSEL_COL_DESTINATION) + { + QString destination = ui->vessels->item(row, VESSEL_COL_DESTINATION)->text(); + if (!destination.isEmpty()) + { + // Find destination on Map + FeatureWebAPIUtils::mapFind(destination); + } + } +} diff --git a/plugins/feature/ais/aisgui.h b/plugins/feature/ais/aisgui.h new file mode 100644 index 000000000..b65f50249 --- /dev/null +++ b/plugins/feature/ais/aisgui.h @@ -0,0 +1,107 @@ +/////////////////////////////////////////////////////////////////////////////////// +// Copyright (C) 2020 Jon Beniston, M7RCE // +// Copyright (C) 2020 Edouard Griffiths, F4EXB // +// // +// This program is free software; you can redistribute it and/or modify // +// it under the terms of the GNU General Public License as published by // +// the Free Software Foundation as version 3 of the License, or // +// (at your option) any later version. // +// // +// This program is distributed in the hope that it will be useful, // +// but WITHOUT ANY WARRANTY; without even the implied warranty of // +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // +// GNU General Public License V3 for more details. // +// // +// You should have received a copy of the GNU General Public License // +// along with this program. If not, see . // +/////////////////////////////////////////////////////////////////////////////////// + +#ifndef INCLUDE_FEATURE_AISGUI_H_ +#define INCLUDE_FEATURE_AISGUI_H_ + +#include +#include + +#include "feature/featuregui.h" +#include "util/messagequeue.h" +#include "util/ais.h" +#include "pipes/pipeendpoint.h" +#include "aissettings.h" + +class PluginAPI; +class FeatureUISet; +class AIS; + +namespace Ui { + class AISGUI; +} + +class AISGUI : public FeatureGUI { + Q_OBJECT +public: + static AISGUI* create(PluginAPI* pluginAPI, FeatureUISet *featureUISet, Feature *feature); + virtual void destroy(); + + void resetToDefaults(); + QByteArray serialize() const; + bool deserialize(const QByteArray& data); + virtual MessageQueue *getInputMessageQueue() { return &m_inputMessageQueue; } + +private: + Ui::AISGUI* ui; + PluginAPI* m_pluginAPI; + FeatureUISet* m_featureUISet; + AISSettings m_settings; + bool m_doApplySettings; + + AIS* m_ais; + MessageQueue m_inputMessageQueue; + QTimer m_statusTimer; + int m_lastFeatureState; + + QMenu *vesselsMenu; // Column select context menu + + explicit AISGUI(PluginAPI* pluginAPI, FeatureUISet *featureUISet, Feature *feature, QWidget* parent = nullptr); + virtual ~AISGUI(); + + void blockApplySettings(bool block); + void applySettings(bool force = false); + void displaySettings(); + bool handleMessage(const Message& message); + + void leaveEvent(QEvent*); + void enterEvent(QEvent*); + + void updateVessels(AISMessage *ais); + void resizeTable(); + QAction *createCheckableItem(QString& text, int idx, bool checked, const char *slot); + + enum VesselCol { + VESSEL_COL_MMSI, + VESSEL_COL_TYPE, + VESSEL_COL_LATITUDE, + VESSEL_COL_LONGITUDE, + VESSEL_COL_COURSE, + VESSEL_COL_SPEED, + VESSEL_COL_HEADING, + VESSEL_COL_STATUS, + VESSEL_COL_IMO, + VESSEL_COL_NAME, + VESSEL_COL_CALLSIGN, + VESSEL_COL_SHIP_TYPE, + VESSEL_COL_DESTINATION + }; + +private slots: + void onMenuDialogCalled(const QPoint &p); + void onWidgetRolled(QWidget* widget, bool rollDown); + void handleInputMessages(); + void updateStatus(); + void on_vessels_cellDoubleClicked(int row, int column); + void vessels_sectionMoved(int logicalIndex, int oldVisualIndex, int newVisualIndex); + void vessels_sectionResized(int logicalIndex, int oldSize, int newSize); + void vesselsColumnSelectMenu(QPoint pos); + void vesselsColumnSelectMenuChecked(bool checked = false); +}; + +#endif // INCLUDE_FEATURE_AISGUI_H_ diff --git a/plugins/feature/ais/aisgui.ui b/plugins/feature/ais/aisgui.ui new file mode 100644 index 000000000..a391051b9 --- /dev/null +++ b/plugins/feature/ais/aisgui.ui @@ -0,0 +1,188 @@ + + + AISGUI + + + + 0 + 0 + 484 + 328 + + + + + 0 + 0 + + + + + 320 + 100 + + + + + 2000 + 500 + + + + + Liberation Sans + 9 + + + + GS-232 Rotator Controller + + + + + 10 + 10 + 461 + 291 + + + + + 0 + 0 + + + + Vessels, Base stations and Aids-to-navigation + + + + 3 + + + 2 + + + 2 + + + 2 + + + 2 + + + + + Vessels + + + QAbstractItemView::NoEditTriggers + + + + MMSI + + + Maritime Mobile Service Identity + + + + + Type + + + + + Lat + + + Latitude in degrees. East positive + + + + + Lon + + + Longitude in degrees. North positive. + + + + + Course + + + Course in degrees. + + + + + Speed + + + Speed in knots. + + + + + Heading + + + Heading in degrees. + + + + + Status + + + + + IMO + + + International Maritime Organization number + + + + + Name + + + Name of the vessel + + + + + Callsign + + + + + Ship Type + + + + + Destination + + + + + + + + + + RollupWidget + QWidget +
    gui/rollupwidget.h
    + 1 +
    +
    + + + + +
    diff --git a/plugins/feature/ais/aisplugin.cpp b/plugins/feature/ais/aisplugin.cpp new file mode 100644 index 000000000..cd50ff8be --- /dev/null +++ b/plugins/feature/ais/aisplugin.cpp @@ -0,0 +1,80 @@ +/////////////////////////////////////////////////////////////////////////////////// +// Copyright (C) 2020 Jon Beniston, M7RCE // +// Copyright (C) 2020 Edouard Griffiths, F4EXB // +// // +// This program is free software; you can redistribute it and/or modify // +// it under the terms of the GNU General Public License as published by // +// the Free Software Foundation as version 3 of the License, or // +// (at your option) any later version. // +// // +// This program is distributed in the hope that it will be useful, // +// but WITHOUT ANY WARRANTY; without even the implied warranty of // +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // +// GNU General Public License V3 for more details. // +// // +// You should have received a copy of the GNU General Public License // +// along with this program. If not, see . // +/////////////////////////////////////////////////////////////////////////////////// + + +#include +#include "plugin/pluginapi.h" + +#ifndef SERVER_MODE +#include "aisgui.h" +#endif +#include "ais.h" +#include "aisplugin.h" +#include "aiswebapiadapter.h" + +const PluginDescriptor AISPlugin::m_pluginDescriptor = { + AIS::m_featureId, + QStringLiteral("AIS"), + QStringLiteral("6.11.1"), + QStringLiteral("(c) Jon Beniston, M7RCE"), + QStringLiteral("https://github.com/f4exb/sdrangel"), + true, + QStringLiteral("https://github.com/f4exb/sdrangel") +}; + +AISPlugin::AISPlugin(QObject* parent) : + QObject(parent), + m_pluginAPI(nullptr) +{ +} + +const PluginDescriptor& AISPlugin::getPluginDescriptor() const +{ + return m_pluginDescriptor; +} + +void AISPlugin::initPlugin(PluginAPI* pluginAPI) +{ + m_pluginAPI = pluginAPI; + + m_pluginAPI->registerFeature(AIS::m_featureIdURI, AIS::m_featureId, this); +} + +#ifdef SERVER_MODE +FeatureGUI* AISPlugin::createFeatureGUI(FeatureUISet *featureUISet, Feature *feature) const +{ + (void) featureUISet; + (void) feature; + return nullptr; +} +#else +FeatureGUI* AISPlugin::createFeatureGUI(FeatureUISet *featureUISet, Feature *feature) const +{ + return AISGUI::create(m_pluginAPI, featureUISet, feature); +} +#endif + +Feature* AISPlugin::createFeature(WebAPIAdapterInterface* webAPIAdapterInterface) const +{ + return new AIS(webAPIAdapterInterface); +} + +FeatureWebAPIAdapter* AISPlugin::createFeatureWebAPIAdapter() const +{ + return new AISWebAPIAdapter(); +} diff --git a/plugins/feature/ais/aisplugin.h b/plugins/feature/ais/aisplugin.h new file mode 100644 index 000000000..c5d4b83a7 --- /dev/null +++ b/plugins/feature/ais/aisplugin.h @@ -0,0 +1,49 @@ +/////////////////////////////////////////////////////////////////////////////////// +// Copyright (C) 2020 Jon Beniston, M7RCE // +// Copyright (C) 2020 Edouard Griffiths, F4EXB // +// // +// This program is free software; you can redistribute it and/or modify // +// it under the terms of the GNU General Public License as published by // +// the Free Software Foundation as version 3 of the License, or // +// (at your option) any later version. // +// // +// This program is distributed in the hope that it will be useful, // +// but WITHOUT ANY WARRANTY; without even the implied warranty of // +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // +// GNU General Public License V3 for more details. // +// // +// You should have received a copy of the GNU General Public License // +// along with this program. If not, see . // +/////////////////////////////////////////////////////////////////////////////////// + +#ifndef INCLUDE_FEATURE_AISPLUGIN_H +#define INCLUDE_FEATURE_AISPLUGIN_H + +#include +#include "plugin/plugininterface.h" + +class FeatureGUI; +class WebAPIAdapterInterface; + +class AISPlugin : public QObject, PluginInterface { + Q_OBJECT + Q_INTERFACES(PluginInterface) + Q_PLUGIN_METADATA(IID "sdrangel.feature.ais") + +public: + explicit AISPlugin(QObject* parent = nullptr); + + const PluginDescriptor& getPluginDescriptor() const; + void initPlugin(PluginAPI* pluginAPI); + + virtual FeatureGUI* createFeatureGUI(FeatureUISet *featureUISet, Feature *feature) const; + virtual Feature* createFeature(WebAPIAdapterInterface *webAPIAdapterInterface) const; + virtual FeatureWebAPIAdapter* createFeatureWebAPIAdapter() const; + +private: + static const PluginDescriptor m_pluginDescriptor; + + PluginAPI* m_pluginAPI; +}; + +#endif // INCLUDE_FEATURE_AISPLUGIN_H diff --git a/plugins/feature/ais/aissettings.cpp b/plugins/feature/ais/aissettings.cpp new file mode 100644 index 000000000..7c86c524c --- /dev/null +++ b/plugins/feature/ais/aissettings.cpp @@ -0,0 +1,122 @@ +/////////////////////////////////////////////////////////////////////////////////// +// Copyright (C) 2020 Jon Beniston, M7RCE // +// Copyright (C) 2020 Edouard Griffiths, F4EXB // +// // +// This program is free software; you can redistribute it and/or modify // +// it under the terms of the GNU General Public License as published by // +// the Free Software Foundation as version 3 of the License, or // +// (at your option) any later version. // +// // +// This program is distributed in the hope that it will be useful, // +// but WITHOUT ANY WARRANTY; without even the implied warranty of // +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // +// GNU General Public License V3 for more details. // +// // +// You should have received a copy of the GNU General Public License // +// along with this program. If not, see . // +/////////////////////////////////////////////////////////////////////////////////// + +#include +#include + +#include "util/simpleserializer.h" +#include "settings/serializable.h" + +#include "aissettings.h" + +const QStringList AISSettings::m_pipeTypes = { + QStringLiteral("AISDemod") +}; + +const QStringList AISSettings::m_pipeURIs = { + QStringLiteral("sdrangel.channel.aisdemod") +}; + +AISSettings::AISSettings() +{ + resetToDefaults(); +} + +void AISSettings::resetToDefaults() +{ + m_title = "AIS"; + m_rgbColor = QColor(102, 0, 0).rgb(); + m_useReverseAPI = false; + m_reverseAPIAddress = "127.0.0.1"; + m_reverseAPIPort = 8888; + m_reverseAPIFeatureSetIndex = 0; + m_reverseAPIFeatureIndex = 0; + for (int i = 0; i < AIS_VESSEL_COLUMNS; i++) + { + m_vesselColumnIndexes[i] = i; + m_vesselColumnSizes[i] = -1; // Autosize + } +} + +QByteArray AISSettings::serialize() const +{ + SimpleSerializer s(1); + + s.writeString(20, m_title); + s.writeU32(21, m_rgbColor); + s.writeBool(22, m_useReverseAPI); + s.writeString(23, m_reverseAPIAddress); + s.writeU32(24, m_reverseAPIPort); + s.writeU32(25, m_reverseAPIFeatureSetIndex); + s.writeU32(26, m_reverseAPIFeatureIndex); + + for (int i = 0; i < AIS_VESSEL_COLUMNS; i++) + s.writeS32(300 + i, m_vesselColumnIndexes[i]); + for (int i = 0; i < AIS_VESSEL_COLUMNS; i++) + s.writeS32(400 + i, m_vesselColumnSizes[i]); + + return s.final(); +} + +bool AISSettings::deserialize(const QByteArray& data) +{ + SimpleDeserializer d(data); + + if (!d.isValid()) + { + resetToDefaults(); + return false; + } + + if (d.getVersion() == 1) + { + QByteArray bytetmp; + uint32_t utmp; + QString strtmp; + QByteArray blob; + + d.readString(20, &m_title, "AIS"); + d.readU32(21, &m_rgbColor, QColor(102, 0, 0).rgb()); + d.readBool(22, &m_useReverseAPI, false); + d.readString(23, &m_reverseAPIAddress, "127.0.0.1"); + d.readU32(24, &utmp, 0); + + if ((utmp > 1023) && (utmp < 65535)) { + m_reverseAPIPort = utmp; + } else { + m_reverseAPIPort = 8888; + } + + d.readU32(25, &utmp, 0); + m_reverseAPIFeatureSetIndex = utmp > 99 ? 99 : utmp; + d.readU32(26, &utmp, 0); + m_reverseAPIFeatureIndex = utmp > 99 ? 99 : utmp; + + for (int i = 0; i < AIS_VESSEL_COLUMNS; i++) + d.readS32(300 + i, &m_vesselColumnIndexes[i], i); + for (int i = 0; i < AIS_VESSEL_COLUMNS; i++) + d.readS32(400 + i, &m_vesselColumnSizes[i], -1); + + return true; + } + else + { + resetToDefaults(); + return false; + } +} diff --git a/plugins/feature/ais/aissettings.h b/plugins/feature/ais/aissettings.h new file mode 100644 index 000000000..5e0d65dab --- /dev/null +++ b/plugins/feature/ais/aissettings.h @@ -0,0 +1,54 @@ +/////////////////////////////////////////////////////////////////////////////////// +// Copyright (C) 2020 Jon Beniston, M7RCE // +// Copyright (C) 2020 Edouard Griffiths, F4EXB // +// // +// This program is free software; you can redistribute it and/or modify // +// it under the terms of the GNU General Public License as published by // +// the Free Software Foundation as version 3 of the License, or // +// (at your option) any later version. // +// // +// This program is distributed in the hope that it will be useful, // +// but WITHOUT ANY WARRANTY; without even the implied warranty of // +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // +// GNU General Public License V3 for more details. // +// // +// You should have received a copy of the GNU General Public License // +// along with this program. If not, see . // +/////////////////////////////////////////////////////////////////////////////////// + +#ifndef INCLUDE_FEATURE_AISSETTINGS_H_ +#define INCLUDE_FEATURE_AISSETTINGS_H_ + +#include +#include + +#include "util/message.h" + +class Serializable; + +// Number of columns in the tables +#define AIS_VESSEL_COLUMNS 13 + +struct AISSettings +{ + QString m_title; + quint32 m_rgbColor; + bool m_useReverseAPI; + QString m_reverseAPIAddress; + uint16_t m_reverseAPIPort; + uint16_t m_reverseAPIFeatureSetIndex; + uint16_t m_reverseAPIFeatureIndex; + + int m_vesselColumnIndexes[AIS_VESSEL_COLUMNS]; + int m_vesselColumnSizes[AIS_VESSEL_COLUMNS]; + + AISSettings(); + void resetToDefaults(); + QByteArray serialize() const; + bool deserialize(const QByteArray& data); + + static const QStringList m_pipeTypes; + static const QStringList m_pipeURIs; +}; + +#endif // INCLUDE_FEATURE_AISSETTINGS_H_ diff --git a/plugins/feature/ais/aiswebapiadapter.cpp b/plugins/feature/ais/aiswebapiadapter.cpp new file mode 100644 index 000000000..8dbdb05e5 --- /dev/null +++ b/plugins/feature/ais/aiswebapiadapter.cpp @@ -0,0 +1,52 @@ +/////////////////////////////////////////////////////////////////////////////////// +// Copyright (C) 2020 Jon Beniston, M7RCE // +// Copyright (C) 2020 Edouard Griffiths, F4EXB. // +// // +// This program is free software; you can redistribute it and/or modify // +// it under the terms of the GNU General Public License as published by // +// the Free Software Foundation as version 3 of the License, or // +// (at your option) any later version. // +// // +// This program is distributed in the hope that it will be useful, // +// but WITHOUT ANY WARRANTY; without even the implied warranty of // +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // +// GNU General Public License V3 for more details. // +// // +// You should have received a copy of the GNU General Public License // +// along with this program. If not, see . // +/////////////////////////////////////////////////////////////////////////////////// + +#include "SWGFeatureSettings.h" +#include "ais.h" +#include "aiswebapiadapter.h" + +AISWebAPIAdapter::AISWebAPIAdapter() +{} + +AISWebAPIAdapter::~AISWebAPIAdapter() +{} + +int AISWebAPIAdapter::webapiSettingsGet( + SWGSDRangel::SWGFeatureSettings& response, + QString& errorMessage) +{ + (void) errorMessage; + response.setAisSettings(new SWGSDRangel::SWGAISSettings()); + response.getAisSettings()->init(); + AIS::webapiFormatFeatureSettings(response, m_settings); + + return 200; +} + +int AISWebAPIAdapter::webapiSettingsPutPatch( + bool force, + const QStringList& featureSettingsKeys, + SWGSDRangel::SWGFeatureSettings& response, + QString& errorMessage) +{ + (void) force; // no action + (void) errorMessage; + AIS::webapiUpdateFeatureSettings(m_settings, featureSettingsKeys, response); + + return 200; +} diff --git a/plugins/feature/ais/aiswebapiadapter.h b/plugins/feature/ais/aiswebapiadapter.h new file mode 100644 index 000000000..648c9be82 --- /dev/null +++ b/plugins/feature/ais/aiswebapiadapter.h @@ -0,0 +1,50 @@ +/////////////////////////////////////////////////////////////////////////////////// +// Copyright (C) 2020 Jon Beniston, M7RCE // +// Copyright (C) 2020 Edouard Griffiths, F4EXB. // +// // +// This program is free software; you can redistribute it and/or modify // +// it under the terms of the GNU General Public License as published by // +// the Free Software Foundation as version 3 of the License, or // +// (at your option) any later version. // +// // +// This program is distributed in the hope that it will be useful, // +// but WITHOUT ANY WARRANTY; without even the implied warranty of // +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // +// GNU General Public License V3 for more details. // +// // +// You should have received a copy of the GNU General Public License // +// along with this program. If not, see . // +/////////////////////////////////////////////////////////////////////////////////// + +#ifndef INCLUDE_AIS_WEBAPIADAPTER_H +#define INCLUDE_AIS_WEBAPIADAPTER_H + +#include "feature/featurewebapiadapter.h" +#include "aissettings.h" + +/** + * Standalone API adapter only for the settings + */ +class AISWebAPIAdapter : public FeatureWebAPIAdapter { +public: + AISWebAPIAdapter(); + virtual ~AISWebAPIAdapter(); + + virtual QByteArray serialize() const { return m_settings.serialize(); } + virtual bool deserialize(const QByteArray& data) { return m_settings.deserialize(data); } + + virtual int webapiSettingsGet( + SWGSDRangel::SWGFeatureSettings& response, + QString& errorMessage); + + virtual int webapiSettingsPutPatch( + bool force, + const QStringList& featureSettingsKeys, + SWGSDRangel::SWGFeatureSettings& response, + QString& errorMessage); + +private: + AISSettings m_settings; +}; + +#endif // INCLUDE_AIS_WEBAPIADAPTER_H diff --git a/plugins/feature/ais/map/aircraft.png b/plugins/feature/ais/map/aircraft.png new file mode 100644 index 0000000000000000000000000000000000000000..77712261c0f566558a9b3f38d7eff66d6d34fce2 GIT binary patch literal 657 zcmV;C0&e|@P)yE0v<_3 zK~z}7?by3(R8bJX;m-$ZL`f9F22l~w!U&3}AShbIPEf3DtPEKA7l!htNfg$=?@;Ag~M##jUMeZxQuaH!4+#xa6piIOuK@tX`| z12}~98GbPxjuq;GjFlb7y@ZZMe5N9{FX8ij_(CJ$xqv+h9mJeeY#|0X<1~T$c!&ph zT1SS(06%5;PM{c6NQ@oWiM`ko0R}6>?W_?gMz-m8gLCy`O>70Dv6UNfDiL#| r6MzS?I%Dig==)-_E+kf0Ix4>bwBa|-9o;Gg00000NkvXXu0mjf`o$aC literal 0 HcmV?d00001 diff --git a/plugins/feature/ais/map/anchor.png b/plugins/feature/ais/map/anchor.png new file mode 100644 index 0000000000000000000000000000000000000000..37a17605720dc41874500a3450f4ac33edd78bff GIT binary patch literal 531 zcmV+u0_^>XP)5ToQeTk#PRjP-0^_H`atLpAR<)<00RI$01)>P06MLlNmc=T5MbHW8_WSa;3( zIRIQTiDQfYCmy^6iWq|NS0bdklfQ}aJ8Mcd!&&gm-@a$@Afl>{<~?2AW{kkMurO z5#{d6upNY_Slj#5l`X=9jkMlB7r+BwPu5sf@Q6m-{d(6~4SN_aHZfJErT-HV`~nVx V8&kPvy7T}5002ovPDHLkV1m+?=*$2B literal 0 HcmV?d00001 diff --git a/plugins/feature/ais/map/bouy.png b/plugins/feature/ais/map/bouy.png new file mode 100644 index 0000000000000000000000000000000000000000..d74e636e9975e8fee1183af930adc7df13e82b59 GIT binary patch literal 375 zcmeAS@N?(olHy`uVBq!ia0vp^{6H+n!3HEh$GDULDb50q$YLO!4Z@6k@zH?{42+td zE{-7~+b1rHhU)IJqxU{+(~oZQgjd4Lf_Iye7zE7Z@y7$r|pm-zW9Mv0;4zCRQ%$ZohEy8Z2j_j_jWYI(cF#kq*?+44`oXQM*PgsG$<_&iH~In#u?z6YJA`BQxz*zP?$+Mvc;!IhlJvQ*{rkH43;aooFqf0Bsj T9IOAp&|~m)^>bP0l+XkKlk}Qt literal 0 HcmV?d00001 diff --git a/plugins/feature/ais/map/cargo.png b/plugins/feature/ais/map/cargo.png new file mode 100644 index 0000000000000000000000000000000000000000..cf2371b72060e7f243da650f41d4c4e6e76d6460 GIT binary patch literal 1191 zcmV;Y1X%ltP)X(HKok<`SoyF~JXU)Z?W{~=nxE^zJELa1{@xUP_|MGv*q)voP29uimR$M-%%b#mt!hZ7#~Gj z_*X9NQeyeE=2FKP)OkPG$!Rl`PqJFG&rEH}$+zaE$hT%9$isYZ-+-^Stme8nXhHCF z#^$~T*YXMTyQu`9D@m?fT}~XB$h77)H`u8yIr-MS6#3Rn1bLWmey%`gfBM_se(aUS z)fpSC8`G;Za)lmdnY8B6%se{A(+~VhUk@R%xIC-YTx2!Lo4eMWU(vi$$uG|?#Z|RD z>+9Z)=K)l{_!@vif4dgy{0y`2c2Zk%=+&pp2e^s>FHSV6($q;)6l|m*Aem z)V2)rFt(~%%=Y(S{DXIp_-&HcP1hH~+xh_9P4_OJFv`eDT>Nt$?z}b~ zH5s0|?9cocD8sPb41WKoh=e0JHgFvM0|U6RstPWb6S6^u+vURf>1k}=u@lt0Ge8+e z-v?7vR65+-(_0;#j$$Gdf}+G(Fhx<=&W&=P4`+fwh@ucz)fD*)hioH|jw~CVTe;Hr zrao5Y=5XP{94r=n1yg{{YGpP3eC}NEV^vK(MrN!7;4deGulvr^-3|Z%002ovPDHLk FV1mo|QBME> literal 0 HcmV?d00001 diff --git a/plugins/feature/ais/map/helicopter.png b/plugins/feature/ais/map/helicopter.png new file mode 100644 index 0000000000000000000000000000000000000000..62d4f7629d3be08479f203c109eac88d0e76847e GIT binary patch literal 779 zcmV+m1N8ifP)le zK~!ko?U~I>RY4esAA%^m)h$p+BwDnJj6#rFSblUh`jBc-&^l<-qCcQz6b2%+YS%&t zB7z{7qL~(1*-DEb+O#gHyjRPx)57<_FnQ+A9M3&7rZ3FqoO#drJ?CTQ{msC7td2Mc zY)p~2A1DF49rNmd=B&hD1R`JzC|Kqk0;VJ{0{#GxfO?DE9l&eJJDCMQGfinin)VAg?R%E)0p?SRKNCu+aW)0OL>4#RkP*OG zikyXz0Ls9kW6qC|02ZZ$aEVVL0RYT6_WuwXz*EQmqoJ9ez49s9j6gF48bg)|G&iWxBhc!f~KR}4<78UVmi1>!{>5C!qUv3Qe*wE>7B z%6UK(q-?fbJ+KAPJ$6kXT*bQr==L;Ih%MrtTvyCo0d#vhAxkeSj>~evS$Wme5CGtW z0&yt|fCgXyXm!j>3lRU<{W8~hiR)hM8=VGPd;_!sPnHok!qWC^NoiMskNmqrecdNOuOemv zc*xQ=t7*{a1%|aeuK?eHJGk%0w*Z4ufqIL2?04^zVh?LM{sNs3hPcoxDzpFq002ov JPDHLkV1fkCNU;C_ literal 0 HcmV?d00001 diff --git a/plugins/feature/ais/map/ship.png b/plugins/feature/ais/map/ship.png new file mode 100644 index 0000000000000000000000000000000000000000..57d9355a3b8a8f285efb4c94968ad56d0d87b627 GIT binary patch literal 1292 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 literal 0 HcmV?d00001 diff --git a/plugins/feature/ais/readme.md b/plugins/feature/ais/readme.md new file mode 100644 index 000000000..7d2ae916e --- /dev/null +++ b/plugins/feature/ais/readme.md @@ -0,0 +1,44 @@ +

    AIS Plugin

    + +

    Introduction

    + +The AIS feature displays a table containing the most recent information about vessels, base-stations and aids-to-navigation, based on messages received via AIS Demodulators. + +The AIS feature can draw corresponding objects on the Map. + +

    Interface

    + +![AIS feature plugin GUI](../../../doc/img/AIS_plugin.png) + +

    Vessels Table

    + +The vessels table displays the current status for each vessel, base station or aid-to-navigation, based on the latest received messages, aggregated from all AIS Demodulators. + +* MMSI - The Maritime Mobile Service Identity number of the vessel or base station. Double clicking on this column will search for the MMSI on VesselFinder. +* Type - Vessel, Base station or Aid-to-Navigation. +* Lat - Latitude in degrees. East positive. Double clicking on this column will center the map on this object. +* Lon - Longitude in degrees. West positive. Double clicking on this column will center the map on this object. +* Course - Course over ground in degrees. +* Speed - Speed over ground in knots. +* Heading - Heading in degrees (Heading is the direction the vessel is facing, whereas course is the direction it is moving in). +* Status - Status of the vessel (E.g. Underway using engine, At anchor). +* IMO - International Maritime Organization (IMO) number which uniquely identifies a ship. Double clicking on this column will search for the IMO on https://www.vesselfinder.com/ +* Name - Name of the vessel. Double clicking on this column will search for the name on https://www.vesselfinder.com/ +* Callsign - Callsign of the vessel. +* Ship Type - Type of ship (E.g. Passenger ship, Cargo ship, Tanker) and activity (Fishing, Towing, Sailing). +* Destination - Destination the vessel is travelling to. Double clicking on this column will search for this location on the map on this object. + +Right clicking on the table header allows you to select which columns to show. The columns can be reorderd by left clicking and dragging the column header. + +

    Map

    + +The AIS feature can plot ships, base stations and aids-to-navigation on the Map. To use, simply open a Map feature and the AIS plugin will display objects based upon the messages it receives from that point. +Selecting an AIS item on the map will display a text bubble containing information from the above table. To centre the map on an item in the table, double click in the Lat or Lon columns. + +![AIS map](../../../doc/img/AIS_plugin_map.png) + +

    Attribution

    + +Map icons are by Maarten van der Werf, DE Alvida Biersack, ID and jokokerto, MY, from the Noun Project https://thenounproject.com/ + +Map icons are from http://all-free-download.com. diff --git a/plugins/feature/demodanalyzer/demodanalyzersettings.cpp b/plugins/feature/demodanalyzer/demodanalyzersettings.cpp index df2a01cf0..750989429 100644 --- a/plugins/feature/demodanalyzer/demodanalyzersettings.cpp +++ b/plugins/feature/demodanalyzer/demodanalyzersettings.cpp @@ -23,6 +23,8 @@ #include "demodanalyzersettings.h" const QStringList DemodAnalyzerSettings::m_channelTypes = { + QStringLiteral("AISDemod"), + QStringLiteral("AISMod"), QStringLiteral("AMDemod"), QStringLiteral("AMMod"), QStringLiteral("DABDemod"), @@ -38,6 +40,8 @@ const QStringList DemodAnalyzerSettings::m_channelTypes = { }; const QStringList DemodAnalyzerSettings::m_channelURIs = { + QStringLiteral("sdrangel.channel.aisdemod"), + QStringLiteral("sdrangel.channel.modais"), QStringLiteral("sdrangel.channel.amdemod"), QStringLiteral("sdrangel.channeltx.modam"), QStringLiteral("sdrangel.channel.dabdemod"), diff --git a/plugins/feature/map/mapsettings.cpp b/plugins/feature/map/mapsettings.cpp index e7bd273b1..aec102391 100644 --- a/plugins/feature/map/mapsettings.cpp +++ b/plugins/feature/map/mapsettings.cpp @@ -25,6 +25,7 @@ const QStringList MapSettings::m_pipeTypes = { QStringLiteral("ADSBDemod"), + QStringLiteral("AIS"), QStringLiteral("APRS"), QStringLiteral("StarTracker"), QStringLiteral("SatelliteTracker") @@ -32,6 +33,7 @@ const QStringList MapSettings::m_pipeTypes = { const QStringList MapSettings::m_pipeURIs = { QStringLiteral("sdrangel.channel.adsbdemod"), + QStringLiteral("sdrangel.feature.ais"), QStringLiteral("sdrangel.feature.aprs"), QStringLiteral("sdrangel.feature.startracker"), QStringLiteral("sdrangel.feature.satellitetracker") diff --git a/plugins/feature/map/mapsettings.h b/plugins/feature/map/mapsettings.h index 8a75937fb..fa512e551 100644 --- a/plugins/feature/map/mapsettings.h +++ b/plugins/feature/map/mapsettings.h @@ -58,11 +58,12 @@ struct MapSettings // The first few should match the order in m_pipeTypes for MapGUI::getSourceMask to work static const quint32 SOURCE_ADSB = 0x1; - static const quint32 SOURCE_APRS = 0x2; - static const quint32 SOURCE_STAR_TRACKER = 0x4; - static const quint32 SOURCE_SATELLITE_TRACKER = 0x8; - static const quint32 SOURCE_BEACONS = 0x10; - static const quint32 SOURCE_STATION = 0x20; + static const quint32 SOURCE_AIS = 0x2; + static const quint32 SOURCE_APRS = 0x4; + static const quint32 SOURCE_STAR_TRACKER = 0x8; + static const quint32 SOURCE_SATELLITE_TRACKER = 0x10; + static const quint32 SOURCE_BEACONS = 0x20; + static const quint32 SOURCE_STATION = 0x40; }; #endif // INCLUDE_FEATURE_MAPSETTINGS_H_ diff --git a/plugins/feature/map/mapsettingsdialog.ui b/plugins/feature/map/mapsettingsdialog.ui index 8ac4c5eaa..6308e36ce 100644 --- a/plugins/feature/map/mapsettingsdialog.ui +++ b/plugins/feature/map/mapsettingsdialog.ui @@ -49,6 +49,14 @@ Checked + + + AIS + + + Checked + + APRS diff --git a/plugins/feature/map/readme.md b/plugins/feature/map/readme.md index 86efe3203..c000a12aa 100644 --- a/plugins/feature/map/readme.md +++ b/plugins/feature/map/readme.md @@ -7,11 +7,12 @@ On top of this, it can plot data from other plugins, such as: * APRS symbols from the APRS Feature, * Aircraft from the ADS-B Demodulator, +* Ships from the AIS Demodulator, * Satellites from the Satellite Tracker, * The Sun, Moon and Stars from the Star Tracker, * Beacons based on the IARU Region 1 beacon database. -It can also create tracks showing the path aircraft and APRS objects have taken, as well as predicted paths for satellites. +It can also create tracks showing the path aircraft, ships and APRS objects have taken, as well as predicted paths for satellites. ![Map feature](../../../doc/img/Map_plugin_beacons.png) diff --git a/plugins/feature/pertester/readme.md b/plugins/feature/pertester/readme.md index 100b1e6fd..b5afb1a70 100644 --- a/plugins/feature/pertester/readme.md +++ b/plugins/feature/pertester/readme.md @@ -2,7 +2,7 @@

    Introduction

    -The Packet Error Rate (PER) Tester feature can be used to measure the packet error rate over digital, packet based protcols such as AX.25 (Packet mod/demod), LoRa (ChipChat mod/demod) and 802.15.4. +The Packet Error Rate (PER) Tester feature can be used to measure the packet error rate over digital, packet based protcols such as AX.25 (Packet mod/demod), LoRa (ChipChat mod/demod), AIS and 802.15.4. The PER Tester feature allows you to define the contents of the packets to transmit, which can include a per-packet 32-bit identifier, as well as a user-defined or random payload, how many packets are sent and the interval between them. @@ -42,7 +42,7 @@ Specify the interval in seconds between packet transmissions. Specify the contents of the packet to transmit and expect to be received. Data should be entered in hexidecimal bytes (E.g: 00 11 22 33 44). -The exact format required will depend on the underlying protocol being used. For AX.25 using the Packet modulator, LoRo using the ChirpChat modulator and 802.15.4, it is not necessary to include the trailing CRC, as this is appended automatically by the SDRangel modulators. +The exact format required will depend on the underlying protocol being used. For AX.25 using the Packet modulator, LoRo using the ChirpChat modulator, AIS and 802.15.4, it is not necessary to include the trailing CRC, as this is appended automatically by the SDRangel modulators. Aside from hex values, a number of variables can be used: @@ -74,6 +74,7 @@ This can be used in cases where the demodulator outputs a different byte sequenc * For AX.25 (with Packet mod/demod), set Leading to 0 and Trailing to 2. * For LoRa (with ChirpChat mod/demod), set Leading to 0 and Trailing to 0. +* For AIS set Leading to 0 and Trailing to 0.

    8: TX UDP port

    diff --git a/sdrbase/CMakeLists.txt b/sdrbase/CMakeLists.txt index 1320e42d7..20b88a5e7 100644 --- a/sdrbase/CMakeLists.txt +++ b/sdrbase/CMakeLists.txt @@ -176,6 +176,7 @@ set(sdrbase_SOURCES settings/preset.cpp settings/mainsettings.cpp + util/ais.cpp util/ax25.cpp util/aprs.cpp util/astronomy.cpp @@ -313,6 +314,7 @@ set(sdrbase_HEADERS dsp/kissfft.h dsp/kissengine.h dsp/firfilter.h + dsp/gaussian.h dsp/mimochannel.h dsp/misc.h dsp/movingaverage.h @@ -372,6 +374,7 @@ set(sdrbase_HEADERS settings/preset.h settings/mainsettings.h + util/ais.h util/ax25.h util/aprs.h util/astronomy.h diff --git a/sdrbase/dsp/gaussian.h b/sdrbase/dsp/gaussian.h new file mode 100644 index 000000000..1240a18e2 --- /dev/null +++ b/sdrbase/dsp/gaussian.h @@ -0,0 +1,121 @@ +/////////////////////////////////////////////////////////////////////////////////// +// Copyright (C) 2015 Edouard Griffiths, F4EXB // +// Copyright (C) 2020 Jon Beniston, M7RCE // +// // +// This program is free software; you can redistribute it and/or modify // +// it under the terms of the GNU General Public License as published by // +// the Free Software Foundation as version 3 of the License, or // +// (at your option) any later version. // +// // +// This program is distributed in the hope that it will be useful, // +// but WITHOUT ANY WARRANTY; without even the implied warranty of // +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // +// GNU General Public License V3 for more details. // +// // +// You should have received a copy of the GNU General Public License // +// along with this program. If not, see . // +/////////////////////////////////////////////////////////////////////////////////// + +#ifndef INCLUDE_GAUSSIAN_H +#define INCLUDE_GAUSSIAN_H + +#include +#include "dsp/dsptypes.h" + +// Standard values for bt +#define GAUSSIAN_BT_BLUETOOTH 0.5 +#define GAUSSIAN_BT_GSM 0.3 +#define GAUSSIAN_BT_CCSDS 0.25 +#define GAUSSIAN_BT_802_15_4 0.5 +#define GAUSSIAN_BT_AIS 0.5 + +// Gaussian low-pass filter for pulse shaping +// https://onlinelibrary.wiley.com/doi/pdf/10.1002/9780470041956.app2 +// Unlike raisedcosine.h, this should be feed NRZ values rather than impulse stream, as described here: +// https://www.mathworks.com/matlabcentral/answers/107231-why-does-the-pulse-shape-generated-by-gaussdesign-differ-from-that-used-in-the-comm-gmskmodulator-ob +template class Gaussian { +public: + Gaussian() : m_ptr(0) { } + + // bt - 3dB bandwidth symbol time product + // symbolSpan - number of symbols over which the filter is spread + // samplesPerSymbol - number of samples per symbol + void create(double bt, int symbolSpan, int samplesPerSymbol) + { + int nTaps = symbolSpan * samplesPerSymbol + 1; + int i; + + // check constraints + if(!(nTaps & 1)) { + qDebug("Gaussian filter has to have an odd number of taps"); + nTaps++; + } + + // make room + m_samples.resize(nTaps); + for(int i = 0; i < nTaps; i++) + m_samples[i] = 0; + m_ptr = 0; + m_taps.resize(nTaps / 2 + 1); + + // See eq B.2 - this is alpha over Ts + double alpha_t = std::sqrt(std::log(2.0) / 2.0) / (bt); + double sqrt_pi_alpha_t = std::sqrt(M_PI) / alpha_t; + + // calculate filter taps + for(i = 0; i < nTaps / 2 + 1; i++) + { + double t = (i - (nTaps / 2)) / (double)samplesPerSymbol; + + // See eq B.5 + m_taps[i] = sqrt_pi_alpha_t * std::exp(-std::pow(t * M_PI / alpha_t, 2.0)); + } + + // normalize + double sum = 0; + for(i = 0; i < (int)m_taps.size() - 1; i++) + sum += m_taps[i] * 2; + sum += m_taps[i]; + for(i = 0; i < (int)m_taps.size(); i++) + m_taps[i] /= sum; + } + + Type filter(Type sample) + { + Type acc = 0; + unsigned int n_samples = m_samples.size(); + unsigned int n_taps = m_taps.size() - 1; + unsigned int a = m_ptr; + unsigned int b = a == n_samples - 1 ? 0 : a + 1; + + m_samples[m_ptr] = sample; + + for (unsigned int i = 0; i < n_taps; ++i) + { + acc += (m_samples[a] + m_samples[b]) * m_taps[i]; + + a = (a == 0) ? n_samples - 1 : a - 1; + b = (b == n_samples - 1) ? 0 : b + 1; + } + + acc += m_samples[a] * m_taps[n_taps]; + + m_ptr = (m_ptr == n_samples - 1) ? 0 : m_ptr + 1; + + return acc; + } +/* + void printTaps() + { + for (int i = 0; i < m_taps.size(); i++) + printf("%.4f ", m_taps[i]); + printf("\n"); + } +*/ +private: + std::vector m_taps; + std::vector m_samples; + int m_ptr; +}; + +#endif // INCLUDE_GAUSSIAN_H diff --git a/sdrbase/util/ais.cpp b/sdrbase/util/ais.cpp new file mode 100644 index 000000000..52ca7d0b7 --- /dev/null +++ b/sdrbase/util/ais.cpp @@ -0,0 +1,757 @@ +/////////////////////////////////////////////////////////////////////////////////// +// Copyright (C) 2021 Jon Beniston, M7RCE // +// // +// This program is free software; you can redistribute it and/or modify // +// it under the terms of the GNU General Public License as published by // +// the Free Software Foundation as version 3 of the License, or // +// (at your option) any later version. // +// // +// This program is distributed in the hope that it will be useful, // +// but WITHOUT ANY WARRANTY; without even the implied warranty of // +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // +// GNU General Public License V3 for more details. // +// // +// You should have received a copy of the GNU General Public License // +// along with this program. If not, see . // +/////////////////////////////////////////////////////////////////////////////////// + +#include "ais.h" + +AISMessage::AISMessage(const QByteArray ba) +{ + // All AIS messages have these 3 fields in common + m_id = (ba[0] & 0xff) >> 2 & 0x3f; + m_repeatIndicator = ba[0] & 0x3; + m_mmsi = ((ba[1] & 0xff) << 22) | ((ba[2] & 0xff) << 14) | ((ba[3] & 0xff) << 6) | ((ba[4] & 0xff) >> 2); + m_bytes = ba; +} + +QString AISMessage::toHex() +{ + return m_bytes.toHex(); +} + +// See: https://gpsd.gitlab.io/gpsd/AIVDM.html +QString AISMessage::toNMEA(QByteArray bytes) +{ + QStringList nmeaSentences; + + // Max payload is ~61 chars -> 366 bits + int sentences = bytes.size() / 45 + 1; + int sentence = 1; + + int bits = 8; + int i = 0; + while (i < bytes.size()) + { + QStringList nmeaSentence; + QStringList nmea; + QStringList payload; + + nmea.append(QString("AIVDM,%1,%2,%3,,").arg(sentences).arg(sentence).arg(sentences > 1 ? "1" : "")); + + int maxPayload = 80 - 1 - nmea[0].length() - 5; + + // Encode message data in 6-bit ASCII + while ((payload.size() < maxPayload) && (i < bytes.size())) + { + int c = 0; + for (int j = 0; j < 6; j++) + { + c = (c << 1) | ((bytes[i] >> (bits - 1)) & 0x1); + bits--; + if (bits == 0) + { + i++; + bits = 8; + } + } + if (c >= 40) { + c += 56; + } else { + c += 48; + } + payload.append(QChar(c)); + } + + nmea.append(payload); + nmea.append(QString(",%1").arg((i == bytes.size()) ? (8 - bits) : 0)); // Number of filler bits to ignore + + // Calculate checksum + QString nmeaProtected = nmea.join(""); + int checksum = AISMessage::nmeaChecksum(nmeaProtected); + + // Construct complete sentence with leading ! and trailing checksum + nmeaSentence.append("!"); + nmeaSentence.append(nmeaProtected); + nmeaSentence.append(QString("*%1").arg(checksum, 2, 16, QChar('0'))); + + nmeaSentences.append(nmeaSentence.join("")); + sentence++; + } + + return nmeaSentences.join("\n"); +} + +QString AISMessage::toNMEA() +{ + return AISMessage::toNMEA(m_bytes); +} + +qint8 AISMessage::nmeaChecksum(QString string) +{ + qint8 checksum = 0; + + for (int i = 0; i < string.length(); i++) + { + qint8 c = (qint8)string[i].toLatin1(); + checksum ^= c; + } + + return checksum; +} + +// Type as in message 5 and 19 +QString AISMessage::typeToString(quint8 type) +{ + if (type == 0) { + return "N/A"; + } else if ((type >= 100) && (type < 199)) { + return "Preserved for regional use"; + } else if ((type >= 200) && (type <= 255)) { + return "Preserved for future use"; + } else if ((type >= 50) && (type <= 59)) { + const QStringList specialCrafts = { + "Pilot vessel", + "Search and rescue vessel", + "Tug", + "Port tender", + "Anti-pollution vessel", + "Law enforcement vessel", + "Spare (56)", + "Spare (57)", + "Medical transport", + "Ships and aircraft of States not parties to an armed conflict" + }; + return specialCrafts[type-50]; + } else { + int firstDigit = type / 10; + int secondDigit = type % 10; + + const QStringList shipType = { + "0", + "Reserved (1)", + "WIG", + "Vessel", + "HSC", + "5", + "Passenger", + "Cargo", + "Tanker", + "Other" + }; + const QStringList activity = { + "Fishing", + "Towing", + "Towing", + "Dredging or underwater operations", + "Diving operations", + "Military operations", + "Sailing", + "Pleasure craft", + "Reserved (8)", + "Reserved (9)" + }; + + if (firstDigit == 3) { + return shipType[firstDigit] + " - " + activity[secondDigit]; + } else { + return shipType[firstDigit]; + } + } +} + +AISMessage* AISMessage::decode(const QByteArray ba) +{ + int id = (ba[0] >> 2) & 0x3f; + + if ((id == 1) || (id == 2) || (id == 3)) { + return new AISPositionReport(ba); + } else if ((id == 4) || (id == 11)) { + return new AISBaseStationReport(ba); + } else if (id == 5) { + return new AISShipStaticAndVoyageData(ba); + } else if (id == 6) { + return new AISBinaryMessage(ba); + } else if (id == 7) { + return new AISBinaryAck(ba); + } else if (id == 8) { + return new AISBinaryBroadcast(ba); + } else if (id == 9) { + return new AISSARAircraftPositionReport(ba); + } else if (id == 10) { + return new AISUTCInquiry(ba); + } else if (id == 12) { + return new AISSafetyMessage(ba); + } else if (id == 13) { + return new AISSafetyAck(ba); + } else if (id == 14) { + return new AISSafetyBroadcast(ba); + } else if (id == 15) { + return new AISInterrogation(ba); + } else if (id == 16) { + return new AISAssignedModeCommand(ba); + } else if (id == 17) { + return new AISGNSSBroadcast(ba); + } else if (id == 18) { + return new AISStandardClassBPositionReport(ba); + } else if (id == 19) { + return new AISExtendedClassBPositionReport(ba); + } else if (id == 20) { + return new AISDatalinkManagement(ba); + } else if (id == 21) { + return new AISAidsToNavigationReport(ba); + } else if (id == 22) { + return new AISChannelManagement(ba); + } else if (id == 23) { + return new AISGroupAssignment(ba); + } else if (id == 24) { + return new AISStaticDataReport(ba); + } else if (id == 25) { + return new AISSingleSlotBinaryMessage(ba); + } else if (id == 26) { + return new AISMultipleSlotBinaryMessage(ba); + } else if (id == 27) { + return new AISLongRangePositionReport(ba); + } else { + return new AISUnknownMessageID(ba); + } +} + +// Extract 6-bit ASCII string +QString AISMessage::getString(const QByteArray ba, int byteIdx, int bitsLeft, int chars) +{ + QString s; + for (int i = 0; i < chars; i++) + { + // Extract 6-bits + int c = 0; + for (int j = 0; j < 6; j++) + { + c = (c << 1) | ((ba[byteIdx] >> (bitsLeft - 1)) & 0x1); + bitsLeft--; + if (bitsLeft == 0) + { + byteIdx++; + bitsLeft = 8; + } + } + // Map from 6-bit to 8-bit ASCII + if (c < 32) { + c |= 0x40; + } + s.append(c); + } + // Remove leading/trailing spaces + s = s.trimmed(); + // Remave @s, which indiciate no character + while (s.endsWith("@")) { + s = s.left(s.length() - 1); + } + while (s.startsWith("@")) { + s = s.mid(1); + } + return s; +} + +AISPositionReport::AISPositionReport(QByteArray ba) : + AISMessage(ba) +{ + m_status = ((ba[4] & 0x3) << 2) | (ba[5] >> 6) & 0x3; + + int rateOfTurn = ((ba[5] << 2) & 0xfc) | ((ba[6] >> 6) & 0x3); + if (rateOfTurn == 127) { + m_rateOfTurn = 720.0f; + } else if (rateOfTurn == -127) { + m_rateOfTurn = -720.0f; + } else { + m_rateOfTurn = (rateOfTurn / 4.733f) * (rateOfTurn / 4.733f); + } + m_rateOfTurnAvailable = rateOfTurn != 0x80; + + int sog = ((ba[6] & 0x3f) << 4) | ((ba[7] >> 4) & 0xf); + m_speedOverGroundAvailable = sog != 1023; + m_speedOverGround = sog * 0.1f; + + m_positionAccuracy = (ba[7] >> 3) & 0x1; + + int32_t longitude = ((ba[7] & 0x7) << 25) | ((ba[8] & 0xff) << 17) | ((ba[9] & 0xff) << 9) | ((ba[10] & 0xff) << 1) | ((ba[11] >> 7) & 1); + longitude = (longitude << 4) >> 4; + m_longitudeAvailable = longitude != 0x6791ac0; + m_longitude = longitude / 60.0f / 10000.0f; + + int32_t latitude = ((ba[11] & 0x7f) << 20) | ((ba[12] & 0xff) << 12) | ((ba[13] & 0xff) << 4) | ((ba[14] >> 4) & 0x4); + latitude = (latitude << 5) >> 5; + m_latitudeAvailable = latitude != 0x3412140; + m_latitude = latitude / 60.0f / 10000.0f; + + int cog = ((ba[14] & 0xf) << 8) | (ba[15] & 0xff); + m_courseAvailable = cog != 3600; + m_course = cog * 0.1f; + + m_heading = ((ba[16] & 0xff) << 1) | ((ba[17] >> 7) & 0x1); + m_headingAvailable = m_heading != 511; + + m_timeStamp = (ba[17] >> 1) & 0x3f; + + m_specialManoeuvre = ((ba[17] & 0x1) << 1) | ((ba[18] >> 7) & 0x1); +} + +QString AISPositionReport::getStatusString(int status) +{ + const QStringList statuses = { + "Under way using engine", + "At anchor", + "Not under command", + "Restricted manoeuvrability", + "Constrained by her draught", + "Moored", + "Aground", + "Engaged in fishing", + "Under way sailing", + "Reserved for future amendment of navigational status for ships carrying DG, HS, or MP, or IMO hazard or pollutant category C (HSC)", + "Reserved for future amendment of navigational status for carrying DG, HS or MP, or IMO hazard or pollutant category A (WIG)", + "Reserved for future use", + "Reserved for future use", + "Reserved for future use", + "Reserved for future use", + "Not defined" + }; + return statuses[status]; +} + +QString AISPositionReport::toString() +{ + return QString("Lat: %1%6 Lon: %2%6 Speed: %3 knts Course: %4%6 Status: %5") + .arg(m_latitude) + .arg(m_longitude) + .arg(m_speedOverGround) + .arg(m_course) + .arg(AISPositionReport::getStatusString(m_status)) + .arg(QChar(0xb0)); +} + +AISBaseStationReport::AISBaseStationReport(QByteArray ba) : + AISMessage(ba) +{ + int year = ((ba[4] & 0x3) << 12) | ((ba[5] & 0xff) << 4) | ((ba[6] >> 4) & 0xf); + int month = ba[6] & 0xf; + int day = ((ba[7] >> 3) & 0x1f); + int hour = ((ba[7] & 0x7) << 2) | ((ba[8] >> 6) & 0x3); + int minute = ba[8] & 0x3f; + int second = (ba[9] >> 2) & 0x3f; + m_utc = QDateTime(QDate(year, month, day), QTime(hour, minute, second), Qt::UTC); + + m_positionAccuracy = (ba[9] >> 1) & 0x1; + + int32_t longitude = ((ba[9] & 0x1) << 27) | ((ba[10] & 0xff) << 19) | ((ba[11] & 0xff) << 11) | ((ba[12] & 0xff) << 3) | ((ba[13] >> 5) & 0x7); + longitude = (longitude << 4) >> 4; + m_longitudeAvailable = longitude != 0x6791ac0; + m_longitude = longitude / 60.0f / 10000.0f; + + int32_t latitude = ((ba[13] & 0x1f) << 22) | ((ba[14] & 0xff) << 14) | ((ba[15] & 0xff) << 6) | ((ba[16] >> 2) & 0x3f); + latitude = (latitude << 5) >> 5; + m_latitudeAvailable = latitude != 0x3412140; + m_latitude = latitude / 60.0f / 10000.0f; +} + +QString AISBaseStationReport::toString() +{ + return QString("Lat: %1%3 Lon: %2%3 %4") + .arg(m_latitude) + .arg(m_longitude) + .arg(QChar(0xb0)) + .arg(m_utc.toString()); +} + +AISShipStaticAndVoyageData::AISShipStaticAndVoyageData(QByteArray ba) : + AISMessage(ba) +{ + m_version = ba[4] & 0x3; + m_imo = ((ba[5] & 0xff) << 22) | ((ba[6] & 0xff) << 14) | ((ba[7] & 0xff) << 6) | ((ba[8] >> 2) & 0x3f); + m_callsign = AISMessage::getString(ba, 8, 2, 7); + m_name = AISMessage::getString(ba, 14, 8, 20); + m_type = ba[29] & 0xff; + m_dimension = ((ba[30] & 0xff) << 22) | ((ba[31] & 0xff) << 14) | ((ba[32] & 0xff) << 6) | ((ba[33] >> 2) & 0x3f); + m_positionFixing = ((ba[33] & 0x3) << 2) | ((ba[34] >> 6) & 0x3); + m_eta = ((ba[34] & 0x3f) << 14) | ((ba[35] & 0xff) << 6) | ((ba[36] >> 2) & 0x3f); + m_draught = ((ba[36] & 0x3) << 6) | ((ba[37] >> 2) & 0x3f); + m_destination = AISMessage::getString(ba, 37, 2, 20); +} + +QString AISShipStaticAndVoyageData::toString() +{ + return QString("IMO: %1 Callsign: %2 Name: %3 Type: %4 Destination: %5") + .arg(m_imo == 0 ? "N/A" : QString::number(m_imo)) + .arg(m_callsign) + .arg(m_name) + .arg(AISMessage::typeToString(m_type)) + .arg(m_destination); +} + +AISBinaryMessage::AISBinaryMessage(QByteArray ba) : + AISMessage(ba) +{ + m_sequenceNumber = ba[4] & 0x3; + m_destinationId = ((ba[5] & 0xff) << 22) | ((ba[6] & 0xff) << 14) | ((ba[7] & 0xff) << 6) | ((ba[8] >> 2) & 0x3f); + m_retransmitFlag = (ba[8] >> 1) & 0x1; +} + +QString AISBinaryMessage::toString() +{ + return QString("Seq No: %1 Destination: %2 Retransmit: %3") + .arg(m_sequenceNumber) + .arg(m_destinationId) + .arg(m_retransmitFlag); +} + + +AISBinaryAck::AISBinaryAck(QByteArray ba) : + AISMessage(ba) +{ +} + +AISBinaryBroadcast::AISBinaryBroadcast(QByteArray ba) : + AISMessage(ba) +{ +} + +AISSARAircraftPositionReport::AISSARAircraftPositionReport(QByteArray ba) : + AISMessage(ba) +{ + m_altitude = ((ba[4] & 0x3) << 10) | ((ba[5] & 0xff) << 2) | ((ba[6] >> 6) & 0x3); + m_altitudeAvailable = m_altitude != 4095; + + int sog = ((ba[6] & 0x3f) << 4) | ((ba[7] >> 4) & 0xf); + m_speedOverGroundAvailable = sog != 1023; + m_speedOverGround = sog * 0.1f; + + m_positionAccuracy = (ba[7] >> 3) & 0x1; + + int32_t longitude = ((ba[7] & 0x7) << 25) | ((ba[8] & 0xff) << 17) | ((ba[9] & 0xff) << 9) | ((ba[10] & 0xff) << 1) | ((ba[11] >> 7) & 1); + longitude = (longitude << 4) >> 4; + m_longitudeAvailable = longitude != 0x6791ac0; + m_longitude = longitude / 60.0f / 10000.0f; + + int32_t latitude = ((ba[11] & 0x7f) << 20) | ((ba[12] & 0xff) << 12) | ((ba[13] & 0xff) << 4) | ((ba[14] >> 4) & 0x4); + latitude = (latitude << 5) >> 5; + m_latitudeAvailable = latitude != 0x3412140; + m_latitude = latitude / 60.0f / 10000.0f; + + int cog = ((ba[14] & 0xf) << 8) | (ba[15] & 0xff); + m_courseAvailable = cog != 3600; + m_course = cog * 0.1f; + + m_timeStamp = (ba[16] >> 2) & 0x3f; +} + +QString AISSARAircraftPositionReport::toString() +{ + return QString("Lat: %1%6 Lon: %2%6 Speed: %3 knts Course: %4%6 Alt: %5 m") + .arg(m_latitude) + .arg(m_longitude) + .arg(m_speedOverGround) + .arg(m_course) + .arg(m_altitude) + .arg(QChar(0xb0)); +} + + +AISUTCInquiry::AISUTCInquiry(QByteArray ba) : + AISMessage(ba) +{ +} + +AISSafetyMessage::AISSafetyMessage(QByteArray ba) : + AISMessage(ba) +{ + m_sequenceNumber = ba[4] & 0x3; + m_destinationId = ((ba[5] & 0xff) << 22) | ((ba[6] & 0xff) << 14) | ((ba[7] & 0xff) << 6) | ((ba[8] >> 2) & 0x3f); + m_retransmitFlag = (ba[8] >> 1) & 0x1; + m_safetyRelatedText = AISMessage::getString(ba, 9, 0, (ba.size() - 9) * 8 / 6); +} + +QString AISSafetyMessage::toString() +{ + return QString("To %1: Safety message: %2").arg(m_destinationId).arg(m_safetyRelatedText); +} + +AISSafetyAck::AISSafetyAck(QByteArray ba) : + AISMessage(ba) +{ +} + +AISSafetyBroadcast::AISSafetyBroadcast(QByteArray ba) : + AISMessage(ba) +{ + m_safetyRelatedText = AISMessage::getString(ba, 5, 0, (ba.size() - 6) * 8 / 6); +} + +QString AISSafetyBroadcast::toString() +{ + return QString("Safety message: %1").arg(m_safetyRelatedText); +} + +AISInterrogation::AISInterrogation(QByteArray ba) : + AISMessage(ba) +{ +} + +AISAssignedModeCommand::AISAssignedModeCommand(QByteArray ba) : + AISMessage(ba) +{ +} + +AISGNSSBroadcast::AISGNSSBroadcast(QByteArray ba) : + AISMessage(ba) +{ +} + +AISStandardClassBPositionReport::AISStandardClassBPositionReport(QByteArray ba) : + AISMessage(ba) +{ + int sog = ((ba[5] & 0x3) << 8) | (ba[6] & 0xff); + m_speedOverGroundAvailable = sog != 1023; + m_speedOverGround = sog * 0.1f; + + m_positionAccuracy = (ba[7] >> 7) & 0x1; + + int32_t longitude = ((ba[7] & 0x7f) << 21) | ((ba[8] & 0xff) << 13) | ((ba[9] & 0xff) << 5) | ((ba[10] >> 3) & 0x1f); + longitude = (longitude << 4) >> 4; + m_longitudeAvailable = longitude != 0x6791ac0; + m_longitude = longitude / 60.0f / 10000.0f; + + int32_t latitude = ((ba[10] & 0x3) << 24) | ((ba[11] & 0xff) << 16) | ((ba[12] & 0xff) << 8) | (ba[13] & 0xff); + latitude = (latitude << 5) >> 5; + m_latitudeAvailable = latitude != 0x3412140; + m_latitude = latitude / 60.0f / 10000.0f; + + int cog = ((ba[14] & 0xff) << 4) | ((ba[15] >> 4) & 0xf); + m_courseAvailable = cog != 3600; + m_course = cog * 0.1f; + + m_heading = ((ba[15] & 0xf) << 5) | ((ba[17] >> 3) & 0x1f); + m_headingAvailable = m_heading != 511; + + m_timeStamp = ((ba[17] & 0x7) << 3) | ((ba[18] >> 5) & 0x7); +} + +QString AISStandardClassBPositionReport::toString() +{ + return QString("Lat: %1%5 Lon: %2%5 Speed: %3 knts Course: %4%5") + .arg(m_latitude) + .arg(m_longitude) + .arg(m_speedOverGround) + .arg(m_course) + .arg(QChar(0xb0)); +} + + +AISExtendedClassBPositionReport::AISExtendedClassBPositionReport(QByteArray ba) : + AISMessage(ba) +{ + int sog = ((ba[5] & 0x3) << 8) | (ba[6] & 0xff); + m_speedOverGroundAvailable = sog != 1023; + m_speedOverGround = sog * 0.1f; + + m_positionAccuracy = (ba[7] >> 7) & 0x1; + + int32_t longitude = ((ba[7] & 0x7f) << 21) | ((ba[8] & 0xff) << 13) | ((ba[9] & 0xff) << 5) | ((ba[10] >> 3) & 0x1f); + longitude = (longitude << 4) >> 4; + m_longitudeAvailable = longitude != 0x6791ac0; + m_longitude = longitude / 60.0f / 10000.0f; + + int32_t latitude = ((ba[10] & 0x3) << 24) | ((ba[11] & 0xff) << 16) | ((ba[12] & 0xff) << 8) | (ba[13] & 0xff); + latitude = (latitude << 5) >> 5; + m_latitudeAvailable = latitude != 0x3412140; + m_latitude = latitude / 60.0f / 10000.0f; + + int cog = ((ba[14] & 0xff) << 4) | ((ba[15] >> 4) & 0xf); + m_courseAvailable = cog != 3600; + m_course = cog * 0.1f; + + m_heading = ((ba[15] & 0xf) << 5) | ((ba[17] >> 3) & 0x1f); + m_headingAvailable = m_heading != 511; + + m_timeStamp = ((ba[17] & 0x7) << 3) | ((ba[18] >> 5) & 0x7); + + m_name = AISMessage::getString(ba, 18, 1, 20); +} + +QString AISExtendedClassBPositionReport::toString() +{ + return QString("Lat: %1%5 Lon: %2%5 Speed: %3 knts Course: %4%5 Name: %6") + .arg(m_latitude) + .arg(m_longitude) + .arg(m_speedOverGround) + .arg(m_course) + .arg(QChar(0xb0)) + .arg(m_name); +} + +AISDatalinkManagement::AISDatalinkManagement(QByteArray ba) : + AISMessage(ba) +{ +} + +AISAidsToNavigationReport::AISAidsToNavigationReport(QByteArray ba) : + AISMessage(ba) +{ + m_type = ((ba[4] & 0x3) << 3) | ((ba[5] >> 5) & 0x7); + + m_name = AISMessage::getString(ba, 5, 5, 20); + + m_positionAccuracy = (ba[20] >> 4) & 0x1; + + int32_t longitude = ((ba[20] & 0xf) << 24) | ((ba[21] & 0xff) << 16) | ((ba[22] & 0xff) << 8) | (ba[23] & 0xff); + longitude = (longitude << 4) >> 4; + m_longitudeAvailable = longitude != 0x6791ac0; + m_longitude = longitude / 60.0f / 10000.0f; + + int32_t latitude = ((ba[24] & 0xff) << 19) | ((ba[25] & 0xff) << 11) | ((ba[26] & 0xff) << 3) | ((ba[27] >> 5) & 0x7); + latitude = (latitude << 5) >> 5; + m_latitudeAvailable = latitude != 0x3412140; + m_latitude = latitude / 60.0f / 10000.0f; +} + +QString AISAidsToNavigationReport::toString() +{ + const QStringList types = { + "N/A", + "Reference point", + "RACON", + "Fixed structure off short", + "Emergency wreck marking buoy", + "Light, without sectors", + "Light, with sectors", + "Leading light front", + "Leading light rear", + "Beacon, Cardinal N", + "Beacon, Cardinal E", + "Beacon, Cardinal S", + "Beacon, Cardianl W", + "Beacon, Port hand", + "Beacon, Starboard hand", + "Beacon, Preferred channel port hand", + "Beacon, Preferred channel starboard hand", + "Beacon, Isolated danger", + "Beacon, Safe water", + "Beacon, Special mark" + "Cardinal mark N", + "Cardinal mark E", + "Cardinal mark S", + "Cardinal mark W", + "Port hand mark", + "Starboard hand mark", + "Preferred channel port hand", + "Preferred channel starboard hand", + "Isolated danger", + "Safe water", + "Special mark", + "Light vessel/LANBY/Rigs" + }; + return QString("Lat: %1%5 Lon: %2%5 Name: %3 Type: %4") + .arg(m_latitude) + .arg(m_longitude) + .arg(m_name) + .arg(types[m_type]) + .arg(QChar(0xb0)); +} + +AISChannelManagement::AISChannelManagement(QByteArray ba) : + AISMessage(ba) +{ +} + +AISGroupAssignment::AISGroupAssignment(QByteArray ba) : + AISMessage(ba) +{ +} + +AISStaticDataReport::AISStaticDataReport(QByteArray ba) : + AISMessage(ba) +{ + m_partNumber = ba[4] & 0x3; + if (m_partNumber == 0) + { + m_name = AISMessage::getString(ba, 5, 0, 20); + } + else if (m_partNumber == 1) + { + m_type = ba[5] & 0xff; + m_vendorId = AISMessage::getString(ba, 6, 0, 7); + m_callsign = AISMessage::getString(ba, 11, 6, 7); + } +} + +QString AISStaticDataReport::toString() +{ + if (m_partNumber == 0) { + return QString("Name: %1").arg(m_name); + } else if (m_partNumber == 1) { + return QString("Type: %1 Vendor ID: %2 Callsign: %3").arg(typeToString(m_type)).arg(m_vendorId).arg(m_callsign); + } else { + return ""; + } +} + +AISSingleSlotBinaryMessage::AISSingleSlotBinaryMessage(QByteArray ba) : + AISMessage(ba) +{ +} + +AISMultipleSlotBinaryMessage::AISMultipleSlotBinaryMessage(QByteArray ba) : + AISMessage(ba) +{ +} + +AISLongRangePositionReport::AISLongRangePositionReport(QByteArray ba) : + AISMessage(ba) +{ + m_positionAccuracy = (ba[4] >> 1) & 0x1; + m_raim = ba[4] & 0x1; + m_status = (ba[5] >> 4) & 0xf; + + int32_t longitude = ((ba[5] & 0xf) << 14) | ((ba[6] & 0xff) << 6) | ((ba[7] >> 2) & 0x3f); + longitude = (longitude << 14) >> 14; + m_longitudeAvailable = longitude != 0x1a838; + m_longitude = longitude / 60.0f / 10.0f; + + int32_t latitude = ((ba[7] & 0x3) << 15) | ((ba[8] & 0xff) << 7) | ((ba[9] >> 1) & 0x7f); + latitude = (latitude << 15) >> 15; + m_latitudeAvailable = latitude != 0xd548; + m_latitude = latitude / 60.0f / 10.0f; + + m_speedOverGround = ((ba[9] & 0x1) << 5) | ((ba[10] >> 3) & 0x1f); + m_speedOverGroundAvailable = m_speedOverGround != 63; + + m_course = ((ba[10] & 0x7) << 6) | ((ba[11] >> 2) & 0x3f); + m_courseAvailable = m_course != 512; +} + +QString AISLongRangePositionReport::toString() +{ + return QString("Lat: %1%6 Lon: %2%6 Speed: %3 knts Course: %4%6 Status: %5") + .arg(m_latitude) + .arg(m_longitude) + .arg(m_speedOverGround) + .arg(m_course) + .arg(AISPositionReport::getStatusString(m_status)) + .arg(QChar(0xb0)); +} + +AISUnknownMessageID::AISUnknownMessageID(QByteArray ba) : + AISMessage(ba) +{ +} + diff --git a/sdrbase/util/ais.h b/sdrbase/util/ais.h new file mode 100644 index 000000000..81f66e0bb --- /dev/null +++ b/sdrbase/util/ais.h @@ -0,0 +1,385 @@ +/////////////////////////////////////////////////////////////////////////////////// +// Copyright (C) 2021 Jon Beniston, M7RCE // +// // +// This program is free software; you can redistribute it and/or modify // +// it under the terms of the GNU General Public License as published by // +// the Free Software Foundation as version 3 of the License, or // +// (at your option) any later version. // +// // +// This program is distributed in the hope that it will be useful, // +// but WITHOUT ANY WARRANTY; without even the implied warranty of // +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // +// GNU General Public License V3 for more details. // +// // +// You should have received a copy of the GNU General Public License // +// along with this program. If not, see . // +/////////////////////////////////////////////////////////////////////////////////// + +#ifndef INCLUDE_AIS_H +#define INCLUDE_AIS_H + +#include +#include +#include +#include + +#include "export.h" + +class SDRBASE_API AISMessage { +public: + + int m_id; + int m_repeatIndicator; + int m_mmsi; + + AISMessage(const QByteArray ba); + virtual QString getType() = 0; + virtual bool hasPosition() { return false; } + virtual float getLatitude() { return 0.0f; } + virtual float getLongitude() { return 0.0f; } + virtual bool hasCourse() { return false; } + virtual float getCourse() { return 0.0f; } + virtual bool hasSpeed() { return false; } + virtual float getSpeed() { return 0.0f; } + virtual bool hasHeading() { return false; } + virtual float getHeading() { return 0.0f; } + virtual QString toString() { return ""; } + QString toHex(); + QString toNMEA(); + + static AISMessage* decode(const QByteArray ba); + static QString toNMEA(const QByteArray ba); + static QString typeToString(quint8 type); + +protected: + static QString getString(QByteArray ba, int byteIdx, int bitsLeft, int chars); + static qint8 nmeaChecksum(QString string); + QByteArray m_bytes; + +}; + +class SDRBASE_API AISPositionReport : public AISMessage { +public: + int m_status; + bool m_rateOfTurnAvailable; + float m_rateOfTurn; // Degrees per minute + bool m_speedOverGroundAvailable; + float m_speedOverGround; // Knots + int m_positionAccuracy; + bool m_longitudeAvailable; + float m_longitude; // Degrees, North positive + bool m_latitudeAvailable; + float m_latitude; // Degrees, East positive + bool m_courseAvailable; + float m_course; // Degrees + bool m_headingAvailable; + int m_heading; // Degrees + int m_timeStamp; + int m_specialManoeuvre; + + AISPositionReport(const QByteArray ba); + virtual QString getType() override { return "Position report"; } + virtual bool hasPosition() { return m_latitudeAvailable && m_longitudeAvailable; } + virtual float getLatitude() { return m_latitude; } + virtual float getLongitude() { return m_longitude; } + virtual bool hasCourse() { return m_courseAvailable; } + virtual float getCourse() { return m_course; } + virtual bool hasSpeed() { return m_speedOverGroundAvailable; } + virtual float getSpeed() { return m_speedOverGround; } + virtual bool hasHeading() { return m_headingAvailable; } + virtual float getHeading() { return m_heading; } + virtual QString toString() override; + + static QString getStatusString(int status); +}; + +class SDRBASE_API AISBaseStationReport : public AISMessage { +public: + QDateTime m_utc; + int m_positionAccuracy; + bool m_longitudeAvailable; + float m_longitude; // Degrees, North positive + bool m_latitudeAvailable; + float m_latitude; // Degrees, East positive + + AISBaseStationReport(const QByteArray ba); + virtual QString getType() override { + if (m_id == 4) + return "Base station report"; + else + return "UTC and data reponse"; + } + virtual bool hasPosition() { return m_latitudeAvailable && m_longitudeAvailable; } + virtual float getLatitude() { return m_latitude; } + virtual float getLongitude() { return m_longitude; } + virtual QString toString() override; +}; + +class SDRBASE_API AISShipStaticAndVoyageData : public AISMessage { +public: + int m_version; + int m_imo; + QString m_callsign; + QString m_name; + quint8 m_type; + int m_dimension; + int m_positionFixing; + int m_eta; + int m_draught; + QString m_destination; + AISShipStaticAndVoyageData(const QByteArray ba); + virtual QString getType() override { return "Ship static and voyage related data"; } + virtual QString toString() override; +}; + +class SDRBASE_API AISBinaryMessage : public AISMessage { +public: + int m_sequenceNumber; + int m_destinationId; + int m_retransmitFlag; + AISBinaryMessage(const QByteArray ba); + virtual QString getType() override { return "Addressed binary message"; } + virtual QString toString() override; +}; + +class SDRBASE_API AISBinaryAck : public AISMessage { +public: + AISBinaryAck(const QByteArray ba); + virtual QString getType() override { return "Binary acknowledge"; } +}; + +class SDRBASE_API AISBinaryBroadcast : public AISMessage { +public: + AISBinaryBroadcast(const QByteArray ba); + virtual QString getType() override { return "Binary broadcast message"; } +}; + +class SDRBASE_API AISSARAircraftPositionReport : public AISMessage { +public: + bool m_altitudeAvailable; + float m_altitude; // Metres. 4094 = 4094+ + bool m_speedOverGroundAvailable; + float m_speedOverGround; // Knots + int m_positionAccuracy; + bool m_longitudeAvailable; + float m_longitude; // Degrees, North positive + bool m_latitudeAvailable; + float m_latitude; // Degrees, East positive + bool m_courseAvailable; + float m_course; // Degrees + bool m_headingAvailable; + int m_heading; // Degrees + int m_timeStamp; + + AISSARAircraftPositionReport(const QByteArray ba); + virtual QString getType() override { return "Standard SAR aircraft position report"; } + virtual bool hasPosition() { return m_latitudeAvailable && m_longitudeAvailable; } + virtual float getLatitude() { return m_latitude; } + virtual float getLongitude() { return m_longitude; } + virtual bool hasCourse() { return m_courseAvailable; } + virtual float getCourse() { return m_course; } + virtual bool hasSpeed() { return m_speedOverGroundAvailable; } + virtual float getSpeed() { return m_speedOverGround; } + virtual QString toString() override; +}; + +class SDRBASE_API AISUTCInquiry : public AISMessage { +public: + AISUTCInquiry(const QByteArray ba); + virtual QString getType() override { return "UTC and date inquiry"; } +}; + +class SDRBASE_API AISSafetyMessage : public AISMessage { +public: + int m_sequenceNumber; + int m_destinationId; + int m_retransmitFlag; + QString m_safetyRelatedText; + + AISSafetyMessage(const QByteArray ba); + virtual QString getType() override { return "Addressed safety related message"; } + virtual QString toString() override; +}; + +class SDRBASE_API AISSafetyAck : public AISMessage { +public: + AISSafetyAck(const QByteArray ba); + virtual QString getType() override { return "Safety related acknowledge"; } +}; + +class SDRBASE_API AISSafetyBroadcast : public AISMessage { +public: + QString m_safetyRelatedText; + + AISSafetyBroadcast(const QByteArray ba); + virtual QString getType() override { return "Safety related broadcast message"; } + virtual QString toString() override; +}; + +class SDRBASE_API AISInterrogation : public AISMessage { +public: + AISInterrogation(const QByteArray ba); + virtual QString getType() override { return "Interrogation"; } +}; + +class SDRBASE_API AISAssignedModeCommand : public AISMessage { +public: + AISAssignedModeCommand(const QByteArray ba); + virtual QString getType() override { return "Assigned mode command"; } +}; + +class SDRBASE_API AISGNSSBroadcast : public AISMessage { +public: + AISGNSSBroadcast(const QByteArray ba); + virtual QString getType() override { return "GNSS broadcast binary message"; } +}; + +class SDRBASE_API AISStandardClassBPositionReport : public AISMessage { +public: + bool m_speedOverGroundAvailable; + float m_speedOverGround; // Knots + int m_positionAccuracy; + bool m_longitudeAvailable; + float m_longitude; // Degrees, North positive + bool m_latitudeAvailable; + float m_latitude; // Degrees, East positive + bool m_courseAvailable; + float m_course; // Degrees + bool m_headingAvailable; + int m_heading; // Degrees + int m_timeStamp; + + AISStandardClassBPositionReport(const QByteArray ba); + virtual QString getType() override { return "Standard Class B equipment position report"; } + virtual bool hasPosition() { return m_latitudeAvailable && m_longitudeAvailable; } + virtual float getLatitude() { return m_latitude; } + virtual float getLongitude() { return m_longitude; } + virtual bool hasCourse() { return m_courseAvailable; } + virtual float getCourse() { return m_course; } + virtual bool hasSpeed() { return m_speedOverGroundAvailable; } + virtual float getSpeed() { return m_speedOverGround; } + virtual QString toString() override; +}; + +class SDRBASE_API AISExtendedClassBPositionReport : public AISMessage { +public: + bool m_speedOverGroundAvailable; + float m_speedOverGround; // Knots + int m_positionAccuracy; + bool m_longitudeAvailable; + float m_longitude; // Degrees, North positive + bool m_latitudeAvailable; + float m_latitude; // Degrees, East positive + bool m_courseAvailable; + float m_course; // Degrees + bool m_headingAvailable; + int m_heading; // Degrees + int m_timeStamp; + QString m_name; + + AISExtendedClassBPositionReport(const QByteArray ba); + virtual QString getType() override { return "Extended Class B equipment position report"; } + virtual bool hasPosition() { return m_latitudeAvailable && m_longitudeAvailable; } + virtual float getLatitude() { return m_latitude; } + virtual float getLongitude() { return m_longitude; } + virtual bool hasCourse() { return m_courseAvailable; } + virtual float getCourse() { return m_course; } + virtual bool hasSpeed() { return m_speedOverGroundAvailable; } + virtual float getSpeed() { return m_speedOverGround; } + virtual QString toString() override; +}; + +class SDRBASE_API AISDatalinkManagement : public AISMessage { +public: + AISDatalinkManagement(const QByteArray ba); + virtual QString getType() override { return "Data link management message"; } +}; + +class SDRBASE_API AISAidsToNavigationReport : public AISMessage { +public: + int m_type; + QString m_name; + int m_positionAccuracy; + bool m_longitudeAvailable; + float m_longitude; // Degrees, North positive + bool m_latitudeAvailable; + float m_latitude; // Degrees, East positive + AISAidsToNavigationReport(const QByteArray ba); + virtual QString getType() override { return "Aids-to-navigation report"; } + virtual bool hasPosition() { return m_latitudeAvailable && m_longitudeAvailable; } + virtual float getLatitude() { return m_latitude; } + virtual float getLongitude() { return m_longitude; } + virtual QString toString() override; +}; + +class SDRBASE_API AISChannelManagement : public AISMessage { +public: + AISChannelManagement(const QByteArray ba); + virtual QString getType() override { return "Channel management"; } +}; + +class SDRBASE_API AISGroupAssignment : public AISMessage { +public: + AISGroupAssignment(const QByteArray ba); + virtual QString getType() override { return "Group assignment command"; } +}; + +class SDRBASE_API AISStaticDataReport : public AISMessage { +public: + int m_partNumber; + QString m_name; + quint8 m_type; + QString m_vendorId; + QString m_callsign; + + AISStaticDataReport(const QByteArray ba); + virtual QString getType() override { return "Static data report"; } + virtual QString toString() override; +}; + +class SDRBASE_API AISSingleSlotBinaryMessage : public AISMessage { +public: + AISSingleSlotBinaryMessage(const QByteArray ba); + virtual QString getType() override { return "Single slot binary message"; } +}; + +class SDRBASE_API AISMultipleSlotBinaryMessage : public AISMessage { +public: + AISMultipleSlotBinaryMessage(const QByteArray ba); + virtual QString getType() override { return "Multiple slot binary message"; } +}; + +class SDRBASE_API AISLongRangePositionReport : public AISMessage { +public: + int m_positionAccuracy; + int m_raim; + int m_status; + bool m_longitudeAvailable; + float m_longitude; // Degrees, North positive + bool m_latitudeAvailable; + float m_latitude; // Degrees, East positive + bool m_speedOverGroundAvailable; + float m_speedOverGround; // Knots + bool m_courseAvailable; + float m_course; // Degrees + + AISLongRangePositionReport(const QByteArray ba); + virtual QString getType() override { return "Position report for long-range applications"; } + virtual bool hasPosition() { return m_latitudeAvailable && m_longitudeAvailable; } + virtual float getLatitude() { return m_latitude; } + virtual float getLongitude() { return m_longitude; } + virtual bool hasCourse() { return m_courseAvailable; } + virtual float getCourse() { return m_course; } + virtual bool hasSpeed() { return m_speedOverGroundAvailable; } + virtual float getSpeed() { return m_speedOverGround; } + QString getStatus(); + virtual QString toString() override; +}; + +class SDRBASE_API AISUnknownMessageID : public AISMessage { +public: + AISUnknownMessageID(const QByteArray ba); + virtual QString getType() override { return QString("Unknown message ID (%1)").arg(m_id); } +}; + +#endif // INCLUDE_AIS_H diff --git a/sdrbase/webapi/webapirequestmapper.cpp b/sdrbase/webapi/webapirequestmapper.cpp index 7021e226f..b42c85dda 100644 --- a/sdrbase/webapi/webapirequestmapper.cpp +++ b/sdrbase/webapi/webapirequestmapper.cpp @@ -3811,6 +3811,16 @@ bool WebAPIRequestMapper::getChannelSettings( channelSettings->setAdsbDemodSettings(new SWGSDRangel::SWGADSBDemodSettings()); channelSettings->getAdsbDemodSettings()->fromJsonObject(settingsJsonObject); } + else if (channelSettingsKey == "AISDemodSettings") + { + channelSettings->setAisDemodSettings(new SWGSDRangel::SWGAISDemodSettings()); + channelSettings->getAisDemodSettings()->fromJsonObject(settingsJsonObject); + } + else if (channelSettingsKey == "AISModSettings") + { + channelSettings->setAisModSettings(new SWGSDRangel::SWGAISModSettings()); + channelSettings->getAisModSettings()->fromJsonObject(settingsJsonObject); + } else if (channelSettingsKey == "AMDemodSettings") { channelSettings->setAmDemodSettings(new SWGSDRangel::SWGAMDemodSettings()); @@ -4023,7 +4033,12 @@ bool WebAPIRequestMapper::getChannelActions( QJsonObject actionsJsonObject = channelActionsJson[channelActionsKey].toObject(); channelActionsKeys = actionsJsonObject.keys(); - if (channelActionsKey == "APTDemodActions") + if (channelActionsKey == "AISModActions") + { + channelActions->setAisModActions(new SWGSDRangel::SWGAISModActions()); + channelActions->getAisModActions()->fromJsonObject(actionsJsonObject); + } + else if (channelActionsKey == "APTDemodActions") { channelActions->setAptDemodActions(new SWGSDRangel::SWGAPTDemodActions()); channelActions->getAptDemodActions()->fromJsonObject(actionsJsonObject); @@ -4405,7 +4420,12 @@ bool WebAPIRequestMapper::getFeatureSettings( QJsonObject settingsJsonObject = featureSettingsJson[featureSettingsKey].toObject(); featureSettingsKeys = settingsJsonObject.keys(); - if (featureSettingsKey == "APRSSettings") + if (featureSettingsKey == "AISSSettings") + { + featureSettings->setAisSettings(new SWGSDRangel::SWGAISSettings()); + featureSettings->getAisSettings()->fromJsonObject(settingsJsonObject); + } + else if (featureSettingsKey == "APRSSettings") { featureSettings->setAprsSettings(new SWGSDRangel::SWGAPRSSettings()); featureSettings->getAprsSettings()->fromJsonObject(settingsJsonObject); @@ -4594,6 +4614,8 @@ void WebAPIRequestMapper::resetChannelSettings(SWGSDRangel::SWGChannelSettings& channelSettings.cleanup(); channelSettings.setChannelType(nullptr); channelSettings.setAdsbDemodSettings(nullptr); + channelSettings.setAisDemodSettings(nullptr); + channelSettings.setAisModSettings(nullptr); channelSettings.setAmDemodSettings(nullptr); channelSettings.setAmModSettings(nullptr); channelSettings.setAptDemodSettings(nullptr); @@ -4623,6 +4645,8 @@ void WebAPIRequestMapper::resetChannelReport(SWGSDRangel::SWGChannelReport& chan channelReport.cleanup(); channelReport.setChannelType(nullptr); channelReport.setAdsbDemodReport(nullptr); + channelReport.setAisDemodReport(nullptr); + channelReport.setAisModReport(nullptr); channelReport.setAmDemodReport(nullptr); channelReport.setAmModReport(nullptr); channelReport.setAtvModReport(nullptr); @@ -4646,6 +4670,7 @@ void WebAPIRequestMapper::resetChannelReport(SWGSDRangel::SWGChannelReport& chan void WebAPIRequestMapper::resetChannelActions(SWGSDRangel::SWGChannelActions& channelActions) { channelActions.cleanup(); + channelActions.setAisModActions(nullptr); channelActions.setAptDemodActions(nullptr); channelActions.setChannelType(nullptr); channelActions.setFileSourceActions(nullptr); diff --git a/sdrbase/webapi/webapiutils.cpp b/sdrbase/webapi/webapiutils.cpp index 933fc6a3e..d525ed001 100644 --- a/sdrbase/webapi/webapiutils.cpp +++ b/sdrbase/webapi/webapiutils.cpp @@ -23,6 +23,8 @@ const QMap WebAPIUtils::m_channelURIToSettingsKey = { {"sdrangel.channel.adsbdemod", "ADSBDemodSettings"}, + {"sdrangel.channel.modais", "AISModSettings"}, + {"sdrangel.channel.aisdemod", "AISDemodSettings"}, {"sdrangel.channel.amdemod", "AMDemodSettings"}, {"sdrangel.channel.aptdemod", "APTDemodSettings"}, {"de.maintech.sdrangelove.channel.am", "AMDemodSettings"}, // remap @@ -122,6 +124,8 @@ const QMap WebAPIUtils::m_deviceIdToSettingsKey = { const QMap WebAPIUtils::m_channelTypeToSettingsKey = { {"ADSBDemod", "ADSBDemodSettings"}, + {"AISDemod", "AISDemodSettings"}, + {"AISMod", "AISModSettings"}, {"APTDemod", "APTemodSettings"}, {"AMDemod", "AMDemodSettings"}, {"AMMod", "AMModSettings"}, @@ -162,6 +166,7 @@ const QMap WebAPIUtils::m_channelTypeToSettingsKey = { }; const QMap WebAPIUtils::m_channelTypeToActionsKey = { + {"AISMod", "AISModActions"}, {"APTDemod", "APTDemodActions"}, {"FileSink", "FileSinkActions"}, {"FileSource", "FileSourceActions"}, @@ -249,6 +254,7 @@ const QMap WebAPIUtils::m_mimoDeviceHwIdToActionsKey = { }; const QMap WebAPIUtils::m_featureTypeToSettingsKey = { + {"AIS", "AISSettings"}, {"APRS", "APRSSettings"}, {"GS232Controller", "GS232ControllerSettings"}, {"Map", "MapSettings"}, @@ -264,6 +270,7 @@ const QMap WebAPIUtils::m_featureTypeToActionsKey = { }; const QMap WebAPIUtils::m_featureURIToSettingsKey = { + {"sdrangel.feature.ais", "AISSSettings"}, {"sdrangel.feature.aprs", "APRSSettings"}, {"sdrangel.feature.gs232controller", "GS232ControllerSettings"}, {"sdrangel.feature.map", "MapSettings"}, diff --git a/swagger/sdrangel/api/swagger/include/AIS.yaml b/swagger/sdrangel/api/swagger/include/AIS.yaml new file mode 100644 index 000000000..ce29b7807 --- /dev/null +++ b/swagger/sdrangel/api/swagger/include/AIS.yaml @@ -0,0 +1,18 @@ +AISSettings: + description: "AIS settings" + properties: + title: + type: string + rgbColor: + type: integer + useReverseAPI: + description: Synchronize with reverse API (1 for yes, 0 for no) + type: integer + reverseAPIAddress: + type: string + reverseAPIPort: + type: integer + reverseAPIDeviceIndex: + type: integer + reverseAPIChannelIndex: + type: integer diff --git a/swagger/sdrangel/api/swagger/include/AISDemod.yaml b/swagger/sdrangel/api/swagger/include/AISDemod.yaml new file mode 100644 index 000000000..320446a62 --- /dev/null +++ b/swagger/sdrangel/api/swagger/include/AISDemod.yaml @@ -0,0 +1,55 @@ +AISDemodSettings: + description: AISDemod + properties: + inputFrequencyOffset: + type: integer + format: int64 + rfBandwidth: + type: number + format: float + fmDeviation: + type: number + format: float + correlationThreshold: + type: number + format: float + udpEnabled: + description: "Whether to forward received messages to specified UDP port" + type: integer + udpAddress: + description: "UDP address to forward received messages to" + type: string + udpPort: + description: "UDP port to forward received messages to" + type: integer + udpFormat: + description: "0 for binary, 1 for NMEA" + type: integer + rgbColor: + type: integer + title: + type: string + streamIndex: + description: MIMO channel. Not relevant when connected to SI (single Rx). + type: integer + useReverseAPI: + description: Synchronize with reverse API (1 for yes, 0 for no) + type: integer + reverseAPIAddress: + type: string + reverseAPIPort: + type: integer + reverseAPIDeviceIndex: + type: integer + reverseAPIChannelIndex: + type: integer + +AISDemodReport: + description: AISDemod + properties: + channelPowerDB: + description: power received in channel (dB) + type: number + format: float + channelSampleRate: + type: integer diff --git a/swagger/sdrangel/api/swagger/include/AISMod.yaml b/swagger/sdrangel/api/swagger/include/AISMod.yaml new file mode 100644 index 000000000..83d5b087a --- /dev/null +++ b/swagger/sdrangel/api/swagger/include/AISMod.yaml @@ -0,0 +1,125 @@ +AISModSettings: + description: AISMod + properties: + inputFrequencyOffset: + description: channel center frequency shift from baseband center in Hz + type: integer + format: int64 + baud: + type: integer + rfBandwidth: + description: channel RF bandwidth in Hz + type: number + format: float + fmDeviation: + description: frequency deviation in Hz + type: integer + gain: + type: number + format: float + channelMute: + type: integer + repeat: + type: integer + repeatDelay: + type: number + format: float + repeatCount: + type: integer + rampUpBits: + type: integer + rampDownBits: + type: integer + rampRange: + type: integer + lpfTaps: + type: integer + rfNoise: + type: integer + description: > + Boolean + * 0 - off + * 1 - on + writeToFile: + type: integer + description: > + Boolean + * 0 - off + * 1 - on + spectrumRate: + type: integer + msgId: + type: integer + mmsi: + type: string + status: + type: integer + latitude: + type: number + format: float + longitude: + type: number + format: float + course: + type: number + format: float + speed: + type: number + format: float + heading: + type: integer + data: + type: string + bt: + type: number + format: float + symbolSpan: + type: integer + rgbColor: + type: integer + title: + type: string + streamIndex: + description: MIMO channel. Not relevant when connected to SI (single Rx). + type: integer + useReverseAPI: + description: Synchronize with reverse API (1 for yes, 0 for no) + type: integer + reverseAPIAddress: + type: string + reverseAPIPort: + type: integer + reverseAPIDeviceIndex: + type: integer + reverseAPIChannelIndex: + type: integer + udpEnabled: + description: "Whether to receive messages to transmit on specified UDP port" + type: integer + udpAddress: + description: "UDP address to receive messages to transmit via" + type: string + udpPort: + description: "UDP port to receive messages to transmit via" + type: integer + +AISModReport: + description: AISMod + properties: + channelPowerDB: + description: power transmitted in channel (dB) + type: number + format: float + channelSampleRate: + type: integer + +AISModActions: + description: AISMod + properties: + tx: + type: object + properties: + data: + type: string + description: > + Transmit a message diff --git a/swagger/sdrangel/api/swagger/include/ChannelActions.yaml b/swagger/sdrangel/api/swagger/include/ChannelActions.yaml index 2b98fa72a..dd497db1b 100644 --- a/swagger/sdrangel/api/swagger/include/ChannelActions.yaml +++ b/swagger/sdrangel/api/swagger/include/ChannelActions.yaml @@ -17,6 +17,8 @@ ChannelActions: originatorChannelIndex: description: Optional for reverse API. This is the channel index from where the message comes from. type: integer + AISModActions: + $ref: "http://swgserver:8081/api/swagger/include/AISMod.yaml#/AISModActions" APTDemodActions: $ref: "http://swgserver:8081/api/swagger/include/APTDemod.yaml#/APTDemodActions" FileSinkActions: diff --git a/swagger/sdrangel/api/swagger/include/ChannelReport.yaml b/swagger/sdrangel/api/swagger/include/ChannelReport.yaml index 233190e7f..f30a67872 100644 --- a/swagger/sdrangel/api/swagger/include/ChannelReport.yaml +++ b/swagger/sdrangel/api/swagger/include/ChannelReport.yaml @@ -13,6 +13,10 @@ ChannelReport: type: integer ADSBDemodReport: $ref: "http://swgserver:8081/api/swagger/include/ADSBDemod.yaml#/ADSBDemodReport" + AISDemodReport: + $ref: "http://swgserver:8081/api/swagger/include/AISDemod.yaml#/AISDemodReport" + AISModReport: + $ref: "http://swgserver:8081/api/swagger/include/AISMod.yaml#/AISModReport" AMDemodReport: $ref: "http://swgserver:8081/api/swagger/include/AMDemod.yaml#/AMDemodReport" AMModReport: diff --git a/swagger/sdrangel/api/swagger/include/ChannelSettings.yaml b/swagger/sdrangel/api/swagger/include/ChannelSettings.yaml index 5bd15c325..ea6f5de8b 100644 --- a/swagger/sdrangel/api/swagger/include/ChannelSettings.yaml +++ b/swagger/sdrangel/api/swagger/include/ChannelSettings.yaml @@ -19,6 +19,10 @@ ChannelSettings: type: integer ADSBDemodSettings: $ref: "http://swgserver:8081/api/swagger/include/ADSBDemod.yaml#/ADSBDemodSettings" + AISDemodSettings: + $ref: "http://swgserver:8081/api/swagger/include/AISDemod.yaml#/AISDemodSettings" + AISModSettings: + $ref: "http://swgserver:8081/api/swagger/include/AISMod.yaml#/AISModSettings" AMDemodSettings: $ref: "http://swgserver:8081/api/swagger/include/AMDemod.yaml#/AMDemodSettings" AMModSettings: diff --git a/swagger/sdrangel/api/swagger/include/FeatureSettings.yaml b/swagger/sdrangel/api/swagger/include/FeatureSettings.yaml index ceaf98c74..5ac4dc6da 100644 --- a/swagger/sdrangel/api/swagger/include/FeatureSettings.yaml +++ b/swagger/sdrangel/api/swagger/include/FeatureSettings.yaml @@ -15,6 +15,8 @@ FeatureSettings: type: integer AFCSettings: $ref: "http://swgserver:8081/api/swagger/include/AFC.yaml#/AFCSettings" + AISSettings: + $ref: "http://swgserver:8081/api/swagger/include/AIS.yaml#/AISSettings" APRSSettings: $ref: "http://swgserver:8081/api/swagger/include/APRS.yaml#/APRSSettings" DemodAnalyzerSettings: diff --git a/swagger/sdrangel/code/qt5/client/SWGAISDemodReport.cpp b/swagger/sdrangel/code/qt5/client/SWGAISDemodReport.cpp new file mode 100644 index 000000000..909123a09 --- /dev/null +++ b/swagger/sdrangel/code/qt5/client/SWGAISDemodReport.cpp @@ -0,0 +1,131 @@ +/** + * SDRangel + * This is the web REST/JSON API of SDRangel SDR software. SDRangel is an Open Source Qt5/OpenGL 3.0+ (4.3+ in Windows) GUI and server Software Defined Radio and signal analyzer in software. It supports Airspy, BladeRF, HackRF, LimeSDR, PlutoSDR, RTL-SDR, SDRplay RSP1 and FunCube --- Limitations and specifcities: * In SDRangel GUI the first Rx device set cannot be deleted. Conversely the server starts with no device sets and its number of device sets can be reduced to zero by as many calls as necessary to /sdrangel/deviceset with DELETE method. * Preset import and export from/to file is a server only feature. * Device set focus is a GUI only feature. * The following channels are not implemented (status 501 is returned): ATV and DATV demodulators, Channel Analyzer NG, LoRa demodulator * The device settings and report structures contains only the sub-structure corresponding to the device type. The DeviceSettings and DeviceReport structures documented here shows all of them but only one will be or should be present at a time * The channel settings and report structures contains only the sub-structure corresponding to the channel type. The ChannelSettings and ChannelReport structures documented here shows all of them but only one will be or should be present at a time --- + * + * OpenAPI spec version: 6.0.0 + * Contact: f4exb06@gmail.com + * + * NOTE: This class is auto generated by the swagger code generator program. + * https://github.com/swagger-api/swagger-codegen.git + * Do not edit the class manually. + */ + + +#include "SWGAISDemodReport.h" + +#include "SWGHelpers.h" + +#include +#include +#include +#include + +namespace SWGSDRangel { + +SWGAISDemodReport::SWGAISDemodReport(QString* json) { + init(); + this->fromJson(*json); +} + +SWGAISDemodReport::SWGAISDemodReport() { + channel_power_db = 0.0f; + m_channel_power_db_isSet = false; + channel_sample_rate = 0; + m_channel_sample_rate_isSet = false; +} + +SWGAISDemodReport::~SWGAISDemodReport() { + this->cleanup(); +} + +void +SWGAISDemodReport::init() { + channel_power_db = 0.0f; + m_channel_power_db_isSet = false; + channel_sample_rate = 0; + m_channel_sample_rate_isSet = false; +} + +void +SWGAISDemodReport::cleanup() { + + +} + +SWGAISDemodReport* +SWGAISDemodReport::fromJson(QString &json) { + QByteArray array (json.toStdString().c_str()); + QJsonDocument doc = QJsonDocument::fromJson(array); + QJsonObject jsonObject = doc.object(); + this->fromJsonObject(jsonObject); + return this; +} + +void +SWGAISDemodReport::fromJsonObject(QJsonObject &pJson) { + ::SWGSDRangel::setValue(&channel_power_db, pJson["channelPowerDB"], "float", ""); + + ::SWGSDRangel::setValue(&channel_sample_rate, pJson["channelSampleRate"], "qint32", ""); + +} + +QString +SWGAISDemodReport::asJson () +{ + QJsonObject* obj = this->asJsonObject(); + + QJsonDocument doc(*obj); + QByteArray bytes = doc.toJson(); + delete obj; + return QString(bytes); +} + +QJsonObject* +SWGAISDemodReport::asJsonObject() { + QJsonObject* obj = new QJsonObject(); + if(m_channel_power_db_isSet){ + obj->insert("channelPowerDB", QJsonValue(channel_power_db)); + } + if(m_channel_sample_rate_isSet){ + obj->insert("channelSampleRate", QJsonValue(channel_sample_rate)); + } + + return obj; +} + +float +SWGAISDemodReport::getChannelPowerDb() { + return channel_power_db; +} +void +SWGAISDemodReport::setChannelPowerDb(float channel_power_db) { + this->channel_power_db = channel_power_db; + this->m_channel_power_db_isSet = true; +} + +qint32 +SWGAISDemodReport::getChannelSampleRate() { + return channel_sample_rate; +} +void +SWGAISDemodReport::setChannelSampleRate(qint32 channel_sample_rate) { + this->channel_sample_rate = channel_sample_rate; + this->m_channel_sample_rate_isSet = true; +} + + +bool +SWGAISDemodReport::isSet(){ + bool isObjectUpdated = false; + do{ + if(m_channel_power_db_isSet){ + isObjectUpdated = true; break; + } + if(m_channel_sample_rate_isSet){ + isObjectUpdated = true; break; + } + }while(false); + return isObjectUpdated; +} +} + diff --git a/swagger/sdrangel/code/qt5/client/SWGAISDemodReport.h b/swagger/sdrangel/code/qt5/client/SWGAISDemodReport.h new file mode 100644 index 000000000..d5080c297 --- /dev/null +++ b/swagger/sdrangel/code/qt5/client/SWGAISDemodReport.h @@ -0,0 +1,64 @@ +/** + * SDRangel + * This is the web REST/JSON API of SDRangel SDR software. SDRangel is an Open Source Qt5/OpenGL 3.0+ (4.3+ in Windows) GUI and server Software Defined Radio and signal analyzer in software. It supports Airspy, BladeRF, HackRF, LimeSDR, PlutoSDR, RTL-SDR, SDRplay RSP1 and FunCube --- Limitations and specifcities: * In SDRangel GUI the first Rx device set cannot be deleted. Conversely the server starts with no device sets and its number of device sets can be reduced to zero by as many calls as necessary to /sdrangel/deviceset with DELETE method. * Preset import and export from/to file is a server only feature. * Device set focus is a GUI only feature. * The following channels are not implemented (status 501 is returned): ATV and DATV demodulators, Channel Analyzer NG, LoRa demodulator * The device settings and report structures contains only the sub-structure corresponding to the device type. The DeviceSettings and DeviceReport structures documented here shows all of them but only one will be or should be present at a time * The channel settings and report structures contains only the sub-structure corresponding to the channel type. The ChannelSettings and ChannelReport structures documented here shows all of them but only one will be or should be present at a time --- + * + * OpenAPI spec version: 6.0.0 + * Contact: f4exb06@gmail.com + * + * NOTE: This class is auto generated by the swagger code generator program. + * https://github.com/swagger-api/swagger-codegen.git + * Do not edit the class manually. + */ + +/* + * SWGAISDemodReport.h + * + * AISDemod + */ + +#ifndef SWGAISDemodReport_H_ +#define SWGAISDemodReport_H_ + +#include + + + +#include "SWGObject.h" +#include "export.h" + +namespace SWGSDRangel { + +class SWG_API SWGAISDemodReport: public SWGObject { +public: + SWGAISDemodReport(); + SWGAISDemodReport(QString* json); + virtual ~SWGAISDemodReport(); + void init(); + void cleanup(); + + virtual QString asJson () override; + virtual QJsonObject* asJsonObject() override; + virtual void fromJsonObject(QJsonObject &json) override; + virtual SWGAISDemodReport* fromJson(QString &jsonString) override; + + float getChannelPowerDb(); + void setChannelPowerDb(float channel_power_db); + + qint32 getChannelSampleRate(); + void setChannelSampleRate(qint32 channel_sample_rate); + + + virtual bool isSet() override; + +private: + float channel_power_db; + bool m_channel_power_db_isSet; + + qint32 channel_sample_rate; + bool m_channel_sample_rate_isSet; + +}; + +} + +#endif /* SWGAISDemodReport_H_ */ diff --git a/swagger/sdrangel/code/qt5/client/SWGAISDemodSettings.cpp b/swagger/sdrangel/code/qt5/client/SWGAISDemodSettings.cpp new file mode 100644 index 000000000..049a311fd --- /dev/null +++ b/swagger/sdrangel/code/qt5/client/SWGAISDemodSettings.cpp @@ -0,0 +1,459 @@ +/** + * SDRangel + * This is the web REST/JSON API of SDRangel SDR software. SDRangel is an Open Source Qt5/OpenGL 3.0+ (4.3+ in Windows) GUI and server Software Defined Radio and signal analyzer in software. It supports Airspy, BladeRF, HackRF, LimeSDR, PlutoSDR, RTL-SDR, SDRplay RSP1 and FunCube --- Limitations and specifcities: * In SDRangel GUI the first Rx device set cannot be deleted. Conversely the server starts with no device sets and its number of device sets can be reduced to zero by as many calls as necessary to /sdrangel/deviceset with DELETE method. * Preset import and export from/to file is a server only feature. * Device set focus is a GUI only feature. * The following channels are not implemented (status 501 is returned): ATV and DATV demodulators, Channel Analyzer NG, LoRa demodulator * The device settings and report structures contains only the sub-structure corresponding to the device type. The DeviceSettings and DeviceReport structures documented here shows all of them but only one will be or should be present at a time * The channel settings and report structures contains only the sub-structure corresponding to the channel type. The ChannelSettings and ChannelReport structures documented here shows all of them but only one will be or should be present at a time --- + * + * OpenAPI spec version: 6.0.0 + * Contact: f4exb06@gmail.com + * + * NOTE: This class is auto generated by the swagger code generator program. + * https://github.com/swagger-api/swagger-codegen.git + * Do not edit the class manually. + */ + + +#include "SWGAISDemodSettings.h" + +#include "SWGHelpers.h" + +#include +#include +#include +#include + +namespace SWGSDRangel { + +SWGAISDemodSettings::SWGAISDemodSettings(QString* json) { + init(); + this->fromJson(*json); +} + +SWGAISDemodSettings::SWGAISDemodSettings() { + input_frequency_offset = 0L; + m_input_frequency_offset_isSet = false; + rf_bandwidth = 0.0f; + m_rf_bandwidth_isSet = false; + fm_deviation = 0.0f; + m_fm_deviation_isSet = false; + correlation_threshold = 0.0f; + m_correlation_threshold_isSet = false; + udp_enabled = 0; + m_udp_enabled_isSet = false; + udp_address = nullptr; + m_udp_address_isSet = false; + udp_port = 0; + m_udp_port_isSet = false; + udp_format = 0; + m_udp_format_isSet = false; + rgb_color = 0; + m_rgb_color_isSet = false; + title = nullptr; + m_title_isSet = false; + stream_index = 0; + m_stream_index_isSet = false; + use_reverse_api = 0; + m_use_reverse_api_isSet = false; + reverse_api_address = nullptr; + m_reverse_api_address_isSet = false; + reverse_api_port = 0; + m_reverse_api_port_isSet = false; + reverse_api_device_index = 0; + m_reverse_api_device_index_isSet = false; + reverse_api_channel_index = 0; + m_reverse_api_channel_index_isSet = false; +} + +SWGAISDemodSettings::~SWGAISDemodSettings() { + this->cleanup(); +} + +void +SWGAISDemodSettings::init() { + input_frequency_offset = 0L; + m_input_frequency_offset_isSet = false; + rf_bandwidth = 0.0f; + m_rf_bandwidth_isSet = false; + fm_deviation = 0.0f; + m_fm_deviation_isSet = false; + correlation_threshold = 0.0f; + m_correlation_threshold_isSet = false; + udp_enabled = 0; + m_udp_enabled_isSet = false; + udp_address = new QString(""); + m_udp_address_isSet = false; + udp_port = 0; + m_udp_port_isSet = false; + udp_format = 0; + m_udp_format_isSet = false; + rgb_color = 0; + m_rgb_color_isSet = false; + title = new QString(""); + m_title_isSet = false; + stream_index = 0; + m_stream_index_isSet = false; + use_reverse_api = 0; + m_use_reverse_api_isSet = false; + reverse_api_address = new QString(""); + m_reverse_api_address_isSet = false; + reverse_api_port = 0; + m_reverse_api_port_isSet = false; + reverse_api_device_index = 0; + m_reverse_api_device_index_isSet = false; + reverse_api_channel_index = 0; + m_reverse_api_channel_index_isSet = false; +} + +void +SWGAISDemodSettings::cleanup() { + + + + + + if(udp_address != nullptr) { + delete udp_address; + } + + + + if(title != nullptr) { + delete title; + } + + + if(reverse_api_address != nullptr) { + delete reverse_api_address; + } + + + +} + +SWGAISDemodSettings* +SWGAISDemodSettings::fromJson(QString &json) { + QByteArray array (json.toStdString().c_str()); + QJsonDocument doc = QJsonDocument::fromJson(array); + QJsonObject jsonObject = doc.object(); + this->fromJsonObject(jsonObject); + return this; +} + +void +SWGAISDemodSettings::fromJsonObject(QJsonObject &pJson) { + ::SWGSDRangel::setValue(&input_frequency_offset, pJson["inputFrequencyOffset"], "qint64", ""); + + ::SWGSDRangel::setValue(&rf_bandwidth, pJson["rfBandwidth"], "float", ""); + + ::SWGSDRangel::setValue(&fm_deviation, pJson["fmDeviation"], "float", ""); + + ::SWGSDRangel::setValue(&correlation_threshold, pJson["correlationThreshold"], "float", ""); + + ::SWGSDRangel::setValue(&udp_enabled, pJson["udpEnabled"], "qint32", ""); + + ::SWGSDRangel::setValue(&udp_address, pJson["udpAddress"], "QString", "QString"); + + ::SWGSDRangel::setValue(&udp_port, pJson["udpPort"], "qint32", ""); + + ::SWGSDRangel::setValue(&udp_format, pJson["udpFormat"], "qint32", ""); + + ::SWGSDRangel::setValue(&rgb_color, pJson["rgbColor"], "qint32", ""); + + ::SWGSDRangel::setValue(&title, pJson["title"], "QString", "QString"); + + ::SWGSDRangel::setValue(&stream_index, pJson["streamIndex"], "qint32", ""); + + ::SWGSDRangel::setValue(&use_reverse_api, pJson["useReverseAPI"], "qint32", ""); + + ::SWGSDRangel::setValue(&reverse_api_address, pJson["reverseAPIAddress"], "QString", "QString"); + + ::SWGSDRangel::setValue(&reverse_api_port, pJson["reverseAPIPort"], "qint32", ""); + + ::SWGSDRangel::setValue(&reverse_api_device_index, pJson["reverseAPIDeviceIndex"], "qint32", ""); + + ::SWGSDRangel::setValue(&reverse_api_channel_index, pJson["reverseAPIChannelIndex"], "qint32", ""); + +} + +QString +SWGAISDemodSettings::asJson () +{ + QJsonObject* obj = this->asJsonObject(); + + QJsonDocument doc(*obj); + QByteArray bytes = doc.toJson(); + delete obj; + return QString(bytes); +} + +QJsonObject* +SWGAISDemodSettings::asJsonObject() { + QJsonObject* obj = new QJsonObject(); + if(m_input_frequency_offset_isSet){ + obj->insert("inputFrequencyOffset", QJsonValue(input_frequency_offset)); + } + if(m_rf_bandwidth_isSet){ + obj->insert("rfBandwidth", QJsonValue(rf_bandwidth)); + } + if(m_fm_deviation_isSet){ + obj->insert("fmDeviation", QJsonValue(fm_deviation)); + } + if(m_correlation_threshold_isSet){ + obj->insert("correlationThreshold", QJsonValue(correlation_threshold)); + } + if(m_udp_enabled_isSet){ + obj->insert("udpEnabled", QJsonValue(udp_enabled)); + } + if(udp_address != nullptr && *udp_address != QString("")){ + toJsonValue(QString("udpAddress"), udp_address, obj, QString("QString")); + } + if(m_udp_port_isSet){ + obj->insert("udpPort", QJsonValue(udp_port)); + } + if(m_udp_format_isSet){ + obj->insert("udpFormat", QJsonValue(udp_format)); + } + if(m_rgb_color_isSet){ + obj->insert("rgbColor", QJsonValue(rgb_color)); + } + if(title != nullptr && *title != QString("")){ + toJsonValue(QString("title"), title, obj, QString("QString")); + } + if(m_stream_index_isSet){ + obj->insert("streamIndex", QJsonValue(stream_index)); + } + if(m_use_reverse_api_isSet){ + obj->insert("useReverseAPI", QJsonValue(use_reverse_api)); + } + if(reverse_api_address != nullptr && *reverse_api_address != QString("")){ + toJsonValue(QString("reverseAPIAddress"), reverse_api_address, obj, QString("QString")); + } + if(m_reverse_api_port_isSet){ + obj->insert("reverseAPIPort", QJsonValue(reverse_api_port)); + } + if(m_reverse_api_device_index_isSet){ + obj->insert("reverseAPIDeviceIndex", QJsonValue(reverse_api_device_index)); + } + if(m_reverse_api_channel_index_isSet){ + obj->insert("reverseAPIChannelIndex", QJsonValue(reverse_api_channel_index)); + } + + return obj; +} + +qint64 +SWGAISDemodSettings::getInputFrequencyOffset() { + return input_frequency_offset; +} +void +SWGAISDemodSettings::setInputFrequencyOffset(qint64 input_frequency_offset) { + this->input_frequency_offset = input_frequency_offset; + this->m_input_frequency_offset_isSet = true; +} + +float +SWGAISDemodSettings::getRfBandwidth() { + return rf_bandwidth; +} +void +SWGAISDemodSettings::setRfBandwidth(float rf_bandwidth) { + this->rf_bandwidth = rf_bandwidth; + this->m_rf_bandwidth_isSet = true; +} + +float +SWGAISDemodSettings::getFmDeviation() { + return fm_deviation; +} +void +SWGAISDemodSettings::setFmDeviation(float fm_deviation) { + this->fm_deviation = fm_deviation; + this->m_fm_deviation_isSet = true; +} + +float +SWGAISDemodSettings::getCorrelationThreshold() { + return correlation_threshold; +} +void +SWGAISDemodSettings::setCorrelationThreshold(float correlation_threshold) { + this->correlation_threshold = correlation_threshold; + this->m_correlation_threshold_isSet = true; +} + +qint32 +SWGAISDemodSettings::getUdpEnabled() { + return udp_enabled; +} +void +SWGAISDemodSettings::setUdpEnabled(qint32 udp_enabled) { + this->udp_enabled = udp_enabled; + this->m_udp_enabled_isSet = true; +} + +QString* +SWGAISDemodSettings::getUdpAddress() { + return udp_address; +} +void +SWGAISDemodSettings::setUdpAddress(QString* udp_address) { + this->udp_address = udp_address; + this->m_udp_address_isSet = true; +} + +qint32 +SWGAISDemodSettings::getUdpPort() { + return udp_port; +} +void +SWGAISDemodSettings::setUdpPort(qint32 udp_port) { + this->udp_port = udp_port; + this->m_udp_port_isSet = true; +} + +qint32 +SWGAISDemodSettings::getUdpFormat() { + return udp_format; +} +void +SWGAISDemodSettings::setUdpFormat(qint32 udp_format) { + this->udp_format = udp_format; + this->m_udp_format_isSet = true; +} + +qint32 +SWGAISDemodSettings::getRgbColor() { + return rgb_color; +} +void +SWGAISDemodSettings::setRgbColor(qint32 rgb_color) { + this->rgb_color = rgb_color; + this->m_rgb_color_isSet = true; +} + +QString* +SWGAISDemodSettings::getTitle() { + return title; +} +void +SWGAISDemodSettings::setTitle(QString* title) { + this->title = title; + this->m_title_isSet = true; +} + +qint32 +SWGAISDemodSettings::getStreamIndex() { + return stream_index; +} +void +SWGAISDemodSettings::setStreamIndex(qint32 stream_index) { + this->stream_index = stream_index; + this->m_stream_index_isSet = true; +} + +qint32 +SWGAISDemodSettings::getUseReverseApi() { + return use_reverse_api; +} +void +SWGAISDemodSettings::setUseReverseApi(qint32 use_reverse_api) { + this->use_reverse_api = use_reverse_api; + this->m_use_reverse_api_isSet = true; +} + +QString* +SWGAISDemodSettings::getReverseApiAddress() { + return reverse_api_address; +} +void +SWGAISDemodSettings::setReverseApiAddress(QString* reverse_api_address) { + this->reverse_api_address = reverse_api_address; + this->m_reverse_api_address_isSet = true; +} + +qint32 +SWGAISDemodSettings::getReverseApiPort() { + return reverse_api_port; +} +void +SWGAISDemodSettings::setReverseApiPort(qint32 reverse_api_port) { + this->reverse_api_port = reverse_api_port; + this->m_reverse_api_port_isSet = true; +} + +qint32 +SWGAISDemodSettings::getReverseApiDeviceIndex() { + return reverse_api_device_index; +} +void +SWGAISDemodSettings::setReverseApiDeviceIndex(qint32 reverse_api_device_index) { + this->reverse_api_device_index = reverse_api_device_index; + this->m_reverse_api_device_index_isSet = true; +} + +qint32 +SWGAISDemodSettings::getReverseApiChannelIndex() { + return reverse_api_channel_index; +} +void +SWGAISDemodSettings::setReverseApiChannelIndex(qint32 reverse_api_channel_index) { + this->reverse_api_channel_index = reverse_api_channel_index; + this->m_reverse_api_channel_index_isSet = true; +} + + +bool +SWGAISDemodSettings::isSet(){ + bool isObjectUpdated = false; + do{ + if(m_input_frequency_offset_isSet){ + isObjectUpdated = true; break; + } + if(m_rf_bandwidth_isSet){ + isObjectUpdated = true; break; + } + if(m_fm_deviation_isSet){ + isObjectUpdated = true; break; + } + if(m_correlation_threshold_isSet){ + isObjectUpdated = true; break; + } + if(m_udp_enabled_isSet){ + isObjectUpdated = true; break; + } + if(udp_address && *udp_address != QString("")){ + isObjectUpdated = true; break; + } + if(m_udp_port_isSet){ + isObjectUpdated = true; break; + } + if(m_udp_format_isSet){ + isObjectUpdated = true; break; + } + if(m_rgb_color_isSet){ + isObjectUpdated = true; break; + } + if(title && *title != QString("")){ + isObjectUpdated = true; break; + } + if(m_stream_index_isSet){ + isObjectUpdated = true; break; + } + if(m_use_reverse_api_isSet){ + isObjectUpdated = true; break; + } + if(reverse_api_address && *reverse_api_address != QString("")){ + isObjectUpdated = true; break; + } + if(m_reverse_api_port_isSet){ + isObjectUpdated = true; break; + } + if(m_reverse_api_device_index_isSet){ + isObjectUpdated = true; break; + } + if(m_reverse_api_channel_index_isSet){ + isObjectUpdated = true; break; + } + }while(false); + return isObjectUpdated; +} +} + diff --git a/swagger/sdrangel/code/qt5/client/SWGAISDemodSettings.h b/swagger/sdrangel/code/qt5/client/SWGAISDemodSettings.h new file mode 100644 index 000000000..24fd6e2f0 --- /dev/null +++ b/swagger/sdrangel/code/qt5/client/SWGAISDemodSettings.h @@ -0,0 +1,149 @@ +/** + * SDRangel + * This is the web REST/JSON API of SDRangel SDR software. SDRangel is an Open Source Qt5/OpenGL 3.0+ (4.3+ in Windows) GUI and server Software Defined Radio and signal analyzer in software. It supports Airspy, BladeRF, HackRF, LimeSDR, PlutoSDR, RTL-SDR, SDRplay RSP1 and FunCube --- Limitations and specifcities: * In SDRangel GUI the first Rx device set cannot be deleted. Conversely the server starts with no device sets and its number of device sets can be reduced to zero by as many calls as necessary to /sdrangel/deviceset with DELETE method. * Preset import and export from/to file is a server only feature. * Device set focus is a GUI only feature. * The following channels are not implemented (status 501 is returned): ATV and DATV demodulators, Channel Analyzer NG, LoRa demodulator * The device settings and report structures contains only the sub-structure corresponding to the device type. The DeviceSettings and DeviceReport structures documented here shows all of them but only one will be or should be present at a time * The channel settings and report structures contains only the sub-structure corresponding to the channel type. The ChannelSettings and ChannelReport structures documented here shows all of them but only one will be or should be present at a time --- + * + * OpenAPI spec version: 6.0.0 + * Contact: f4exb06@gmail.com + * + * NOTE: This class is auto generated by the swagger code generator program. + * https://github.com/swagger-api/swagger-codegen.git + * Do not edit the class manually. + */ + +/* + * SWGAISDemodSettings.h + * + * AISDemod + */ + +#ifndef SWGAISDemodSettings_H_ +#define SWGAISDemodSettings_H_ + +#include + + +#include + +#include "SWGObject.h" +#include "export.h" + +namespace SWGSDRangel { + +class SWG_API SWGAISDemodSettings: public SWGObject { +public: + SWGAISDemodSettings(); + SWGAISDemodSettings(QString* json); + virtual ~SWGAISDemodSettings(); + void init(); + void cleanup(); + + virtual QString asJson () override; + virtual QJsonObject* asJsonObject() override; + virtual void fromJsonObject(QJsonObject &json) override; + virtual SWGAISDemodSettings* fromJson(QString &jsonString) override; + + qint64 getInputFrequencyOffset(); + void setInputFrequencyOffset(qint64 input_frequency_offset); + + float getRfBandwidth(); + void setRfBandwidth(float rf_bandwidth); + + float getFmDeviation(); + void setFmDeviation(float fm_deviation); + + float getCorrelationThreshold(); + void setCorrelationThreshold(float correlation_threshold); + + qint32 getUdpEnabled(); + void setUdpEnabled(qint32 udp_enabled); + + QString* getUdpAddress(); + void setUdpAddress(QString* udp_address); + + qint32 getUdpPort(); + void setUdpPort(qint32 udp_port); + + qint32 getUdpFormat(); + void setUdpFormat(qint32 udp_format); + + qint32 getRgbColor(); + void setRgbColor(qint32 rgb_color); + + QString* getTitle(); + void setTitle(QString* title); + + qint32 getStreamIndex(); + void setStreamIndex(qint32 stream_index); + + qint32 getUseReverseApi(); + void setUseReverseApi(qint32 use_reverse_api); + + QString* getReverseApiAddress(); + void setReverseApiAddress(QString* reverse_api_address); + + qint32 getReverseApiPort(); + void setReverseApiPort(qint32 reverse_api_port); + + qint32 getReverseApiDeviceIndex(); + void setReverseApiDeviceIndex(qint32 reverse_api_device_index); + + qint32 getReverseApiChannelIndex(); + void setReverseApiChannelIndex(qint32 reverse_api_channel_index); + + + virtual bool isSet() override; + +private: + qint64 input_frequency_offset; + bool m_input_frequency_offset_isSet; + + float rf_bandwidth; + bool m_rf_bandwidth_isSet; + + float fm_deviation; + bool m_fm_deviation_isSet; + + float correlation_threshold; + bool m_correlation_threshold_isSet; + + qint32 udp_enabled; + bool m_udp_enabled_isSet; + + QString* udp_address; + bool m_udp_address_isSet; + + qint32 udp_port; + bool m_udp_port_isSet; + + qint32 udp_format; + bool m_udp_format_isSet; + + qint32 rgb_color; + bool m_rgb_color_isSet; + + QString* title; + bool m_title_isSet; + + qint32 stream_index; + bool m_stream_index_isSet; + + qint32 use_reverse_api; + bool m_use_reverse_api_isSet; + + QString* reverse_api_address; + bool m_reverse_api_address_isSet; + + qint32 reverse_api_port; + bool m_reverse_api_port_isSet; + + qint32 reverse_api_device_index; + bool m_reverse_api_device_index_isSet; + + qint32 reverse_api_channel_index; + bool m_reverse_api_channel_index_isSet; + +}; + +} + +#endif /* SWGAISDemodSettings_H_ */ diff --git a/swagger/sdrangel/code/qt5/client/SWGAISModActions.cpp b/swagger/sdrangel/code/qt5/client/SWGAISModActions.cpp new file mode 100644 index 000000000..dd2df4252 --- /dev/null +++ b/swagger/sdrangel/code/qt5/client/SWGAISModActions.cpp @@ -0,0 +1,110 @@ +/** + * SDRangel + * This is the web REST/JSON API of SDRangel SDR software. SDRangel is an Open Source Qt5/OpenGL 3.0+ (4.3+ in Windows) GUI and server Software Defined Radio and signal analyzer in software. It supports Airspy, BladeRF, HackRF, LimeSDR, PlutoSDR, RTL-SDR, SDRplay RSP1 and FunCube --- Limitations and specifcities: * In SDRangel GUI the first Rx device set cannot be deleted. Conversely the server starts with no device sets and its number of device sets can be reduced to zero by as many calls as necessary to /sdrangel/deviceset with DELETE method. * Preset import and export from/to file is a server only feature. * Device set focus is a GUI only feature. * The following channels are not implemented (status 501 is returned): ATV and DATV demodulators, Channel Analyzer NG, LoRa demodulator * The device settings and report structures contains only the sub-structure corresponding to the device type. The DeviceSettings and DeviceReport structures documented here shows all of them but only one will be or should be present at a time * The channel settings and report structures contains only the sub-structure corresponding to the channel type. The ChannelSettings and ChannelReport structures documented here shows all of them but only one will be or should be present at a time --- + * + * OpenAPI spec version: 6.0.0 + * Contact: f4exb06@gmail.com + * + * NOTE: This class is auto generated by the swagger code generator program. + * https://github.com/swagger-api/swagger-codegen.git + * Do not edit the class manually. + */ + + +#include "SWGAISModActions.h" + +#include "SWGHelpers.h" + +#include +#include +#include +#include + +namespace SWGSDRangel { + +SWGAISModActions::SWGAISModActions(QString* json) { + init(); + this->fromJson(*json); +} + +SWGAISModActions::SWGAISModActions() { + tx = nullptr; + m_tx_isSet = false; +} + +SWGAISModActions::~SWGAISModActions() { + this->cleanup(); +} + +void +SWGAISModActions::init() { + tx = new SWGAISModActions_tx(); + m_tx_isSet = false; +} + +void +SWGAISModActions::cleanup() { + if(tx != nullptr) { + delete tx; + } +} + +SWGAISModActions* +SWGAISModActions::fromJson(QString &json) { + QByteArray array (json.toStdString().c_str()); + QJsonDocument doc = QJsonDocument::fromJson(array); + QJsonObject jsonObject = doc.object(); + this->fromJsonObject(jsonObject); + return this; +} + +void +SWGAISModActions::fromJsonObject(QJsonObject &pJson) { + ::SWGSDRangel::setValue(&tx, pJson["tx"], "SWGAISModActions_tx", "SWGAISModActions_tx"); + +} + +QString +SWGAISModActions::asJson () +{ + QJsonObject* obj = this->asJsonObject(); + + QJsonDocument doc(*obj); + QByteArray bytes = doc.toJson(); + delete obj; + return QString(bytes); +} + +QJsonObject* +SWGAISModActions::asJsonObject() { + QJsonObject* obj = new QJsonObject(); + if((tx != nullptr) && (tx->isSet())){ + toJsonValue(QString("tx"), tx, obj, QString("SWGAISModActions_tx")); + } + + return obj; +} + +SWGAISModActions_tx* +SWGAISModActions::getTx() { + return tx; +} +void +SWGAISModActions::setTx(SWGAISModActions_tx* tx) { + this->tx = tx; + this->m_tx_isSet = true; +} + + +bool +SWGAISModActions::isSet(){ + bool isObjectUpdated = false; + do{ + if(tx && tx->isSet()){ + isObjectUpdated = true; break; + } + }while(false); + return isObjectUpdated; +} +} + diff --git a/swagger/sdrangel/code/qt5/client/SWGAISModActions.h b/swagger/sdrangel/code/qt5/client/SWGAISModActions.h new file mode 100644 index 000000000..d2e518141 --- /dev/null +++ b/swagger/sdrangel/code/qt5/client/SWGAISModActions.h @@ -0,0 +1,59 @@ +/** + * SDRangel + * This is the web REST/JSON API of SDRangel SDR software. SDRangel is an Open Source Qt5/OpenGL 3.0+ (4.3+ in Windows) GUI and server Software Defined Radio and signal analyzer in software. It supports Airspy, BladeRF, HackRF, LimeSDR, PlutoSDR, RTL-SDR, SDRplay RSP1 and FunCube --- Limitations and specifcities: * In SDRangel GUI the first Rx device set cannot be deleted. Conversely the server starts with no device sets and its number of device sets can be reduced to zero by as many calls as necessary to /sdrangel/deviceset with DELETE method. * Preset import and export from/to file is a server only feature. * Device set focus is a GUI only feature. * The following channels are not implemented (status 501 is returned): ATV and DATV demodulators, Channel Analyzer NG, LoRa demodulator * The device settings and report structures contains only the sub-structure corresponding to the device type. The DeviceSettings and DeviceReport structures documented here shows all of them but only one will be or should be present at a time * The channel settings and report structures contains only the sub-structure corresponding to the channel type. The ChannelSettings and ChannelReport structures documented here shows all of them but only one will be or should be present at a time --- + * + * OpenAPI spec version: 6.0.0 + * Contact: f4exb06@gmail.com + * + * NOTE: This class is auto generated by the swagger code generator program. + * https://github.com/swagger-api/swagger-codegen.git + * Do not edit the class manually. + */ + +/* + * SWGAISModActions.h + * + * AISMod + */ + +#ifndef SWGAISModActions_H_ +#define SWGAISModActions_H_ + +#include + + +#include "SWGAISModActions_tx.h" + +#include "SWGObject.h" +#include "export.h" + +namespace SWGSDRangel { + +class SWG_API SWGAISModActions: public SWGObject { +public: + SWGAISModActions(); + SWGAISModActions(QString* json); + virtual ~SWGAISModActions(); + void init(); + void cleanup(); + + virtual QString asJson () override; + virtual QJsonObject* asJsonObject() override; + virtual void fromJsonObject(QJsonObject &json) override; + virtual SWGAISModActions* fromJson(QString &jsonString) override; + + SWGAISModActions_tx* getTx(); + void setTx(SWGAISModActions_tx* tx); + + + virtual bool isSet() override; + +private: + SWGAISModActions_tx* tx; + bool m_tx_isSet; + +}; + +} + +#endif /* SWGAISModActions_H_ */ diff --git a/swagger/sdrangel/code/qt5/client/SWGAISModActions_tx.cpp b/swagger/sdrangel/code/qt5/client/SWGAISModActions_tx.cpp new file mode 100644 index 000000000..26eddd1b2 --- /dev/null +++ b/swagger/sdrangel/code/qt5/client/SWGAISModActions_tx.cpp @@ -0,0 +1,110 @@ +/** + * SDRangel + * This is the web REST/JSON API of SDRangel SDR software. SDRangel is an Open Source Qt5/OpenGL 3.0+ (4.3+ in Windows) GUI and server Software Defined Radio and signal analyzer in software. It supports Airspy, BladeRF, HackRF, LimeSDR, PlutoSDR, RTL-SDR, SDRplay RSP1 and FunCube --- Limitations and specifcities: * In SDRangel GUI the first Rx device set cannot be deleted. Conversely the server starts with no device sets and its number of device sets can be reduced to zero by as many calls as necessary to /sdrangel/deviceset with DELETE method. * Preset import and export from/to file is a server only feature. * Device set focus is a GUI only feature. * The following channels are not implemented (status 501 is returned): ATV and DATV demodulators, Channel Analyzer NG, LoRa demodulator * The device settings and report structures contains only the sub-structure corresponding to the device type. The DeviceSettings and DeviceReport structures documented here shows all of them but only one will be or should be present at a time * The channel settings and report structures contains only the sub-structure corresponding to the channel type. The ChannelSettings and ChannelReport structures documented here shows all of them but only one will be or should be present at a time --- + * + * OpenAPI spec version: 6.0.0 + * Contact: f4exb06@gmail.com + * + * NOTE: This class is auto generated by the swagger code generator program. + * https://github.com/swagger-api/swagger-codegen.git + * Do not edit the class manually. + */ + + +#include "SWGAISModActions_tx.h" + +#include "SWGHelpers.h" + +#include +#include +#include +#include + +namespace SWGSDRangel { + +SWGAISModActions_tx::SWGAISModActions_tx(QString* json) { + init(); + this->fromJson(*json); +} + +SWGAISModActions_tx::SWGAISModActions_tx() { + data = nullptr; + m_data_isSet = false; +} + +SWGAISModActions_tx::~SWGAISModActions_tx() { + this->cleanup(); +} + +void +SWGAISModActions_tx::init() { + data = new QString(""); + m_data_isSet = false; +} + +void +SWGAISModActions_tx::cleanup() { + if(data != nullptr) { + delete data; + } +} + +SWGAISModActions_tx* +SWGAISModActions_tx::fromJson(QString &json) { + QByteArray array (json.toStdString().c_str()); + QJsonDocument doc = QJsonDocument::fromJson(array); + QJsonObject jsonObject = doc.object(); + this->fromJsonObject(jsonObject); + return this; +} + +void +SWGAISModActions_tx::fromJsonObject(QJsonObject &pJson) { + ::SWGSDRangel::setValue(&data, pJson["data"], "QString", "QString"); + +} + +QString +SWGAISModActions_tx::asJson () +{ + QJsonObject* obj = this->asJsonObject(); + + QJsonDocument doc(*obj); + QByteArray bytes = doc.toJson(); + delete obj; + return QString(bytes); +} + +QJsonObject* +SWGAISModActions_tx::asJsonObject() { + QJsonObject* obj = new QJsonObject(); + if(data != nullptr && *data != QString("")){ + toJsonValue(QString("data"), data, obj, QString("QString")); + } + + return obj; +} + +QString* +SWGAISModActions_tx::getData() { + return data; +} +void +SWGAISModActions_tx::setData(QString* data) { + this->data = data; + this->m_data_isSet = true; +} + + +bool +SWGAISModActions_tx::isSet(){ + bool isObjectUpdated = false; + do{ + if(data && *data != QString("")){ + isObjectUpdated = true; break; + } + }while(false); + return isObjectUpdated; +} +} + diff --git a/swagger/sdrangel/code/qt5/client/SWGAISModActions_tx.h b/swagger/sdrangel/code/qt5/client/SWGAISModActions_tx.h new file mode 100644 index 000000000..e03dd89a6 --- /dev/null +++ b/swagger/sdrangel/code/qt5/client/SWGAISModActions_tx.h @@ -0,0 +1,59 @@ +/** + * SDRangel + * This is the web REST/JSON API of SDRangel SDR software. SDRangel is an Open Source Qt5/OpenGL 3.0+ (4.3+ in Windows) GUI and server Software Defined Radio and signal analyzer in software. It supports Airspy, BladeRF, HackRF, LimeSDR, PlutoSDR, RTL-SDR, SDRplay RSP1 and FunCube --- Limitations and specifcities: * In SDRangel GUI the first Rx device set cannot be deleted. Conversely the server starts with no device sets and its number of device sets can be reduced to zero by as many calls as necessary to /sdrangel/deviceset with DELETE method. * Preset import and export from/to file is a server only feature. * Device set focus is a GUI only feature. * The following channels are not implemented (status 501 is returned): ATV and DATV demodulators, Channel Analyzer NG, LoRa demodulator * The device settings and report structures contains only the sub-structure corresponding to the device type. The DeviceSettings and DeviceReport structures documented here shows all of them but only one will be or should be present at a time * The channel settings and report structures contains only the sub-structure corresponding to the channel type. The ChannelSettings and ChannelReport structures documented here shows all of them but only one will be or should be present at a time --- + * + * OpenAPI spec version: 6.0.0 + * Contact: f4exb06@gmail.com + * + * NOTE: This class is auto generated by the swagger code generator program. + * https://github.com/swagger-api/swagger-codegen.git + * Do not edit the class manually. + */ + +/* + * SWGAISModActions_tx.h + * + * Transmit a message + */ + +#ifndef SWGAISModActions_tx_H_ +#define SWGAISModActions_tx_H_ + +#include + + +#include + +#include "SWGObject.h" +#include "export.h" + +namespace SWGSDRangel { + +class SWG_API SWGAISModActions_tx: public SWGObject { +public: + SWGAISModActions_tx(); + SWGAISModActions_tx(QString* json); + virtual ~SWGAISModActions_tx(); + void init(); + void cleanup(); + + virtual QString asJson () override; + virtual QJsonObject* asJsonObject() override; + virtual void fromJsonObject(QJsonObject &json) override; + virtual SWGAISModActions_tx* fromJson(QString &jsonString) override; + + QString* getData(); + void setData(QString* data); + + + virtual bool isSet() override; + +private: + QString* data; + bool m_data_isSet; + +}; + +} + +#endif /* SWGAISModActions_tx_H_ */ diff --git a/swagger/sdrangel/code/qt5/client/SWGAISModReport.cpp b/swagger/sdrangel/code/qt5/client/SWGAISModReport.cpp new file mode 100644 index 000000000..fcbfc0703 --- /dev/null +++ b/swagger/sdrangel/code/qt5/client/SWGAISModReport.cpp @@ -0,0 +1,131 @@ +/** + * SDRangel + * This is the web REST/JSON API of SDRangel SDR software. SDRangel is an Open Source Qt5/OpenGL 3.0+ (4.3+ in Windows) GUI and server Software Defined Radio and signal analyzer in software. It supports Airspy, BladeRF, HackRF, LimeSDR, PlutoSDR, RTL-SDR, SDRplay RSP1 and FunCube --- Limitations and specifcities: * In SDRangel GUI the first Rx device set cannot be deleted. Conversely the server starts with no device sets and its number of device sets can be reduced to zero by as many calls as necessary to /sdrangel/deviceset with DELETE method. * Preset import and export from/to file is a server only feature. * Device set focus is a GUI only feature. * The following channels are not implemented (status 501 is returned): ATV and DATV demodulators, Channel Analyzer NG, LoRa demodulator * The device settings and report structures contains only the sub-structure corresponding to the device type. The DeviceSettings and DeviceReport structures documented here shows all of them but only one will be or should be present at a time * The channel settings and report structures contains only the sub-structure corresponding to the channel type. The ChannelSettings and ChannelReport structures documented here shows all of them but only one will be or should be present at a time --- + * + * OpenAPI spec version: 6.0.0 + * Contact: f4exb06@gmail.com + * + * NOTE: This class is auto generated by the swagger code generator program. + * https://github.com/swagger-api/swagger-codegen.git + * Do not edit the class manually. + */ + + +#include "SWGAISModReport.h" + +#include "SWGHelpers.h" + +#include +#include +#include +#include + +namespace SWGSDRangel { + +SWGAISModReport::SWGAISModReport(QString* json) { + init(); + this->fromJson(*json); +} + +SWGAISModReport::SWGAISModReport() { + channel_power_db = 0.0f; + m_channel_power_db_isSet = false; + channel_sample_rate = 0; + m_channel_sample_rate_isSet = false; +} + +SWGAISModReport::~SWGAISModReport() { + this->cleanup(); +} + +void +SWGAISModReport::init() { + channel_power_db = 0.0f; + m_channel_power_db_isSet = false; + channel_sample_rate = 0; + m_channel_sample_rate_isSet = false; +} + +void +SWGAISModReport::cleanup() { + + +} + +SWGAISModReport* +SWGAISModReport::fromJson(QString &json) { + QByteArray array (json.toStdString().c_str()); + QJsonDocument doc = QJsonDocument::fromJson(array); + QJsonObject jsonObject = doc.object(); + this->fromJsonObject(jsonObject); + return this; +} + +void +SWGAISModReport::fromJsonObject(QJsonObject &pJson) { + ::SWGSDRangel::setValue(&channel_power_db, pJson["channelPowerDB"], "float", ""); + + ::SWGSDRangel::setValue(&channel_sample_rate, pJson["channelSampleRate"], "qint32", ""); + +} + +QString +SWGAISModReport::asJson () +{ + QJsonObject* obj = this->asJsonObject(); + + QJsonDocument doc(*obj); + QByteArray bytes = doc.toJson(); + delete obj; + return QString(bytes); +} + +QJsonObject* +SWGAISModReport::asJsonObject() { + QJsonObject* obj = new QJsonObject(); + if(m_channel_power_db_isSet){ + obj->insert("channelPowerDB", QJsonValue(channel_power_db)); + } + if(m_channel_sample_rate_isSet){ + obj->insert("channelSampleRate", QJsonValue(channel_sample_rate)); + } + + return obj; +} + +float +SWGAISModReport::getChannelPowerDb() { + return channel_power_db; +} +void +SWGAISModReport::setChannelPowerDb(float channel_power_db) { + this->channel_power_db = channel_power_db; + this->m_channel_power_db_isSet = true; +} + +qint32 +SWGAISModReport::getChannelSampleRate() { + return channel_sample_rate; +} +void +SWGAISModReport::setChannelSampleRate(qint32 channel_sample_rate) { + this->channel_sample_rate = channel_sample_rate; + this->m_channel_sample_rate_isSet = true; +} + + +bool +SWGAISModReport::isSet(){ + bool isObjectUpdated = false; + do{ + if(m_channel_power_db_isSet){ + isObjectUpdated = true; break; + } + if(m_channel_sample_rate_isSet){ + isObjectUpdated = true; break; + } + }while(false); + return isObjectUpdated; +} +} + diff --git a/swagger/sdrangel/code/qt5/client/SWGAISModReport.h b/swagger/sdrangel/code/qt5/client/SWGAISModReport.h new file mode 100644 index 000000000..f879d2b92 --- /dev/null +++ b/swagger/sdrangel/code/qt5/client/SWGAISModReport.h @@ -0,0 +1,64 @@ +/** + * SDRangel + * This is the web REST/JSON API of SDRangel SDR software. SDRangel is an Open Source Qt5/OpenGL 3.0+ (4.3+ in Windows) GUI and server Software Defined Radio and signal analyzer in software. It supports Airspy, BladeRF, HackRF, LimeSDR, PlutoSDR, RTL-SDR, SDRplay RSP1 and FunCube --- Limitations and specifcities: * In SDRangel GUI the first Rx device set cannot be deleted. Conversely the server starts with no device sets and its number of device sets can be reduced to zero by as many calls as necessary to /sdrangel/deviceset with DELETE method. * Preset import and export from/to file is a server only feature. * Device set focus is a GUI only feature. * The following channels are not implemented (status 501 is returned): ATV and DATV demodulators, Channel Analyzer NG, LoRa demodulator * The device settings and report structures contains only the sub-structure corresponding to the device type. The DeviceSettings and DeviceReport structures documented here shows all of them but only one will be or should be present at a time * The channel settings and report structures contains only the sub-structure corresponding to the channel type. The ChannelSettings and ChannelReport structures documented here shows all of them but only one will be or should be present at a time --- + * + * OpenAPI spec version: 6.0.0 + * Contact: f4exb06@gmail.com + * + * NOTE: This class is auto generated by the swagger code generator program. + * https://github.com/swagger-api/swagger-codegen.git + * Do not edit the class manually. + */ + +/* + * SWGAISModReport.h + * + * AISMod + */ + +#ifndef SWGAISModReport_H_ +#define SWGAISModReport_H_ + +#include + + + +#include "SWGObject.h" +#include "export.h" + +namespace SWGSDRangel { + +class SWG_API SWGAISModReport: public SWGObject { +public: + SWGAISModReport(); + SWGAISModReport(QString* json); + virtual ~SWGAISModReport(); + void init(); + void cleanup(); + + virtual QString asJson () override; + virtual QJsonObject* asJsonObject() override; + virtual void fromJsonObject(QJsonObject &json) override; + virtual SWGAISModReport* fromJson(QString &jsonString) override; + + float getChannelPowerDb(); + void setChannelPowerDb(float channel_power_db); + + qint32 getChannelSampleRate(); + void setChannelSampleRate(qint32 channel_sample_rate); + + + virtual bool isSet() override; + +private: + float channel_power_db; + bool m_channel_power_db_isSet; + + qint32 channel_sample_rate; + bool m_channel_sample_rate_isSet; + +}; + +} + +#endif /* SWGAISModReport_H_ */ diff --git a/swagger/sdrangel/code/qt5/client/SWGAISModSettings.cpp b/swagger/sdrangel/code/qt5/client/SWGAISModSettings.cpp new file mode 100644 index 000000000..e229ae18b --- /dev/null +++ b/swagger/sdrangel/code/qt5/client/SWGAISModSettings.cpp @@ -0,0 +1,969 @@ +/** + * SDRangel + * This is the web REST/JSON API of SDRangel SDR software. SDRangel is an Open Source Qt5/OpenGL 3.0+ (4.3+ in Windows) GUI and server Software Defined Radio and signal analyzer in software. It supports Airspy, BladeRF, HackRF, LimeSDR, PlutoSDR, RTL-SDR, SDRplay RSP1 and FunCube --- Limitations and specifcities: * In SDRangel GUI the first Rx device set cannot be deleted. Conversely the server starts with no device sets and its number of device sets can be reduced to zero by as many calls as necessary to /sdrangel/deviceset with DELETE method. * Preset import and export from/to file is a server only feature. * Device set focus is a GUI only feature. * The following channels are not implemented (status 501 is returned): ATV and DATV demodulators, Channel Analyzer NG, LoRa demodulator * The device settings and report structures contains only the sub-structure corresponding to the device type. The DeviceSettings and DeviceReport structures documented here shows all of them but only one will be or should be present at a time * The channel settings and report structures contains only the sub-structure corresponding to the channel type. The ChannelSettings and ChannelReport structures documented here shows all of them but only one will be or should be present at a time --- + * + * OpenAPI spec version: 6.0.0 + * Contact: f4exb06@gmail.com + * + * NOTE: This class is auto generated by the swagger code generator program. + * https://github.com/swagger-api/swagger-codegen.git + * Do not edit the class manually. + */ + + +#include "SWGAISModSettings.h" + +#include "SWGHelpers.h" + +#include +#include +#include +#include + +namespace SWGSDRangel { + +SWGAISModSettings::SWGAISModSettings(QString* json) { + init(); + this->fromJson(*json); +} + +SWGAISModSettings::SWGAISModSettings() { + input_frequency_offset = 0L; + m_input_frequency_offset_isSet = false; + baud = 0; + m_baud_isSet = false; + rf_bandwidth = 0.0f; + m_rf_bandwidth_isSet = false; + fm_deviation = 0; + m_fm_deviation_isSet = false; + gain = 0.0f; + m_gain_isSet = false; + channel_mute = 0; + m_channel_mute_isSet = false; + repeat = 0; + m_repeat_isSet = false; + repeat_delay = 0.0f; + m_repeat_delay_isSet = false; + repeat_count = 0; + m_repeat_count_isSet = false; + ramp_up_bits = 0; + m_ramp_up_bits_isSet = false; + ramp_down_bits = 0; + m_ramp_down_bits_isSet = false; + ramp_range = 0; + m_ramp_range_isSet = false; + lpf_taps = 0; + m_lpf_taps_isSet = false; + rf_noise = 0; + m_rf_noise_isSet = false; + write_to_file = 0; + m_write_to_file_isSet = false; + spectrum_rate = 0; + m_spectrum_rate_isSet = false; + msg_id = 0; + m_msg_id_isSet = false; + mmsi = nullptr; + m_mmsi_isSet = false; + status = 0; + m_status_isSet = false; + latitude = 0.0f; + m_latitude_isSet = false; + longitude = 0.0f; + m_longitude_isSet = false; + course = 0.0f; + m_course_isSet = false; + speed = 0.0f; + m_speed_isSet = false; + heading = 0; + m_heading_isSet = false; + data = nullptr; + m_data_isSet = false; + bt = 0.0f; + m_bt_isSet = false; + symbol_span = 0; + m_symbol_span_isSet = false; + rgb_color = 0; + m_rgb_color_isSet = false; + title = nullptr; + m_title_isSet = false; + stream_index = 0; + m_stream_index_isSet = false; + use_reverse_api = 0; + m_use_reverse_api_isSet = false; + reverse_api_address = nullptr; + m_reverse_api_address_isSet = false; + reverse_api_port = 0; + m_reverse_api_port_isSet = false; + reverse_api_device_index = 0; + m_reverse_api_device_index_isSet = false; + reverse_api_channel_index = 0; + m_reverse_api_channel_index_isSet = false; + udp_enabled = 0; + m_udp_enabled_isSet = false; + udp_address = nullptr; + m_udp_address_isSet = false; + udp_port = 0; + m_udp_port_isSet = false; +} + +SWGAISModSettings::~SWGAISModSettings() { + this->cleanup(); +} + +void +SWGAISModSettings::init() { + input_frequency_offset = 0L; + m_input_frequency_offset_isSet = false; + baud = 0; + m_baud_isSet = false; + rf_bandwidth = 0.0f; + m_rf_bandwidth_isSet = false; + fm_deviation = 0; + m_fm_deviation_isSet = false; + gain = 0.0f; + m_gain_isSet = false; + channel_mute = 0; + m_channel_mute_isSet = false; + repeat = 0; + m_repeat_isSet = false; + repeat_delay = 0.0f; + m_repeat_delay_isSet = false; + repeat_count = 0; + m_repeat_count_isSet = false; + ramp_up_bits = 0; + m_ramp_up_bits_isSet = false; + ramp_down_bits = 0; + m_ramp_down_bits_isSet = false; + ramp_range = 0; + m_ramp_range_isSet = false; + lpf_taps = 0; + m_lpf_taps_isSet = false; + rf_noise = 0; + m_rf_noise_isSet = false; + write_to_file = 0; + m_write_to_file_isSet = false; + spectrum_rate = 0; + m_spectrum_rate_isSet = false; + msg_id = 0; + m_msg_id_isSet = false; + mmsi = new QString(""); + m_mmsi_isSet = false; + status = 0; + m_status_isSet = false; + latitude = 0.0f; + m_latitude_isSet = false; + longitude = 0.0f; + m_longitude_isSet = false; + course = 0.0f; + m_course_isSet = false; + speed = 0.0f; + m_speed_isSet = false; + heading = 0; + m_heading_isSet = false; + data = new QString(""); + m_data_isSet = false; + bt = 0.0f; + m_bt_isSet = false; + symbol_span = 0; + m_symbol_span_isSet = false; + rgb_color = 0; + m_rgb_color_isSet = false; + title = new QString(""); + m_title_isSet = false; + stream_index = 0; + m_stream_index_isSet = false; + use_reverse_api = 0; + m_use_reverse_api_isSet = false; + reverse_api_address = new QString(""); + m_reverse_api_address_isSet = false; + reverse_api_port = 0; + m_reverse_api_port_isSet = false; + reverse_api_device_index = 0; + m_reverse_api_device_index_isSet = false; + reverse_api_channel_index = 0; + m_reverse_api_channel_index_isSet = false; + udp_enabled = 0; + m_udp_enabled_isSet = false; + udp_address = new QString(""); + m_udp_address_isSet = false; + udp_port = 0; + m_udp_port_isSet = false; +} + +void +SWGAISModSettings::cleanup() { + + + + + + + + + + + + + + + + + + if(mmsi != nullptr) { + delete mmsi; + } + + + + + + + if(data != nullptr) { + delete data; + } + + + + if(title != nullptr) { + delete title; + } + + + if(reverse_api_address != nullptr) { + delete reverse_api_address; + } + + + + + if(udp_address != nullptr) { + delete udp_address; + } + +} + +SWGAISModSettings* +SWGAISModSettings::fromJson(QString &json) { + QByteArray array (json.toStdString().c_str()); + QJsonDocument doc = QJsonDocument::fromJson(array); + QJsonObject jsonObject = doc.object(); + this->fromJsonObject(jsonObject); + return this; +} + +void +SWGAISModSettings::fromJsonObject(QJsonObject &pJson) { + ::SWGSDRangel::setValue(&input_frequency_offset, pJson["inputFrequencyOffset"], "qint64", ""); + + ::SWGSDRangel::setValue(&baud, pJson["baud"], "qint32", ""); + + ::SWGSDRangel::setValue(&rf_bandwidth, pJson["rfBandwidth"], "float", ""); + + ::SWGSDRangel::setValue(&fm_deviation, pJson["fmDeviation"], "qint32", ""); + + ::SWGSDRangel::setValue(&gain, pJson["gain"], "float", ""); + + ::SWGSDRangel::setValue(&channel_mute, pJson["channelMute"], "qint32", ""); + + ::SWGSDRangel::setValue(&repeat, pJson["repeat"], "qint32", ""); + + ::SWGSDRangel::setValue(&repeat_delay, pJson["repeatDelay"], "float", ""); + + ::SWGSDRangel::setValue(&repeat_count, pJson["repeatCount"], "qint32", ""); + + ::SWGSDRangel::setValue(&ramp_up_bits, pJson["rampUpBits"], "qint32", ""); + + ::SWGSDRangel::setValue(&ramp_down_bits, pJson["rampDownBits"], "qint32", ""); + + ::SWGSDRangel::setValue(&ramp_range, pJson["rampRange"], "qint32", ""); + + ::SWGSDRangel::setValue(&lpf_taps, pJson["lpfTaps"], "qint32", ""); + + ::SWGSDRangel::setValue(&rf_noise, pJson["rfNoise"], "qint32", ""); + + ::SWGSDRangel::setValue(&write_to_file, pJson["writeToFile"], "qint32", ""); + + ::SWGSDRangel::setValue(&spectrum_rate, pJson["spectrumRate"], "qint32", ""); + + ::SWGSDRangel::setValue(&msg_id, pJson["msgId"], "qint32", ""); + + ::SWGSDRangel::setValue(&mmsi, pJson["mmsi"], "QString", "QString"); + + ::SWGSDRangel::setValue(&status, pJson["status"], "qint32", ""); + + ::SWGSDRangel::setValue(&latitude, pJson["latitude"], "float", ""); + + ::SWGSDRangel::setValue(&longitude, pJson["longitude"], "float", ""); + + ::SWGSDRangel::setValue(&course, pJson["course"], "float", ""); + + ::SWGSDRangel::setValue(&speed, pJson["speed"], "float", ""); + + ::SWGSDRangel::setValue(&heading, pJson["heading"], "qint32", ""); + + ::SWGSDRangel::setValue(&data, pJson["data"], "QString", "QString"); + + ::SWGSDRangel::setValue(&bt, pJson["bt"], "float", ""); + + ::SWGSDRangel::setValue(&symbol_span, pJson["symbolSpan"], "qint32", ""); + + ::SWGSDRangel::setValue(&rgb_color, pJson["rgbColor"], "qint32", ""); + + ::SWGSDRangel::setValue(&title, pJson["title"], "QString", "QString"); + + ::SWGSDRangel::setValue(&stream_index, pJson["streamIndex"], "qint32", ""); + + ::SWGSDRangel::setValue(&use_reverse_api, pJson["useReverseAPI"], "qint32", ""); + + ::SWGSDRangel::setValue(&reverse_api_address, pJson["reverseAPIAddress"], "QString", "QString"); + + ::SWGSDRangel::setValue(&reverse_api_port, pJson["reverseAPIPort"], "qint32", ""); + + ::SWGSDRangel::setValue(&reverse_api_device_index, pJson["reverseAPIDeviceIndex"], "qint32", ""); + + ::SWGSDRangel::setValue(&reverse_api_channel_index, pJson["reverseAPIChannelIndex"], "qint32", ""); + + ::SWGSDRangel::setValue(&udp_enabled, pJson["udpEnabled"], "qint32", ""); + + ::SWGSDRangel::setValue(&udp_address, pJson["udpAddress"], "QString", "QString"); + + ::SWGSDRangel::setValue(&udp_port, pJson["udpPort"], "qint32", ""); + +} + +QString +SWGAISModSettings::asJson () +{ + QJsonObject* obj = this->asJsonObject(); + + QJsonDocument doc(*obj); + QByteArray bytes = doc.toJson(); + delete obj; + return QString(bytes); +} + +QJsonObject* +SWGAISModSettings::asJsonObject() { + QJsonObject* obj = new QJsonObject(); + if(m_input_frequency_offset_isSet){ + obj->insert("inputFrequencyOffset", QJsonValue(input_frequency_offset)); + } + if(m_baud_isSet){ + obj->insert("baud", QJsonValue(baud)); + } + if(m_rf_bandwidth_isSet){ + obj->insert("rfBandwidth", QJsonValue(rf_bandwidth)); + } + if(m_fm_deviation_isSet){ + obj->insert("fmDeviation", QJsonValue(fm_deviation)); + } + if(m_gain_isSet){ + obj->insert("gain", QJsonValue(gain)); + } + if(m_channel_mute_isSet){ + obj->insert("channelMute", QJsonValue(channel_mute)); + } + if(m_repeat_isSet){ + obj->insert("repeat", QJsonValue(repeat)); + } + if(m_repeat_delay_isSet){ + obj->insert("repeatDelay", QJsonValue(repeat_delay)); + } + if(m_repeat_count_isSet){ + obj->insert("repeatCount", QJsonValue(repeat_count)); + } + if(m_ramp_up_bits_isSet){ + obj->insert("rampUpBits", QJsonValue(ramp_up_bits)); + } + if(m_ramp_down_bits_isSet){ + obj->insert("rampDownBits", QJsonValue(ramp_down_bits)); + } + if(m_ramp_range_isSet){ + obj->insert("rampRange", QJsonValue(ramp_range)); + } + if(m_lpf_taps_isSet){ + obj->insert("lpfTaps", QJsonValue(lpf_taps)); + } + if(m_rf_noise_isSet){ + obj->insert("rfNoise", QJsonValue(rf_noise)); + } + if(m_write_to_file_isSet){ + obj->insert("writeToFile", QJsonValue(write_to_file)); + } + if(m_spectrum_rate_isSet){ + obj->insert("spectrumRate", QJsonValue(spectrum_rate)); + } + if(m_msg_id_isSet){ + obj->insert("msgId", QJsonValue(msg_id)); + } + if(mmsi != nullptr && *mmsi != QString("")){ + toJsonValue(QString("mmsi"), mmsi, obj, QString("QString")); + } + if(m_status_isSet){ + obj->insert("status", QJsonValue(status)); + } + if(m_latitude_isSet){ + obj->insert("latitude", QJsonValue(latitude)); + } + if(m_longitude_isSet){ + obj->insert("longitude", QJsonValue(longitude)); + } + if(m_course_isSet){ + obj->insert("course", QJsonValue(course)); + } + if(m_speed_isSet){ + obj->insert("speed", QJsonValue(speed)); + } + if(m_heading_isSet){ + obj->insert("heading", QJsonValue(heading)); + } + if(data != nullptr && *data != QString("")){ + toJsonValue(QString("data"), data, obj, QString("QString")); + } + if(m_bt_isSet){ + obj->insert("bt", QJsonValue(bt)); + } + if(m_symbol_span_isSet){ + obj->insert("symbolSpan", QJsonValue(symbol_span)); + } + if(m_rgb_color_isSet){ + obj->insert("rgbColor", QJsonValue(rgb_color)); + } + if(title != nullptr && *title != QString("")){ + toJsonValue(QString("title"), title, obj, QString("QString")); + } + if(m_stream_index_isSet){ + obj->insert("streamIndex", QJsonValue(stream_index)); + } + if(m_use_reverse_api_isSet){ + obj->insert("useReverseAPI", QJsonValue(use_reverse_api)); + } + if(reverse_api_address != nullptr && *reverse_api_address != QString("")){ + toJsonValue(QString("reverseAPIAddress"), reverse_api_address, obj, QString("QString")); + } + if(m_reverse_api_port_isSet){ + obj->insert("reverseAPIPort", QJsonValue(reverse_api_port)); + } + if(m_reverse_api_device_index_isSet){ + obj->insert("reverseAPIDeviceIndex", QJsonValue(reverse_api_device_index)); + } + if(m_reverse_api_channel_index_isSet){ + obj->insert("reverseAPIChannelIndex", QJsonValue(reverse_api_channel_index)); + } + if(m_udp_enabled_isSet){ + obj->insert("udpEnabled", QJsonValue(udp_enabled)); + } + if(udp_address != nullptr && *udp_address != QString("")){ + toJsonValue(QString("udpAddress"), udp_address, obj, QString("QString")); + } + if(m_udp_port_isSet){ + obj->insert("udpPort", QJsonValue(udp_port)); + } + + return obj; +} + +qint64 +SWGAISModSettings::getInputFrequencyOffset() { + return input_frequency_offset; +} +void +SWGAISModSettings::setInputFrequencyOffset(qint64 input_frequency_offset) { + this->input_frequency_offset = input_frequency_offset; + this->m_input_frequency_offset_isSet = true; +} + +qint32 +SWGAISModSettings::getBaud() { + return baud; +} +void +SWGAISModSettings::setBaud(qint32 baud) { + this->baud = baud; + this->m_baud_isSet = true; +} + +float +SWGAISModSettings::getRfBandwidth() { + return rf_bandwidth; +} +void +SWGAISModSettings::setRfBandwidth(float rf_bandwidth) { + this->rf_bandwidth = rf_bandwidth; + this->m_rf_bandwidth_isSet = true; +} + +qint32 +SWGAISModSettings::getFmDeviation() { + return fm_deviation; +} +void +SWGAISModSettings::setFmDeviation(qint32 fm_deviation) { + this->fm_deviation = fm_deviation; + this->m_fm_deviation_isSet = true; +} + +float +SWGAISModSettings::getGain() { + return gain; +} +void +SWGAISModSettings::setGain(float gain) { + this->gain = gain; + this->m_gain_isSet = true; +} + +qint32 +SWGAISModSettings::getChannelMute() { + return channel_mute; +} +void +SWGAISModSettings::setChannelMute(qint32 channel_mute) { + this->channel_mute = channel_mute; + this->m_channel_mute_isSet = true; +} + +qint32 +SWGAISModSettings::getRepeat() { + return repeat; +} +void +SWGAISModSettings::setRepeat(qint32 repeat) { + this->repeat = repeat; + this->m_repeat_isSet = true; +} + +float +SWGAISModSettings::getRepeatDelay() { + return repeat_delay; +} +void +SWGAISModSettings::setRepeatDelay(float repeat_delay) { + this->repeat_delay = repeat_delay; + this->m_repeat_delay_isSet = true; +} + +qint32 +SWGAISModSettings::getRepeatCount() { + return repeat_count; +} +void +SWGAISModSettings::setRepeatCount(qint32 repeat_count) { + this->repeat_count = repeat_count; + this->m_repeat_count_isSet = true; +} + +qint32 +SWGAISModSettings::getRampUpBits() { + return ramp_up_bits; +} +void +SWGAISModSettings::setRampUpBits(qint32 ramp_up_bits) { + this->ramp_up_bits = ramp_up_bits; + this->m_ramp_up_bits_isSet = true; +} + +qint32 +SWGAISModSettings::getRampDownBits() { + return ramp_down_bits; +} +void +SWGAISModSettings::setRampDownBits(qint32 ramp_down_bits) { + this->ramp_down_bits = ramp_down_bits; + this->m_ramp_down_bits_isSet = true; +} + +qint32 +SWGAISModSettings::getRampRange() { + return ramp_range; +} +void +SWGAISModSettings::setRampRange(qint32 ramp_range) { + this->ramp_range = ramp_range; + this->m_ramp_range_isSet = true; +} + +qint32 +SWGAISModSettings::getLpfTaps() { + return lpf_taps; +} +void +SWGAISModSettings::setLpfTaps(qint32 lpf_taps) { + this->lpf_taps = lpf_taps; + this->m_lpf_taps_isSet = true; +} + +qint32 +SWGAISModSettings::getRfNoise() { + return rf_noise; +} +void +SWGAISModSettings::setRfNoise(qint32 rf_noise) { + this->rf_noise = rf_noise; + this->m_rf_noise_isSet = true; +} + +qint32 +SWGAISModSettings::getWriteToFile() { + return write_to_file; +} +void +SWGAISModSettings::setWriteToFile(qint32 write_to_file) { + this->write_to_file = write_to_file; + this->m_write_to_file_isSet = true; +} + +qint32 +SWGAISModSettings::getSpectrumRate() { + return spectrum_rate; +} +void +SWGAISModSettings::setSpectrumRate(qint32 spectrum_rate) { + this->spectrum_rate = spectrum_rate; + this->m_spectrum_rate_isSet = true; +} + +qint32 +SWGAISModSettings::getMsgId() { + return msg_id; +} +void +SWGAISModSettings::setMsgId(qint32 msg_id) { + this->msg_id = msg_id; + this->m_msg_id_isSet = true; +} + +QString* +SWGAISModSettings::getMmsi() { + return mmsi; +} +void +SWGAISModSettings::setMmsi(QString* mmsi) { + this->mmsi = mmsi; + this->m_mmsi_isSet = true; +} + +qint32 +SWGAISModSettings::getStatus() { + return status; +} +void +SWGAISModSettings::setStatus(qint32 status) { + this->status = status; + this->m_status_isSet = true; +} + +float +SWGAISModSettings::getLatitude() { + return latitude; +} +void +SWGAISModSettings::setLatitude(float latitude) { + this->latitude = latitude; + this->m_latitude_isSet = true; +} + +float +SWGAISModSettings::getLongitude() { + return longitude; +} +void +SWGAISModSettings::setLongitude(float longitude) { + this->longitude = longitude; + this->m_longitude_isSet = true; +} + +float +SWGAISModSettings::getCourse() { + return course; +} +void +SWGAISModSettings::setCourse(float course) { + this->course = course; + this->m_course_isSet = true; +} + +float +SWGAISModSettings::getSpeed() { + return speed; +} +void +SWGAISModSettings::setSpeed(float speed) { + this->speed = speed; + this->m_speed_isSet = true; +} + +qint32 +SWGAISModSettings::getHeading() { + return heading; +} +void +SWGAISModSettings::setHeading(qint32 heading) { + this->heading = heading; + this->m_heading_isSet = true; +} + +QString* +SWGAISModSettings::getData() { + return data; +} +void +SWGAISModSettings::setData(QString* data) { + this->data = data; + this->m_data_isSet = true; +} + +float +SWGAISModSettings::getBt() { + return bt; +} +void +SWGAISModSettings::setBt(float bt) { + this->bt = bt; + this->m_bt_isSet = true; +} + +qint32 +SWGAISModSettings::getSymbolSpan() { + return symbol_span; +} +void +SWGAISModSettings::setSymbolSpan(qint32 symbol_span) { + this->symbol_span = symbol_span; + this->m_symbol_span_isSet = true; +} + +qint32 +SWGAISModSettings::getRgbColor() { + return rgb_color; +} +void +SWGAISModSettings::setRgbColor(qint32 rgb_color) { + this->rgb_color = rgb_color; + this->m_rgb_color_isSet = true; +} + +QString* +SWGAISModSettings::getTitle() { + return title; +} +void +SWGAISModSettings::setTitle(QString* title) { + this->title = title; + this->m_title_isSet = true; +} + +qint32 +SWGAISModSettings::getStreamIndex() { + return stream_index; +} +void +SWGAISModSettings::setStreamIndex(qint32 stream_index) { + this->stream_index = stream_index; + this->m_stream_index_isSet = true; +} + +qint32 +SWGAISModSettings::getUseReverseApi() { + return use_reverse_api; +} +void +SWGAISModSettings::setUseReverseApi(qint32 use_reverse_api) { + this->use_reverse_api = use_reverse_api; + this->m_use_reverse_api_isSet = true; +} + +QString* +SWGAISModSettings::getReverseApiAddress() { + return reverse_api_address; +} +void +SWGAISModSettings::setReverseApiAddress(QString* reverse_api_address) { + this->reverse_api_address = reverse_api_address; + this->m_reverse_api_address_isSet = true; +} + +qint32 +SWGAISModSettings::getReverseApiPort() { + return reverse_api_port; +} +void +SWGAISModSettings::setReverseApiPort(qint32 reverse_api_port) { + this->reverse_api_port = reverse_api_port; + this->m_reverse_api_port_isSet = true; +} + +qint32 +SWGAISModSettings::getReverseApiDeviceIndex() { + return reverse_api_device_index; +} +void +SWGAISModSettings::setReverseApiDeviceIndex(qint32 reverse_api_device_index) { + this->reverse_api_device_index = reverse_api_device_index; + this->m_reverse_api_device_index_isSet = true; +} + +qint32 +SWGAISModSettings::getReverseApiChannelIndex() { + return reverse_api_channel_index; +} +void +SWGAISModSettings::setReverseApiChannelIndex(qint32 reverse_api_channel_index) { + this->reverse_api_channel_index = reverse_api_channel_index; + this->m_reverse_api_channel_index_isSet = true; +} + +qint32 +SWGAISModSettings::getUdpEnabled() { + return udp_enabled; +} +void +SWGAISModSettings::setUdpEnabled(qint32 udp_enabled) { + this->udp_enabled = udp_enabled; + this->m_udp_enabled_isSet = true; +} + +QString* +SWGAISModSettings::getUdpAddress() { + return udp_address; +} +void +SWGAISModSettings::setUdpAddress(QString* udp_address) { + this->udp_address = udp_address; + this->m_udp_address_isSet = true; +} + +qint32 +SWGAISModSettings::getUdpPort() { + return udp_port; +} +void +SWGAISModSettings::setUdpPort(qint32 udp_port) { + this->udp_port = udp_port; + this->m_udp_port_isSet = true; +} + + +bool +SWGAISModSettings::isSet(){ + bool isObjectUpdated = false; + do{ + if(m_input_frequency_offset_isSet){ + isObjectUpdated = true; break; + } + if(m_baud_isSet){ + isObjectUpdated = true; break; + } + if(m_rf_bandwidth_isSet){ + isObjectUpdated = true; break; + } + if(m_fm_deviation_isSet){ + isObjectUpdated = true; break; + } + if(m_gain_isSet){ + isObjectUpdated = true; break; + } + if(m_channel_mute_isSet){ + isObjectUpdated = true; break; + } + if(m_repeat_isSet){ + isObjectUpdated = true; break; + } + if(m_repeat_delay_isSet){ + isObjectUpdated = true; break; + } + if(m_repeat_count_isSet){ + isObjectUpdated = true; break; + } + if(m_ramp_up_bits_isSet){ + isObjectUpdated = true; break; + } + if(m_ramp_down_bits_isSet){ + isObjectUpdated = true; break; + } + if(m_ramp_range_isSet){ + isObjectUpdated = true; break; + } + if(m_lpf_taps_isSet){ + isObjectUpdated = true; break; + } + if(m_rf_noise_isSet){ + isObjectUpdated = true; break; + } + if(m_write_to_file_isSet){ + isObjectUpdated = true; break; + } + if(m_spectrum_rate_isSet){ + isObjectUpdated = true; break; + } + if(m_msg_id_isSet){ + isObjectUpdated = true; break; + } + if(mmsi && *mmsi != QString("")){ + isObjectUpdated = true; break; + } + if(m_status_isSet){ + isObjectUpdated = true; break; + } + if(m_latitude_isSet){ + isObjectUpdated = true; break; + } + if(m_longitude_isSet){ + isObjectUpdated = true; break; + } + if(m_course_isSet){ + isObjectUpdated = true; break; + } + if(m_speed_isSet){ + isObjectUpdated = true; break; + } + if(m_heading_isSet){ + isObjectUpdated = true; break; + } + if(data && *data != QString("")){ + isObjectUpdated = true; break; + } + if(m_bt_isSet){ + isObjectUpdated = true; break; + } + if(m_symbol_span_isSet){ + isObjectUpdated = true; break; + } + if(m_rgb_color_isSet){ + isObjectUpdated = true; break; + } + if(title && *title != QString("")){ + isObjectUpdated = true; break; + } + if(m_stream_index_isSet){ + isObjectUpdated = true; break; + } + if(m_use_reverse_api_isSet){ + isObjectUpdated = true; break; + } + if(reverse_api_address && *reverse_api_address != QString("")){ + isObjectUpdated = true; break; + } + if(m_reverse_api_port_isSet){ + isObjectUpdated = true; break; + } + if(m_reverse_api_device_index_isSet){ + isObjectUpdated = true; break; + } + if(m_reverse_api_channel_index_isSet){ + isObjectUpdated = true; break; + } + if(m_udp_enabled_isSet){ + isObjectUpdated = true; break; + } + if(udp_address && *udp_address != QString("")){ + isObjectUpdated = true; break; + } + if(m_udp_port_isSet){ + isObjectUpdated = true; break; + } + }while(false); + return isObjectUpdated; +} +} + diff --git a/swagger/sdrangel/code/qt5/client/SWGAISModSettings.h b/swagger/sdrangel/code/qt5/client/SWGAISModSettings.h new file mode 100644 index 000000000..2dcb62608 --- /dev/null +++ b/swagger/sdrangel/code/qt5/client/SWGAISModSettings.h @@ -0,0 +1,281 @@ +/** + * SDRangel + * This is the web REST/JSON API of SDRangel SDR software. SDRangel is an Open Source Qt5/OpenGL 3.0+ (4.3+ in Windows) GUI and server Software Defined Radio and signal analyzer in software. It supports Airspy, BladeRF, HackRF, LimeSDR, PlutoSDR, RTL-SDR, SDRplay RSP1 and FunCube --- Limitations and specifcities: * In SDRangel GUI the first Rx device set cannot be deleted. Conversely the server starts with no device sets and its number of device sets can be reduced to zero by as many calls as necessary to /sdrangel/deviceset with DELETE method. * Preset import and export from/to file is a server only feature. * Device set focus is a GUI only feature. * The following channels are not implemented (status 501 is returned): ATV and DATV demodulators, Channel Analyzer NG, LoRa demodulator * The device settings and report structures contains only the sub-structure corresponding to the device type. The DeviceSettings and DeviceReport structures documented here shows all of them but only one will be or should be present at a time * The channel settings and report structures contains only the sub-structure corresponding to the channel type. The ChannelSettings and ChannelReport structures documented here shows all of them but only one will be or should be present at a time --- + * + * OpenAPI spec version: 6.0.0 + * Contact: f4exb06@gmail.com + * + * NOTE: This class is auto generated by the swagger code generator program. + * https://github.com/swagger-api/swagger-codegen.git + * Do not edit the class manually. + */ + +/* + * SWGAISModSettings.h + * + * AISMod + */ + +#ifndef SWGAISModSettings_H_ +#define SWGAISModSettings_H_ + +#include + + +#include + +#include "SWGObject.h" +#include "export.h" + +namespace SWGSDRangel { + +class SWG_API SWGAISModSettings: public SWGObject { +public: + SWGAISModSettings(); + SWGAISModSettings(QString* json); + virtual ~SWGAISModSettings(); + void init(); + void cleanup(); + + virtual QString asJson () override; + virtual QJsonObject* asJsonObject() override; + virtual void fromJsonObject(QJsonObject &json) override; + virtual SWGAISModSettings* fromJson(QString &jsonString) override; + + qint64 getInputFrequencyOffset(); + void setInputFrequencyOffset(qint64 input_frequency_offset); + + qint32 getBaud(); + void setBaud(qint32 baud); + + float getRfBandwidth(); + void setRfBandwidth(float rf_bandwidth); + + qint32 getFmDeviation(); + void setFmDeviation(qint32 fm_deviation); + + float getGain(); + void setGain(float gain); + + qint32 getChannelMute(); + void setChannelMute(qint32 channel_mute); + + qint32 getRepeat(); + void setRepeat(qint32 repeat); + + float getRepeatDelay(); + void setRepeatDelay(float repeat_delay); + + qint32 getRepeatCount(); + void setRepeatCount(qint32 repeat_count); + + qint32 getRampUpBits(); + void setRampUpBits(qint32 ramp_up_bits); + + qint32 getRampDownBits(); + void setRampDownBits(qint32 ramp_down_bits); + + qint32 getRampRange(); + void setRampRange(qint32 ramp_range); + + qint32 getLpfTaps(); + void setLpfTaps(qint32 lpf_taps); + + qint32 getRfNoise(); + void setRfNoise(qint32 rf_noise); + + qint32 getWriteToFile(); + void setWriteToFile(qint32 write_to_file); + + qint32 getSpectrumRate(); + void setSpectrumRate(qint32 spectrum_rate); + + qint32 getMsgId(); + void setMsgId(qint32 msg_id); + + QString* getMmsi(); + void setMmsi(QString* mmsi); + + qint32 getStatus(); + void setStatus(qint32 status); + + float getLatitude(); + void setLatitude(float latitude); + + float getLongitude(); + void setLongitude(float longitude); + + float getCourse(); + void setCourse(float course); + + float getSpeed(); + void setSpeed(float speed); + + qint32 getHeading(); + void setHeading(qint32 heading); + + QString* getData(); + void setData(QString* data); + + float getBt(); + void setBt(float bt); + + qint32 getSymbolSpan(); + void setSymbolSpan(qint32 symbol_span); + + qint32 getRgbColor(); + void setRgbColor(qint32 rgb_color); + + QString* getTitle(); + void setTitle(QString* title); + + qint32 getStreamIndex(); + void setStreamIndex(qint32 stream_index); + + qint32 getUseReverseApi(); + void setUseReverseApi(qint32 use_reverse_api); + + QString* getReverseApiAddress(); + void setReverseApiAddress(QString* reverse_api_address); + + qint32 getReverseApiPort(); + void setReverseApiPort(qint32 reverse_api_port); + + qint32 getReverseApiDeviceIndex(); + void setReverseApiDeviceIndex(qint32 reverse_api_device_index); + + qint32 getReverseApiChannelIndex(); + void setReverseApiChannelIndex(qint32 reverse_api_channel_index); + + qint32 getUdpEnabled(); + void setUdpEnabled(qint32 udp_enabled); + + QString* getUdpAddress(); + void setUdpAddress(QString* udp_address); + + qint32 getUdpPort(); + void setUdpPort(qint32 udp_port); + + + virtual bool isSet() override; + +private: + qint64 input_frequency_offset; + bool m_input_frequency_offset_isSet; + + qint32 baud; + bool m_baud_isSet; + + float rf_bandwidth; + bool m_rf_bandwidth_isSet; + + qint32 fm_deviation; + bool m_fm_deviation_isSet; + + float gain; + bool m_gain_isSet; + + qint32 channel_mute; + bool m_channel_mute_isSet; + + qint32 repeat; + bool m_repeat_isSet; + + float repeat_delay; + bool m_repeat_delay_isSet; + + qint32 repeat_count; + bool m_repeat_count_isSet; + + qint32 ramp_up_bits; + bool m_ramp_up_bits_isSet; + + qint32 ramp_down_bits; + bool m_ramp_down_bits_isSet; + + qint32 ramp_range; + bool m_ramp_range_isSet; + + qint32 lpf_taps; + bool m_lpf_taps_isSet; + + qint32 rf_noise; + bool m_rf_noise_isSet; + + qint32 write_to_file; + bool m_write_to_file_isSet; + + qint32 spectrum_rate; + bool m_spectrum_rate_isSet; + + qint32 msg_id; + bool m_msg_id_isSet; + + QString* mmsi; + bool m_mmsi_isSet; + + qint32 status; + bool m_status_isSet; + + float latitude; + bool m_latitude_isSet; + + float longitude; + bool m_longitude_isSet; + + float course; + bool m_course_isSet; + + float speed; + bool m_speed_isSet; + + qint32 heading; + bool m_heading_isSet; + + QString* data; + bool m_data_isSet; + + float bt; + bool m_bt_isSet; + + qint32 symbol_span; + bool m_symbol_span_isSet; + + qint32 rgb_color; + bool m_rgb_color_isSet; + + QString* title; + bool m_title_isSet; + + qint32 stream_index; + bool m_stream_index_isSet; + + qint32 use_reverse_api; + bool m_use_reverse_api_isSet; + + QString* reverse_api_address; + bool m_reverse_api_address_isSet; + + qint32 reverse_api_port; + bool m_reverse_api_port_isSet; + + qint32 reverse_api_device_index; + bool m_reverse_api_device_index_isSet; + + qint32 reverse_api_channel_index; + bool m_reverse_api_channel_index_isSet; + + qint32 udp_enabled; + bool m_udp_enabled_isSet; + + QString* udp_address; + bool m_udp_address_isSet; + + qint32 udp_port; + bool m_udp_port_isSet; + +}; + +} + +#endif /* SWGAISModSettings_H_ */ diff --git a/swagger/sdrangel/code/qt5/client/SWGAISSettings.cpp b/swagger/sdrangel/code/qt5/client/SWGAISSettings.cpp new file mode 100644 index 000000000..58bcbdfb2 --- /dev/null +++ b/swagger/sdrangel/code/qt5/client/SWGAISSettings.cpp @@ -0,0 +1,250 @@ +/** + * SDRangel + * This is the web REST/JSON API of SDRangel SDR software. SDRangel is an Open Source Qt5/OpenGL 3.0+ (4.3+ in Windows) GUI and server Software Defined Radio and signal analyzer in software. It supports Airspy, BladeRF, HackRF, LimeSDR, PlutoSDR, RTL-SDR, SDRplay RSP1 and FunCube --- Limitations and specifcities: * In SDRangel GUI the first Rx device set cannot be deleted. Conversely the server starts with no device sets and its number of device sets can be reduced to zero by as many calls as necessary to /sdrangel/deviceset with DELETE method. * Preset import and export from/to file is a server only feature. * Device set focus is a GUI only feature. * The following channels are not implemented (status 501 is returned): ATV and DATV demodulators, Channel Analyzer NG, LoRa demodulator * The device settings and report structures contains only the sub-structure corresponding to the device type. The DeviceSettings and DeviceReport structures documented here shows all of them but only one will be or should be present at a time * The channel settings and report structures contains only the sub-structure corresponding to the channel type. The ChannelSettings and ChannelReport structures documented here shows all of them but only one will be or should be present at a time --- + * + * OpenAPI spec version: 6.0.0 + * Contact: f4exb06@gmail.com + * + * NOTE: This class is auto generated by the swagger code generator program. + * https://github.com/swagger-api/swagger-codegen.git + * Do not edit the class manually. + */ + + +#include "SWGAISSettings.h" + +#include "SWGHelpers.h" + +#include +#include +#include +#include + +namespace SWGSDRangel { + +SWGAISSettings::SWGAISSettings(QString* json) { + init(); + this->fromJson(*json); +} + +SWGAISSettings::SWGAISSettings() { + title = nullptr; + m_title_isSet = false; + rgb_color = 0; + m_rgb_color_isSet = false; + use_reverse_api = 0; + m_use_reverse_api_isSet = false; + reverse_api_address = nullptr; + m_reverse_api_address_isSet = false; + reverse_api_port = 0; + m_reverse_api_port_isSet = false; + reverse_api_device_index = 0; + m_reverse_api_device_index_isSet = false; + reverse_api_channel_index = 0; + m_reverse_api_channel_index_isSet = false; +} + +SWGAISSettings::~SWGAISSettings() { + this->cleanup(); +} + +void +SWGAISSettings::init() { + title = new QString(""); + m_title_isSet = false; + rgb_color = 0; + m_rgb_color_isSet = false; + use_reverse_api = 0; + m_use_reverse_api_isSet = false; + reverse_api_address = new QString(""); + m_reverse_api_address_isSet = false; + reverse_api_port = 0; + m_reverse_api_port_isSet = false; + reverse_api_device_index = 0; + m_reverse_api_device_index_isSet = false; + reverse_api_channel_index = 0; + m_reverse_api_channel_index_isSet = false; +} + +void +SWGAISSettings::cleanup() { + if(title != nullptr) { + delete title; + } + + + if(reverse_api_address != nullptr) { + delete reverse_api_address; + } + + + +} + +SWGAISSettings* +SWGAISSettings::fromJson(QString &json) { + QByteArray array (json.toStdString().c_str()); + QJsonDocument doc = QJsonDocument::fromJson(array); + QJsonObject jsonObject = doc.object(); + this->fromJsonObject(jsonObject); + return this; +} + +void +SWGAISSettings::fromJsonObject(QJsonObject &pJson) { + ::SWGSDRangel::setValue(&title, pJson["title"], "QString", "QString"); + + ::SWGSDRangel::setValue(&rgb_color, pJson["rgbColor"], "qint32", ""); + + ::SWGSDRangel::setValue(&use_reverse_api, pJson["useReverseAPI"], "qint32", ""); + + ::SWGSDRangel::setValue(&reverse_api_address, pJson["reverseAPIAddress"], "QString", "QString"); + + ::SWGSDRangel::setValue(&reverse_api_port, pJson["reverseAPIPort"], "qint32", ""); + + ::SWGSDRangel::setValue(&reverse_api_device_index, pJson["reverseAPIDeviceIndex"], "qint32", ""); + + ::SWGSDRangel::setValue(&reverse_api_channel_index, pJson["reverseAPIChannelIndex"], "qint32", ""); + +} + +QString +SWGAISSettings::asJson () +{ + QJsonObject* obj = this->asJsonObject(); + + QJsonDocument doc(*obj); + QByteArray bytes = doc.toJson(); + delete obj; + return QString(bytes); +} + +QJsonObject* +SWGAISSettings::asJsonObject() { + QJsonObject* obj = new QJsonObject(); + if(title != nullptr && *title != QString("")){ + toJsonValue(QString("title"), title, obj, QString("QString")); + } + if(m_rgb_color_isSet){ + obj->insert("rgbColor", QJsonValue(rgb_color)); + } + if(m_use_reverse_api_isSet){ + obj->insert("useReverseAPI", QJsonValue(use_reverse_api)); + } + if(reverse_api_address != nullptr && *reverse_api_address != QString("")){ + toJsonValue(QString("reverseAPIAddress"), reverse_api_address, obj, QString("QString")); + } + if(m_reverse_api_port_isSet){ + obj->insert("reverseAPIPort", QJsonValue(reverse_api_port)); + } + if(m_reverse_api_device_index_isSet){ + obj->insert("reverseAPIDeviceIndex", QJsonValue(reverse_api_device_index)); + } + if(m_reverse_api_channel_index_isSet){ + obj->insert("reverseAPIChannelIndex", QJsonValue(reverse_api_channel_index)); + } + + return obj; +} + +QString* +SWGAISSettings::getTitle() { + return title; +} +void +SWGAISSettings::setTitle(QString* title) { + this->title = title; + this->m_title_isSet = true; +} + +qint32 +SWGAISSettings::getRgbColor() { + return rgb_color; +} +void +SWGAISSettings::setRgbColor(qint32 rgb_color) { + this->rgb_color = rgb_color; + this->m_rgb_color_isSet = true; +} + +qint32 +SWGAISSettings::getUseReverseApi() { + return use_reverse_api; +} +void +SWGAISSettings::setUseReverseApi(qint32 use_reverse_api) { + this->use_reverse_api = use_reverse_api; + this->m_use_reverse_api_isSet = true; +} + +QString* +SWGAISSettings::getReverseApiAddress() { + return reverse_api_address; +} +void +SWGAISSettings::setReverseApiAddress(QString* reverse_api_address) { + this->reverse_api_address = reverse_api_address; + this->m_reverse_api_address_isSet = true; +} + +qint32 +SWGAISSettings::getReverseApiPort() { + return reverse_api_port; +} +void +SWGAISSettings::setReverseApiPort(qint32 reverse_api_port) { + this->reverse_api_port = reverse_api_port; + this->m_reverse_api_port_isSet = true; +} + +qint32 +SWGAISSettings::getReverseApiDeviceIndex() { + return reverse_api_device_index; +} +void +SWGAISSettings::setReverseApiDeviceIndex(qint32 reverse_api_device_index) { + this->reverse_api_device_index = reverse_api_device_index; + this->m_reverse_api_device_index_isSet = true; +} + +qint32 +SWGAISSettings::getReverseApiChannelIndex() { + return reverse_api_channel_index; +} +void +SWGAISSettings::setReverseApiChannelIndex(qint32 reverse_api_channel_index) { + this->reverse_api_channel_index = reverse_api_channel_index; + this->m_reverse_api_channel_index_isSet = true; +} + + +bool +SWGAISSettings::isSet(){ + bool isObjectUpdated = false; + do{ + if(title && *title != QString("")){ + isObjectUpdated = true; break; + } + if(m_rgb_color_isSet){ + isObjectUpdated = true; break; + } + if(m_use_reverse_api_isSet){ + isObjectUpdated = true; break; + } + if(reverse_api_address && *reverse_api_address != QString("")){ + isObjectUpdated = true; break; + } + if(m_reverse_api_port_isSet){ + isObjectUpdated = true; break; + } + if(m_reverse_api_device_index_isSet){ + isObjectUpdated = true; break; + } + if(m_reverse_api_channel_index_isSet){ + isObjectUpdated = true; break; + } + }while(false); + return isObjectUpdated; +} +} + diff --git a/swagger/sdrangel/code/qt5/client/SWGAISSettings.h b/swagger/sdrangel/code/qt5/client/SWGAISSettings.h new file mode 100644 index 000000000..48db7e976 --- /dev/null +++ b/swagger/sdrangel/code/qt5/client/SWGAISSettings.h @@ -0,0 +1,95 @@ +/** + * SDRangel + * This is the web REST/JSON API of SDRangel SDR software. SDRangel is an Open Source Qt5/OpenGL 3.0+ (4.3+ in Windows) GUI and server Software Defined Radio and signal analyzer in software. It supports Airspy, BladeRF, HackRF, LimeSDR, PlutoSDR, RTL-SDR, SDRplay RSP1 and FunCube --- Limitations and specifcities: * In SDRangel GUI the first Rx device set cannot be deleted. Conversely the server starts with no device sets and its number of device sets can be reduced to zero by as many calls as necessary to /sdrangel/deviceset with DELETE method. * Preset import and export from/to file is a server only feature. * Device set focus is a GUI only feature. * The following channels are not implemented (status 501 is returned): ATV and DATV demodulators, Channel Analyzer NG, LoRa demodulator * The device settings and report structures contains only the sub-structure corresponding to the device type. The DeviceSettings and DeviceReport structures documented here shows all of them but only one will be or should be present at a time * The channel settings and report structures contains only the sub-structure corresponding to the channel type. The ChannelSettings and ChannelReport structures documented here shows all of them but only one will be or should be present at a time --- + * + * OpenAPI spec version: 6.0.0 + * Contact: f4exb06@gmail.com + * + * NOTE: This class is auto generated by the swagger code generator program. + * https://github.com/swagger-api/swagger-codegen.git + * Do not edit the class manually. + */ + +/* + * SWGAISSettings.h + * + * AIS settings + */ + +#ifndef SWGAISSettings_H_ +#define SWGAISSettings_H_ + +#include + + +#include + +#include "SWGObject.h" +#include "export.h" + +namespace SWGSDRangel { + +class SWG_API SWGAISSettings: public SWGObject { +public: + SWGAISSettings(); + SWGAISSettings(QString* json); + virtual ~SWGAISSettings(); + void init(); + void cleanup(); + + virtual QString asJson () override; + virtual QJsonObject* asJsonObject() override; + virtual void fromJsonObject(QJsonObject &json) override; + virtual SWGAISSettings* fromJson(QString &jsonString) override; + + QString* getTitle(); + void setTitle(QString* title); + + qint32 getRgbColor(); + void setRgbColor(qint32 rgb_color); + + qint32 getUseReverseApi(); + void setUseReverseApi(qint32 use_reverse_api); + + QString* getReverseApiAddress(); + void setReverseApiAddress(QString* reverse_api_address); + + qint32 getReverseApiPort(); + void setReverseApiPort(qint32 reverse_api_port); + + qint32 getReverseApiDeviceIndex(); + void setReverseApiDeviceIndex(qint32 reverse_api_device_index); + + qint32 getReverseApiChannelIndex(); + void setReverseApiChannelIndex(qint32 reverse_api_channel_index); + + + virtual bool isSet() override; + +private: + QString* title; + bool m_title_isSet; + + qint32 rgb_color; + bool m_rgb_color_isSet; + + qint32 use_reverse_api; + bool m_use_reverse_api_isSet; + + QString* reverse_api_address; + bool m_reverse_api_address_isSet; + + qint32 reverse_api_port; + bool m_reverse_api_port_isSet; + + qint32 reverse_api_device_index; + bool m_reverse_api_device_index_isSet; + + qint32 reverse_api_channel_index; + bool m_reverse_api_channel_index_isSet; + +}; + +} + +#endif /* SWGAISSettings_H_ */ diff --git a/swagger/sdrangel/code/qt5/client/SWGChannelActions.cpp b/swagger/sdrangel/code/qt5/client/SWGChannelActions.cpp index 7893bbcaa..10fe16473 100644 --- a/swagger/sdrangel/code/qt5/client/SWGChannelActions.cpp +++ b/swagger/sdrangel/code/qt5/client/SWGChannelActions.cpp @@ -36,6 +36,8 @@ SWGChannelActions::SWGChannelActions() { m_originator_device_set_index_isSet = false; originator_channel_index = 0; m_originator_channel_index_isSet = false; + ais_mod_actions = nullptr; + m_ais_mod_actions_isSet = false; apt_demod_actions = nullptr; m_apt_demod_actions_isSet = false; file_sink_actions = nullptr; @@ -64,6 +66,8 @@ SWGChannelActions::init() { m_originator_device_set_index_isSet = false; originator_channel_index = 0; m_originator_channel_index_isSet = false; + ais_mod_actions = new SWGAISModActions(); + m_ais_mod_actions_isSet = false; apt_demod_actions = new SWGAPTDemodActions(); m_apt_demod_actions_isSet = false; file_sink_actions = new SWGFileSinkActions(); @@ -86,6 +90,9 @@ SWGChannelActions::cleanup() { + if(ais_mod_actions != nullptr) { + delete ais_mod_actions; + } if(apt_demod_actions != nullptr) { delete apt_demod_actions; } @@ -125,6 +132,8 @@ SWGChannelActions::fromJsonObject(QJsonObject &pJson) { ::SWGSDRangel::setValue(&originator_channel_index, pJson["originatorChannelIndex"], "qint32", ""); + ::SWGSDRangel::setValue(&ais_mod_actions, pJson["AISModActions"], "SWGAISModActions", "SWGAISModActions"); + ::SWGSDRangel::setValue(&apt_demod_actions, pJson["APTDemodActions"], "SWGAPTDemodActions", "SWGAPTDemodActions"); ::SWGSDRangel::setValue(&file_sink_actions, pJson["FileSinkActions"], "SWGFileSinkActions", "SWGFileSinkActions"); @@ -165,6 +174,9 @@ SWGChannelActions::asJsonObject() { if(m_originator_channel_index_isSet){ obj->insert("originatorChannelIndex", QJsonValue(originator_channel_index)); } + if((ais_mod_actions != nullptr) && (ais_mod_actions->isSet())){ + toJsonValue(QString("AISModActions"), ais_mod_actions, obj, QString("SWGAISModActions")); + } if((apt_demod_actions != nullptr) && (apt_demod_actions->isSet())){ toJsonValue(QString("APTDemodActions"), apt_demod_actions, obj, QString("SWGAPTDemodActions")); } @@ -227,6 +239,16 @@ SWGChannelActions::setOriginatorChannelIndex(qint32 originator_channel_index) { this->m_originator_channel_index_isSet = true; } +SWGAISModActions* +SWGChannelActions::getAisModActions() { + return ais_mod_actions; +} +void +SWGChannelActions::setAisModActions(SWGAISModActions* ais_mod_actions) { + this->ais_mod_actions = ais_mod_actions; + this->m_ais_mod_actions_isSet = true; +} + SWGAPTDemodActions* SWGChannelActions::getAptDemodActions() { return apt_demod_actions; @@ -304,6 +326,9 @@ SWGChannelActions::isSet(){ if(m_originator_channel_index_isSet){ isObjectUpdated = true; break; } + if(ais_mod_actions && ais_mod_actions->isSet()){ + isObjectUpdated = true; break; + } if(apt_demod_actions && apt_demod_actions->isSet()){ isObjectUpdated = true; break; } diff --git a/swagger/sdrangel/code/qt5/client/SWGChannelActions.h b/swagger/sdrangel/code/qt5/client/SWGChannelActions.h index 04d8f2f5a..d80b97983 100644 --- a/swagger/sdrangel/code/qt5/client/SWGChannelActions.h +++ b/swagger/sdrangel/code/qt5/client/SWGChannelActions.h @@ -22,6 +22,7 @@ #include +#include "SWGAISModActions.h" #include "SWGAPTDemodActions.h" #include "SWGFileSinkActions.h" #include "SWGFileSourceActions.h" @@ -60,6 +61,9 @@ public: qint32 getOriginatorChannelIndex(); void setOriginatorChannelIndex(qint32 originator_channel_index); + SWGAISModActions* getAisModActions(); + void setAisModActions(SWGAISModActions* ais_mod_actions); + SWGAPTDemodActions* getAptDemodActions(); void setAptDemodActions(SWGAPTDemodActions* apt_demod_actions); @@ -94,6 +98,9 @@ private: qint32 originator_channel_index; bool m_originator_channel_index_isSet; + SWGAISModActions* ais_mod_actions; + bool m_ais_mod_actions_isSet; + SWGAPTDemodActions* apt_demod_actions; bool m_apt_demod_actions_isSet; diff --git a/swagger/sdrangel/code/qt5/client/SWGChannelReport.cpp b/swagger/sdrangel/code/qt5/client/SWGChannelReport.cpp index ab469933b..d61c58255 100644 --- a/swagger/sdrangel/code/qt5/client/SWGChannelReport.cpp +++ b/swagger/sdrangel/code/qt5/client/SWGChannelReport.cpp @@ -34,6 +34,10 @@ SWGChannelReport::SWGChannelReport() { m_direction_isSet = false; adsb_demod_report = nullptr; m_adsb_demod_report_isSet = false; + ais_demod_report = nullptr; + m_ais_demod_report_isSet = false; + ais_mod_report = nullptr; + m_ais_mod_report_isSet = false; am_demod_report = nullptr; m_am_demod_report_isSet = false; am_mod_report = nullptr; @@ -102,6 +106,10 @@ SWGChannelReport::init() { m_direction_isSet = false; adsb_demod_report = new SWGADSBDemodReport(); m_adsb_demod_report_isSet = false; + ais_demod_report = new SWGAISDemodReport(); + m_ais_demod_report_isSet = false; + ais_mod_report = new SWGAISModReport(); + m_ais_mod_report_isSet = false; am_demod_report = new SWGAMDemodReport(); m_am_demod_report_isSet = false; am_mod_report = new SWGAMModReport(); @@ -167,6 +175,12 @@ SWGChannelReport::cleanup() { if(adsb_demod_report != nullptr) { delete adsb_demod_report; } + if(ais_demod_report != nullptr) { + delete ais_demod_report; + } + if(ais_mod_report != nullptr) { + delete ais_mod_report; + } if(am_demod_report != nullptr) { delete am_demod_report; } @@ -267,6 +281,10 @@ SWGChannelReport::fromJsonObject(QJsonObject &pJson) { ::SWGSDRangel::setValue(&adsb_demod_report, pJson["ADSBDemodReport"], "SWGADSBDemodReport", "SWGADSBDemodReport"); + ::SWGSDRangel::setValue(&ais_demod_report, pJson["AISDemodReport"], "SWGAISDemodReport", "SWGAISDemodReport"); + + ::SWGSDRangel::setValue(&ais_mod_report, pJson["AISModReport"], "SWGAISModReport", "SWGAISModReport"); + ::SWGSDRangel::setValue(&am_demod_report, pJson["AMDemodReport"], "SWGAMDemodReport", "SWGAMDemodReport"); ::SWGSDRangel::setValue(&am_mod_report, pJson["AMModReport"], "SWGAMModReport", "SWGAMModReport"); @@ -346,6 +364,12 @@ SWGChannelReport::asJsonObject() { if((adsb_demod_report != nullptr) && (adsb_demod_report->isSet())){ toJsonValue(QString("ADSBDemodReport"), adsb_demod_report, obj, QString("SWGADSBDemodReport")); } + if((ais_demod_report != nullptr) && (ais_demod_report->isSet())){ + toJsonValue(QString("AISDemodReport"), ais_demod_report, obj, QString("SWGAISDemodReport")); + } + if((ais_mod_report != nullptr) && (ais_mod_report->isSet())){ + toJsonValue(QString("AISModReport"), ais_mod_report, obj, QString("SWGAISModReport")); + } if((am_demod_report != nullptr) && (am_demod_report->isSet())){ toJsonValue(QString("AMDemodReport"), am_demod_report, obj, QString("SWGAMDemodReport")); } @@ -461,6 +485,26 @@ SWGChannelReport::setAdsbDemodReport(SWGADSBDemodReport* adsb_demod_report) { this->m_adsb_demod_report_isSet = true; } +SWGAISDemodReport* +SWGChannelReport::getAisDemodReport() { + return ais_demod_report; +} +void +SWGChannelReport::setAisDemodReport(SWGAISDemodReport* ais_demod_report) { + this->ais_demod_report = ais_demod_report; + this->m_ais_demod_report_isSet = true; +} + +SWGAISModReport* +SWGChannelReport::getAisModReport() { + return ais_mod_report; +} +void +SWGChannelReport::setAisModReport(SWGAISModReport* ais_mod_report) { + this->ais_mod_report = ais_mod_report; + this->m_ais_mod_report_isSet = true; +} + SWGAMDemodReport* SWGChannelReport::getAmDemodReport() { return am_demod_report; @@ -745,6 +789,12 @@ SWGChannelReport::isSet(){ if(adsb_demod_report && adsb_demod_report->isSet()){ isObjectUpdated = true; break; } + if(ais_demod_report && ais_demod_report->isSet()){ + isObjectUpdated = true; break; + } + if(ais_mod_report && ais_mod_report->isSet()){ + isObjectUpdated = true; break; + } if(am_demod_report && am_demod_report->isSet()){ isObjectUpdated = true; break; } diff --git a/swagger/sdrangel/code/qt5/client/SWGChannelReport.h b/swagger/sdrangel/code/qt5/client/SWGChannelReport.h index fec5907e6..2ae0d6700 100644 --- a/swagger/sdrangel/code/qt5/client/SWGChannelReport.h +++ b/swagger/sdrangel/code/qt5/client/SWGChannelReport.h @@ -23,6 +23,8 @@ #include "SWGADSBDemodReport.h" +#include "SWGAISDemodReport.h" +#include "SWGAISModReport.h" #include "SWGAMDemodReport.h" #include "SWGAMModReport.h" #include "SWGATVModReport.h" @@ -79,6 +81,12 @@ public: SWGADSBDemodReport* getAdsbDemodReport(); void setAdsbDemodReport(SWGADSBDemodReport* adsb_demod_report); + SWGAISDemodReport* getAisDemodReport(); + void setAisDemodReport(SWGAISDemodReport* ais_demod_report); + + SWGAISModReport* getAisModReport(); + void setAisModReport(SWGAISModReport* ais_mod_report); + SWGAMDemodReport* getAmDemodReport(); void setAmDemodReport(SWGAMDemodReport* am_demod_report); @@ -173,6 +181,12 @@ private: SWGADSBDemodReport* adsb_demod_report; bool m_adsb_demod_report_isSet; + SWGAISDemodReport* ais_demod_report; + bool m_ais_demod_report_isSet; + + SWGAISModReport* ais_mod_report; + bool m_ais_mod_report_isSet; + SWGAMDemodReport* am_demod_report; bool m_am_demod_report_isSet; diff --git a/swagger/sdrangel/code/qt5/client/SWGChannelSettings.cpp b/swagger/sdrangel/code/qt5/client/SWGChannelSettings.cpp index 188d7a738..f368bd81c 100644 --- a/swagger/sdrangel/code/qt5/client/SWGChannelSettings.cpp +++ b/swagger/sdrangel/code/qt5/client/SWGChannelSettings.cpp @@ -38,6 +38,10 @@ SWGChannelSettings::SWGChannelSettings() { m_originator_channel_index_isSet = false; adsb_demod_settings = nullptr; m_adsb_demod_settings_isSet = false; + ais_demod_settings = nullptr; + m_ais_demod_settings_isSet = false; + ais_mod_settings = nullptr; + m_ais_mod_settings_isSet = false; am_demod_settings = nullptr; m_am_demod_settings_isSet = false; am_mod_settings = nullptr; @@ -132,6 +136,10 @@ SWGChannelSettings::init() { m_originator_channel_index_isSet = false; adsb_demod_settings = new SWGADSBDemodSettings(); m_adsb_demod_settings_isSet = false; + ais_demod_settings = new SWGAISDemodSettings(); + m_ais_demod_settings_isSet = false; + ais_mod_settings = new SWGAISModSettings(); + m_ais_mod_settings_isSet = false; am_demod_settings = new SWGAMDemodSettings(); m_am_demod_settings_isSet = false; am_mod_settings = new SWGAMModSettings(); @@ -221,6 +229,12 @@ SWGChannelSettings::cleanup() { if(adsb_demod_settings != nullptr) { delete adsb_demod_settings; } + if(ais_demod_settings != nullptr) { + delete ais_demod_settings; + } + if(ais_mod_settings != nullptr) { + delete ais_mod_settings; + } if(am_demod_settings != nullptr) { delete am_demod_settings; } @@ -358,6 +372,10 @@ SWGChannelSettings::fromJsonObject(QJsonObject &pJson) { ::SWGSDRangel::setValue(&adsb_demod_settings, pJson["ADSBDemodSettings"], "SWGADSBDemodSettings", "SWGADSBDemodSettings"); + ::SWGSDRangel::setValue(&ais_demod_settings, pJson["AISDemodSettings"], "SWGAISDemodSettings", "SWGAISDemodSettings"); + + ::SWGSDRangel::setValue(&ais_mod_settings, pJson["AISModSettings"], "SWGAISModSettings", "SWGAISModSettings"); + ::SWGSDRangel::setValue(&am_demod_settings, pJson["AMDemodSettings"], "SWGAMDemodSettings", "SWGAMDemodSettings"); ::SWGSDRangel::setValue(&am_mod_settings, pJson["AMModSettings"], "SWGAMModSettings", "SWGAMModSettings"); @@ -465,6 +483,12 @@ SWGChannelSettings::asJsonObject() { if((adsb_demod_settings != nullptr) && (adsb_demod_settings->isSet())){ toJsonValue(QString("ADSBDemodSettings"), adsb_demod_settings, obj, QString("SWGADSBDemodSettings")); } + if((ais_demod_settings != nullptr) && (ais_demod_settings->isSet())){ + toJsonValue(QString("AISDemodSettings"), ais_demod_settings, obj, QString("SWGAISDemodSettings")); + } + if((ais_mod_settings != nullptr) && (ais_mod_settings->isSet())){ + toJsonValue(QString("AISModSettings"), ais_mod_settings, obj, QString("SWGAISModSettings")); + } if((am_demod_settings != nullptr) && (am_demod_settings->isSet())){ toJsonValue(QString("AMDemodSettings"), am_demod_settings, obj, QString("SWGAMDemodSettings")); } @@ -633,6 +657,26 @@ SWGChannelSettings::setAdsbDemodSettings(SWGADSBDemodSettings* adsb_demod_settin this->m_adsb_demod_settings_isSet = true; } +SWGAISDemodSettings* +SWGChannelSettings::getAisDemodSettings() { + return ais_demod_settings; +} +void +SWGChannelSettings::setAisDemodSettings(SWGAISDemodSettings* ais_demod_settings) { + this->ais_demod_settings = ais_demod_settings; + this->m_ais_demod_settings_isSet = true; +} + +SWGAISModSettings* +SWGChannelSettings::getAisModSettings() { + return ais_mod_settings; +} +void +SWGChannelSettings::setAisModSettings(SWGAISModSettings* ais_mod_settings) { + this->ais_mod_settings = ais_mod_settings; + this->m_ais_mod_settings_isSet = true; +} + SWGAMDemodSettings* SWGChannelSettings::getAmDemodSettings() { return am_demod_settings; @@ -1033,6 +1077,12 @@ SWGChannelSettings::isSet(){ if(adsb_demod_settings && adsb_demod_settings->isSet()){ isObjectUpdated = true; break; } + if(ais_demod_settings && ais_demod_settings->isSet()){ + isObjectUpdated = true; break; + } + if(ais_mod_settings && ais_mod_settings->isSet()){ + isObjectUpdated = true; break; + } if(am_demod_settings && am_demod_settings->isSet()){ isObjectUpdated = true; break; } diff --git a/swagger/sdrangel/code/qt5/client/SWGChannelSettings.h b/swagger/sdrangel/code/qt5/client/SWGChannelSettings.h index c86733487..f03b3c711 100644 --- a/swagger/sdrangel/code/qt5/client/SWGChannelSettings.h +++ b/swagger/sdrangel/code/qt5/client/SWGChannelSettings.h @@ -23,6 +23,8 @@ #include "SWGADSBDemodSettings.h" +#include "SWGAISDemodSettings.h" +#include "SWGAISModSettings.h" #include "SWGAMDemodSettings.h" #include "SWGAMModSettings.h" #include "SWGAPTDemodSettings.h" @@ -96,6 +98,12 @@ public: SWGADSBDemodSettings* getAdsbDemodSettings(); void setAdsbDemodSettings(SWGADSBDemodSettings* adsb_demod_settings); + SWGAISDemodSettings* getAisDemodSettings(); + void setAisDemodSettings(SWGAISDemodSettings* ais_demod_settings); + + SWGAISModSettings* getAisModSettings(); + void setAisModSettings(SWGAISModSettings* ais_mod_settings); + SWGAMDemodSettings* getAmDemodSettings(); void setAmDemodSettings(SWGAMDemodSettings* am_demod_settings); @@ -229,6 +237,12 @@ private: SWGADSBDemodSettings* adsb_demod_settings; bool m_adsb_demod_settings_isSet; + SWGAISDemodSettings* ais_demod_settings; + bool m_ais_demod_settings_isSet; + + SWGAISModSettings* ais_mod_settings; + bool m_ais_mod_settings_isSet; + SWGAMDemodSettings* am_demod_settings; bool m_am_demod_settings_isSet; diff --git a/swagger/sdrangel/code/qt5/client/SWGFeatureSettings.cpp b/swagger/sdrangel/code/qt5/client/SWGFeatureSettings.cpp index 68846af33..8934debaa 100644 --- a/swagger/sdrangel/code/qt5/client/SWGFeatureSettings.cpp +++ b/swagger/sdrangel/code/qt5/client/SWGFeatureSettings.cpp @@ -36,6 +36,8 @@ SWGFeatureSettings::SWGFeatureSettings() { m_originator_feature_index_isSet = false; afc_settings = nullptr; m_afc_settings_isSet = false; + ais_settings = nullptr; + m_ais_settings_isSet = false; aprs_settings = nullptr; m_aprs_settings_isSet = false; demod_analyzer_settings = nullptr; @@ -72,6 +74,8 @@ SWGFeatureSettings::init() { m_originator_feature_index_isSet = false; afc_settings = new SWGAFCSettings(); m_afc_settings_isSet = false; + ais_settings = new SWGAISSettings(); + m_ais_settings_isSet = false; aprs_settings = new SWGAPRSSettings(); m_aprs_settings_isSet = false; demod_analyzer_settings = new SWGDemodAnalyzerSettings(); @@ -104,6 +108,9 @@ SWGFeatureSettings::cleanup() { if(afc_settings != nullptr) { delete afc_settings; } + if(ais_settings != nullptr) { + delete ais_settings; + } if(aprs_settings != nullptr) { delete aprs_settings; } @@ -155,6 +162,8 @@ SWGFeatureSettings::fromJsonObject(QJsonObject &pJson) { ::SWGSDRangel::setValue(&afc_settings, pJson["AFCSettings"], "SWGAFCSettings", "SWGAFCSettings"); + ::SWGSDRangel::setValue(&ais_settings, pJson["AISSettings"], "SWGAISSettings", "SWGAISSettings"); + ::SWGSDRangel::setValue(&aprs_settings, pJson["APRSSettings"], "SWGAPRSSettings", "SWGAPRSSettings"); ::SWGSDRangel::setValue(&demod_analyzer_settings, pJson["DemodAnalyzerSettings"], "SWGDemodAnalyzerSettings", "SWGDemodAnalyzerSettings"); @@ -203,6 +212,9 @@ SWGFeatureSettings::asJsonObject() { if((afc_settings != nullptr) && (afc_settings->isSet())){ toJsonValue(QString("AFCSettings"), afc_settings, obj, QString("SWGAFCSettings")); } + if((ais_settings != nullptr) && (ais_settings->isSet())){ + toJsonValue(QString("AISSettings"), ais_settings, obj, QString("SWGAISSettings")); + } if((aprs_settings != nullptr) && (aprs_settings->isSet())){ toJsonValue(QString("APRSSettings"), aprs_settings, obj, QString("SWGAPRSSettings")); } @@ -277,6 +289,16 @@ SWGFeatureSettings::setAfcSettings(SWGAFCSettings* afc_settings) { this->m_afc_settings_isSet = true; } +SWGAISSettings* +SWGFeatureSettings::getAisSettings() { + return ais_settings; +} +void +SWGFeatureSettings::setAisSettings(SWGAISSettings* ais_settings) { + this->ais_settings = ais_settings; + this->m_ais_settings_isSet = true; +} + SWGAPRSSettings* SWGFeatureSettings::getAprsSettings() { return aprs_settings; @@ -394,6 +416,9 @@ SWGFeatureSettings::isSet(){ if(afc_settings && afc_settings->isSet()){ isObjectUpdated = true; break; } + if(ais_settings && ais_settings->isSet()){ + isObjectUpdated = true; break; + } if(aprs_settings && aprs_settings->isSet()){ isObjectUpdated = true; break; } diff --git a/swagger/sdrangel/code/qt5/client/SWGFeatureSettings.h b/swagger/sdrangel/code/qt5/client/SWGFeatureSettings.h index c9fb52814..e6f8b95f5 100644 --- a/swagger/sdrangel/code/qt5/client/SWGFeatureSettings.h +++ b/swagger/sdrangel/code/qt5/client/SWGFeatureSettings.h @@ -23,6 +23,7 @@ #include "SWGAFCSettings.h" +#include "SWGAISSettings.h" #include "SWGAPRSSettings.h" #include "SWGDemodAnalyzerSettings.h" #include "SWGGS232ControllerSettings.h" @@ -65,6 +66,9 @@ public: SWGAFCSettings* getAfcSettings(); void setAfcSettings(SWGAFCSettings* afc_settings); + SWGAISSettings* getAisSettings(); + void setAisSettings(SWGAISSettings* ais_settings); + SWGAPRSSettings* getAprsSettings(); void setAprsSettings(SWGAPRSSettings* aprs_settings); @@ -111,6 +115,9 @@ private: SWGAFCSettings* afc_settings; bool m_afc_settings_isSet; + SWGAISSettings* ais_settings; + bool m_ais_settings_isSet; + SWGAPRSSettings* aprs_settings; bool m_aprs_settings_isSet; diff --git a/swagger/sdrangel/code/qt5/client/SWGModelFactory.h b/swagger/sdrangel/code/qt5/client/SWGModelFactory.h index 73489fc32..ce79d5135 100644 --- a/swagger/sdrangel/code/qt5/client/SWGModelFactory.h +++ b/swagger/sdrangel/code/qt5/client/SWGModelFactory.h @@ -19,6 +19,13 @@ #include "SWGAFCActions.h" #include "SWGAFCReport.h" #include "SWGAFCSettings.h" +#include "SWGAISDemodReport.h" +#include "SWGAISDemodSettings.h" +#include "SWGAISModActions.h" +#include "SWGAISModActions_tx.h" +#include "SWGAISModReport.h" +#include "SWGAISModSettings.h" +#include "SWGAISSettings.h" #include "SWGAMBEDevice.h" #include "SWGAMBEDevices.h" #include "SWGAMDemodReport.h" @@ -280,6 +287,27 @@ namespace SWGSDRangel { if(QString("SWGAFCSettings").compare(type) == 0) { return new SWGAFCSettings(); } + if(QString("SWGAISDemodReport").compare(type) == 0) { + return new SWGAISDemodReport(); + } + if(QString("SWGAISDemodSettings").compare(type) == 0) { + return new SWGAISDemodSettings(); + } + if(QString("SWGAISModActions").compare(type) == 0) { + return new SWGAISModActions(); + } + if(QString("SWGAISModActions_tx").compare(type) == 0) { + return new SWGAISModActions_tx(); + } + if(QString("SWGAISModReport").compare(type) == 0) { + return new SWGAISModReport(); + } + if(QString("SWGAISModSettings").compare(type) == 0) { + return new SWGAISModSettings(); + } + if(QString("SWGAISSettings").compare(type) == 0) { + return new SWGAISSettings(); + } if(QString("SWGAMBEDevice").compare(type) == 0) { return new SWGAMBEDevice(); }